| @ -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]) | def frontend_javascript_importmap_tags(only_use = %w[application]) | ||||
| only_use = Array(only_use) | only_use = Array(only_use) | ||||
| importmap_json = JSON.parse(Rails.application.importmap.to_json(resolver: self))["imports"].select { |k, v| only_use.include?(k) } | 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 | 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 | 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 | 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 | 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 | ||||
| end | end | ||||
| @ -1,6 +1,6 @@ | |||||
| <div data-controller="language-menu"> | <div data-controller="language-menu"> | ||||
| <header> | <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" | <button type="button" | ||||
| class="language-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 | |||||