User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'visible')' in or related to this line: 'if (towerPreview.visible) {' Line Number: 3031
User prompt
Please fix the bug: 'ReferenceError: towerPreview is not defined' in or related to this line: 'if (towerPreview.visible) {' Line Number: 3030
User prompt
Please fix the bug: 'Uncaught ReferenceError: grid is not defined' in or related to this line: 'if (grid && grid.destroy) {' Line Number: 2483
User prompt
Please fix the bug: 'Uncaught ReferenceError: sourceTowers is not defined' in or related to this line: 'for (var i = 0; i < sourceTowers.length; i++) {' Line Number: 2711
User prompt
Objective: Implement a level selection screen as an initial game overlay, presenting 6 selectable maps with sequential unlock progression. **Action Items:** 1. **Asset Definition for Map Icons** * Define new image assets for the map icons. * **1.1. Add Map Icon Assets** * **Instruction:** Declare `LK.init.image` for `Map_Icon_1` through `Map_Icon_6` in the `/**** Assets ****/` section. Use placeholder IDs and dimensions (`width:300, height:300`) as actual assets are not provided. 2. **Global Game State Variables** * Introduce variables to manage the level selection state and progression. * **2.1. Add Level Select State Variables** * **Instruction:** Declare `selectedMapId = 0;` (0 for no map selected initially), `unlockedMaps = [1];` (Map 1 unlocked by default), `gameStartedFromMenu = false;`, and `totalMaps = 6;` in the `/**** Game Code ****/` section, alongside other global variables. 3. **`LevelSelectScreen` Class Implementation** * Define a new `Container.expand` class for the level selection UI. * **3.1. Class Definition** * **Instruction:** Create a `var LevelSelectScreen = Container.expand(function () { ... });` class. * **3.2. Constructor Logic** * **Instruction:** Inside the `LevelSelectScreen` constructor, add a background element (e.g., `notification` asset, scaled appropriately, tinted `0x222222`, alpha `0.9`). Position it to cover the entire game area (e.g., `self.x = 2048 / 2; self.y = 2732 / 2;`). * **Instruction:** Implement a nested loop to create a 2x3 grid of map icon containers. * For each map (from 1 to `totalMaps`): * Create a `Container` for each map icon. * Attach the corresponding `Map_Icon_X` asset. * Add a `Text2` label displaying "Map X". * Add another `Text2` label or a graphic overlay for "Locked" state. * Calculate `x` and `y` positions for the 2x3 grid, adjusting for spacing (e.g., `mapSpacingX = 400`, `mapSpacingY = 400`). Center the grid within the screen. * Set a `down` event handler for each map icon. * **3.3. `updateMapStatus` Method** * **Instruction:** Add a `self.updateMapStatus = function() { ... };` method to `LevelSelectScreen`. * **Instruction:** In `updateMapStatus`, iterate through all map icon containers. * **Instruction:** For each map, check if its `mapId` is present in the global `unlockedMaps` array. * **Instruction:** Adjust the tint, alpha, and visibility of the "Locked" overlay/text based on the `unlockedMaps` status. Make unlocked maps fully opaque and clickable, locked maps semi-transparent with a lock icon/text. * **3.4. Click Handling (`down` method for map icons)** * **Instruction:** Inside the map icon's `down` handler: * Check if the clicked map's `mapId` is in `unlockedMaps`. * If unlocked: Set `selectedMapId` to the clicked map's ID. Call a new global function `startGame(selectedMapId)`. Destroy the `LevelSelectScreen` instance. * If locked: Display a "Map is locked!" notification. * **3.5. `update` Method** * **Instruction:** Add a `self.update = function() { self.updateMapStatus(); };` method to ensure locked/unlocked states are visually refreshed. 4. **Game Initialization Refactoring** * Modify the main game initialization to show the `LevelSelectScreen` first. * **4.1. Initial Game State** * **Instruction:** In `/**** Initialize Game ****/` and `/**** Game Code ****/`, ensure that `gameStartedFromMenu` is initially `false`. * **4.2. `startGame` Function** * **Instruction:** Encapsulate the entire existing game setup logic (from `// Add Map_1 background...` down to `sourceTowers.push(tower);`) into a new global function `function startGame(mapId) { ... }`. * **Instruction:** This `startGame` function should set `gameStartedFromMenu = true;`. * **Instruction:** Modify `grid = new Grid(24, 29 + 6);` inside `startGame` to `grid = new Grid(24, 29 + 6, mapId);`. * **4.3. Initial Screen Display** * **Instruction:** In the `/**** Initialize Game ****/` section, after `var game = new LK.Game(...)`, instantiate `var levelSelectScreen = new LevelSelectScreen();`. * **Instruction:** Add `game.addChild(levelSelectScreen);`. * **Instruction:** Call `levelSelectScreen.updateMapStatus();` immediately after adding it, to correctly initialize the display. * **4.4. `game.update` Conditional Execution** * **Instruction:** Wrap the entire content of the existing `game.update = function () { ... };` function within an `if (gameStartedFromMenu) { ... }` block. 5. **`Grid` Class Integration with Map Selection** * Modify the `Grid` class to load map layouts based on the `mapId`. * **5.1. `Grid` Constructor Update** * **Instruction:** Change `var Grid = Container.expand(function (gridWidth, gridHeight) { ... });` to `var Grid = Container.expand(function (gridWidth, gridHeight, mapId) { ... });`. * **5.2. Dynamic Map Layout Loading** * **Instruction:** Replace the hardcoded `var mapLayout = [...]` in the `Grid` constructor with a call to a new helper function: `var mapLayout = getMapLayout(mapId);`. * **5.3. `getMapLayout` Function** * **Instruction:** Define a new global function `function getMapLayout(mapId) { ... }`. * **Instruction:** Inside `getMapLayout`, use a `switch` statement or `if/else if` to return different `mapLayout` 2D arrays based on the `mapId`. * For `mapId === 1`, return the existing `mapLayout` array. * For other `mapId` values, return placeholder or simplified layouts (e.g., a completely empty map or a basic path). Note that unique `mapLayout` data for `Map_2` through `Map_6` must be provided externally. 6. **Game Win Condition and Level Progression** * Modify `LK.showYouWin()` to unlock the next map and return to the level selection screen. * **6.1. `resetGame` Function** * **Instruction:** Create a new global function `function resetGame() { ... }`. * **Instruction:** Move all current game state resets (clearing `enemies`, `bullets`, `coins` arrays, setting `lives = 20`, `gold = 80`, `score = 0`, `currentWave = 0`, `waveTimer = 0`, `waveInProgress = false`, `waveSpawned = false`, etc.) into this `resetGame` function. * **Instruction:** Ensure all dynamically added children (`enemyLayer`, `bullets`, `towers`, `goldIndicators`, `notifications`) are properly `destroy()`-ed and removed from their parent containers and global arrays within `resetGame`. This is critical for memory management. Example: `for (var i = enemies.length - 1; i >= 0; i--) { enemies[i].destroy(); } enemies = [];`. Apply this pattern to all game entities. * **Instruction:** Call `updateUI()` at the end of `resetGame`. * **6.2. `LK.showYouWin` Modification** * **Instruction:** In the `if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress)` block within `game.update` (which calls `LK.showYouWin()`): * **Instruction:** Add `if (selectedMapId < totalMaps && !unlockedMaps.includes(selectedMapId + 1)) { unlockedMaps.push(selectedMapId + 1); }`. * **Instruction:** Add a `Notification` announcing "Map X Unlocked!" or "All Maps Complete!". * **Instruction:** Call `resetGame();`. * **Instruction:** Set `gameStartedFromMenu = false;`. * **Instruction:** Instantiate and add `levelSelectScreen = new LevelSelectScreen();` to the `game` object. * **Instruction:** Call `levelSelectScreen.updateMapStatus();` immediately after adding it. * **Instruction:** Remove `LK.showYouWin();` or ensure it does not immediately terminate the game, but rather allows the transition back to the menu. 7. **Map Icon Styling & Layout (within `LevelSelectScreen`)** * **7.1. Positioning** * **Instruction:** Define `gridCols = 3; gridRows = 2;` and `iconWidth = 300; iconHeight = 300;` (matching asset size). * **Instruction:** Calculate `paddingX = 100; paddingY = 100;`. * **Instruction:** Position icons using `(col * (iconWidth + paddingX)) - (gridCols * (iconWidth + paddingX)) / 2 + iconWidth / 2` for `x` and `(row * (iconHeight + paddingY)) - (gridRows * (iconHeight + paddingY)) / 2 + iconHeight / 2` for `y`, relative to the `LevelSelectScreen` center. * **7.2. Locked Overlay** * **Instruction:** For each map icon, create a semi-transparent `Black` asset (`0xcb1010` tint, alpha `0.7`) to overlay locked maps. * **Instruction:** Add a `Text2` label "LOCKED" on top of the blocker. Control its visibility based on `unlockedMaps`.
User prompt
please create 6 new assets named between Map_Icon_1 to Map_Icon_6
User prompt
Objective: Rectify waypoint path generation logic to correctly interpret grid coordinate systems and ensure accurate ground unit movement. **Action Items:** 1. **Refactor `buildWaypointPath` Function** * Ensure `buildWaypointPath` correctly processes the `grid.cells` structure and generates accurate waypoint coordinates. * **1.1. Restore Function Signature and Core Logic** * **Instruction:** Locate the `buildWaypointPath` function definition. * **Instruction:** Replace the entire current implementation of `buildWaypointPath` with the `buildWaypointPath` function provided in "Phase 2: Step 2.1" of the original migration guide. This will restore the logic that interprets explicit path tile types (e.g., `case 1`, `case 5`). * **1.2. Correct Internal Grid Indexing** * **Instruction:** Within the restored `buildWaypointPath` function, modify the line `const grid = gridCells.map(row => row.map(cell => cell.type));` to `const internalGrid = gridCells.map(colArray => colArray.map(cell => cell.type));`. This variable will consistently represent the grid as `internalGrid[column_index][row_index]`. * **Instruction:** Update all internal references to `grid` within `buildWaypointPath` to `internalGrid`. * **Instruction:** Modify the `findTile` function's loops: Change `for (let y = 0; y < internalGrid.length; y++)` to `for (let col = 0; col < internalGrid.length; col++)`. Change `for (let x = 0; x < internalGrid[y].length; x++)` to `for (let row = 0; row < internalGrid[col].length; row++)`. * **Instruction:** Modify the `findTile` function's return statement: Change `return { x, y };` to `return { x: col, y: row };`. This ensures the returned coordinate pair is `(column, row)`. * **Instruction:** In `findFirstStep`, ensure cell type lookup uses `internalGrid[pos.x][pos.y]` instead of `internalGrid[pos.y][pos.x]`. This aligns with the `internalGrid[column_index][row_index]` structure. * **Instruction:** In `getNextWaypoint`, ensure cell type lookup uses `internalGrid[currentX][currentY]` instead of `internalGrid[currentY][currentX]`. This aligns with the `internalGrid[column_index][row_index]` structure. * **Instruction:** In `getNextWaypoint`, correctly implement directional logic. For `case 5` (Top-Left Corner), if `entryDirY === 1` (entered from top, moving down), set `nextX = currentX + 1;` (move right). If `entryDirX === 1` (entered from left, moving right), set `nextY = currentY + 1;` (move down). Apply similar re-evaluations for `case 6`, `case 7`, and `case 8` ensuring `entryDirX`/`entryDirY` are correctly matched to `nextX`/`nextY` for turns based on a `[column][row]` grid. * Specifically, for `getNextWaypoint`'s `switch` cases `5` through `8`, replace the coordinate updates with the following: * `case 5: // ┌ Corner (Top-Left)` * `if (entryDirY === 1) { nextX = currentX + 1; } else if (entryDirX === 1) { nextY = currentY + 1; }` * `case 6: // ┐ Corner (Top-Right)` * `if (entryDirY === 1) { nextX = currentX - 1; } else if (entryDirX === -1) { nextY = currentY + 1; }` * `case 7: // └ Corner (Bottom-Left)` * `if (entryDirY === -1) { nextX = currentX + 1; } else if (entryDirX === 1) { nextY = currentY - 1; }` * `case 8: // ┘ Corner (Bottom-Right)` * `if (entryDirY === -1) { nextX = currentX - 1; } else if (entryDirX === -1) { nextY = currentY - 1; }` * **Instruction:** In `buildWaypointPath`'s main `while` loop, ensure the goal check accesses the grid correctly: `while (internalGrid[currentPos.x][currentPos.y] !== 10 && path.length < MAX_PATH_LENGTH)`. * **Note:** The rest of the enemy movement and tower targeting logic from Phase 3 relies on correct `waypointPath` generation and its coordinates (`{x: col, y: row}`). This fix will ensure consistency across the system. 2. **Verify Existing Implementations** * Confirm that modifications made in Phase 3 (enemy `targetWaypointIndex` initialization and `Grid.updateEnemy` logic) correctly utilize the `waypointPath` array's `x` and `y` properties as `column` and `row` indices, respectively. * **2.1. Confirm `enemy.targetWaypointIndex` Initialization** * **Instruction:** No modification is required here, as the existing instruction `enemy.targetWaypointIndex = 1;` correctly points to the second element of the `waypointPath` array. * **2.2. Confirm `Grid.updateEnemy` Logic** * **Instruction:** No modification is required within the `Grid.updateEnemy` function for ground enemies, as `targetWaypoint.x` and `targetWaypoint.y` are already correctly interpreted as `column` and `row` indices, respectively. The logic for calculating pixel positions (`targetPixelX`, `targetPixelY`) and updating `enemy.x`, `enemy.y`, `enemy.cellX`, `enemy.cellY` is robust against the now-correct waypoint coordinates.
User prompt
Objective: Resolve ground enemy movement stall by correcting speed unit application in the new waypoint following system. **Action Items:** 1. **Grid.updateEnemy Method Refactor** * Implement pixel-per-frame speed calculation for ground enemy movement. * **1.1. Ground Enemy Movement Vector Scale Adjustment** * **Instruction:** Within the `Grid` class, locate the `self.updateEnemy` method. * **Instruction:** Navigate to the `// NEW LOGIC FOR GROUND ENEMIES` block. * **Instruction:** Modify the `enemy.x` and `enemy.y` update statements. Multiply `enemy.speed` by `CELL_SIZE` to convert grid unit speed to pixel speed for positional increments. * **Current (Incorrect) Line:** `enemy.x += Math.cos(angle) * enemy.speed;` * **Current (Incorrect) Line:** `enemy.y += Math.sin(angle) * enemy.speed;` * **Required Change:** `enemy.x += Math.cos(angle) * enemy.speed * CELL_SIZE;` * **Required Change:** `enemy.y += Math.sin(angle) * enemy.speed * CELL_SIZE;` * **Note:** This ensures `enemy.speed` remains consistent as a grid unit per frame throughout the codebase, with explicit scaling performed only at the point of pixel coordinate application.
User prompt
Objective: Rewrite enemy movement to utilize the pre-calculated `waypointPath` and update tower targeting accordingly for increased performance and reliability. Action Items: 1. **Enemy Object Initialization** * Add a new property to the `Enemy` instance for path tracking. * **Instruction:** Navigate to the `game.update` function. * **Instruction:** Locate the `for` loop responsible for spawning `Enemy` instances. * **Instruction:** Within this loop, after `enemy.currentCellY` assignment, add a property assignment: `enemy.targetWaypointIndex = 1;`. * **Note:** This sets the initial target to the second element of the `waypointPath` array, as index 0 is the spawn point itself. 2. **Grid Enemy Movement Logic Refactor** * Replace the existing ground enemy movement logic within `Grid.prototype.updateEnemy`. * **Instruction:** Access the `Grid` class definition. * **Instruction:** Locate the `self.updateEnemy` method. * **Instruction:** Replace the entire existing implementation of `self.updateEnemy` with a new composite implementation. * **2.1. Goal Reached Check** * **Instruction:** Implement logic to check if the enemy's current rounded cell coordinates match a `GOAL` cell type (value 10). If so, return `true`. * **2.2. Flying Enemy Shadow Update** * **Instruction:** Preserve the existing logic block that updates the `enemy.shadow` position and rotation if `enemy.isFlying` is `true`. * **2.3. Initial Entry Animation** * **Instruction:** Preserve the existing logic block that handles the initial vertical movement of the enemy until `enemy.currentCellY` reaches `4`. This includes updating `enemy.currentCellY`, `enemy.x`, `enemy.y`, and `enemy.children[0].rotation` to face downwards. Return `false` if this phase is active. * **2.4. Flying Enemy Pathing** * **Instruction:** Preserve the existing logic block that defines movement for `enemy.isFlying` units directly towards `self.goals[0]`, including target assignment, distance calculation, position updates, and rotation adjustments. Return `true` if the goal is reached, `false` otherwise. * **2.5. Ground Enemy Waypoint Following (New Logic)** * **Instruction:** Introduce a new logic block for non-flying enemies. * **Instruction:** Check if `enemy.targetWaypointIndex` is greater than or equal to `waypointPath.length`. If true, return `true` (enemy finished path). * **Instruction:** Retrieve the `targetWaypoint` object from the `waypointPath` array using `enemy.targetWaypointIndex`. * **Instruction:** Convert `targetWaypoint.x` and `targetWaypoint.y` (grid coordinates) into target pixel coordinates (`targetPixelX`, `targetPixelY`) by applying `grid.x`, `grid.y`, and `CELL_SIZE` offsets, ensuring the target is the center of the cell. * **Instruction:** Calculate the Euclidean distance (`dist`) and angle (`angle`) between the enemy's current pixel position (`enemy.x`, `enemy.y`) and the `targetPixelX`, `targetPixelY`. * **Instruction:** If `dist` is less than `enemy.speed * 2`, snap the enemy's `x` and `y` to `targetPixelX` and `targetPixelY`. Update `enemy.cellX` and `enemy.cellY` to `targetWaypoint.x` and `targetWaypoint.y`. Increment `enemy.targetWaypointIndex`. Immediately re-check if `enemy.targetWaypointIndex` is now out of bounds; if so, return `true`. * **Instruction:** Otherwise (if `dist` is greater or equal to `enemy.speed * 2`), move the enemy by `enemy.speed` towards `targetPixelX`, `targetPixelY` using trigonometric functions. * **Instruction:** Ensure the enemy's visual rotation (`enemy.children[0].rotation`) is smoothly interpolated towards the calculated `angle`, utilizing the existing `tween` logic for rotation. * **Instruction:** Update `enemy.currentCellX` and `enemy.currentCellY` based on the enemy's new pixel position. * **Instruction:** Return `false` to indicate the enemy has not yet reached the goal. * **Note:** This new logic replaces the old Dijkstra/A* based pathfinding for ground enemies entirely. 3. **Tower Targeting Logic Update** * Modify `Tower.prototype.findTarget` to use `enemy.targetWaypointIndex` for ground enemy prioritization. * **Instruction:** Access the `Tower` class definition. * **Instruction:** Locate the `self.findTarget` method. * **Instruction:** Replace the entire existing implementation of `self.findTarget` with a new logic. * **Instruction:** Initialize `bestTarget` to `null` and `maxWaypointIndex` to `-1`. * **Instruction:** Iterate through the global `enemies` array. * **Instruction:** For each `enemy` in range (`distance <= self.getRange()`): * **Instruction:** If `enemy.isFlying` is true: Prioritize this `enemy` if `bestTarget` is currently `null` or if `bestTarget` is not a flying enemy (indicating flying enemies have higher targeting priority). Update `bestTarget`. * **Instruction:** Else (if `enemy` is a ground enemy): Compare `enemy.targetWaypointIndex` with `maxWaypointIndex`. If `enemy.targetWaypointIndex` is greater, update `maxWaypointIndex` and set `bestTarget` to this `enemy`. * **Instruction:** After iterating, assign `self.targetEnemy` to the determined `bestTarget` (or `null` if no target found). * **Instruction:** Return `bestTarget`. * **Note:** The `cell.score` property no longer exists; `targetWaypointIndex` serves as the new metric for enemy progress along the path.
User prompt
**Objective:** Refactor the game's pathfinding system by eliminating the dynamic `grid.pathFind()` mechanism in favor of a pre-calculated, static waypoint path. **Action Items:** 1. **Waypoint Path Generation Function** * Integrate the provided path-tracing algorithm. * **1.1. Function Insertion** * **Instruction:** Insert the provided `buildWaypointPath` function definition into the global scope, immediately following the `LK.import` plugin declarations. 2. **Initial Path Computation** * Compute the definitive level path at game initialization. * **2.1. Path Calculation on Grid Initialization** * **Instruction:** Locate the `Grid` instance instantiation within the `Initialize Game` section. Immediately following the `grid.y` assignment, insert a new statement to invoke `buildWaypointPath`, passing `grid.cells` as the argument, and assign the returned value to a new global variable named `waypointPath`. Include the specified `console.log` for validation. 3. **Old Pathfinding System Removal** * Decommission all references to the deprecated `grid.pathFind()` method to prevent runtime overhead and prepare for the new enemy movement system. * **3.1. Global Call Removal** * **Instruction:** Delete the `grid.pathFind();` line in the global scope, found just before `grid.renderDebug();` during game initialization. * **3.2. `placeTower` Function Update** * **Instruction:** Remove the `grid.pathFind();` call within the global `placeTower` function. * **3.3. `Tower.placeOnGrid` Method Update** * **Instruction:** Eliminate the `grid.pathFind();` call inside the `Tower.placeOnGrid` method. * **3.4. `UpgradeMenu.sellButton.down` Handler Update** * **Instruction:** Remove the `grid.pathFind();` call within the `sellButton.down` handler of the `UpgradeMenu` class. * **3.5. `game.move` Function Update** * **Instruction:** Delete the `grid.pathFind();` call within the `game.move` function, specifically inside the `if (dragDistance > 20)` conditional block. * **3.6. `game.up` Function Update** * **Instruction:** Remove the `grid.pathFind();` call within the `game.up` function, specifically inside the `if (towerPreview.canPlace)` conditional block. * **Note:** Tower targeting logic, specifically in `Tower.findTarget`, currently relies on `cell.score` which was populated by `grid.pathFind()`. This dependency will cause towers to exhibit non-optimal targeting behavior. This is an expected side effect of this phase and will be addressed in a subsequent iteration.
User prompt
Objective: Refactor ground enemy pathfinding logic to exclusively utilize static directional cell types and clarify the role of the existing pathfinding algorithm. **Action Items:** 1. **`Grid.updateEnemy` Method Refactor** * Modify ground enemy movement logic to derive next direction from `cell.type` values (0-8) instead of the computed `cell.targets`. * **1.1. Eliminate `enemy.currentTarget` for Ground Enemies** * **Instruction:** Remove the `if (!enemy.currentTarget) { ... }` block and all subsequent `enemy.currentTarget` references within the non-flying enemy section of `Grid.updateEnemy`. * **Instruction:** Implement logic to determine `ox` and `oy` based on the current `enemy.cellX`, `enemy.cellY`, and the directional information encoded in `cell.type`. * **Note:** This change makes ground enemies strictly follow the pre-defined path segments in `mapLayout`, making the "static directional mechanism" fully operational for movement. 2. **`Grid.pathFind` Method Clarification** * The `Grid.pathFind` method calculates `score` and `targets` and performs path validation. * **Instruction:** Confirm that `Grid.pathFind` is intended to remain as a core component for **path connectivity validation** following tower placements and removals, and for checking if spawns or active enemies are blocked. * **Instruction:** Explicitly note that while `pathFind`'s `score` and `targets` calculations are necessary for this validation, they no longer directly dictate the per-frame movement of ground enemies after the `Grid.updateEnemy` modification. 3. **`Tower.findTarget` Method Review** * The `Tower.findTarget` method currently prioritizes ground enemies based on `cell.score`. * **Instruction:** Review `Tower.findTarget` to confirm if prioritizing ground enemies by `cell.score` (distance to goal) remains the desired targeting logic, given that ground enemy movement will no longer be directly guided by these scores. * **Note:** `cell.score` will still be computed by `Grid.pathFind` for path validation purposes, so the data will be available if this targeting heuristic is still desired. 4. **Ambiguous Comment Removal** * An erroneous comment "This update method was incorrectly placed here and should be removed" is present above the `EffectIndicator` class definition. `EffectIndicator` does not define an `update` method. * **Instruction:** Remove this misleading comment to improve code clarity.
User prompt
Objective: Implement a pre-calculated waypoint path system for ground enemy movement and update tower targeting to utilize this new system. Action Items: 1. **Enemy Initialization (`game.update` context)** * Initialize ground enemy path progress upon instantiation. * **1.1. `game.update` loop modification** * **Instruction:** Locate the `for` loop within the `game.update` function responsible for instantiating `Enemy` objects. * **Instruction:** Inside this loop, after existing enemy property assignments (specifically after `enemy.currentCellY = spawnCell.y - (1 + Math.random() * 5);`), add a new property assignment: `enemy.targetWaypointIndex = 1;`. * **Note:** This sets the initial target to the second waypoint, accounting for the spawn cell being the first waypoint. 2. **Grid Enemy Movement Logic (`Grid.prototype.updateEnemy`)** * Refactor ground enemy movement to traverse the `waypointPath` array. * **2.1. Method Replacement** * **Instruction:** Completely replace the existing implementation of `Grid.prototype.updateEnemy`. * **2.2. Goal Check** * **Instruction:** Retain the initial check for enemy presence on a `GOAL` cell (`cell.type == 10`), returning `true` for despawn if reached. * **2.3. Flying Enemy Shadow Management** * **Instruction:** Retain the existing logic for updating flying enemy shadows (`enemy.isFlying && enemy.shadow`). * **2.4. Initial Entry Animation** * **Instruction:** Retain the existing logic that handles initial vertical movement and rotation until the enemy reaches the viewable entry area (`enemy.currentCellY >= 4`). * **2.5. Flying Enemy Direct Movement** * **Instruction:** Retain the existing logic that directs flying enemies directly towards the closest `GOAL` tile, including position and rotation updates. * **2.6. Ground Enemy Waypoint Traversal** * **Instruction:** Implement a new code block for non-flying enemies (`!enemy.isFlying`). * **Instruction:** Check if `enemy.targetWaypointIndex` exceeds the bounds of `waypointPath.length`; if true, return `true` to despawn the enemy (goal reached). * **Instruction:** Retrieve the current target waypoint coordinates (`targetWaypoint`) from the global `waypointPath` array using `enemy.targetWaypointIndex`. * **Instruction:** Convert `targetWaypoint` grid coordinates (`x`, `y`) into absolute pixel coordinates (`targetPixelX`, `targetPixelY`) by applying `grid.x`, `grid.y`, and `CELL_SIZE`. Add `CELL_SIZE / 2` for center alignment. * **Instruction:** Calculate the Euclidean distance (`dist`) and directional components (`dx`, `dy`) between the current `enemy.x`/`enemy.y` and `targetPixelX`/`targetPixelY`. * **Instruction:** If `dist` is less than `enemy.speed * 2`, snap the `enemy.x` and `enemy.y` to `targetPixelX` and `targetPixelY`. Update `enemy.cellX` and `enemy.cellY` to `targetWaypoint.x` and `targetWaypoint.y`. Increment `enemy.targetWaypointIndex`. * **Instruction:** After incrementing `enemy.targetWaypointIndex`, check again if it exceeds `waypointPath.length`; if true, return `true`. * **Instruction:** Else (if not close enough to snap), update `enemy.x` and `enemy.y` by moving the enemy `enemy.speed` units towards the `targetPixelX`/`targetPixelY` using trigonometric functions based on the calculated angle. * **Instruction:** Preserve and adapt the existing enemy graphic rotation logic to smoothly orient `enemy.children[0]` towards the current movement direction. * **Instruction:** Update `enemy.currentCellX` and `enemy.currentCellY` based on the new `enemy.x` and `enemy.y` relative to the grid. * **Instruction:** Return `false` to indicate the enemy has not yet reached the goal. 3. **Tower Target Acquisition (`Tower.prototype.findTarget`)** * Update tower targeting to prioritize enemies further along the global `waypointPath`. * **3.1. Method Replacement** * **Instruction:** Completely replace the existing implementation of `Tower.prototype.findTarget`. * **3.2. Target Selection Logic** * **Instruction:** Initialize `bestTarget` to `null` and `maxWaypointIndex` to `-1`. * **Instruction:** Iterate through the global `enemies` array. * **Instruction:** For each `enemy`, calculate its distance from the tower (`self.x`, `self.y`). * **Instruction:** If the `enemy` is within `self.getRange()`: * If `enemy.isFlying` is `true`, prioritize it as `bestTarget` if `bestTarget` is currently `null` or if `bestTarget.isFlying` is `false`. * If `enemy.isFlying` is `false` (ground enemy), compare its `enemy.targetWaypointIndex` with `maxWaypointIndex`. If `enemy.targetWaypointIndex` is greater, update `maxWaypointIndex` and set `bestTarget` to this `enemy`. * **Instruction:** After iterating, assign `bestTarget` to `self.targetEnemy` if `bestTarget` is not `null`; otherwise, set `self.targetEnemy` to `null`. * **Instruction:** Return `bestTarget`. * **Note:** This logic prioritizes ground enemies furthest along the path, with a basic fallback to target the first flying enemy encountered within range. Further refinement for flying enemy priority or closest-to-goal for flying enemies can be implemented in a future iteration.
User prompt
**Objective:** Refactor game movement logic by migrating from a real-time A\* pathfinding system to a pre-generated waypoint path. **Action Items:** 1. **Waypoint Path Generation Function Integration** * Integrate the provided `buildWaypointPath` utility function into the global scope. * **1.1. Function Insertion** * **Instruction:** Locate `LK.import("@upit/tween.v1");`. * **Instruction:** Insert the complete `buildWaypointPath` function block immediately following the `var tween = LK.import("@upit/tween.v1");` declaration. 2. **Initial Path Calculation** * Generate the definitive waypoint path once during game initialization. * **2.1. Path Variable Instantiation** * **Instruction:** Locate the `var grid = new Grid(24, 29 + 6);` block. * **Instruction:** Insert `var waypointPath = buildWaypointPath(grid.cells);` immediately after the `grid.y = 200 - CELL_SIZE * 4;` line. * **Instruction:** Append `console.log("Waypoint Path Generated:", waypointPath);` for verification. 3. **Pathfinding System Deprecation** * Eliminate all calls to the legacy `grid.pathFind()` method, including internal calls within class methods. * **3.1. Global Initialization Call Removal** * **Instruction:** Locate the line `grid.pathFind();` that appears after the `// blockers.push(blocker);` comment block. * **Instruction:** Delete this line. * **3.2. `placeTower` Call Removal** * **Instruction:** Locate the `placeTower` function. * **Instruction:** Delete the line `grid.pathFind();` within this function. * **3.3. `UpgradeMenu.sellButton.down` Modification** * **Instruction:** Navigate to the `UpgradeMenu` class, then locate the `sellButton.down` event handler. * **Instruction:** Modify the line `grid.pathFind(); grid.renderDebug();` to `grid.renderDebug();`. * **3.4. `game.move` Call Removal** * **Instruction:** Locate the `game.move` function. * **Instruction:** Delete the line `grid.pathFind();` within the `if (dragDistance > 20)` block. * **3.5. `Tower.placeOnGrid` Internal Call Removal** * **Instruction:** Navigate to the `Tower` class, then locate the `self.placeOnGrid` function. * **Instruction:** Delete the line `grid.pathFind();` within this function. * **Note:** Ensure `self.refreshCellsInRange();` remains intact. * **3.6. Validation** * **Instruction:** Perform a full codebase search for all remaining occurrences of `grid.pathFind()` to confirm complete removal. * **Note:** Ground enemy movement will cease post-modification. This is expected behavior and indicates successful removal of the old system. Tower targeting will revert to proximity-based, which is acceptable for this phase.
Code edit (1 edits merged)
Please save this source code
User prompt
complete the migration by implementing this refactored system. # Grid Implementation & Migration Guide This document explains how to interpret and use the grid layout data from the TD Level Editor in your game engine. It provides both a reference for the data structure and a step-by-step guide for migrating from a traditional pathfinding system (like A*) to this explicit path-following model. ## Core Concept: From Path*finding* to Path*following* Traditional tower defense engines often use a single `PATH` tile type and rely on an algorithm like A* to calculate a route from `SPAWN` to `GOAL` at runtime. This editor uses a different paradigm. The path is **explicitly defined** by the level designer using a set of directional and corner path tiles. This means your game logic doesn't need to *find* a path; it just needs to *follow* the one laid out in the grid data. **The Benefits:** - **Zero Runtime Pathfinding:** The path is traced once when the level loads, creating a static list of waypoints. This saves significant CPU cycles during gameplay. - **Deterministic Movement:** Enemy movement is perfectly predictable and immune to pathfinding quirks. - **Simplified Rendering:** Each path tile number maps directly to a specific visual asset (or a specific rotation of a single asset), as detailed in `STEP_BY_STEP_MIGRATION.md`. --- ## Data Structure Reference The grid layout is exported as a JSON file containing a 2D array of numbers. ```json [ [12, 12, 12, ...], [12, 9, 2, ...], [12, 12, 7, ...], ... ] ``` ### `GridItemType` Enum Each number in the array maps to a `GridItemType`. It is highly recommended to use an enum in your game's code for clarity and type safety. | Value | Enum Name | Visual | Description | Connects Entry From... | To Exit... | | ----- | ---------------------------- | :----: | ------------------------------------------------- | ---------------------- | --------------- | | `0` | `PATH_CROSSROAD` | `+` | A 4-way intersection. | Any cardinal direction | Opposite direction | | `1` | `PATH_STRAIGHT_UP` | `↑` | A straight vertical path. | Top/Bottom | Bottom/Top | | `2` | `PATH_STRAIGHT_DOWN` | `↓` | A straight vertical path. | Top/Bottom | Bottom/Top | | `3` | `PATH_STRAIGHT_LEFT` | `←` | A straight horizontal path. | Left/Right | Right/Left | | `4` | `PATH_STRAIGHT_RIGHT` | `→` | A straight horizontal path. | Left/Right | Right/Left | | `5` | `PATH_CORNER_TOP_LEFT` | `┌` | A corner connecting paths below and to the right. | Bottom/Right | Right/Bottom | | `6` | `PATH_CORNER_TOP_RIGHT` | `┐` | A corner connecting paths below and to the left. | Bottom/Left | Left/Bottom | | `7` | `PATH_CORNER_BOTTOM_LEFT` | `└` | A corner connecting paths above and to the right. | Top/Right | Right/Top | | `8` | `PATH_CORNER_BOTTOM_RIGHT` | `┘` | A corner connecting paths above and to the left. | Top/Left | Left/Top | | `9` | `SPAWN` | | The starting point for enemies. | - | - | | `10` | `GOAL` | | The destination point for enemies. | - | - | | `11` | `TOWER_SLOT` | | A designated area for building towers. | - | - | | `12` | `EMPTY` | | An empty cell. Not part of the path. | - | - | --- ## Migration Guide: The Path-Tracing Implementation This section provides the complete JavaScript code to build a path-following system using the editor's output. ### Step 1: Load Grid Data First, parse the exported JSON string into a 2D array in your game engine. ```javascript // Example: Loading in JavaScript const gridData = JSON.parse(jsonString); ``` ### Step 2: Generate the Waypoint Path At level-load, you will call a single function to trace the pre-defined path and generate an ordered list of waypoints. This list is then used by all enemies. The main function, `buildWaypointPath`, will look like this: ```javascript /** * Traces the explicit path defined in the grid to generate a list of waypoints. * @param {number[][]} grid The 2D array of GridItemType values. * @returns { {x: number, y: number}[] } An ordered array of waypoint coordinates. */ function buildWaypointPath(grid) { // 1. Find the starting SPAWN tile. const spawnPos = findTile(grid, 9); // 9 is GridItemType.SPAWN if (!spawnPos) { console.error("No SPAWN point found on the map!"); return []; } const path = [spawnPos]; let currentPos = spawnPos; // 2. Find the first path tile adjacent to the spawn point. This is our first move. let nextPos = findFirstStep(grid, currentPos); if (!nextPos) { console.error("No path tile adjacent to the SPAWN point!"); return path; // Path is just the spawn point } let prevPos = currentPos; currentPos = nextPos; path.push(currentPos); // 3. Loop and trace the path until we hit the GOAL tile. const MAX_PATH_LENGTH = grid.length * grid[0].length; // Safety break while (grid[currentPos.y][currentPos.x] !== 10 && path.length < MAX_PATH_LENGTH) { // 10 is GOAL nextPos = getNextWaypoint(grid, currentPos, prevPos); if (!nextPos) { console.error("Path is broken or could not find next waypoint at:", currentPos); return path; // Return partial path } path.push(nextPos); prevPos = currentPos; currentPos = nextPos; } if (path.length >= MAX_PATH_LENGTH) { console.error("Path seems to be an infinite loop. Truncating."); } return path; // The final, ordered list of {x, y} waypoints } ``` ### Step 3: Implement the Helper Functions The `buildWaypointPath` function relies on a few helpers. #### Finding Tiles: `findTile` and `findFirstStep` ```javascript /** * Finds the first occurrence of a specific tile type in the grid. */ function findTile(grid, tileType) { for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { if (grid[y][x] === tileType) { return { x, y }; } } } return null; // Not found } /** * Looks around a position (like the SPAWN) to find the first valid path tile. */ function findFirstStep(grid, startPos) { const { x, y } = startPos; const neighbors = [ { pos: { x, y: y - 1 }, from: 'bottom' }, // Check Up { pos: { x, y: y + 1 }, from: 'top' }, // Check Down { pos: { x: x - 1, y }, from: 'right' }, // Check Left { pos: { x: x + 1, y }, from: 'left' }, // Check Right ]; for (const neighbor of neighbors) { const { pos } = neighbor; if (pos.y >= 0 && pos.y < grid.length && pos.x >= 0 && pos.x < grid[0].length) { const tileType = grid[pos.y][pos.x]; // If the neighboring tile is any kind of path, it's a valid first step. if (tileType >= 0 && tileType <= 8) { return pos; } } } return null; } ``` #### The Core Logic: `getNextWaypoint` This is the most critical function. It determines the next coordinate based on the current tile's type and the direction from which an enemy entered it. **Note the corrected logic for corner pieces.** ```javascript /** * Given the grid, a current position, and the previous position, find the next one. * @param {number[][]} grid The level layout. * @param {{x: number, y: number}} currentPos The current tile coordinate. * @param {{x: number, y: number}} prevPos The previous tile coordinate. * @returns {{x: number, y: number} | null} The next waypoint, or null if path is broken. */ function getNextWaypoint(grid, currentPos, prevPos) { const { x, y } = currentPos; const cellType = grid[y][x]; // Determine entry direction vector. // entryDir.y = 1 means we entered from the tile ABOVE. // entryDir.y = -1 means we entered from the tile BELOW. // entryDir.x = 1 means we entered from the tile to the LEFT. // entryDir.x = -1 means we entered from the tile to the RIGHT. const entryDir = { x: x - prevPos.x, y: y - prevPos.y }; switch (cellType) { // Straight Paths (Vertical) case 1: // PATH_STRAIGHT_UP case 2: // PATH_STRAIGHT_DOWN // Continue in the same vertical direction. return { x: x, y: y + entryDir.y }; // Straight Paths (Horizontal) case 3: // PATH_STRAIGHT_LEFT case 4: // PATH_STRAIGHT_RIGHT // Continue in the same horizontal direction. return { x: x + entryDir.x, y: y }; // --- Corner Paths --- // The logic is: "If I came from direction A, I must exit towards direction B." case 5: // PATH_CORNER_TOP_LEFT (┌, connects Bottom and Right) if (entryDir.y === -1) return { x: x + 1, y: y }; // Entered from Below, exit Right if (entryDir.x === -1) return { x: x, y: y + 1 }; // Entered from Right, exit Below break; case 6: // PATH_CORNER_TOP_RIGHT (┐, connects Bottom and Left) if (entryDir.y === -1) return { x: x - 1, y: y }; // Entered from Below, exit Left if (entryDir.x === 1) return { x: x, y: y + 1 }; // Entered from Left, exit Below break; case 7: // PATH_CORNER_BOTTOM_LEFT (└, connects Top and Right) if (entryDir.y === 1) return { x: x + 1, y: y }; // Entered from Top, exit Right if (entryDir.x === -1) return { x: x, y: y - 1 }; // Entered from Right, exit Top break; case 8: // PATH_CORNER_BOTTOM_RIGHT (┘, connects Top and Left) if (entryDir.y === 1) return { x: x - 1, y: y }; // Entered from Top, exit Left if (entryDir.x === 1) return { x: x, y: y - 1 }; // Entered from Left, exit Top break; // Crossroads just pass through. case 0: // PATH_CROSSROAD // Exit in the same direction as entry. return { x: x + entryDir.x, y: y + entryDir.y }; } // If no case matches, the path is broken or has hit a non-path tile unexpectedly. console.error(`Path broken at [${x}, ${y}] with type ${cellType} coming from [${prevPos.x}, ${prevPos.y}]`); return null; } ``` ### Step 4: Adapt Your Enemy Movement Finally, refactor your enemy's movement logic. It becomes much simpler. 1. At level start, call `buildWaypointPath(grid)` once and store the returned array. 2. Give each spawned enemy a reference to this path array. 3. Each enemy also needs a `targetWaypointIndex`, initialized to `1` (waypoint `0` is the spawn point itself, which the enemy already occupies). The enemy's `update` function will now look something like this: ```javascript // Pseudocode for an Enemy class class Enemy { constructor(path) { this.path = path; // Start at the pixel coordinates of the first waypoint (the spawn point) this.pixelPosition = convertGridToPixels(path[0]); this.targetWaypointIndex = 1; } update(deltaTime) { if (this.targetWaypointIndex >= this.path.length) { // Enemy has reached the goal! this.handleGoalReached(); return; } const targetWaypoint = this.path[this.targetWaypointIndex]; const targetPixelPosition = convertGridToPixels(targetWaypoint); // Move towards targetPixelPosition const direction = { x: targetPixelPosition.x - this.pixelPosition.x, y: targetPixelPosition.y - this.pixelPosition.y }; // ... logic to normalize direction and move based on speed * deltaTime ... this.pixelPosition.x += /* moveX */; this.pixelPosition.y += /* moveY */; // Check if we've reached the target waypoint const distanceToTarget = /* calculate distance between pixelPosition and targetPixelPosition */; if (distanceToTarget < 1) { // Use a small threshold (e.g., 1 pixel) // Snap to target position to avoid overshooting this.pixelPosition = targetPixelPosition; // Aim for the next waypoint in the list this.targetWaypointIndex++; } } handleGoalReached() { //... logic to despawn enemy and reduce player lives ... } } function convertGridToPixels(gridPos) { const CELL_SIZE = 76; // Match your game's cell size return { x: gridPos.x * CELL_SIZE + CELL_SIZE / 2, y: gridPos.y * CELL_SIZE + CELL_SIZE / 2 }; } ``` This guide provides a complete framework for using the editor's output. By shifting from dynamic pathfinding to static path-following, your game's pathing logic becomes simpler, faster, and more reliable. Good luck!
User prompt
please update the map layout. [ [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ] ]
User prompt
please implement this path [ [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ] ]
User prompt
Update Your `DebugCell.render` function Your `DebugCell` is the key. It already knows how to draw rotated arrows. I will adapt it to draw our new, more complex path types. I'd find the `render` function inside your `DebugCell` class and modify the `switch (data.type)` statement. The goal is to replace the logic for `case 0:` and `case 2:` with new logic that handles all 9 path types. Here is the helper function I would write and add to your game script: ```javascript // Add this function somewhere in your game's script. function getArrowRotationsForCell(cellType) { const PI = Math.PI; const UP = -PI / 2; const DOWN = PI / 2; const LEFT = PI; const RIGHT = 0; // The editor uses numbers 0-8 for path types. switch (cellType) { case 1: // PATH_STRAIGHT_UP return [UP]; case 2: // PATH_STRAIGHT_DOWN return [DOWN]; case 3: // PATH_STRAIGHT_LEFT return [LEFT]; case 4: // PATH_STRAIGHT_RIGHT return [RIGHT]; case 5: // PATH_CORNER_TOP_LEFT (┌) return [DOWN, RIGHT]; case 6: // PATH_CORNER_TOP_RIGHT (┐) return [DOWN, LEFT]; case 7: // PATH_CORNER_BOTTOM_LEFT (└) return [UP, RIGHT]; case 8: // PATH_CORNER_BOTTOM_RIGHT (┘) return [UP, LEFT]; case 0: // PATH_CROSSROAD (+) return [UP, DOWN, LEFT, RIGHT]; default: return []; // No arrows for non-path types } } ``` Now, I'd modify `DebugCell.render` to use this new function. I'm essentially replacing the old path drawing logic with this new, more detailed one. ```javascript // Inside your DebugCell class, the MODIFIED render function: self.render = function (data) { self.removeArrows(); // This function already exists, perfect. numberLabel.visible = false; // We don't need the score numbers anymore. // NEW: Check if it's a path type (0-8) if (data.type >= 0 && data.type <= 8) { cellGraphics.tint = 0x888888; // A neutral path color cellGraphics.alpha = 0.1; const rotations = getArrowRotationsForCell(data.type); for (let i = 0; i < rotations.length; i++) { const angle = rotations[i]; // Your existing arrow creation logic is perfect for this. if (!debugArrows[i]) { debugArrows[i] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[i].alpha = .5; self.addChildAt(debugArrows[i], 1); } debugArrows[i].rotation = angle; } } else { // This is the logic for non-path cells (SPAWN, GOAL, etc.) switch (data.type) { case 11: // TOWER_SLOT cellGraphics.tint = data.isOccupied ? 0x0088CC : 0x00BFFF; cellGraphics.alpha = 0.1; break; case 9: // SPAWN cellGraphics.tint = 0x008800; cellGraphics.alpha = 0.1; break; case 10: // GOAL cellGraphics.tint = 0x880000; cellGraphics.alpha = 0.1; break; case 12: // EMPTY default: cellGraphics.tint = 0xaaaaaa; cellGraphics.alpha = 0; break; } } numberLabel.setText(""); }; ```
User prompt
implement this new map. [ [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ], [ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 ] ]
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'var spawnCell = grid.spawns[Math.floor(Math.random() * grid.spawns.length)];' Line Number: 2589
User prompt
Please fix the bug: 'self.generateWaypointPath is not a function' in or related to this line: 'self.waypointPath = self.generateWaypointPath();' Line Number: 655
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Prevent poison effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var Coin = Container.expand(function (gridX, gridY) { var self = Container.call(this); self.gridX = gridX; self.gridY = gridY; var coinGraphics = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); // Position coin on grid self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; // Add some visual flair self.alpha = 0; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 300, easing: tween.elasticOut }); return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { self.removeArrows(); numberLabel.visible = false; // Check if it's a path type (0-8) if (data.type >= 0 && data.type <= 8) { cellGraphics.tint = 0x888888; // A neutral path color cellGraphics.alpha = 0.1; var rotations = getArrowRotationsForCell(data.type); for (var i = 0; i < rotations.length; i++) { var angle = rotations[i]; // Use existing arrow creation logic if (!debugArrows[i]) { debugArrows[i] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[i].alpha = .5; self.addChildAt(debugArrows[i], 1); } debugArrows[i].rotation = angle; } } else { // Handle non-path cells switch (data.type) { case 11: // TOWER_SLOT cellGraphics.tint = data.isOccupied ? 0x0088CC : 0x00BFFF; cellGraphics.alpha = 0.1; break; case 9: // SPAWN cellGraphics.tint = 0x008800; cellGraphics.alpha = 0.1; break; case 10: // GOAL cellGraphics.tint = 0x880000; cellGraphics.alpha = 0.1; break; case 12: // EMPTY default: cellGraphics.tint = 0xaaaaaa; cellGraphics.alpha = 0; break; } } numberLabel.setText(""); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight, mapId) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [], isOccupied: false }; } } /* Cell Types 0: Path (walkable by ground enemies) 1: Wall (impassable) 2: Spawn 3: Goal 4: Tower Slot (impassable by ground enemies, buildable) */ // Get map layout based on mapId var mapLayout = getMapLayout(mapId); for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; // Get cell type from map layout var cellType = 1; // Default to wall if (j < mapLayout.length && i < mapLayout[j].length) { cellType = mapLayout[j][i]; } // Add to appropriate arrays based on cell type if (cellType === 9) { self.spawns.push(cell); } else if (cellType === 10) { self.goals.push(cell); } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type !== 12 && node.type !== 11) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 12 && node.left.type != 12) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 12 && node.right.type != 12) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 12 && node.right.type != 12) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 12 && node.left.type != 12) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 10) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var LevelSelectScreen = Container.expand(function () { var self = Container.call(this); // Background overlay var backgroundOverlay = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, scaleX: 25, scaleY: 35 }); backgroundOverlay.tint = 0x222222; backgroundOverlay.alpha = 0.9; // Grid layout settings var gridCols = 3; var gridRows = 2; var iconWidth = 300; var iconHeight = 300; var paddingX = 100; var paddingY = 100; // Map icon containers var mapIconContainers = []; // Create map icons in 2x3 grid for (var mapId = 1; mapId <= totalMaps; mapId++) { var col = (mapId - 1) % gridCols; var row = Math.floor((mapId - 1) / gridCols); // Calculate position var x = col * (iconWidth + paddingX) - gridCols * (iconWidth + paddingX) / 2 + iconWidth / 2; var y = row * (iconHeight + paddingY) - gridRows * (iconHeight + paddingY) / 2 + iconHeight / 2; // Create icon container var iconContainer = new Container(); iconContainer.x = x; iconContainer.y = y; iconContainer.mapId = mapId; // Add map icon var mapIcon = iconContainer.attachAsset('Map_Icon_' + mapId, { anchorX: 0.5, anchorY: 0.5 }); // Add map label var mapLabel = new Text2('Map ' + mapId, { size: 60, fill: 0xFFFFFF, weight: 800 }); mapLabel.anchor.set(0.5, 0.5); mapLabel.y = iconHeight / 2 + 40; iconContainer.addChild(mapLabel); // Add locked overlay var lockedOverlay = iconContainer.attachAsset('blocker', { anchorX: 0.5, anchorY: 0.5 }); lockedOverlay.tint = 0xcb1010; lockedOverlay.alpha = 0.7; lockedOverlay.width = iconWidth; lockedOverlay.height = iconHeight; // Add locked text var lockedText = new Text2('LOCKED', { size: 50, fill: 0xFFFFFF, weight: 800 }); lockedText.anchor.set(0.5, 0.5); lockedOverlay.addChild(lockedText); iconContainer.lockedOverlay = lockedOverlay; // Add click handler iconContainer.down = function (x, y, obj) { var clickedMapId = this.mapId; if (unlockedMaps.indexOf(clickedMapId) !== -1) { // Map is unlocked, start game selectedMapId = clickedMapId; startGame(selectedMapId); self.destroy(); } else { // Map is locked, show notification var notification = game.addChild(new Notification("Map is locked!")); notification.x = 2048 / 2; notification.y = 2732 / 2; } }; mapIconContainers.push(iconContainer); self.addChild(iconContainer); } self.updateMapStatus = function () { for (var i = 0; i < mapIconContainers.length; i++) { var container = mapIconContainers[i]; var mapId = container.mapId; var isUnlocked = unlockedMaps.indexOf(mapId) !== -1; // Update locked overlay visibility container.lockedOverlay.visible = !isUnlocked; // Update icon appearance if (isUnlocked) { container.children[0].alpha = 1; container.children[0].tint = 0xFFFFFF; } else { container.children[0].alpha = 0.5; container.children[0].tint = 0x888888; } } }; self.update = function () { self.updateMapStatus(); }; // Position the screen self.x = 2048 / 2; self.y = 2732 / 2; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { return 12 * CELL_SIZE; // Significantly increased range for max level } return (5 + (self.level - 1) * 0.8) * CELL_SIZE; case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'slow': // Slow: base 3.5, +0.5 per level return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; case 'poison': // Poison: base 3.2, +0.5 per level return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 25; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'sniper': baseGraphics.tint = 0xFF5500; break; case 'splash': baseGraphics.tint = 0x33CC00; break; case 'slow': baseGraphics.tint = 0x9900FF; break; case 'poison': baseGraphics.tint = 0x00FFAA; break; default: baseGraphics.tint = 0xAAAAAA; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'sniper': towerLevelIndicator.tint = 0xFF5500; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; case 'slow': towerLevelIndicator.tint = 0x9900FF; break; case 'poison': towerLevelIndicator.tint = 0x00FFAA; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { // Store drag start position dragStartX = x; dragStartY = y; draggedTower = self; var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } // Close any existing menus when starting to drag for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } // Remove all range indicators when starting to drag for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; // Store original position for potential dragging draggedTowerOriginalX = self.gridX; draggedTowerOriginalY = self.gridY; // Don't clear grid cells here - only clear them when actually dragging starts }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'sniper': bullet.children[0].tint = 0xFF5500; bullet.children[0].width = 15; bullet.children[0].height = 15; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; case 'slow': bullet.children[0].tint = 0x9900FF; bullet.children[0].width = 35; bullet.children[0].height = 35; break; case 'poison': bullet.children[0].tint = 0x00FFAA; bullet.children[0].width = 35; bullet.children[0].height = 35; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; // Mark all cells in the 2x2 tower area as occupied for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.isOccupied = true; } } } // Recalculate pathfinding after placing tower to ensure paths are updated grid.pathFind(); self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'poison': previewGraphics.tint = 0x00FFAA; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 11 || cell.isOccupied === true) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var displayName = self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower'; var towerTypeText = new Text2(displayName, { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; var buttonText; var upgradeCost; // For towers, show Upgrade button var baseUpgradeCost = getTowerCost(self.tower.id); if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); // Add sell button for towers var sellButton; var sellButtonText; sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { // Handle tower upgrade if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; if (sellButton) { sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.isOccupied = false; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; } closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { // For towers, handle upgrade logic if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Add Map_1 background without stretching var backgroundMap = game.attachAsset('Map_1', { anchorX: 0, anchorY: 0, x: 0, y: 170 }); var selectedMapId = 0; var unlockedMaps = [1]; var gameStartedFromMenu = false; var totalMaps = 6; var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 76; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var coins = []; var selectedTower = null; var gold = 80; var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); function startGame(mapId) { gameStartedFromMenu = true; selectedMapId = mapId; // Create new grid with selected map if (grid && grid.destroy) { grid.destroy(); } grid = new Grid(24, 29 + 6, mapId); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; // Initial blockers can be placed manually on specific tower slots if desired // For example: // var blocker = new Blocker(); // blocker.placeOnGrid(4, 8); // Place at tower slot coordinates // towerLayer.addChild(blocker); // blockers.push(blocker); grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; } var isDragging = false; var draggedTower = null; var dragStartX = 0; var dragStartY = 0; var draggedTowerOriginalX = 0; var draggedTowerOriginalY = 0; // Helper function to get arrow rotations for different path cell types function getArrowRotationsForCell(cellType) { var PI = Math.PI; var UP = -PI / 2; var DOWN = PI / 2; var LEFT = PI; var RIGHT = 0; // The editor uses numbers 0-8 for path types. switch (cellType) { case 1: // PATH_STRAIGHT_UP return [UP]; case 2: // PATH_STRAIGHT_DOWN return [DOWN]; case 3: // PATH_STRAIGHT_LEFT return [LEFT]; case 4: // PATH_STRAIGHT_RIGHT return [RIGHT]; case 5: // PATH_CORNER_TOP_LEFT (┌) return [DOWN, RIGHT]; case 6: // PATH_CORNER_TOP_RIGHT (┐) return [DOWN, LEFT]; case 7: // PATH_CORNER_BOTTOM_LEFT (└) return [UP, RIGHT]; case 8: // PATH_CORNER_BOTTOM_RIGHT (┘) return [UP, LEFT]; case 0: // PATH_CROSSROAD (+) return [UP, DOWN, LEFT, RIGHT]; default: return []; // No arrows for non-path types } } function getMapLayout(mapId) { switch (mapId) { case 1: return [[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]]; case 2: case 3: case 4: case 5: case 6: default: // Placeholder layouts for other maps - return a simple path return [[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]]; } } function getTowerCost(towerType) { var cost = 5; switch (towerType) { case 'rapid': cost = 15; break; case 'sniper': cost = 25; break; case 'splash': cost = 35; break; case 'slow': cost = 45; break; case 'poison': cost = 55; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function resetGame() { // Clear all enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies[i].destroy(); } enemies = []; // Clear all bullets for (var i = bullets.length - 1; i >= 0; i--) { bullets[i].destroy(); } bullets = []; // Clear all towers for (var i = towers.length - 1; i >= 0; i--) { towers[i].destroy(); } towers = []; // Clear all coins for (var i = coins.length - 1; i >= 0; i--) { coins[i].destroy(); } coins = []; // Clear all upgrade menus var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } // Clear all notifications var notifications = game.children.filter(function (child) { return child instanceof Notification; }); for (var i = 0; i < notifications.length; i++) { notifications[i].destroy(); } // Clear all range indicators for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } // Reset game state lives = 20; gold = 80; score = 0; currentWave = 0; waveTimer = 0; waveInProgress = false; waveSpawned = false; selectedTower = null; isDragging = false; draggedTower = null; // Reset grid cells for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { grid.cells[i][j].isOccupied = false; grid.cells[i][j].towersInRange = []; } } updateUI(); } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); // Center the tower preview on the cursor position when starting drag towerPreview.snapToGrid(x, y); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Center the tower preview on the cursor position towerPreview.snapToGrid(x, y); } else if (draggedTower) { // Calculate distance moved to determine if this is a drag var dragDistance = Math.sqrt((x - dragStartX) * (x - dragStartX) + (y - dragStartY) * (y - dragStartY)); if (dragDistance > 20) { // Start dragging if moved more than 20 pixels isDragging = true; // Now clear the old tower position on the grid since we're actually dragging for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(draggedTower.gridX + i, draggedTower.gridY + j); if (cell) { cell.isOccupied = false; // Remove tower from cell's towersInRange array var towerIndex = cell.towersInRange.indexOf(draggedTower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } grid.pathFind(); // Recalculate path after removing tower grid.renderDebug(); // Update debug view towerPreview.visible = true; towerPreview.towerType = draggedTower.id; towerPreview.updateAppearance(); towerPreview.snapToGrid(x, y); // Hide the original tower while dragging draggedTower.visible = false; } } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (draggedTower) { // Show the tower again draggedTower.visible = true; // Handle moving existing tower if (towerPreview.canPlace) { // Move tower to new position draggedTower.placeOnGrid(towerPreview.gridX, towerPreview.gridY); grid.pathFind(); grid.renderDebug(); } else if (towerPreview.blockedByEnemy) { // Blocked by enemy, restore tower to original position draggedTower.placeOnGrid(draggedTowerOriginalX, draggedTowerOriginalY); var notification = game.addChild(new Notification("Cannot move: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { // Invalid placement, restore tower to original position draggedTower.placeOnGrid(draggedTowerOriginalX, draggedTowerOriginalY); var notification = game.addChild(new Notification("Cannot move here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else { // Handle placing new tower if (towerPreview.canPlace) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } towerPreview.visible = false; draggedTower = null; // Close upgrade menus when dragging is complete var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } else if (draggedTower) { // Handle tap without drag - show upgrade menu var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = draggedTower; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = draggedTower; game.addChild(rangeIndicator); rangeIndicator.x = draggedTower.x; rangeIndicator.y = draggedTower.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = draggedTower.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(draggedTower); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); draggedTower = null; } }; // Initialize level selection screen var levelSelectScreen = new LevelSelectScreen(); game.addChild(levelSelectScreen); levelSelectScreen.updateMapStatus(); game.update = function () { if (!gameStartedFromMenu) { return; } if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // Select a random spawn point from the grid.spawns array var spawnCell = grid.spawns[Math.floor(Math.random() * grid.spawns.length)]; enemy.cellX = spawnCell.x; enemy.cellY = spawnCell.y; enemy.currentCellX = spawnCell.x; enemy.currentCellY = spawnCell.y - (1 + Math.random() * 5); // Vertical offset for entry animation enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Coin spawning removed // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { // Unlock next map if not already unlocked if (selectedMapId < totalMaps && unlockedMaps.indexOf(selectedMapId + 1) === -1) { unlockedMaps.push(selectedMapId + 1); var unlockNotification = game.addChild(new Notification("Map " + (selectedMapId + 1) + " Unlocked!")); unlockNotification.x = 2048 / 2; unlockNotification.y = 2732 / 2; } else if (selectedMapId === totalMaps) { var completeNotification = game.addChild(new Notification("All Maps Complete!")); completeNotification.x = 2048 / 2; completeNotification.y = 2732 / 2; } // Reset game and return to level selection resetGame(); gameStartedFromMenu = false; // Create new level selection screen levelSelectScreen = new LevelSelectScreen(); game.addChild(levelSelectScreen); levelSelectScreen.updateMapStatus(); } };
===================================================================
--- original.js
+++ change.js
@@ -546,9 +546,9 @@
}
});
return self;
});
-var Grid = Container.expand(function (gridWidth, gridHeight) {
+var Grid = Container.expand(function (gridWidth, gridHeight, mapId) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
@@ -570,10 +570,10 @@
2: Spawn
3: Goal
4: Tower Slot (impassable by ground enemies, buildable)
*/
- // Predefined map layout
- var mapLayout = [[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]];
+ // Get map layout based on mapId
+ var mapLayout = getMapLayout(mapId);
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
// Get cell type from map layout
@@ -858,8 +858,115 @@
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
+var LevelSelectScreen = Container.expand(function () {
+ var self = Container.call(this);
+ // Background overlay
+ var backgroundOverlay = self.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ scaleX: 25,
+ scaleY: 35
+ });
+ backgroundOverlay.tint = 0x222222;
+ backgroundOverlay.alpha = 0.9;
+ // Grid layout settings
+ var gridCols = 3;
+ var gridRows = 2;
+ var iconWidth = 300;
+ var iconHeight = 300;
+ var paddingX = 100;
+ var paddingY = 100;
+ // Map icon containers
+ var mapIconContainers = [];
+ // Create map icons in 2x3 grid
+ for (var mapId = 1; mapId <= totalMaps; mapId++) {
+ var col = (mapId - 1) % gridCols;
+ var row = Math.floor((mapId - 1) / gridCols);
+ // Calculate position
+ var x = col * (iconWidth + paddingX) - gridCols * (iconWidth + paddingX) / 2 + iconWidth / 2;
+ var y = row * (iconHeight + paddingY) - gridRows * (iconHeight + paddingY) / 2 + iconHeight / 2;
+ // Create icon container
+ var iconContainer = new Container();
+ iconContainer.x = x;
+ iconContainer.y = y;
+ iconContainer.mapId = mapId;
+ // Add map icon
+ var mapIcon = iconContainer.attachAsset('Map_Icon_' + mapId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ // Add map label
+ var mapLabel = new Text2('Map ' + mapId, {
+ size: 60,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ mapLabel.anchor.set(0.5, 0.5);
+ mapLabel.y = iconHeight / 2 + 40;
+ iconContainer.addChild(mapLabel);
+ // Add locked overlay
+ var lockedOverlay = iconContainer.attachAsset('blocker', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ lockedOverlay.tint = 0xcb1010;
+ lockedOverlay.alpha = 0.7;
+ lockedOverlay.width = iconWidth;
+ lockedOverlay.height = iconHeight;
+ // Add locked text
+ var lockedText = new Text2('LOCKED', {
+ size: 50,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ lockedText.anchor.set(0.5, 0.5);
+ lockedOverlay.addChild(lockedText);
+ iconContainer.lockedOverlay = lockedOverlay;
+ // Add click handler
+ iconContainer.down = function (x, y, obj) {
+ var clickedMapId = this.mapId;
+ if (unlockedMaps.indexOf(clickedMapId) !== -1) {
+ // Map is unlocked, start game
+ selectedMapId = clickedMapId;
+ startGame(selectedMapId);
+ self.destroy();
+ } else {
+ // Map is locked, show notification
+ var notification = game.addChild(new Notification("Map is locked!"));
+ notification.x = 2048 / 2;
+ notification.y = 2732 / 2;
+ }
+ };
+ mapIconContainers.push(iconContainer);
+ self.addChild(iconContainer);
+ }
+ self.updateMapStatus = function () {
+ for (var i = 0; i < mapIconContainers.length; i++) {
+ var container = mapIconContainers[i];
+ var mapId = container.mapId;
+ var isUnlocked = unlockedMaps.indexOf(mapId) !== -1;
+ // Update locked overlay visibility
+ container.lockedOverlay.visible = !isUnlocked;
+ // Update icon appearance
+ if (isUnlocked) {
+ container.children[0].alpha = 1;
+ container.children[0].tint = 0xFFFFFF;
+ } else {
+ container.children[0].alpha = 0.5;
+ container.children[0].tint = 0x888888;
+ }
+ }
+ };
+ self.update = function () {
+ self.updateMapStatus();
+ };
+ // Position the screen
+ self.x = 2048 / 2;
+ self.y = 2732 / 2;
+ return self;
+});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
@@ -2246,8 +2353,12 @@
anchorY: 0,
x: 0,
y: 170
});
+var selectedMapId = 0;
+var unlockedMaps = [1];
+var gameStartedFromMenu = false;
+var totalMaps = 6;
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
@@ -2333,27 +2444,59 @@
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
-var grid = new Grid(24, 29 + 6);
-grid.x = 150;
-grid.y = 200 - CELL_SIZE * 4;
-// Initial blockers can be placed manually on specific tower slots if desired
-// For example:
-// var blocker = new Blocker();
-// blocker.placeOnGrid(4, 8); // Place at tower slot coordinates
-// towerLayer.addChild(blocker);
-// blockers.push(blocker);
-grid.pathFind();
-grid.renderDebug();
-debugLayer.addChild(grid);
-game.addChild(debugLayer);
-game.addChild(towerLayer);
-game.addChild(enemyLayer);
-var offset = 0;
-var towerPreview = new TowerPreview();
-game.addChild(towerPreview);
-towerPreview.visible = false;
+function startGame(mapId) {
+ gameStartedFromMenu = true;
+ selectedMapId = mapId;
+ // Create new grid with selected map
+ if (grid && grid.destroy) {
+ grid.destroy();
+ }
+ grid = new Grid(24, 29 + 6, mapId);
+ grid.x = 150;
+ grid.y = 200 - CELL_SIZE * 4;
+ // Initial blockers can be placed manually on specific tower slots if desired
+ // For example:
+ // var blocker = new Blocker();
+ // blocker.placeOnGrid(4, 8); // Place at tower slot coordinates
+ // towerLayer.addChild(blocker);
+ // blockers.push(blocker);
+ grid.pathFind();
+ grid.renderDebug();
+ debugLayer.addChild(grid);
+ game.addChild(debugLayer);
+ game.addChild(towerLayer);
+ game.addChild(enemyLayer);
+ var offset = 0;
+ var towerPreview = new TowerPreview();
+ game.addChild(towerPreview);
+ towerPreview.visible = false;
+ var waveIndicator = new WaveIndicator();
+ waveIndicator.x = 2048 / 2;
+ waveIndicator.y = 2732 - 80;
+ game.addChild(waveIndicator);
+ var nextWaveButtonContainer = new Container();
+ var nextWaveButton = new NextWaveButton();
+ nextWaveButton.x = 2048 - 200;
+ nextWaveButton.y = 2732 - 100 + 20;
+ nextWaveButtonContainer.addChild(nextWaveButton);
+ game.addChild(nextWaveButtonContainer);
+ var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
+ var sourceTowers = [];
+ var towerSpacing = 300; // Increase spacing for larger towers
+ var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
+ var towerY = 2732 - CELL_SIZE * 3 - 90;
+ for (var i = 0; i < towerTypes.length; i++) {
+ var tower = new SourceTower(towerTypes[i]);
+ tower.x = startX + i * towerSpacing;
+ tower.y = towerY;
+ towerLayer.addChild(tower);
+ sourceTowers.push(tower);
+ }
+ sourceTower = null;
+ enemiesToSpawn = 10;
+}
var isDragging = false;
var draggedTower = null;
var dragStartX = 0;
var dragStartY = 0;
@@ -2399,8 +2542,22 @@
return [];
// No arrows for non-path types
}
}
+function getMapLayout(mapId) {
+ switch (mapId) {
+ case 1:
+ return [[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12], [12, 12, 12, 12, 5, 3, 3, 3, 3, 3, 3, 8, 7, 4, 4, 4, 4, 4, 4, 6, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 2, 12, 11, 11, 11, 11, 11, 12, 12, 11, 11, 11, 11, 11, 12, 2, 12, 12, 12, 12], [12, 12, 12, 12, 7, 4, 4, 4, 4, 4, 4, 6, 5, 3, 3, 3, 3, 3, 3, 8, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 11, 11, 11, 11, 11, 11, 2, 2, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]];
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ default:
+ // Placeholder layouts for other maps - return a simple path
+ return [[12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 2, 2, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12], [12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12]];
+ }
+}
function getTowerCost(towerType) {
var cost = 5;
switch (towerType) {
case 'rapid':
@@ -2423,8 +2580,79 @@
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
+function resetGame() {
+ // Clear all enemies
+ for (var i = enemies.length - 1; i >= 0; i--) {
+ var enemy = enemies[i];
+ if (enemy.isFlying && enemy.shadow) {
+ enemyLayerMiddle.removeChild(enemy.shadow);
+ enemy.shadow = null;
+ }
+ if (enemy.isFlying) {
+ enemyLayerTop.removeChild(enemy);
+ } else {
+ enemyLayerBottom.removeChild(enemy);
+ }
+ enemies[i].destroy();
+ }
+ enemies = [];
+ // Clear all bullets
+ for (var i = bullets.length - 1; i >= 0; i--) {
+ bullets[i].destroy();
+ }
+ bullets = [];
+ // Clear all towers
+ for (var i = towers.length - 1; i >= 0; i--) {
+ towers[i].destroy();
+ }
+ towers = [];
+ // Clear all coins
+ for (var i = coins.length - 1; i >= 0; i--) {
+ coins[i].destroy();
+ }
+ coins = [];
+ // Clear all upgrade menus
+ var upgradeMenus = game.children.filter(function (child) {
+ return child instanceof UpgradeMenu;
+ });
+ for (var i = 0; i < upgradeMenus.length; i++) {
+ upgradeMenus[i].destroy();
+ }
+ // Clear all notifications
+ var notifications = game.children.filter(function (child) {
+ return child instanceof Notification;
+ });
+ for (var i = 0; i < notifications.length; i++) {
+ notifications[i].destroy();
+ }
+ // Clear all range indicators
+ for (var i = game.children.length - 1; i >= 0; i--) {
+ if (game.children[i].isTowerRange) {
+ game.removeChild(game.children[i]);
+ }
+ }
+ // Reset game state
+ lives = 20;
+ gold = 80;
+ score = 0;
+ currentWave = 0;
+ waveTimer = 0;
+ waveInProgress = false;
+ waveSpawned = false;
+ selectedTower = null;
+ isDragging = false;
+ draggedTower = null;
+ // Reset grid cells
+ for (var i = 0; i < grid.cells.length; i++) {
+ for (var j = 0; j < grid.cells[i].length; j++) {
+ grid.cells[i][j].isOccupied = false;
+ grid.cells[i][j].towersInRange = [];
+ }
+ }
+ updateUI();
+}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
@@ -2627,33 +2855,16 @@
grid.renderDebug();
draggedTower = null;
}
};
-var waveIndicator = new WaveIndicator();
-waveIndicator.x = 2048 / 2;
-waveIndicator.y = 2732 - 80;
-game.addChild(waveIndicator);
-var nextWaveButtonContainer = new Container();
-var nextWaveButton = new NextWaveButton();
-nextWaveButton.x = 2048 - 200;
-nextWaveButton.y = 2732 - 100 + 20;
-nextWaveButtonContainer.addChild(nextWaveButton);
-game.addChild(nextWaveButtonContainer);
-var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
-var sourceTowers = [];
-var towerSpacing = 300; // Increase spacing for larger towers
-var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
-var towerY = 2732 - CELL_SIZE * 3 - 90;
-for (var i = 0; i < towerTypes.length; i++) {
- var tower = new SourceTower(towerTypes[i]);
- tower.x = startX + i * towerSpacing;
- tower.y = towerY;
- towerLayer.addChild(tower);
- sourceTowers.push(tower);
-}
-sourceTower = null;
-enemiesToSpawn = 10;
+// Initialize level selection screen
+var levelSelectScreen = new LevelSelectScreen();
+game.addChild(levelSelectScreen);
+levelSelectScreen.updateMapStatus();
game.update = function () {
+ if (!gameStartedFromMenu) {
+ return;
+ }
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
@@ -2786,7 +2997,24 @@
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
- LK.showYouWin();
+ // Unlock next map if not already unlocked
+ if (selectedMapId < totalMaps && unlockedMaps.indexOf(selectedMapId + 1) === -1) {
+ unlockedMaps.push(selectedMapId + 1);
+ var unlockNotification = game.addChild(new Notification("Map " + (selectedMapId + 1) + " Unlocked!"));
+ unlockNotification.x = 2048 / 2;
+ unlockNotification.y = 2732 / 2;
+ } else if (selectedMapId === totalMaps) {
+ var completeNotification = game.addChild(new Notification("All Maps Complete!"));
+ completeNotification.x = 2048 / 2;
+ completeNotification.y = 2732 / 2;
+ }
+ // Reset game and return to level selection
+ resetGame();
+ gameStartedFromMenu = false;
+ // Create new level selection screen
+ levelSelectScreen = new LevelSelectScreen();
+ game.addChild(levelSelectScreen);
+ levelSelectScreen.updateMapStatus();
}
};
\ No newline at end of file
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
White simple circular enemy seen from above, black outline. Black eyes, with a single shield in-font of it. Black and white only. Blue background.
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Top-down map asset for a TD game. Imagine a single, contiguous ribbon laid across a canyon floor; this ribbon is the path. It is a unicursal maze, meaning it's a single, non-branching route that twists through the map in a complex, grid-like pattern. The path has absolutely no junctions or alternative routes. The path starts at the top-middle edge and the very same path ends at the bottom-middle edge. The path is flanked by wide, flat, sandy plateaus that are completely barren for building. The map is framed by large, decorative sandstone formations pushed to the far edges, which do not intrude on the central play area. Semi-realistic style. --no towers, no buildings, no characters, no junctions. In-Game asset. 2d. High contrast. No shadows