|
54 | 54 | const TS = TILE * PX; |
55 | 55 | let W, H, COLS, ROWS; |
56 | 56 | function resize() { |
57 | | - W = canvas.width = window.innerWidth; |
58 | | - H = canvas.height = window.innerHeight; |
| 57 | + W = window.innerWidth; |
| 58 | + H = window.innerHeight; |
| 59 | + const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); |
| 60 | + canvas.width = Math.floor(W * pixelRatio); |
| 61 | + canvas.height = Math.floor(H * pixelRatio); |
| 62 | + ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); |
| 63 | + ctx.imageSmoothingEnabled = false; |
59 | 64 | COLS = Math.ceil(W / TS) + 2; |
60 | 65 | ROWS = Math.ceil(H / TS) + 2; |
61 | 66 | } |
|
85 | 90 | // ══════════════════════════════════════════════════════ |
86 | 91 | // MAP (24x20 River Factory Zone) |
87 | 92 | // ══════════════════════════════════════════════════════ |
88 | | -const { DIR, oppositeDir, createWorldMap, findPath: findWorldPath } = LittleAndroidLogic; |
| 93 | +const { |
| 94 | + DIR, |
| 95 | + oppositeDir, |
| 96 | + createWorldMap, |
| 97 | + findPath: findWorldPath, |
| 98 | + entityOccupiesTile, |
| 99 | + isTileReserved, |
| 100 | + tickTimedItems, |
| 101 | +} = LittleAndroidLogic; |
89 | 102 | const world = createWorldMap(77); |
90 | 103 | const MW = world.width, MH = world.height, map = world.map; |
91 | 104 |
|
|
95 | 108 | return t!==4&&t!==5&&t!==6&&t!==7&&t!==8&&t!==9&&t!==11&&t!==13&&t!==14&&t!==17; |
96 | 109 | } |
97 | 110 | function isTileOccupiedByNPC(tx,ty,excludeNPC){ |
98 | | - for(const n of npcs){ |
99 | | - if(n===excludeNPC)continue; |
100 | | - if(Math.round(n.tileX)===tx&&Math.round(n.tileY)===ty)return n; |
101 | | - } |
102 | | - return null; |
| 111 | + return npcs.find(n => n !== excludeNPC && entityOccupiesTile(n, tx, ty)) || null; |
103 | 112 | } |
104 | 113 |
|
105 | 114 | // ══════════════════════════════════════════════════════ |
|
299 | 308 | const tx = npc.tileX + d.dx, ty = npc.tileY + d.dy; |
300 | 309 | if (!isWalkable(tx, ty)) return false; |
301 | 310 | // Check if player is on target tile |
302 | | - if (Math.round(player.tileX) === tx && Math.round(player.tileY) === ty) return false; |
| 311 | + if (entityOccupiesTile(player, tx, ty)) return false; |
303 | 312 | // Check other NPCs |
304 | | - if (isTileOccupiedByNPC(tx, ty, npc)) return false; |
| 313 | + if (isTileReserved(npcs, tx, ty, npc)) return false; |
305 | 314 | npc.facing = dir; |
306 | 315 | npc.moveFromX = npc.tileX; npc.moveFromY = npc.tileY; |
307 | 316 | npc.moveToX = tx; npc.moveToY = ty; |
|
550 | 559 | } |
551 | 560 | } |
552 | 561 |
|
| 562 | +function beginPlayerInteraction(npc) { |
| 563 | + player.facing = dirToward(player.tileX, player.tileY, npc.tileX, npc.tileY); |
| 564 | + player.state = 'INTERACTING'; |
| 565 | + player.interactTarget = npc; |
| 566 | + player.interactArmTimer = 0.2; |
| 567 | + triggerNPCInteraction(npc); |
| 568 | +} |
| 569 | + |
| 570 | +function requestPlayerInteraction(npc) { |
| 571 | + if (!npc || npc.interacting) return false; |
| 572 | + player.interactTarget = npc; |
| 573 | + targetPath = []; |
| 574 | + if (npc.moving) { |
| 575 | + player.state = 'WAITING_INTERACTION'; |
| 576 | + return true; |
| 577 | + } |
| 578 | + const distance = Math.abs(npc.tileX - player.tileX) + Math.abs(npc.tileY - player.tileY); |
| 579 | + if (distance !== 1) { |
| 580 | + player.interactTarget = null; |
| 581 | + return false; |
| 582 | + } |
| 583 | + beginPlayerInteraction(npc); |
| 584 | + return true; |
| 585 | +} |
| 586 | + |
553 | 587 | function endNPCInteraction(npc) { |
554 | 588 | npc.interacting = false; |
555 | 589 |
|
|
747 | 781 | const adj = isTileOccupiedByNPC(tileX, tileY, null); |
748 | 782 | if (adj && Math.abs(tileX - player.tileX) + Math.abs(tileY - player.tileY) === 1) { |
749 | 783 | // Trigger interaction |
750 | | - player.facing = dirToward(player.tileX, player.tileY, tileX, tileY); |
751 | | - player.state = 'INTERACTING'; |
752 | | - player.interactTarget = adj; |
753 | | - player.interactArmTimer = 0.2; // 200ms arm raise |
754 | | - triggerNPCInteraction(adj); |
| 784 | + requestPlayerInteraction(adj); |
755 | 785 | return; |
756 | 786 | } |
757 | 787 | } |
|
776 | 806 | const keysDown = {}; |
777 | 807 | window.addEventListener('keydown', (e) => { |
778 | 808 | const k = e.key.toLowerCase(); |
779 | | - if (['w','a','s','d',' '].includes(k)) e.preventDefault(); |
| 809 | + if (['w','a','s','d','arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault(); |
780 | 810 | keysDown[k] = true; |
781 | 811 |
|
782 | 812 | // Spacebar: action button / scan pulse |
|
787 | 817 | const adj = isTileOccupiedByNPC(fx, fy, null); |
788 | 818 | if (adj) { |
789 | 819 | // Interact |
790 | | - player.state = 'INTERACTING'; |
791 | | - player.interactTarget = adj; |
792 | | - player.interactArmTimer = 0.2; |
793 | | - triggerNPCInteraction(adj); |
| 820 | + requestPlayerInteraction(adj); |
794 | 821 | } else { |
795 | 822 | // Scan pulse |
796 | 823 | triggerScanPulse(); |
797 | 824 | } |
798 | 825 | } |
799 | 826 | }); |
800 | 827 | window.addEventListener('keyup', (e) => { keysDown[e.key.toLowerCase()] = false; }); |
| 828 | +window.addEventListener('blur', () => { |
| 829 | + for (const key of Object.keys(keysDown)) keysDown[key] = false; |
| 830 | +}); |
801 | 831 |
|
802 | 832 | function getWASDDirection() { |
803 | | - if (keysDown['w']) return 'north'; |
804 | | - if (keysDown['s']) return 'south'; |
805 | | - if (keysDown['a']) return 'west'; |
806 | | - if (keysDown['d']) return 'east'; |
| 833 | + if (keysDown['w'] || keysDown['arrowup']) return 'north'; |
| 834 | + if (keysDown['s'] || keysDown['arrowdown']) return 'south'; |
| 835 | + if (keysDown['a'] || keysDown['arrowleft']) return 'west'; |
| 836 | + if (keysDown['d'] || keysDown['arrowright']) return 'east'; |
807 | 837 | return null; |
808 | 838 | } |
809 | 839 |
|
|
873 | 903 | const npcOnNext = isTileOccupiedByNPC(next.x, next.y, null); |
874 | 904 | if (npcOnNext) { |
875 | 905 | // Walk up to NPC, trigger interaction |
876 | | - player.facing = dirToward(player.tileX, player.tileY, next.x, next.y); |
877 | | - player.state = 'INTERACTING'; |
878 | | - player.interactTarget = npcOnNext; |
879 | | - player.interactArmTimer = 0.2; |
880 | | - triggerNPCInteraction(npcOnNext); |
| 906 | + requestPlayerInteraction(npcOnNext); |
881 | 907 | targetPath = []; |
882 | 908 | } else { |
883 | 909 | const dir = dirToward(player.tileX, player.tileY, next.x, next.y); |
|
937 | 963 | } |
938 | 964 | break; |
939 | 965 | } |
| 966 | + case 'WAITING_INTERACTION': { |
| 967 | + player.renderX = player.tileX; |
| 968 | + player.renderY = player.tileY; |
| 969 | + const target = player.interactTarget; |
| 970 | + if (!target) { |
| 971 | + player.state = 'IDLE'; |
| 972 | + } else if (!target.moving) { |
| 973 | + const distance = Math.abs(target.tileX - player.tileX) + Math.abs(target.tileY - player.tileY); |
| 974 | + if (distance === 1) beginPlayerInteraction(target); |
| 975 | + else { |
| 976 | + player.interactTarget = null; |
| 977 | + player.state = 'IDLE'; |
| 978 | + } |
| 979 | + } |
| 980 | + break; |
| 981 | + } |
940 | 982 | case 'INTERACTING': { |
941 | 983 | player.interactArmTimer -= dt; |
942 | 984 | player.renderX = player.tileX; |
|
957 | 999 |
|
958 | 1000 | // ── Update all NPCs ── |
959 | 1001 | for (const npc of npcs) updateNPC(npc, dt); |
| 1002 | + scanLabels = tickTimedItems(scanLabels, dt); |
960 | 1003 |
|
961 | 1004 | // ── Emote positioning ── |
962 | 1005 | if (emoteNPC) { |
|
1077 | 1120 | // Draw persistent scan labels above NPCs |
1078 | 1121 | for (let i = scanLabels.length - 1; i >= 0; i--) { |
1079 | 1122 | const sl = scanLabels[i]; |
1080 | | - sl.timer -= 1/60; // approximate dt |
1081 | | - if (sl.timer <= 0) { scanLabels.splice(i, 1); continue; } |
1082 | 1123 | const fadeAlpha = Math.min(1, sl.timer / 0.5); // fade out in last 0.5s |
1083 | 1124 | const nx = sl.npc.renderX * TS - camX + TS/2; |
1084 | 1125 | const ny = sl.npc.renderY * TS - camY - 4; |
|
1118 | 1159 | } |
1119 | 1160 |
|
1120 | 1161 | function gameLoop(ts) { |
| 1162 | + if (document.hidden) { |
| 1163 | + lastTime = ts; |
| 1164 | + requestAnimationFrame(gameLoop); |
| 1165 | + return; |
| 1166 | + } |
1121 | 1167 | const dt = Math.min((ts - lastTime) / 1000, 0.1); |
1122 | 1168 | lastTime = ts; |
1123 | 1169 | update(dt); |
|
0 commit comments