Here's the improved README.md file, incorporating the new content while maintaining the existing structure and coherence:
This project is designed to provide a comprehensive framework for managing and interpreting vendor level archives within the engine's architecture. The primary focus is on parsing and normalizing these archives into the engine’s LevelDefinition model, specifically within the LavaRun.Core namespace.
Scope: This document covers parsing and normalization only. Gameplay rules (hazards, scoring, completion/cooldowns) are managed by the engine (
SessionEngine).
This section describes how vendor level archives (typically *.zip) are interpreted and normalized into the engine’s LevelDefinition model.
A vendor level is usually a zip file containing:
Level.json(required)Spirits.json(optional, but commonly present)
- The engine's logical grid size is defined by
FloorSpec.Rows×FloorSpec.Cols(currently 25×12). - Vendor coordinates may not match the engine's orientation. The loader applies a transform:
- Optional swap row/column:
FloorSpec.SwapRC - Optional flip rows:
FloorSpec.FlipR - Optional flip columns:
FloorSpec.FlipC
After applying these transformations, the loader bounds-checks the transformed coordinate against the engine grid.
Lives are stored in Level.json under option:
option.lifeLimit(boolean-like: can betrue/false,0/1, or string)option.lifeLimitValue(integer-like)
Spec (source of truth):
InfiniteLives = (!lifeLimit) || (lifeLimitValue <= 0)- This treats
0or-1as infinite (vendor packs use this for “free play” / intro stages).
- This treats
- If
InfiniteLives == false, then:Lives = lifeLimitValue
Note:
GamePackLoader.LoadLevelcurrently contains a compatibility fallback whenlifeLimit=truebutlifeLimitValueis missing/invalid (falls back to 3). Aligning perfectly to the spec is tracked as a TODO in code.
Vendor levels define animation frames in Level.json.frameList.
- Each frame typically has
repeatTimes. - If a frame omits
repeatTimes, the loader usesframeDefaultRepeatTimes. - Each repeat corresponds to a base “tick” duration (the loader uses
repeatTickMsif present; otherwise, it derives fromfps).
Spec formula:
DurationMs = repeatTimes * TickMs
Where:
repeatTimes = frame.repeatTimes ?? frameDefaultRepeatTimesTickMsis the base unit (commonly ~33ms), unless overridden.
Practical consequence (why blinking levels differ): large repeatTimes values produce very slow frame changes.
Each frame can contain a matrix array. Each matrix item has the form:
[rowOffset, colOffset, spiritId, meta]
Where:
rowOffset,colOffsetare base offsets.spiritIdis a string key used to look up shapes inSpirits.json.metais an object that includes:meta.color(int)meta.points(optional array of point offsets)
Vendor meta.color → engine TileColor:
0→ Green1→ Blue2→ Red3→ Purple
A matrix entry produces one or more tiles by expanding point offsets:
- Each point is
[dr, dc] - Final raw coordinate:
r = rowOffset + dr c = colOffset + dc
Then:
- Apply transform (swap/flip) via
FloorSpec. - Bounds check against the 25×12 grid.
Multiple matrix entries may stamp the same cell. The loader deduplicates by cell and applies a priority override rule; higher priority wins:
- Yellow (runtime-only) > Green > Red > Purple > Blue
(White is not a tile color; it is a runtime overlay used by the engine.)
Spirits.json defines reusable shapes.
Spec:
- If
meta.pointsexists and is non-empty → usemeta.points - Else → use
Spirits[spiritId].points
This allows vendor packs to reference complex shapes without repeating full point lists in every frame.
Even if the engine does not use these fields today, the loader preserves them in LevelDefinition so features can be added later without changing parsing:
option.timeLimit/option.timeLimitValueoption.bgVoiceoption.startGIFoption.integralWeights
Current code captures
bgVoice,startGIF/startGif, andtimeLimitValue.integralWeightsis planned (see TODO(s) in loader comments).
Vendor packs often define an intro/free-play stage as “Level 0” and set:
option.lifeLimit = true(or sometimes false)option.lifeLimitValue = 0or-1
Per spec:
lifeLimitValue <= 0→InfiniteLives = true
Assume TickMs = 33ms.
repeatTimes = 5DurationMs = 5 * 33 = 165ms
This yields a fairly fast animation.
Assume TickMs = 33ms.
repeatTimes = 100DurationMs = 100 * 33 = 3300ms(~3.3s)
This yields a slow blink (long hold per frame), which is why some levels appear to “blink” very slowly.
This document serves as a guide for developers working with vendor level archives, providing essential details on the structure, parsing, and normalization processes. For further inquiries or contributions, please refer to the project's contribution guidelines.
This revised README maintains the original structure while enhancing clarity and coherence, ensuring that all new content is seamlessly integrated.