| @ -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> | |||||