Files
deepseek 8fbe70d0b0 Repterra Web — full game: base building, power grid, taming & breeding, aquatic raiders, day/night, save/load
- Isometric canvas RTS vs dinosaur waves (fan demake of Repterra)
- Economy: houses/taxes, farms, foresters, quarries; colonist staffing
- Power grid: generators extend build range; brownout + recovery
- Defense: walls/gates, watchtowers (AA), cannon towers (ground-only)
- Taming: Primal Pen + Tamers collar weakened dinos; pets obey commands
- Breeding: tamed pairs incubate eggs at the pen; hatchlings grow up
- 7 dino species incl. flying Pteranodons and lake-raiding Suchomimus
- Telegraphed waves with direction arrows; day-15 final horde; 3 difficulties
- Day/night cycle, fog of war, minimap, synth audio, 1x-3x speeds
- Save/Load/Continue + dawn autosave (full JSON state snapshots)
- Tests: 80-assertion headless suite, browser boot + E2E, balance harness
2026-08-23 07:00:23 +00:00

309 lines
11 KiB
JavaScript

/* =========================================================
* REPRTERRA WEB — input.js
* Mouse & keyboard: camera, selection, placement, commands.
* ========================================================= */
'use strict';
window.RTS = window.RTS || {};
RTS.input = (function () {
const U = RTS.util;
let cv = null;
const IN = {};
IN.ui = null; // shared UI state (set by ui.init)
IN.mouse = { sx: 0, sy: 0, wx: 0, wy: 0, down: false, mid: false, button: 0 };
IN.keys = {};
let dragStart = null; // {sx,sy} screen for rect select
let panning = false;
let panStart = null;
let lastPaintTile = null;
const C_TILE_W = RTS.CONFIG.WORLD.TILE_W, C_TILE_H = RTS.CONFIG.WORLD.TILE_H;
const TW2Z = C_TILE_W / 2, TH2Z = C_TILE_H / 2;
IN.attach = function (canvas) {
cv = canvas;
cv.addEventListener('contextmenu', e => e.preventDefault());
cv.addEventListener('mousedown', (e) => {
RTS.audio.resume();
const st = RTS.sim.state();
if (!st || st.over || RTS.main.state() !== 'playing') return;
const ui = IN.ui;
IN.mouse.sx = e.offsetX; IN.mouse.sy = e.offsetY;
updateWorldPos();
if (e.button === 1) { // middle -> pan
panning = true; panStart = { sx: e.clientX, sy: e.clientY, cx: RTS.render.cam.x, cy: RTS.render.cam.y };
e.preventDefault();
return;
}
if (e.button === 0) {
if (ui.placing) {
tryPlaceAt(true);
return;
}
dragStart = { sx: e.offsetX, sy: e.offsetY };
ui.dragRect = null;
}
});
window.addEventListener('mousemove', (e) => {
// edge scroll + hover even outside canvas
IN.mouse.cx = e.clientX; IN.mouse.cy = e.clientY;
});
cv.addEventListener('mousemove', (e) => {
IN.mouse.sx = e.offsetX; IN.mouse.sy = e.offsetY;
updateWorldPos();
const ui = IN.ui;
if (panning && panStart) {
const z = RTS.render.cam.zoom;
const dx = (e.clientX - panStart.sx);
const dy = (e.clientY - panStart.sy);
// convert screen delta to world delta (grab-the-map)
RTS.render.cam.x = panStart.cx - (dx / (TW2Z * z) + dy / (TH2Z * z)) * 0.5;
RTS.render.cam.y = panStart.cy - (dy / (TH2Z * z) - dx / (TW2Z * z)) * 0.5;
RTS.render.clampCam();
return;
}
if (ui.placing) {
ui.placing.x = Math.round(IN.mouse.wx);
ui.placing.y = Math.round(IN.mouse.wy);
if (IN.mouse.down === 'paint') tryPlaceAt(false);
return;
}
if (dragStart) {
const dx = IN.mouse.sx - dragStart.sx, dy = IN.mouse.sy - dragStart.sy;
if (dx * dx + dy * dy > 36) {
ui.dragRect = { x0: dragStart.sx, y0: dragStart.sy, x1: IN.mouse.sx, y1: IN.mouse.sy };
}
}
});
window.addEventListener('mouseup', (e) => {
const ui = IN.ui;
if (e.button === 1) { panning = false; panStart = null; }
if (e.button !== 0 || !cv) return;
IN.mouse.down = false;
if (ui.placing) { lastPaintTile = null; return; }
if (dragStart && ui.dragRect) {
// rect select units
const w0 = RTS.render.screenToWorld(ui.dragRect.x0, ui.dragRect.y0);
const w1 = RTS.render.screenToWorld(ui.dragRect.x1, ui.dragRect.y1);
const found = RTS.sim.selectUnitsInRect(w0.x, w0.y, w1.x, w1.y);
const rangers = found.filter(u => u.unitId === 'ranger');
if (rangers.length) {
ui.select({ kind: 'units', ids: rangers.map(u => u.id) });
} else {
ui.select(null);
}
ui.dragRect = null;
dragStart = null;
return;
}
if (dragStart) {
// single click select
dragStart = null;
const ent = RTS.sim.entityAt(IN.mouse.wx, IN.mouse.wy);
if (ent && ent.kind === 'unit' && ent.unitId === 'colonist') {
// colonists not individually selectable; show nothing special
ui.select(null);
} else if (ent && ent.kind === 'building') {
ui.select({ kind: 'building', id: ent.id });
} else if (ent && ent.kind === 'dino') {
ui.select({ kind: 'dino', id: ent.id });
} else if (ent && ent.kind === 'unit') {
ui.select({ kind: 'units', ids: [ent.id] });
} else {
ui.select(null);
}
}
});
cv.addEventListener('mouseup', (e) => {
const ui = IN.ui;
const st = RTS.sim.state();
if (!st || e.button !== 2) return;
if (ui.placing) { ui.setPlacing(null); return; }
updateWorldPos();
const ent = RTS.sim.entityAt(IN.mouse.wx, IN.mouse.wy);
// rally point
if (ui.sel && ui.sel.kind === 'building') {
const b = RTS.sim.getBuilding(ui.sel.id);
if (b && (b.defId === 'barracks' || b.defId === 'primalpen')) {
RTS.sim.setRally(b.id, IN.mouse.wx, IN.mouse.wy);
ui.fxPing(IN.mouse.wx, IN.mouse.wy, '#cfe8ff');
return;
}
}
// tamed pet commands
if (ui.sel && ui.sel.kind === 'dino' && RTS.sim.selectedPetCommandable(ui.sel.id)) {
if (ent && ent.kind === 'dino' && !ent.tamed) {
RTS.sim.commandPetAttack(ui.sel.id, ent.id);
ui.fxPing(ent.x, ent.y, '#ff5f4a');
} else {
RTS.sim.commandPet(ui.sel.id, IN.mouse.wx, IN.mouse.wy);
ui.fxPing(IN.mouse.wx, IN.mouse.wy, '#7fd6ff');
}
return;
}
if (ui.selUnits.length) {
// tamers: right-click a dino = prioritize its capture
const tamerIds = [];
const rangerIds = [];
for (const id of ui.selUnits) {
const u = st.units.find(v => v.id === id && !v.dead);
if (!u) continue;
if (u.unitId === 'tamer') tamerIds.push(id);
else if (u.unitId === 'ranger') rangerIds.push(id);
}
if (ent && ent.kind === 'dino' && !ent.tamed && tamerIds.length) {
RTS.sim.setCapturePriority(tamerIds, ent.id);
ui.fxPing(ent.x, ent.y, '#b06fe0');
return;
}
if (rangerIds.length || tamerIds.length) {
if (rangerIds.length) {
if (ent && ent.kind === 'dino') {
for (const id of rangerIds) {
const u = st.units.find(v => v.id === id && !v.dead);
if (u) u.targetId = ent.id;
}
ui.fxPing(ent.x, ent.y, '#ff5f4a');
} else {
RTS.sim.commandMove(rangerIds, IN.mouse.wx, IN.mouse.wy, false);
ui.fxPing(IN.mouse.wx, IN.mouse.wy, '#7fff9f');
}
} else {
// tamers only selected: send them toward the spot
for (const tid of tamerIds) {
const u = st.units.find(v => v.id === tid && !v.dead);
if (u) { u.tx = IN.mouse.wx; u.ty = IN.mouse.wy; u.path = [{ x: IN.mouse.wx, y: IN.mouse.wy }]; u.pathI = 0; }
}
ui.fxPing(IN.mouse.wx, IN.mouse.wy, '#b06fe0');
}
}
}
});
cv.addEventListener('dblclick', () => {
const ui = IN.ui;
// select all rangers & tamers on screen
const found = [];
for (const u of RTS.sim.state().units) {
if (u.dead || (u.unitId !== 'ranger' && u.unitId !== 'tamer')) continue;
const sp = RTS.render.worldToScreen(u.x, u.y);
if (sp.x >= 0 && sp.x <= cv.clientWidth && sp.y >= 0 && sp.y <= cv.clientHeight) found.push(u.id);
}
if (found.length) ui.select({ kind: 'units', ids: found });
});
cv.addEventListener('wheel', (e) => {
e.preventDefault();
const cam = RTS.render.cam;
const before = RTS.render.screenToWorld(e.offsetX, e.offsetY);
cam.zoom *= e.deltaY < 0 ? 1.12 : 1 / 1.12;
RTS.render.clampCam();
const after = RTS.render.screenToWorld(e.offsetX, e.offsetY);
cam.x += before.x - after.x;
cam.y += before.y - after.y;
RTS.render.clampCam();
}, { passive: false });
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
IN.keys[k] = true;
const ui = IN.ui;
if (RTS.main.state() !== 'playing' && k !== 'escape') {
if (k === 'enter' || k === ' ') RTS.main.hotkey(e.key);
return;
}
if (k >= '1' && k <= '9') {
const slot = parseInt(k) - 1;
const flat = RTS.ui.flatPalette();
if (flat[slot]) ui.togglePlacing(flat[slot]);
e.preventDefault();
}
switch (k) {
case 'escape':
if (ui.placing) ui.setPlacing(null);
else ui.select(null);
break;
case ' ': case 'p': RTS.main.togglePause(); e.preventDefault(); break;
case 'f': {
const q = RTS.sim.hq();
if (q) { RTS.render.cam.x = q.x; RTS.render.cam.y = q.y; }
break;
}
case 'delete': if (ui.sel && ui.sel.kind === 'building') RTS.sim.demolish(ui.sel.id); break;
case 'm': ui.toggleMute(); break;
case '+': case '=': RTS.main.bumpSpeed(1); break;
case '-': RTS.main.bumpSpeed(-1); break;
case '0': RTS.main.setSpeed(1); break;
}
});
window.addEventListener('keyup', (e) => { IN.keys[e.key.toLowerCase()] = false; });
window.addEventListener('blur', () => { IN.keys = {}; });
};
function updateWorldPos() {
const w = RTS.render.screenToWorld(IN.mouse.sx, IN.mouse.sy);
IN.mouse.wx = w.x; IN.mouse.wy = w.y;
}
function tryPlaceAt(click) {
const ui = IN.ui;
const st = RTS.sim.state();
const defId = ui.placing.defId;
const tx = Math.round(IN.mouse.wx), ty = Math.round(IN.mouse.wy);
const tileKey = tx + ',' + ty;
if (C_isLine(defId) && !click && lastPaintTile === tileKey) return; // same tile
const res = RTS.sim.place(defId, tx, ty);
if (!res.ok) {
if (click) { RTS.audio.deny(); ui.toast(res.why, 'bad'); }
return;
}
lastPaintTile = tileKey;
if (C_isLine(defId)) {
IN.mouse.down = 'paint';
} else if (!IN.keys['shift']) {
// stop placing unless shift-held (multi place)
ui.setPlacing(null);
}
}
function C_isLine(defId) { return defId === 'wall' || defId === 'gate'; }
IN.tick = function (dtReal) {
const ui = IN.ui;
if (!ui) return;
const z = RTS.render.cam.zoom;
const v = 520 / z * dtReal; // px/sec along screen axes
let ex = 0, ey = 0; // screen-space direction
if (IN.keys['w'] || IN.keys['arrowup']) ey -= 1;
if (IN.keys['s'] || IN.keys['arrowdown']) ey += 1;
if (IN.keys['a'] || IN.keys['arrowleft']) ex -= 1;
if (IN.keys['d'] || IN.keys['arrowright']) ex += 1;
// edge scroll
if (IN.mouse.cx != null && document.hasFocus()) {
const m = 22, w = window.innerWidth, h = window.innerHeight;
if (IN.mouse.cx < m) ex -= 1; else if (IN.mouse.cx > w - m) ex += 1;
if (IN.mouse.cy < m) ey -= 1; else if (IN.mouse.cy > h - m) ey += 1;
}
if (ex || ey) {
const len = Math.hypot(ex, ey); ex /= len; ey /= len;
// screen dir -> world dir
const wxd = (ex / TW2Z + ey / TH2Z) * 0.5;
const wyd = (ey / TH2Z - ex / TW2Z) * 0.5;
RTS.render.cam.x += wxd * v;
RTS.render.cam.y += wyd * v;
RTS.render.clampCam();
}
};
return IN;
})();