1// ============================================================================= 2// PHASER SCENE â CabinScene 3// ============================================================================= 4class CabinScene extends Phaser.Scene { 5 constructor() { 6 super({ key: "CabinScene" }); 7 } 8 9 preload() { 10 ["female-crew", "male-crew"].forEach((g) => { 11 ["south", "north", "east", "west"].forEach((d) => { 12 this.load.image(`${g}-${d}`, `sprites/${g}-${d}.png`); 13 }); 14 }); 15 this.load.image("bubble_oj", "sprites/bubble_oj.png"); 16 this.load.image("bubble_water", "sprites/bubble_water.png"); 17 this.load.image("bubble_wine", "sprites/bubble_wine.png"); 18 this.load.image("bubble_sleeping", "sprites/bubble_sleeping.png"); 19 this.load.image("bubble_emptycup", "sprites/bubble_emptycup.png"); 20 this.load.audio("landing_announcement", "audio/landing_announcement.mp3"); 21 this.load.audio("cabin_ambience", "audio/cabin_ambience.mp3"); 22 this.load.audio("sfx_correct", "audio/sfx_correct.mp3"); 23 this.load.audio("sfx_wrong", "audio/sfx_wrong.mp3"); 24 this.load.audio("sfx_collect", "audio/sfx_collect.mp3"); 25 this.load.audio("sfx_seatbelt_chime", "audio/sfx_seatbelt_chime.mp3"); 26 this.load.audio("turbulence", "audio/turbulence.mp3"); 27 this.load.on("loaderror", (file) => { 28 console.warn("Asset failed to load:", file.key); 29 }); 30 this.load.atlas( 31 "passengers", 32 "sprites/passengers.png", 33 "sprites/passengers.json", 34 ); 35 generateTextures(this); 36 } 37 38 create() { 39 this.ensureFallbackSprites(); 40 this.routeKey = window.selectedRoute || "KUL"; 41 this.route = ROUTES[this.routeKey] || ROUTES.KUL; 42 this.paxMap = buildPaxMap(this.routeKey); 43 this.score = 0; 44 this.remainingSec = this.route.timerSeconds; 45 this.phase = "idle"; 46 this.facing = "east"; 47 this.selectedSeatIdx = null; 48 this.selectedServiceTarget = null; 49 this.serviceSideChosen = false; 50 this.promptState = null; 51 52 // ------------------------------------------------------------------------- 53 // FIX â Issue 3: Reset all persistent DOM HUD elements on every new game. 54 // The #hud-timer "is-warning" CSS class (red/pulsing) is only ever added, 55 // never removed â so it carries over into subsequent playthroughs. 56 // We also reset the timer label since Phaser scene restarts do not touch 57 // DOM elements outside the canvas. 58 // ------------------------------------------------------------------------- 59 const timerEl = document.getElementById("hud-timer"); 60 if (timerEl) { 61 timerEl.classList.remove("is-warning"); 62 timerEl.textContent = "04:00"; 63 } 64 const scoreEl = document.getElementById("hud-score"); 65 if (scoreEl) scoreEl.textContent = "SCORE: 0"; 66 const phaseEl = document.getElementById("hud-phase"); 67 if (phaseEl) phaseEl.textContent = "PHASE: BOARDING"; 68 const hintEl = document.getElementById("hint-bar"); 69 if (hintEl) hintEl.textContent = "SPACEBAR â Begin service"; 70 const armedEl = document.getElementById("hud-armed"); 71 if (armedEl) { 72 armedEl.textContent = "DOORS TO ARMED"; 73 armedEl.style.color = "#ff4444"; 74 } 75 76 this.add.rectangle(CW / 2, CH / 2, CW, CH, C.bg); 77 this.drawFuselage(); 78 this.drawGalley(); 79 this.drawAisle(); 80 this.drawCabin(); 81 this.drawCockpit(); 82 this.aisleTint = this.add.graphics(); 83 this.bubbleByKey = {}; 84 this.bubbleWindowStartDisplay = 1; 85 this.playerCol = TC.aisle; 86 this.playerRow = TR.cabin + 7; 87 this.drawCrew(); 88 this.drawLabels(); 89 90 this.keyMap = this.input.keyboard.addKeys({ 91 up: Phaser.Input.Keyboard.KeyCodes.UP, 92 down: Phaser.Input.Keyboard.KeyCodes.DOWN, 93 w: Phaser.Input.Keyboard.KeyCodes.W, 94 s: Phaser.Input.Keyboard.KeyCodes.S, 95 left: Phaser.Input.Keyboard.KeyCodes.LEFT, 96 right: Phaser.Input.Keyboard.KeyCodes.RIGHT, 97 a: Phaser.Input.Keyboard.KeyCodes.A, 98 d: Phaser.Input.Keyboard.KeyCodes.D, 99 space: Phaser.Input.Keyboard.KeyCodes.SPACE, 100 c: Phaser.Input.Keyboard.KeyCodes.C, 101 esc: Phaser.Input.Keyboard.KeyCodes.ESC, 102 shift: Phaser.Input.Keyboard.KeyCodes.SHIFT, 103 one: Phaser.Input.Keyboard.KeyCodes.ONE, 104 two: Phaser.Input.Keyboard.KeyCodes.TWO, 105 three: Phaser.Input.Keyboard.KeyCodes.THREE, 106 }); 107 108 this.trolley = null; 109 this.collectionWrapUpStarted = false;
110 this.winSceneScheduled = false; 111 this.serviceEntryComplete = true; 112 this.setPhase("idle"); 113 this.updateHudMeta(); 114 this.updateTimerHud(); 115 this.updateScore(0); 116 this.updateHintBar(); 117 118 this.soundAmbience = this.cache.audio.exists("cabin_ambience") 119 ? this.sound.add("cabin_ambience", { loop: true, volume: 0.25 }) 120 : null; 121 this.sfxCorrect = this.cache.audio.exists("sfx_correct") 122 ? this.sound.add("sfx_correct", { volume: 0.6 }) 123 : null; 124 this.sfxWrong = this.cache.audio.exists("sfx_wrong") 125 ? this.sound.add("sfx_wrong", { volume: 0.6 }) 126 : null; 127 this.sfxCollect = this.cache.audio.exists("sfx_collect") 128 ? this.sound.add("sfx_collect", { volume: 0.6 }) 129 : null; 130 this.turbulenceSound = this.cache.audio.exists("turbulence") 131 ? this.sound.add("turbulence", { volume: 0.55 }) 132 : null; 133 } 134 135 ensureFallbackSprites() { 136 const dirs = ["south", "north", "east", "west"]; 137 const makeFallback = (key, bodyColor) => { 138 if (this.textures.exists(key)) return; 139 const g = this.make.graphics({ x: 0, y: 0, add: false }); 140 g.fillStyle(bodyColor, 1); 141 g.fillRect(10, 18, 28, 24); 142 g.fillStyle(C.crewSkin, 1); 143 g.fillRect(14, 6, 20, 12); 144 g.generateTexture(key, 48, 48); 145 g.destroy(); 146 }; 147 for (let i = 0; i < dirs.length; i++) { 148 makeFallback(`female-crew-${dirs[i]}`, 0x00ff7f); 149 makeFallback(`male-crew-${dirs[i]}`, 0x44aaff); 150 } 151 if (!this.textures.exists("passengers")) { 152 const fallbackFrames = [ 153 "pax-intern-a", 154 "pax-bizlady-a", 155 "pax-tourist-a", 156 "pax-elderly-a", 157 "pax-backpacker-a", 158 ]; 159 for (let i = 0; i < fallbackFrames.length; i++) { 160 const key = fallbackFrames[i]; 161 if (this.textures.exists(key)) continue; 162 const g = this.make.graphics({ x: 0, y: 0, add: false }); 163 g.fillStyle(0x63708a + i * 0x060606, 1); 164 g.fillRect(10, 18, 28, 24); 165 g.fillStyle(0xe0bd9a, 1); 166 g.fillRect(14, 6, 20, 12); 167 g.generateTexture(key, 48, 48); 168 g.destroy(); 169 } 170 } 171 } 172 173 update() { 174 if (!this.serviceEntryComplete) return; 175 if (this.handleMovement()) { 176 this.syncPassengerBubbles(); 177 if (this.phase === "collection") this.syncCollectionBubbles(); 178 } 179 this.handleFacingAndSelection(); 180 this.handleInteractionKeys(); 181 } 182 183 handleMovement() { 184 if (this.phase === "landing") return false; 185 const k = this.keyMap; 186 const moveUp = 187 Phaser.Input.Keyboard.JustDown(k.up) || 188 Phaser.Input.Keyboard.JustDown(k.w); 189 const moveDown = 190 Phaser.Input.Keyboard.JustDown(k.down) || 191 Phaser.Input.Keyboard.JustDown(k.s); 192 let moved = false; 193 if (moveUp && this.playerRow > TR.galley) { 194 this.playerRow -= 1; 195 this.setCrewDirection("north"); 196 moved = true; 197 } 198 if (moveDown && this.playerRow <= TR.cabin + 7) { 199 this.playerRow += 1; 200 if (this.phase === "service" || this.phase === "collection") { 201 this.setCrewDirection("north"); 202 } else { 203 this.setCrewDirection("south"); 204 } 205 moved = true; 206 } 207 if (moved) { 208 this.syncCrewPosition(); 209 if (this.phase === "service") { 210 this.selectedServiceTarget = null; 211 this.promptState = null; 212 } else if (this.phase === "collection") { 213 this.selectedSeatIdx = null; 214 this.promptState = null; 215 } 216 } 217 return moved; 218 } 219 220 handleFacingAndSelection() { 221 const k = this.keyMap; 222 if (Phaser.Input.Keyboard.JustDown(k.a)) { 223 this.facing = "west"; 224 this.setCrewDirection("west"); 225 if (this.phase === "service") { 226 this.serviceSideChosen = true; 227 this.selectedServiceTarget = null; 228 } else { 229 this.selectedSeatIdx = null; 230 } 231 this.syncPassengerBubbles(); 232 if (this.phase === "collection") this.syncCollectionBubbles(); 233 } 234 if (Phaser.Input.Keyboard.JustDown(k.d)) { 235 this.facing = "east"; 236 this.setCrewDirection("east"); 237 if (this.phase === "service") { 238 this.serviceSideChosen = true; 239 this.selectedServiceTarget = null; 240 } else { 241 this.selectedSeatIdx = null; 242 } 243 this.syncPassengerBubbles(); 244 if (this.phase === "collection") this.syncCollectionBubbles(); 245 } 246 if (Phaser.Input.Keyboard.JustDown(k.left)) { 247 if (this.phase === "service" && !this.serviceSideChosen) return; 248 this.moveSeatSelection(-1); 249 } 250 if (Phaser.Input.Keyboard.JustDown(k.right)) { 251 if (this.phase === "service" && !this.serviceSideChosen) return; 252 this.moveSeatSelection(1); 253 } 254 } 255 256 handleInteractionKeys() {
257 const k = this.keyMap; 258 if (Phaser.Input.Keyboard.JustDown(k.esc)) { 259 this.promptState = null; 260 this.selectedSeatIdx = null; 261 this.selectedServiceTarget = null; 262 if (this.phase === "service") this.serviceSideChosen = false; 263 this.updateHintBar(); 264 this.syncPassengerBubbles(); 265 if (this.phase === "collection") this.syncCollectionBubbles(); 266 return; 267 } 268 if (this.phase === "idle" && Phaser.Input.Keyboard.JustDown(k.space)) { 269 this.startServicePhase(); 270 return; 271 } 272 if (this.phase === "service") { 273 this.handleServiceInputs(); 274 return; 275 } 276 if (this.phase === "collection") { 277 this.handleCollectionInputs(); 278 return; 279 } 280 } 281 282 handleServiceInputs() { 283 const k = this.keyMap; 284 const seat = this.getSelectedSeat(); 285 if (!seat || !seat.occ || seat.served) return; 286 287 if (Phaser.Input.Keyboard.JustDown(k.shift)) { 288 if (seat.state === "sleeping" || seat.state === "nothanks") { 289 seat.served = true; 290 this.updateAfterServiceResolution(); 291 } 292 return; 293 } 294 295 if (Phaser.Input.Keyboard.JustDown(k.space)) { 296 this.promptState = "service"; 297 this.updateHintBar(); 298 return; 299 } 300 301 if (this.promptState !== "service") return; 302 303 const keyToItem = { 1: "oj", 2: "water", 3: "wine" }; 304 const pressed = this.getPressedServiceKey(); 305 if (!pressed) return; 306 const chosen = keyToItem[pressed]; 307 if (!chosen) return; 308 309 if (seat.state === "sleeping") { 310 this.updateScore(SERVICE_POINTS.sleepingWake); 311 this.promptState = null; 312 this.updateHintBar(); 313 return; 314 } 315 316 if (seat.state === chosen) { 317 if (this.sfxCorrect) { 318 this.sfxCorrect.stop(); 319 this.sfxCorrect.play(); 320 } 321 this.updateScore(SERVICE_POINTS.drink); 322 seat.served = true; 323 seat.hasCup = true; 324 this.promptState = null; 325 this.updateAfterServiceResolution(); 326 } else { 327 if (this.sfxWrong) { 328 this.sfxWrong.stop(); 329 this.sfxWrong.play(); 330 } 331 this.updateScore(SERVICE_POINTS.wrongItem); 332 this.promptState = null; 333 this.updateHintBar(); 334 } 335 } 336 337 handleCollectionInputs() { 338 const k = this.keyMap; 339 340 if (Phaser.Input.Keyboard.JustDown(k.space)) { 341 const seat = this.getSelectedSeat(); 342 if (!seat) return; 343 const isCollectable = seat.hasCup && !seat.cupCollected; 344 const isSleeping = seat.state === "sleeping" && !seat.cupCollected; 345 if (!isCollectable && !isSleeping) return; 346 this.promptState = "collection"; 347 this.updateHintBar(); 348 return; 349 } 350 351 if (this.promptState !== "collection") return; 352 353 const seat = this.getSelectedSeat(); 354 if (!seat) return; 355 356 if (Phaser.Input.Keyboard.JustDown(k.c)) { 357 if (seat.state === "sleeping") { 358 this.updateScore(-10); 359 this.promptState = null; 360 this.updateHintBar(); 361 return; 362 } 363 if (seat.hasCup && !seat.cupCollected) { 364 if (this.sfxCollect) { 365 this.sfxCollect.stop(); 366 this.sfxCollect.play(); 367 } 368 seat.cupCollected = true; 369 this.updateScore(SCORE_RULES.collectionGood); 370 this.promptState = null; 371 this.updateHintBar(); 372 this.syncCollectionBubbles(); 373 this.tryCompleteCollectionEarly(); 374 } 375 return; 376 } 377 378 if (Phaser.Input.Keyboard.JustDown(k.shift)) { 379 seat.cupCollected = true; 380 this.promptState = null; 381 this.updateHintBar(); 382 this.syncCollectionBubbles(); 383 this.tryCompleteCollectionEarly(); 384 } 385 } 386 387 getPressedServiceKey() { 388 const k = this.keyMap; 389 if (Phaser.Input.Keyboard.JustDown(k.one)) return 1; 390 if (Phaser.Input.Keyboard.JustDown(k.two)) return 2; 391 if (Phaser.Input.Keyboard.JustDown(k.three)) return 3; 392 return null; 393 } 394 395 getCurrentRowNum() { 396 return this.playerRow - TR.cabin + 1; 397 } 398 399 seatColForSeatIndex(seatIdx) { 400 return [...SEAT_COLS_LEFT, ...SEAT_COLS_RIGHT][seatIdx]; 401 } 402 403 getSelectableSeatIndices() { 404 if (this.phase === "service") return this.getSelectableServiceTargets(); 405 const rowNum = this.getCurrentRowNum(); 406 if (rowNum < 1 || rowNum > CABIN_ROWS) return []; 407 if (this.phase === "collection") { 408 const arr = this.facing === "east" ? [3, 4, 5] : [0, 1, 2]; 409 const seen = new Set(); 410 const result = []; 411 for (let i = 0; i < arr.length; i++) { 412 const idx = arr[i]; 413 if (seen.has(idx)) continue; 414 const seat = this.paxMap[rowNum][idx]; 415 if (!seat.occ) continue; 416 if ( 417 (seat.hasCup && !seat.cupCollected) || 418 (seat.state === "sleeping" && !seat.cupCollected) 419 ) { 420 result.push(idx); 421 seen.add(idx); 422 } 423 } 424 return result; 425 } 426 return []; 427 } 428 429 moveSeatSelection(delta) {
430 const seats = this.getSelectableSeatIndices(); 431 if (!seats.length) { 432 if (this.phase === "service") this.selectedServiceTarget = null; 433 else { 434 this.selectedSeatIdx = null; 435 if (this.phase === "collection") this.syncCollectionBubbles(); 436 } 437 return; 438 } 439 const current = 440 this.phase === "service" 441 ? this.selectedServiceTarget 442 : this.selectedSeatIdx; 443 let i = seats.indexOf(current); 444 if (i < 0) i = 0; 445 i = Phaser.Math.Wrap(i + delta, 0, seats.length); 446 if (this.phase === "service") { 447 this.selectedServiceTarget = seats[i]; 448 this.syncPassengerBubbles(); 449 } else { 450 this.selectedSeatIdx = seats[i]; 451 if (this.phase === "collection") this.syncCollectionBubbles(); 452 } 453 } 454 455 getSelectedSeat() { 456 if (this.phase === "service") { 457 if (!this.selectedServiceTarget) return null; 458 const [rowText, seatText] = this.selectedServiceTarget.split("-"); 459 const rowNum = Number(rowText); 460 const seatIdx = Number(seatText); 461 if (rowNum < 1 || rowNum > CABIN_ROWS || seatIdx < 0 || seatIdx > 5) 462 return null; 463 return this.paxMap[rowNum][seatIdx]; 464 } 465 const rowNum = this.getCurrentRowNum(); 466 if (rowNum < 1 || rowNum > CABIN_ROWS || this.selectedSeatIdx == null) 467 return null; 468 return this.paxMap[rowNum][this.selectedSeatIdx]; 469 } 470 471 updateAfterServiceResolution() { 472 this.checkBubbleWindowAdvance(); 473 const liveTargets = this.getSelectableServiceTargets(); 474 if (!liveTargets.includes(this.selectedServiceTarget)) 475 this.selectedServiceTarget = null; 476 this.syncPassengerBubbles(); 477 this.updateHintBar(); 478 479 if (this.allServiceResolved() && this.phase === "service") { 480 const bonus = Math.max(0, this.remainingSec - 100); 481 if (bonus > 0) this.updateScore(bonus); 482 this.remainingSec = 100; 483 this.updateTimerHud(); 484 this.showServicePopup(); 485 this.enterCollectionPhase(); 486 } 487 } 488 489 getSelectableServiceTargets() { 490 const rowNum = this.getCurrentRowNum(); 491 if (rowNum < 1 || rowNum > CABIN_ROWS) return []; 492 const side = this.facing === "west" ? [0, 1, 2] : [3, 4, 5]; 493 const targets = []; 494 for (let s = 0; s < side.length; s++) { 495 const seatIdx = side[s]; 496 const seat = this.paxMap[rowNum][seatIdx]; 497 if (seat.occ && !seat.served && seat.state) 498 targets.push(`${rowNum}-${seatIdx}`); 499 } 500 return targets; 501 } 502 503 checkBubbleWindowAdvance() { 504 const d = this.bubbleWindowStartDisplay; 505 const rowNum = 9 - d; 506 if (rowNum < 1 || rowNum > CABIN_ROWS) return; 507 const row = this.paxMap[rowNum]; 508 const done = row.every((s) => !s.occ || s.served); 509 if (done) this.bubbleWindowStartDisplay = Math.min(7, d + 1); 510 } 511 512 setPhase(phase) { 513 this.phase = phase; 514 const labels = { 515 idle: "BOARDING", 516 service: "SERVICE", 517 collection: "COLLECTION", 518 landing: "LANDING", 519 }; 520 const el = document.getElementById("hud-phase"); 521 if (el) el.textContent = `PHASE: ${labels[phase] || phase.toUpperCase()}`; 522 if (phase === "service") this.setAisleTint(true); 523 if (phase === "landing") { 524 this.setAisleTint(false); 525 if (this.soundAmbience && this.soundAmbience.isPlaying) { 526 this.soundAmbience.stop(); 527 } 528 } 529 if (phase !== "service") { 530 this.fadeOutAllPassengerBubbles(); 531 this.serviceSideChosen = false; 532 this.selectedServiceTarget = null; 533 } 534 this.updateHintBar(); 535 } 536 537 stopGamestartMusic() { 538 const s = this.registry.get("gamestartSound"); 539 if (s) { 540 s.stop(); 541 s.destroy(); 542 this.registry.remove("gamestartSound"); 543 } 544 } 545 546 startServicePhase() { 547 this.stopGamestartMusic(); 548 if (!this.trolley) { 549 this.trolley = this.add 550 .image(tx(TC.aisle) + TILE / 2, ty(TR.galley) + TILE / 2 + 4, "trolley") 551 .setDepth(10); 552 } 553 this.setPhase("service"); 554 if (this.soundAmbience && !this.soundAmbience.isPlaying) { 555 this.soundAmbience.play(); 556 } 557 this.startTimer(); 558 this.serviceEntryComplete = false; 559 this.setCrewDirection("north"); 560 this.tweens.add({ 561 targets: this.crewSprite, 562 y: ty(TR.galley + 1) + TILE / 2, 563 duration: 240, 564 onComplete: () => { 565 this.playerRow = TR.galley + 1; 566 this.syncCrewPosition(); 567 this.serviceEntryComplete = true; 568 this.bubbleWindowStartDisplay = 1; 569 this.serviceSideChosen = false;
570 this.selectedServiceTarget = null; 571 this.syncPassengerBubbles(); 572 }, 573 }); 574 this.scheduleTurbulence(); 575 } 576 577 startTimer() { 578 if (this.timerEvent) return; 579 this.timerEvent = this.time.addEvent({ 580 delay: 1000, 581 loop: true, 582 callback: () => { 583 if (this.remainingSec <= 0) return; 584 this.remainingSec -= 1; 585 this.updateTimerHud(); 586 this.checkPhaseCheckpoints(); 587 }, 588 }); 589 } 590 591 checkPhaseCheckpoints() { 592 if (this.remainingSec <= 60) { 593 const timerEl = document.getElementById("hud-timer"); 594 if (timerEl) timerEl.classList.add("is-warning"); 595 } 596 if (this.remainingSec === 100 && this.phase === "service") { 597 this.showServicePopup(); 598 this.enterCollectionPhase(); 599 } 600 if ( 601 this.remainingSec === 30 && 602 this.phase === "collection" && 603 !this.collectionWrapUpStarted 604 ) { 605 this.finishCollectionPhase(); 606 } 607 } 608 609 showServicePopup() { 610 const allServed = this.allServiceResolved(); 611 const txt = allServed 612 ? "Great job! Time to collect back what we have served out." 613 : "Please buck up on your service speed for the next flight."; 614 const popup = this.add 615 .text(CW / 2, CH / 2, txt, { 616 fontFamily: '"Press Start 2P"', 617 fontSize: "9px", 618 color: "#1b2a4a", 619 backgroundColor: "#f5f0e8", 620 padding: { x: 12, y: 10 }, 621 align: "center", 622 wordWrap: { width: 600 }, 623 }) 624 .setOrigin(0.5) 625 .setDepth(300); 626 this.serviceEntryComplete = false; 627 this.time.delayedCall(2800, () => { 628 popup.destroy(); 629 this.serviceEntryComplete = true; 630 }); 631 } 632 633 enterCollectionPhase() { 634 this.collectionWrapUpStarted = false; 635 this.selectedSeatIdx = null; 636 this.promptState = null; 637 if (this.trolley) 638 this.trolley.setPosition( 639 tx(TC.aisle) + TILE / 2, 640 ty(TR.galley) + TILE / 2 + 4, 641 ); 642 this.setPhase("collection"); 643 this.syncCollectionBubbles(); 644 } 645 646 allCollectionResolved() { 647 for (let r = 1; r <= CABIN_ROWS; r++) { 648 for (let s = 0; s < 6; s++) { 649 const seat = this.paxMap[r][s]; 650 if (!seat.occ) continue; 651 if (seat.hasCup && !seat.cupCollected) return false; 652 if (seat.state === "sleeping" && !seat.cupCollected) return false; 653 } 654 } 655 return true; 656 } 657 658 tryCompleteCollectionEarly() { 659 if (this.phase !== "collection" || this.collectionWrapUpStarted) return; 660 if (this.allCollectionResolved()) this.finishCollectionPhase(); 661 } 662 663 finishCollectionPhase() { 664 if (this.phase !== "collection" || this.collectionWrapUpStarted) return; 665 this.collectionWrapUpStarted = true; 666 667 const allCupsDone = this.allCollectionResolved(); 668 669 const bonus = Math.max(0, this.remainingSec - 30); 670 if (bonus > 0) this.updateScore(bonus); 671 672 for (let r = 1; r <= CABIN_ROWS; r++) { 673 for (let s = 0; s < 6; s++) { 674 const seat = this.paxMap[r][s]; 675 if (seat.hasCup && !seat.cupCollected && seat.state !== "sleeping") { 676 this.updateScore(SCORE_RULES.collectionMiss); 677 } 678 } 679 } 680 681 const txt = allCupsDone 682 ? "Great! All cups collected. Time to prepare for landing." 683 : "Need to be quicker next time. Time to prepare for landing."; 684 const popup = this.add 685 .text(CW / 2, CH / 2, txt, { 686 fontFamily: '"Press Start 2P"', 687 fontSize: "9px", 688 color: "#1b2a4a", 689 backgroundColor: "#f5f0e8", 690 padding: { x: 12, y: 10 }, 691 align: "center", 692 wordWrap: { width: 600 }, 693 }) 694 .setOrigin(0.5) 695 .setDepth(300); 696 this.serviceEntryComplete = false; 697 this.time.delayedCall(2800, () => { 698 popup.destroy(); 699 this.startLandingSequence(); 700 }); 701 } 702 703 goToWinSceneAfterLanding(delayMs = 0) { 704 if (this.winSceneScheduled) return; 705 this.winSceneScheduled = true; 706 this.time.delayedCall(delayMs, () => { 707 if (this.scene.isActive("CabinScene")) { 708 this.scene.start("WinScene", { score: this.score }); 709 } 710 }); 711 } 712 713 runLandingPatrol() { 714 const moveMs = 380; 715 const lookMs = 420; 716 const returnMoveMs = 520; 717 const aisleX = tx(TC.aisle) + TILE / 2; 718 this.crewSprite.setAlpha(1); 719 this.playerRow = TR.cabin; 720 this.crewSprite.setPosition(aisleX, ty(this.playerRow) + TILE); 721 this.setCrewDirection("north"); 722 723 const fadeCrewOut = () => { 724 this.tweens.add({ 725 targets: this.crewSprite, 726 alpha: 0, 727 duration: 1400, 728 ease: "Sine.easeIn", 729 }); 730 }; 731 732 const runReturn = (rowNum) => { 733 if (rowNum < 1) { 734 this.setCrewDirection("north"); 735 this.playerRow = TR.galley; 736 const galleyY = ty(this.playerRow) + TILE; 737 this.tweens.add({ 738 targets: this.crewSprite, 739 y: galleyY, 740 duration: returnMoveMs, 741 ease: "Sine.easeInOut", 742 onComplete: () => fadeCrewOut(), 743 }); 744 return; 745 } 746 this.setCrewDirection("north"); 747 this.playerRow = TR.cabin + rowNum - 1; 748 const targetY = ty(this.playerRow) + TILE; 749 this.tweens.add({ 750 targets: this.crewSprite, 751 y: targetY, 752 duration: returnMoveMs, 753 ease: "Sine.easeInOut", 754 onComplete: () => { 755 runReturn(rowNum - 1); 756 }, 757 }); 758 }; 759 760 const runRow = (rowNum) => { 761 if (rowNum > CABIN_ROWS) { 762 runReturn(CABIN_ROWS - 1); 763 return; 764 } 765 this.playerRow = TR.cabin + rowNum - 1; 766 const targetY = ty(this.playerRow) + TILE; 767 this.tweens.add({ 768 targets: this.crewSprite, 769 y: targetY, 770 duration: moveMs, 771 ease: "Sine.easeInOut", 772 onComplete: () => { 773 this.setCrewDirection("west"); 774 this.time.delayedCall(lookMs, () => { 775 this.setCrewDirection("east"); 776 this.time.delayedCall(lookMs, () => { 777 runRow(rowNum + 1); 778 }); 779 }); 780 }, 781 }); 782 }; 783 784 runRow(1); 785 } 786 787 startLandingSequence() { 788 this.winSceneScheduled = false;
789 this.serviceEntryComplete = false; 790 this.setPhase("landing"); 791 if (this.trolley) { 792 this.trolley.setVisible(false); 793 } 794 this.cameras.main.shake(1000, 0.004); 795 796 this.runLandingPatrol(); 797 798 const playSeatbeltChimeThenWin = () => { 799 if (!this.cache.audio.exists("sfx_seatbelt_chime")) { 800 this.goToWinSceneAfterLanding(1000); 801 return; 802 } 803 const chime = this.sound.add("sfx_seatbelt_chime", { volume: 0.6 }); 804 chime.once("complete", () => { 805 // Win scene should begin 1s after seatbelt chime ends. 806 this.goToWinSceneAfterLanding(1000); 807 }); 808 chime.play(); 809 }; 810 811 if (this.cache.audio.exists("landing_announcement")) { 812 const announcement = this.sound.add("landing_announcement", { 813 volume: 0.6, 814 }); 815 announcement.play(); 816 announcement.once("complete", () => { 817 playSeatbeltChimeThenWin(); 818 }); 819 } else { 820 playSeatbeltChimeThenWin(); 821 } 822 823 this.time.delayedCall(25000, () => { 824 if (this.scene.isActive("CabinScene")) { 825 this.goToWinSceneAfterLanding(0); 826 } 827 }); 828 } 829 830 scheduleTurbulence() { 831 const t = this.route.turbulence && this.route.turbulence[0]; 832 if (!t) return; 833 this.time.delayedCall(t.elapsedSec * 1000, () => { 834 if (this.phase !== "service") return; 835 this.cameras.main.shake(t.durationSec * 1000, 0.005); 836 if (this.turbulenceSound) { 837 this.turbulenceSound.stop(); 838 this.turbulenceSound.play(); 839 this.time.delayedCall(t.durationSec * 1000, () => { 840 if (this.turbulenceSound && this.turbulenceSound.isPlaying) { 841 this.turbulenceSound.stop(); 842 } 843 }); 844 } 845 const txt = this.add 846 .text(CW / 2, CH / 2 - 40, "WE HIT TURBULENCE\n(not the song)", { 847 fontFamily: '"Press Start 2P"', 848 fontSize: "11px", 849 color: "#ffff44", 850 backgroundColor: "#1b2a4a", 851 padding: { x: 14, y: 10 }, 852 align: "center", 853 }) 854 .setOrigin(0.5) 855 .setDepth(200); 856 const flashTween = this.tweens.add({ 857 targets: txt, 858 alpha: { from: 1, to: 0 }, 859 duration: 400, 860 yoyo: true, 861 repeat: -1, 862 }); 863 this.time.delayedCall(t.durationSec * 1000, () => { 864 flashTween.stop(); 865 txt.destroy(); 866 }); 867 }); 868 } 869 870 allServiceResolved() { 871 for (let r = 1; r <= CABIN_ROWS; r++) { 872 for (let s = 0; s < 6; s++) { 873 const seat = this.paxMap[r][s]; 874 if (seat.occ && !seat.served) return false; 875 } 876 } 877 return true; 878 } 879 880 updateHudMeta() { 881 const routeEl = document.getElementById("hud-route"); 882 if (routeEl) routeEl.textContent = this.route.label; 883 const armed = document.getElementById("hud-armed"); 884 if (armed) { 885 armed.textContent = "DOORS TO ARMED"; 886 armed.style.color = "#ff4444"; 887 } 888 } 889 890 updateTimerHud() { 891 const mm = String(Math.floor(this.remainingSec / 60)).padStart(2, "0"); 892 const ss = String(this.remainingSec % 60).padStart(2, "0"); 893 const el = document.getElementById("hud-timer"); 894 if (el) el.textContent = `${mm}:${ss}`; 895 } 896 897 updateScore(delta) { 898 this.score += delta; 899 const el = document.getElementById("hud-score"); 900 if (el) el.textContent = `SCORE: ${this.score}`; 901 } 902 903 updateHintBar() { 904 const el = document.getElementById("hint-bar"); 905 if (!el) return; 906 if (this.phase === "idle") { 907 el.textContent = "SPACEBAR â Begin service"; 908 } else if (this.phase === "service" && this.promptState !== "service") { 909 el.textContent = 910 "A / D â Face seats | â â â Select | SPACEBAR â Serve | SHIFT â Skip"; 911 } else if (this.phase === "service" && this.promptState === "service") { 912 el.textContent = "1 OJ | 2 WATER | 3 WINE | ESC CANCEL"; 913 } else if ( 914 this.phase === "collection" && 915 this.promptState === "collection" 916 ) { 917 el.textContent = "C â Collect | SHIFT â Skip | ESC â Cancel"; 918 } else if (this.phase === "collection") { 919 el.textContent = 920 "A / D â Face seats | â â â Select | SPACEBAR â Interact | C â Collect | SHIFT â Skip"; 921 } else if (this.phase === "landing") { 922 el.textContent = "Cabin checked and ready for landing..."; 923 } 924 } 925 926 syncCrewPosition() { 927 this.crewSprite.setPosition( 928 tx(TC.aisle) + TILE / 2, 929 ty(this.playerRow) + TILE, 930 ); 931 if ( 932 (this.phase === "service" || this.phase === "collection") && 933 this.trolley 934 ) { 935 this.trolley.setPosition( 936 tx(TC.aisle) + TILE / 2, 937 ty(this.playerRow - 1) + TILE / 2 + 4, 938 ); 939 } 940 } 941 942 setAisleTint(active) { 943 this.aisleTint.clear(); 944 if (!active) return; 945 this.aisleTint.fillStyle(0xd4a96a, 0.35); 946 for (let r = 0; r < CABIN_ROWS; r++) { 947 this.aisleTint.fillRect(tx(TC.aisle), ty(TR.cabin + r), TILE, TILE); 948 } 949 } 950 951 fadeOutAllPassengerBubbles() {
952 const keys = Object.keys(this.bubbleByKey); 953 for (let i = 0; i < keys.length; i++) { 954 const spr = this.bubbleByKey[keys[i]]; 955 if (spr) spr.destroy(); 956 delete this.bubbleByKey[keys[i]]; 957 } 958 } 959 960 syncPassengerBubbles() { 961 if (this.phase !== "service") return; 962 const bubbleSize = Math.round(TILE * 0.82); 963 const bubbleSizeActive = Math.round(TILE * 0.9); 964 const desired = new Set(); 965 const d0 = this.bubbleWindowStartDisplay; 966 const rows = [9 - d0, 9 - (d0 + 1)].filter( 967 (r) => r >= 1 && r <= CABIN_ROWS, 968 ); 969 for (let i = 0; i < rows.length; i++) { 970 const rowNum = rows[i]; 971 for (let seatIdx = 0; seatIdx < 6; seatIdx++) { 972 const seat = this.paxMap[rowNum][seatIdx]; 973 if (seat.occ && !seat.served && seat.state) 974 desired.add(`${rowNum}-${seatIdx}`); 975 } 976 } 977 const keys = Object.keys(this.bubbleByKey); 978 for (let i = 0; i < keys.length; i++) { 979 if (!desired.has(keys[i])) { 980 this.bubbleByKey[keys[i]].destroy(); 981 delete this.bubbleByKey[keys[i]]; 982 } 983 } 984 if ( 985 this.selectedServiceTarget && 986 !desired.has(this.selectedServiceTarget) 987 ) { 988 this.selectedServiceTarget = null; 989 } 990 desired.forEach((key) => { 991 if (this.bubbleByKey[key]) return; 992 const [rowText, seatText] = key.split("-"); 993 const rowNum = Number(rowText); 994 const seatIdx = Number(seatText); 995 const seat = this.paxMap[rowNum][seatIdx]; 996 const col = this.seatColForSeatIndex(seatIdx); 997 const tex = `bubble_${seat.state.split("+")[0]}`; 998 const img = this.add 999 .image(tx(col) + TILE / 2, ty(TR.cabin + rowNum - 1) + 3, tex) 1000 .setOrigin(0.5, 1) 1001 .setDisplaySize(bubbleSize, bubbleSize) 1002 .setDepth(100); 1003 this.bubbleByKey[key] = img; 1004 }); 1005 const allKeys = Object.keys(this.bubbleByKey); 1006 for (let i = 0; i < allKeys.length; i++) { 1007 const key = allKeys[i]; 1008 const bubble = this.bubbleByKey[key]; 1009 if (!bubble) continue; 1010 const active = key === this.selectedServiceTarget; 1011 bubble.setDisplaySize( 1012 active ? bubbleSizeActive : bubbleSize, 1013 active ? bubbleSizeActive : bubbleSize, 1014 ); 1015 bubble.setTint(active ? 0xd6ffad : 0xffffff); 1016 } 1017 } 1018 1019 syncCollectionBubbles() { 1020 if (this.phase !== "collection") return; 1021 const bubbleSize = Math.round(TILE * 0.82); 1022 const bubbleSizeActive = Math.round(TILE * 0.9); 1023 1024 const desired = new Set(); 1025 for (let rowNum = 1; rowNum <= CABIN_ROWS; rowNum++) { 1026 for (let seatIdx = 0; seatIdx < 6; seatIdx++) { 1027 const seat = this.paxMap[rowNum][seatIdx]; 1028 if (!seat.occ) continue; 1029 if (seat.hasCup && !seat.cupCollected) 1030 desired.add(`${rowNum}-${seatIdx}`); 1031 else if (seat.state === "sleeping" && !seat.cupCollected) 1032 desired.add(`${rowNum}-${seatIdx}`); 1033 } 1034 } 1035 1036 const keys = Object.keys(this.bubbleByKey); 1037 for (let i = 0; i < keys.length; i++) { 1038 if (!desired.has(keys[i])) { 1039 this.bubbleByKey[keys[i]].destroy(); 1040 delete this.bubbleByKey[keys[i]]; 1041 } 1042 } 1043 1044 desired.forEach((key) => { 1045 if (this.bubbleByKey[key]) return; 1046 const [rowText, seatText] = key.split("-"); 1047 const rowNum = Number(rowText); 1048 const seatIdx = Number(seatText); 1049 const seat = this.paxMap[rowNum][seatIdx]; 1050 const col = this.seatColForSeatIndex(seatIdx); 1051 const tex = 1052 seat.state === "sleeping" ? "bubble_sleeping" : "bubble_emptycup"; 1053 const img = this.add 1054 .image(tx(col) + TILE / 2, ty(TR.cabin + rowNum - 1) + 3, tex) 1055 .setOrigin(0.5, 1) 1056 .setDisplaySize(bubbleSize, bubbleSize) 1057 .setDepth(100); 1058 this.bubbleByKey[key] = img; 1059 }); 1060 1061 const activeKey = 1062 this.selectedSeatIdx !== null 1063 ? `${this.getCurrentRowNum()}-${this.selectedSeatIdx}` 1064 : null; 1065 const allKeys = Object.keys(this.bubbleByKey); 1066 for (let i = 0; i < allKeys.length; i++) { 1067 const key = allKeys[i]; 1068 const bubble = this.bubbleByKey[key]; 1069 if (!bubble) continue; 1070 const isActive = key === activeKey; 1071 bubble.setDisplaySize( 1072 isActive ? bubbleSizeActive : bubbleSize, 1073 isActive ? bubbleSizeActive : bubbleSize, 1074 ); 1075 bubble.setTint(isActive ? 0xd6ffad : 0xffffff); 1076 } 1077 } 1078 1079 drawAisle() { 1080 for (let r = 0; r < CABIN_ROWS; r++) { 1081 this.add.image( 1082 tx(TC.aisle) + TILE / 2, 1083 ty(TR.cabin + r) + TILE / 2, 1084 "aisle_tile", 1085 ); 1086 } 1087 } 1088 1089 drawFuselage() { 1090 for (let r = 0; r < GRID_ROWS; r++) { 1091 this.add.image(tx(TC.fuseLeft) + TILE / 2, ty(r) + TILE / 2, "fuse_wall"); 1092 this.add.image( 1093 tx(TC.fuseRight) + TILE / 2, 1094 ty(r) + TILE / 2, 1095 "fuse_wall", 1096 ); 1097 } 1098 for (let c = 0; c < GRID_COLS; c++) { 1099 this.add.image(tx(c) + TILE / 2, ty(TR.fuseTop) + TILE / 2, "fuse_wall"); 1100 this.add 1101 .image(tx(c) + TILE / 2, ty(TR.fuseBot) + TILE / 2, "fuse_wall") 1102 .setFlipY(true); 1103 } 1104 for (let r = 0; r < CABIN_ROWS; r++) { 1105 const tileRow = TR.cabin + r; 1106 const py = ty(tileRow) + TILE / 2; 1107 this.add.image(tx(TC.fuseLeft) + TILE / 2, py, "porthole"); 1108 this.add.image(tx(TC.fuseRight) + TILE / 2, py, "porthole"); 1109 } 1110 } 1111 1112 drawCabin() { 1113 const seatBg = this.add.graphics(); 1114 seatBg.fillStyle(0x03165c, 1); 1115 const seatCols = [...SEAT_COLS_LEFT, ...SEAT_COLS_RIGHT]; 1116 for (let r = 0; r < CABIN_ROWS; r++) { 1117 const tileRow = TR.cabin + r; 1118 const rowNum = r + 1;
1119 seatCols.forEach((col, seatIdx) => { 1120 const cx = tx(col) + TILE / 2; 1121 const cy = ty(tileRow) + TILE / 2; 1122 const seat = this.paxMap[rowNum][seatIdx]; 1123 seatBg.fillRect(tx(col) + 2, ty(tileRow) + 2, TILE - 4, TILE - 4); 1124 if (seat.occ) { 1125 const usesAtlas = 1126 this.textures.exists("passengers") && 1127 this.textures.get("passengers").has(seat.spriteFrame); 1128 if (usesAtlas) { 1129 this.add 1130 .image(cx, cy, "passengers", seat.spriteFrame) 1131 .setDisplaySize(TILE - 20, TILE - 20); 1132 } else { 1133 this.add 1134 .image(cx, cy, "pax-intern-a") 1135 .setDisplaySize(TILE - 20, TILE - 20); 1136 } 1137 } 1138 }); 1139 } 1140 } 1141 1142 drawCockpit() { 1143 for (let c = TC.seatA; c <= TC.seatF; c++) { 1144 this.add.image( 1145 tx(c) + TILE / 2, 1146 ty(TR.cockpit) + TILE / 2, 1147 "cockpit_fill", 1148 ); 1149 } 1150 const g = this.add.graphics(); 1151 const x0 = tx(TC.seatA); 1152 const y0 = ty(TR.cockpit); 1153 const w = (TC.seatF - TC.seatA + 1) * TILE; 1154 const h = TILE; 1155 g.lineStyle(2, 0x2a4068, 1); 1156 g.strokeRect(x0 + 2, y0 + 2, w - 4, h - 4); 1157 g.fillStyle(0x1b2a4a); 1158 g.fillRect(x0 + 5, y0 + 5, w - 10, h - 10); 1159 g.lineStyle(1.5, 0x0a1020); 1160 g.strokeRect(x0 + 7, y0 + 7, w - 14, h - 14); 1161 g.lineStyle(1.5, 0x0a1020); 1162 g.beginPath(); 1163 g.moveTo(x0 + w / 2, y0 + 9); 1164 g.lineTo(x0 + w / 2, y0 + h - 9); 1165 g.strokePath(); 1166 g.fillStyle(0x3a5080); 1167 g.fillRect(x0 + w - 15, y0 + h / 2 - 5, 6, 10); 1168 const doorCx = x0 + w / 2; 1169 const doorCy = y0 + h / 2; 1170 this.add.image(doorCx - 26, doorCy, "porthole").setScale(1); 1171 this.add.image(doorCx + 26, doorCy, "porthole").setScale(1); 1172 this.add 1173 .text(x0 + w / 2, y0 + h / 2 + 1, "COCKPIT", { 1174 fontFamily: '"Press Start 2P"', 1175 fontSize: "7px", 1176 color: "#c8e6ff", 1177 stroke: "#0a1020", 1178 strokeThickness: 3, 1179 }) 1180 .setOrigin(0.5, 0.5); 1181 } 1182 1183 drawGalley() { 1184 for (let c = TC.seatA; c <= TC.seatF; c++) { 1185 this.add.image(tx(c) + TILE / 2, ty(TR.galley) + TILE / 2, "galley_fill"); 1186 } 1187 const g = this.add.graphics(); 1188 const x0 = tx(TC.seatA); 1189 const y0 = ty(TR.galley); 1190 const w = (TC.seatF - TC.seatA + 1) * TILE; 1191 g.fillStyle(0xb8b8b8); 1192 g.fillRect(x0 + 2, y0 + 2, w - 4, 11); 1193 g.lineStyle(1, 0x888888, 1); 1194 g.strokeRect(x0 + 2, y0 + 2, w - 4, 11); 1195 g.fillStyle(0x999999); 1196 g.fillRect(x0 + 4, y0 + 16, w - 8, TILE - 20); 1197 g.fillStyle(0x888888); 1198 g.fillRect(x0 + 6, y0 + 19, w - 12, 4); 1199 const lightsY = y0 + 24; 1200 const step = Math.floor((w - 20) / 6); 1201 for (let i = 0; i < 6; i++) { 1202 const lx = x0 + 10 + i * step; 1203 g.fillStyle(i % 2 === 0 ? 0x00cc44 : 0xffaa00); 1204 g.fillRect(lx, lightsY, 4, 4); 1205 } 1206 this.add 1207 .text(x0 + w / 2, y0 + 34, "GALLEY", { 1208 fontFamily: '"Press Start 2P"', 1209 fontSize: "7px", 1210 color: "#1b2a4a", 1211 stroke: "#e8e8e8", 1212 strokeThickness: 2, 1213 }) 1214 .setOrigin(0.5, 0.5); 1215 } 1216 1217 drawCrew() { 1218 const cx = tx(this.playerCol) + TILE / 2; 1219 const cy = ty(this.playerRow) + TILE; 1220 this.crewGender = window.selectedCrew === "male" ? "male" : "female"; 1221 const prefix = this.crewGender === "male" ? "male-crew-" : "female-crew-"; 1222 this.crewTexturePrefix = prefix; 1223 this.crewDir = "south"; 1224 this.crewSprite = this.add 1225 .image(cx, cy, `${this.crewTexturePrefix}south`) 1226 .setOrigin(0.5, 1) 1227 .setDisplaySize(TILE, TILE * 1.5) 1228 .setDepth(20); 1229 } 1230 1231 setCrewDirection(dir) { 1232 this.crewDir = dir; 1233 this.crewSprite.setTexture(`${this.crewTexturePrefix}${dir}`); 1234 } 1235 1236 drawLabels() { 1237 const labelStyle = { 1238 fontFamily: '"Press Start 2P"', 1239 fontSize: "5px", 1240 color: "#888888", 1241 }; 1242 const rowLabelX = tx(TC.fuseLeft) - 10; 1243 for (let r = 0; r < CABIN_ROWS; r++) { 1244 const tileRow = TR.cabin + r; 1245 const rowLabel = String(CABIN_ROWS - r); 1246 this.add 1247 .text(rowLabelX, ty(tileRow) + TILE / 2, rowLabel, labelStyle) 1248 .setOrigin(0.5); 1249 } 1250 const seatLetters = ["F", "E", "D", "C", "B", "A"]; 1251 const seatCols = [...SEAT_COLS_LEFT, ...SEAT_COLS_RIGHT]; 1252 const seatLabelY = ty(TR.cabin) - 12; 1253 const seatLetterStyle = { 1254 fontFamily: '"Press Start 2P"', 1255 fontSize: "8px", 1256 color: "#ffffff", 1257 stroke: "#000000", 1258 strokeThickness: 2, 1259 };
1260 seatCols.forEach((col, i) => { 1261 this.add 1262 .text(tx(col) + TILE / 2, seatLabelY, seatLetters[i], seatLetterStyle) 1263 .setOrigin(0.5); 1264 }); 1265 } 1266}
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.