| @ -0,0 +1,122 @@ | |||
| # Development-only shortcut so any screen can be opened directly instead of | |||
| # playing the game from the start. Creates a player on demand and back-fills | |||
| # whatever progress the requested screen expects to find. | |||
| # | |||
| # ?dev=good|bad|chance|chance_bad picks which answer the back-fill records and | |||
| # always starts from a fresh player, so the whole run leading up to the screen | |||
| # is answered that way. Without it an existing session player is kept and only | |||
| # the gaps are filled, so a jump can be played on from. ?dev_last_save=1 also | |||
| # answers the last-save question. | |||
| module DevJump | |||
| extend ActiveSupport::Concern | |||
| SCREENS = %w[index facts intro stage stage_result last_save done results].freeze | |||
| included do | |||
| prepend_before_action :dev_jump!, only: SCREENS.map(&:to_sym), if: :dev_jump? | |||
| end | |||
| private | |||
| def dev_jump? | |||
| Rails.env.development? | |||
| end | |||
| def dev_jump! | |||
| set_locale | |||
| # The start screen is the one place a leftover player is in the way: it | |||
| # would show the debug panel a first-time visitor never sees. | |||
| return session.delete(:player_id) if action_name == "index" | |||
| session.delete(:player_id) if params[:dev] | |||
| dev_player | |||
| dev_seed_progress | |||
| end | |||
| def dev_player | |||
| Current.player = Player.find_by(id: session[:player_id]) | |||
| return Current.player if Current.player | |||
| Current.player = Player.create(locale: I18n.locale.to_s, | |||
| country: Player.valid_country_code(params[:country]), | |||
| device: Player.device_from_user_agent(request.user_agent)) | |||
| session[:player_id] = Current.player.id | |||
| Current.player | |||
| end | |||
| def dev_seed_progress | |||
| case action_name | |||
| when "stage" then dev_fill_stages(params[:id].to_i - 1) | |||
| when "stage_result" then dev_fill_stages(params[:id].to_i) | |||
| when "last_save" then dev_fill_stages(n_stages) | |||
| when "done", "results" | |||
| dev_fill_stages(n_stages) | |||
| dev_fill_last_save if params[:dev_last_save] | |||
| end | |||
| end | |||
| # Answers every stage up to `upto` that the player hasn't answered yet, scoring | |||
| # each one the way the real answer action would. | |||
| def dev_fill_stages(upto) | |||
| return if upto < 1 | |||
| stages.first(upto).each_with_index do |stage_node, i| | |||
| index = i + 1 | |||
| next if current_player.answer_id_for(index) | |||
| answer = dev_answer_for(stage_node) | |||
| next unless answer | |||
| current_player.record_answer(index, answer.id) | |||
| apply_score(score_entry_for(index, answer)) | |||
| end | |||
| end | |||
| def dev_fill_last_save | |||
| return if current_player.last_save_answer_id | |||
| node = last_save_node | |||
| return unless node | |||
| answer = dev_pick_from(node.children.ordered) | |||
| return unless answer | |||
| current_player.record_last_save_answer(answer.id) | |||
| apply_score(last_save_score_entry_for(answer)) | |||
| end | |||
| # A chance answer is only ever recorded through one of its outcomes, so it | |||
| # resolves one level deeper -- ?dev=chance lands on the good outcome, | |||
| # ?dev=chance_bad on the bad one. | |||
| def dev_answer_for(stage_node) | |||
| answer = dev_pick_from(stage_node.children.ordered) | |||
| return answer unless answer&.chance? | |||
| outcomes = answer.children.ordered.to_a | |||
| wanted = params[:dev] == "chance_bad" ? :bad_answer? : :good_answer? | |||
| outcomes.find(&wanted) || outcomes.first | |||
| end | |||
| # Not every stage offers every kind -- stage 5 has no bad answer, stage 1's | |||
| # chance cannot go wrong -- so an unavailable pick settles for the first | |||
| # answer rather than leaving the stage unanswered. | |||
| def dev_pick_from(nodes) | |||
| nodes = nodes.to_a | |||
| case params[:dev] | |||
| when "bad" then nodes.find(&:bad_answer?) | |||
| when "chance", "chance_bad" then nodes.find(&:chance?) | |||
| else nodes.find(&:good_answer?) | |||
| end || nodes.first | |||
| end | |||
| end | |||
| @ -0,0 +1,11 @@ | |||
| # Only ever called from the development-only jump panel. | |||
| module DevJumpHelper | |||
| def dev_jump_stage_count | |||
| Node.roots.viewable.first&.children&.ordered&.stage&.size.to_i | |||
| end | |||
| def dev_jump_path(path, query = {}) | |||
| [ "/#{I18n.locale}#{path}", query.to_query.presence ].compact.join("?") | |||
| end | |||
| end | |||
| @ -0,0 +1,124 @@ | |||
| <% stage_count = dev_jump_stage_count %> | |||
| <div id="dev-jump"> | |||
| <details id="dev-jump-panel"> | |||
| <summary>⚡</summary> | |||
| <div class="dev-jump-rows"> | |||
| <div> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("") %>">start</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/facts") %>">facts</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/intro") %>">intro</a> | |||
| </div> | |||
| <% (1..stage_count).each do |i| %> | |||
| <div> | |||
| <a data-turbo-frame="_top" class="key" href="<%= dev_jump_path("/stage/#{i}") %>">stage <%= i %></a> | |||
| <% { "good" => "✓", "bad" => "✗", "chance" => "50✓", "chance_bad" => "50✗" }.each do |pick, label| %> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/stage/#{i}/result", dev: pick) %>" title="result, <%= pick %>"><%= label %></a> | |||
| <% end %> | |||
| </div> | |||
| <% end %> | |||
| <div> | |||
| <a data-turbo-frame="_top" class="key" href="<%= dev_jump_path("/last_save") %>">last save</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/done") %>">done</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/done", dev_last_save: 1) %>" title="done via last save">done ls</a> | |||
| </div> | |||
| <div> | |||
| <a data-turbo-frame="_top" class="key" href="<%= dev_jump_path("/results") %>">results</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/results", dev: "good") %>">✓</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/results", dev: "bad") %>">✗</a> | |||
| <a data-turbo-frame="_top" href="<%= dev_jump_path("/results", dev: "chance_bad", dev_last_save: 1) %>" title="results, bad chance + last save">50✗ ls</a> | |||
| </div> | |||
| </div> | |||
| </details> | |||
| </div> | |||
| <style> | |||
| #dev-jump { | |||
| position: fixed; | |||
| inset: auto auto 0.5rem 0.5rem; | |||
| z-index: 2147483647; | |||
| margin: 0; | |||
| padding: 0; | |||
| border: none; | |||
| background: none; | |||
| overflow: visible; | |||
| font: 400 11px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; | |||
| } | |||
| #dev-jump-panel { | |||
| background: rgba(20, 20, 20, 0.92); | |||
| color: #fff; | |||
| border-radius: 0.75rem; | |||
| padding: 0.25rem; | |||
| max-width: min(20rem, calc(100vw - 1rem)); | |||
| } | |||
| #dev-jump-panel > summary { | |||
| list-style: none; | |||
| cursor: pointer; | |||
| width: 1.5rem; | |||
| height: 1.5rem; | |||
| display: grid; | |||
| place-items: center; | |||
| user-select: none; | |||
| } | |||
| #dev-jump-panel > summary::-webkit-details-marker { | |||
| display: none; | |||
| } | |||
| #dev-jump .dev-jump-rows { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 0.25rem; | |||
| padding: 0.25rem; | |||
| } | |||
| #dev-jump .dev-jump-rows > div { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 0.25rem; | |||
| align-items: center; | |||
| } | |||
| #dev-jump a { | |||
| color: #fff; | |||
| text-decoration: none; | |||
| background: rgba(255, 255, 255, 0.14); | |||
| border-radius: 100vw; | |||
| padding: 0.25rem 0.5rem; | |||
| white-space: nowrap; | |||
| } | |||
| #dev-jump a.key { | |||
| min-width: 4.5rem; | |||
| background: rgba(63, 184, 62, 0.55); | |||
| } | |||
| #dev-jump a:hover { | |||
| background: rgba(255, 255, 255, 0.35); | |||
| } | |||
| </style> | |||
| <script> | |||
| (() => { | |||
| const wrapper = document.getElementById("dev-jump") | |||
| const panel = document.getElementById("dev-jump-panel") | |||
| panel.open = localStorage.getItem("dev-jump-open") === "1" | |||
| panel.addEventListener("toggle", () => localStorage.setItem("dev-jump-open", panel.open ? "1" : "0")) | |||
| // A modal dialog paints in the top layer, above anything left in the page, | |||
| // so the panel rides along inside it for as long as it is open. | |||
| const showModal = HTMLDialogElement.prototype.showModal | |||
| HTMLDialogElement.prototype.showModal = function () { | |||
| showModal.call(this) | |||
| this.append(wrapper) | |||
| this.addEventListener("close", () => document.body.append(wrapper), { once: true }) | |||
| } | |||
| })() | |||
| </script> | |||