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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ runs keep their existing pause layout and rules.
- The web galaxy map uses the existing sector positions to draw connected routes, distinguish locked and charted systems, and show the selected sector's objective without duplicating campaign progression or save data.
- Planetary surveys show sector-specific environmental conditions, hazards, research focus, and field notes while reusing the existing 12 optional objectives and rewards.
- Campaign boss encounters now use sector-specific profiles, with distinct core movement, target-ring patterns, timers, and names. Daily/ranked runs keep the standard Corona Surge profile.
- The existing cosmetics shop has a live preview for ship skins, trails, and titles. Preview selection does not equip or purchase an item; the existing BUY/EQUIP action remains explicit.

### Realism pass

Expand Down
58 changes: 44 additions & 14 deletions web/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ const SHOP_ITEMS = [
{ id: "title_legend", type: "title", name: "Orbit Legend", cost: 900, value: "ORBIT LEGEND" },
];

function cosmeticPreviewItem(type, previewId, equipped = {}, items = SHOP_ITEMS) {
const candidates = items.filter(item => item.type === type);
return candidates.find(item => item.id === previewId) ||
candidates.find(item => item.id === equipped[type]) || candidates[0] || null;
}

const SKIN_LIVERIES = {
scout: { hull: "#e8eef8", wing: "#9fb3cc", trim: "#78d8ff", accent: "#ff6a6a" },
interceptor: { hull: "#eef6ff", wing: "#c0d4e8", trim: "#78d8ff", accent: "#ff6a6a" },
Expand Down Expand Up @@ -2569,26 +2575,50 @@ class Game {
this.shopTab === id ? "#ffd45a" : "#55557a", () => { this.shopTab = id; });
});
const items = SHOP_ITEMS.filter(i => i.type === this.shopTab);
const preview = cosmeticPreviewItem(this.shopTab, this.shopPreviewId, this.save.cosmetics.equipped);
panel(c, 60, 230, W - 120, 86, "rgba(16,25,44,0.92)", "rgba(120,200,255,0.24)", 14);
text(c, "LIVE PREVIEW", 78, 252, 11, "#7fa7c8", true, "left");
if (preview) {
text(c, preview.name, 78, 282, 18, "#e8eef8", true, "left");
if (preview.type === "skin") {
const livery = SKIN_LIVERIES[preview.value] || SKIN_LIVERIES.scout;
drawAircraft(c, W - 160, 270, -Math.PI / 2, livery, this.time, 0);
} else if (preview.type === "trail") {
c.globalAlpha = 0.28;
c.strokeStyle = preview.value; c.lineWidth = 14;
c.beginPath(); c.moveTo(W - 260, 270); c.lineTo(W - 130, 270); c.stroke();
c.globalAlpha = 1;
for (let i = 0; i < 5; i++) circle(c, W - 250 + i * 24, 270, 4 + (i % 2) * 2, preview.value);
} else {
text(c, preview.value, W - 170, 270, 18, "#b48cff", true);
}
const equipped = this.save.cosmetics.equipped[preview.type] === preview.id;
text(c, equipped ? "CURRENTLY EQUIPPED" :
this.save.cosmetics.owned.includes(preview.id) ? "OWNED — READY TO EQUIP" :
`COST ${preview.cost} CRYSTALS`,
W - 78, 297, 10, equipped ? "#78ff8c" : "#9aa6c0", true, "right");
}
items.forEach((item, i) => {
const y = 252 + i * 104;
const y = 330 + i * 100;
const owned = this.save.cosmetics.owned.includes(item.id);
const equipped = this.save.cosmetics.equipped[item.type] === item.id;
panel(c, 60, y, W - 120, 88,
equipped ? "rgba(120,255,140,0.08)" : "rgba(255,255,255,0.035)",
equipped ? "rgba(120,255,140,0.45)" : "rgba(255,255,255,0.07)", 14);
text(c, item.name, 82, y + 30, 20, equipped ? "#78ff8c" : "#e8eef8", true, "left");
const selected = preview && preview.id === item.id;
panel(c, 60, y, W - 120, 86,
equipped ? "rgba(120,255,140,0.08)" : selected ? "rgba(120,200,255,0.08)" : "rgba(255,255,255,0.035)",
equipped ? "rgba(120,255,140,0.45)" : selected ? "rgba(120,200,255,0.55)" : "rgba(255,255,255,0.07)", 14);
text(c, item.name, 82, y + 28, 19, equipped ? "#78ff8c" : "#e8eef8", true, "left");
const status = equipped ? "EQUIPPED" : owned ? "OWNED" : `${item.cost} CRYSTALS`;
text(c, status, 82, y + 60, 14, equipped ? "#78ff8c" : owned ? "#9696aa" : "#ffd45a", false, "left");
text(c, status, 82, y + 56, 13, equipped ? "#78ff8c" : owned ? "#9696aa" : "#ffd45a", false, "left");
this.smallButton(c, equipped ? "ACTIVE" : owned ? "EQUIP" : "BUY",
W - 250, y + 22, 170, 44, equipped ? "#78ff8c" : owned ? "#78c8ff" : "#ffd45a",
W - 250, y + 20, 170, 44, equipped ? "#78ff8c" : owned ? "#78c8ff" : "#ffd45a",
() => this.buyOrEquip(item));
if (item.type === "trail") {
circle(c, W - 300, y + 44, 9, item.value);
}
if (item.type === "trail") circle(c, W - 300, y + 43, 9, item.value);
// Row selection is registered after the action button so BUY/EQUIP remains clickable.
this.buttons.push({ x: 60, y, w: W - 120, h: 86, fn: () => { this.shopPreviewId = item.id; } });
});
if (this.shopMessage) text(c, this.shopMessage, W / 2, 820, 18, "#ffd45a");
this.button(c, "BACK", 850, "#c8c8dc", () => this.goTitle(), 72);
text(c, "1/2/3 tabs - B / ESC back", W / 2, 960, 17, "#828296");
if (this.shopMessage) text(c, this.shopMessage, W / 2, 890, 16, "#ffd45a");
this.button(c, "BACK", 950, "#c8c8dc", () => this.goTitle(), 62);
text(c, "SELECT AN ITEM TO PREVIEW • BUY / EQUIP TO APPLY • B / ESC BACK", W / 2, 1035, 13, "#828296");
}

