| @ -0,0 +1 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22"><path d="M17.62,21.75c-2.03,0-3.25-1.65-3.25-3.25,0-2.4,2.75-4.08,2.86-4.15l.39-.23.39.23c.12.07,2.86,1.75,2.86,4.15,0,1.6-1.22,3.25-3.25,3.25ZM17.62,15.91c-.7.52-1.75,1.54-1.75,2.6,0,.86.65,1.75,1.75,1.75s1.75-.89,1.75-1.75c0-1.06-1.05-2.07-1.75-2.6ZM20.12,11.75v-1.5c-4.28,0-8.14,2.53-9.86,6.45l1.38.6c1.47-3.37,4.8-5.55,8.48-5.55ZM20.12,7.75v-1.5c-6.48,0-12.29,4.33-14.14,10.54l1.44.43c1.66-5.57,6.88-9.46,12.7-9.46ZM20.12,3.75v-1.5C11.31,2.25,3.79,8.25,1.84,16.83l1.46.33C5.09,9.27,12.01,3.75,20.12,3.75ZM2.86,8.6c-.89-.23-1.81-.35-2.73-.35v1.5c.8,0,1.59.1,2.35.3l.38-1.45ZM5.68,5.33c-1.77-.72-3.64-1.08-5.56-1.08v1.5c1.73,0,3.41.33,4.99.97l.57-1.39ZM9.24,2.61C6.47,1.07,3.32.25.12.25v1.5c2.94,0,5.83.75,8.38,2.17l.73-1.31Z"/></svg> | |||
| @ -0,0 +1,90 @@ | |||
| class GameController < ApplicationController | |||
| include QuizHelperMethods | |||
| skip_before_action :require_player!, only: [ :index, :start ] | |||
| before_action :set_node, only: [ :stage, :answer, :stage_result ] | |||
| helper_method :root_node, | |||
| :stage_index, | |||
| :n_stages | |||
| def index | |||
| @node = root_node | |||
| not_found and return unless @node | |||
| render action: @node.template | |||
| end | |||
| # POST | |||
| def start | |||
| player = Player.create(locale: I18n.locale.to_s) | |||
| session[:player_id] = player.id | |||
| redirect_to action: "facts" | |||
| end | |||
| def facts | |||
| @node = root_node&.children&.facts&.first | |||
| end | |||
| def intro | |||
| @node = root_node&.children&.intro&.first | |||
| end | |||
| def stage | |||
| end | |||
| # POST | |||
| def answer | |||
| @answer = @node.children.find_by(id: params[:value]) | |||
| current_player.record_answer(stage_index, @answer.id) | |||
| redirect_to action: :stage_result, id: params[:id] | |||
| end | |||
| def stage_result | |||
| end | |||
| private | |||
| def root_node | |||
| @root_node ||= Node.roots.viewable.first | |||
| end | |||
| def stages | |||
| @stages ||= root_node.children.stage | |||
| end | |||
| def set_node | |||
| @node = stages[params[:id].to_i - 1] | |||
| not_found and return unless @node | |||
| end | |||
| def stage_index | |||
| @stage_index ||= stages.index(@node) + 1 | |||
| end | |||
| def n_stages | |||
| @n_stages ||= stages.size | |||
| end | |||
| def url_from_param | |||
| return "" unless params[:url] | |||
| # return File.join('', params[:url]) if I18n.default_locale == I18n.locale | |||
| File.join("", I18n.locale.to_s, params[:url]) | |||
| end | |||
| end | |||
| @ -1,41 +0,0 @@ | |||
| class SiteController < ApplicationController | |||
| before_action :set_locale | |||
| helper_method :root_node | |||
| def index | |||
| @node = root_node | |||
| not_found and return unless @node | |||
| render(template: "site/#{@node.template}") | |||
| end | |||
| def facts | |||
| @node = root_node&.children&.facts&.first | |||
| end | |||
| def intro | |||
| @node = root_node&.children&.intro&.first | |||
| end | |||
| def stage | |||
| @node = root_node&.children&.stage&.first | |||
| end | |||
| private | |||
| def root_node | |||
| @root_node ||= Node.roots.viewable.first | |||
| end | |||
| def url_from_param | |||
| return "" unless params[:url] | |||
| # return File.join('', params[:url]) if I18n.default_locale == I18n.locale | |||
| File.join("", I18n.locale.to_s, params[:url]) | |||
| end | |||
| end | |||
| @ -1,4 +1,4 @@ | |||
| module SiteHelper | |||
| module GameHelper | |||
| def frontend_javascript_importmap_tags(only_use = %w[application]) | |||
| only_use = Array(only_use) | |||
| importmap_json = JSON.parse(Rails.application.importmap.to_json(resolver: self))["imports"].select { |k, v| only_use.include?(k) } | |||
| @ -0,0 +1,26 @@ | |||
| import { Controller } from "@hotwired/stimulus" | |||
| export default class extends Controller { | |||
| static targets = ["dialog"] | |||
| confirm(event) { | |||
| event.preventDefault() | |||
| this.pendingForm = event.target | |||
| this.dialogTarget.showModal() | |||
| } | |||
| ok() { | |||
| this.dialogTarget.close() | |||
| this.pendingForm?.submit() | |||
| } | |||
| cancel() { | |||
| this.dialogTarget.close() | |||
| this.pendingForm = null | |||
| } | |||
| backdropClick(event) { | |||
| console.info(event.target === this.dialogTarget) | |||
| if (event.target === this.dialogTarget) this.cancel() | |||
| } | |||
| } | |||
| @ -1,45 +1,29 @@ | |||
| class Player < ApplicationRecord | |||
| attribute :current_stage, :integer, default: 1 | |||
| attribute :bonus_points, :integer, default: 0 | |||
| attribute :progress, :json, default: {} | |||
| attribute :is_done, :boolean, default: false | |||
| # progress shape: | |||
| # { "1" => { "answer_id" => 42, "result_id" => 99 }, "2" => { ... } } | |||
| scope :playing, -> { where(is_done: false) } | |||
| scope :done, -> { where(is_done: true) } | |||
| def is_playing? | |||
| !self.is_done? | |||
| def record_answer(stage, answer_id) | |||
| stage_data(stage)["answer_id"] = answer_id | |||
| save | |||
| end | |||
| def reset_when_game_over | |||
| self.progress = {} | |||
| self.current_stage = 1 | |||
| self.save | |||
| def record_result(stage, result_id) | |||
| stage_data(stage)["result_id"] = result_id | |||
| save | |||
| end | |||
| # stage_index: 1, 2, 3... | |||
| # outcome: 'chance' or 'choice' | |||
| def record_flip(stage_index, outcome) | |||
| self.progress[stage_index.to_s] ||= {} | |||
| self.progress[stage_index.to_s]["flip"] = outcome | |||
| save | |||
| def answer_id_for(stage) | |||
| progress[stage.to_s]&.dig("answer_id") | |||
| end | |||
| def result_id_for(stage) | |||
| progress[stage.to_s]&.dig("result_id") | |||
| end | |||
| # stage_index: 1, 2, 3... | |||
| # result: 'good', 'bad', 'game_over' | |||
| def record_pick_result(stage_index, result) | |||
| self.progress[stage_index.to_s] ||= {} | |||
| self.progress[stage_index.to_s]["result"] = result | |||
| save | |||
| end | |||
| private | |||
| def record_bonus_pick_result(stage_index, result) | |||
| self.progress[stage_index.to_s] ||= {} | |||
| self.progress[stage_index.to_s]["bonus_result"] = result | |||
| save | |||
| def stage_data(stage) | |||
| progress[stage.to_s] ||= {} | |||
| end | |||
| end | |||
| @ -1,6 +1,6 @@ | |||
| <div data-controller="language-menu"> | |||
| <header> | |||
| <%= link_to svg("ikea-foundation-203x22"), url_for(controller: "site", action: "index") %> | |||
| <%= link_to svg("ikea-foundation-203x22"), url_for(controller: "game", action: "index") %> | |||
| <button type="button" | |||
| class="language-button" | |||
| @ -0,0 +1,53 @@ | |||
| <%- content_for :title, node_title(@node) %> | |||
| <% image_attachment = @node.attachments.select { |a| a.asset&.file&.image? }[0] %> | |||
| <div class="stage-header-container"> | |||
| <ol class="stage-progress" role="progressbar"> | |||
| <% n_stages.times do |i| %> | |||
| <%= tag.li class: ("done" if stage_index >= (i + 1)) %> | |||
| <% end %> | |||
| </ol> | |||
| <div class="stage-header"> | |||
| <div class="stage-icon"><%= svg t("icons.stages")[stage_index] %></div> | |||
| <div> | |||
| <%= tag.div t("game.stage_i_of_n", i: stage_index, n: n_stages), class: "stage-progress" %> | |||
| <%= tag.h1 @node.title %> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="hero-container"> | |||
| <%= render_responsive_picture(image_attachment.asset, alt: "Hero", fetchpriority: "high") %> | |||
| <article> | |||
| <%= image_attachment.body.html_safe %> | |||
| </article> | |||
| <%= svg "ico-wave" %> | |||
| </div> | |||
| <article class="answers-container" data-controller="chance"> | |||
| <% @node.children.each do |node| %> | |||
| <%= button_to node.title, url_for(action: "answer", value: node.id), | |||
| class: ("chance" if node.chance?), | |||
| form: ((stage_index == 1 and node.chance?) ? { data: { action: "submit->chance#confirm" } } : {}) %> | |||
| <% end %> | |||
| <% if stage_index == 1 %> | |||
| <dialog data-chance-target="dialog" data-action="click->chance#backdropClick"> | |||
| <div class="header-label"><%= t("game.take_a_chance") %></div> | |||
| <%= tag.h2 t("game.its_risky").html_safe %> | |||
| <div class="dialog-actions"> | |||
| <%= svg "ico-wave" %> | |||
| <div> | |||
| <button type="button" data-action="click->chance#cancel"><%= t("game.cancel") %></button> | |||
| <button type="button" data-action="click->chance#ok"><%= t("game.ok") %></button> | |||
| </div> | |||
| </div> | |||
| </dialog> | |||
| <% end %> | |||
| </article> | |||
| @ -0,0 +1,11 @@ | |||
| class CreatePlayers < ActiveRecord::Migration[8.1] | |||
| def change | |||
| create_table :players do |t| | |||
| t.string :locale, null: false | |||
| t.integer :score, default: 0, null: false | |||
| t.jsonb :progress, default: {}, null: false | |||
| t.timestamps | |||
| end | |||
| end | |||
| end | |||