CollapseLogic/CLAUDE.md

272 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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