drawSpace(c, ox = 0, oy = 0) {
Expand Down Expand Up @@ -3630,5 +3660,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, shouldApplyHitStop, campaignPauseHint, campaignMapPoint, storySignalState, planetSurveyProfile, campaignBossProfile, W, H };
dateKey, weekKey, mulberry32, difficultyProfile, flareWarningText, bossWarningText, comboMilestoneText, shouldRenderScreenFlash, shouldApplyHitStop, campaignPauseHint, campaignMapPoint, storySignalState, planetSurveyProfile, campaignBossProfile, cosmeticPreviewItem, 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-v22";
const CACHE = "orbit-runner-v23";
const FILES = ["./", "./index.html", "./config.js", "./game.js", "./manifest.json", "./icon-192.png", "./icon-512.png"];

self.addEventListener("install", e => {
Expand Down
12 changes: 11 additions & 1 deletion 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, shouldApplyHitStop, campaignPauseHint, campaignMapPoint, storySignalState, planetSurveyProfile, campaignBossProfile } = require("../game.js");
sanitizeCampaign, difficultyProfile, flareWarningText, comboMilestoneText, shouldRenderScreenFlash, shouldApplyHitStop, campaignPauseHint, campaignMapPoint, storySignalState, planetSurveyProfile, campaignBossProfile, cosmeticPreviewItem } = require("../game.js");

function localStore() {
const store = {};
Expand Down Expand Up @@ -430,6 +430,16 @@ test("volatile meteors explode and clear nearby rocks", () => {
assert.equal(other.dead, true);
});

test("cosmetic preview selects the requested item without changing equipped state", () => {
const equipped = { trail: "trail_default", skin: "skin_scout", title: "title_rookie" };
const selected = cosmeticPreviewItem("skin", "skin_stealth", equipped);
assert.equal(selected.id, "skin_stealth");
assert.equal(equipped.skin, "skin_scout");
assert.equal(cosmeticPreviewItem("trail", "unknown-item", equipped).id, "trail_default");
assert.equal(cosmeticPreviewItem("title", null, equipped).id, "title_rookie");
assert.equal(cosmeticPreviewItem("unknown", null, equipped), null);
});

test("biomes drive speed, score and crystal rates", () => {
const g = newGame();
g.start("daily");
Expand Down
Loading