Files
battleship/data/target_interaction.js

93 lines
3.8 KiB
JavaScript

(() => {
function sameTarget(left, right) { return left?.x === right?.x && left?.y === right?.y; }
function createTargetActivator({ canFire, select, fire, thresholdMs = 360, now = () => Date.now() }) {
let lastTap;
let firing = false;
async function fireOnce(target) {
if (firing || !canFire(target)) return false;
firing = true;
lastTap = undefined;
try {
await fire(target);
return true;
} finally {
firing = false;
}
}
function tap(target) {
if (firing || !canFire(target)) return { action: 'ignored' };
const timestampMs = now();
if (lastTap && sameTarget(lastTap.target, target) && timestampMs - lastTap.timestampMs <= thresholdMs) {
return { action: 'fire', promise: fireOnce(target) };
}
select(target);
lastTap = { target, timestampMs };
return { action: 'select' };
}
function keyboard(target) {
if (firing || !canFire(target)) return { action: 'ignored' };
select(target);
lastTap = undefined;
return { action: 'select' };
}
return {
tap,
keyboard,
doubleActivate: (target) => ({ action: 'fire', promise: fireOnce(target) }),
isFiring: () => firing,
};
}
function detectFeedback(previous, next, viewer) {
if (!previous || !next || previous.gameId !== next.gameId || next.version <= previous.version) return undefined;
const player = viewer === 'player1' ? 0 : viewer === 'player2' ? 1 : -1;
if (previous.phase !== next.phase && next.phase === 'finished') {
if (player < 0) return { type: 'finish' };
return { type: next.winner === player ? 'victory' : 'defeat' };
}
if (previous.phase !== 'in_progress' && next.phase === 'in_progress') return { type: 'start' };
if (!Array.isArray(previous.boards) || !Array.isArray(next.boards)) return undefined;
let best;
for (let board = 0; board < 2; board += 1) {
if (typeof previous.boards[board] !== 'string' || typeof next.boards[board] !== 'string') continue;
for (let cell = 0; cell < 100; cell += 1) {
const before = previous.boards[board][cell];
const after = next.boards[board][cell];
if (before === after) continue;
const perspective = player < 0 ? 'watch' : board === player ? 'damage' : 'attack';
if (after === '4') best = { type: perspective === 'damage' ? 'sunk-damage' : perspective === 'attack' ? 'sunk' : 'watch-sunk' };
else if (!best && after === '3') best = { type: perspective === 'damage' ? 'damage' : perspective === 'attack' ? 'hit' : 'watch-hit' };
else if (!best && after === '2') best = { type: perspective === 'attack' ? 'miss' : perspective === 'damage' ? 'dodged' : 'watch-miss' };
}
}
if (best) return best;
if (player >= 0 && previous.turn !== next.turn && next.turn === viewer) return { type: 'turn' };
return undefined;
}
function createReactionPicker(catalog, random = Math.random) {
const previousByType = new Map();
return type => {
const variants = catalog[type];
if (!Array.isArray(variants) || variants.length === 0) return undefined;
if (variants.length === 1) return variants[0];
const previous = previousByType.get(type);
const candidateCount = previous === undefined ? variants.length : variants.length - 1;
let index = Math.min(candidateCount - 1, Math.max(0, Math.floor(random() * candidateCount)));
if (previous !== undefined && index >= previous) index += 1;
previousByType.set(type, index);
return variants[index];
};
}
const api = { createTargetActivator, detectFeedback, createReactionPicker };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else globalThis.BattleshipTargetInteraction = api;
})();