Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ still runs offline with a personal local board.
crystals), volatile (chain explosion) and splitter (splits on approach).
- Crystal shop: trails, ship skins and titles. Cosmetic-only; no ranked
advantage.
- Settings: colour-blind flare cues, reduced motion (including no full-screen flash overlays), low-FX mode, music and SFX.
- Settings: colour-blind flare cues, reduced motion (no screen shake, full-screen flash overlays, or impact hit-stop pauses), low-FX mode, music and SFX.
- Share result cards (PNG) and challenge links, plus PWA install prompt and
manifest shortcuts.
- Local analytics counters plus server event logging for the run/board/install
Expand Down
15 changes: 11 additions & 4 deletions web/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,13 @@ function comboMilestoneText(combo) {
function shouldRenderScreenFlash(settings = {}) {
return settings.reducedMotion !== true;
}
function shouldApplyHitStop(settings = {}) {
return settings.reducedMotion !== true;
}
function campaignPauseHint(sectorId) {
const sector = CAMPAIGN_SECTORS.find(item => item.id === sectorId);
return sector ? `${sector.name} — ${sector.objective}` : "";
return sector ? `${sector.name} - ${sector.objective}` : "";
}

function segDist(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const len2 = dx * dx + dy * dy;
Expand Down Expand Up @@ -2163,7 +2165,12 @@ class Game {
this.time += dt; this.ads.update(dt);
this.flash = Math.max(0, this.flash - dt);
if (this.paused) return;
if (this.hitStop > 0) { this.hitStop = Math.max(0, this.hitStop - dt); return; }
if (this.hitStop > 0 && shouldApplyHitStop(this.save.settings)) {
this.hitStop = Math.max(0, this.hitStop - dt);
return;
}
// Reduced Motion skips the brief simulation freeze used for impact emphasis.
this.hitStop = 0;
this.shake = Math.max(0, this.shake - dt);
this.parts = this.parts.filter(p => { p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= .96; p.vy *= .96; return (p.life -= dt) > 0; });
this.floats = this.floats.filter(f => { f.y -= 50 * dt; return (f.life -= dt) > 0; });
Expand Down Expand Up @@ -3475,5 +3482,5 @@ if (typeof module !== "undefined" && module.exports)
module.exports = { Game, MockAdProvider, AdMobProvider, createAdProvider, Save, Sfx, Social, Leaderboard,
CAMPAIGN_SECTORS, STORY_ENTRIES, EXPLORATION_OBJECTIVES, freshCampaign, sanitizeCampaign, DemoIapProvider, DisabledIapProvider, CdvIapProvider, createIapProvider, GEM_PACKS, CONTINUE_BASE_COST,
SHIPS, CONTRACTS, MISSION_POOL, BIOMES, METEOR_TYPES, SHOP_ITEMS,
dateKey, weekKey, mulberry32, difficultyProfile, flareWarningText, bossWarningText, comboMilestoneText, shouldRenderScreenFlash, campaignPauseHint, W, H };
dateKey, weekKey, mulberry32, difficultyProfile, flareWarningText, bossWarningText, comboMilestoneText, shouldRenderScreenFlash, shouldApplyHitStop, campaignPauseHint, W, H };
else window.addEventListener("load", boot);
2 changes: 1 addition & 1 deletion web/sw.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Service worker: caches the game so it works offline once installed.
// Bump CACHE whenever the game files change. Navigation is network-first so a
// deployed update is picked up as soon as the player is online.
const CACHE = "orbit-runner-v17";
const CACHE = "orbit-runner-v18";
const FILES = ["./", "./index.html", "./config.js", "./game.js", "./manifest.json", "./icon-192.png", "./icon-512.png"];

self.addEventListener("install", e => {
Expand Down
10 changes: 8 additions & 2 deletions web/test/game.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const test = require("node:test");
const assert = require("node:assert/strict");
const { Game, MockAdProvider, AdMobProvider, Save, Social, Leaderboard, MISSION_POOL, CONTRACTS,
BIOMES, SHOP_ITEMS, GEM_PACKS, DemoIapProvider, dateKey, weekKey, CAMPAIGN_SECTORS, STORY_ENTRIES, EXPLORATION_OBJECTIVES, freshCampaign,
sanitizeCampaign, difficultyProfile, flareWarningText, comboMilestoneText, shouldRenderScreenFlash, campaignPauseHint } = require("../game.js");
sanitizeCampaign, difficultyProfile, flareWarningText, comboMilestoneText, shouldRenderScreenFlash, shouldApplyHitStop, campaignPauseHint } = require("../game.js");

function localStore() {
const store = {};
Expand Down Expand Up @@ -675,7 +675,7 @@ test("replaying an already-charted sector does not trigger the campaign ending a

test("campaign pause hint explains the selected sector objective and ignores unknown sectors", () => {
assert.equal(campaignPauseHint("sol-frontier"),
"SOL FRONTIER — Complete a run here to chart the frontier.");
"SOL FRONTIER - Complete a run here to chart the frontier.");
assert.equal(campaignPauseHint("not-a-sector"), "");
assert.equal(campaignPauseHint(null), "");
});
Expand All @@ -687,6 +687,12 @@ test("reduced motion disables full-screen flash overlays without changing defaul
assert.equal(shouldRenderScreenFlash(), true);
});

test("reduced motion skips hit-stop pauses while normal settings retain them", () => {
assert.equal(shouldApplyHitStop({ reducedMotion: true }), false);
assert.equal(shouldApplyHitStop({ reducedMotion: false }), true);
assert.equal(shouldApplyHitStop(), true);
});

test("combo milestone copy names the streak tiers clearly and rejects invalid values", () => {
assert.equal(comboMilestoneText(2), "x2 WARM STREAK");
assert.equal(comboMilestoneText(4), "x4 HOT STREAK");
Expand Down
Loading