/* ========================================================= * 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; })();