Appearance
How to Build: Level Structure
Up to this point we have been building systems that generate and render terrain. But we have not answered a fundamental question: what is a "level" at runtime? When does it start? When does it end? What happens when the player dies?
The level structure system answers all of these. It defines the lifecycle of a level -- from selection through generation, play, completion, and death -- and manages the transitions between them. This is the orchestrator that ties the terrain pipeline, biome system, and (eventually) enemy spawning together into a coherent gameplay loop.
What a level is
A level is a fixed-size hex chunk grid (for example, 10 chunks wide by 10 chunks long) generated procedurally by the WFC pipeline. Each level has:
- A seed that determines its layout -- same seed always produces the same terrain
- A level number that drives difficulty scaling and tier selection
- A tier index that determines the biome and visual identity (0 = Hollows, 1 = Verdant Deep)
- A main road with a boss encounter at the end
The level number is the backbone of progression. It determines which enemies appear, how strong they are, and which depth tier applies.
The lifecycle
Select --> Generate --> Play --> Complete --> Select next
|
v
Death --> Regenerate (new seed, same level)- Selection: Player picks a level from unlocked checkpoints
- Generation: Level manager passes the seed and tier config to the WFC pipeline, which generates terrain, then chunk integration writes it to the vertex registry
- Play: Player auto-walks the road, encounters enemies (Phase 3)
- Completion: Boss dies in the final chunk, next checkpoint unlocks
- Death: Seed randomizes, WFC generates a completely fresh layout for the same level number
On death, the player keeps all experience points (XP), gold, equipment, and inventory. Only the terrain and enemies are cleared. This is a critical design rule and the level structure must enforce it.
Key traits
LevelState (world-level)
typescript
const LevelState = trait({
seed: 0, // random 31-bit integer
levelNumber: 1, // drives difficulty and tier
tierIndex: 0, // 0 = Hollows, 1 = Verdant Deep
chunksWide: 10,
chunksLong: 10,
isComplete: false,
bossDefeated: false,
})PlayerProgression (world-level)
typescript
const PlayerProgression = trait({
highestUnlockedLevel: 1,
unlockedCheckpoints: [] as number[],
})LevelRoadPath (world-level)
typescript
const LevelRoadPath = trait(() => ({
waypoints: [] as Array<{ worldX: number, worldZ: number }>,
}))The road path is extracted from the WFC zone grid after generation. It gives the auto-walk system a sequence of world-space positions to follow.
IsLevelBoss (entity tag)
typescript
const IsLevelBoss = trait()Tagged on the boss enemy entity. When the death system detects that an IsLevelBoss entity has been destroyed, it triggers level completion.
Tier configuration
The tier config maps level number ranges to gameplay parameters:
typescript
interface TierConfig {
tierIndex: number
biomeId: string
enemyTypes: string[]
clusterSizeRange: [number, number]
statScaleFactor: number
}
function getTierConfig(levelNumber: number): TierConfigFor example, levels 1-10 are the Hollows tier with rats and goblins early on, skeletons appearing at level 3+, and steadily increasing stat scaling. The enemy type availability follows a gradual introduction curve so the player is not overwhelmed.
The level manager functions
startLevel
typescript
function startLevel(world: World, levelNumber: number): voidThis is the main entry point. It:
- Generates a random seed
- Looks up the tier config for this level number
- Sets
LevelStateon the world - Calls
clearLevelto remove all existing terrain and enemy entities - Calls
loadBiomePresetwith the tier's biome ID - Triggers WFC generation and chunk integration
- Repositions the player at the first road waypoint
That last step is important: the player entity is never destroyed on level transitions. startLevel queries for the existing player, moves them to the start position, resets their health to max, clears their combat state, and re-enables auto-walk. Skills, gold, equipment, inventory -- all preserved.
typescript
function repositionPlayer(world: World): void {
const player = world.queryFirst(IsPlayer)
if (!player) return
const roadPath = world.get(LevelRoadPath)
const start = roadPath.waypoints[0]
// Move player to start, reset health, clear combat, enable auto-walk
}completeLevel
typescript
function completeLevel(world: World): voidCalled when the boss entity is destroyed. Sets isComplete and bossDefeated on LevelState, then unlocks the next checkpoint in PlayerProgression if it has not been unlocked already.
handlePlayerDeath
typescript
function handlePlayerDeath(world: World): voidCalled when the player's health reaches zero. Simply calls startLevel with the same level number -- the new random seed produces a completely different layout, giving the player a fresh run while keeping all their accumulated progress.
clearLevel
typescript
function clearLevel(world: World): voidDestroys all level-specific entities: terrain chunks, enemies, checkpoints. Clears the vertex registry and resets the road path.
The critical rule: never destroy IsPlayer entities. The player's skills, gold, equipment, and inventory must survive level transitions. This function queries specifically for ChunkCoord, IsEnemy, and IsCheckpoint entities and destroys only those.
File structure
src/level/
types.ts -- LevelConfig, TierConfig interfaces
level-manager.ts -- startLevel, completeLevel, handlePlayerDeath, clearLevel
tier-config.ts -- maps level numbers to tier configs
index.ts -- public API re-exportsNo direct beta equivalent
Beta uses a static manifest system (MapManifest + init-map-manifest.ts) to load authored levels from .bin files. The level manager replaces this with a procedural generation trigger. However, some beta patterns are still relevant:
systems/init-map-manifest.ts-- how beta initializes a level (manifest loading, view radius setup)systems/stream-chunks.ts-- how chunks are loaded and unloaded around the player
What comes next
With the level lifecycle defined, we know when terrain appears and disappears, but it still looks flat. The ribbon projection system takes our flat hex grid and wraps it around a 3D helix, giving the game its distinctive spiraling tower visual.