93 lines
3.8 KiB
JavaScript
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;
|
|
})();
|