# 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
