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>
|
</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="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>
|
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -203,7 +205,16 @@
|
|||||||
stick.addEventListener('contextmenu', (event) => event.preventDefault());
|
stick.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||||
|
|
||||||
for (const button of document.querySelectorAll('.mobile-control')) {
|
for (const button of document.querySelectorAll('.mobile-control')) {
|
||||||
|
const isAction = Boolean(button.dataset.action);
|
||||||
const fire = (type) => {
|
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));
|
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.progression = createStarterProgression();
|
||||||
this.playerStats = deriveShipStats(this.progression.loadout);
|
this.playerStats = deriveShipStats(this.progression.loadout);
|
||||||
|
this.pendingMobileActions = new Set();
|
||||||
this.playerSpeed = 0;
|
this.playerSpeed = 0;
|
||||||
this.playerHull = this.playerStats.maxHull;
|
this.playerHull = this.playerStats.maxHull;
|
||||||
this.gameState = 'playing';
|
this.gameState = 'playing';
|
||||||
@ -220,6 +221,11 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.helmVector.y = clamp(Number(event.detail?.y) || 0, -1, 1);
|
this.helmVector.y = clamp(Number(event.detail?.y) || 0, -1, 1);
|
||||||
};
|
};
|
||||||
window.addEventListener('airship:helm-vector', this.handleHelmVector);
|
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({
|
this.keys = this.input.keyboard.addKeys({
|
||||||
firePort: Phaser.Input.Keyboard.KeyCodes.Q,
|
firePort: Phaser.Input.Keyboard.KeyCodes.Q,
|
||||||
fireStarboard: Phaser.Input.Keyboard.KeyCodes.E,
|
fireStarboard: Phaser.Input.Keyboard.KeyCodes.E,
|
||||||
@ -240,6 +246,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.scale.off('resize', this.layoutHud, this);
|
this.scale.off('resize', this.layoutHud, this);
|
||||||
registerDebugSnapshot(null);
|
registerDebugSnapshot(null);
|
||||||
window.removeEventListener('airship:helm-vector', this.handleHelmVector);
|
window.removeEventListener('airship:helm-vector', this.handleHelmVector);
|
||||||
|
window.removeEventListener('airship:action', this.handleMobileAction);
|
||||||
airshipLog('scene:shutdown', {
|
airshipLog('scene:shutdown', {
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
hull: this.playerHull,
|
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 { port, range } = nearestPort(this.world, this.player.x, this.player.y);
|
||||||
const docked = Boolean(port && range <= 230);
|
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) {
|
if (!docked) {
|
||||||
this.status.setText('GET WITHIN 230m OF A PORT TO BUY UPGRADES');
|
this.status.setText('GET WITHIN 230m OF A PORT TO BUY UPGRADES');
|
||||||
} else {
|
} 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];
|
const moduleId = this.progression.inventory?.[0];
|
||||||
if (!moduleId) {
|
if (!moduleId) {
|
||||||
this.status.setText('NO SALVAGED MODULES IN HOLD');
|
this.status.setText('NO SALVAGED MODULES IN HOLD');
|
||||||
@ -805,7 +815,9 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createMerchantTraffic() {
|
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) {
|
for (const plan of plans) {
|
||||||
const route = this.world.tradeRoutes.find(({ id }) => id === plan.routeId);
|
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 portState = nearestPort(this.world, player.x, player.y);
|
||||||
const influence = pirateInfluenceAt(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 {
|
return {
|
||||||
gameState: this.gameState,
|
gameState: this.gameState,
|
||||||
@ -1632,6 +1650,16 @@ class GameScene extends Phaser.Scene {
|
|||||||
inventoryCount: this.progression?.inventory?.length || 0,
|
inventoryCount: this.progression?.inventory?.length || 0,
|
||||||
nearestPort: portState.port?.name || null,
|
nearestPort: portState.port?.name || null,
|
||||||
nearestPortRange: portState.range,
|
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: {
|
world: {
|
||||||
seed: this.world?.seed,
|
seed: this.world?.seed,
|
||||||
|
|||||||
@ -389,6 +389,45 @@ if (!isDesktopPointer.matches) {
|
|||||||
bottom: max(8px, env(safe-area-inset-bottom));
|
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 {
|
#mobile-controls.controller-active .mobile-control.restart {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 1%;
|
right: 1%;
|
||||||
@ -499,6 +538,22 @@ if (!isDesktopPointer.matches) {
|
|||||||
right: calc(var(--steam-wide-edge-right) + var(--steam-wide-cannon-size) + 14px);
|
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 {
|
#mobile-controls.controller-active .mobile-control.restart {
|
||||||
right: max(12px, env(safe-area-inset-right));
|
right: max(12px, env(safe-area-inset-right));
|
||||||
top: clamp(118px, 30vh, 168px);
|
top: clamp(118px, 30vh, 168px);
|
||||||
@ -641,6 +696,8 @@ if (!isDesktopPointer.matches) {
|
|||||||
const fields = Object.fromEntries(
|
const fields = Object.fromEntries(
|
||||||
[...hud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]),
|
[...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');
|
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['port-reload'].style.transform = `scaleX(${clamp01(state.reload?.port ?? 1)})`;
|
||||||
fields['starboard-reload'].style.transform = `scaleX(${clamp01(state.reload?.starboard ?? 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();
|
const statusText = String(state.status || '').trim();
|
||||||
fields.status.textContent = statusText;
|
fields.status.textContent = statusText;
|
||||||
fields.status.classList.toggle('visible', Boolean(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/);
|
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 () => {
|
test('merchant traffic follows trade routes and pirates can raid it', async () => {
|
||||||
const source = await read('src/game-v4.js');
|
const source = await read('src/game-v4.js');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user