1// game-world.js 2// Game-specific world wrapper that uses the reusable CollisionWorld engine 3// and the WORLD_COLLIDERS data config to drive Dylan and world collisions. 4 5(function (global) { 6 if (typeof CollisionWorld === "undefined") { 7 console.error( 8 "CollisionWorld is not available. Did you load engine/collision-world.js?" 9 ); 10 return; 11 } 12 13 // Sprite and collider dimensions for Dylan. 14 // These control both the physics body and the debug overlay. 15 var PLAYER_SPRITE_WIDTH = 32; 16 var PLAYER_SPRITE_HEIGHT = 64; 17 var PLAYER_COLLIDER_WIDTH = 24; 18 var PLAYER_COLLIDER_HEIGHT = 40; 19 var PLAYER_DEFAULT_SPEED = 2.2; 20 21 // World/map dimensions (in world units) â match #wrapper in CSS. 22 var WORLD_WIDTH = 2000; 23 var WORLD_HEIGHT = 1200; 24 // Visual map scale (matches CSS: transform: scale(3, 3)) 25 var MAP_SCALE = 3; 26 // CSS zoom is visual-only; ignore it for camera math so centering isn't affected. 27 var MAP_ZOOM = 1; 28 // Effective scale used for camera math (keep equal to MAP_SCALE) 29 var EFFECTIVE_MAP_SCALE = MAP_SCALE; 30 // Intro settings 31 var INTRO_DURATION_MS = 500; // Reduced from 1300ms to 500ms for faster movement enable 32 33 class GameWorld { 34 constructor() { 35 this.collisionWorld = null; 36 this.sceneName = null; 37 this.player = null; 38 // Collider debug overlays default OFF 39 this._colliderDebugEnabled = false; 40 this._colliderDebugEls = []; 41 this._initialMapMarginLeft = 0; 42 this._initialMapMarginTop = 0; 43 this._spawn = { x: 0, y: 0 }; 44 this._cameraOffset = { x: 0, y: 0 }; 45 this._shakeOffset = { x: 0, y: 0 }; 46 // Bonfire camera animation state 47 this._bonfireCameraTarget = null; // { x, y } when sitting, null when not 48 this._bonfireCameraCurrent = null; // Current interpolated position 49 // Star creation interval 50 this._starCreationInterval = null; 51 this._shootingStarInterval = null; 52 // Animation state 53 this._animFrame = 2; 54 this._animAccum = 0; 55 this._lastDir = "down"; // 'down' | 'up' | 'left' | 'right' 56 this._lastFacingApplied = null; 57 this._lastFrameApplied = null; 58 this._introPlayed = false; 59 // Player speed (configurable via settings) 60 this._playerSpeed = this._getInitialSpeed(); 61 } 62 63 /** 64 * Get initial player speed from cookie or default 65 */ 66 _getInitialSpeed() { 67 if (typeof getCookie === "function") { 68 var savedSpeed = getCookie("character-speed"); 69 if (savedSpeed) { 70 return parseFloat(savedSpeed); 71 } 72 } 73 return PLAYER_DEFAULT_SPEED; 74 } 75 76 /** 77 * Initialize the game world from WORLD_COLLIDERS for a given scene. 78 * sceneName: 'mainMap', 'tent1', etc. 79 */ 80 init(sceneName) { 81 this._buildScene(sceneName, null); 82 // Hide player until intro completes 83 $("#dylan").css("visibility", "hidden"); 84 } 85 86 /** 87 * Change to a different scene (e.g., enter tent/cave). 88 * options.spawnOverride can set a custom spawn {x,y} within the target scene. 89 */ 90 changeScene(sceneName, options) { 91 options = options || {}; 92 var spawnOverride = options.spawnOverride || null; 93 94 // Clear debug overlays from previous scene 95 this.clearColliderDebugOverlays(); 96 97 // Clear z-index debug overlays when changing scenes 98 if (typeof ZIndexManager !== "undefined") { 99 ZIndexManager.setDebugEnabled(false); 100 } 101 102 // Reset z-index tracking when changing scenes 103 if (typeof ZIndexManager !== "undefined") { 104 ZIndexManager.reset(); 105 } 106 107 // Stop star creation when changing scenes 108 if (this._starCreationInterval) { 109 clearInterval(this._starCreationInterval); 110 this._starCreationInterval = null; 111 } 112 113 // Stop shooting star creation when changing scenes 114 if (this._shootingStarInterval) { 115 clearInterval(this._shootingStarInterval); 116 this._shootingStarInterval = null; 117 } 118 119 // Notify CloudManager of scene change 120 if (typeof CloudManager !== "undefined" && typeof CloudManager.onSceneChange === "function") { 121 CloudManager.onSceneChange(sceneName); 122 } 123 124 // Rebuild scene 125 this._buildScene(sceneName, spawnOverride); 126 } 127 128 /** 129 * Internal helper to build scene bodies/triggers/player. 130 */ 131 _buildScene(sceneName, spawnOverride) { 132 if (typeof WORLD_COLLIDERS === "undefined") { 133 console.error("WORLD_COLLIDERS is not defined. Did you load world.config.js?"); 134 return; 135 } 136 137 this.sceneName = sceneName || "mainMap"; 138 this._setSceneVisibility(this.sceneName);
139 // Force debug overlays to regenerate on scene change 140 this._colliderDebugRendered = false; 141 // Build tent DOM if entering tent 142 if (this.sceneName === "tent1" && typeof buildTentDomIfNeeded === "function") { 143 buildTentDomIfNeeded(); 144 } 145 // Build cave DOM if entering cave 146 if (this.sceneName === "cave" && typeof buildCaveDomIfNeeded === "function") { 147 buildCaveDomIfNeeded(); 148 // Immediately update z-indexes after building cave DOM so signs have correct z-index on entry 149 if (this.player) { 150 this.updateZIndexes(); 151 } 152 } 153 // Move Dylan to the active scene container 154 this._moveDylanToActiveScene(); 155 var cfg = WORLD_COLLIDERS[this.sceneName]; 156 if (!cfg) { 157 console.error("WORLD_COLLIDERS has no entry for scene:", this.sceneName); 158 return; 159 } 160 161 this._spawn = spawnOverride || cfg.playerSpawn || { x: 0, y: 0 }; 162 163 // Capture initial map camera offsets so we can pan relative to spawn 164 var $parent = this._getSceneElement(); 165 if ($parent.length) { 166 var ml = parseInt($parent.css("margin-left"), 10); 167 var mt = parseInt($parent.css("margin-top"), 10); 168 this._initialMapMarginLeft = isNaN(ml) ? 0 : ml; 169 this._initialMapMarginTop = isNaN(mt) ? 0 : mt; 170 } else { 171 this._initialMapMarginLeft = 0; 172 this._initialMapMarginTop = 0; 173 } 174 175 this.collisionWorld = new CollisionWorld({}); 176 177 // Player spawn 178 var spawn = this._spawn; 179 this.player = this.collisionWorld.addPlayer({ 180 x: spawn.x, 181 y: spawn.y, 182 width: PLAYER_COLLIDER_WIDTH, 183 height: PLAYER_COLLIDER_HEIGHT, 184 maxSpeed: this._playerSpeed 185 }); 186 187 // Register solids 188 if (cfg.solids && cfg.solids.length) { 189 for (var i = 0; i < cfg.solids.length; i++) { 190 var s = cfg.solids[i]; 191 this.collisionWorld.addSolidRect({ 192 x: s.x, 193 y: s.y, 194 width: s.width, 195 height: s.height, 196 tag: s.tag 197 }); 198 } 199 } 200 201 // Register triggers 202 if (cfg.triggers && cfg.triggers.length) { 203 for (var j = 0; j < cfg.triggers.length; j++) { 204 var t = cfg.triggers[j]; 205 this.collisionWorld.addTriggerRect({ 206 x: t.x, 207 y: t.y, 208 width: t.width, 209 height: t.height, 210 tag: t.tag 211 }); 212 } 213 } 214 215 // Wire trigger-based scene transitions if provided in config 216 this._wireSceneTriggers(cfg); 217 218 // Example: wire tutorial trigger if present 219 var self = this; 220 this.collisionWorld.on("enter:tutorialArea", function () { 221 if (typeof openTutorial === "function") { 222 openTutorial(); 223 } 224 }); 225 226 // Initial sync of Dylan to physics position (do not auto-center; preserve manual offset) 227 this.syncToDom(); 228 229 // Play intro drop only the first time we enter the main map 230 if (!this._introPlayed && this.sceneName === "mainMap") { 231 if (global.playerController) { 232 global.playerController.disableInput = true; 233 } 234 this._playIntroDrop(); 235 this._introPlayed = true; 236 } else { 237 // Ensure input is enabled for all other scenes 238 if (global.playerController) { 239 global.playerController.disableInput = false; 240 } 241 } 242 243 // Initialize z-index manager for this scene 244 if (typeof ZIndexManager !== "undefined") { 245 ZIndexManager.init(this.sceneName); 246 // If debug was enabled, render debug overlays 247 if (this._colliderDebugEnabled) { 248 ZIndexManager.setDebugEnabled(true, this.sceneName); 249 } 250 } 251 252 // If debug was enabled before init, render overlays once 253 if (this._colliderDebugEnabled) { 254 this.renderColliderDebugOverlays(); 255 this._colliderDebugRendered = true; 256 } 257 } 258 259 /** 260 * Advance physics and sync Dylan to DOM. 261 * input: { x: -1..1, y: -1..1 } 262 */ 263 update(deltaMs, input) { 264 if (!this.collisionWorld) return; 265 var effectiveInput = input || { x: 0, y: 0 }; 266 267 // Check if fishing - maintain position and disable movement 268 var isFishing = (typeof window !== "undefined" && 269 ((typeof window.inFishing !== "undefined" && window.inFishing) || 270 (typeof window.isReelingIn !== "undefined" && window.isReelingIn))); 271 272 if (isFishing && this.player) { 273 // Lock player to fishing position 274 this.player.setPosition({ x: 992, y: 1558 }); 275 if (typeof this.player.setVelocity === "function") { 276 this.player.setVelocity({ x: 0, y: 0 }); 277 } 278 effectiveInput = { x: 0, y: 0 }; 279 } 280 281 // Check if chopping tree - maintain position and disable movement 282 // Only lock if we have the axe (meaning we're chopping, not picking up the axe) 283 var isChoppingTree = (typeof window !== "undefined" && 284 typeof window.eventOccurence !== "undefined" && 285 window.eventOccurence && 286 typeof window.hitCount !== "undefined" && 287 window.hitCount < 7 && 288 typeof window.inventory !== "undefined" && 289 window.inventory.axe); 290 291 if (isChoppingTree && this.player) { 292 // Lock player to tree chopping position (at actual tree location) 293 this.player.setPosition({ x: 242, y: 456 }); 294 if (typeof this.player.setVelocity === "function") { 295 this.player.setVelocity({ x: 0, y: 0 }); 296 } 297 effectiveInput = { x: 0, y: 0 }; 298 } 299 300 // Check if picking up axe - maintain position at stump 301 var isPickingUpAxe = (typeof window !== "undefined" && 302 typeof window.eventOccurence !== "undefined" && 303 window.eventOccurence && 304 typeof window.inventory !== "undefined" && 305 !window.inventory.axe); 306 307 if (isPickingUpAxe && this.player) { 308 // Lock player to axe pickup position 309 this.player.setPosition({ x: 423, y: 434 }); 310 if (typeof this.player.setVelocity === "function") { 311 this.player.setVelocity({ x: 0, y: 0 }); 312 } 313 effectiveInput = { x: 0, y: 0 }; 314 } 315 316 // Check if input is disabled 317 var inputDisabled = false;
318 if (global.playerController && global.playerController.disableInput) { 319 effectiveInput = { x: 0, y: 0 }; 320 inputDisabled = true; 321 if (this.player && typeof this.player.setVelocity === "function") { 322 this.player.setVelocity({ x: 0, y: 0 }); 323 } 324 } 325 326 // Store original input BEFORE any modifications (use the raw input parameter) 327 var originalInput = input ? { x: input.x || 0, y: input.y || 0 } : { x: 0, y: 0 }; 328 329 // Apply bonfire stairs modifier (migrated from old navigation.js) 330 // Bottom stairs: left/right movement also moves up/down (diagonal) 331 // Top stairs: vertical speed is increased when moving 332 // We bypass applyInput and set velocity directly when on stairs 333 var onStairs = false; 334 var stairsVelocity = null; 335 336 if (this.sceneName === "mainMap" && this.player) { 337 var pos = this.player.getPosition(); 338 var maxSpeed = this.player.maxSpeed || 4; 339 340 // Bottom/Left stairs area: world coordinates converted from old CSS coordinates 341 // Old CSS: left 736-816, top 908-952 (exclusive: top < 952) 342 // World: x = CSS left + 16, y = CSS top + 44 343 var stairsBottomMinX = 752; 344 var stairsBottomMaxX = 832; 345 var stairsBottomMinY = 952; 346 var stairsBottomMaxY = 996; 347 348 // Top/Front stairs area - exact coordinates: x: 972, y: 602, w: 56, h: 68 349 var stairsTopMinX = 972; 350 var stairsTopMaxX = 972 + 56; 351 var stairsTopMinY = 602; 352 var stairsTopMaxY = 602 + 68; 353 354 // Check bottom stairs (diagonal movement: left+up, right+down) 355 if (pos.x >= stairsBottomMinX && pos.x <= stairsBottomMaxX && 356 pos.y >= stairsBottomMinY && pos.y < stairsBottomMaxY) { 357 // On bottom bonfire stairs - set velocity directly for diagonal movement 358 // Only apply when moving horizontally (matches old behavior) 359 if (originalInput.x !== 0) { 360 onStairs = true; 361 // Calculate velocity maintaining 3:1 ratio from old frame-based movement 362 // Old: 12px/frame horizontal, 4px/frame vertical = 3:1 ratio 363 var stairsSpeed = maxSpeed * 2.5; // Scale to match old frame-based speed 364 var vx = originalInput.x * stairsSpeed; 365 // Vertical: left (x < 0) moves up (y < 0), right (x > 0) moves down (y > 0) 366 var vy = originalInput.x * stairsSpeed / 3; // 1/3 of horizontal speed 367 stairsVelocity = { x: vx, y: vy }; 368 } 369 } 370 // Check top stairs (increased vertical speed) 371 else if (pos.x >= stairsTopMinX && pos.x <= stairsTopMaxX && 372 pos.y >= stairsTopMinY && pos.y <= stairsTopMaxY) { 373 // On top bonfire stairs - increase vertical speed when moving up/down 374 // Horizontal movement should work normally (no vertical component) 375 if (originalInput.y !== 0 || originalInput.x !== 0) { 376 onStairs = true; 377 // Calculate velocity with increased vertical speed 378 var stairsSpeed = maxSpeed * 2.5; 379 var vx = originalInput.x * stairsSpeed; 380 var vy = 0; 381 if (originalInput.y !== 0) { 382 // Moving up or down - increase vertical speed 383 vy = originalInput.y * stairsSpeed; 384 } 385 // If only moving horizontally, just use normal horizontal speed (no vertical component) 386 stairsVelocity = { x: vx, y: vy }; 387 } 388 } 389 } 390 391 // If on stairs, set velocity directly and skip applyInput 392 // Otherwise, use normal input 393 if (onStairs && stairsVelocity) { 394 // Set velocity BEFORE physics update so it's used during the update 395 if (this.player && typeof this.player.setVelocity === "function") { 396 this.player.setVelocity(stairsVelocity); 397 } 398 // Update physics with zero input (velocity already set above) 399 // But we need to prevent applyInput from overriding it 400 // So we'll manually update the engine without calling applyInput 401 if (this.collisionWorld && this.collisionWorld.engine) { 402 // Skip applyInput and just update the engine 403 var Engine = Matter.Engine; 404 Engine.update(this.collisionWorld.engine, deltaMs); 405 } else { 406 this.collisionWorld.update(deltaMs, { x: 0, y: 0 }); 407 } 408 // Re-apply velocity after update in case it was modified 409 if (this.player && typeof this.player.setVelocity === "function") { 410 this.player.setVelocity(stairsVelocity); 411 } 412 } else { 413 // Normal movement - use regular input 414 this.collisionWorld.update(deltaMs, effectiveInput); 415 } 416 this.updateAnimation(deltaMs, effectiveInput); 417 this.syncToDom(); 418 this.updateDoors(); 419 this.updateZIndexes(); 420 421 // Update player debug box position every frame if debug is enabled 422 if (this._colliderDebugEnabled && this.player) { 423 this._updatePlayerDebugBox(); 424 } 425
426 // Do not re-render debug overlays every frame; only on enable/scene change 427 } 428 429 /** 430 * Update player debug box position to follow the character 431 */ 432 _updatePlayerDebugBox() { 433 if (!this.player) return; 434 435 // Find the player debug element 436 var $playerDebug = null; 437 for (var i = 0; i < this._colliderDebugEls.length; i++) { 438 var el = this._colliderDebugEls[i]; 439 if (el && el.classList && el.classList.contains("world-collider-debug-player")) { 440 $playerDebug = $(el); 441 break; 442 } 443 } 444 445 if (!$playerDebug || !$playerDebug.length) return; 446 447 // Update position based on current player position 448 var pos = this.player.getPosition(); 449 var footY = pos.y + PLAYER_COLLIDER_HEIGHT / 2; 450 var left = pos.x - PLAYER_COLLIDER_WIDTH / 2; 451 var top = footY - PLAYER_COLLIDER_HEIGHT; 452 453 $playerDebug.css({ 454 left: left + "px", 455 top: top + "px" 456 }); 457 } 458 459 /** 460 * Update tent doors based on player position. 461 * Replicates legacy door logic from script.js move() function. 462 */ 463 updateDoors() { 464 if (this.sceneName !== "mainMap") return; 465 if (!this.player) return; 466 467 var $dylan = $("#dylan"); 468 if (!$dylan.length) return; 469 470 var dylanLeft = parseInt($dylan.css("left")); 471 472 // Control tent1 door based on player position 473 // Door opens when player moves left (left < 428), closes when moving right (left >= 428) 474 if (typeof doorOpen === "undefined") { 475 global.doorOpen = false; 476 } 477 478 if (!global.doorOpen) { 479 // Door is closed, check if we should open it 480 if (dylanLeft < 428) { 481 $(".tentdoor.tent1").animate({ "height": 0 }, 1000); 482 global.doorOpen = true; 483 } 484 } else { 485 // Door is open, check if we should close it 486 if (dylanLeft >= 428) { 487 $(".tentdoor.tent1").animate({ "height": 38 }, 1000); 488 global.doorOpen = false; 489 } 490 } 491 } 492 493 /** 494 * Update z-indexes of objects based on player position. 495 * Uses ZIndexManager to dynamically change z-indexes so player can walk behind/in front of objects. 496 */ 497 updateZIndexes() { 498 if (!this.player) return; 499 if (typeof ZIndexManager === "undefined") return; 500 501 // Get player position (center of collider) 502 var pos = this.player.getPosition(); 503 504 // Update z-indexes based on player position 505 ZIndexManager.update(pos.y, pos.x, this.sceneName); 506 507 // Update timeline signs z-index individually based on their Y position 508 if (this.sceneName === "cave") { 509 var $signs = $(".cave-timeline-sign-interact"); 510 $signs.each(function() { 511 var $sign = $(this); 512 var signY = parseFloat($sign.attr("data-sign-y")) || 0; 513 // Player z-index in cave is 9999 514 // Use player's center Y position for comparison 515 // signY is the sign's top Y position (stored in data-sign-y) 516 // In cave: Y increases as you go down 517 // When player Y > sign Y: player should be in front (player is below sign) 518 // When player Y <= sign Y: sign should be in front (player is at or above sign) 519 if (pos.y > signY) { 520 // Player center is below sign top - player should be in front (sign behind player) 521 $sign.css("z-index", "9998"); 522 } else { 523 // Player center is at or above sign top - sign should be in front of player 524 $sign.css("z-index", "10000"); 525 } 526 }); 527 } 528 529 // Update bonfire vignette effect 530 this.updateBonfireVignette(pos); 531 } 532 533 /** 534 * Update bonfire vignette effect based on player position. 535 * Creates a cozy, focused atmosphere when player is in the bonfire area. 536 * Uses two vignette layers to darken both background and foreground elements. 537 */ 538 updateBonfireVignette(pos) { 539 if (this.sceneName !== "mainMap") return; 540 541 var $vignetteBottom = $("#bonfire-vignette-bottom"); 542 var $vignetteMiddle = $("#bonfire-vignette-middle"); 543 var $vignetteTop = $("#bonfire-vignette-top"); 544 var $stars = $("#bonfire-stars"); 545 if (!$vignetteBottom.length && !$vignetteMiddle.length && !$vignetteTop.length) return; 546 547 // Bonfire area boundaries (adjusted to match collision bounds) 548 // Center is approximately at x: 984, y: 652 549 var bonfireMinX = 780; // Left wall 550 var bonfireMaxX = 1292; // Right wall 551 var bonfireMinY = 650; // Top cliff 552 var bonfireMaxY = 1068; // Bottom trees 553 554 // Check if player is in bonfire area 555 var inBonfireArea = pos.x >= bonfireMinX && pos.x <= bonfireMaxX && 556 pos.y >= bonfireMinY && pos.y <= bonfireMaxY; 557 558 // Toggle vignette class on all three layers 559 if (inBonfireArea) { 560 $vignetteBottom.addClass("active"); 561 $vignetteMiddle.addClass("active"); 562 $vignetteTop.addClass("active"); 563 564 // Show stars container 565 if ($stars.length) { 566 $stars.addClass("active"); 567 568 // Initialize star creation interval if not already set 569 if (!this._starCreationInterval) { 570 this._startStarCreation($stars); 571 } 572 573 // Initialize shooting star creation if not already set 574 if (!this._shootingStarInterval) { 575 this._startShootingStarCreation($stars); 576 } 577 } 578 } else { 579 $vignetteBottom.removeClass("active"); 580 $vignetteMiddle.removeClass("active"); 581 $vignetteTop.removeClass("active"); 582 583 // Hide stars and stop creating new ones 584 if ($stars.length) { 585 $stars.removeClass("active"); 586 } 587 588 // Stop star creation 589 if (this._starCreationInterval) { 590 clearInterval(this._starCreationInterval); 591 this._starCreationInterval = null; 592 } 593 594 // Stop shooting star creation 595 if (this._shootingStarInterval) { 596 clearInterval(this._shootingStarInterval); 597 this._shootingStarInterval = null; 598 } 599 } 600 } 601 602 /** 603 * Start creating stars continuously while in bonfire area 604 */ 605 _startStarCreation($container) { 606 var self = this; 607 var mapWidth = 2000; 608 var mapHeight = 1200; 609 610 // Create a new star every 200-400ms 611 this._starCreationInterval = setInterval(function() { 612 // Remove old stars that have faded out (opacity 0 after animation) 613 $container.find(".bonfire-star").each(function() { 614 var $star = $(this); 615 // Check if animation has completed (stars fade out at 50% of 4s = 2s) 616 var animationDelay = parseFloat($star.css("animation-delay")) || 0;
617 var elapsed = (Date.now() - ($star.data("createdAt") || Date.now())) / 1000; 618 if (elapsed > 2 + animationDelay) { 619 $star.remove(); 620 } 621 }); 622 623 // Create a new star 624 var $star = $("<div>").addClass("bonfire-star"); 625 626 // Random position across the map 627 var x = Math.random() * mapWidth; 628 var y = Math.random() * mapHeight; 629 630 // Make some stars brighter (20% chance) 631 if (Math.random() < 0.2) { 632 $star.addClass("bright"); 633 } 634 635 // Random animation delay for variety (0-1s) 636 var delay = Math.random() * 1; 637 $star.css({ 638 left: x + "px", 639 top: y + "px", 640 "animation-delay": delay + "s" 641 }); 642 643 // Track when star was created 644 $star.data("createdAt", Date.now()); 645 646 $container.append($star); 647 648 // Remove star after animation completes (4s + delay) 649 setTimeout(function() { 650 $star.remove(); 651 }, (4000 + delay * 1000)); 652 }, 300); // Create a new star every 300ms 653 } 654 655 /** 656 * Start creating shooting stars occasionally for ambiance 657 */ 658 _startShootingStarCreation($container) { 659 var self = this; 660 var mapWidth = 2000; 661 var mapHeight = 1200; 662 663 // Create a shooting star every 3-8 seconds 664 function createShootingStar() { 665 var $shootingStar = $("<div>").addClass("shooting-star"); 666 667 // Random starting position (top-left area for diagonal movement) 668 var startX = Math.random() * (mapWidth * 0.3); // Start in left 30% of map 669 var startY = Math.random() * (mapHeight * 0.2); // Start in top 20% of map 670 671 $shootingStar.css({ 672 left: startX + "px", 673 top: startY + "px" 674 }); 675 676 $container.append($shootingStar); 677 678 // Remove after animation completes (2s) 679 setTimeout(function() { 680 $shootingStar.remove(); 681 }, 2000); 682 683 // Schedule next shooting star (3-8 seconds) 684 var nextDelay = 3000 + Math.random() * 5000; 685 self._shootingStarInterval = setTimeout(createShootingStar, nextDelay); 686 } 687 688 // Start the first shooting star after a short delay 689 var initialDelay = 2000 + Math.random() * 3000; 690 this._shootingStarInterval = setTimeout(createShootingStar, initialDelay); 691 } 692 693 /** 694 * Push the physics player position into the #dylan DOM element. 695 * We treat the physics body position as the center of the collider, 696 * whose bottom edge roughly corresponds to Dylan's feet. 697 */ 698 syncToDom() { 699 if (!this.player) return; 700 var pos = this.player.getPosition(); 701 var $dylan = $("#dylan"); 702 if ($dylan.length) { 703 // Compute foot position from collider center 704 var footY = pos.y + PLAYER_COLLIDER_HEIGHT / 2; 705 var left = pos.x - PLAYER_SPRITE_WIDTH / 2; 706 var top = footY - PLAYER_SPRITE_HEIGHT; 707 708 // Set z-index based on scene 709 var currentZIndex = $dylan.css("z-index"); 710 var cssUpdate = { 711 left: left + "px", 712 top: top + "px" 713 }; 714 // Set z-index based on scene: 9999 in tent and cave, 49 on main map 715 // Special case: when sitting on any log/bench, set player z-index to 100 716 var isSittingOnTopBench = typeof window !== "undefined" && 717 typeof window.onLogTop !== "undefined" && 718 window.onLogTop === true; 719 var isSittingOnLeftStump = typeof window !== "undefined" && 720 typeof window.onLogLeft !== "undefined" && 721 window.onLogLeft === true; 722 var isSittingOnRightStump = typeof window !== "undefined" && 723 typeof window.onLogRight !== "undefined" && 724 window.onLogRight === true; 725 var isSittingOnAnyLog = isSittingOnTopBench || isSittingOnLeftStump || isSittingOnRightStump; 726 727 if (this.sceneName === "tent1" || this.sceneName === "cave") { 728 if (currentZIndex !== "9999") { 729 cssUpdate["z-index"] = "9999"; 730 } 731 } else if (this.sceneName === "mainMap") { 732 // When sitting on any log/bench, player should be above (z-index 100) 733 if (isSittingOnAnyLog) { 734 if (currentZIndex !== "100") { 735 cssUpdate["z-index"] = "100"; 736 } 737 } else { 738 // Reset to 49 when on main map (correct z-index for player on main map) 739 if (currentZIndex !== "49") { 740 cssUpdate["z-index"] = "49"; 741 } 742 } 743 } 744 $dylan.css(cssUpdate); 745 } 746 747 // Camera follow: keep player centered using world pos and effective scale. 748 var $parent = this._getSceneElement(); 749 if ($parent.length) { 750 var sceneWidth = WORLD_WIDTH; 751 var sceneHeight = WORLD_HEIGHT; 752 753 if (this.sceneName === "tent1") { 754 sceneWidth = 416; 755 sceneHeight = 1200; 756 } else if (this.sceneName === "cave") { 757 sceneWidth = 2000; 758 sceneHeight = 1200; 759 } 760 761 var mapWidthPx = sceneWidth * EFFECTIVE_MAP_SCALE;
762 var mapHeightPx = sceneHeight * EFFECTIVE_MAP_SCALE; 763 764 // Special handling for tent: center horizontally, follow vertically (no clamping) 765 if (this.sceneName === "tent1") { 766 var newMarginLeft = (window.innerWidth - mapWidthPx) / 2; 767 768 // Follow player vertically with camera offset (no clamping in tent) 769 var viewportCenterY = window.innerHeight / 2; 770 var camY = pos.y; 771 var newMarginTop = viewportCenterY - camY * EFFECTIVE_MAP_SCALE + this._cameraOffset.y; 772 newMarginTop = Math.round(newMarginTop); 773 774 // No clamping in tent - allow free camera movement 775 776 $parent.css({ 777 "margin-left": Math.round(newMarginLeft) + "px", 778 "margin-top": newMarginTop + "px" 779 }); 780 } 781 // For small scenes (like tent) that fit within viewport, center the scene itself 782 else if (mapWidthPx <= window.innerWidth && mapHeightPx <= window.innerHeight) { 783 var newMarginLeft = (window.innerWidth - mapWidthPx) / 2; 784 var newMarginTop = (window.innerHeight - mapHeightPx) / 2; 785 786 $parent.css({ 787 "margin-left": Math.round(newMarginLeft) + "px", 788 "margin-top": Math.round(newMarginTop) + "px" 789 }); 790 } else { 791 // For large scenes, follow the player with camera clamping 792 var viewportCenterX = window.innerWidth / 2; 793 var viewportCenterY = window.innerHeight / 2; 794 795 var camX, camY; 796 797 // Check if player is sitting on a log - if so, smoothly center camera on bonfire instead of player 798 var isSitting = typeof window !== "undefined" && typeof window.onLog !== "undefined" && window.onLog; 799 var bonfireCenterX = 999; 800 var bonfireCenterY = 834; 801 802 if (isSitting) { 803 // Set target for smooth camera movement 804 if (!this._bonfireCameraTarget) { 805 // Initialize target and current position 806 this._bonfireCameraTarget = { x: bonfireCenterX, y: bonfireCenterY }; 807 // Start from current camera position (derived from current margin) 808 var $parent = this._getSceneElement(); 809 if ($parent.length) { 810 var currentMarginLeft = parseInt($parent.css("margin-left")) || 0; 811 var currentMarginTop = parseInt($parent.css("margin-top")) || 0; 812 var viewportCenterX = window.innerWidth / 2; 813 var viewportCenterY = window.innerHeight / 2; 814 // Reverse the camera formula: worldPos = (viewportCenter - margin + offset) / scale 815 var currentWorldX = (viewportCenterX - currentMarginLeft + this._cameraOffset.x) / EFFECTIVE_MAP_SCALE; 816 var currentWorldY = (viewportCenterY - currentMarginTop + this._cameraOffset.y) / EFFECTIVE_MAP_SCALE; 817 this._bonfireCameraCurrent = { x: currentWorldX, y: currentWorldY }; 818 } else { 819 this._bonfireCameraCurrent = { x: bonfireCenterX, y: bonfireCenterY }; 820 } 821 } else { 822 // Update target in case it changed 823 this._bonfireCameraTarget = { x: bonfireCenterX, y: bonfireCenterY }; 824 } 825 826 // Smoothly interpolate towards target (lerp with factor 0.1 for smooth movement) 827 var lerpFactor = 0.1; 828 this._bonfireCameraCurrent.x += (this._bonfireCameraTarget.x - this._bonfireCameraCurrent.x) * lerpFactor; 829 this._bonfireCameraCurrent.y += (this._bonfireCameraTarget.y - this._bonfireCameraCurrent.y) * lerpFactor; 830 831 camX = this._bonfireCameraCurrent.x; 832 camY = this._bonfireCameraCurrent.y; 833 } else { 834 // Not sitting - clear bonfire camera state and follow player 835 this._bonfireCameraTarget = null; 836 this._bonfireCameraCurrent = null; 837 camX = pos.x; 838 camY = pos.y; 839 } 840 841 // Optional micro-adjust to fine-tune visual centering 842 var NUDGE_X = 0; // negative shifts camera left;
842 adjust as needed 843 var NUDGE_Y = 0; // negative shifts camera up; adjust as needed 844 845 // Always apply camera offset - it allows free panning even when snapped 846 // Also apply shake offset for screen shake effects 847 var newMarginLeft = 848 viewportCenterX - camX * EFFECTIVE_MAP_SCALE + this._cameraOffset.x + this._shakeOffset.x + NUDGE_X; 849 var newMarginTop = 850 viewportCenterY - camY * EFFECTIVE_MAP_SCALE + this._cameraOffset.y + this._shakeOffset.y + NUDGE_Y; 851 852 // Snap to whole pixels to avoid subpixel drift/lead 853 newMarginLeft = Math.round(newMarginLeft); 854 newMarginTop = Math.round(newMarginTop); 855 856 // Normal clamping for all areas 857 // Clamp so the camera never slides past the left or top page edges. 858 if (newMarginLeft > 0) { 859 newMarginLeft = 0; 860 } 861 862 var minMarginLeft = window.innerWidth - mapWidthPx; 863 if (newMarginLeft < minMarginLeft) { 864 newMarginLeft = minMarginLeft; 865 } 866 867 if (newMarginTop > 0) { 868 newMarginTop = 0; 869 } 870 871 var bottomBufferPx = 0; 872 if (this.sceneName === "mainMap") { 873 bottomBufferPx = 2000; // allow slight extra downward pan on main map 874 } 875 var minMarginTop = window.innerHeight - mapHeightPx - bottomBufferPx; 876 if (newMarginTop < minMarginTop) { 877 newMarginTop = minMarginTop; 878 } 879 880 $parent.css({ 881 "margin-left": newMarginLeft + "px", 882 "margin-top": newMarginTop + "px" 883 }); 884 } 885 } 886 } 887 888 // ---- Collider debug overlays (WORLD_COLLIDERS-based) ---- 889 890 setColliderDebugEnabled(enabled) { 891 this._colliderDebugEnabled = !!enabled; 892 this._colliderDebugRendered = false; 893 if (this._colliderDebugEnabled) { 894 this.renderColliderDebugOverlays(); 895 this._colliderDebugRendered = true; 896 // Also enable z-index debug visualization 897 if (typeof ZIndexManager !== "undefined") { 898 ZIndexManager.setDebugEnabled(true, this.sceneName); 899 } 900 } else { 901 this.clearColliderDebugOverlays(); 902 // Disable z-index debug visualization 903 if (typeof ZIndexManager !== "undefined") { 904 ZIndexManager.setDebugEnabled(false); 905 } 906 } 907 } 908 909 getColliderDebugEnabled() { 910 return !!this._colliderDebugEnabled; 911 } 912 913 /** 914 * Set player movement speed 915 */ 916 setPlayerSpeed(speed) { 917 this._playerSpeed = speed; 918 // Update the player's maxSpeed directly 919 if (this.player) { 920 this.player.maxSpeed = speed; 921 } 922 } 923 924 /** 925 * Get current player speed 926 */ 927 getPlayerSpeed() { 928 return this._playerSpeed; 929 } 930 931 clearColliderDebugOverlays() { 932 for (var i = 0; i < this._colliderDebugEls.length; i++) { 933 var el = this._colliderDebugEls[i]; 934 if (el && el.parentNode) { 935 el.parentNode.removeChild(el); 936 } 937 } 938 this._colliderDebugEls = []; 939 } 940 941 renderColliderDebugOverlays() { 942 // Avoid rebuilding repeatedly each frame; only build when enabled/changed. 943 if (!this._colliderDebugEnabled) return; 944 // Clear existing overlays once before drawing 945 this.clearColliderDebugOverlays(); 946 if (typeof WORLD_COLLIDERS === "undefined") return; 947 948 var cfg = WORLD_COLLIDERS[this.sceneName]; 949 if (!cfg) return; 950 951 var parentSelector = 952 this.sceneName === "tent1" ? "#tent1" : this.sceneName === "cave" ? "#cave" : "#map"; 953 var $parent = $(parentSelector); 954 if (!$parent.length) return; 955 956 // Add purple debug overlay for bonfire stairs areas 957 if (this.sceneName === "mainMap") { 958 // Bottom/Left stairs area (stairs-left.campfire at CSS top: 752px, left: 752px) 959 // Old code checked: left 736-816, top 908-952 960 // World coordinates: x = CSS left + 16, y = CSS top + 44 961 var stairsLeftMinX = 752; 962 var stairsLeftMaxX = 832; 963 var stairsLeftMinY = 952; 964 var stairsLeftMaxY = 996; 965 var stairsLeftWidth = stairsLeftMaxX - stairsLeftMinX; 966 var stairsLeftHeight = stairsLeftMaxY - stairsLeftMinY; 967 968 var $stairsLeftDebug = $("<div class='world-collider-debug world-collider-debug-stairs'></div>"); 969 $stairsLeftDebug.css({ 970 position: "absolute", 971 left: stairsLeftMinX + "px", 972 top: stairsLeftMinY + "px", 973 width: stairsLeftWidth + "px", 974 height: stairsLeftHeight + "px", 975 border: "2px dashed rgba(168, 85, 247, 0.9)", // purple 976 backgroundColor: "rgba(168, 85, 247, 0.25)", // purple with transparency 977 pointerEvents: "none", 978 zIndex: 999998 979 }); 980 $stairsLeftDebug.attr("title", "Bonfire Left Stairs Area"); 981 $parent.append($stairsLeftDebug); 982 this._colliderDebugEls.push($stairsLeftDebug[0]); 983 984 // Top/Front stairs area - exact coordinates provided by user 985 // x: 972, y: 602, width: 56, height: 68 986 var stairsFrontMinX = 972;
987 var stairsFrontMaxX = 972 + 56; 988 var stairsFrontMinY = 602; 989 var stairsFrontMaxY = 602 + 68; 990 var stairsFrontWidth = 56; 991 var stairsFrontHeight = 68; 992 993 var $stairsFrontDebug = $("<div class='world-collider-debug world-collider-debug-stairs'></div>"); 994 $stairsFrontDebug.css({ 995 position: "absolute", 996 left: stairsFrontMinX + "px", 997 top: stairsFrontMinY + "px", 998 width: stairsFrontWidth + "px", 999 height: stairsFrontHeight + "px", 1000 border: "2px dashed rgba(168, 85, 247, 0.9)", // purple 1001 backgroundColor: "rgba(168, 85, 247, 0.25)", // purple with transparency 1002 pointerEvents: "none", 1003 zIndex: 999998 1004 }); 1005 $stairsFrontDebug.attr("title", "Bonfire Front/Top Stairs Area"); 1006 $parent.append($stairsFrontDebug); 1007 this._colliderDebugEls.push($stairsFrontDebug[0]); 1008 } 1009 1010 // Solids 1011 if (cfg.solids && cfg.solids.length) { 1012 for (var i = 0; i < cfg.solids.length; i++) { 1013 var s = cfg.solids[i]; 1014 var $el = $( 1015 "<div class='world-collider-debug world-collider-debug-solid'></div>" 1016 ); 1017 $el.css({ 1018 position: "absolute", 1019 left: s.x + "px", 1020 top: s.y + "px", 1021 width: s.width + "px", 1022 height: s.height + "px" 1023 }); 1024 if (s.tag) { 1025 $el.attr("title", s.tag); 1026 } 1027 $parent.append($el); 1028 this._colliderDebugEls.push($el[0]); 1029 } 1030 } 1031 1032 // Triggers 1033 if (cfg.triggers && cfg.triggers.length) { 1034 for (var j = 0; j < cfg.triggers.length; j++) { 1035 var t = cfg.triggers[j]; 1036 var $tEl = $( 1037 "<div class='world-collider-debug world-collider-debug-trigger'></div>" 1038 ); 1039 $tEl.css({ 1040 position: "absolute", 1041 left: t.x + "px", 1042 top: t.y + "px", 1043 width: t.width + "px", 1044 height: t.height + "px", 1045 background: "rgba(255,0,0,0.15)", 1046 border: "2px solid rgba(255,0,0,0.7)" 1047 }); 1048 if (t.tag) { 1049 $tEl.attr("title", t.tag); 1050 } 1051 $parent.append($tEl); 1052 this._colliderDebugEls.push($tEl[0]); 1053 } 1054 } 1055 1056 // Timeline sign z-index transition points (show where z-index changes) 1057 if (this.sceneName === "cave" && typeof TIMELINE_ENTRIES !== "undefined") { 1058 TIMELINE_ENTRIES.forEach(function(entry, idx) { 1059 var signY = (entry.y || 0) - 10; 1060 // Show the Y position where z-index transitions (at signY) 1061 var $zIndexDebug = $( 1062 "<div class='world-collider-debug' style='border: 2px dashed rgba(255, 255, 0, 0.8); background-color: rgba(255, 255, 0, 0.1);'></div>" 1063 ); 1064 $zIndexDebug.css({ 1065 position: "absolute", 1066 left: "0px", 1067 top: signY + "px", 1068 width: "2000px", 1069 height: "2px" 1070 }); 1071 $zIndexDebug.attr("title", "Z-Index Transition: Sign " + idx + " (" + (entry.year || "") + ") - Player behind above this line, in front below"); 1072 $parent.append($zIndexDebug); 1073 this._colliderDebugEls.push($zIndexDebug[0]); 1074 }.bind(this)); 1075 } 1076 1077 // Player hitbox (for reference) 1078 if (this.player) { 1079 var pos = this.player.getPosition(); 1080 var footY = pos.y + PLAYER_COLLIDER_HEIGHT / 2; 1081 var left = pos.x - PLAYER_COLLIDER_WIDTH / 2; 1082 var top = footY - PLAYER_COLLIDER_HEIGHT; 1083 var $pEl = $( 1084 "<div class='world-collider-debug world-collider-debug-player'></div>" 1085 ); 1086 $pEl.css({ 1087 position: "absolute", 1088 left: left + "px", 1089 top: top + "px", 1090 width: PLAYER_COLLIDER_WIDTH + "px", 1091 height: PLAYER_COLLIDER_HEIGHT + "px" 1092 }); 1093 $pEl.attr("title", "player"); 1094 $parent.append($pEl); 1095 this._colliderDebugEls.push($pEl[0]); 1096 } 1097 } 1098 1099 // Utility: log current player center position to console for collider authoring 1100 logPlayerPositionOnce() { 1101 if (!this.player) return; 1102 var pos = this.player.getPosition(); 1103 console.log( 1104 "WORLD_COLLIDER_POINT:", 1105 Math.round(pos.x), 1106 Math.round(pos.y) 1107 ); 1108 } 1109 1110 // Utility: log player and camera state (for debugging) 1111 logCameraState() { 1112 if (!this.player) return; 1113 var pos = this.player.getPosition(); 1114 var $parent = this._getSceneElement(); 1115 var ml = $parent.length ? parseInt($parent.css("margin-left"), 10) || 0 : 0; 1116 var mt = $parent.length ? parseInt($parent.css("margin-top"), 10) || 0 : 0; 1117 console.log( 1118 "[CameraLog] player=", pos.x.toFixed(1), pos.y.toFixed(1), 1119 "offset=", this._cameraOffset.x.toFixed(1), this._cameraOffset.
1119y.toFixed(1), 1120 "margin=", ml.toFixed(1), mt.toFixed(1), 1121 "scene=", this.sceneName 1122 ); 1123 } 1124 1125 // Utility: pan camera by a fixed offset (in world pixels) 1126 panCamera(dx, dy) { 1127 this._cameraOffset.x += dx; 1128 this._cameraOffset.y += dy; 1129 // Re-apply camera immediately 1130 this.syncToDom(); 1131 if (this._colliderDebugEnabled) { 1132 this.renderColliderDebugOverlays(); 1133 } 1134 } 1135 1136 // Utility: set camera offset absolutely (used for mouse dragging) 1137 setCameraOffset(x, y) { 1138 this._cameraOffset.x = x; 1139 this._cameraOffset.y = y; 1140 this.syncToDom(); 1141 if (this._colliderDebugEnabled) { 1142 this.renderColliderDebugOverlays(); 1143 } 1144 } 1145 1146 getCameraOffset() { 1147 return { x: this._cameraOffset.x, y: this._cameraOffset.y }; 1148 } 1149 1150 _setSceneVisibility(sceneName) { 1151 if (sceneName === "cave") { 1152 $("#cave").css("display", "block"); 1153 $("#map").css("display", "none"); 1154 $("#tent1").css("display", "none"); 1155 } else if (sceneName === "tent1") { 1156 $("#tent1").css("display", "block"); 1157 $("#map").css("display", "none"); 1158 $("#cave").css("display", "none"); 1159 // Close timeline modal if open when leaving cave 1160 if (typeof closeTimelineModal === "function") { 1161 closeTimelineModal(); 1162 } 1163 } else { 1164 $("#map").css("display", "block"); 1165 $("#tent1").css("display", "none"); 1166 $("#cave").css("display", "none"); 1167 // Close timeline modal if open when leaving cave 1168 if (typeof closeTimelineModal === "function") { 1169 closeTimelineModal(); 1170 } 1171 // Re-apply visibility state of collectible items based on inventory 1172 this._updateCollectibleVisibility(); 1173 } 1174 } 1175 1176 /** 1177 * Update visibility of collectible items based on inventory state. 1178 * This ensures items that have been collected remain hidden when returning to the map. 1179 */ 1180 _updateCollectibleVisibility() { 1181 if (typeof inventory === "undefined") return; 1182 1183 // Fishing rod 1184 if (inventory.fishingRod) { 1185 $(".fishing-rod").css("display", "none"); 1186 } else { 1187 $(".fishing-rod").css("display", "block"); 1188 } 1189 1190 // Matchbox 1191 if (inventory.matchbox) { 1192 $(".matchbox").css("display", "none"); 1193 } else { 1194 $(".matchbox").css("display", "block"); 1195 } 1196 1197 // Resume/Paper 1198 if (inventory.resume) { 1199 $(".paper").css("display", "none"); 1200 } else { 1201 $(".paper").css("display", "block"); 1202 } 1203 1204 // Minimap 1205 if (inventory.minimap) { 1206 $(".scroll").css("display", "none"); 1207 } else { 1208 $(".scroll").css("display", "block"); 1209 } 1210 1211 // Axe (stump state) 1212 if (inventory.axe) { 1213 $("#stump").css("background-image", "url(" + (typeof URL !== "undefined" ? URL.getNature() : "") + "/stump.png)"); 1214 } else { 1215 $("#stump").css("background-image", "url(" + (typeof URL !== "undefined" ? URL.getNature() : "") + "/stump-with-axe.png)"); 1216 } 1217 1218 // Wood log 1219 if (inventory.wood) { 1220 $("#wood-log").css("display", "none"); 1221 } 1222 1223 // Fishbook 1224 if (inventory.fishbook) { 1225 $(".book-item").css("display", "none"); 1226 } else { 1227 $(".book-item").css("display", "block"); 1228 } 1229 1230 // Coins - hide collected coins 1231 if (inventory.coins && Array.isArray(inventory.coins)) { 1232 inventory.coins.forEach(function(coinId) { 1233 $(".coin-item[data-coin-id='" + coinId + "']").remove(); 1234 }); 1235 } 1236 } 1237 1238 _moveDylanToActiveScene() { 1239 var $dylan = $("#dylan"); 1240 if (!$dylan.length) return; 1241 1242 var targetContainer; 1243 if (this.sceneName === "cave") { 1244 targetContainer = $("#cave"); 1245 } else if (this.sceneName === "tent1") { 1246 targetContainer = $("#tent1"); 1247 } else { 1248 targetContainer = $("#map"); 1249 } 1250 1251 // Only move if Dylan is not already in the target container 1252 if (targetContainer.length && $dylan.parent()[0] !== targetContainer[0]) { 1253 targetContainer.append($dylan); 1254 } 1255 1256 // Ensure Dylan is visible 1257 $dylan.css({ 1258 "visibility": "visible", 1259 "display": "block" 1260 }); 1261 } 1262 1263 _getSceneElement() { 1264 var selector = 1265 this.sceneName === "tent1" 1266 ? "#tent1" 1267 : this.sceneName === "cave" 1268 ? "#cave" 1269 : "#map"; 1270 var $el = $(selector); 1271 if ($el.length) return $el; 1272 return $("#map"); 1273 } 1274 1275 // Intro animation: make Dylan fall in from above the viewport 1276 _playIntroDrop() { 1277 var $dylan = $("#dylan"); 1278 if (!$dylan.length) return; 1279 $dylan.removeClass("intro-drop"); 1280 // Force reflow so animation can restart if needed 1281 void $dylan[0].offsetWidth; 1282 $dylan.addClass("intro-drop"); 1283 // Show player immediately as drop starts 1284 $dylan.css("visibility", "visible"); 1285 // Disable movement during intro 1286 if (global.playerController) { 1287 global.playerController.disableInput = true;
1288 global.playerController.keysDown = {}; 1289 global.playerController.inputState = { x: 0, y: 0 }; 1290 } 1291 if (this.player && typeof this.player.setVelocity === "function") { 1292 this.player.setVelocity({ x: 0, y: 0 }); 1293 } 1294 // Clean up after animation finishes to avoid lingering transforms 1295 setTimeout(function () { 1296 $dylan.removeClass("intro-drop"); 1297 if (global.playerController) { 1298 global.playerController.disableInput = false; 1299 } 1300 }, INTRO_DURATION_MS); 1301 } 1302 1303 // Utility: recenter camera on the player's current world position. 1304 // Clears manual pan offsets and lets syncToDom recompute from physics. 1305 centerCameraOnPlayer() { 1306 this._cameraOffset.x = 0; 1307 this._cameraOffset.y = 0; 1308 this.syncToDom(); 1309 if (this._colliderDebugEnabled) { 1310 this.renderColliderDebugOverlays(); 1311 } 1312 } 1313 1314 // Wire scene transitions from trigger definitions 1315 _wireSceneTriggers(cfg) { 1316 var self = this; 1317 if (!cfg || !cfg.triggers || !cfg.triggers.length) return; 1318 cfg.triggers.forEach(function (t) { 1319 if (!t.tag || !t.nextScene) return; 1320 var eventName = "enter:" + t.tag; 1321 self.collisionWorld.on(eventName, function () { 1322 self.changeScene(t.nextScene, { 1323 spawnOverride: t.nextSpawn || null 1324 }); 1325 }); 1326 }); 1327 } 1328 1329 // ---- Sprite animation based on input direction ---- 1330 1331 updateAnimation(deltaMs, input) { 1332 var dx = input && typeof input.x === "number" ? input.x : 0; 1333 var dy = input && typeof input.y === "number" ? input.y : 0; 1334 1335 var isMoving = dx !== 0 || dy !== 0; 1336 var dir = this._lastDir; 1337 1338 if (isMoving) { 1339 // Choose dominant axis for direction 1340 if (Math.abs(dx) > Math.abs(dy)) { 1341 dir = dx < 0 ? "left" : "right"; 1342 } else { 1343 dir = dy < 0 ? "up" : "down"; 1344 } 1345 } 1346 1347 // If direction changed, reset animation frame 1348 if (dir !== this._lastDir) { 1349 this._animFrame = 2; 1350 this._animAccum = 0; 1351 this._lastDir = dir; 1352 } 1353 1354 if (isMoving) { 1355 this._animAccum += deltaMs; 1356 var frameDuration = 90; // ms per frame (slightly snappier) 1357 if (this._animAccum >= frameDuration) { 1358 this._animAccum = 0; 1359 this._animFrame++; 1360 if (this._animFrame > 7) { 1361 this._animFrame = 2; // cycle frames 2..7 1362 } 1363 } 1364 } else { 1365 // Idle pose: stand on frame 2 1366 this._animFrame = 2; 1367 } 1368 1369 // Apply sprite frame if SpriteManager is available 1370 var frame = this._animFrame; 1371 var facing; 1372 switch (dir) { 1373 case "up": 1374 facing = "back"; 1375 break; 1376 case "down": 1377 facing = "front"; 1378 break; 1379 case "left": 1380 facing = "left"; 1381 break; 1382 case "right": 1383 facing = "right"; 1384 break; 1385 } 1386 1387 // Only update sprite when facing/frame actually change 1388 if (facing !== this._lastFacingApplied || frame !== this._lastFrameApplied) { 1389 this._lastFacingApplied = facing; 1390 this._lastFrameApplied = frame; 1391 1392 if (typeof SpriteManager !== "undefined") { 1393 SpriteManager.setSpriteFrame(facing, frame); 1394 } else { 1395 // Fallback: basic background-image updates if SpriteManager is unavailable 1396 var url = 1397 typeof URL !== "undefined" && URL.getDylan 1398 ? URL.getDylan() 1399 : "resources/images/characters/dylan"; 1400 var suffix = ""; 1401 if (facing === "front") suffix = "/dylan-front-"; 1402 else if (facing === "back") suffix = "/dylan-back-"; 1403 else if (facing === "left") suffix = "/dylan-left-"; 1404 else if (facing === "right") suffix = "/dylan-right-"; 1405 $("#dylan").css( 1406 "background-image", 1407 "url(" + url + suffix + frame + ".png)" 1408 ); 1409 } 1410 } 1411 } 1412 } 1413 1414 // Build tent DOM on-demand (used when entering tent scene) 1415 function buildTentDomIfNeeded() { 1416 var $tent = $("#tent1"); 1417 if (!$tent.length) return; 1418 1419 // Add all tent elements directly to #tent1 (like cave does) 1420 $tent.empty(); 1421 1422 // Add the main tentInterior background container 1423 $tent.append('<div class="tentInterior tent1"></div>'); 1424 // Only add matchbox if it hasn't been collected yet 1425 if (typeof inventory !== "undefined" && !inventory.matchbox) { 1426 $tent.append('<a><div onClick="takeMatchbox()" class="matchbox"></div></a>'); 1427 } 1428 $tent.append('<div class="divider"></div>'); 1429 $tent.append('<div class="tentInterior tent1 door front"></div>'); 1430 $tent.append('<div class="tentInterior tent1 backwall"></div>'); 1431 $tent.append('<div class="tentInterior tent1 divider-wall"></div>'); 1432 $tent.append('<div class="tentInterior tent1 divider-wall bottomLining"></div>'); 1433 $tent.append('<div class="tentInterior tent1 divider-wall topLining"></div>'); 1434 $tent.append('<div class="tentInterior tent1 divider-wall topLining t2"></div>'); 1435 $tent.append('<div class="tentInterior tent1 bottomLining"></div>'); 1436 $tent.append('<div class="tentInterior tent1 topLining"></div>'); 1437 $tent.append('<div class="tentInterior tent1 topLining t2"></div>'); 1438 1439 // Kitchen 1440 $tent.append('<div class="shelf"></div>'); 1441 $tent.append('<div class="kitchen"></div>'); 1442 $tent.append('<div class="kitchen-floor"></div>'); 1443 $tent.append('<div class="backdoor"></div>'); 1444 $tent.append('<div class="doormat back"></div>'); 1445 $tent.append('<div class="kitchen-table"></div>'); 1446 1447 // Center 1448 $tent.append('<a><div onClick="openPaintingLightbox()" class="painting"></div></a>'); 1449 $tent.append('<div class="doormat front"></div>'); 1450
1451 // Den 1452 $tent.append('<a><div onClick="sitOnCouch()" class="couch"></div></a>'); 1453 $tent.append('<a><div onClick="openTVScreen()" class="tv back"><div class="tv front"><p style="position: absolute; left: 13px; color: darkgray; font-size: 0.4rem; top: 10px;">About Me</p></div></div></a>'); 1454 } 1455 1456 // Build cave DOM on-demand (used when entering cave scene) 1457 function buildCaveDomIfNeeded() { 1458 var $cave = $("#cave"); 1459 if (!$cave.length) return; 1460 var $hall = $('<div class="cave-hall"></div>'); 1461 var $sideRocksLeft = $('<div class="cave-side-rocks-left"></div>'); 1462 var $sideRocksRight = $('<div class="cave-side-rocks-right"></div>'); 1463 var $overlayLeft = $('<div class="cave-dark left"></div>'); 1464 var $overlayRight = $('<div class="cave-dark right"></div>'); 1465 var $exit = $('<div class="cave-exit" aria-label="Exit cave"></div>'); 1466 var $exitLightBeam = $('<div class="cave-exit-light-beam"></div>'); 1467 var $exitRocksLeft = $('<div class="cave-exit-rocks-left"></div>'); 1468 var $exitRocksTop = $('<div class="cave-exit-rocks-top"></div>'); 1469 1470 $cave.empty(); 1471 $cave.append($overlayLeft); 1472 $cave.append($overlayRight); 1473 $cave.append($exitLightBeam); 1474 $cave.append($sideRocksLeft); 1475 $cave.append($sideRocksRight); 1476 $cave.append($hall); 1477 $cave.append($exitRocksLeft); 1478 $cave.append($exitRocksTop); 1479 $cave.append($exit); 1480 1481 if (typeof TIMELINE_ENTRIES !== "undefined") { 1482 // Create desktop timeline signs (absolute positioned) 1483 TIMELINE_ENTRIES.forEach(function (entry, idx) { 1484 var side = entry.side === "right" ? "right" : "left"; 1485 var $sign = $('<div class="cave-sign ' + side + '"></div>'); 1486 var year = entry.year || ""; 1487 var title = entry.title || ""; 1488 var desc = entry.description || ""; 1489 var img = entry.image || ""; 1490 1491 if (img) { 1492 var $img = $('<div class="img"></div>'); 1493 $img.css("background-image", "url(" + img + ")"); 1494 $sign.append($img); 1495 } 1496 1497 $sign.append("<div class='year'>" + year + "</div>"); 1498 $sign.append("<div class='title'>" + title + "</div>"); 1499 $sign.append("<div class='desc'>" + desc + "</div>"); 1500 $sign.css("top", (entry.y || 0) + "px"); 1501 $cave.append($sign); 1502 1503 // Add interactive sign aligned with each timeline entry 1504 // Position on left or right side of bridge based on entry side 1505 var $interactiveSign = $('<div class="cave-timeline-sign-interact"></div>'); 1506 // Cave hall is at left: 940px, width: 120px (center at 1000px) 1507 // Position signs closer to center, on the appropriate side 1508 var signX; 1509 if (side === "left") { 1510 signX = 940; // Left side of bridge 1511 } else { 1512 signX = 1028; // Right side of bridge, closer to center 1513 } 1514 // Align sign vertically with the timeline entry (same Y position, adjusted 4px up) 1515 var signY = (entry.y || 0) - 10; 1516 $interactiveSign.css({ 1517 "left": signX + "px", 1518 "top": signY + "px", 1519 "z-index": "9998" // Default z-index (behind player's 9999) 1520 }); 1521 $interactiveSign.attr("data-timeline-index", idx); 1522 $interactiveSign.attr("data-sign-y", signY); // Store Y position for z-index calculation 1523 1524 $cave.append($interactiveSign); 1525 }); 1526 1527 // Create timeline modal 1528 var $timelineModal = $('<div class="timeline-modal"></div>'); 1529 var $timelineModalOverlay = $('<div class="timeline-modal-overlay"></div>'); 1530 var $timelineModalContent = $('<div class="timeline-modal-content"></div>'); 1531 var $timelineModalClose = $('<a class="timeline-modal-close" onClick="closeTimelineModal()"><i class="nes-icon close"></i></a>'); 1532 1533 $timelineModalContent.append($timelineModalClose); 1534 $timelineModal.append($timelineModalOverlay); 1535 $timelineModal.append($timelineModalContent); 1536 $('body').append($timelineModal); 1537 1538 // Add click handlers for interactive signs using event delegation
1539 // Use native addEventListener to avoid jQuery dependency issues 1540 document.addEventListener('click', function(e) { 1541 var target = e.target; 1542 var signElement = null; 1543 1544 // Check if clicked element or its parent is the interactive sign 1545 while (target && target !== document.body) { 1546 if (target.classList && target.classList.contains('cave-timeline-sign-interact')) { 1547 signElement = target; 1548 break; 1549 } 1550 target = target.parentElement; 1551 } 1552 1553 if (signElement) { 1554 var index = parseInt(signElement.getAttribute("data-timeline-index")); 1555 if (!isNaN(index) && typeof TIMELINE_ENTRIES !== "undefined" && TIMELINE_ENTRIES[index]) { 1556 if (typeof openTimelineModal === "function") { 1557 openTimelineModal(TIMELINE_ENTRIES[index]); 1558 } 1559 } 1560 } 1561 }); 1562 1563 // Close modal on overlay click 1564 var overlayElement = $timelineModalOverlay[0] || $timelineModalOverlay; 1565 if (overlayElement && overlayElement.addEventListener) { 1566 overlayElement.addEventListener('click', function() { 1567 if (typeof closeTimelineModal === "function") { 1568 closeTimelineModal(); 1569 } 1570 }); 1571 } 1572 } 1573 } 1574 1575 global.GameWorld = GameWorld; 1576})(window); 1577 1578
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.