Replace directed path with free 2D movement, NPC spawns in random direction each leg

This commit is contained in:
justin committed 2026-05-20 21:32:50 -07:00
1 parent 427381aecf
commit 5877210cb4
2 files changed
+83 -173

No files matched your search

+83 -172
View File
@@ -253,8 +253,6 @@ class Game {
worldX: 0, worldY: 0,
npcWorldX: 0, npcWorldY: 0,
pathDir: 'right',
legProgress: 0,
legLength: 0,
lastDir: 'right',
cursor:0,
};
@@ -376,82 +374,63 @@ class Game {
};
}
_legVector() {
const d = this.state.pathDir;
return {
dx: d === 'right' ? 1 : d === 'left' ? -1 : 0,
dy: d === 'down' ? 1 : d === 'up' ? -1 : 0,
};
}
_playerScreenPos() {
const {viewW, viewH} = this._getViewport();
return { sx: viewW / 2 - 16, sy: viewH / 2 - 32 };
}
_walkTick() {
if (this.state.screen !== 'map') return;
const {viewW, viewH} = this._getViewport();
const SPEED = 8;
const pd = this.state.pathDir;
const fwd = { right:'right', left:'left', up:'up', down:'down' }[pd];
if (!this._heldKeys.has(fwd)) return;
let dx = 0, dy = 0, dir = '';
if (this._heldKeys.has('right')) { dx = SPEED; dir = 'right'; }
if (this._heldKeys.has('left')) { dx = -SPEED; dir = 'left'; }
if (this._heldKeys.has('up')) { dy = -SPEED; dir = 'up'; }
if (this._heldKeys.has('down')) { dy = SPEED; dir = 'down'; }
if (!dir) return;
const {dx, dy} = this._legVector();
const maxProg = this.state.legLength - 60;
this.state.legProgress = Math.min(maxProg, Math.max(0, this.state.legProgress + SPEED));
this.state.lastDir = pd;
this.state.worldX += dx;
this.state.worldY += dy;
this.state.lastDir = dir;
this._applyCamera();
this._placePlayer(`walk-${pd}`);
const p = document.getElementById('map-player');
const bub = document.getElementById('player-bubble');
if (p) {
const cx = viewW / 2 - 16;
const bot = viewH * 0.35;
p.style.left = cx + 'px';
p.style.bottom = bot + 'px';
p.className = `map-player walk-${dir}`;
if (bub) { bub.style.left = (cx - 4) + 'px'; bub.style.bottom = (bot + 66) + 'px'; }
}
this._checkNPCProximity();
this._drawPath();
}
_applyCamera() {
const {viewW, viewH} = this._getViewport();
const {dx, dy} = this._legVector();
const prog = this.state.legProgress;
const tx = -(prog * dx);
const ty = -(prog * dy);
const spread = Math.max(viewW, viewH) * 5;
const inner = document.getElementById('map-inner');
const tx = -(spread + this.state.worldX - viewW / 2);
const ty = -(spread + this.state.worldY - viewH / 2);
if (inner) inner.style.transform = `translate(${tx}px,${ty}px)`;
this._positionNPCOnScreen();
}
_positionNPCOnScreen() {
const {viewW, viewH} = this._getViewport();
const {dx, dy} = this._legVector();
const prog = this.state.legProgress;
const legLen = this.state.legLength;
const npcWrap = document.getElementById('map-npc-wrap');
if (npcWrap) {
const screenX = viewW / 2 + (legLen - prog) * dx - 24;
const screenY = viewH / 2 + (legLen - prog) * dy - 24;
const screenX = viewW / 2 + (this.state.npcWorldX - this.state.worldX) - 24;
const screenY = viewH / 2 + (this.state.npcWorldY - this.state.worldY) - 24;
npcWrap.style.left = screenX + 'px';
npcWrap.style.bottom = (viewH - screenY - 48) + 'px';
}
}
_placePlayer(cls) {
const {viewW, viewH} = this._getViewport();
const p = document.getElementById('map-player');
const bub = document.getElementById('player-bubble');
if (p) {
const cx = viewW / 2 - 16;
const cy = viewH / 2 - 32;
const bot = viewH - cy - 64;
p.style.left = cx + 'px';
p.style.bottom = bot + 'px';
p.className = 'map-player ' + cls;
if (bub) { bub.style.left = (cx - 4) + 'px'; bub.style.bottom = (bot + 66) + 'px'; }
}
}
_checkNPCProximity() {
const {viewW, viewH} = this._getViewport();
const remaining = this.state.legLength - this.state.legProgress;
const near = this.state.legLength > 0 && remaining < 60;
const dx = this.state.npcWorldX - this.state.worldX;
const dy = this.state.npcWorldY - this.state.worldY;
const dist = Math.sqrt(dx * dx + dy * dy);
const near = this.state.npcWorldX !== 0 && dist < 60;
const hint = document.getElementById('map-talk-hint');
const bub = document.getElementById('npc-bubble');
@@ -469,12 +448,18 @@ class Game {
if (!arrow) return;
if (nearNPC) { arrow.style.display = 'none'; arrow.style.animation = 'none'; return; }
const {viewW, viewH} = this._getViewport();
const pd = this.state.pathDir;
const dx = this.state.npcWorldX - this.state.worldX;
const dy = this.state.npcWorldY - this.state.worldY;
let pd;
if (Math.abs(dx) >= Math.abs(dy)) {
pd = dx >= 0 ? 'right' : 'left';
} else {
pd = dy >= 0 ? 'down' : 'up';
}
const glyphs = { right:'▶', left:'◀', up:'▲', down:'▼' };
arrow.textContent = glyphs[pd] || '▶';
arrow.textContent = glyphs[pd];
arrow.style.display = 'block';
arrow.style.animation = 'arrowPulse .7s ease-in-out infinite';
if (pd === 'right') {
arrow.style.removeProperty('left');
arrow.style.right = '12px';
@@ -487,7 +472,7 @@ class Game {
arrow.style.removeProperty('right');
arrow.style.left = (viewW / 2 - 16) + 'px';
arrow.style.bottom = (viewH - 48) + 'px';
} else if (pd === 'down') {
} else {
arrow.style.removeProperty('right');
arrow.style.left = (viewW / 2 - 16) + 'px';
arrow.style.bottom = '12px';
@@ -495,9 +480,10 @@ class Game {
}
_nearNPC() {
if (!this.state.legLength) return false;
const remaining = this.state.legLength - this.state.legProgress;
return remaining < 60;
if (!this.state.npcWorldX && !this.state.npcWorldY) return false;
const dx = this.state.npcWorldX - this.state.worldX;
const dy = this.state.npcWorldY - this.state.worldY;
return Math.sqrt(dx * dx + dy * dy) < 60;
}
_talkToNPC() {
@@ -521,13 +507,19 @@ class Game {
}
_placeMapSprites() {
const {viewW, viewH} = this._getViewport();
const idleFrames = { right:'-32px', down:'-32px', left:'-128px', up:'-224px' };
const p = document.getElementById('map-player');
const bub = document.getElementById('player-bubble');
if (p) {
const cx = viewW / 2 - 16;
const bot = viewH * 0.35;
p.style.left = cx + 'px';
p.style.bottom = bot + 'px';
p.className = 'map-player idle';
p.style.backgroundPositionX = idleFrames[this.state.pathDir] || '-32px';
if (bub) { bub.style.left = (cx - 4) + 'px'; bub.style.bottom = (bot + 66) + 'px'; }
}
this._placePlayer('map-player idle');
this._applyCamera();
const hint = document.getElementById('map-talk-hint');
if (hint) hint.style.display = 'none';
@@ -1017,23 +1009,23 @@ class Game {
if (!container) return;
container.innerHTML = '';
const {viewW, viewH} = this._getViewport();
const legLen = this.state.legLength || viewW * 3;
const pd = this.state.pathDir;
const isHoriz = pd === 'right' || pd === 'left';
const worldW = isHoriz ? legLen + viewW : viewW * 2;
const worldH = isHoriz ? viewH * 2 : legLen + viewH;
const spread = Math.max(viewW, viewH) * 5;
const inner = document.getElementById('map-inner');
if (inner) { inner.style.width = worldW + 'px'; inner.style.height = worldH + 'px'; }
const COUNT = Math.max(8, Math.round(legLen / 100));
for (let i = 0; i < COUNT; i++) {
if (inner) { inner.style.width = spread * 2 + 'px'; inner.style.height = spread * 2 + 'px'; }
const cx = spread;
const cy = spread;
const npcX = cx + this.state.npcWorldX;
const npcY = cy + this.state.npcWorldY;
for (let i = 0; i < 60; i++) {
const span = document.createElement('span');
span.className = 'tree';
const size = 28 + Math.random() * 16;
const t = (i + Math.random()) / COUNT;
const band = 60 + Math.random() * 80;
const side = Math.random() < 0.5 ? 1 : -1;
const x = isHoriz ? t * (legLen + viewW * 0.5) : viewW / 2 - 16 + side * band;
const y = isHoriz ? viewH / 2 - 32 + side * band : t * (legLen + viewH * 0.5);
let x, y, tries = 0;
do {
x = cx + (Math.random() - 0.5) * spread * 1.8;
y = cy + (Math.random() - 0.5) * spread * 1.8;
tries++;
} while (tries < 10 && Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80);
span.textContent = '🌳';
span.style.fontSize = size + 'px';
span.style.left = x + 'px';
@@ -1045,20 +1037,18 @@ class Game {
}
_preGenerateNextLeg() {
const {viewW, viewH} = this._getViewport();
const nextDir = this._pickNextDir(this.state.pathDir);
const isHoriz = nextDir === 'right' || nextDir === 'left';
const range = isHoriz ? viewW : viewH;
const nextLen = Math.round(range * 3.0 + Math.random() * range * 0.8);
const savedDir = this.state.pathDir;
const savedLen = this.state.legLength;
this.state.pathDir = nextDir;
this.state.legLength = nextLen;
this._generateTrees();
this.state.pathDir = savedDir;
this.state.legLength = savedLen;
this._nextLegDir = nextDir;
this._nextLegLen = nextLen;
this._nextLegDir = this._pickNextDir(this.state.pathDir);
this._nextLegLen = null;
}
_npcOffsetForDir(dir, viewW, viewH) {
const dist = dir === 'right' || dir === 'left'
? Math.round(viewW * (3.0 + Math.random() * 0.8))
: Math.round(viewH * (3.0 + Math.random() * 0.8));
return {
nx: dir === 'right' ? dist : dir === 'left' ? -dist : 0,
ny: dir === 'down' ? dist : dir === 'up' ? -dist : 0,
};
}
showMap() {
@@ -1070,23 +1060,22 @@ class Game {
if (this.state.currentQ === 0) {
this.state.pathDir = 'right';
this.state.legLength = Math.round(viewW * 3.5);
this.state.legProgress = 0;
} else if (this._nextLegDir) {
this.state.pathDir = this._nextLegDir;
this.state.legLength = this._nextLegLen;
this.state.legProgress = 0;
this._nextLegDir = null;
this._nextLegLen = null;
} else {
const dir = this._pickNextDir(this.state.pathDir);
const isH = dir === 'right' || dir === 'left';
this.state.pathDir = dir;
this.state.legLength = Math.round((isH ? viewW : viewH) * 3.0 + Math.random() * (isH ? viewW : viewH) * 0.8);
this.state.legProgress = 0;
this.state.pathDir = this._pickNextDir(this.state.pathDir);
}
this.state.worldX = 0;
this.state.worldY = 0;
this.state.lastDir = this.state.pathDir;
const {nx, ny} = this._npcOffsetForDir(this.state.pathDir, viewW, viewH);
this.state.npcWorldX = nx;
this.state.npcWorldY = ny;
this._generateTrees();
document.getElementById('hud-name').textContent = this.state.playerName||'ASH';
@@ -1098,7 +1087,6 @@ class Game {
this._placeMapSprites();
this._updateDirArrow(false);
this._drawPath();
Music.play();
saveGame(this.state);
this.flashSaveDot();
@@ -1111,83 +1099,6 @@ class Game {
});
}
_drawPath() {
const canvas = document.getElementById('map-path-canvas');
if (!canvas) return;
const {viewW, viewH} = this._getViewport();
canvas.width = viewW;
canvas.height = viewH;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, viewW, viewH);
const pd = this.state.pathDir;
const prog = this.state.legProgress;
const len = this.state.legLength;
const cx = viewW / 2;
const cy = viewH / 2;
const pathW = 80;
const R = 20;
ctx.save();
const ahead = len - prog;
let x1, y1, x2, y2;
if (pd === 'right') {
x1 = 0; y1 = cy - pathW / 2;
x2 = cx + Math.min(ahead, viewW); y2 = cy + pathW / 2;
} else if (pd === 'left') {
x1 = cx - Math.min(ahead, viewW); y1 = cy - pathW / 2;
x2 = viewW; y2 = cy + pathW / 2;
} else if (pd === 'down') {
x1 = cx - pathW / 2; y1 = 0;
x2 = cx + pathW / 2; y2 = cy + Math.min(ahead, viewH);
} else {
x1 = cx - pathW / 2; y1 = cy - Math.min(ahead, viewH);
x2 = cx + pathW / 2; y2 = viewH;
}
const rr = (x, y, w, h, r) => {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
};
const bx = Math.min(x1, x2), by = Math.min(y1, y2);
const bw = Math.abs(x2 - x1), bh = Math.abs(y2 - y1);
ctx.shadowColor = 'rgba(0,0,0,0.25)';
ctx.shadowBlur = 6;
ctx.fillStyle = '#b89858';
rr(bx, by, bw, bh, R);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#2d6e1a';
ctx.lineWidth = 3;
rr(bx, by, bw, bh, R);
ctx.stroke();
const dashes = pd === 'right' || pd === 'left' ? bh * 0.1 : bw * 0.1;
ctx.setLineDash([12, 10]);
ctx.strokeStyle = 'rgba(168,136,72,0.6)';
ctx.lineWidth = 2;
if (pd === 'right' || pd === 'left') {
ctx.beginPath(); ctx.moveTo(bx, cy); ctx.lineTo(bx + bw, cy); ctx.stroke();
} else {
ctx.beginPath(); ctx.moveTo(cx, by); ctx.lineTo(cx, by + bh); ctx.stroke();
}
ctx.setLineDash([]);
ctx.restore();
}
updateHUD() {
const tot=this.state.questions.length||100, done=this.state.currentQ, pct=(done/tot)*100;
-1
View File
@@ -99,7 +99,6 @@
</div>
</div>
<div class="overworld-bg map-world" id="map-world">
<canvas id="map-path-canvas" style="position:absolute;inset:0;width:100%;height:100%;z-index:4;pointer-events:none;"></canvas>
<div class="map-inner" id="map-inner">
<div class="map-trees" id="map-trees"></div>
</div>