344 lines
9.2 KiB
Markdown
344 lines
9.2 KiB
Markdown
|
|
# Collapse Logic — Level Format Specification
|
|||
|
|
|
|||
|
|
**Version:** 1.0
|
|||
|
|
**Last Updated:** March 2026
|
|||
|
|
**Studio:** Vulcara Games
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
Levels are defined as JSON files bundled in the app. Each world has a directory of level files. The format is designed to be human-readable for hand-crafting levels and machine-parseable for the game engine.
|
|||
|
|
|
|||
|
|
## File Structure
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
CollapseLogic/
|
|||
|
|
├── Levels/
|
|||
|
|
│ ├── world1/
|
|||
|
|
│ │ ├── level_01.json
|
|||
|
|
│ │ ├── level_02.json
|
|||
|
|
│ │ └── ...
|
|||
|
|
│ ├── world2/
|
|||
|
|
│ │ └── ...
|
|||
|
|
│ └── metadata.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Level JSON Schema
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "w1_01",
|
|||
|
|
"world": 1,
|
|||
|
|
"level": 1,
|
|||
|
|
"title": "First Steps",
|
|||
|
|
"grid": {
|
|||
|
|
"width": 5,
|
|||
|
|
"height": 5
|
|||
|
|
},
|
|||
|
|
"blocks": [
|
|||
|
|
{ "x": 1, "y": 1, "color": "red" },
|
|||
|
|
{ "x": 3, "y": 1, "color": "red" },
|
|||
|
|
{ "x": 2, "y": 3, "color": "blue" },
|
|||
|
|
{ "x": 4, "y": 3, "color": "blue" }
|
|||
|
|
],
|
|||
|
|
"walls": [
|
|||
|
|
{ "x": 2, "y": 2 }
|
|||
|
|
],
|
|||
|
|
"special_tiles": [],
|
|||
|
|
"objective": {
|
|||
|
|
"type": "clear_all"
|
|||
|
|
},
|
|||
|
|
"par": 4,
|
|||
|
|
"hints": [
|
|||
|
|
"Try pushing the top-left red block down first."
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Field Definitions
|
|||
|
|
|
|||
|
|
### Top-Level Fields
|
|||
|
|
|
|||
|
|
| Field | Type | Required | Description |
|
|||
|
|
|-------|------|----------|-------------|
|
|||
|
|
| `id` | string | Yes | Unique identifier. Convention: `w{world}_{level:02d}` (e.g., `w1_01`, `w3_15`) |
|
|||
|
|
| `world` | integer | Yes | World number (1-6) |
|
|||
|
|
| `level` | integer | Yes | Level number within the world (1-20) |
|
|||
|
|
| `title` | string | No | Display name for the level (optional flavor text) |
|
|||
|
|
| `grid` | object | Yes | Grid dimensions |
|
|||
|
|
| `blocks` | array | Yes | Array of block objects placed on the grid |
|
|||
|
|
| `walls` | array | No | Array of wall positions. Default: `[]` |
|
|||
|
|
| `special_tiles` | array | No | Array of special tile objects. Default: `[]` |
|
|||
|
|
| `objective` | object | Yes | Win condition for the level |
|
|||
|
|
| `par` | integer | Yes | Target move count for 3-star rating |
|
|||
|
|
| `hints` | array | No | Array of hint strings (revealed progressively). Default: `[]` |
|
|||
|
|
|
|||
|
|
### Grid Object
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"width": 5,
|
|||
|
|
"height": 5
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Field | Type | Constraints | Description |
|
|||
|
|
|-------|------|-------------|-------------|
|
|||
|
|
| `width` | integer | 3–10 | Number of columns |
|
|||
|
|
| `height` | integer | 3–10 | Number of rows |
|
|||
|
|
|
|||
|
|
Coordinate system: `(0, 0)` is the **top-left** cell. `x` increases rightward, `y` increases downward.
|
|||
|
|
|
|||
|
|
### Block Object
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "x": 2, "y": 3, "color": "red" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Field | Type | Values | Description |
|
|||
|
|
|-------|------|--------|-------------|
|
|||
|
|
| `x` | integer | 0 to `width-1` | Column position |
|
|||
|
|
| `y` | integer | 0 to `height-1` | Row position |
|
|||
|
|
| `color` | string | See Color Values | Block color |
|
|||
|
|
|
|||
|
|
### Color Values
|
|||
|
|
|
|||
|
|
**Primary colors** (can merge):
|
|||
|
|
|
|||
|
|
| Value | Display | Hex |
|
|||
|
|
|-------|---------|-----|
|
|||
|
|
| `"red"` | Ruby | #E63946 |
|
|||
|
|
| `"blue"` | Sapphire | #457B9D |
|
|||
|
|
| `"yellow"` | Topaz | #F4D35E |
|
|||
|
|
|
|||
|
|
**Secondary colors** (result of merges, cannot merge further):
|
|||
|
|
|
|||
|
|
| Value | Created From | Hex |
|
|||
|
|
|-------|-------------|-----|
|
|||
|
|
| `"purple"` | red + blue | #7B2D8B |
|
|||
|
|
| `"orange"` | red + yellow | #E76F51 |
|
|||
|
|
| `"green"` | blue + yellow | #2A9D8F |
|
|||
|
|
|
|||
|
|
Secondary-color blocks can appear in level definitions as pre-placed blocks, allowing level designers to create puzzles that start with merged colors already on the board.
|
|||
|
|
|
|||
|
|
### Wall Object
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "x": 2, "y": 2 }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Simple position. Walls are impassable — blocks stop when they would move into a wall cell.
|
|||
|
|
|
|||
|
|
### Special Tile Object
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "x": 3, "y": 4, "type": "mirror", "direction": "horizontal" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Field | Type | Values | Description |
|
|||
|
|
|-------|------|--------|-------------|
|
|||
|
|
| `x` | integer | 0 to `width-1` | Column position |
|
|||
|
|
| `y` | integer | 0 to `height-1` | Row position |
|
|||
|
|
| `type` | string | See below | Tile type |
|
|||
|
|
| Additional fields vary by type | | | |
|
|||
|
|
|
|||
|
|
**Tile types and their extra fields:**
|
|||
|
|
|
|||
|
|
#### `mirror`
|
|||
|
|
Reverses block direction on contact.
|
|||
|
|
|
|||
|
|
| Field | Values | Description |
|
|||
|
|
|-------|--------|-------------|
|
|||
|
|
| `direction` | `"horizontal"`, `"vertical"`, `"both"` | Which axis the mirror reflects |
|
|||
|
|
|
|||
|
|
- `"horizontal"` — reverses left↔right movement; vertical movement passes through
|
|||
|
|
- `"vertical"` — reverses up↔down movement; horizontal movement passes through
|
|||
|
|
- `"both"` — reverses any direction (block bounces back the way it came)
|
|||
|
|
|
|||
|
|
#### `splitter`
|
|||
|
|
Breaks a merged (secondary) block into its two primary components. The two resulting blocks are placed on either side of the splitter along the axis of movement.
|
|||
|
|
|
|||
|
|
No extra fields.
|
|||
|
|
|
|||
|
|
If a primary-color block hits a splitter, it passes through (no effect).
|
|||
|
|
|
|||
|
|
#### `void`
|
|||
|
|
Absorbs any block that enters. Single use — the void tile is consumed along with the block.
|
|||
|
|
|
|||
|
|
| Field | Values | Description |
|
|||
|
|
|-------|--------|-------------|
|
|||
|
|
| `charges` | integer (default: 1) | Number of blocks it can absorb before being consumed |
|
|||
|
|
|
|||
|
|
#### `ice`
|
|||
|
|
Block slides through without stopping. The block continues moving until it hits a non-ice cell's wall/block/boundary.
|
|||
|
|
|
|||
|
|
No extra fields.
|
|||
|
|
|
|||
|
|
#### `lock`
|
|||
|
|
A block that can only be destroyed by a matching `key` block.
|
|||
|
|
|
|||
|
|
| Field | Values | Description |
|
|||
|
|
|-------|--------|-------------|
|
|||
|
|
| `lock_color` | color string | The color of key required to destroy this lock |
|
|||
|
|
|
|||
|
|
#### `key`
|
|||
|
|
When colliding with a matching lock, both are destroyed.
|
|||
|
|
|
|||
|
|
| Field | Values | Description |
|
|||
|
|
|-------|--------|-------------|
|
|||
|
|
| `key_color` | color string | Must match a lock's `lock_color` to destroy it |
|
|||
|
|
|
|||
|
|
### Objective Object
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "type": "clear_all" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Type | Extra Fields | Description |
|
|||
|
|
|------|-------------|-------------|
|
|||
|
|
| `"clear_all"` | None | Remove all blocks from the board |
|
|||
|
|
| `"clear_color"` | `"color": "red"` | Remove all blocks of a specific color |
|
|||
|
|
| `"reduce_to"` | `"count": 1` | Reduce total blocks to the specified count |
|
|||
|
|
| `"clear_targets"` | `"targets": [{"x":2,"y":3}, ...]` | Clear specific cells (blocks must be destroyed at those positions) |
|
|||
|
|
|
|||
|
|
## Metadata File
|
|||
|
|
|
|||
|
|
`Levels/metadata.json` provides an index of all worlds and levels:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": "1.0",
|
|||
|
|
"worlds": [
|
|||
|
|
{
|
|||
|
|
"id": 1,
|
|||
|
|
"name": "Primary",
|
|||
|
|
"description": "Learn the basics of pushing and destroying.",
|
|||
|
|
"new_mechanic": null,
|
|||
|
|
"levels": [
|
|||
|
|
{
|
|||
|
|
"id": "w1_01",
|
|||
|
|
"title": "First Steps",
|
|||
|
|
"file": "world1/level_01.json",
|
|||
|
|
"is_challenge": false
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "w1_16",
|
|||
|
|
"title": "Ruby Gauntlet",
|
|||
|
|
"file": "world1/level_16.json",
|
|||
|
|
"is_challenge": true,
|
|||
|
|
"stars_required": 30
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Challenge levels have `is_challenge: true` and require a cumulative star count (`stars_required`) to unlock.
|
|||
|
|
|
|||
|
|
## Example Levels
|
|||
|
|
|
|||
|
|
### Tutorial Level (World 1, Level 1)
|
|||
|
|
|
|||
|
|
Two red blocks on a 4x4 grid. Push one into the other.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "w1_01",
|
|||
|
|
"world": 1,
|
|||
|
|
"level": 1,
|
|||
|
|
"title": "First Steps",
|
|||
|
|
"grid": { "width": 4, "height": 4 },
|
|||
|
|
"blocks": [
|
|||
|
|
{ "x": 0, "y": 1, "color": "red" },
|
|||
|
|
{ "x": 3, "y": 1, "color": "red" }
|
|||
|
|
],
|
|||
|
|
"walls": [],
|
|||
|
|
"special_tiles": [],
|
|||
|
|
"objective": { "type": "clear_all" },
|
|||
|
|
"par": 1,
|
|||
|
|
"hints": ["Push the left block to the right."]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Intermediate Level (World 1, Level 8)
|
|||
|
|
|
|||
|
|
Walls force an indirect path.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "w1_08",
|
|||
|
|
"world": 1,
|
|||
|
|
"level": 8,
|
|||
|
|
"title": "Detour",
|
|||
|
|
"grid": { "width": 5, "height": 5 },
|
|||
|
|
"blocks": [
|
|||
|
|
{ "x": 0, "y": 0, "color": "red" },
|
|||
|
|
{ "x": 4, "y": 0, "color": "red" },
|
|||
|
|
{ "x": 1, "y": 4, "color": "blue" },
|
|||
|
|
{ "x": 3, "y": 4, "color": "blue" }
|
|||
|
|
],
|
|||
|
|
"walls": [
|
|||
|
|
{ "x": 2, "y": 0 },
|
|||
|
|
{ "x": 2, "y": 1 },
|
|||
|
|
{ "x": 2, "y": 3 },
|
|||
|
|
{ "x": 2, "y": 4 }
|
|||
|
|
],
|
|||
|
|
"special_tiles": [],
|
|||
|
|
"objective": { "type": "clear_all" },
|
|||
|
|
"par": 6,
|
|||
|
|
"hints": [
|
|||
|
|
"The wall column splits the board in two.",
|
|||
|
|
"Row 2 is the only crossing point."
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Merge Level (World 2, Level 5)
|
|||
|
|
|
|||
|
|
Create purple to clear it.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "w2_05",
|
|||
|
|
"world": 2,
|
|||
|
|
"level": 5,
|
|||
|
|
"title": "Color Theory",
|
|||
|
|
"grid": { "width": 5, "height": 5 },
|
|||
|
|
"blocks": [
|
|||
|
|
{ "x": 0, "y": 2, "color": "red" },
|
|||
|
|
{ "x": 4, "y": 2, "color": "blue" },
|
|||
|
|
{ "x": 2, "y": 0, "color": "purple" }
|
|||
|
|
],
|
|||
|
|
"walls": [],
|
|||
|
|
"special_tiles": [],
|
|||
|
|
"objective": { "type": "clear_all" },
|
|||
|
|
"par": 3,
|
|||
|
|
"hints": [
|
|||
|
|
"You need to make a purple block to match the one already on the board.",
|
|||
|
|
"Merge red and blue first, then align the two purples."
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Validation Rules
|
|||
|
|
|
|||
|
|
A level file is valid if:
|
|||
|
|
|
|||
|
|
1. `id` is unique across all levels
|
|||
|
|
2. `grid.width` and `grid.height` are between 3 and 10
|
|||
|
|
3. All block, wall, and special tile positions are within grid bounds
|
|||
|
|
4. No two objects occupy the same cell
|
|||
|
|
5. `par` is a positive integer
|
|||
|
|
6. `objective.type` is one of the defined types
|
|||
|
|
7. If `objective.type` is `"clear_color"`, the specified color exists in `blocks`
|
|||
|
|
8. At least 2 blocks are present (a puzzle requires at minimum something to collide)
|
|||
|
|
9. Block colors are valid primary or secondary color strings
|
|||
|
|
|
|||
|
|
## Extending the Format
|
|||
|
|
|
|||
|
|
New special tile types can be added by defining a new `type` string and its associated extra fields. The game engine should gracefully ignore unrecognized tile types (forward compatibility for older app versions loading newer level packs).
|
|||
|
|
|
|||
|
|
New objective types follow the same pattern. The `objective` object is intentionally flexible — additional fields can be added per type without breaking existing levels.
|