CollapseLogic/CLAUDE.md

273 lines
9.7 KiB
Markdown
Raw Normal View History

# 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 13: Introduce one merge pair (red+blue=purple). No pre-placed secondaries.
- Levels 46: Add yellow. Teach red+yellow=orange and blue+yellow=green.
- Levels 79: Pre-placed secondary blocks appear. Player must match them.
- Levels 1011: 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