# CLAUDE.md — Collapse Logic iOS ## Project Overview Collapse Logic is a grid-based puzzle game for iPhone where the player pushes colored blocks to trigger same-color destruction and different-color merging. **Studio:** Vulcara Games **Platform:** iOS 17.0+ (iPhone primary) **Language:** Swift 5.9+ **Frameworks:** SpriteKit (gameplay), SwiftUI (menus/HUD) **Architecture:** MVVM with pure-struct game state --- ## Current Build Status - **World 1 — COMPLETE.** All 10 levels playable. Engine, SpriteKit scene, SwiftUI shell, undo, star rating, and persistence are all working. - **World 2 — IN PROGRESS.** Adding color merging. See `TASKS_World2.md` for the step-by-step build plan. --- ## Architecture ``` CollapseLogic/ ├── App/ │ └── CollapseLogicApp.swift ├── Models/ │ ├── GameState.swift │ ├── Block.swift # BlockColor enum lives here — add yellow + secondaries │ ├── GridPosition.swift │ ├── Direction.swift │ ├── MoveResult.swift # .merged event already stubbed — wire it up │ └── LevelDefinition.swift ├── Engine/ │ ├── GameEngine.swift # Core change: different-color collision now merges │ └── LevelLoader.swift ├── Views/ │ ├── MainMenuView.swift │ ├── LevelSelectView.swift # Needs world selector (World 1 / World 2) │ └── GameContainerView.swift ├── Scene/ │ ├── GameScene.swift │ ├── BlockNode.swift # Add color rendering for yellow + 3 secondaries │ └── GridRenderer.swift ├── Audio/ │ └── SFXManager.swift # Add merge.wav ├── Persistence/ │ └── ProgressStore.swift ├── Levels/ │ ├── metadata.json # Add World 2 entry │ └── world1/ │ └── level_01.json … level_10.json │ └── world2/ │ └── level_01.json … level_12.json └── Resources/ └── SFX/ ``` --- ## Block Colors — Full Set ### Primary colors (can be pushed into each other to merge) | Color | Hex | Name | |-------|-----|------| | `red` | #E63946 | Ruby | | `blue` | #457B9D | Sapphire | | `yellow` | #F4D35E | Topaz | ### Secondary colors (result of merges — can destroy each other but cannot merge further) | Color | Created From | Hex | Name | |-------|-------------|-----|------| | `purple` | red + blue | #7B2D8B | Amethyst | | `orange` | red + yellow | #E76F51 | Ember | | `green` | blue + yellow | #2A9D8F | Jade | Secondary blocks **can** be pre-placed in level JSON. They **can** destroy each other on same-color collision. They **cannot** merge — two different secondaries just stop (no interaction). --- ## Merge Rules (World 2 Engine Logic) When a moving block collides with a stationary block of a **different** color: ``` if both are PRIMARY colors → merge into the secondary color, placed at the stationary block's position if moving is PRIMARY, stationary is SECONDARY → block stops (no interaction) if moving is SECONDARY, stationary is PRIMARY → block stops (no interaction) if both are SECONDARY colors (different) → block stops (no interaction) if same color (primary or secondary) → both destroyed (existing World 1 rule, unchanged) ``` ### Merge color table ```swift static func mergeColor(_ a: BlockColor, _ b: BlockColor) -> BlockColor? { let pair = Set([a, b]) if pair == [.red, .blue] { return .purple } if pair == [.red, .yellow] { return .orange } if pair == [.blue, .yellow] { return .green } return nil // no merge possible } ``` When a merge occurs: 1. Both source blocks are removed from state. 2. A new block of the merged color is placed at the **stationary block's position**. 3. A `.merged` event is emitted (for animation). 4. Move count increments (merge is a valid move). 5. Win condition is checked after the merge. --- ## GameEngine Changes (World 2) The only change to `GameEngine.processMove` is step 5 of the movement algorithm: **Before (World 1):** ``` 5. Check cell adjacent to T in direction D: - Same color → destroy both - Different color → block stops ``` **After (World 2):** ``` 5. Check cell adjacent to T in direction D: - Same color (any) → destroy both [unchanged] - Both are primary, different colors → merge; new secondary block at stationary position - Any other different-color combination → block stops [unchanged] ``` No other engine logic changes. The pure-struct GameState design means this is fully unit-testable in isolation before touching SpriteKit. --- ## MoveResult Events ```swift enum GameEvent { case slid(blockIndex: Int, from: GridPosition, to: GridPosition) case destroyed(blockIndex1: Int, blockIndex2: Int, at: GridPosition) case merged(blockIndex1: Int, blockIndex2: Int, resultColor: BlockColor, at: GridPosition) } ``` The `.merged` event was already stubbed in World 1. Wire it up now. --- ## Animation — Merge In `GameScene`, handle `.merged` events: 1. Slide the moving block to position T (same 0.15s ease-out as existing slide). 2. Run a **merge burst**: scale both blocks up to 1.2× over 0.1s, then scale down and fade out over 0.1s simultaneously. 3. Spawn a **new block node** of the merged color at the stationary position, starting at scale 0.0, and animate it scaling up to 1.0 over 0.15s with a slight overshoot (spring feel — scale to 1.15 then settle to 1.0). 4. Total merge animation duration: ~0.35s. Keep it snappy. The new merged block "popping" into existence is the payoff moment. --- ## New Color Rendering (BlockNode) Add cases for `yellow`, `purple`, `orange`, `green` to the color switch in `BlockNode`. Use the hex values from the color table above. Visual treatment: - Primary blocks (red, blue, yellow): same rounded rect style as World 1. - Secondary blocks (purple, orange, green): add a subtle **white inner ring** (2px, 40% opacity) to visually distinguish them as merged/special. This gives players a quick read that these are second-tier blocks. --- ## World Select UI `LevelSelectView` currently shows World 1 levels only. For World 2, add a simple world selector at the top — two tappable pills: "World 1" and "World 2". World 2 unlocks when the player has earned at least **15 stars** in World 1 (out of 30 possible). Lock state: if World 2 is locked, show the pill grayed out with a lock icon and "15 ★ to unlock" label. Do not over-engineer this — a simple `@State var selectedWorld: Int` in `LevelSelectView` is enough. --- ## Sound Effects Add one new sound: 5. `merge.wav` — A two-tone "blip-bloom" sound (0.25s). Higher pitch than destroy. Suggests transformation rather than destruction. Use `SKAction.playSoundFileNamed()` same as the existing four sounds. --- ## Level Design — World 2 (12 levels) World 2 introduces merging progressively across 12 levels. All level JSON files are in `Levels/world2/`. Grid sizes 5×5 to 6×6. **Teaching sequence:** - Levels 1–3: Introduce one merge pair (red+blue=purple). No pre-placed secondaries. - Levels 4–6: Add yellow. Teach red+yellow=orange and blue+yellow=green. - Levels 7–9: Pre-placed secondary blocks appear. Player must match them. - Levels 10–11: Multi-step chains — merge, then use the result to destroy another secondary. - Level 12: Boss level — all three primaries, all three secondaries, careful sequencing required. See `Levels/world2/level_*.json` for the hand-crafted level files. --- ## Testing Priorities (World 2 additions) **Engine unit tests — add these:** - `red` + `blue` collision → produces `purple` block at stationary position - `red` + `yellow` collision → produces `orange` - `blue` + `yellow` collision → produces `green` - Secondary + primary collision → block stops (no merge) - Secondary + different secondary collision → block stops (no merge) - Same secondary collision → both destroyed (e.g. purple + purple) - Merged block is placed at correct position (stationary block's cell) - Move count increments on merge - Undo correctly restores state before a merge - Win condition detected after a merge clears the last blocks **Level loading tests:** - All 12 World 2 JSON files parse without errors - Pre-placed secondary color blocks load correctly **Manual playtesting:** - Each World 2 level is solvable at par - Merge animation feels satisfying, not jarring - World 2 unlock (15 stars) triggers correctly --- ## Style & Conventions - Swift naming conventions (camelCase properties, PascalCase types) - Prefer value types (structs, enums) over classes except for SpriteKit nodes - No force unwraps except in tests - Keep files under 200 lines. Split if needed. - Comment non-obvious game logic - `// MARK: -` sections in longer files --- ## Build & Run - Xcode 15+ - iOS 17.0 deployment target - No external dependencies - Primary simulator: iPhone 15 Pro - Small-screen test: iPhone SE (3rd gen) --- ## Definition of Done — World 2 - [ ] `mergeColor(_:_:)` function implemented and all merge combinations correct - [ ] `GameEngine.processMove` handles primary+primary different-color collision as merge - [ ] `.merged` GameEvent emitted with correct blockIndex values and resultColor - [ ] All existing World 1 unit tests still pass (no regressions) - [ ] New merge unit tests pass (see Testing Priorities above) - [ ] `yellow`, `purple`, `orange`, `green` render correctly in `BlockNode` - [ ] Secondary blocks have white inner ring visual treatment - [ ] Merge animation plays: slide → burst → new block pop-in (~0.35s total) - [ ] `merge.wav` plays on merge - [ ] All 12 World 2 level JSONs load and are playable - [ ] World selector UI in `LevelSelectView` (two pills, lock at <15 stars) - [ ] `metadata.json` updated with World 2 entry - [ ] World 2 levels are fun and teach the mechanic clearly