Add dock hub with missions trade and shipyard
This commit is contained in:
parent
2488de8d96
commit
364a20e9b2
@ -132,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="mobile-control port" data-code="KeyQ" data-key="q" data-keycode="81" aria-label="Fire port broadside">PORT</button>
|
<button class="mobile-control port" data-code="KeyQ" data-key="q" data-keycode="81" aria-label="Fire port broadside">PORT</button>
|
||||||
<button class="mobile-control starboard" data-code="KeyE" data-key="e" data-keycode="69" aria-label="Fire starboard broadside">STARBOARD</button>
|
<button class="mobile-control starboard" data-code="KeyE" data-key="e" data-keycode="69" aria-label="Fire starboard broadside">STARBOARD</button>
|
||||||
<button class="mobile-control ship-action buy" data-action="open-shipyard" aria-label="Open shipyard">SHIPYARD</button>
|
<button class="mobile-control ship-action buy" data-action="open-dock" aria-label="Dock at port">DOCK</button>
|
||||||
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
|
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import './game-v4.js';
|
import './game-v4.js';
|
||||||
import './mobile-hud.js';
|
import './mobile-hud.js';
|
||||||
|
import './dock-ui.js';
|
||||||
import './shipyard-ui.js';
|
import './shipyard-ui.js';
|
||||||
|
|||||||
178
src/game-v4.js
178
src/game-v4.js
@ -43,6 +43,16 @@ import {
|
|||||||
upgradePathForSlot,
|
upgradePathForSlot,
|
||||||
} from './progression/ship-loadout.js';
|
} from './progression/ship-loadout.js';
|
||||||
import { MODULE_CATALOG } from './progression/modules.js';
|
import { MODULE_CATALOG } from './progression/modules.js';
|
||||||
|
import {
|
||||||
|
acceptMission,
|
||||||
|
buyTradeGood,
|
||||||
|
completeMission,
|
||||||
|
marketAtPort,
|
||||||
|
missionOffersAtPort,
|
||||||
|
missionStatus,
|
||||||
|
sellTradeGood,
|
||||||
|
tradeCargoUnits,
|
||||||
|
} from './progression/dock-economy.js';
|
||||||
import {
|
import {
|
||||||
createMerchantTrafficPlans,
|
createMerchantTrafficPlans,
|
||||||
merchantPointOnRoute,
|
merchantPointOnRoute,
|
||||||
@ -106,7 +116,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.progression = createStarterProgression();
|
this.progression = createStarterProgression();
|
||||||
this.playerStats = deriveShipStats(this.progression.loadout);
|
this.playerStats = deriveShipStats(this.progression.loadout);
|
||||||
this.pendingMobileActions = new Set();
|
this.pendingMobileActions = new Set();
|
||||||
|
this.pendingActionDetails = new Map();
|
||||||
this.pendingModulePurchaseId = null;
|
this.pendingModulePurchaseId = null;
|
||||||
|
this.dockOpen = false;
|
||||||
this.shipyardOpen = false;
|
this.shipyardOpen = false;
|
||||||
this.playerSpeed = 0;
|
this.playerSpeed = 0;
|
||||||
this.playerHull = this.playerStats.maxHull;
|
this.playerHull = this.playerStats.maxHull;
|
||||||
@ -230,6 +242,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.handleMobileAction = (event) => {
|
this.handleMobileAction = (event) => {
|
||||||
const action = String(event.detail?.action || '');
|
const action = String(event.detail?.action || '');
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
|
this.pendingActionDetails.set(action, { ...(event.detail || {}) });
|
||||||
if (action === 'purchase-module') {
|
if (action === 'purchase-module') {
|
||||||
this.pendingModulePurchaseId = String(event.detail?.moduleId || '');
|
this.pendingModulePurchaseId = String(event.detail?.moduleId || '');
|
||||||
}
|
}
|
||||||
@ -550,39 +563,76 @@ class GameScene extends Phaser.Scene {
|
|||||||
|
|
||||||
const { port, range } = nearestPort(this.world, this.player.x, this.player.y);
|
const { port, range } = nearestPort(this.world, this.player.x, this.player.y);
|
||||||
const docked = Boolean(port && range <= 230);
|
const docked = Boolean(port && range <= 230);
|
||||||
const openRequested = this.pendingMobileActions.delete('open-shipyard')
|
const takeAction = (action) => {
|
||||||
|| Phaser.Input.Keyboard.JustDown(this.keys.buyUpgrade);
|
const requested = this.pendingMobileActions.delete(action);
|
||||||
const closeRequested = this.pendingMobileActions.delete('close-shipyard')
|
const detail = this.pendingActionDetails.get(action) || null;
|
||||||
|| Phaser.Input.Keyboard.JustDown(this.keys.closeShipyard);
|
if (requested) this.pendingActionDetails.delete(action);
|
||||||
const purchaseRequested = this.pendingMobileActions.delete('purchase-module');
|
return requested ? (detail || {}) : null;
|
||||||
const mobileFit = this.pendingMobileActions.delete('fit-salvage');
|
};
|
||||||
const scrapSalvage = this.pendingMobileActions.delete('scrap-salvage');
|
|
||||||
|
|
||||||
if (closeRequested && this.shipyardOpen) {
|
const openDockAction = takeAction('open-dock');
|
||||||
|
const openShipyardAction = takeAction('open-shipyard');
|
||||||
|
const closeShipyardAction = takeAction('close-shipyard');
|
||||||
|
const closeDockAction = takeAction('close-dock');
|
||||||
|
const purchaseAction = takeAction('purchase-module');
|
||||||
|
const fitAction = takeAction('fit-salvage');
|
||||||
|
const scrapAction = takeAction('scrap-salvage');
|
||||||
|
const buyTradeAction = takeAction('buy-trade-good');
|
||||||
|
const sellTradeAction = takeAction('sell-trade-good');
|
||||||
|
const acceptMissionAction = takeAction('accept-mission');
|
||||||
|
const completeMissionAction = takeAction('complete-mission');
|
||||||
|
|
||||||
|
const keyboardDock = Phaser.Input.Keyboard.JustDown(this.keys.buyUpgrade);
|
||||||
|
const keyboardEscape = Phaser.Input.Keyboard.JustDown(this.keys.closeShipyard);
|
||||||
|
|
||||||
|
if ((closeShipyardAction || (keyboardEscape && this.shipyardOpen)) && this.shipyardOpen) {
|
||||||
this.shipyardOpen = false;
|
this.shipyardOpen = false;
|
||||||
this.pendingModulePurchaseId = null;
|
this.pendingModulePurchaseId = null;
|
||||||
window.dispatchEvent(new CustomEvent('airship:shipyard-close'));
|
window.dispatchEvent(new CustomEvent('airship:shipyard-close'));
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:dock-return', {
|
||||||
|
detail: this.buildHudState(),
|
||||||
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openRequested) {
|
if ((closeDockAction || (keyboardEscape && this.dockOpen && !this.shipyardOpen)) && this.dockOpen) {
|
||||||
|
this.shipyardOpen = false;
|
||||||
|
this.dockOpen = false;
|
||||||
|
this.pendingModulePurchaseId = null;
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:shipyard-close'));
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:dock-close'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openDockAction || keyboardDock) {
|
||||||
if (!docked) {
|
if (!docked) {
|
||||||
this.status.setText('GET WITHIN 230m OF A PORT TO ENTER SHIPYARD');
|
this.status.setText('GET WITHIN 230m OF A PORT TO DOCK');
|
||||||
this.time.delayedCall(1400, () => {
|
this.time.delayedCall(1400, () => {
|
||||||
if (this.gameState === 'playing') this.status.setText('');
|
if (this.gameState === 'playing') this.status.setText('');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.shipyardOpen = true;
|
this.dockOpen = true;
|
||||||
|
this.shipyardOpen = false;
|
||||||
this.playerSpeed = 0;
|
this.playerSpeed = 0;
|
||||||
this.player.setVelocity(0, 0);
|
this.player.setVelocity(0, 0);
|
||||||
window.dispatchEvent(new CustomEvent('airship:shipyard-open', {
|
window.dispatchEvent(new CustomEvent('airship:dock-open', {
|
||||||
detail: this.buildHudState(),
|
detail: this.buildHudState(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (purchaseRequested) {
|
if (openShipyardAction && this.dockOpen && docked && !this.shipyardOpen) {
|
||||||
const moduleId = this.pendingModulePurchaseId;
|
this.shipyardOpen = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:dock-hide'));
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:shipyard-open', {
|
||||||
|
detail: this.buildHudState(),
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseAction) {
|
||||||
|
const moduleId = String(purchaseAction.moduleId || this.pendingModulePurchaseId || '');
|
||||||
this.pendingModulePurchaseId = null;
|
this.pendingModulePurchaseId = null;
|
||||||
if (!this.shipyardOpen || !docked || !moduleId) return;
|
if (!this.shipyardOpen || !docked || !moduleId) return;
|
||||||
|
|
||||||
@ -607,7 +657,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mobileFit || Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) {
|
if (fitAction || Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) {
|
||||||
if (!this.shipyardOpen || !docked) {
|
if (!this.shipyardOpen || !docked) {
|
||||||
this.status.setText('FIT RECOVERED MODULES AT THE SHIPYARD');
|
this.status.setText('FIT RECOVERED MODULES AT THE SHIPYARD');
|
||||||
} else {
|
} else {
|
||||||
@ -630,7 +680,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrapSalvage) {
|
if (scrapAction) {
|
||||||
if (!this.shipyardOpen || !docked) {
|
if (!this.shipyardOpen || !docked) {
|
||||||
this.status.setText('SCRAP RECOVERED MODULES AT THE SHIPYARD');
|
this.status.setText('SCRAP RECOVERED MODULES AT THE SHIPYARD');
|
||||||
} else {
|
} else {
|
||||||
@ -648,6 +698,68 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.dockOpen && docked && !this.shipyardOpen) {
|
||||||
|
const market = marketAtPort(this.world, port);
|
||||||
|
|
||||||
|
if (buyTradeAction) {
|
||||||
|
const item = market.find(({ goodId }) => goodId === buyTradeAction.goodId);
|
||||||
|
this.progression = buyTradeGood(this.progression, item, this.playerStats, 1);
|
||||||
|
this.status.setText(
|
||||||
|
this.progression.traded
|
||||||
|
? `BOUGHT ${item?.name || 'CARGO'} // ¢${this.progression.credits}`
|
||||||
|
: this.progression.reason === 'cargo-full'
|
||||||
|
? 'CARGO HOLD FULL'
|
||||||
|
: 'NOT ENOUGH COIN',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sellTradeAction) {
|
||||||
|
const item = market.find(({ goodId }) => goodId === sellTradeAction.goodId);
|
||||||
|
this.progression = sellTradeGood(this.progression, item, 1);
|
||||||
|
this.status.setText(
|
||||||
|
this.progression.traded
|
||||||
|
? `SOLD ${item?.name || 'CARGO'} // ¢${this.progression.credits}`
|
||||||
|
: 'NONE OF THAT CARGO IN HOLD',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptMissionAction) {
|
||||||
|
const offers = missionOffersAtPort(this.world, port, this.defeated);
|
||||||
|
const mission = offers.find(({ id }) => id === acceptMissionAction.missionId);
|
||||||
|
this.progression = acceptMission(
|
||||||
|
this.progression,
|
||||||
|
mission,
|
||||||
|
this.playerStats,
|
||||||
|
this.defeated,
|
||||||
|
);
|
||||||
|
this.status.setText(
|
||||||
|
this.progression.missionAccepted
|
||||||
|
? `MISSION ACCEPTED\n${mission?.title || ''}`
|
||||||
|
: this.progression.reason === 'cargo-full'
|
||||||
|
? 'NOT ENOUGH CARGO SPACE'
|
||||||
|
: 'FINISH CURRENT MISSION FIRST',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completeMissionAction) {
|
||||||
|
const activeBefore = this.progression.activeMission;
|
||||||
|
this.progression = completeMission(this.progression, {
|
||||||
|
portId: port.id,
|
||||||
|
defeated: this.defeated,
|
||||||
|
});
|
||||||
|
this.status.setText(
|
||||||
|
this.progression.missionCompleted
|
||||||
|
? `MISSION COMPLETE +¢${activeBefore?.reward || 0}`
|
||||||
|
: 'MISSION NOT COMPLETE YET',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buyTradeAction || sellTradeAction || acceptMissionAction || completeMissionAction) {
|
||||||
|
window.dispatchEvent(new CustomEvent('airship:dock-state', {
|
||||||
|
detail: this.buildHudState(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEnvironmentPhysics() {
|
createEnvironmentPhysics() {
|
||||||
@ -1230,7 +1342,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateProgressionInteraction();
|
this.updateProgressionInteraction();
|
||||||
if (this.shipyardOpen) {
|
if (this.dockOpen) {
|
||||||
this.player.setVelocity(0, 0);
|
this.player.setVelocity(0, 0);
|
||||||
const hudState = this.buildHudState();
|
const hudState = this.buildHudState();
|
||||||
if (!this.mobileHudActive) this.drawHud(hudState);
|
if (!this.mobileHudActive) this.drawHud(hudState);
|
||||||
@ -1707,6 +1819,15 @@ class GameScene extends Phaser.Scene {
|
|||||||
upgradePathForSlot(this.progression.loadout, slot, marketStock),
|
upgradePathForSlot(this.progression.loadout, slot, marketStock),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
const dockMarket = docked ? marketAtPort(this.world, portState.port) : [];
|
||||||
|
const cargoUsed = tradeCargoUnits(this.progression);
|
||||||
|
const missionOffers = docked
|
||||||
|
? missionOffersAtPort(this.world, portState.port, this.defeated)
|
||||||
|
: [];
|
||||||
|
const activeMission = missionStatus(this.progression, {
|
||||||
|
portId: docked ? portState.port?.id : null,
|
||||||
|
defeated: this.defeated,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
@ -1741,6 +1862,28 @@ class GameScene extends Phaser.Scene {
|
|||||||
losses: this.tradeLosses,
|
losses: this.tradeLosses,
|
||||||
playerLosses: this.playerTradeLosses,
|
playerLosses: this.playerTradeLosses,
|
||||||
},
|
},
|
||||||
|
dock: {
|
||||||
|
open: this.dockOpen,
|
||||||
|
shipyardOpen: this.shipyardOpen,
|
||||||
|
portId: docked ? portState.port?.id : null,
|
||||||
|
portName: docked ? portState.port?.name : null,
|
||||||
|
portType: docked ? portState.port?.type : null,
|
||||||
|
portExports: docked ? portState.port?.exports || [] : [],
|
||||||
|
cargoUsed,
|
||||||
|
cargoCapacity: this.playerStats?.cargoCapacity || 0,
|
||||||
|
market: dockMarket.map((item) => {
|
||||||
|
const held = this.progression.tradeCargo?.[item.goodId] || 0;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
held,
|
||||||
|
canBuy: (this.progression.credits || 0) >= item.buyPrice
|
||||||
|
&& cargoUsed < (this.playerStats?.cargoCapacity || 0),
|
||||||
|
canSell: held > 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
missionOffers,
|
||||||
|
activeMission,
|
||||||
|
},
|
||||||
progression: {
|
progression: {
|
||||||
credits: this.progression?.credits || 0,
|
credits: this.progression?.credits || 0,
|
||||||
scrap: this.progression?.scrap || 0,
|
scrap: this.progression?.scrap || 0,
|
||||||
@ -1748,6 +1891,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
nearestPort: portState.port?.name || null,
|
nearestPort: portState.port?.name || null,
|
||||||
nearestPortRange: portState.range,
|
nearestPortRange: portState.range,
|
||||||
docked,
|
docked,
|
||||||
|
dockOpen: this.dockOpen,
|
||||||
shipyardOpen: this.shipyardOpen,
|
shipyardOpen: this.shipyardOpen,
|
||||||
shipStats: { ...this.playerStats },
|
shipStats: { ...this.playerStats },
|
||||||
loadout: { ...this.progression.loadout },
|
loadout: { ...this.progression.loadout },
|
||||||
|
|||||||
@ -805,9 +805,14 @@ if (!isDesktopPointer.matches) {
|
|||||||
buyButton.hidden = !progression.docked;
|
buyButton.hidden = !progression.docked;
|
||||||
buyButton.disabled = !progression.docked;
|
buyButton.disabled = !progression.docked;
|
||||||
const salvageCount = progression.inventoryCount || 0;
|
const salvageCount = progression.inventoryCount || 0;
|
||||||
buyButton.textContent = salvageCount > 0
|
const missionActive = Boolean(state.dock?.activeMission);
|
||||||
? `SHIPYARD\nSALVAGE ${salvageCount}`
|
const badges = [
|
||||||
: 'SHIPYARD';
|
missionActive ? 'MISSION' : '',
|
||||||
|
salvageCount > 0 ? `SALVAGE ${salvageCount}` : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
buyButton.textContent = badges.length
|
||||||
|
? `DOCK\n${badges.join(' • ')}`
|
||||||
|
: 'DOCK';
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusText = String(state.status || '').trim();
|
const statusText = String(state.status || '').trim();
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export const createStarterProgression = () => ({
|
|||||||
scrap: 0,
|
scrap: 0,
|
||||||
loadout: { ...STARTER_LOADOUT },
|
loadout: { ...STARTER_LOADOUT },
|
||||||
inventory: [],
|
inventory: [],
|
||||||
|
tradeCargo: {},
|
||||||
|
activeMission: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deriveShipStats = (loadout = STARTER_LOADOUT) => {
|
export const deriveShipStats = (loadout = STARTER_LOADOUT) => {
|
||||||
|
|||||||
87
tests/dock-economy.test.mjs
Normal file
87
tests/dock-economy.test.mjs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { generateWorld } from '../src/world/world-generator.js';
|
||||||
|
import { createStarterProgression, deriveShipStats } from '../src/progression/ship-loadout.js';
|
||||||
|
import {
|
||||||
|
acceptMission,
|
||||||
|
buyTradeGood,
|
||||||
|
completeMission,
|
||||||
|
marketAtPort,
|
||||||
|
missionOffersAtPort,
|
||||||
|
missionStatus,
|
||||||
|
sellTradeGood,
|
||||||
|
tradeCargoUnits,
|
||||||
|
} from '../src/progression/dock-economy.js';
|
||||||
|
|
||||||
|
test('port markets are deterministic and expose buy/sell prices', () => {
|
||||||
|
const world = generateWorld({ seed: 'DOCK-MARKET' });
|
||||||
|
const port = world.ports[0];
|
||||||
|
const a = marketAtPort(world, port);
|
||||||
|
const b = marketAtPort(world, port);
|
||||||
|
assert.deepEqual(a, b);
|
||||||
|
assert.ok(a.length >= 4);
|
||||||
|
assert.ok(a.every((item) => item.buyPrice > item.sellPrice));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trade cargo respects capacity and selling returns coin', () => {
|
||||||
|
const world = generateWorld({ seed: 'DOCK-TRADE' });
|
||||||
|
const market = marketAtPort(world, world.ports[0]);
|
||||||
|
const item = market[0];
|
||||||
|
const starter = { ...createStarterProgression(), credits: 2000 };
|
||||||
|
const stats = deriveShipStats(starter.loadout);
|
||||||
|
const bought = buyTradeGood(starter, item, stats);
|
||||||
|
assert.equal(bought.traded, true);
|
||||||
|
assert.equal(tradeCargoUnits(bought), 1);
|
||||||
|
assert.ok(bought.credits < starter.credits);
|
||||||
|
|
||||||
|
const sold = sellTradeGood(bought, item);
|
||||||
|
assert.equal(sold.traded, true);
|
||||||
|
assert.equal(tradeCargoUnits(sold), 0);
|
||||||
|
assert.ok(sold.credits > bought.credits);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delivery missions follow generated trade routes and complete at destination', () => {
|
||||||
|
const world = generateWorld({ seed: 'DOCK-MISSIONS' });
|
||||||
|
const origin = world.ports[0];
|
||||||
|
const offers = missionOffersAtPort(world, origin, 0);
|
||||||
|
const delivery = offers.find(({ type }) => type === 'delivery');
|
||||||
|
assert.ok(delivery);
|
||||||
|
assert.ok(world.tradeRoutes.some((route) =>
|
||||||
|
(route.fromPortId === origin.id && route.toPortId === delivery.targetPortId)
|
||||||
|
|| (route.toPortId === origin.id && route.fromPortId === delivery.targetPortId)
|
||||||
|
));
|
||||||
|
|
||||||
|
const starter = createStarterProgression();
|
||||||
|
const stats = deriveShipStats(starter.loadout);
|
||||||
|
const accepted = acceptMission(starter, delivery, stats, 0);
|
||||||
|
assert.equal(accepted.missionAccepted, true);
|
||||||
|
assert.equal(missionStatus(accepted, { portId: origin.id, defeated: 0 }).ready, false);
|
||||||
|
|
||||||
|
const completed = completeMission(accepted, {
|
||||||
|
portId: delivery.targetPortId,
|
||||||
|
defeated: 0,
|
||||||
|
});
|
||||||
|
assert.equal(completed.missionCompleted, true);
|
||||||
|
assert.equal(completed.activeMission, null);
|
||||||
|
assert.equal(completed.credits, starter.credits + delivery.reward);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bounty missions advance from actual pirate kill count', () => {
|
||||||
|
const world = generateWorld({ seed: 'DOCK-BOUNTY' });
|
||||||
|
const offers = missionOffersAtPort(world, world.ports[0], 5);
|
||||||
|
const bounty = offers.find(({ type }) => type === 'bounty');
|
||||||
|
const starter = createStarterProgression();
|
||||||
|
const stats = deriveShipStats(starter.loadout);
|
||||||
|
const accepted = acceptMission(starter, bounty, stats, 5);
|
||||||
|
const pending = missionStatus(accepted, {
|
||||||
|
portId: world.ports[0].id,
|
||||||
|
defeated: 5 + bounty.targetKills - 1,
|
||||||
|
});
|
||||||
|
assert.equal(pending.ready, false);
|
||||||
|
const ready = missionStatus(accepted, {
|
||||||
|
portId: world.ports[0].id,
|
||||||
|
defeated: 5 + bounty.targetKills,
|
||||||
|
});
|
||||||
|
assert.equal(ready.ready, true);
|
||||||
|
});
|
||||||
@ -263,21 +263,25 @@ test('environment physics adds wind, soft island collision, turbulence, and proj
|
|||||||
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
|
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mobile controls route salvage management through the docked shipyard', async () => {
|
test('mobile controls enter a dock hub before shipyard, trade, or missions', async () => {
|
||||||
const [game, hud, index] = await Promise.all([
|
const [game, hud, index, entry, dockUi] = await Promise.all([
|
||||||
read('src/game-v4.js'),
|
read('src/game-v4.js'),
|
||||||
read('src/mobile-hud.js'),
|
read('src/mobile-hud.js'),
|
||||||
read('index.html'),
|
read('index.html'),
|
||||||
|
read('src/game-entry.js'),
|
||||||
|
read('src/dock-ui.js'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.match(index, /data-action="open-shipyard"/);
|
assert.match(index, /data-action="open-dock"/);
|
||||||
assert.doesNotMatch(index, /data-action="fit-salvage"/);
|
assert.match(entry, /import '.\/dock-ui\.js'/);
|
||||||
assert.match(index, /CustomEvent\('airship:action'/);
|
assert.match(game, /this\.pendingMobileActions\.delete\('open-dock'\)/);
|
||||||
assert.match(game, /this\.pendingMobileActions\.delete\('open-shipyard'\)/);
|
assert.match(game, /this\.dockOpen = true/);
|
||||||
assert.match(game, /this\.pendingMobileActions\.delete\('fit-salvage'\)/);
|
assert.match(game, /if \(this\.dockOpen\)/);
|
||||||
assert.match(game, /this\.pendingMobileActions\.delete\('scrap-salvage'\)/);
|
assert.match(dockUi, /MISSIONS/);
|
||||||
assert.match(hud, /buyButton\.textContent = salvageCount > 0/);
|
assert.match(dockUi, /TRADE/);
|
||||||
assert.doesNotMatch(hud, /const fitButton = root\.querySelector/);
|
assert.match(dockUi, /SHIPYARD/);
|
||||||
|
assert.match(dockUi, /dispatchAction\('open-shipyard'\)/);
|
||||||
|
assert.match(hud, /buyButton\.textContent = badges\.length/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shipyard overlay uses supplied component art and explicit module purchases', async () => {
|
test('shipyard overlay uses supplied component art and explicit module purchases', async () => {
|
||||||
@ -305,12 +309,36 @@ test('shipyard overlay uses supplied component art and explicit module purchases
|
|||||||
assert.match(game, /marketForPort\(port\)/);
|
assert.match(game, /marketForPort\(port\)/);
|
||||||
assert.match(game, /airship:shipyard-state/);
|
assert.match(game, /airship:shipyard-state/);
|
||||||
assert.match(game, /this\.shipyardOpen = true/);
|
assert.match(game, /this\.shipyardOpen = true/);
|
||||||
|
assert.match(game, /airship:dock-hide/);
|
||||||
|
assert.match(game, /airship:dock-return/);
|
||||||
assert.match(modules, /'reinforced-rudder'/);
|
assert.match(modules, /'reinforced-rudder'/);
|
||||||
assert.match(modules, /price: 800/);
|
assert.match(modules, /price: 800/);
|
||||||
assert.match(modules, /price: 600/);
|
assert.match(modules, /price: 600/);
|
||||||
assert.match(modules, /price: 400/);
|
assert.match(modules, /price: 400/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dock trade and missions are driven by generated port economy state', async () => {
|
||||||
|
const [game, economy, dockUi] = await Promise.all([
|
||||||
|
read('src/game-v4.js'),
|
||||||
|
read('src/progression/dock-economy.js'),
|
||||||
|
read('src/dock-ui.js'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(game, /marketAtPort\(this\.world, port\)/);
|
||||||
|
assert.match(game, /missionOffersAtPort\(this\.world, port, this\.defeated\)/);
|
||||||
|
assert.match(game, /buyTradeGood\(/);
|
||||||
|
assert.match(game, /sellTradeGood\(/);
|
||||||
|
assert.match(game, /acceptMission\(/);
|
||||||
|
assert.match(game, /completeMission\(/);
|
||||||
|
assert.match(game, /dock: \{/);
|
||||||
|
assert.match(economy, /connectedRoutes\(world, port\.id\)/);
|
||||||
|
assert.match(economy, /port\.exports/);
|
||||||
|
assert.match(dockUi, /buy-trade-good/);
|
||||||
|
assert.match(dockUi, /sell-trade-good/);
|
||||||
|
assert.match(dockUi, /accept-mission/);
|
||||||
|
assert.match(dockUi, /complete-mission/);
|
||||||
|
});
|
||||||
|
|
||||||
test('mobile merchant traffic uses a reduced render budget', async () => {
|
test('mobile merchant traffic uses a reduced render budget', async () => {
|
||||||
const source = await read('src/game-v4.js');
|
const source = await read('src/game-v4.js');
|
||||||
assert.match(source, /const merchantLimit = desktopPointer \? MAX_MERCHANTS : Math\.min\(12, MAX_MERCHANTS\)/);
|
assert.match(source, /const merchantLimit = desktopPointer \? MAX_MERCHANTS : Math\.min\(12, MAX_MERCHANTS\)/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user