Skip to content

Commit fd5ce07

Browse files
committed
Merge branch 'codex/feedback-polish' into codex/daily-engagement
2 parents 67dc24b + 48c8d19 commit fd5ce07

2 files changed

Lines changed: 90 additions & 26 deletions

File tree

index.html

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
#title-bar .dot{width:8px;height:8px;background:#D4A030;border-radius:50%;animation:blink 1.6s ease-in-out infinite}
1919
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
2020
#hint{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);font-family:'Silkscreen',monospace;font-size:11px;color:#D4A030;opacity:0.6;pointer-events:none;z-index:10;text-shadow:1px 1px 0px #6B4820;animation:fadeHint 4s ease-in-out infinite}
21+
#hint.status-hidden{visibility:hidden}
2122
@keyframes fadeHint{0%,100%{opacity:0.6}50%{opacity:0.2}}
2223
#controls{position:absolute;bottom:20px;left:20px;pointer-events:none;z-index:10;font-family:'Silkscreen',monospace;font-size:9px;color:#8A9870;line-height:1.8;text-shadow:1px 1px 0px #1a1510;opacity:0.7}
2324
#controls .key{display:inline-block;background:rgba(90,136,72,0.15);border:1px solid rgba(90,136,72,0.3);border-radius:3px;padding:1px 5px;font-family:'Press Start 2P',monospace;font-size:7px;color:#A8C088;margin-right:4px;vertical-align:middle}
24-
#coords{position:absolute;bottom:24px;right:24px;font-family:'Press Start 2P',monospace;font-size:8px;color:#5A8848;pointer-events:none;z-index:10}
25+
#coords{position:absolute;bottom:24px;right:76px;font-family:'Press Start 2P',monospace;font-size:8px;color:#5A8848;pointer-events:none;z-index:10}
2526
#emote{position:absolute;pointer-events:none;z-index:15;font-size:24px;transition:transform 0.15s ease-out,opacity 0.15s;transform:scale(0);opacity:0}
2627
#emote.show{transform:scale(1);opacity:1}
2728
#action-button{position:absolute;right:max(20px,env(safe-area-inset-right));bottom:max(20px,env(safe-area-inset-bottom));z-index:20;display:none;min-width:72px;height:52px;padding:0 12px;border:2px solid #D4A030;border-radius:4px;background:rgba(10,10,10,0.88);color:#E8C040;font:700 12px 'Silkscreen',monospace;cursor:pointer;box-shadow:0 0 18px rgba(212,160,48,0.2)}
@@ -64,6 +65,9 @@
6465
#sound-button{right:max(104px,calc(104px + env(safe-area-inset-right)));bottom:max(28px,calc(28px + env(safe-area-inset-bottom)))}
6566
#status{bottom:max(84px,calc(64px + env(safe-area-inset-bottom)));font-size:10px}
6667
}
68+
@media(hover:none),(pointer:coarse){
69+
#action-button{display:block}
70+
}
6771
@media(prefers-reduced-motion:reduce){
6872
#title-bar .dot,#hint{animation:none}
6973
#emote,#status{transition:none}
@@ -260,9 +264,10 @@ <h2 id="journal-title">SIGNAL LOG</h2>
260264
const tx = player.tileX + d.dx, ty = player.tileY + d.dy;
261265
player.facing = dir;
262266

263-
// Check NPC on target tile
267+
// Interaction targets reserve their tile so the player stops facing them.
264268
const npcOnTile = isTileOccupiedByNPC(tx, ty, null);
265-
if (npcOnTile || !isWalkable(tx, ty)) {
269+
const fragmentOnTile = getUncollectedFragmentAt(tx, ty);
270+
if (npcOnTile || fragmentOnTile || !isWalkable(tx, ty)) {
266271
// BLOCKED
267272
player.state = 'BLOCKED';
268273
player.bumpDir = dir;
@@ -858,7 +863,8 @@ <h2 id="journal-title">SIGNAL LOG</h2>
858863
// PATHFINDING
859864
// ══════════════════════════════════════════════════════
860865
function findPath(sx,sy,ex,ey){
861-
return findWorldPath(sx, sy, ex, ey, isWalkable, { allowNearest: false });
866+
const isPlayerWalkable = (x, y) => isWalkable(x, y) && !getUncollectedFragmentAt(x, y);
867+
return findWorldPath(sx, sy, ex, ey, isPlayerWalkable, { allowNearest: false });
862868
}
863869

864870
// ══════════════════════════════════════════════════════
@@ -879,6 +885,12 @@ <h2 id="journal-title">SIGNAL LOG</h2>
879885

880886
// Check if clicking adjacent NPC while idle
881887
if (player.state === 'IDLE') {
888+
const fragment = getUncollectedFragmentAt(tileX, tileY);
889+
if (fragment && Math.abs(tileX - player.tileX) + Math.abs(tileY - player.tileY) === 1) {
890+
player.facing = dirToward(player.tileX, player.tileY, tileX, tileY);
891+
collectFragment(fragment);
892+
return;
893+
}
882894
const adj = isTileOccupiedByNPC(tileX, tileY, null);
883895
if (adj && Math.abs(tileX - player.tileX) + Math.abs(tileY - player.tileY) === 1) {
884896
// Trigger interaction
@@ -909,7 +921,13 @@ <h2 id="journal-title">SIGNAL LOG</h2>
909921

910922
// ── WASD Keyboard Movement ──
911923
const keysDown = {};
924+
function shouldHandleGameKey(target) {
925+
return !target || typeof target.closest !== 'function' ||
926+
!target.closest('button, dialog, input, select, textarea, a[href]');
927+
}
928+
912929
window.addEventListener('keydown', (e) => {
930+
if (!shouldHandleGameKey(e.target)) return;
913931
const k = e.key.toLowerCase();
914932
if (['w','a','s','d','arrowup','arrowdown','arrowleft','arrowright',' ','enter'].includes(k)) e.preventDefault();
915933
keysDown[k] = true;
@@ -948,6 +966,12 @@ <h2 id="journal-title">SIGNAL LOG</h2>
948966
)) || null;
949967
}
950968

969+
function getUncollectedFragmentAt(x, y) {
970+
return signalFragments.find(fragment => (
971+
fragment.discovered && !fragment.collected && fragment.x === x && fragment.y === y
972+
)) || null;
973+
}
974+
951975
function updateQuestUI() {
952976
const collected = signalFragments.filter(fragment => fragment.collected).length;
953977
questEl.textContent = quest.complete ? `FACTORY ONLINE · ${dailyChallenge.code}` : `SIGNALS ${collected}/${signalFragments.length} · ${dailyChallenge.code}`;
@@ -1085,6 +1109,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
10851109
function showStatus(message, duration = 1.5) {
10861110
statusEl.textContent = message;
10871111
statusEl.classList.add('show');
1112+
hintEl.classList.add('status-hidden');
10881113
statusTimer = duration;
10891114
}
10901115

@@ -1115,8 +1140,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11151140
}
11161141
scanPulse = {
11171142
x: player.tileX, y: player.tileY,
1118-
time: Date.now(),
1119-
radius: 0,
1143+
elapsed: 0,
11201144
maxRadius: 6 * TS, // 6 tiles radius
11211145
duration: 800, // ms
11221146
};
@@ -1130,6 +1154,31 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11301154
return true;
11311155
}
11321156

1157+
function updateScanPulse(dt) {
1158+
if (!scanPulse) return;
1159+
const previousRadius = (scanPulse.elapsed / scanPulse.duration) * scanPulse.maxRadius;
1160+
scanPulse.elapsed = Math.min(scanPulse.duration, scanPulse.elapsed + dt * 1000);
1161+
const radius = (scanPulse.elapsed / scanPulse.duration) * scanPulse.maxRadius;
1162+
1163+
for (const fragment of signalFragments) {
1164+
if (fragment.discovered || fragment.collected) continue;
1165+
const distance = Math.hypot(
1166+
(fragment.x - scanPulse.x) * TS,
1167+
(fragment.y - scanPulse.y) * TS,
1168+
);
1169+
if (distance >= previousRadius && distance <= radius) {
1170+
fragment.discovered = true;
1171+
persistProgress();
1172+
updateJournal();
1173+
showStatus(`SIGNAL FOUND: ${fragment.name}`, 2.5);
1174+
feedback.play('discover');
1175+
feedback.haptic([15, 20, 15]);
1176+
}
1177+
}
1178+
1179+
if (scanPulse.elapsed >= scanPulse.duration) scanPulse = null;
1180+
}
1181+
11331182
// ══════════════════════════════════════════════════════
11341183
// GAME LOOP
11351184
// ══════════════════════════════════════════════════════
@@ -1138,9 +1187,13 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11381187
function update(dt) {
11391188
// Scan cooldown
11401189
if (scanCooldownTimer > 0) scanCooldownTimer -= dt;
1190+
updateScanPulse(dt);
11411191
if (statusTimer > 0) {
11421192
statusTimer -= dt;
1143-
if (statusTimer <= 0) statusEl.classList.remove('show');
1193+
if (statusTimer <= 0) {
1194+
statusEl.classList.remove('show');
1195+
hintEl.classList.remove('status-hidden');
1196+
}
11441197
}
11451198

11461199
// ── Player State Machine ──
@@ -1349,10 +1402,7 @@ <h2 id="journal-title">SIGNAL LOG</h2>
13491402

13501403
// Scan pulse effect
13511404
if (scanPulse) {
1352-
const age = (Date.now() - scanPulse.time) / scanPulse.duration;
1353-
if (age >= 1) {
1354-
scanPulse = null;
1355-
} else {
1405+
const age = scanPulse.elapsed / scanPulse.duration;
13561406
const cx = scanPulse.x * TS - camX + TS/2;
13571407
const cy = scanPulse.y * TS - camY + TS/2;
13581408
const radius = age * scanPulse.maxRadius;
@@ -1393,21 +1443,6 @@ <h2 id="journal-title">SIGNAL LOG</h2>
13931443
rememberNPC(npc);
13941444
}
13951445
}
1396-
for (const fragment of signalFragments) {
1397-
if (fragment.discovered || fragment.collected) continue;
1398-
const fragmentX = fragment.x * TS - camX + TS/2;
1399-
const fragmentY = fragment.y * TS - camY + TS/2;
1400-
const distance = Math.hypot(fragmentX - cx, fragmentY - cy);
1401-
if (distance < radius && distance > radius - TS * 2) {
1402-
fragment.discovered = true;
1403-
persistProgress();
1404-
updateJournal();
1405-
showStatus(`SIGNAL FOUND: ${fragment.name}`, 2.5);
1406-
feedback.play('discover');
1407-
feedback.haptic([15, 20, 15]);
1408-
}
1409-
}
1410-
}
14111446
}
14121447

14131448
// Discovered signal fragments

tests/runtime-smoke.test.cjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,35 @@ test('runtime initializes and renders a frame', () => {
9292
assert.equal(elements.game.width, 1600);
9393
assert.equal(elements.game.height, 1200);
9494

95+
const keyboardTargets = vm.runInContext(`({
96+
game: shouldHandleGameKey(null),
97+
canvas: shouldHandleGameKey({ closest: () => null }),
98+
control: shouldHandleGameKey({ closest: () => ({}) }),
99+
})`, sandbox);
100+
assert.deepEqual(
101+
{ ...keyboardTargets },
102+
{ game: true, canvas: true, control: false },
103+
);
104+
105+
const retrieval = vm.runInContext(`
106+
const fragment = signalFragments[0];
107+
player.tileX = fragment.x;
108+
player.tileY = fragment.y - 1;
109+
player.renderX = player.tileX;
110+
player.renderY = player.tileY;
111+
triggerScanPulse();
112+
updateScanPulse(1);
113+
playerStartMove('south');
114+
const blocked = player.state;
115+
player.state = 'IDLE';
116+
performAction();
117+
({ discovered: fragment.discovered, blocked, collected: fragment.collected });
118+
`, sandbox);
119+
assert.deepEqual(
120+
{ ...retrieval },
121+
{ discovered: true, blocked: 'BLOCKED', collected: true },
122+
);
123+
95124
const completion = vm.runInContext(`
96125
for (const fragment of signalFragments) {
97126
fragment.discovered = true;

0 commit comments

Comments
 (0)