2026-06-10 14:26:58 +02:00
|
|
|
// App shell: screens, character select, local/host/client match drivers.
|
|
|
|
|
import { SNAP_EVERY } from './consts.js';
|
|
|
|
|
import { randomSeed } from './rng.js';
|
|
|
|
|
import { CHARS } from './characters.js';
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
import * as simSide from './sim.js';
|
|
|
|
|
import * as simArena from './sim_arena.js';
|
2026-06-10 14:26:58 +02:00
|
|
|
import { initInput, getBits, getMergedBits } from './input.js';
|
|
|
|
|
import { initRender, resetRender, render, fxEvents } from './render.js';
|
|
|
|
|
import { initAudio, playEvents, sfx, toggleMute } from './audio.js';
|
|
|
|
|
import { randomKey, hostRoom, joinRoom, peerAvailable } from './net.js';
|
|
|
|
|
|
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
|
|
|
|
|
|
// ---- state ----
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
const SIMS = { side: simSide, arena: simArena }; // the two game modes
|
|
|
|
|
let gameMode = 'side'; // 'side' (parkour) | 'arena' (top-down dogpit)
|
|
|
|
|
let matchSim = simSide; // sim module driving the current game object
|
2026-06-10 14:26:58 +02:00
|
|
|
let mode = null; // 'local' | 'host' | 'client'
|
|
|
|
|
let net = null;
|
|
|
|
|
let game = null;
|
|
|
|
|
let picks = [null, null]; // local char select
|
|
|
|
|
let myPick = null, remotePick = null, peerJoined = false;
|
|
|
|
|
let remoteBits = 0;
|
|
|
|
|
let pendingEvs = [];
|
|
|
|
|
let clientOverT = 0;
|
|
|
|
|
let resultShown = false;
|
|
|
|
|
let paused = false;
|
|
|
|
|
let rafId = null, acc = 0, last = 0;
|
|
|
|
|
const TICK_MS = 1000 / 60;
|
|
|
|
|
|
|
|
|
|
// ---- screens ----
|
|
|
|
|
const SCREENS = ['s-menu', 's-host', 's-join', 's-chars', 's-game', 's-result'];
|
|
|
|
|
function show(id) {
|
|
|
|
|
for (const s of SCREENS) $(s).classList.toggle('hidden', s !== id);
|
|
|
|
|
$('cv').classList.toggle('dim', id !== 's-game');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setStatus(el, msg, isErr = false) {
|
|
|
|
|
el.textContent = msg;
|
|
|
|
|
el.classList.toggle('err', isErr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- boot ----
|
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
initInput();
|
|
|
|
|
initRender($('cv'));
|
|
|
|
|
buildCharCards();
|
|
|
|
|
wireMenus();
|
|
|
|
|
show('s-menu');
|
|
|
|
|
// background demo map behind menus
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
matchSim = simSide;
|
|
|
|
|
game = simSide.createGame(randomSeed(), [{ charId: 0 }, { charId: 1 }]);
|
2026-06-10 14:26:58 +02:00
|
|
|
resetRender(game);
|
|
|
|
|
startLoop();
|
|
|
|
|
});
|
|
|
|
|
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
window.addEventListener('pointerdown', () => initAudio()); // any gesture unlocks audio
|
2026-06-10 14:26:58 +02:00
|
|
|
window.addEventListener('keydown', (e) => {
|
|
|
|
|
initAudio(); // browsers require a gesture before audio
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
if (e.target && e.target.tagName === 'INPUT') return; // don't eat typing in the room-key field
|
2026-06-10 14:26:58 +02:00
|
|
|
if (e.code === 'KeyM') {
|
|
|
|
|
const m = toggleMute();
|
|
|
|
|
$('mute').textContent = m ? '🔇' : '🔊';
|
|
|
|
|
}
|
|
|
|
|
if (e.code === 'Escape') onEscape();
|
|
|
|
|
if (e.code === 'Enter' && !$('s-result').classList.contains('hidden')) doRematch();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function onEscape() {
|
|
|
|
|
if (!$('s-game').classList.contains('hidden')) {
|
|
|
|
|
if (mode === 'local') {
|
|
|
|
|
paused = !paused;
|
|
|
|
|
$('pause-tag').classList.toggle('hidden', !paused);
|
|
|
|
|
} else {
|
|
|
|
|
leaveToMenu('Left the match.');
|
|
|
|
|
}
|
|
|
|
|
} else if ($('s-menu').classList.contains('hidden')) {
|
|
|
|
|
leaveToMenu();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function leaveToMenu(msg) {
|
|
|
|
|
if (net) { try { net.send({ t: 'bye' }); net.destroy(); } catch (_) {} net = null; }
|
|
|
|
|
mode = null; paused = false; resultShown = false;
|
|
|
|
|
myPick = null; remotePick = null; peerJoined = false; picks = [null, null];
|
|
|
|
|
$('pause-tag').classList.add('hidden');
|
|
|
|
|
if (msg) setStatus($('menu-status'), msg);
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
matchSim = simSide;
|
|
|
|
|
game = simSide.createGame(randomSeed(), [{ charId: 0 }, { charId: 1 }]);
|
2026-06-10 14:26:58 +02:00
|
|
|
resetRender(game);
|
|
|
|
|
show('s-menu');
|
|
|
|
|
refreshCharUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- menus ----
|
|
|
|
|
function wireMenus() {
|
|
|
|
|
$('btn-local').onclick = () => { sfx('ui'); mode = 'local'; picks = [null, null]; show('s-chars'); refreshCharUI(); };
|
|
|
|
|
$('btn-host').onclick = () => { sfx('ui'); startHost(); };
|
|
|
|
|
$('btn-join').onclick = () => { sfx('ui'); show('s-join'); setStatus($('join-status'), ''); $('join-key').focus(); };
|
|
|
|
|
$('btn-join-go').onclick = () => doJoin();
|
|
|
|
|
$('join-key').addEventListener('keydown', (e) => { if (e.key === 'Enter') doJoin(); });
|
|
|
|
|
$('btn-host-cancel').onclick = () => leaveToMenu();
|
|
|
|
|
$('btn-join-cancel').onclick = () => leaveToMenu();
|
|
|
|
|
$('btn-chars-back').onclick = () => leaveToMenu();
|
|
|
|
|
$('btn-start').onclick = () => tryStart();
|
|
|
|
|
$('btn-rematch').onclick = () => doRematch();
|
|
|
|
|
$('btn-newdogs').onclick = () => doChangeDogs();
|
|
|
|
|
$('btn-result-menu').onclick = () => leaveToMenu();
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
$('mode-side').onclick = () => setGameMode('side');
|
|
|
|
|
$('mode-arena').onclick = () => setGameMode('arena');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setGameMode(m) {
|
|
|
|
|
if (mode === 'client') return; // host picks the arena style
|
|
|
|
|
if (gameMode === m) return;
|
|
|
|
|
sfx('ui');
|
|
|
|
|
gameMode = m;
|
|
|
|
|
refreshModeUI();
|
|
|
|
|
if (mode === 'host' && net) net.send({ t: 'mode', m });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function refreshModeUI() {
|
|
|
|
|
$('mode-side').classList.toggle('sel', gameMode === 'side');
|
|
|
|
|
$('mode-arena').classList.toggle('sel', gameMode === 'arena');
|
|
|
|
|
const locked = mode === 'client';
|
|
|
|
|
$('mode-side').disabled = locked;
|
|
|
|
|
$('mode-arena').disabled = locked;
|
|
|
|
|
$('mode-hint').textContent =
|
|
|
|
|
locked ? 'The host picks the arena style.'
|
|
|
|
|
: gameMode === 'side' ? 'Side-view platforming: wall-jumps, blast zones, gravity.'
|
|
|
|
|
: 'Top-down dogpit: 8-way movement, hop (Space / ,) over pits and saws — knock dogs into the void!';
|
2026-06-10 14:26:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- char select ----
|
|
|
|
|
function buildCharCards() {
|
|
|
|
|
const grid = $('char-grid');
|
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
for (const c of CHARS) {
|
|
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = 'card';
|
|
|
|
|
card.id = `card-${c.id}`;
|
|
|
|
|
card.style.borderColor = c.color;
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
<div class="card-head" style="color:${c.color}">${c.name}</div>
|
|
|
|
|
<div class="card-breed">${c.breed}</div>
|
|
|
|
|
<canvas class="card-cv" width="90" height="56"></canvas>
|
|
|
|
|
<div class="card-desc">${c.desc}</div>
|
|
|
|
|
<ul class="card-moves">${c.moves.map(m => `<li>${m}</li>`).join('')}</ul>
|
|
|
|
|
<div class="card-tags"></div>`;
|
|
|
|
|
card.onclick = () => pickChar(c.id);
|
|
|
|
|
grid.appendChild(card);
|
|
|
|
|
drawCardDog(card.querySelector('.card-cv'), c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawCardDog(cv2, c) {
|
|
|
|
|
const x = cv2.getContext('2d');
|
|
|
|
|
x.translate(45, 32);
|
|
|
|
|
x.fillStyle = c.color;
|
|
|
|
|
x.beginPath(); x.ellipse(0, 0, 22 * c.body, 13 * c.body, 0, 0, 7); x.fill();
|
|
|
|
|
x.beginPath(); x.arc(17, -12, 11 * c.body, 0, 7); x.fill();
|
|
|
|
|
x.beginPath(); x.ellipse(17 + 9, -10, 9 * c.snout, 5, 0, 0, 7); x.fill();
|
|
|
|
|
x.fillStyle = c.accent;
|
|
|
|
|
x.beginPath(); x.moveTo(13, -20); x.lineTo(16, -30); x.lineTo(20, -19); x.fill();
|
|
|
|
|
x.fillStyle = '#1a1a1a';
|
|
|
|
|
x.beginPath(); x.arc(19, -14, 2.5, 0, 7); x.fill();
|
|
|
|
|
x.strokeStyle = c.accent; x.lineWidth = 4; x.lineCap = 'round';
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
x.beginPath(); x.moveTo(-14 + i * 9, 10); x.lineTo(-14 + i * 9, 20); x.stroke();
|
|
|
|
|
}
|
|
|
|
|
x.lineWidth = 3;
|
|
|
|
|
x.beginPath(); x.moveTo(-21, -4); x.quadraticCurveTo(-32, -14, -34, -8); x.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pickChar(id) {
|
|
|
|
|
sfx('ui');
|
|
|
|
|
if (mode === 'local') {
|
|
|
|
|
if (picks[0] === null) picks[0] = id;
|
|
|
|
|
else if (picks[1] === null) picks[1] = id;
|
|
|
|
|
else { picks = [id, null]; } // third click restarts picking
|
|
|
|
|
} else {
|
|
|
|
|
myPick = id;
|
|
|
|
|
if (net) net.send({ t: 'char', c: id });
|
|
|
|
|
}
|
|
|
|
|
refreshCharUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function refreshCharUI() {
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
refreshModeUI();
|
2026-06-10 14:26:58 +02:00
|
|
|
for (const c of CHARS) {
|
|
|
|
|
const tags = $(`card-${c.id}`)?.querySelector('.card-tags');
|
|
|
|
|
if (!tags) continue;
|
|
|
|
|
const t = [];
|
|
|
|
|
if (mode === 'local') {
|
|
|
|
|
if (picks[0] === c.id) t.push('<span class="tag p1">P1</span>');
|
|
|
|
|
if (picks[1] === c.id) t.push('<span class="tag p2">P2</span>');
|
|
|
|
|
} else {
|
|
|
|
|
if (myPick === c.id) t.push('<span class="tag p1">YOU</span>');
|
|
|
|
|
if (remotePick === c.id) t.push('<span class="tag p2">THEM</span>');
|
|
|
|
|
}
|
|
|
|
|
tags.innerHTML = t.join('');
|
|
|
|
|
}
|
|
|
|
|
const status = $('chars-status');
|
|
|
|
|
const startBtn = $('btn-start');
|
|
|
|
|
if (mode === 'local') {
|
|
|
|
|
startBtn.classList.remove('hidden');
|
|
|
|
|
startBtn.disabled = picks[0] === null || picks[1] === null;
|
|
|
|
|
setStatus(status, picks[0] === null ? 'Player 1: pick your dog'
|
|
|
|
|
: picks[1] === null ? 'Player 2: pick your dog' : 'Ready! Hit Start.');
|
|
|
|
|
} else if (mode === 'host') {
|
|
|
|
|
startBtn.classList.remove('hidden');
|
|
|
|
|
startBtn.disabled = myPick === null || remotePick === null;
|
|
|
|
|
const key = $('room-key-tag');
|
|
|
|
|
key.classList.remove('hidden');
|
|
|
|
|
setStatus(status, !peerJoined ? 'Waiting for player 2 to join…'
|
|
|
|
|
: myPick === null ? 'Pick your dog'
|
|
|
|
|
: remotePick === null ? 'Waiting for them to pick…' : 'Both ready — Start!');
|
|
|
|
|
} else if (mode === 'client') {
|
|
|
|
|
startBtn.classList.add('hidden');
|
|
|
|
|
setStatus(status, myPick === null ? 'Pick your dog' : 'Waiting for host to start…');
|
|
|
|
|
}
|
|
|
|
|
$('room-key-tag').classList.toggle('hidden', mode !== 'host');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tryStart() {
|
|
|
|
|
if (mode === 'local') {
|
|
|
|
|
if (picks[0] === null || picks[1] === null) return;
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
startMatch(randomSeed(), [{ charId: picks[0] }, { charId: picks[1] }], gameMode);
|
2026-06-10 14:26:58 +02:00
|
|
|
} else if (mode === 'host') {
|
|
|
|
|
if (myPick === null || remotePick === null) return;
|
|
|
|
|
const seed = randomSeed();
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
net.send({ t: 'start', seed, chars: [myPick, remotePick], mode: gameMode });
|
|
|
|
|
startMatch(seed, [{ charId: myPick }, { charId: remotePick }], gameMode);
|
2026-06-10 14:26:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- networking ----
|
|
|
|
|
function startHost() {
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
if (net) return; // a connection attempt is already in flight
|
2026-06-10 14:26:58 +02:00
|
|
|
if (!peerAvailable()) { setStatus($('menu-status'), 'PeerJS not loaded — online play needs internet.', true); return; }
|
|
|
|
|
mode = 'host';
|
|
|
|
|
myPick = null; remotePick = null; peerJoined = false;
|
|
|
|
|
const key = randomKey();
|
|
|
|
|
show('s-host');
|
|
|
|
|
setStatus($('host-status'), 'Opening room…');
|
|
|
|
|
$('host-key').textContent = key;
|
|
|
|
|
$('room-key').textContent = key;
|
|
|
|
|
net = hostRoom(key, {
|
|
|
|
|
open: () => setStatus($('host-status'), 'Room open! Share the key — waiting for player 2…'),
|
|
|
|
|
conn: () => {
|
|
|
|
|
peerJoined = true;
|
|
|
|
|
net.send({ t: 'welcome' });
|
|
|
|
|
show('s-chars');
|
|
|
|
|
refreshCharUI();
|
|
|
|
|
},
|
|
|
|
|
data: onNetData,
|
|
|
|
|
close: (msg) => netClosed(msg),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doJoin() {
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
if (net) return; // a connection attempt is already in flight
|
2026-06-10 14:26:58 +02:00
|
|
|
if (!peerAvailable()) { setStatus($('join-status'), 'PeerJS not loaded — online play needs internet.', true); return; }
|
|
|
|
|
const key = $('join-key').value.toUpperCase().trim();
|
|
|
|
|
if (key.length < 4) { setStatus($('join-status'), 'Enter the 5-letter room key.', true); return; }
|
|
|
|
|
mode = 'client';
|
|
|
|
|
myPick = null; remotePick = null;
|
|
|
|
|
setStatus($('join-status'), 'Connecting…');
|
|
|
|
|
net = joinRoom(key, {
|
|
|
|
|
conn: () => { show('s-chars'); refreshCharUI(); },
|
|
|
|
|
data: onNetData,
|
|
|
|
|
close: (msg) => netClosed(msg),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function netClosed(msg) {
|
|
|
|
|
if (mode === 'host' || mode === 'client') leaveToMenu(msg || 'Connection closed.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onNetData(msg) {
|
|
|
|
|
if (!msg || typeof msg !== 'object') return;
|
|
|
|
|
switch (msg.t) {
|
|
|
|
|
case 'char':
|
|
|
|
|
remotePick = msg.c;
|
|
|
|
|
refreshCharUI();
|
|
|
|
|
break;
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
case 'mode': // client: host flipped the arena style
|
|
|
|
|
if (mode === 'client') { gameMode = msg.m === 'arena' ? 'arena' : 'side'; refreshModeUI(); }
|
|
|
|
|
break;
|
2026-06-10 14:26:58 +02:00
|
|
|
case 'start': // client: host started the match
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
if (mode === 'client') startMatch(msg.seed, [{ charId: msg.chars[0] }, { charId: msg.chars[1] }], msg.mode || 'side');
|
2026-06-10 14:26:58 +02:00
|
|
|
break;
|
|
|
|
|
case 'i': // host: remote inputs
|
|
|
|
|
remoteBits = msg.b | 0;
|
|
|
|
|
break;
|
|
|
|
|
case 's': // client: snapshot
|
|
|
|
|
if (mode === 'client' && game) {
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
const evs = matchSim.applySnapshot(game, msg.s);
|
2026-06-10 14:26:58 +02:00
|
|
|
fxEvents(evs, game);
|
|
|
|
|
playEvents(evs);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'rematch':
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
if (mode === 'client') startMatch(msg.seed, [{ charId: msg.chars[0] }, { charId: msg.chars[1] }], msg.mode || 'side');
|
2026-06-10 14:26:58 +02:00
|
|
|
break;
|
|
|
|
|
case 'tochars':
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
if (mode === 'client') { myPick = null; remotePick = null; resultShown = false; show('s-chars'); refreshCharUI(); }
|
2026-06-10 14:26:58 +02:00
|
|
|
break;
|
|
|
|
|
case 'bye':
|
|
|
|
|
netClosed('Other player left.');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- match lifecycle ----
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
function startMatch(seed, defs, modeKey = 'side') {
|
|
|
|
|
gameMode = modeKey;
|
|
|
|
|
matchSim = SIMS[modeKey] || simSide;
|
|
|
|
|
game = matchSim.createGame(seed, defs);
|
2026-06-10 14:26:58 +02:00
|
|
|
resetRender(game);
|
|
|
|
|
pendingEvs = [];
|
|
|
|
|
remoteBits = 0;
|
|
|
|
|
clientOverT = 0;
|
|
|
|
|
resultShown = false;
|
|
|
|
|
paused = false;
|
|
|
|
|
acc = 0;
|
|
|
|
|
show('s-game');
|
|
|
|
|
sfx('count');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doRematch() {
|
|
|
|
|
sfx('ui');
|
|
|
|
|
if (mode === 'local') {
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
startMatch(randomSeed(), [{ charId: picks[0] }, { charId: picks[1] }], gameMode);
|
2026-06-10 14:26:58 +02:00
|
|
|
} else if (mode === 'host') {
|
|
|
|
|
const seed = randomSeed();
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
net.send({ t: 'rematch', seed, chars: [myPick, remotePick], mode: gameMode });
|
|
|
|
|
startMatch(seed, [{ charId: myPick }, { charId: remotePick }], gameMode);
|
2026-06-10 14:26:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doChangeDogs() {
|
|
|
|
|
sfx('ui');
|
fix: address all 21 confirmed findings from multi-agent review + live P2P testing
Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal
velocity, grapple constraint no longer drags players into terrain, bite
window ends on getting hit, shield/boost cleared on death, spawn invuln
preserved through countdown, spike/saw events only on real damage.
Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds),
saw paths rejected if they grind terrain or sweep spawns, towers no longer
hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error
tolerance once P2P link is up, join/host reentrancy guards. UI: text-field
keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline
leak, snapshot carries onGround/landT so client dogs animate.
2026-06-10 14:47:40 +02:00
|
|
|
resultShown = false; // lets the host tick() guard idle the finished match
|
2026-06-10 14:26:58 +02:00
|
|
|
if (mode === 'local') {
|
|
|
|
|
picks = [null, null];
|
|
|
|
|
show('s-chars'); refreshCharUI();
|
|
|
|
|
} else if (mode === 'host') {
|
|
|
|
|
net.send({ t: 'tochars' });
|
|
|
|
|
myPick = null; remotePick = null;
|
|
|
|
|
show('s-chars'); refreshCharUI();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showResult() {
|
|
|
|
|
resultShown = true;
|
|
|
|
|
const w = game.over.winner;
|
|
|
|
|
$('result-title').textContent = w >= 0 ? `${game.players[w].name} wins!` : 'Double KO!';
|
|
|
|
|
$('result-title').style.color = w >= 0 ? CHARS[game.players[w].charId].color : '#fff';
|
|
|
|
|
const isClient = mode === 'client';
|
|
|
|
|
$('btn-rematch').classList.toggle('hidden', isClient);
|
|
|
|
|
$('btn-newdogs').classList.toggle('hidden', isClient);
|
|
|
|
|
$('result-wait').classList.toggle('hidden', !isClient);
|
|
|
|
|
show('s-result');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- main loop ----
|
|
|
|
|
function startLoop() {
|
|
|
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
|
|
|
last = performance.now();
|
|
|
|
|
const frame = (t) => {
|
|
|
|
|
rafId = requestAnimationFrame(frame);
|
|
|
|
|
acc += t - last;
|
|
|
|
|
last = t;
|
|
|
|
|
if (acc > 200) acc = 200; // tab was backgrounded; don't spiral
|
|
|
|
|
while (acc >= TICK_MS) {
|
|
|
|
|
acc -= TICK_MS;
|
|
|
|
|
tick();
|
|
|
|
|
}
|
|
|
|
|
if (game) render(game, { smooth: mode === 'client' });
|
|
|
|
|
};
|
|
|
|
|
rafId = requestAnimationFrame(frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tick() {
|
|
|
|
|
if (!game || paused) return;
|
|
|
|
|
const inGame = !$('s-game').classList.contains('hidden');
|
|
|
|
|
|
|
|
|
|
if (mode === 'local' || mode === null) {
|
|
|
|
|
// local match, or attract-mode demo behind menus (no inputs)
|
|
|
|
|
const i0 = inGame ? getBits(0) : 0;
|
|
|
|
|
const i1 = inGame ? getBits(1) : 0;
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
const evs = matchSim.step(game, [i0, i1]);
|
2026-06-10 14:26:58 +02:00
|
|
|
if (inGame) { fxEvents(evs, game); playEvents(evs); }
|
|
|
|
|
if (inGame && game.over && game.overT > 140 && !resultShown) showResult();
|
|
|
|
|
|
|
|
|
|
} else if (mode === 'host') {
|
|
|
|
|
if (!inGame && !resultShown) return; // still in lobby/charselect
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
const evs = matchSim.step(game, [getMergedBits(), remoteBits]);
|
2026-06-10 14:26:58 +02:00
|
|
|
pendingEvs.push(...evs);
|
|
|
|
|
fxEvents(evs, game);
|
|
|
|
|
playEvents(evs);
|
|
|
|
|
if (game.tick % SNAP_EVERY === 0 && net) {
|
feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
pads, mirrored saw pairs; fallback pit pair since pits are the only KO
mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
side view); mode toggle UI + mode carried in start/rematch net messages
Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00
|
|
|
net.send({ t: 's', s: matchSim.snapshot(game, pendingEvs) });
|
2026-06-10 14:26:58 +02:00
|
|
|
pendingEvs = [];
|
|
|
|
|
}
|
|
|
|
|
if (game.over && game.overT > 140 && !resultShown) showResult();
|
|
|
|
|
|
|
|
|
|
} else if (mode === 'client') {
|
|
|
|
|
if (!inGame && !resultShown) return;
|
|
|
|
|
if (net) net.send({ t: 'i', b: getMergedBits() });
|
|
|
|
|
// sim runs on host; we just count toward the result screen
|
|
|
|
|
if (game.over && ++clientOverT > 140 && !resultShown) showResult();
|
|
|
|
|
}
|
|
|
|
|
}
|