Code edit (1 edits merged)
Please save this source code
User prompt
when intro is being played in any puzzle, do not allow to skip it by tapping.
Code edit (3 edits merged)
Please save this source code
User prompt
in puzzle 4 also show the text and the vo of The echoes guide you, after the puzzle itnro.
Code edit (1 edits merged)
Please save this source code
User prompt
In puzzle 4, after the chime description vo is played when the chcime is single tapped, play vo_chime_general_descrpition too.
User prompt
after the chime description sound, play vo_general_chime_descrpition
User prompt
actually the vo_chime_general_description should be pllayed after the single tap on the chime sound, not the double tap.
User prompt
add a new vo that wwill be use to describe any chime. this vo should be played after the vo_chime_description sound of each chime in puzzle 4
User prompt
make sure success message frompuzzle 4 finishes playing before intro of puzzle 5
User prompt
remove main menu text from bottom gui in main menu
User prompt
When rock starts to be dragged, stop the label announce VO
User prompt
Now I don't get the label on tap of the heavy rock, please fix it
User prompt
this function is not working. only the tap is playing the sound but not the double tap. can you refactor it? self.setup("Heavy Rock", "A large, heavy rock. It might be moveable with effort.", "", function () { setGuiText("The rock is too heavy to pick up, \nbut you might be able to drag it.", 'vo_rock_drag_hint'); }, 'vo_heavy_rock_label', 'vo_rock_drag_hint');\
User prompt
vo_rock_drag_hint is not played on heavyrock double tap, and it should.
User prompt
add mores space between chimes
User prompt
heavy rock in puzzle 3 should have a different VO if the player is taping it or double tapping it. But keep the current sound when dragging.
User prompt
make sure elements on puzzles always spawn with a little of space between each other, never overlaping with a little margin even
User prompt
remove the message, the gate slides shut ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make sure puzzle 2 success messsage finished before starting puzzle 3 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
When a menu option is selected, tint the color of the text of the option to yellow ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
When a menu option is selected, tint the color of the text of the option to yellow ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Great! When a menu option is tapped, it will be selected, double tap execute.
User prompt
Replace numbers in the menu items for bullet
User prompt
remove main menu in the bottomm of main menu
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var InteractiveElement = Container.expand(function () { var self = Container.call(this); self.label = ""; self.description = ""; self.interactText = ""; self.onInteract = null; self.voiceOverAnnounceId = null; self.voiceOverInteractId = null; self.lastTapTime = 0; self.tapCount = 0; // Visual: use tapCircle for all, but color can be changed if needed var circle = self.attachAsset('tapCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.7 }); // Apply global visibility state circle.visible = typeof tapCirclesVisible !== 'undefined' ? tapCirclesVisible : false; // Store the tap circle's radius for hit detection (half of width) self.hitRadius = circle.width / 2; // Set up element self.setup = function (label, description, interactText, onInteract, voiceOverAnnounceId, voiceOverInteractId) { self.label = label; self.description = description; self.interactText = interactText; self.onInteract = onInteract; self.voiceOverAnnounceId = voiceOverAnnounceId; self.voiceOverInteractId = voiceOverInteractId; }; // Announce what this is self.announce = function () { setGuiText(self.label + "\n" + self.description + "\n\n(Double tap to interact)", self.voiceOverAnnounceId); // Optionally, play a sound or narration for the element }; // Interact with the element self.interact = function () { // If onInteract is defined, it is responsible for calling setGuiText. if (typeof self.onInteract === "function") { self.onInteract(); } else { // Only call setGuiText here if there's no specific onInteract handler // that would call it. This uses the generic interactText and voiceOverInteractId. setGuiText(self.interactText, self.voiceOverInteractId); } }; // Track pending voice-over timeout self.pendingVOTimeout = null; self.justDoubleTapped = false; self.justDoubleTappedTimeout = null; // Handle tap/double-tap self.down = function (x, y, obj) { var now = Date.now(); var timeSinceLastTap = now - self.lastTapTime; // If within double-tap window if (timeSinceLastTap < 500 && timeSinceLastTap > 50) { // Added minimum time to avoid accidental double taps // This is a double tap // Cancel any pending single-tap voice-over if (self.pendingVOTimeout) { LK.clearTimeout(self.pendingVOTimeout); self.pendingVOTimeout = null; } // Stop any current voice-over before playing the double-tap VO if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } self.interact(); self.lastTapTime = 0; // Reset to prevent triple taps // Cooldown after double tap to prevent immediate single tap on this element if (self.justDoubleTappedTimeout) { LK.clearTimeout(self.justDoubleTappedTimeout); } self.justDoubleTapped = true; self.justDoubleTappedTimeout = LK.setTimeout(function () { self.justDoubleTapped = false; self.justDoubleTappedTimeout = null; }, 700); // 700ms cooldown // Play a subtle sound to indicate interaction LK.getSound('sfx_tap').play(); } else { // This is a single tap // If a double tap just occurred on this element, suppress the immediate single tap action // but record the tap time for a potential new double tap. if (self.justDoubleTapped) { self.lastTapTime = now; // Do not schedule announce, do not clear other timers or VOs, as the double tap just handled interaction. } else { // Standard single tap logic: // Cancel any existing pending voice-over if (self.pendingVOTimeout) { LK.clearTimeout(self.pendingVOTimeout); // {s} self.pendingVOTimeout = null; // Explicitly nullify the handle after clearing an old timeout } // {t} // Stop any current voice-over before scheduling new one if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Delay the announce to check if it's actually a double-tap self.pendingVOTimeout = LK.setTimeout(function () { self.announce(); self.pendingVOTimeout = null; }, 300); // Wait 300ms to see if second tap comes self.lastTapTime = now; } } }; return self; }); // Stone Door for Puzzle 2 var StoneDoor = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.setup("Stone Door", "A massive stone door blocks your path.", "", function () { if (bothChestsOpened) { LK.getSound('sfx_rock_door_rumble').play(); setGuiText("The stone door grinds open. You've solved the puzzle!", 'vo_stone_door_open'); LK.getSound('sfx_success').play(); goToState(STATE_PUZZLE2_SUCCESS); } else { setGuiText("The door won't budge. Perhaps something else needs to be done first.", 'vo_stone_door_locked'); } }, 'vo_stone_door_label', 'vo_stone_door_locked'); return self; }); // Key var Key = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.setup("Key", "Something lies on the floor. Cold, small... metal.", "You pick up a rusty key. It hums faintly in your hand.", function () { hasKey = true; LK.getSound('sfx_key_pickup').play(); setGuiText("You pick up a rusty key. It hums faintly in your hand.", 'vo_key_pickup'); self.visible = false; // Hide key after pickup self.interactive = false; // Make it non-interactive after pickup // Remove from puzzle elements to prevent further interaction if (puzzle1Elements) { var index = puzzle1Elements.indexOf(self); if (index !== -1) { puzzle1Elements.splice(index, 1); } } }, 'vo_key_label', 'vo_key_pickup'); return self; }); // Heavy Rock for Puzzle 3 var HeavyRock = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.isDragging = false; self.startX = 0; self.startY = 0; self.lastX = 0; // Add lastX property self.lastY = 0; // Add lastY property self.lastDragSoundTime = 0; self.setup("Heavy Rock", "A large, heavy rock. It might be moveable with effort.", "", function () { setGuiText("The rock is too heavy to pick up, \nbut you might be able to drag it.", 'vo_rock_drag_hint'); }, 'vo_heavy_rock_label', 'vo_rock_drag_hint'); // Override down to start dragging var originalDown = self.down; self.down = function (x, y, obj) { originalDown.call(self, x, y, obj); // Check if this is a hold (not a double tap) var now = Date.now(); var timeSinceLastTap = now - self.lastTapTime; // If not a double tap, start dragging if (timeSinceLastTap > 500 || timeSinceLastTap < 50) { self.isDragging = true; self.startX = self.x; self.startY = self.y; // No drag offset needed; set position directly during drag self.dragOffsetX = 0; self.dragOffsetY = 0; // Stop rock label voice-over when dragging starts if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } } // Also update lastX and lastY immediately to the current position self.lastX = self.x; self.lastY = self.y; }; // Handle dragging self.handleDrag = function (x, y, obj) { if (self.isDragging) { // Check if rock would overlap with tile at new position var wouldOverlap50Percent = false; if (puzzle3FloorTile) { // Temporarily move rock to check intersection var oldX = self.x; var oldY = self.y; self.x = x; self.y = y; // Calculate overlap percentage if (self.intersects(puzzle3FloorTile)) { // Get bounds of both objects var rockLeft = self.x - self.hitRadius; var rockRight = self.x + self.hitRadius; var rockTop = self.y - self.hitRadius; var rockBottom = self.y + self.hitRadius; var tileLeft = puzzle3FloorTile.x - puzzle3FloorTile.hitRadius; var tileRight = puzzle3FloorTile.x + puzzle3FloorTile.hitRadius; var tileTop = puzzle3FloorTile.y - puzzle3FloorTile.hitRadius; var tileBottom = puzzle3FloorTile.y + puzzle3FloorTile.hitRadius; // Calculate overlap area var overlapLeft = Math.max(rockLeft, tileLeft); var overlapRight = Math.min(rockRight, tileRight); var overlapTop = Math.max(rockTop, tileTop); var overlapBottom = Math.min(rockBottom, tileBottom); var overlapWidth = Math.max(0, overlapRight - overlapLeft); var overlapHeight = Math.max(0, overlapBottom - overlapTop); var overlapArea = overlapWidth * overlapHeight; // Calculate rock area (assuming circular, use diameter squared) var rockArea = self.hitRadius * 2 * (self.hitRadius * 2); // Check if overlap is 50% or more var overlapPercentage = overlapArea / rockArea; wouldOverlap50Percent = overlapPercentage >= 0.5; } // Restore position self.x = oldX; self.y = oldY; } // Only move if not overlapping 50% with tile if (!wouldOverlap50Percent) { // Move rock directly to touch position (no offset) self.x = x; self.y = y; // Play drag sound periodically var now = Date.now(); if (now - self.lastDragSoundTime > 500) { LK.getSound('sfx_rock_drag').play(); self.lastDragSoundTime = now; } } else { // Rock overlaps 50% with tile - stop dragging and place it self.isDragging = false; // Play click sound LK.getSound('sfx_rock_place').play(); // Play rock stuck sound LK.getSound('sfx_rock_stuck').play(); // Trigger tile press puzzle3FloorTile.pressWithRock(); } } ; // Add update method to track position changes self.update = function () { self.lastX = self.x; // Update lastX self.lastY = self.y; // Update lastY }; }; // Stop dragging on up self.up = function (x, y, obj) { if (self.isDragging) { self.isDragging = false; // Check if rock is on the floor tile if (puzzle3FloorTile && self.intersects(puzzle3FloorTile)) { puzzle3FloorTile.pressWithRock(); } else if (puzzle3FloorTile && puzzle3FloorTile.isPressedByRock) { // Rock was moved off the tile puzzle3FloorTile.releaseFromRock(); } } }; return self; }); // Gate for Puzzle 3 var Gate = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.isOpen = false; // Override announce to provide different instructions when open var originalAnnounce = self.announce; self.announce = function () { if (self.isOpen) { setGuiText("Gate\nThe gate stands open before you.\n\n(Double tap to pass through)", 'vo_gate_open_announce'); } else { originalAnnounce.call(self); } }; self.setup("Gate", "A heavy iron gate blocks your path.", "", function () { if (self.isOpen) { // When gate is open, double-tap allows passage setGuiText("The gate is open! You step through carefully...", 'vo_gate_passage'); LK.getSound('sfx_success').play(); goToState(STATE_PUZZLE3_SUCCESS); } else { setGuiText("The gate is closed. Something needs to keep it open.", 'vo_gate_closed'); } }, 'vo_gate_label', 'vo_gate_closed'); self.open = function () { if (!self.isOpen) { self.isOpen = true; LK.getSound('sfx_gate_open').play(); setGuiText("The gate slowly grinds open!", 'vo_gate_opening'); } }; self.close = function () { if (self.isOpen) { self.isOpen = false; tween(self, { x: self.x - 200 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { LK.getSound('sfx_gate_close').play(); } }); } }; return self; }); // Floor Tile for Puzzle 3 var FloorTile = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.isPressed = false; self.isPressedByRock = false; self.linkedGate = null; self.setup("Floor Tile", "A loose stone tile. It seems to move slightly when touched.", "", function () { if (self.isPressedByRock) { setGuiText("The heavy rock keeps the tile pressed down.", 'vo_tile_pressed_by_rock'); } else { // Updated message for double tap - play gate open/close sounds for feedback if (self.linkedGate) { self.linkedGate.open(); // Schedule gate close after a short delay LK.setTimeout(function () { if (self.linkedGate && !self.isPressedByRock) { self.linkedGate.close(); } }, 1000); // Gate stays open for 1 second } setGuiText("The gate opened, but shut down as soon as I lift my hand. I need something heavy to keep it pressed.", 'vo_tile_springs_back'); } }, 'vo_floor_tile_label', 'vo_tile_springs_back'); self.pressWithRock = function () { if (!self.isPressedByRock) { self.isPressedByRock = true; self.isPressed = true; if (self.linkedGate) { self.linkedGate.open(); } LK.getSound('sfx_rock_place').play(); setGuiText("The rock settles onto the tile with a satisfying thunk. The gate remains open!", 'vo_rock_on_tile'); } }; self.releaseFromRock = function () { if (self.isPressedByRock) { self.isPressedByRock = false; self.isPressed = false; if (self.linkedGate) { self.linkedGate.close(); } } }; return self; }); // Door var Door = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.setup("Door", "A heavy wooden door. It doesn't budge.", "", function () { // Door can only be opened if player has the key if (typeof hasKey !== "undefined" && hasKey) { self.interactText = "The key turns with a satisfying click. The path ahead opens."; setGuiText("The key turns with a satisfying click. The path ahead opens.", 'vo_door_unlocked'); // Play new door unlock sound LK.getSound('sfx_door_unlock').play(); goToState(STATE_PUZZLE1_SUCCESS); } else { self.interactText = "The door is locked tight. You'll need something to open it."; setGuiText("The door is locked tight. You'll need something to open it.", 'vo_door_locked'); } }, 'vo_door_label', 'vo_door_locked'); return self; }); // Chime for Puzzle 4 var Chime = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.chimeIndex = 0; self.isCommitted = false; self.chimeSound = null; self.pendingSingleTapTimeout = null; self.setup = function (index) { self.chimeIndex = index; var label = "Chime " + (index + 1); var description = "A hanging chime. Single tap to hear it softly, double tap to strike it."; // Use specific voice-over for each chime description var chimeVoiceOverId = 'vo_chime_description_' + (index + 1); InteractiveElement.call(self).setup(label, description, "", function () { // Double tap - commit to this chime if (!self.isCommitted && !puzzle4Complete) { self.commit(); } }, chimeVoiceOverId, null); // Override the circle color based on chime index if (self.children[0]) { var colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00]; // Red, Green, Blue, Yellow self.children[0].tint = colors[index % 4]; } }; // Override announce to play instruction VO first self.announce = function () { // Play the instruction VO first var instructionVO = LK.getSound('vo_chime_instruction'); instructionVO.play(); // Wait for instruction to finish before playing chime description LK.setTimeout(function () { // Call parent announce method which will play the chime description InteractiveElement.prototype.announce.call(self); }, 3000); // Adjust timing based on instruction VO length }; // Override down method from InteractiveElement to handle chime-specific behavior self.down = function (x, y, obj) { var now = Date.now(); var timeSinceLastTap = now - self.lastTapTime; // If within double-tap window if (timeSinceLastTap < 500 && timeSinceLastTap > 50) { // This is a double tap // Cancel any pending single-tap sound if (self.pendingSingleTapTimeout) { LK.clearTimeout(self.pendingSingleTapTimeout); self.pendingSingleTapTimeout = null; } // Cancel any pending single-tap voice-over if (self.pendingVOTimeout) { LK.clearTimeout(self.pendingVOTimeout); self.pendingVOTimeout = null; } // Stop any current voice-over before playing the double-tap VO if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } self.interact(); self.lastTapTime = 0; // Reset to prevent triple taps // Cooldown after double tap to prevent immediate single tap on this element if (self.justDoubleTappedTimeout) { LK.clearTimeout(self.justDoubleTappedTimeout); } self.justDoubleTapped = true; self.justDoubleTappedTimeout = LK.setTimeout(function () { self.justDoubleTapped = false; self.justDoubleTappedTimeout = null; }, 700); // 700ms cooldown // Play a subtle sound to indicate interaction LK.getSound('sfx_tap').play(); } else { // This is a single tap // If a double tap just occurred on this element, suppress the immediate single tap action // but record the tap time for a potential new double tap. if (self.justDoubleTapped) { self.lastTapTime = now; // Do not schedule announce, do not clear other timers or VOs, as the double tap just handled interaction. } else { // Standard single tap logic: // Cancel any existing pending voice-over if (self.pendingVOTimeout) { LK.clearTimeout(self.pendingVOTimeout); // {s} self.pendingVOTimeout = null; // Explicitly nullify the handle after clearing an old timeout } // {t} // Cancel any existing pending single tap sound if (self.pendingSingleTapTimeout) { LK.clearTimeout(self.pendingSingleTapTimeout); self.pendingSingleTapTimeout = null; } // Stop any current voice-over before scheduling new one if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Delay both the announce and the soft chime sound to check if it's actually a double-tap self.pendingVOTimeout = LK.setTimeout(function () { self.announce(); self.pendingVOTimeout = null; }, 300); // Wait 300ms to see if second tap comes // Schedule soft chime sound self.pendingSingleTapTimeout = LK.setTimeout(function () { // Play instruction VO first var instructionVO = LK.getSound('vo_chime_instruction'); instructionVO.play(); // Wait for instruction to finish before playing chime sound LK.setTimeout(function () { // Play soft chime sound at lower volume if (self.chimeSound && !self.isCommitted) { var softSound = self.chimeSound; softSound.volume = 0.3; // Set to 30% volume for soft tap softSound.play(); // Reset volume after playing LK.setTimeout(function () { softSound.volume = 1.0; // Reset to full volume }, 100); } }, 3000); // Wait for instruction VO to finish self.pendingSingleTapTimeout = null; }, 300); // Same delay as voice-over self.lastTapTime = now; } } }; self.commit = function () { self.isCommitted = true; playerChimeSequence.push(self.chimeIndex); // Play louder committed sound at full volume if (self.chimeSound) { self.chimeSound.volume = 1.0; // Ensure full volume for double tap self.chimeSound.play(); } // Visual feedback - flash the chime tween(self, { alpha: 0.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 1 }, { duration: 200, easing: tween.easeIn }); } }); // Check if player has entered all required chimes var requiredLength = correctChimeSequence.length; if (playerChimeSequence.length === requiredLength) { checkChimeSequence(); } else { setGuiText("Chime " + playerChimeSequence.length + " of " + requiredLength + " selected.", 'vo_chime_selected'); } }; return self; }); // Chest for Puzzle 2 var Chest = InteractiveElement.expand(function () { var self = InteractiveElement.call(this); self.isOpened = false; self.chestId = 0; self.isVialChest = false; // Will be set during setup self.isTrapChest = false; // Will be set during setup self.setup = function (id, isVial, isTrap) { self.chestId = id; self.isVialChest = !!isVial; self.isTrapChest = !!isTrap; // Inspection text var inspectText = "A wooden chest with intricate carvings."; if (self.isVialChest) { inspectText = "A wooden chest. It smells faintly of herbs."; } else if (self.isTrapChest) { inspectText = "Another chest, this one colder to the touch."; } // Interact logic InteractiveElement.call(self).setup("Chest " + (id + 1), inspectText, "", function () { if (!self.isOpened) { self.isOpened = true; // Play chest open sound LK.getSound('sfx_chest_open').play(); // Vial Chest if (self.isVialChest) { hasVialOfClarity = true; // Play background music for vial chest open LK.playMusic('vial_chest_open_bgm', { loop: true }); // If trap was already triggered, heal and stop distortion if (trapActive) { trapActive = false; // Stop annoying trap sound if running stopAnnoyingTrapSound(); var vialText = "You open the chest… a soft chime rings out. \nInside, a small vial, warm to the touch. \nYou feel a little more focused. \nThe distortion fades."; // If this is the second chest, append the stone shifting message var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount === 2) { vialText += "\nYou hear a heavy stone shifting nearby."; } setGuiText(vialText, 'vo_vial_heals_trap'); // Play soft chime, then steady hum (simulate with sfx_success for now) LK.getSound('sfx_success').play(); } else { var vialText = "You open the chest… a soft chime rings out.\n Inside, a small vial, warm to the touch. \nYou feel a little more focused."; var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount === 2) { vialText += "\nYou hear a heavy stone shifting nearby."; } setGuiText(vialText, 'vo_vial_found'); LK.getSound('sfx_success').play(); } } // Trap Chest else if (self.isTrapChest) { // If player already has vial, neutralize trap if (hasVialOfClarity) { var trapText = "You open the chest… a rush of wind escapes, but the vial in your hand pulses gently, shielding your senses."; var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount === 2) { trapText += "\nYou hear a heavy stone shifting nearby."; } setGuiText(trapText, 'vo_trap_neutralized'); // Play wind muffled/steady hum (simulate with sfx_tap for now) LK.getSound('sfx_tap').play(); // Stop annoying trap sound if running stopAnnoyingTrapSound(); } else { trapActive = true; var trapText = "You open the chest… a rush of wind escapes. A sharp whisper pierces your ears. Everything feels… twisted."; var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount === 2) { trapText += "\nYou hear a heavy stone shifting nearby."; } setGuiText(trapText, 'vo_trap_triggered'); // Play rushing wind/sharp whisper (simulate with sfx_tap for now) LK.getSound('sfx_tap').play(); // Start annoying trap sound startAnnoyingTrapSound(); // TODO: Add actual distortion effect if available } } // Fallback (should not happen) else { var fallbackText = "The chest creaks open. You find nothing inside, but sense progress."; var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount === 2) { fallbackText += "\nYou hear a heavy stone shifting nearby."; } setGuiText(fallbackText, 'vo_chest_opened'); LK.getSound('sfx_success').play(); } checkPuzzle2Progress(); } else { setGuiText("The chest is already open.", 'vo_chest_empty'); } }, // Announce/inspect voice-over self.isVialChest ? 'vo_vial_inspect' : self.isTrapChest ? 'vo_trap_inspect' : 'vo_chest_label', // Interact voice-over self.isVialChest ? 'vo_vial_found' : self.isTrapChest ? 'vo_trap_triggered' : 'vo_chest_opened'); }; return self; }); // Chest // Simple visual feedback for taps (for sighted users or those with some vision) var TapFeedback = Container.expand(function () { var self = Container.call(this); var circle = self.attachAsset('tapCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); self.showAt = function (x, y) { self.x = x; self.y = y; self.alpha = 0.5; self.scaleX = 1; self.scaleY = 1; tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // missing, added // Narration music tracks // Voice-over sounds for game states and feedback // Voice-over sounds for puzzle elements // Narration assets // Voice-over assets for all text displays // Voice-over sounds for narration // --- State Management --- // No visual assets needed for MVP, but we will use a simple shape for tap feedback. // Audio assets (narration and sfx) are referenced by id, LK will load them automatically. // Voice-over assets for "nothing here" phrases // Puzzle 1 & 2: All unique VO assets var STATE_HEADPHONES = 0; var STATE_MENU = 1; var STATE_HOW_TO_PLAY = 2; var STATE_CREDITS = 3; var STATE_INTRO = 4; var STATE_PUZZLE1 = 5; var STATE_PUZZLE1_SUCCESS = 6; var STATE_PUZZLE2 = 7; var STATE_PUZZLE2_SUCCESS = 8; var STATE_PUZZLE3 = 9; var STATE_PUZZLE3_SUCCESS = 10; var STATE_PUZZLE4 = 11; var STATE_PUZZLE4_SUCCESS = 12; var STATE_PUZZLE5 = 13; var STATE_PUZZLE5_SUCCESS = 14; var currentState = STATE_HEADPHONES; var headphonesIcon = null; // Store reference to headphones icon // Used to prevent double-tap triggers var inputLocked = false; // Used to track narration/music var currentNarration = null; // Used to track current voice-over sound var currentVoiceOver = null; // Used for puzzle state var puzzle1Step = 0; // 0 = waiting for first tap, 1 = waiting for second tap // Double-tap tracking at game level var lastTapTime = 0; var lastTapX = 0; var lastTapY = 0; var doubleTapThreshold = 500; // milliseconds var doubleTapDistanceThreshold = 50; // pixels var currentlySelectedOption = null; // Track currently selected menu option // --- Puzzle 1 element state --- var puzzle1Elements = []; var chestOpened = false; var hasKey = false; // --- Puzzle 2 element state --- var puzzle2Elements = []; var bothChestsOpened = false; // --- Puzzle 2: Vial/Trap state --- var hasVialOfClarity = false; var trapActive = false; // --- Puzzle 3 element state --- var puzzle3Elements = []; var puzzle3Gate = null; var puzzle3FloorTile = null; var puzzle3Rock = null; // --- Puzzle 4 element state --- var puzzle4Elements = []; var correctChimeSequence = []; var playerChimeSequence = []; var puzzle4Complete = false; // --- Annoying sound for trap logic --- var annoyingTrapSound = null; var annoyingTrapSoundInterval = null; var debugLevelLinks = []; // Array to store debug level links var tapCirclesVisible = false; // Global state for tapCircle visibility - starts hidden var tapCircleToggle = null; // Reference to toggle button function startAnnoyingTrapSound() { if (annoyingTrapSoundInterval) { return; } // Already running // Play the annoying sound as looping music LK.playMusic('sfx_trap_trigger', { loop: true }); } function stopAnnoyingTrapSound() { // Stop the annoying trap music if playing LK.stopMusic(); if (annoyingTrapSoundInterval) { LK.clearInterval(annoyingTrapSoundInterval); annoyingTrapSoundInterval = null; } annoyingTrapSound = null; } // --- GUI Text for minimal visual feedback (for sighted users) --- var guiText = new Text2('', { size: 50, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1600, // Reduced from 1800 to provide more margin align: 'center' // Center align the text lines within the text object }); guiText.anchor.set(0.5, 1.0); LK.gui.bottom.addChild(guiText); guiText.y = -50; // Offset from bottom edge // GUI text should always be visible guiText.visible = true; // --- Helper Functions --- function updateTapCircleVisibility() { // Update puzzle 1 elements if (puzzle1Elements) { for (var i = 0; i < puzzle1Elements.length; i++) { var el = puzzle1Elements[i]; if (el && el.children && el.children[0]) { el.children[0].visible = tapCirclesVisible; } } } // Update puzzle 2 elements if (puzzle2Elements) { for (var i = 0; i < puzzle2Elements.length; i++) { var el = puzzle2Elements[i]; if (el && el.children && el.children[0]) { el.children[0].visible = tapCirclesVisible; } } } // Update puzzle 3 elements if (puzzle3Elements) { for (var i = 0; i < puzzle3Elements.length; i++) { var el = puzzle3Elements[i]; if (el && el.children && el.children[0]) { el.children[0].visible = tapCirclesVisible; } } } // Update puzzle 4 and 5 elements if (puzzle4Elements) { for (var i = 0; i < puzzle4Elements.length; i++) { var el = puzzle4Elements[i]; if (el && el.children && el.children[0]) { el.children[0].visible = tapCirclesVisible; } } } } function playNarration(id, options) { // Stop any current narration/music LK.stopMusic(); currentNarration = id; LK.playMusic(id, options || {}); } function stopNarration() { LK.stopMusic(); currentNarration = null; } function setGuiText(txt, voiceOverId) { guiText.setText(txt); // Add a subtle scale animation when text updates guiText.scaleX = 1.1; guiText.scaleY = 1.1; tween(guiText, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut }); // Stop current voice-over if playing if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Play voice-over if provided, but delay if a SFX is currently playing if (voiceOverId) { // Check if a SFX was just played (by convention, SFX are played right before setGuiText) // We'll check if any SFX is currently playing and delay VO until it finishes var sfxIds = ['sfx_door_unlock', 'sfx_success', 'sfx_tap']; var maxSfxDuration = 0; for (var i = 0; i < sfxIds.length; i++) { var sfx = LK.getSound(sfxIds[i]); if (sfx && sfx.isPlaying && typeof sfx.duration === "number") { if (sfx.duration > maxSfxDuration) { maxSfxDuration = sfx.duration; } } } // If any SFX is playing, delay VO by the max duration (or a minimum of 400ms) if (maxSfxDuration > 0) { LK.setTimeout(function () { currentVoiceOver = LK.getSound(voiceOverId); currentVoiceOver.play(); }, Math.max(400, maxSfxDuration * 1000)); } else { currentVoiceOver = LK.getSound(voiceOverId); currentVoiceOver.play(); } } } function lockInput(ms) { inputLocked = true; LK.setTimeout(function () { inputLocked = false; }, ms || 600); } // Check if both chests are opened in puzzle 2 function playChimeSequence() { setGuiText("Listen to the sequence...", 'vo_puzzle4_sequence_play'); var index = 0; var _playNextChime = function playNextChime() { if (index < correctChimeSequence.length) { var chimeIndex = correctChimeSequence[index]; var chime = puzzle4Elements[chimeIndex]; if (chime && chime.chimeSound) { chime.chimeSound.play(); // Visual feedback tween(chime, { scaleX: 1.2, scaleY: 1.2 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(chime, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeIn }); } }); } index++; LK.setTimeout(_playNextChime, 800); } else { // Sequence complete, player can now interact setGuiText("The echoes guide you. Tap once to listen, \ntwice to step forward.", 'vo_repeat_sequence'); } }; _playNextChime(); } function checkChimeSequence() { var correct = true; var expectedLength = correctChimeSequence.length; for (var i = 0; i < expectedLength; i++) { if (correctChimeSequence[i] !== playerChimeSequence[i]) { correct = false; break; } } if (correct) { puzzle4Complete = true; LK.getSound('sfx_success').play(); if (currentState === STATE_PUZZLE4) { goToState(STATE_PUZZLE4_SUCCESS); } else if (currentState === STATE_PUZZLE5) { goToState(STATE_PUZZLE5_SUCCESS); } } else { // Wrong sequence behavior depends on current puzzle if (currentState === STATE_PUZZLE4) { // For simple puzzle 4, just reset and let them try again playerChimeSequence = []; // Reset all chimes for (var i = 0; i < puzzle4Elements.length; i++) { if (puzzle4Elements[i]) { puzzle4Elements[i].isCommitted = false; } } setGuiText("Not quite right. Listen again...\nRemember: from lowest tone to highest.", 'vo_puzzle4_simple_retry'); } else if (currentState === STATE_PUZZLE5) { // Wrong sequence - just reset and let them try again playerChimeSequence = []; // Reset all chimes for (var i = 0; i < puzzle4Elements.length; i++) { if (puzzle4Elements[i]) { puzzle4Elements[i].isCommitted = false; } } setGuiText("The sequence is incorrect. The chimes await your touch again...\nListen carefully and try once more.", 'vo_puzzle5_wrong'); } } } function checkPuzzle2Progress() { var openedCount = 0; for (var i = 0; i < puzzle2Elements.length; i++) { if (puzzle2Elements[i] instanceof Chest && puzzle2Elements[i].isOpened) { openedCount++; } } if (openedCount >= 2 && !bothChestsOpened) { bothChestsOpened = true; // No separate message for the door noise; handled in Chest logic after opening the second chest. } } // --- State Transitions --- function goToState(state) { // Clear existing debug level links first if (debugLevelLinks.length > 0) { for (var i = 0; i < debugLevelLinks.length; i++) { if (debugLevelLinks[i] && typeof debugLevelLinks[i].destroy === 'function') { debugLevelLinks[i].destroy(); } } debugLevelLinks = []; } stopNarration(); // Clear selected menu option currentlySelectedOption = null; // Stop all playing sounds when restarting game if (state === STATE_HEADPHONES) { // Stop any current voice-over if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Stop any trap sounds stopAnnoyingTrapSound(); } // Remove headphones icon if it exists if (headphonesIcon) { // Clean up extra elements (like text below icon) if (headphonesIcon.extraElements) { for (var i = 0; i < headphonesIcon.extraElements.length; i++) { if (headphonesIcon.extraElements[i]) { headphonesIcon.extraElements[i].destroy(); } } } headphonesIcon.destroy(); headphonesIcon = null; } // Remove toggle button if it exists if (tapCircleToggle) { tapCircleToggle.destroy(); tapCircleToggle = null; } // Clear all game children (including menu options and title) when transitioning states for (var i = game.children.length - 1; i >= 0; i--) { game.children[i].destroy(); } // Clear all GUI elements LK.gui.top.removeChildren(); LK.gui.bottom.removeChildren(); // Re-add the guiText to bottom after clearing LK.gui.bottom.addChild(guiText); currentState = state; if (state === STATE_HEADPHONES) { setGuiText("(Double tap to continue)"); // Voice-over is already handled by playNarration playNarration('screen_headphones_voice', { loop: false }); // Add headphones icon headphonesIcon = LK.getAsset('headphonesIcon', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 - 200 // Move icon up to make room for text below }); game.addChild(headphonesIcon); // Add text below headphones icon var headphonesText = new Text2("Use headphones for a better experience", { size: 100, fill: 0xFFFFFF, align: 'center', wordWrap: true, wordWrapWidth: 1600 }); headphonesText.anchor.set(0.5, 0.5); headphonesText.x = 2048 / 2; headphonesText.y = 2732 / 2 + 400; // Position further below the icon game.addChild(headphonesText); // Store reference to clean up later if (!headphonesIcon.extraElements) { headphonesIcon.extraElements = []; } headphonesIcon.extraElements.push(headphonesText); // Add debug links for development var levelsForDebug = [{ name: "Menu", state: STATE_MENU }, { name: "How to Play", state: STATE_HOW_TO_PLAY }, { name: "Credits", state: STATE_CREDITS }, { name: "Intro", state: STATE_INTRO }, { name: "Puzzle 1", state: STATE_PUZZLE1 }, { name: "P1 Success", state: STATE_PUZZLE1_SUCCESS }, { name: "Puzzle 2", state: STATE_PUZZLE2 }, { name: "P2 Success", state: STATE_PUZZLE2_SUCCESS }, { name: "Puzzle 3", state: STATE_PUZZLE3 }, { name: "P3 Success", state: STATE_PUZZLE3_SUCCESS }, { name: "Puzzle 4", state: STATE_PUZZLE4 }, { name: "P4 Success", state: STATE_PUZZLE4_SUCCESS }, { name: "Puzzle 5", state: STATE_PUZZLE5 }, { name: "P5 Success", state: STATE_PUZZLE5_SUCCESS }]; var startY = 120; // Start below the 100px top-left reserved area var spacingY = 55; for (var i = 0; i < levelsForDebug.length; i++) { var levelData = levelsForDebug[i]; var linkText = new Text2(levelData.name, { size: 38, fill: 0xFFFF00, // Bright yellow for debug align: 'left', wordWrap: true, wordWrapWidth: 300 // Ensure debug links don't overflow }); linkText.anchor.set(0, 0); // Anchor top-left linkText.x = 20; // Small offset from left edge linkText.y = startY + i * spacingY; linkText.interactive = true; linkText.buttonMode = true; // Shows hand cursor on desktop // IIFE to correctly capture levelData.state in the closure (function (targetState) { linkText.down = function () { // Stop any ongoing VO from the headphone screen before jumping if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } goToState(targetState); }; })(levelData.state); LK.gui.top.addChild(linkText); // Add to gui.top so it's above game content debugLevelLinks.push(linkText); // Store for cleanup } } else if (state === STATE_MENU) { setGuiText("Main Menu\n\nChoose an option below", 'vo_menu_title'); // This ensures the menu title voice-over is played // No narration for menu, or could add a short one if desired // Add "Main Menu" title text var menuTitle = new Text2("Main Screen", { size: 120, fill: 0xFFFFFF, align: 'center' }); menuTitle.anchor.set(0.5, 0); menuTitle.interactive = true; menuTitle.buttonMode = true; // Add voice-over functionality to title menuTitle.lastTapTime = 0; menuTitle.pendingVOTimeout = null; menuTitle.justDoubleTapped = false; menuTitle.justDoubleTappedTimeout = null; menuTitle.down = function (x, y, obj) { var now = Date.now(); var timeSinceLastTap = now - menuTitle.lastTapTime; // Single tap to announce title if (timeSinceLastTap > 500 || timeSinceLastTap < 50) { // Cancel any existing pending voice-over if (menuTitle.pendingVOTimeout) { LK.clearTimeout(menuTitle.pendingVOTimeout); menuTitle.pendingVOTimeout = null; } // Stop any current voice-over if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Delay the announce to check if it's actually a double-tap menuTitle.pendingVOTimeout = LK.setTimeout(function () { setGuiText("Main Menu\n\nChoose an option below", 'vo_menu_title_announce'); menuTitle.pendingVOTimeout = null; }, 300); menuTitle.lastTapTime = now; } }; LK.gui.top.addChild(menuTitle); menuTitle.y = 50; // Position near top but below the reserved 100px area // Add menu options var menuOptions = [{ text: "• Intro", label: "Intro", description: "Start the adventure story", voiceOverId: "vo_menu_intro", action: function action() { goToState(STATE_INTRO); } }, { text: "• How to Play", label: "How to Play", description: "Learn the game controls", voiceOverId: "vo_menu_how_to_play", action: function action() { goToState(STATE_HOW_TO_PLAY); } }, { text: "• Credits", label: "Credits", description: "View game credits", voiceOverId: "vo_menu_credits", action: function action() { goToState(STATE_CREDITS); } }]; var startY = 800; // Start position for menu options var spacingY = 200; // Increased spacing between options for (var i = 0; i < menuOptions.length; i++) { var option = menuOptions[i]; var optionText = new Text2(option.text, { size: 120, fill: 0xFFFFFF, align: 'center', wordWrap: true, wordWrapWidth: 1600 }); optionText.anchor.set(0.5, 0.5); optionText.x = 2048 / 2; optionText.y = startY + i * spacingY; optionText.interactive = true; optionText.buttonMode = true; // Add double-tap functionality like InteractiveElement optionText.lastTapTime = 0; optionText.tapCount = 0; optionText.pendingVOTimeout = null; optionText.justDoubleTapped = false; optionText.justDoubleTappedTimeout = null; optionText.isSelected = false; optionText.isSelected = false; // Store the action function and option data for each option (function (optionAction, optionData) { optionText.down = function () { var now = Date.now(); var timeSinceLastTap = now - optionText.lastTapTime; // If within double-tap window if (timeSinceLastTap < 500 && timeSinceLastTap > 50) { // This is a double tap - execute action only if this option is selected if (optionText.isSelected && currentlySelectedOption === optionText) { // Cancel any pending single-tap voice-over if (optionText.pendingVOTimeout) { LK.clearTimeout(optionText.pendingVOTimeout); optionText.pendingVOTimeout = null; } // Stop any current voice-over before executing if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } optionAction(); optionText.lastTapTime = 0; // Reset to prevent triple taps // Cooldown after double tap if (optionText.justDoubleTappedTimeout) { LK.clearTimeout(optionText.justDoubleTappedTimeout); } optionText.justDoubleTapped = true; optionText.justDoubleTappedTimeout = LK.setTimeout(function () { optionText.justDoubleTapped = false; optionText.justDoubleTappedTimeout = null; }, 700); // 700ms cooldown // Play interaction sound LK.getSound('sfx_tap').play(); } else { // Double tap on unselected option - treat as single tap to select it optionText.lastTapTime = now; } } else { // This is a single tap - announce and show selection // If a double tap just occurred, suppress immediate single tap action if (optionText.justDoubleTapped) { optionText.lastTapTime = now; } else { // Clear any existing pending voice-over if (optionText.pendingVOTimeout) { LK.clearTimeout(optionText.pendingVOTimeout); optionText.pendingVOTimeout = null; } // Stop any current voice-over if (currentVoiceOver) { currentVoiceOver.stop(); currentVoiceOver = null; } // Mark as selected and update appearance optionText.isSelected = true; currentlySelectedOption = optionText; // Animate to yellow highlight tween(optionText, { fill: 0xFFFF00 }, { duration: 200, easing: tween.easeOut }); // Clear selection from other options for (var j = 0; j < menuOptions.length; j++) { if (j !== i && game.children[j + 1]) { // +1 to account for menu title var otherOption = game.children[j + 1]; if (otherOption !== optionText && otherOption.isSelected) { otherOption.isSelected = false; // Animate back to white tween(otherOption, { fill: 0xFFFFFF }, { duration: 200, easing: tween.easeOut }); } } } // Delay the announce to check if it's actually a double-tap optionText.pendingVOTimeout = LK.setTimeout(function () { var announceText = optionData.label + " - " + optionData.description + "\n\n(Double tap to select)"; // Use the voice-over ID from the option data setGuiText(announceText, optionData.voiceOverId); optionText.pendingVOTimeout = null; }, 300); // Wait 300ms to see if second tap comes optionText.lastTapTime = now; } } }; })(option.action, option); game.addChild(optionText); } } else if (state === STATE_HOW_TO_PLAY) { setGuiText("How to Play\n\nThis is an audio-first adventure game.\n\nSingle tap to explore and hear descriptions.\nDouble tap to interact with objects.\nUse headphones for the best experience.\n\n(Double tap to return to menu)", 'vo_how_to_play_content'); } else if (state === STATE_CREDITS) { setGuiText("Credits\n\nI am Juan and I wanted to create an \ninteractive audio story for the visually impaired first.\n\nDeveloped with accessibility in mind.\nThank you for playing!\n\n(Double tap to return to menu)", 'vo_credits_content'); } else if (state === STATE_INTRO) { // Create a dedicated text object for the intro story with proper wrapping var introText = new Text2("Long ago, beneath the roots of the ancient world, a vault was sealed away—its halls forgotten, its treasures buried in silence. Many have tried to claim its secrets, but none have returned.\n\nYou, a Seeker of Echoes, are drawn by the voice of the Vault itself—a whisper only you can hear. It calls to you through stone and shadow, promising answers... if you can find the path.\n\nArmed with only your senses and your will, you step into the maze below...\n\n(Tap to continue)", { size: 70, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1800, align: 'center' }); introText.anchor.set(0.5, 0.5); introText.x = 2048 / 2; introText.y = 2732 / 2; game.addChild(introText); // Also set GUI text for voice-over setGuiText("", 'vo_loading_story'); playNarration('narr_intro'); // Wait for tap to continue instead of auto-advancing } else if (state === STATE_PUZZLE1) { setGuiText("Darkness surrounds you.\nTap to feel your way forward.\nDouble tap when something stirs... it may be more than stone.", 'vo_dungeon_intro'); playNarration('narr_puzzle1'); // Start dungeon background sounds LK.playMusic('dungeon_background_sounds'); // Only create elements if they don't exist yet if (typeof puzzle1Elements === "undefined" || puzzle1Elements.length === 0) { puzzle1Step = 0; puzzle1Elements = []; chestOpened = false; hasKey = false; // Random positions for puzzle 1 elements var positions = []; // Generate random positions ensuring they don't overlap and are within bounds for (var p = 0; p < 2; p++) { var validPosition = false; var attempts = 0; while (!validPosition && attempts < 50) { var newX = 200 + Math.random() * (2048 - 400); // Keep away from edges var newY = 400 + Math.random() * (2732 - 800); // Keep away from edges and top menu // Check if position is far enough from existing positions var tooClose = false; for (var existingPos = 0; existingPos < positions.length; existingPos++) { var dx = newX - positions[existingPos].x; var dy = newY - positions[existingPos].y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 300) { // Minimum distance between elements tooClose = true; break; } } if (!tooClose) { positions.push({ x: newX, y: newY }); validPosition = true; } attempts++; } // Fallback if no valid position found after attempts if (!validPosition) { positions.push({ x: 400 + p * 600, y: 800 + p * 400 }); } } // Key var key = new Key(); key.x = positions[0].x; key.y = positions[0].y; game.addChild(key); puzzle1Elements.push(key); // Door var door = new Door(); door.x = positions[1].x; door.y = positions[1].y; game.addChild(door); puzzle1Elements.push(door); } } else if (state === STATE_PUZZLE1_SUCCESS) { // Remove puzzle elements if (typeof puzzle1Elements !== "undefined") { for (var i = 0; i < puzzle1Elements.length; i++) { puzzle1Elements[i].destroy(); } } } else if (state === STATE_PUZZLE2) { setGuiText("A quiet chamber. \nFaint scents and the weight of stone surround you.", 'vo_puzzle2_intro'); // Clear previous puzzle elements if (puzzle2Elements.length > 0) { for (var i = 0; i < puzzle2Elements.length; i++) { puzzle2Elements[i].destroy(); } } puzzle2Elements = []; bothChestsOpened = false; hasVialOfClarity = false; trapActive = false; // Generate random positions for 2 chests and 1 door var positions = []; for (var p = 0; p < 3; p++) { var validPosition = false; var attempts = 0; while (!validPosition && attempts < 50) { var newX = 300 + Math.random() * (2048 - 600); var newY = 500 + Math.random() * (2732 - 1000); var tooClose = false; for (var existingPos = 0; existingPos < positions.length; existingPos++) { var dx = newX - positions[existingPos].x; var dy = newY - positions[existingPos].y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 400) { tooClose = true; break; } } if (!tooClose) { positions.push({ x: newX, y: newY }); validPosition = true; } attempts++; } if (!validPosition) { positions.push({ x: 400 + p * 500, y: 900 + p * 300 }); } } // Randomly assign which chest is Vial and which is Trap var vialChestIndex = Math.floor(Math.random() * 2); var trapChestIndex = 1 - vialChestIndex; // Create chests for (var c = 0; c < 2; c++) { var chest = new Chest(); var isVial = c === vialChestIndex; var isTrap = c === trapChestIndex; chest.setup(c, isVial, isTrap); chest.x = positions[c].x; chest.y = positions[c].y; game.addChild(chest); puzzle2Elements.push(chest); } // Create stone door (always visible) var stoneDoor = new StoneDoor(); stoneDoor.x = positions[2].x; stoneDoor.y = positions[2].y; stoneDoor.visible = true; // Always visible game.addChild(stoneDoor); puzzle2Elements.push(stoneDoor); } else if (state === STATE_PUZZLE2_SUCCESS) { // Clean up puzzle 2 elements if (puzzle2Elements.length > 0) { for (var i = 0; i < puzzle2Elements.length; i++) { puzzle2Elements[i].destroy(); } puzzle2Elements = []; } // Stop annoying trap sound if running stopAnnoyingTrapSound(); // Individual success narration for puzzle2 setGuiText("A deep rumble. The door yields, revealing the unknown beyond.", 'vo_puzzle2_success'); // Wait for the success message voice-over to finish before starting puzzle 3 var successVO = LK.getSound('vo_puzzle2_success'); var successDuration = 5000; // Estimated duration for puzzle 2 success message // Transition to puzzle 3 after success message finishes LK.setTimeout(function () { goToState(STATE_PUZZLE3); }, successDuration); } else if (state === STATE_PUZZLE3) { setGuiText("Another chamber. \nYou sense a gate ahead, but something blocks it.", 'vo_puzzle3_intro'); playNarration('vo_puzzle3_intro', { loop: false }); // Don't clear puzzle 3 elements, just remove them from display if needed if (puzzle3Elements.length > 0) { for (var i = 0; i < puzzle3Elements.length; i++) { if (puzzle3Elements[i].parent) { puzzle3Elements[i].parent.removeChild(puzzle3Elements[i]); } } } // Only generate positions if elements don't exist yet if (puzzle3Elements.length === 0) { // Generate positions for puzzle 3 elements var positions = []; for (var p = 0; p < 3; p++) { var validPosition = false; var attempts = 0; while (!validPosition && attempts < 50) { var newX = 300 + Math.random() * (2048 - 600); var newY = 500 + Math.random() * (2732 - 1000); var tooClose = false; for (var existingPos = 0; existingPos < positions.length; existingPos++) { var dx = newX - positions[existingPos].x; var dy = newY - positions[existingPos].y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 400) { tooClose = true; break; } } if (!tooClose) { positions.push({ x: newX, y: newY }); validPosition = true; } attempts++; } if (!validPosition) { positions.push({ x: 400 + p * 500, y: 900 + p * 300 }); } } // Create gate puzzle3Gate = new Gate(); puzzle3Gate.x = positions[0].x; puzzle3Gate.y = positions[0].y; game.addChild(puzzle3Gate); puzzle3Elements.push(puzzle3Gate); // Create floor tile puzzle3FloorTile = new FloorTile(); puzzle3FloorTile.x = positions[1].x; puzzle3FloorTile.y = positions[1].y; puzzle3FloorTile.linkedGate = puzzle3Gate; game.addChild(puzzle3FloorTile); puzzle3Elements.push(puzzle3FloorTile); // Create heavy rock puzzle3Rock = new HeavyRock(); puzzle3Rock.x = positions[2].x; puzzle3Rock.y = positions[2].y; // Store initial position to prevent unwanted position changes puzzle3Rock.initialX = positions[2].x; puzzle3Rock.lastX = positions[2].x; // Initialize lastX puzzle3Rock.initialY = positions[2].y; puzzle3Rock.lastY = positions[2].y; // Initialize lastY game.addChild(puzzle3Rock); puzzle3Elements.push(puzzle3Rock); } else { // Re-add existing elements to the game for (var i = 0; i < puzzle3Elements.length; i++) { game.addChild(puzzle3Elements[i]); } } } else if (state === STATE_PUZZLE3_SUCCESS) { // Clean up puzzle 3 elements if (puzzle3Elements.length > 0) { for (var i = 0; i < puzzle3Elements.length; i++) { puzzle3Elements[i].destroy(); } puzzle3Elements = []; } setGuiText("Success! The gate remains open. You've mastered the chamber's mechanics.", 'vo_puzzle3_success'); // Transition to puzzle 4 after a delay LK.setTimeout(function () { goToState(STATE_PUZZLE4); }, 3000); } else if (state === STATE_PUZZLE4) { setGuiText("You enter a chamber of whispered melodies.\nThree chimes hang before you, each with its own voice.\nListen to their songs... from the deepest tone to the highest.\nA simple harmony awaits.", 'vo_puzzle4_simple_intro'); // Clear previous puzzle elements if (puzzle4Elements.length > 0) { for (var i = 0; i < puzzle4Elements.length; i++) { puzzle4Elements[i].destroy(); } } puzzle4Elements = []; correctChimeSequence = [0, 1, 2]; // Simple sequence: low to high pitch playerChimeSequence = []; puzzle4Complete = false; // Create 3 chimes arranged horizontally var chimeWidth = 2048 / 3; for (var i = 0; i < 3; i++) { var chime = new Chime(); chime.setup(i); chime.x = chimeWidth * i + chimeWidth / 2; chime.y = 2732 / 2; // Assign chime sounds - use chimes 1, 2, 3 for low to high progression chime.chimeSound = LK.getSound('sfx_chime_' + (i + 1)); game.addChild(chime); puzzle4Elements.push(chime); } } else if (state === STATE_PUZZLE4_SUCCESS) { // Clean up puzzle 4 elements if (puzzle4Elements.length > 0) { for (var i = 0; i < puzzle4Elements.length; i++) { puzzle4Elements[i].destroy(); } puzzle4Elements = []; } setGuiText("The gentle harmony resonates perfectly.\nYou've learned the language of the chimes.\nA passage opens to deeper mysteries.", 'vo_puzzle4_simple_success'); // Transition to puzzle 5 after a delay LK.setTimeout(function () { goToState(STATE_PUZZLE5); }, 3000); } else if (state === STATE_PUZZLE5) { setGuiText("The room hums with delicate tension.\n Chimes sway in the unseen air.\n Some call to you gently, others pulse with danger. \n Listen carefully...", 'vo_puzzle5_intro'); // Clear previous puzzle elements if (puzzle4Elements.length > 0) { for (var i = 0; i < puzzle4Elements.length; i++) { puzzle4Elements[i].destroy(); } } puzzle4Elements = []; correctChimeSequence = []; playerChimeSequence = []; puzzle4Complete = false; // Create 4 chimes arranged horizontally var chimeWidth = 2048 / 4; for (var i = 0; i < 4; i++) { var chime = new Chime(); chime.setup(i); chime.x = chimeWidth * i + chimeWidth / 2; chime.y = 2732 / 2; // Assign chime sounds chime.chimeSound = LK.getSound('sfx_chime_' + (i + 1)); game.addChild(chime); puzzle4Elements.push(chime); } // Generate unique random sequence var availableChimes = [0, 1, 2, 3]; for (var i = 0; i < 4; i++) { var randomIndex = Math.floor(Math.random() * availableChimes.length); var selectedChime = availableChimes.splice(randomIndex, 1)[0]; correctChimeSequence.push(selectedChime); } // Wait for puzzle 5 intro VO to complete before playing chime sequence var puzzle5IntroVO = LK.getSound('vo_puzzle5_intro'); var introDuration = 11000; // Estimated duration, adjust based on actual VO length LK.setTimeout(function () { playChimeSequence(); }, introDuration); } else if (state === STATE_PUZZLE5_SUCCESS) { var showPuzzle5Congrats = function showPuzzle5Congrats() { if (waitingForPuzzle5Congrats) { waitingForPuzzle5Congrats = false; setGuiText("Congratulations! You've completed all puzzles.\n\nThank you for playing!", 'vo_game_complete'); } }; // Clean up puzzle 5 elements if (puzzle4Elements.length > 0) { for (var i = 0; i < puzzle4Elements.length; i++) { puzzle4Elements[i].destroy(); } puzzle4Elements = []; } setGuiText("Harmony at last. The chimes fall silent \n and a distant door creaks open.", 'vo_puzzle5_success'); var waitingForPuzzle5Congrats = true; // Allow player to tap to see final message var oldDown = game.down; game.down = function (x, y, obj) { if (currentState === STATE_PUZZLE5_SUCCESS && waitingForPuzzle5Congrats) { showPuzzle5Congrats(); return; } if (typeof oldDown === "function") { oldDown(x, y, obj); } }; } lockInput(800); // Create toggle button for tapCircle visibility (only when there are interactive elements) var hasInteractiveElements = state === STATE_PUZZLE1 && puzzle1Elements.length > 0 || state === STATE_PUZZLE2 && puzzle2Elements.length > 0 || state === STATE_PUZZLE3 && puzzle3Elements.length > 0 || state === STATE_PUZZLE4 && puzzle4Elements.length > 0 || state === STATE_PUZZLE5 && puzzle4Elements.length > 0; if (state !== STATE_HEADPHONES && hasInteractiveElements) { tapCircleToggle = new Container(); var eyeIcon = tapCircleToggle.attachAsset(tapCirclesVisible ? 'eyeOpen' : 'eyeClosed', { anchorX: 0.5, anchorY: 0.5 }); tapCircleToggle.interactive = true; tapCircleToggle.buttonMode = true; tapCircleToggle.down = function () { tapCirclesVisible = !tapCirclesVisible; // Update the eye icon tapCircleToggle.removeChildren(); var newEyeIcon = tapCircleToggle.attachAsset(tapCirclesVisible ? 'eyeOpen' : 'eyeClosed', { anchorX: 0.5, anchorY: 0.5 }); // Update visibility of all tapCircles only updateTapCircleVisibility(); }; LK.gui.top.addChild(tapCircleToggle); tapCircleToggle.y = 100; // Move toggle button 100 pixels down } } // --- Puzzle 1 Logic --- // For MVP: Simple puzzle, e.g. "Tap twice to continue" function handlePuzzle1Tap() { if (puzzle1Step === 0) { // First tap puzzle1Step = 1; LK.getSound('sfx_tap').play(); // Optionally, play a short instruction or feedback setGuiText("Good! Tap again to solve the puzzle."); lockInput(600); } else if (puzzle1Step === 1) { // Second tap, puzzle solved LK.getSound('sfx_success').play(); goToState(STATE_PUZZLE1_SUCCESS); } } // --- Input Handling --- // Visual tap feedback for sighted users function showTapFeedback(x, y) { var tapFx = new TapFeedback(); tapFx.showAt(x, y); game.addChild(tapFx); } // Main tap handler game.down = function (x, y, obj) { if (inputLocked) { return; } // Check if this is a double-tap at game level var now = Date.now(); var timeSinceLastTap = now - lastTapTime; var tapDistance = Math.sqrt((x - lastTapX) * (x - lastTapX) + (y - lastTapY) * (y - lastTapY)); var isDoubleTap = timeSinceLastTap < doubleTapThreshold && timeSinceLastTap > 50 && tapDistance < doubleTapDistanceThreshold; // First check if tap is on an interactive element var tappedElement = null; var tapOnElement = false; // Check puzzle 1 elements if (currentState === STATE_PUZZLE1 && typeof puzzle1Elements !== "undefined") { for (var i = 0; i < puzzle1Elements.length; i++) { var el = puzzle1Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { tappedElement = el; tapOnElement = true; break; } } } // Check puzzle 2 elements if (currentState === STATE_PUZZLE2 && typeof puzzle2Elements !== "undefined") { for (var i = 0; i < puzzle2Elements.length; i++) { var el = puzzle2Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { tappedElement = el; tapOnElement = true; break; } } } // Check puzzle 3 elements if (currentState === STATE_PUZZLE3 && typeof puzzle3Elements !== "undefined") { for (var i = 0; i < puzzle3Elements.length; i++) { var el = puzzle3Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { tappedElement = el; tapOnElement = true; break; } } } // If tapping on an element, let the element handle voice-over management // Only stop voice-over if NOT tapping on an element, or if it's a single tap on non-interactive area if (currentVoiceOver && !tapOnElement) { currentVoiceOver.stop(); currentVoiceOver = null; } // Update last tap info lastTapTime = now; lastTapX = x; lastTapY = y; showTapFeedback(x, y); if (currentState === STATE_HEADPHONES) { if (isDoubleTap) { goToState(STATE_MENU); } } else if (currentState === STATE_MENU) { // Menu taps are handled by individual menu option elements } else if (currentState === STATE_HOW_TO_PLAY) { if (isDoubleTap) { goToState(STATE_MENU); } } else if (currentState === STATE_CREDITS) { if (isDoubleTap) { goToState(STATE_MENU); } } else if (currentState === STATE_INTRO) { // Double tap to continue from intro to puzzle if (isDoubleTap) { goToState(STATE_PUZZLE1); } } else if (currentState === STATE_PUZZLE1) { // Route tap to nearest element if within range, else generic feedback var tapped = false; if (typeof puzzle1Elements !== "undefined") { for (var i = 0; i < puzzle1Elements.length; i++) { var el = puzzle1Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { // within tapCircle radius if (typeof el.down === "function") { el.down(x, y, obj); tapped = true; break; } } } } if (!tapped) { // Pool of random 'nothing here' phrases and their corresponding voice-over asset ids var nothingHerePhrasesVO = [{ text: "Just cold stone beneath your fingers.", vo: "vo_nothing_cold_stone" }, { text: "Nothing unusual here.", vo: "vo_nothing_unusual" }, { text: "Rough wall. Nothing of interest.", vo: "vo_nothing_rough_wall" }, { text: "Only silence greets you.", vo: "vo_nothing_silence" }, { text: "You reach out… and find nothing new.", vo: "vo_nothing_reach_out" }, { text: "Your hand brushes empty air.", vo: "vo_nothing_brush_air" }, { text: "It’s quiet here. Too quiet.", vo: "vo_nothing_quiet" }, { text: "Nothing but shadows.", vo: "vo_nothing_shadows" }, { text: "This spot feels empty.", vo: "vo_nothing_spot_empty" }, { text: "Just part of the dungeon wall.", vo: "vo_nothing_dungeon_wall" }, { text: "You feel around, but there’s nothing here.", vo: "vo_nothing_feel_around" }, { text: "Only old stone. Try elsewhere.", vo: "vo_nothing_old_stone" }]; // Pick a random phrase and its voice-over var idx = Math.floor(Math.random() * nothingHerePhrasesVO.length); setGuiText(nothingHerePhrasesVO[idx].text, nothingHerePhrasesVO[idx].vo); } } else if (currentState === STATE_PUZZLE1_SUCCESS) { // Double tap to continue to puzzle 2 if (isDoubleTap) { goToState(STATE_PUZZLE2); } } else if (currentState === STATE_PUZZLE2) { // Route tap to nearest element var tapped = false; for (var i = 0; i < puzzle2Elements.length; i++) { var el = puzzle2Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { if (typeof el.down === "function") { el.down(x, y, obj); tapped = true; break; } } } if (!tapped) { // Pool of random 'nothing here' phrases and their corresponding voice-over asset ids var nothingHerePhrasesVO = [{ text: "Just cold stone beneath your fingers.", vo: "vo_nothing_cold_stone" }, { text: "Nothing unusual here.", vo: "vo_nothing_unusual" }, { text: "Rough wall. Nothing of interest.", vo: "vo_nothing_rough_wall" }, { text: "Only silence greets you.", vo: "vo_nothing_silence" }, { text: "You reach out… and find nothing new.", vo: "vo_nothing_reach_out" }, { text: "Your hand brushes empty air.", vo: "vo_nothing_brush_air" }, { text: "It’s quiet here. Too quiet.", vo: "vo_nothing_quiet" }, { text: "Nothing but shadows.", vo: "vo_nothing_shadows" }, { text: "This spot feels empty.", vo: "vo_nothing_spot_empty" }, { text: "Just part of the dungeon wall.", vo: "vo_nothing_dungeon_wall" }, { text: "You feel around, but there’s nothing here.", vo: "vo_nothing_feel_around" }, { text: "Only old stone. Try elsewhere.", vo: "vo_nothing_old_stone" }]; // Pick a random phrase and its voice-over var idx = Math.floor(Math.random() * nothingHerePhrasesVO.length); setGuiText(nothingHerePhrasesVO[idx].text, nothingHerePhrasesVO[idx].vo); } } else if (currentState === STATE_PUZZLE2_SUCCESS) { // Double tap to continue to puzzle 3 if (isDoubleTap) { goToState(STATE_PUZZLE3); } } else if (currentState === STATE_PUZZLE3) { // Route tap to nearest element var tapped = false; for (var i = 0; i < puzzle3Elements.length; i++) { var el = puzzle3Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { if (typeof el.down === "function") { el.down(x, y, obj); tapped = true; break; } } } if (!tapped) { // Random 'nothing here' feedback var nothingHerePhrasesVO = [{ text: "Just cold stone beneath your fingers.", vo: "vo_nothing_cold_stone" }, { text: "Nothing unusual here.", vo: "vo_nothing_unusual" }, { text: "Rough wall. Nothing of interest.", vo: "vo_nothing_rough_wall" }, { text: "Only silence greets you.", vo: "vo_nothing_silence" }, { text: "You reach out… and find nothing new.", vo: "vo_nothing_reach_out" }]; var idx = Math.floor(Math.random() * nothingHerePhrasesVO.length); setGuiText(nothingHerePhrasesVO[idx].text, nothingHerePhrasesVO[idx].vo); } } else if (currentState === STATE_PUZZLE3_SUCCESS) { // Handled by state transition } else if (currentState === STATE_PUZZLE4) { // Route tap to chimes var tapped = false; for (var i = 0; i < puzzle4Elements.length; i++) { var el = puzzle4Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { if (typeof el.down === "function") { el.down(x, y, obj); tapped = true; break; } } } if (!tapped) { setGuiText("Three gentle voices await... \nStart with the deepest tone.", 'vo_tap_chimes_hint'); } } else if (currentState === STATE_PUZZLE4_SUCCESS) { // Double tap to continue to puzzle 5 if (isDoubleTap) { goToState(STATE_PUZZLE5); } } else if (currentState === STATE_PUZZLE5) { // Route tap to chimes var tapped = false; for (var i = 0; i < puzzle4Elements.length; i++) { var el = puzzle4Elements[i]; var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius) { if (typeof el.down === "function") { el.down(x, y, obj); tapped = true; break; } } } if (!tapped) { setGuiText("The chimes hang in stillness… \naligned at the heart of the chamber....", 'vo_tap_chimes_hint'); } } else if (currentState === STATE_PUZZLE5_SUCCESS) { // Double tap to restart game if (isDoubleTap) { goToState(STATE_HEADPHONES); } } }; // Prevent drag/hold from causing issues game.move = function (x, y, obj) { // Handle rock dragging in puzzle 3 if (currentState === STATE_PUZZLE3 && puzzle3Rock && puzzle3Rock.isDragging) { puzzle3Rock.handleDrag(x, y, obj); } }; game.up = function (x, y, obj) { // Route up event to elements for hold detection if (currentState === STATE_PUZZLE1) { if (typeof puzzle1Elements !== "undefined") { for (var i = 0; i < puzzle1Elements.length; i++) { var el = puzzle1Elements[i]; if (typeof el.up === "function" && el.isHolding) { el.up(x, y, obj); } } } } else if (currentState === STATE_PUZZLE3) { // Always stop rock dragging on any finger lift if (puzzle3Rock && puzzle3Rock.isDragging) { puzzle3Rock.isDragging = false; // Check if rock is on the floor tile if (puzzle3FloorTile && puzzle3Rock.intersects(puzzle3FloorTile)) { puzzle3FloorTile.pressWithRock(); } else if (puzzle3FloorTile && puzzle3FloorTile.isPressedByRock) { // Rock was moved off the tile puzzle3FloorTile.releaseFromRock(); } } // Handle release for puzzle 3 elements for (var i = 0; i < puzzle3Elements.length; i++) { var el = puzzle3Elements[i]; if (typeof el.up === "function") { // Check if release is near the element var dx = x - el.x; var dy = y - el.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < el.hitRadius || el.isPressed) { el.up(x, y, obj); } } } // Do not automatically complete puzzle when rock is on tile // Player must double-tap the open gate to proceed } }; // --- Game Start --- goToState(STATE_HEADPHONES);
===================================================================
--- original.js
+++ change.js
@@ -303,10 +303,17 @@
};
self.close = function () {
if (self.isOpen) {
self.isOpen = false;
- LK.getSound('sfx_gate_close').play();
- setGuiText("The gate slides shut again.", 'vo_gate_closing');
+ tween(self, {
+ x: self.x - 200
+ }, {
+ duration: 1500,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ LK.getSound('sfx_gate_close').play();
+ }
+ });
}
};
return self;
});
screen_headphones_voice
Sound effect
vo_menu_title
Sound effect
vo_loading_story
Sound effect
vo_dungeon_intro
Sound effect
vo_nothing_cold_stone
Sound effect
vo_nothing_unusual
Sound effect
vo_nothing_rough_wall
Sound effect
vo_nothing_silence
Sound effect
vo_nothing_reach_out
Sound effect
vo_nothing_brush_air
Sound effect
vo_nothing_quiet
Sound effect
vo_nothing_shadows
Sound effect
vo_nothing_spot_empty
Sound effect
vo_nothing_dungeon_wall
Sound effect
vo_nothing_feel_around
Sound effect
vo_nothing_old_stone
Sound effect
vo_stone_door_label
Sound effect
vo_key_label
Sound effect
vo_key_pickup
Sound effect
vo_door_label
Sound effect
vo_door_unlocked
Sound effect
vo_puzzle2_intro
Sound effect
vo_vial_inspect
Sound effect
vo_vial_found
Sound effect
vo_vial_heals_trap
Sound effect
vo_trap_inspect
Sound effect
vo_trap_triggered
Sound effect
vo_trap_neutralized
Sound effect
dungeon_background_sounds
Music
vo_door_locked
Sound effect
sfx_trap_trigger
Music
sfx_door_unlock
Sound effect
sfx_key_pickup
Sound effect
sfx_chest_open
Sound effect
vial_chest_open_bgm
Music
vo_puzzle2_success
Sound effect
sfx_rock_door_rumble
Sound effect
vo_puzzle3_intro
Sound effect
vo_heavy_rock_label
Sound effect
vo_rock_drag_hint
Sound effect
vo_rock_door_rumble
Sound effect
sfx_rock_drag
Sound effect
vo_gate_label
Sound effect
vo_floor_tile_label
Sound effect
vo_gate_closed
Sound effect
vo_menu_intro
Sound effect
vo_menu_how_to_play
Sound effect
vo_menu_credits
Sound effect
vo_how_to_play_content
Sound effect
vo_credits_content
Sound effect
vo_menu_title_announce
Sound effect
vo_stone_door_locked
Sound effect
sfx_chime_1
Sound effect
sfx_chime_2
Sound effect
sfx_chime_3
Sound effect
sfx_chime_4
Sound effect
vo_chime_description_1
Sound effect
vo_chime_description_2
Sound effect
vo_chime_description_3
Sound effect
vo_chime_description_4
Sound effect
vo_chime_instruction
Sound effect
vo_puzzle4_intro
Sound effect
vo_tap_chimes_hint
Sound effect
vo_puzzle4_wrong
Sound effect
vo_repeat_sequence
Sound effect
vo_puzzle4_success
Sound effect
vo_tile_double_tap
Sound effect
vo_rock_on_tile
Sound effect
sfx_rock_place
Sound effect
vo_gate_open_announce
Sound effect
vo_puzzle5_wrong
Sound effect
vo_puzzle5_intro
Sound effect
vo_puzzle4_simple_intro
Sound effect
vo_puzzle4_simple_success
Sound effect
vo_tile_springs_back
Sound effect
vo_chime_general_description
Sound effect
vo_puzzle3_success
Sound effect
vo_puzzle5_success
Sound effect
vo_game_complete
Sound effect
vo_hint_icon_message
Sound effect