Make progression and trade gameplay mobile compatible
This commit is contained in:
parent
39d881d375
commit
90b55fadd5
11
index.html
11
index.html
@ -132,6 +132,8 @@
|
||||
</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 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="buy-upgrade" aria-label="Buy ship upgrade">BUY</button>
|
||||
<button class="mobile-control ship-action fit" data-action="fit-salvage" aria-label="Fit salvaged module">FIT</button>
|
||||
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
|
||||
</div>
|
||||
|
||||
@ -203,7 +205,16 @@
|
||||
stick.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||
|
||||
for (const button of document.querySelectorAll('.mobile-control')) {
|
||||
const isAction = Boolean(button.dataset.action);
|
||||
const fire = (type) => {
|
||||
if (isAction) {
|
||||
if (type === 'keydown') {
|
||||
window.dispatchEvent(new CustomEvent('airship:action', {
|
||||
detail: { action: button.dataset.action },
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
dispatchKey(type, button.dataset.key, button.dataset.code, Number(button.dataset.keycode));
|
||||
};
|
||||
|
||||
|
||||
@ -101,6 +101,7 @@ class GameScene extends Phaser.Scene {
|
||||
});
|
||||
this.progression = createStarterProgression();
|
||||
this.playerStats = deriveShipStats(this.progression.loadout);
|
||||
this.pendingMobileActions = new Set();
|
||||
this.playerSpeed = 0;
|
||||
this.playerHull = this.playerStats.maxHull;
|
||||
this.gameState = 'playing';
|
||||
@ -220,6 +221,11 @@ class GameScene extends Phaser.Scene {
|
||||
this.helmVector.y = clamp(Number(event.detail?.y) || 0, -1, 1);
|
||||
};
|
||||
window.addEventListener('airship:helm-vector', this.handleHelmVector);
|
||||
this.handleMobileAction = (event) => {
|
||||
const action = String(event.detail?.action || '');
|
||||
if (action) this.pendingMobileActions.add(action);
|
||||
};
|
||||
window.addEventListener('airship:action', this.handleMobileAction);
|
||||
this.keys = this.input.keyboard.addKeys({
|
||||
firePort: Phaser.Input.Keyboard.KeyCodes.Q,
|
||||
fireStarboard: Phaser.Input.Keyboard.KeyCodes.E,
|
||||
@ -240,6 +246,7 @@ class GameScene extends Phaser.Scene {
|
||||
this.scale.off('resize', this.layoutHud, this);
|
||||
registerDebugSnapshot(null);
|
||||
window.removeEventListener('airship:helm-vector', this.handleHelmVector);
|
||||
window.removeEventListener('airship:action', this.handleMobileAction);
|
||||
airshipLog('scene:shutdown', {
|
||||
gameState: this.gameState,
|
||||
hull: this.playerHull,
|
||||
@ -521,7 +528,10 @@ class GameScene extends Phaser.Scene {
|
||||
const { port, range } = nearestPort(this.world, this.player.x, this.player.y);
|
||||
const docked = Boolean(port && range <= 230);
|
||||
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.buyUpgrade)) {
|
||||
const mobileBuy = this.pendingMobileActions.delete('buy-upgrade');
|
||||
const mobileFit = this.pendingMobileActions.delete('fit-salvage');
|
||||
|
||||
if (mobileBuy || Phaser.Input.Keyboard.JustDown(this.keys.buyUpgrade)) {
|
||||
if (!docked) {
|
||||
this.status.setText('GET WITHIN 230m OF A PORT TO BUY UPGRADES');
|
||||
} else {
|
||||
@ -542,7 +552,7 @@ class GameScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
if (Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) {
|
||||
if (mobileFit || Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) {
|
||||
const moduleId = this.progression.inventory?.[0];
|
||||
if (!moduleId) {
|
||||
this.status.setText('NO SALVAGED MODULES IN HOLD');
|
||||
@ -805,7 +815,9 @@ class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
createMerchantTraffic() {
|
||||
const plans = createMerchantTrafficPlans(this.world, { maxShips: MAX_MERCHANTS });
|
||||
const desktopPointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
const merchantLimit = desktopPointer ? MAX_MERCHANTS : Math.min(12, MAX_MERCHANTS);
|
||||
const plans = createMerchantTrafficPlans(this.world, { maxShips: merchantLimit });
|
||||
|
||||
for (const plan of plans) {
|
||||
const route = this.world.tradeRoutes.find(({ id }) => id === plan.routeId);
|
||||
@ -1592,6 +1604,12 @@ class GameScene extends Phaser.Scene {
|
||||
);
|
||||
const portState = nearestPort(this.world, player.x, player.y);
|
||||
const influence = pirateInfluenceAt(this.world, player.x, player.y);
|
||||
const docked = Boolean(portState.port && portState.range <= 230);
|
||||
const marketUpgrade = docked
|
||||
? bestAffordableMarketUpgrade(this.progression, portState.port)
|
||||
: null;
|
||||
const salvageModuleId = this.progression?.inventory?.[0] || null;
|
||||
const salvageModule = salvageModuleId ? MODULE_CATALOG[salvageModuleId] : null;
|
||||
|
||||
return {
|
||||
gameState: this.gameState,
|
||||
@ -1632,6 +1650,16 @@ class GameScene extends Phaser.Scene {
|
||||
inventoryCount: this.progression?.inventory?.length || 0,
|
||||
nearestPort: portState.port?.name || null,
|
||||
nearestPortRange: portState.range,
|
||||
docked,
|
||||
marketUpgrade: marketUpgrade ? {
|
||||
id: marketUpgrade.id,
|
||||
name: marketUpgrade.name,
|
||||
price: marketUpgrade.price,
|
||||
} : null,
|
||||
salvageModule: salvageModule ? {
|
||||
id: salvageModule.id,
|
||||
name: salvageModule.name,
|
||||
} : null,
|
||||
},
|
||||
world: {
|
||||
seed: this.world?.seed,
|
||||
|
||||
@ -389,6 +389,45 @@ if (!isDesktopPointer.matches) {
|
||||
bottom: max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.ship-action {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: max(10px, env(safe-area-inset-bottom));
|
||||
width: min(28vw, 126px);
|
||||
min-width: 92px;
|
||||
height: 48px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid rgba(232, 184, 91, .65);
|
||||
border-radius: 12px;
|
||||
background: rgba(8, 25, 31, .88);
|
||||
color: #ffe5a0;
|
||||
font: 700 clamp(9px, 2.7vw, 12px)/1.05 Georgia, 'Times New Roman', serif;
|
||||
text-align: center;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, .38);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.ship-action:disabled {
|
||||
opacity: .34;
|
||||
filter: grayscale(.7);
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.ship-action.active,
|
||||
#mobile-controls.controller-active .mobile-control.ship-action:active {
|
||||
background: rgba(117, 78, 31, .9);
|
||||
transform: translateX(-50%) scale(.96);
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.buy {
|
||||
margin-left: -16%;
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.fit {
|
||||
margin-left: 16%;
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.restart {
|
||||
left: auto;
|
||||
right: 1%;
|
||||
@ -499,6 +538,22 @@ if (!isDesktopPointer.matches) {
|
||||
right: calc(var(--steam-wide-edge-right) + var(--steam-wide-cannon-size) + 14px);
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.ship-action {
|
||||
bottom: var(--steam-wide-bottom);
|
||||
width: clamp(100px, 15vw, 142px);
|
||||
height: 50px;
|
||||
min-width: 100px;
|
||||
font-size: clamp(9px, 1.15vw, 12px);
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.buy {
|
||||
margin-left: -86px;
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.fit {
|
||||
margin-left: 86px;
|
||||
}
|
||||
|
||||
#mobile-controls.controller-active .mobile-control.restart {
|
||||
right: max(12px, env(safe-area-inset-right));
|
||||
top: clamp(118px, 30vh, 168px);
|
||||
@ -641,6 +696,8 @@ if (!isDesktopPointer.matches) {
|
||||
const fields = Object.fromEntries(
|
||||
[...hud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]),
|
||||
);
|
||||
const buyButton = root.querySelector('.mobile-control.buy');
|
||||
const fitButton = root.querySelector('.mobile-control.fit');
|
||||
|
||||
const radarContacts = radarPiece.querySelector('#steam-radar-contacts');
|
||||
|
||||
@ -740,6 +797,24 @@ if (!isDesktopPointer.matches) {
|
||||
fields['port-reload'].style.transform = `scaleX(${clamp01(state.reload?.port ?? 1)})`;
|
||||
fields['starboard-reload'].style.transform = `scaleX(${clamp01(state.reload?.starboard ?? 1)})`;
|
||||
|
||||
const progression = state.progression || {};
|
||||
if (buyButton) {
|
||||
const upgrade = progression.marketUpgrade;
|
||||
buyButton.disabled = !progression.docked || !upgrade;
|
||||
buyButton.textContent = !progression.docked
|
||||
? 'BUY\nPORT'
|
||||
: upgrade
|
||||
? `BUY\n${upgrade.name} ¢${upgrade.price}`
|
||||
: 'BUY\nNO UPGRADE';
|
||||
}
|
||||
if (fitButton) {
|
||||
const module = progression.salvageModule;
|
||||
fitButton.disabled = !module;
|
||||
fitButton.textContent = module
|
||||
? `FIT\n${module.name}`
|
||||
: 'FIT\nEMPTY';
|
||||
}
|
||||
|
||||
const statusText = String(state.status || '').trim();
|
||||
fields.status.textContent = statusText;
|
||||
fields.status.classList.toggle('visible', Boolean(statusText));
|
||||
|
||||
@ -263,6 +263,33 @@ test('environment physics adds wind, soft island collision, turbulence, and proj
|
||||
assert.match(source, /windSpeed: this\.environmentWind\?\.strength/);
|
||||
});
|
||||
|
||||
test('mobile controls expose native buy and salvage-fit actions', async () => {
|
||||
const [game, hud, index] = await Promise.all([
|
||||
read('src/game-v4.js'),
|
||||
read('src/mobile-hud.js'),
|
||||
read('index.html'),
|
||||
]);
|
||||
|
||||
assert.match(index, /data-action="buy-upgrade"/);
|
||||
assert.match(index, /data-action="fit-salvage"/);
|
||||
assert.match(index, /CustomEvent\('airship:action'/);
|
||||
assert.match(game, /addEventListener\('airship:action', this\.handleMobileAction\)/);
|
||||
assert.match(game, /this\.pendingMobileActions\.delete\('buy-upgrade'\)/);
|
||||
assert.match(game, /this\.pendingMobileActions\.delete\('fit-salvage'\)/);
|
||||
assert.match(game, /marketUpgrade: marketUpgrade \? \{/);
|
||||
assert.match(game, /salvageModule: salvageModule \? \{/);
|
||||
assert.match(hud, /root\.querySelector\('\.mobile-control\.buy'\)/);
|
||||
assert.match(hud, /root\.querySelector\('\.mobile-control\.fit'\)/);
|
||||
assert.match(hud, /buyButton\.disabled = !progression\.docked \|\| !upgrade/);
|
||||
assert.match(hud, /fitButton\.disabled = !module/);
|
||||
});
|
||||
|
||||
test('mobile merchant traffic uses a reduced render budget', async () => {
|
||||
const source = await read('src/game-v4.js');
|
||||
assert.match(source, /const merchantLimit = desktopPointer \? MAX_MERCHANTS : Math\.min\(12, MAX_MERCHANTS\)/);
|
||||
assert.match(source, /createMerchantTrafficPlans\(this\.world, \{ maxShips: merchantLimit \}\)/);
|
||||
});
|
||||
|
||||
test('merchant traffic follows trade routes and pirates can raid it', async () => {
|
||||
const source = await read('src/game-v4.js');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user