Compare commits

..
15 Commits
Author SHA1 Message Date
Lena 002d1e83bf fairy stuff
fairy quest and attack mode
2026-10-05 18:19:49 +02:00
Lena d56540785f flyer boss, town
flyer boss adjustments, town addition
2026-10-05 15:04:20 +02:00
Lena 73cfd870f6 flying boss
boss addtion, small editor changes
2026-10-02 10:56:51 +02:00
Lena c7558a3947 attack changes
dissolve platforms, mouse for attack, directional attack (i think), hold for attack
2026-10-02 08:12:53 +02:00
Lena bf4e8de590 mist/attack
mist, charge empty lock, attack changes, floating platform changes
2026-10-01 10:49:35 +02:00
Lena ab35845572 requirements
requirements.txt added
2026-09-30 12:19:29 +02:00
Lena e42f8ceba1 AGENTS.md
datei angelegt, um in zukunft schneller und günstiger anfangen zu können
2026-09-30 12:05:34 +02:00
Lena faa8b16658 more rooms
more rooms, level editor changes, more logic for ability checks and trigger doors, platform body collision fixes
2026-09-30 12:00:56 +02:00
Lena 53fc17338f room 3 changes
chest, tiny jump and run with moving platforms
2026-09-29 19:48:49 +02:00
Lena a9e999b49e room overhaul
more editor changes, new rooms
2026-09-29 18:55:29 +02:00
Lena f676e59008 level editor overhaul
more objects, NPCs, better toolbar and snapping
2026-09-29 12:13:56 +02:00
Lena db8aa53d3f smol stuff again
added fairy sprite, unequip inventory, proper death recovery
2026-09-29 09:41:05 +02:00
Lena 2a86881108 death + charge regen
death penalty and corpse, lantern to butterfly, butterfly jar, added the new stuff to level editor
2026-09-28 21:01:30 +02:00
Lena 17d4e9cdf3 full rename
rename of folder, subfolders and main.py
2026-09-28 09:47:18 +02:00
Lena ac40f2176b backup
main menu, roadmap, save file system
2026-09-28 09:23:55 +02:00
83 changed files with 8189 additions and 2246 deletions
+3 -1
View File
@@ -1 +1,3 @@
.vs
.vs
light_and_dark/light_and_dark/saves/
**/__pycache__/
+42
View File
@@ -0,0 +1,42 @@
# AGENTS.md
## Project
"Light & Dark" — a 2D light/shade platformer prototype (Python 3.10 + pygame).
- Main game: `light_and_dark/main.py` (single large file, ~9500 lines, no tests)
- Level editor: `light_and_dark/level_editor_v43.html` (standalone HTML/JS, no build step)
- Levels: `light_and_dark/data/world/rooms/room_0N.json`
- Data/config: `light_and_dark/data/config/*.json`, `data/items/*.json`, `data/objects/*.json`, `data/enemies/*.json`
## Standard session start
1. `git status` + `git log --oneline -5` to see uncommitted/untracked work (currently `room_08`/`room_09`/`room_boss_fly_refight` and the `Boss_fly_token.png` sprite are untracked/WIP).
2. Read the specific file(s) the request touches before editing; follow existing style.
3. After edits, always verify (see below).
## Verification
- `python -m py_compile light_and_dark/main.py` (must pass after any edit)
- Validate touched JSON: `python -c "import json; json.load(open('<path>', encoding='utf-8'))"`
- To confirm the game boots: run `python light_and_dark/main.py` with a short timeout; no traceback in the first lines = OK (the window blocks, so it will time out).
## Pitfalls / conventions
- **Python booleans are `True`/`False`** — never `true`/`false` in main.py (this once crashed startup). JSON files use lowercase `true`.
- Don't add code comments unless asked. Match surrounding style.
- Rooms load through `normalize_room_json` + `Room` (main.py) which rewrite JSON into the game format; keep new room JSON keys mirrored there (e.g. trigger/ability-door flags).
## Key systems (in main.py unless noted)
- **Saves**: lantern-only. `save_game` / `apply_save_data`. Quest progress, broken doors, defeated bosses persist ONLY on lantern interaction.
- **Trigger walls** (`trigger_wall_blocking`): meta keys `quest`, `quest_complete`, `quest_turned_in`, `lock_when_pass` (appears once walked through), `open_on_miniboss_dead`, `open_on_boss_dead` (released once the room's boss is slain; also hides/inerts moving platforms while the boss lives).
- **Exits**: `"requires_boss_dead": true` hides the exit and disables it while the room's boss is alive (used for boss doors and refight rooms). The editor round-trips this flag (checkbox in the Exit properties).
- **Bosses** (`BOSS_DEFS_DEFAULT`): `boss_id -> {name, room, refight_room, token_name, token_sprite}`. `boss_01` = Gloom Warden (legacy `room_boss_01`, not in the current build); `boss_02` = **Sky Terror** (flying, `room_08`, refight `room_boss_fly_refight`). A boss drops its token in its world room (not during a refight); tokens feed the Boss Diary (`boss_diary`) and the Combat Master upgrades them for the Hard fight. Token art is per-boss via `boss_token_sprite(boss_id)`.
- **Boss aggro**: once the player first enters a boss's `detection_range` in Light form, `Enemy.boss_engaged` latches and the boss chases forever (ignores range and Shade).
- **Flying boss** (`behavior: "flyer"`, config `enemy_types.json` → `boss_flyer.flyer`): hovers/bobs, slam-dives (shockwave `poison=False`), summons expiring minions, AOE rings, homing shots. Casts `_flyer_max_casts()` skills per slam-cycle — normal 1 (2 at `phase2_hp_pct`), hard 2 (3 at `hard_phase3_hp_pct`); `self.flyer_hard` is set by `start_refight(..., "hard")`.
- **Ability doors** (`AbilityDoor`): `kind` = `"ranged"` (shot open, persists in `broken_doors`) or `"shade"` (solid in light, passable in shade + a `shade_door_grace` window after form switch; ejected to nearest edge after grace).
- **Quests** (`QUEST_DEFS_DEFAULT`): objective `"shade": True` counts kills only in shade form. Rewards via `guaranteed_item` (one-time) or `pool` (repeatable). Items by `item_id` (e.g. `necklace_placeholder` = "Plain Necklace").
- **Minibosses**: stay defeated per room (`DEFEATED_MINIBOSSES`) until a lantern save in a different room, then respawn.
- **Enemies**: insta-die on hazard contact with no drops (`hazard_kill` flag); normal enemies respawn on room re-entry.
- **Chests** (`RewardChest`): spawn coin `CoinPickup`s above the chest top (not center, which clips into the platform).
- **Shade-kills quest** (`first_kills_shade` in room 6): interactable/prompted only while in shade form (`QuestManager.requires_shade`).
## Editor (level_editor_v43.html)
- Link lines: `drawLinkLinesForCanvas` — exits with a `spawn_id` missing in the target room draw NO line (avoids stale-coordinate artifacts).
- Keyboard shortcuts live in the `window keydown` handler: V/P/E/N/O switch tools, F fit, Esc deselect, Ctrl+Z/Y/C/X/V/D.
+5
View File
@@ -0,0 +1,5 @@
<Solution>
<Project Path="light_and_dark/light_and_dark.pyproj" Type="888888a0-9f3d-457c-b088-3a5042f75d52" Id="e4b51f4c-e0be-4f9e-8a8e-d814a78b30c5">
<Build Project="false" />
</Project>
</Solution>

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 702 B

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 695 B

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 879 B

+183
View File
@@ -0,0 +1,183 @@
# Light & Dark — Prototype
A 2D platformer built with **Pygame**. You switch between two forms — **Light** and **Shade** — each with different movement, combat, and ability behavior, all driven by a single shared resource: **Charge (Flow)**.
> Note: this is a prototype. Some progress (quests, boss tokens, broken walls, defeated bosses) is **in-memory** and resets when you restart — intentional, for fast testing. Saves (via pause menu or lanterns) persist player state, coins, equipment, inventory, and your respawn point.
---
## Running the game
```
python main.py
```
Requires Python 3.10+ and `pygame`. Game data (rooms, items, sets, enemies, drops) lives in `data/`. Rooms are edited with the included browser tool: **`level_editor_v43.html`** — see [Level Editor](#level-editor) below.
---
## Controls
| Action | Key |
|---|---|
| Move | A / D |
| Jump | W |
| Move down / drop | S |
| Heal | Space (hold) |
| Melee attack | E |
| Ranged shot | Q |
| Dash | Tab |
| Shatterstep | R |
| Tether | X |
| Reality Break | C |
| Fairy Mode (once unlocked) | Z |
| Toggle Light/Shade | Left Shift |
| Interact / Talk / Save | F |
| Use Butterfly Jar | G |
| Inventory | I |
| Stats overlay | H |
| Pause / Settings | Esc |
**Mouse**: **Left button** = melee attack, **Right button** = ranged shot.
**Hold-to-recast**: Melee (E / left mouse) and Ranged (Q / right mouse) can be **held** — the attack auto-recasts as soon as its cooldown is ready, and never faster than the cooldown.
---
## The two forms
**Light** — faster movement, higher jump, and charge **refills** over time. You take contact damage from enemies.
**Shade** — slower but tanky: **immune to enemy contact damage**, deals double melee damage, and can use shade-specific gear bonuses. But being in Shade **drains Charge** (and Shade is forced off when Charge hits 0). Shade also drains Charge **twice as fast while clipping inside an enemy**.
**Charge (Flow)** — base max 100, refills at **10/s** in Light (halved to make light sources matter), drains at **40/s** in Shade. Standing near a **butterfly** light source doubles Charge regeneration. Attacks, dash (Light), ranged shots, and healing all cost Charge.
---
## Saving & lanterns
**Lanterns are save points.** Walk up to a lantern and press **F** to save — a "Press F - Save at lantern" prompt appears when you're in range. Saving **fully restores your health and Charge** (and clears poison), and sets your respawn point to that lantern, so dying (or reloading the save) puts you back at your latest lantern. Saving via a lantern in a *different room* than where you last died also respawns butterfly jars in the world.
You can also save anytime from the pause menu (**Esc → Save Game**).
---
## Butterfly jars
Butterfly-in-a-jar pickups are scattered through the rooms (you can hold up to **3**). Press **G** to consume one and instantly refill your Charge bar to max. Collected jars are shown below your coin counters (empty slots are greyed out).
Jars are lost when you die. They respawn at their pickup points after you save at a lantern in a different room, or after reloading the game.
---
## Death & recovery
When you die you lose your **butterfly jars for good** and drop your coins. A **faded player model (corpse)** marks the spot — touch it to reclaim all lost coins. Dying again *without* reclaiming keeps only **50%** of the coins in a new corpse at the new death spot (the old corpse vanishes), so the rest is lost. Falling out of the world (void deaths) leaves no recoverable corpse.
---
## Abilities (by form)
| Ability | Key | Light | Shade |
|---|---|---|---|
| **Melee** | E | 1 damage, costs 1 Charge | **2 damage**, costs 1 Charge |
| **Dash** | Tab | Costs 10 Charge; **stuns** enemies hit | Costs 1 HP; deals **4 damage** to enemies hit |
| **Ranged** | Q | Costs 5 Charge, 0.63s cooldown, 0.5 damage; **breaks green ability walls** | Same, but 1.0 damage |
| **Shatterstep** | R | Spawns a **platform under your feet** while airborne (extra jumps) | On landing, creates a damaging **shockwave AOE** below you |
| **Tether** | X | **Binds** the enemy (3s) and **fears** it (runs away) | **Insta-kills** enemies below a HP threshold (normal ⅓, miniboss 20%, boss 5%); otherwise **slows** them |
| **Reality Break** | C | Doubles drop & coin rates and glows enemies | **Instantly kills all enemies** but halves drop & coin rates |
| **Heal** | Space | Spend Charge to heal over ~2s (slower while healing) | Unavailable |
---
## Combat notes
- Enemy contact damage only applies in **Light** form.
- The **Gloom Warden's** ground shockwaves **poison** you for 4 seconds (the health bar turns green, −1 HP/s). **Sky Terror's** slam does **not** poison.
- Once a boss first spots you in Light form it tracks you permanently — even in Shade form and from any distance.
- Attacks and hits use invincibility frames and knockback; your melee hitbox covers ~30% into your body so close/overlapping hits always connect.
---
## Equipment & Armor Sets
Slots: **helmet, chest, pants, necklace**. Pieces roll stats (move speed, heal, charge, health, etc.).
**Rarities:** `tempered` (tier 1) → `attuned` (tier 2) → `awakened` (tier 3, unlocks ability slots). The **Upgrader** NPC turns 5 tempered copies of an item into 1 attuned copy.
**Sets** (3 pieces of the same set, attuned or better):
| Set | Summary |
|---|---|
| Dawnrunner | +HP; awakened adds Light move speed |
| Gravemark | +max Charge; awakened adds Charge regen |
| Vitalweave | +HP; awakened adds +heal |
| Duskwarrior | +Shade damage; awakened restores Charge on Shade hits |
| Twister | Removes Shade speed penalty; awakened gives move speed after switching forms |
| Paladin | Chance to negate incoming damage; awakened adds +HP |
| Luminal Growth | +Charge regen while standing still in Light; awakened also negates damage |
| Abyss Cleaver | Chance to heal on kill; awakened also gives move speed on kill |
| Umbraflow | Reduces Shade Charge drain |
| Adaptive Traveller | Move speed after switching forms; awakened restores Charge on switch |
The shade-focused sets (**Duskwarrior, Umbraflow, Abyss Cleaver, Adaptive Traveller**) are intentionally **excluded** from early boss and town-shop loot, so you can always complete the remaining sets.
---
## NPCs
> The town room isn't in the current build (removed in the room overhaul); these NPC types still exist and can be placed with the editor.
- **Vendor** — sells basic armor pieces.
- **Necklace Vendor** — sells necklaces.
- **Upgrader** — attunes 5× tempered → attuned.
- **Combat Master** — upgrades your boss token to unlock the **Hard** boss variant.
- **Boss Diary** — an interactable book: view your collected boss tokens in a grid, slot/unslot them, and start refights.
---
## Rooms & Progression
> The room set was overhauled; the old reward/town rooms are gone. Notable rooms:
- `room_01` — tutorial. Contains a **quest object** (pink): accept the **"First Kills"** quest (kill 4 normal enemies → 5–15 light coins, repeatable).
- `room_02` — first enemies.
- `room_04` — **miniboss** arena; the walls seal until the miniboss is slain.
- `room_05` — **charger boss** arena (seals until slain).
- `room_07` — hub, branching to the boss arena and back.
- `room_08` — **Sky Terror**, the flying boss. Entering locks you in: the exit back to `room_07` and the right-hand door to `room_09` only appear once he's dead.
- `room_09` — the area past Sky Terror's right door.
- `room_boss_fly_refight` — Sky Terror **diary refight** arena.
---
## Boss Diary & Tokens
1. Beat a world boss → collect its **token** (a distinct pickup per boss).
2. **Slot** it in the Boss Diary (or keep it in your inventory).
3. Slotting unlocks **Fight (Normal)**. Upgrading the token at the **Combat Master** unlocks **Fight (Hard)**.
4. Dying in a refight ends it; removing a token from the diary sends it back to your inventory.
Bosses: **Gloom Warden** (`boss_01`, legacy) and **Sky Terror** (`boss_02`) — Sky Terror lives in `room_08` (51 HP) and refights in `room_boss_fly_refight`; on Hard he opens with **two** skills per attack cycle and escalates to **all three** below 20% HP.
---
## HUD & Stats
- Top-right: health bar (turns **green while poisoned**), Charge bar, coin counts.
- Butterfly-jar slots below the coin counters (shown once you own at least one; remaining slots are greyed out).
- Ability icons with cooldown indicators (top-left).
- **H** toggles a detailed overlay: exact HP/Charge, move speeds (both forms), heal duration, attack rate, Charge regen, the lantern/light buff, and any stat bonuses from gear.
---
## Level Editor
Open `level_editor_v43.html` in a browser to edit rooms. Highlights:
- **Icon toolbar** on the right edge of the canvas (Select, Platform, Enemy, NPC, Player, Entry, Exit, Ability Door, Object). Toggle **grid snap** in the left sidebar.
- **Objects catalog** (built-in, always available; `data/objects/objects.json` can override/add): `flow_butterfly` (light source), `lamp` (save point), `flow_butterfly_jar` (collectible), `house`, `diary`, and `player` (a size reference that renders nothing in-game).
- **NPC support**: place/select/move NPCs (vendor, upgrader, necklace_vendor, combat_master) and standalone **house props**.
- **Layers**: every object (platforms, houses, NPCs, enemies, props, ability doors, exits) has a **layer** number. Lower = further back, higher = further forward; lower is set in the Properties panel. The editor and game both draw back-to-front by `(layer, category)`, and the select tool is layer-aware.
- **Link lines**: enable **Draw link lines** (with Compare view) to see dashed lines from a room's exits to the matching doors in the other room.
- Load the game's enemy types (`data/enemies/enemy_types.json`) to preview hitboxes and aggro ranges; enemy/miniboss/boss are also built in.
+87
View File
@@ -0,0 +1,87 @@
# Light & Dark — Roadmap
Destination: **polished vertical slice** — Regions 1–2, save system, skill upgrades.
Proves the core loop is fun before scaling to the full vision.
## Design decisions (locked)
- **Death penalty**: on death, coins drop into a **corpse** (faded player model) at the death spot; touch it to reclaim them. Dying again without reclaiming keeps only 50% (a new corpse appears, the old one vanishes). Butterfly jars are lost for good on death. Void deaths (falling out of the world) leave no corpse.
- **Skill upgrades**:
- **Vendor** (smith/trainer) gives direct linear power upgrades. Upgrades one selected spell; changing selection costs coins.
- **Gems / rings** give alternate behaviors / sidegrades.
- **Ranged identity**: keep the bolt, add upgrades (no charge-up / homing rework).
- **Bosses**: after the player first enters a boss's detection range (Light form), it tracks them permanently — ignoring range and Shade. The flying boss (**Sky Terror**, `boss_02`) casts a limited number of skills per slam-cycle: normal 1 (2 at ≤33% HP), Hard 2 (all 3 at ≤20% HP). Bosses drop a per-boss token that feeds the Boss Diary.
- **Fairy (in slice)**: guide + ambient light source (bonus flow regen near player). Scout mode, teleport, fairy holes deferred to regions 3+.
- **Ascended armor**: cut for now. Awakened is good enough at the start; less balancing needed.
## Milestones
### M0 — Foundation (the game remembers)
| Item | Effort | Notes |
|---|---|---|
| ~~Save/persistence system~~ | L | **DONE (2026-09-28)**: JSON save to `saves/`, save via pause menu. Stores player state, room, coins, equipment, inventory, quests, tokens, boss diary, defeated bosses, broken doors. |
| Charge -> Flow rename | S | Code + UI + configs. |
| ~~Main menu~~ | M | **DONE (2026-09-28)**: Select Game (greyed with no saves), New Game (name entry), Settings (rebinding), Quit. Wipe-all-saves button (bottom right) with confirmation dialog. |
| ~~Lantern save points~~ | M | **DONE (2026-09-29)**: Lanterns are explicit save points — walk up and press F to save; you respawn at your last lantern. Visible radius removed. |
### M1 — Region 1 complete loop
| Item | Effort | Notes |
|---|---|---|
| ~~Death penalty (drop coins, reclaim at death spot; fairy marks it; lose on 2nd death)~~ | M | **DONE (2026-09-29)**: corpse at death spot holds 100% of coins; a repeat death before reclaiming keeps only 50% and the old corpse vanishes. Butterfly jars are lost for good on death. |
| ~~Fairy companion (guide + ambient light source -> bonus flow regen)~~ | M | **DONE (2026-09-29)**: fairy follows behind the player's head; flips with facing. |
| ~~Flying boss + boss tokens~~ | M | **DONE (2026-10-05)**: **Sky Terror** (`boss_02`) in `room_08` — 51 HP, poison-free slam, skill-cycle phases (2 at ≤33%). Drops a per-boss token feeding the Boss Diary; Hard refight (`room_boss_fly_refight`) starts at 2 skills/cycle and hits all 3 at ≤20% HP. |
| More quests (repeatable + one-time; system exists) | S | Just content. |
| Lore dialogue for fairy / NPCs | S | Needs written copy. |
### M2 — Skill upgrades
| Item | Effort | Notes |
|---|---|---|
| Vendor (smith/trainer) NPC | M | Reuses NPC + coin systems. |
| Vendor upgrades (linear): Dash = lower cooldown, Ranged = more damage, Tether = longer bind/fear | S | Data + UI. |
| Ring slot + gems (sidegrades): Dash = speed-stacks / refund-on-kill / afterimage taunt; Ranged = pierce (light) / aoe (shade) / kill-stacks; Tether = boss effects / higher threshold | L | New slot, new item type, build UI. |
### M3 — Region 2: shade matters
| Item | Effort | Notes |
|---|---|---|
| Shade-gated interactions (doors, an NPC, an area) | S-M | Room JSON + existing toggle. |
| New enemy behaviors (patrol, dodge-and-leap, burrow-shooter) | M | Enemy system supports it. |
| Enemy line-of-sight detection (radius -> raycast) | S-M | Replace/augment the euclidean aggro in `Enemy.update` with a `Rect.clipline` raycast against solid platforms so walls block vision. Per-type `"detection": radius/los` config in `enemy_types.json` + editor toggle. Edge cases: skip the enemy's own support platform, sample player center+feet for tolerance, apply LOS to boss slam `trigger_close/far`. |
| ~~Quick recharge pickup~~ | S | **DONE (2026-09-29)**: butterfly jars — collect up to 3, press G to instantly refill Charge. Lost on death; respawn at their points after saving at a lantern in a different room or reloading. |
| Ability tweaks: ranged upgrades, ground-slam knockback/iframe, light-step buffs | M | Picks up remaining notes ideas. |
| Region-2 boss + ability pacing | M | Re-align pacing (Ranged already exists). |
### Stretch / core (post-slice, in order)
- Map system (current-room minimap -> world graph -> lantern fast travel) — see below
- Fast travel (rune stones)
- Fairy scout + bond teleport + fairy holes
- Regions 3-5 per pacing notes
- World-change weapon (stretch-end, decide variant)
### Cut
- Ascended armor (removed).
## Map system (deferred — brainstorm, not started)
Feels huge; split into two independent features rather than one.
1. **Current-room minimap** — cheap and self-contained: scale `room_bounds` + platform rects + a player arrow into a corner/overlay panel. No new data, no persistence. Good first slice.
2. **World map / region graph** — nodes = rooms, edges = `exits[]` (derivable automatically from room JSON rather than hand-authored).
Design notes
- **Layout**: add `meta.map_x` / `meta.map_y` (+ optional `meta.region`) per room and place nodes in the editor; fall back to auto-layout from exit topology if absent.
- **Discovery**: persist `visited_rooms` and `discovered_links` alongside the existing lantern-only save data. Undiscovered = "?" / fog.
- **Theme hook**: map clear in Light, fogged/faded in Shade, or fog dispelled by lanterns — reinforces the light/shade identity (keep it cosmetic).
- **Fairy as cartographer**: she fills in rooms and comments on new ones; reuses her room-aware voice lines and mirrors the Boss Diary UI pattern.
- **Fast travel**: discovered lanterns become warp nodes; overlaps with the deferred "rune stones" / "fairy bond teleport" items.
- **Edge cases**: one-way exits, boss-refight rooms, the missing `room_town_gloomreach.json`, exits with empty `to` (ignore), multiple entry spawns = one node.
Phasing
1. Current-room minimap overlay.
2. `visited_rooms` + world map from auto-derived exit links (auto-positioned).
3. Manual `map_x/map_y` layout in the editor.
4. Lantern fast travel + fairy-bond teleport.
## Dependencies
- Save is the keystone: death penalty, main menu, and any progress feature hang off it.
- Fairy and Flow rename can run in parallel with M0.
@@ -22,7 +22,9 @@
"miniboss": [170, 40, 210],
"miniboss_hit": [220, 140, 255],
"boss": [240, 220, 80],
"boss_hit": [255, 245, 160]
"boss_hit": [255, 245, 160],
"boss_flyer": [150, 90, 220],
"boss_flyer_hit": [210, 170, 255]
},
"healing": {
+89
View File
@@ -0,0 +1,89 @@
{
"talk_range": 80,
"bubble_duration": 3.5,
"idle_interval": 45.0,
"idle_lines": ["idle_1", "idle_2", "idle_3"],
"dust_lines": ["dust_1", "dust_2", "dust_3"],
"attack_lines": ["attack_1", "attack_2", "attack_3"],
"auto": {
"room_01.json": { "on_enter": "welcome" }
},
"lines": {
"welcome": {
"light": "Wake up, sleepyhead! We have a long way to go.",
"shade": "Ooh, you found your dark side already?"
},
"idle_1": {
"light": "The butterflies always know the way.",
"shade": "Even in the dark, I can still see you."
},
"idle_2": {
"light": "Rest at a lantern and it will remember this place.",
"shade": "Careful - the gloom hides sharp things."
},
"idle_3": {
"light": "I will stay close. I always do.",
"shade": "You are not alone down here. I am right here."
},
"spot_greeting": {
"light": "Isn't this place pretty?",
"shade": "I can barely make you out in here."
},
"dust_1": {
"light": "Fairy dust! I can feel its sparkle.",
"shade": "Ooh, shiny. Even the dark likes this."
},
"dust_2": {
"light": "Another pinch of dust - keep going!",
"shade": "The dark tastes sweeter with this."
},
"dust_3": {
"light": "This dust hums when I hold it.",
"shade": "Keep collecting; I want all of it."
},
"quest_intro": {
"light": "I scattered my fairy dust across these rooms. Bring me 5 and I will fight beside you!",
"shade": "Find my 5 fairy dust. I will lend you my sting."
},
"quest_reward": {
"light": "You found them all! Now I can fight - press the Fairy Mode key!",
"shade": "All 5! My sting is yours. Press the Fairy Mode key."
},
"quest_return": {
"light": "That's all 5! Return to me at the fairy spot - I have something to show you!",
"shade": "All 5 gathered. Come back to my spot - I am ready to change."
},
"quest_unlock": {
"light": "Watch this - hold still!",
"shade": "Observe. I am about to change."
},
"quest_done": {
"light": "I am ready whenever you are!",
"shade": "Say the word and I strike."
},
"combat_on": {
"light": "To battle! I will pick off the stragglers.",
"shade": "Time to hunt. I like this form of yours."
},
"combat_off": {
"light": "Resting my wings now.",
"shade": "Fine, back to lurking."
},
"combat_empty": {
"light": "I am out of charge - I need to rest!",
"shade": "Empty. Let me recharge."
},
"attack_1": {
"light": "Huah!",
"shade": "Huah!"
},
"attack_2": {
"light": "Take this!",
"shade": "Take this!"
},
"attack_3": {
"light": "Hyah!",
"shade": "Hyah!"
}
}
}
@@ -6,7 +6,7 @@
"fps": 60
},
"colors": {
"bg": [30, 30, 30],
"bg": [58, 58, 64],
"player_light": [200, 200, 200],
"player_dark": [60, 60, 60],
"platform": [80, 80, 80],
@@ -5,13 +5,16 @@
"jump_light": -12.0,
"jump_shade": -9.0,
"gravity_light": 0.5,
"gravity_shade": 0.4
"gravity_shade": 0.4,
"jump_release_gravity_mult": 3.0,
"shade_door_grace": 0.6
},
"resources": {
"base_max_charge": 100.0,
"charge_drain_rate": 40.0,
"charge_refill_rate": 20.0,
"charge_refill_rate": 10.0,
"charge_empty_lock": 1.5,
"base_max_health": 10,
"invincibility_duration": 1.0
},
+21
View File
@@ -0,0 +1,21 @@
{
"charge_cost": 5.0,
"damage_mult": 0.5,
"cooldown": 0.63,
"projectile": {
"speed": 650.0,
"lifetime": 1.2,
"size": [14, 6]
},
"fairy": {
"charge_cost": 5.0,
"cooldown": 0.7,
"range": 560.0,
"voice_every": 3,
"damage_mult": 0.5,
"speed": 520.0,
"lifetime": 1.4,
"size": [16, 16],
"spread_deg": 6.0
}
}
@@ -1,5 +1,5 @@
{
"room_id": "room_05.json",
"room_id": "room_reward_01.json",
"chest_pos": [520, 330],
"ability_pos": [820, 210],
"chest_coins": [30, 30]
@@ -7,6 +7,11 @@
"group_spacing": 90
},
"butterfly_jars": {
"icon_size": 16,
"icon_gap": 4
},
"inventory_ui": {
"panel": { "x": 20, "y": 20, "w": 560, "h": 545, "pad": 10 },
"list": { "w": 360, "line_h": 18, "header_h": 40, "pad_y": 6, "pad_right": 14 },
@@ -0,0 +1,93 @@
{
"enemy": {
"rect": [40, 60],
"health": 3,
"speed": 2.2,
"air_speed": 2.0,
"detection_range": 240,
"contact_damage": 1,
"drop_profile": "normal"
},
"miniboss": {
"rect": [70, 95],
"health": 18,
"speed": 1.8,
"air_speed": 1.6,
"detection_range": 360,
"contact_damage": 2,
"drop_profile": "miniboss"
},
"boss": {
"rect": [110, 120],
"health": 10,
"speed": 1.6,
"air_speed": 1.4,
"detection_range": 500,
"contact_damage": 3,
"drop_profile": "boss",
"behavior": "shockwave",
"charger": {
"retreat_dist": 150,
"retreat_jump_vel": -14.0,
"retreat_speed_mult": 1.2,
"charge_speed_mult": 4.0,
"charge_duration": 1.1,
"dash_min_dist": 160,
"dash_max_dist": 420,
"dash_speed_mult": 4.2,
"dash_duration": 0.7,
"attack_cooldown": 1.8,
"above_y": 40
}
},
"boss_flyer": {
"rect": [120, 90],
"health": 51,
"speed": 3.0,
"air_speed": 3.0,
"detection_range": 620,
"contact_damage": 3,
"drop_profile": "boss",
"behavior": "flyer",
"flyer": {
"flight_height": 280,
"bob_amplitude": 20,
"bob_speed": 2.2,
"hover_speed": 2.6,
"startup_delay": 3.0,
"phase2_hp_pct": 0.33,
"hard_phase3_hp_pct": 0.20,
"slam_cd": 9.0,
"slam_dive_vel": 16.0,
"ground_time": 2.0,
"summon_cd": 8.0,
"summon_max": 3,
"summon_type": "boss_minion",
"summon_life": 6.0,
"summon_heal": 4,
"summon_damage": 3,
"aoe_cd": 9.0,
"aoe_radius": 300,
"aoe_damage": 1,
"aoe_duration": 0.9,
"shot_cd": 5.0,
"shot_damage": 5,
"shot_speed": 300,
"shot_lifetime": 2.2,
"shot_homing": 1.8
}
},
"boss_minion": {
"rect": [34, 50],
"health": 4,
"speed": 2.4,
"air_speed": 2.2,
"detection_range": 320,
"contact_damage": 1,
"drop_profile": "normal"
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"objects": {
"flow_butterfly": {
"type": "light_source",
"charge_regen_range": 180
},
"lamp": {
"type": "save_point",
"charge_regen_range": 90
},
"flow_butterfly_jar": {
"type": "butterfly_jar"
},
"house": {
"type": "house"
},
"diary": {
"type": "diary"
},
"player": {
"type": "player"
},
"quest_point": {
"type": "quest_giver"
},
"fairy_spot": {
"type": "fairy_spot"
},
"fairy_dust": {
"type": "fairy_dust"
}
}
}

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 319 B

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 313 B

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 614 B

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 306 B

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

Before

Width:  |  Height:  |  Size: 219 B

After

Width:  |  Height:  |  Size: 219 B

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 215 B

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 150 B

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 135 B

Before

Width:  |  Height:  |  Size: 136 B

After

Width:  |  Height:  |  Size: 136 B

@@ -0,0 +1,228 @@
{
"name": "room_01",
"display_name": "room_01",
"meta": {
"id": "room_01",
"name": "room_01"
},
"bounds": {},
"player_spawn": [
100,
180
],
"entry_spawns": {
"room1_right": [
1040,
680
]
},
"platforms": [
{
"x": 20,
"y": 260,
"w": 1000,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 0,
"w": 20,
"h": 780,
"group": "ground",
"layer": 0
},
{
"x": 540,
"y": 140,
"w": 160,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 900,
"y": 400,
"w": 700,
"h": 100,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 20,
"w": 20,
"h": 380,
"group": "ground",
"layer": 0
},
{
"x": 1120,
"y": 280,
"w": 40,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 840,
"y": 460,
"w": 60,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 600,
"y": 400,
"w": 140,
"h": 100,
"group": "ground",
"layer": 0
},
{
"x": 380,
"y": 400,
"w": 140,
"h": 100,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 400,
"w": 140,
"h": 100,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 620,
"w": 760,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 500,
"w": 20,
"h": 120,
"group": "ground",
"layer": 0
},
{
"x": 900,
"y": 500,
"w": 20,
"h": 120,
"group": "ground",
"layer": 0
},
{
"x": 800,
"y": 520,
"w": 60,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 760,
"w": 1140,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1140,
"y": 500,
"w": 20,
"h": 260,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 760,
"w": 140,
"h": 20,
"group": "moving",
"path": [
[
20,
760
],
[
20,
400
]
]
}
],
"enemies": [
{
"x": 1480,
"y": 360,
"type": "enemy",
"layer": 0,
"behavior": ""
},
{
"x": 340,
"y": 580,
"type": "enemy",
"layer": 0,
"behavior": ""
}
],
"npcs": [],
"props": [
{
"x": 700,
"y": 760,
"type": "butterfly_jar",
"radius": 180,
"object_id": "flow_butterfly_jar",
"layer": 0,
"quest_id": "",
"dialogue_id": "",
"auto": false
},
{
"x": 600,
"y": 140,
"type": "fairy_dust",
"radius": 40,
"object_id": "fairy_dust",
"layer": 0,
"quest_id": "",
"dialogue_id": "",
"auto": false
}
],
"exits": [
{
"x": 1060,
"y": 660,
"w": 80,
"h": 100,
"to": "room_02",
"spawn_id": "room2_left",
"spawn": [
120,
240
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,241 @@
{
"name": "room_02",
"display_name": "room_02",
"meta": {
"id": "room_02",
"name": "room_02"
},
"bounds": {},
"player_spawn": [
820,
440
],
"entry_spawns": {
"room2_bottom_left": [
120,
460
],
"room2_top_left": [
120,
240
],
"hazard_respawn_1": [
840,
440
]
},
"platforms": [
{
"x": 0,
"y": 320,
"w": 1040,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 120,
"w": 20,
"h": 200,
"group": "ground",
"layer": 0
},
{
"x": 420,
"y": 220,
"w": 220,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1060,
"y": 360,
"w": 180,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1260,
"y": 320,
"w": 140,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1500,
"y": 100,
"w": 20,
"h": 540,
"group": "ground",
"layer": 0
},
{
"x": 860,
"y": 620,
"w": 640,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 800,
"y": 500,
"w": 60,
"h": 140,
"group": "ground",
"layer": 0
},
{
"x": 1400,
"y": 500,
"w": 100,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1380,
"y": 380,
"w": 40,
"h": 40,
"group": "ground",
"layer": 0
},
{
"x": 380,
"y": 620,
"w": 420,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 260,
"y": 580,
"w": 120,
"h": 60,
"group": "ground",
"layer": 0
},
{
"x": 640,
"y": 500,
"w": 80,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 480,
"y": 500,
"w": 80,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 540,
"w": 220,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 340,
"w": 20,
"h": 220,
"group": "ground",
"layer": 0
},
{
"x": 240,
"y": 560,
"w": 20,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 380,
"y": 600,
"w": 420,
"h": 20,
"group": "hazard",
"layer": 0,
"respawn": [
840,
440
]
}
],
"enemies": [
{
"x": 1160,
"y": 320,
"type": "enemy",
"layer": 0,
"behavior": ""
},
{
"x": 1160,
"y": 580,
"type": "enemy",
"layer": 0,
"behavior": ""
}
],
"npcs": [],
"props": [
{
"x": 720,
"y": 320,
"type": "save_point",
"radius": 90,
"object_id": "lamp",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 20,
"y": 220,
"w": 80,
"h": 100,
"to": "room_01",
"spawn_id": "room1_right",
"spawn": [
1040,
680
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
},
{
"x": 20,
"y": 440,
"w": 80,
"h": 100,
"to": "room_03",
"spawn_id": "room3_top_right",
"spawn": [
1480,
220
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,324 @@
{
"name": "room_03",
"display_name": "room_03",
"meta": {
"id": "room_03",
"name": "room_03"
},
"bounds": {},
"player_spawn": [
1460,
220
],
"entry_spawns": {
"room3_top_right": [
1480,
220
],
"room3_bottom_left": [
140,
800
]
},
"platforms": [
{
"x": 1080,
"y": 300,
"w": 520,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 60,
"w": 20,
"h": 240,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 40,
"w": 1580,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1080,
"y": 320,
"w": 20,
"h": 140,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 620,
"w": 1080,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 60,
"w": 20,
"h": 560,
"group": "ground",
"layer": 0
},
{
"x": 620,
"y": 520,
"w": 80,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
620,
520
],
[
620,
340
]
]
},
{
"x": 1080,
"y": 460,
"w": 20,
"h": 160,
"group": "trigger",
"layer": 0,
"quest": "quest_first_kills"
},
{
"x": 1100,
"y": 620,
"w": 320,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 800,
"y": 320,
"w": 160,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 320,
"w": 20,
"h": 580,
"group": "ground",
"layer": 0
},
{
"x": 1220,
"y": 760,
"w": 360,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 880,
"w": 1560,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 640,
"w": 20,
"h": 240,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 640,
"w": 20,
"h": 240,
"group": "trigger",
"layer": 0,
"name": "first_quest_complete",
"quest_turned_in": "quest_first_kills"
},
{
"x": 580,
"y": 640,
"w": 20,
"h": 120,
"group": "ground",
"layer": 0
},
{
"x": 480,
"y": 740,
"w": 100,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 400,
"y": 400,
"w": 100,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 220,
"y": 400,
"w": 80,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
220,
400
],
[
220,
260
]
]
},
{
"x": 40,
"y": 220,
"w": 140,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1520,
"y": 640,
"w": 60,
"h": 20,
"group": "ground",
"layer": 0
}
],
"enemies": [
{
"x": 600,
"y": 840,
"type": "enemy",
"layer": 0
},
{
"x": 960,
"y": 840,
"type": "enemy",
"layer": 0
},
{
"x": 200,
"y": 580,
"type": "enemy",
"layer": 0
},
{
"x": 520,
"y": 700,
"type": "enemy",
"layer": 0
},
{
"x": 1520,
"y": 840,
"type": "enemy",
"layer": 0
}
],
"npcs": [],
"props": [
{
"x": 960,
"y": 580,
"type": "quest_giver",
"radius": 180,
"object_id": "quest_point",
"layer": 0,
"quest_id": "quest_first_kills"
},
{
"x": 90,
"y": 194,
"type": "reward_chest",
"radius": 180,
"object_id": null,
"layer": 0,
"quest_id": "",
"coins": [
10,
2
]
},
{
"x": 880,
"y": 320,
"type": "light_source",
"radius": 180,
"object_id": "flow_butterfly",
"layer": 0,
"quest_id": ""
},
{
"x": 440,
"y": 400,
"type": "fairy_dust",
"radius": 40,
"object_id": "fairy_dust",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 1500,
"y": 200,
"w": 80,
"h": 100,
"to": "room_02",
"spawn_id": "room2_bottom_left",
"spawn": [
120,
460
],
"hint": "",
"layer": 0
},
{
"x": 40,
"y": 780,
"w": 80,
"h": 100,
"to": "room_04",
"spawn_id": "room4_top_right",
"spawn": [
1480,
100
],
"hint": "",
"layer": 0
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900
}
@@ -0,0 +1,353 @@
{
"name": "room_04",
"display_name": "room_04",
"meta": {
"id": "room_04",
"name": "room_04"
},
"bounds": {},
"player_spawn": [
1440,
100
],
"entry_spawns": {
"room4_top_right": [
1480,
100
],
"hazard_respawn": [
1080,
180
],
"room4_bottom_left": [
120,
620
],
"room4_bottom_right": [
1480,
640
]
},
"platforms": [
{
"x": 1340,
"y": 160,
"w": 260,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 0,
"w": 1440,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 20,
"w": 20,
"h": 140,
"group": "ground",
"layer": 0
},
{
"x": 1340,
"y": 180,
"w": 20,
"h": 60,
"group": "ground",
"layer": 0
},
{
"x": 1020,
"y": 240,
"w": 340,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 920,
"y": 240,
"w": 100,
"h": 20,
"group": "hazard",
"layer": 0,
"respawn": [
1080,
180
]
},
{
"x": 460,
"y": 240,
"w": 460,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 900,
"y": 260,
"w": 140,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 160,
"y": 20,
"w": 20,
"h": 520,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 700,
"w": 1580,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 520,
"w": 160,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 540,
"w": 20,
"h": 180,
"group": "ground",
"layer": 0
},
{
"x": 240,
"y": 280,
"w": 160,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
240,
280
],
[
240,
640
]
]
},
{
"x": 460,
"y": 260,
"w": 20,
"h": 280,
"group": "ground",
"layer": 0
},
{
"x": 480,
"y": 300,
"w": 880,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1340,
"y": 320,
"w": 20,
"h": 220,
"group": "ground",
"layer": 0
},
{
"x": 460,
"y": 540,
"w": 20,
"h": 160,
"group": "trigger",
"layer": 0,
"name": "close_when_pass",
"lock_when_pass": true,
"open_on_miniboss_dead": true
},
{
"x": 1340,
"y": 540,
"w": 20,
"h": 160,
"group": "trigger",
"layer": 0,
"name": "open_miniboss_dead",
"open_on_miniboss_dead": true
},
{
"x": 1360,
"y": 520,
"w": 240,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 540,
"w": 20,
"h": 160,
"group": "ground",
"layer": 0
},
{
"x": 720,
"y": 620,
"w": 60,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
720,
620
],
[
720,
540
]
]
},
{
"x": 820,
"y": 540,
"w": 60,
"h": 20,
"group": "dissolve",
"layer": 0,
"path": [
[
820,
540
],
[
820,
600
]
],
"stand_time": 2,
"dissolve_time": 2
},
{
"x": 1040,
"y": 580,
"w": 100,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
1040,
580
],
[
1040,
480
]
]
}
],
"enemies": [
{
"x": 760,
"y": 200,
"type": "enemy",
"layer": 0,
"behavior": ""
},
{
"x": 880,
"y": 640,
"type": "miniboss",
"layer": 0,
"behavior": ""
}
],
"npcs": [],
"props": [
{
"x": 560,
"y": 240,
"type": "save_point",
"radius": 90,
"object_id": "lamp",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 1500,
"y": 60,
"w": 80,
"h": 100,
"to": "room_03",
"spawn_id": "room3_bottom_left",
"spawn": [
140,
800
],
"hint": "",
"layer": 0
},
{
"x": 20,
"y": 600,
"w": 80,
"h": 100,
"to": "room_06",
"spawn_id": "room6_top_right",
"spawn": [
520,
240
],
"hint": "",
"layer": 0
},
{
"x": 1500,
"y": 600,
"w": 80,
"h": 100,
"to": "room_05",
"spawn_id": "room5_top_left",
"spawn": [
120,
120
],
"hint": "",
"layer": 0
}
],
"ability_doors": [
{
"x": 160,
"y": 540,
"w": 20,
"h": 160,
"layer": 0,
"kind": "shade"
}
],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,186 @@
{
"name": "room_05",
"display_name": "room_05",
"meta": {
"id": "room_05",
"name": "room_05"
},
"bounds": {},
"player_spawn": [
140,
100
],
"entry_spawns": {
"room5_top_left": [
120,
120
],
"room5_bottom_right": [
1420,
780
]
},
"platforms": [
{
"x": 20,
"y": 180,
"w": 220,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 0,
"w": 20,
"h": 840,
"group": "ground",
"layer": 0
},
{
"x": 340,
"y": 180,
"w": 240,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 700,
"y": 180,
"w": 200,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 840,
"w": 1520,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1520,
"y": 680,
"w": 20,
"h": 180,
"group": "ground",
"layer": 0
},
{
"x": 1380,
"y": 700,
"w": 20,
"h": 140,
"group": "trigger",
"layer": 0,
"name": "open_when_boss_slain",
"open_on_boss_dead": true
},
{
"x": 1380,
"y": 680,
"w": 140,
"h": 20,
"group": "trigger",
"layer": 0,
"name": "open_when_boss_slain",
"open_on_boss_dead": true
},
{
"x": 1380,
"y": 0,
"w": 20,
"h": 680,
"group": "ground",
"layer": 0
},
{
"x": 1040,
"y": 740,
"w": 160,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
1040,
740
],
[
1040,
200
]
],
"open_on_boss_dead": true
}
],
"enemies": [
{
"x": 620,
"y": 720,
"type": "boss",
"layer": 0,
"behavior": "shockwave"
}
],
"npcs": [],
"props": [
{
"x": 860,
"y": 180,
"type": "save_point",
"radius": 90,
"object_id": "lamp",
"layer": 0,
"quest_id": ""
},
{
"x": 740,
"y": 180,
"type": "fairy_dust",
"radius": 40,
"object_id": "fairy_dust",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 20,
"y": 80,
"w": 80,
"h": 100,
"to": "room_04",
"spawn_id": "room4_bottom_right",
"spawn": [
1480,
640
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
},
{
"x": 1440,
"y": 740,
"w": 80,
"h": 100,
"to": "room_07",
"spawn_id": "room7_top_left",
"spawn": [
180,
460
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,102 @@
{
"name": "room_06",
"display_name": "room_06",
"meta": {
"id": "room_06",
"name": "room_06"
},
"bounds": {},
"player_spawn": [
480,
240
],
"entry_spawns": {
"room6_top_right": [
520,
240
]
},
"platforms": [
{
"x": 0,
"y": 300,
"w": 660,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 420,
"y": 180,
"w": 240,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 640,
"y": 200,
"w": 20,
"h": 100,
"group": "ground",
"layer": 0
},
{
"x": 420,
"y": 0,
"w": 20,
"h": 180,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 0,
"w": 420,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 0,
"y": 20,
"w": 20,
"h": 280,
"group": "ground",
"layer": 0
}
],
"enemies": [],
"npcs": [],
"props": [
{
"x": 140,
"y": 260,
"type": "quest_giver",
"radius": 180,
"object_id": "quest_point",
"layer": 0,
"quest_id": "first_kills_shade"
}
],
"exits": [
{
"x": 560,
"y": 200,
"w": 80,
"h": 100,
"to": "room_04",
"spawn_id": "room4_bottom_left",
"spawn": [
120,
620
],
"hint": "",
"layer": 0
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": true
}
@@ -0,0 +1,127 @@
{
"name": "room_07",
"display_name": "room_07",
"meta": {
"id": "room_07",
"name": "room_07"
},
"bounds": {},
"player_spawn": [
220,
440
],
"entry_spawns": {
"room7_top_left": [
180,
460
],
"room7_bottom_right": [
1040,
460
]
},
"platforms": [
{
"x": 60,
"y": 200,
"w": 20,
"h": 340,
"group": "ground",
"layer": 0
},
{
"x": 80,
"y": 520,
"w": 1080,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1160,
"y": 320,
"w": 20,
"h": 220,
"group": "ground",
"layer": 0
}
],
"enemies": [],
"npcs": [],
"props": [
{
"x": 440,
"y": 494,
"type": "reward_chest",
"radius": 180,
"object_id": null,
"layer": 0,
"quest_id": "",
"coins": [
30,
10
]
},
{
"x": 820,
"y": 500,
"type": "ability_pickup",
"radius": 180,
"object_id": null,
"layer": 0,
"quest_id": ""
},
{
"x": 640,
"y": 520,
"type": "save_point",
"radius": 90,
"object_id": "lamp",
"layer": 0,
"quest_id": ""
},
{
"x": 1100,
"y": 520,
"type": "fairy_dust",
"radius": 40,
"object_id": "fairy_dust",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 80,
"y": 420,
"w": 80,
"h": 100,
"to": "room_05",
"spawn_id": "room5_bottom_right",
"spawn": [
1420,
780
],
"hint": "",
"layer": 0
},
{
"x": 1080,
"y": 420,
"w": 80,
"h": 100,
"to": "room_08",
"spawn_id": "room8_bottom_left",
"spawn": [
300,
400
],
"hint": "",
"layer": 0
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,96 @@
{
"name": "room_08",
"display_name": "room_08",
"meta": {
"id": "room_08",
"name": "room_08"
},
"bounds": {},
"player_spawn": [
340,
300
],
"entry_spawns": {
"room8_bottom_left": [
300,
400
],
"room8_bottom_right": [
1400,
400
]
},
"platforms": [
{
"x": 200,
"y": 460,
"w": 1340,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 180,
"y": 120,
"w": 20,
"h": 360,
"group": "ground",
"layer": 0
},
{
"x": 1520,
"y": 100,
"w": 20,
"h": 360,
"group": "ground",
"layer": 0
}
],
"enemies": [
{
"x": 820,
"y": 220,
"type": "boss_flyer",
"layer": 0,
"behavior": "flyer"
}
],
"npcs": [],
"props": [],
"exits": [
{
"x": 200,
"y": 360,
"w": 80,
"h": 100,
"to": "room_07",
"spawn_id": "room7_bottom_right",
"spawn": [
1040,
460
],
"hint": "",
"layer": 0,
"requires_boss_dead": true
},
{
"x": 1440,
"y": 360,
"w": 80,
"h": 100,
"to": "room_09",
"spawn_id": "room9_top_left",
"spawn": [
220,
300
],
"hint": "",
"layer": 0,
"requires_boss_dead": true
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,252 @@
{
"name": "room_09",
"display_name": "room_09",
"meta": {
"id": "room_09",
"name": "room_09"
},
"bounds": {},
"player_spawn": [
220,
260
],
"entry_spawns": {
"room9_top_left": [
220,
300
],
"room9_top_right": [
2660,
240
],
"room9_bottom_right": [
2660,
600
]
},
"platforms": [
{
"x": 100,
"y": 360,
"w": 300,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 80,
"y": 220,
"w": 20,
"h": 440,
"group": "ground",
"layer": 0
},
{
"x": 460,
"y": 440,
"w": 140,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 620,
"y": 500,
"w": 120,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 800,
"y": 560,
"w": 80,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 80,
"y": 660,
"w": 2699,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1580,
"y": 540,
"w": 240,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 1980,
"y": 540,
"w": 100,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 2260,
"y": 660,
"w": 140,
"h": 20,
"group": "moving",
"layer": 0,
"path": [
[
2260,
660
],
[
2260,
340
]
]
},
{
"x": 2460,
"y": 300,
"w": 320,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 2780,
"y": 120,
"w": 20,
"h": 200,
"group": "ground",
"layer": 0
},
{
"x": 2780,
"y": 480,
"w": 20,
"h": 200,
"group": "ground",
"layer": 0
}
],
"enemies": [],
"npcs": [
{
"x": 1296,
"y": 606,
"type": "necklace_vendor",
"layer": 1
},
{
"x": 2012,
"y": 486,
"type": "combat_master",
"layer": 0
}
],
"props": [
{
"x": 1240,
"y": 660,
"type": "house",
"radius": 180,
"object_id": "house",
"layer": 0,
"quest_id": "",
"dialogue_id": "",
"auto": false
},
{
"x": 1660,
"y": 540,
"type": "house",
"radius": 180,
"object_id": "house",
"layer": 0,
"quest_id": "",
"dialogue_id": "",
"auto": false
},
{
"x": 1754,
"y": 490,
"type": "diary",
"radius": 180,
"object_id": "diary",
"layer": 1,
"quest_id": "",
"dialogue_id": "",
"auto": false
},
{
"x": 1460,
"y": 660,
"type": "save_point",
"radius": 90,
"object_id": "lamp"
},
{
"x": 840,
"y": 560,
"type": "fairy_dust",
"radius": 40,
"object_id": "fairy_dust",
"layer": 0,
"quest_id": ""
}
],
"exits": [
{
"x": 100,
"y": 260,
"w": 80,
"h": 100,
"to": "room_08",
"spawn_id": "room8_bottom_right",
"spawn": [
1400,
400
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
},
{
"x": 2700,
"y": 560,
"w": 80,
"h": 100,
"to": "",
"spawn_id": "",
"spawn": [
2700,
560
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
},
{
"x": 2700,
"y": 200,
"w": 80,
"h": 100,
"to": "room_10",
"spawn_id": "room10_top_left",
"spawn": [
140,
280
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,71 @@
{
"name": "room_10",
"display_name": "room_10",
"meta": {
"id": "room_10",
"name": "room_10"
},
"bounds": {},
"player_spawn": [
160,
260
],
"entry_spawns": {
"room10_top_left": [
140,
280
]
},
"platforms": [
{
"x": 20,
"y": 340,
"w": 440,
"h": 20,
"group": "ground",
"layer": 0
},
{
"x": 20,
"y": 120,
"w": 20,
"h": 220,
"group": "ground",
"layer": 0
}
],
"enemies": [],
"npcs": [],
"props": [
{
"x": 340,
"y": 300,
"type": "fairy_spot",
"radius": 180,
"object_id": "fairy_spot",
"dialogue_id": "spot_greeting",
"quest_id": "quest_fairy_dust"
}
],
"exits": [
{
"x": 40,
"y": 240,
"w": 80,
"h": 100,
"to": "room_09",
"spawn_id": "room9_top_right",
"spawn": [
2660,
240
],
"hint": "",
"layer": 0,
"requires_boss_dead": false
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
@@ -0,0 +1,67 @@
{
"name": "boss_fly_refight",
"display_name": "boss_fly_refight",
"meta": {
"id": "boss_fly_refight",
"name": "boss_fly_refight"
},
"bounds": {},
"player_spawn": [
280,
400
],
"entry_spawns": {},
"platforms": [
{
"x": 200,
"y": 460,
"w": 1340,
"h": 20,
"group": "ground"
},
{
"x": 180,
"y": 120,
"w": 20,
"h": 360,
"group": "ground"
},
{
"x": 1540,
"y": 120,
"w": 20,
"h": 360,
"group": "ground"
}
],
"enemies": [
{
"x": 820,
"y": 220,
"type": "boss_flyer",
"layer": 0,
"behavior": "flyer"
}
],
"npcs": [],
"props": [],
"exits": [
{
"x": 1460,
"y": 360,
"w": 80,
"h": 100,
"to": "room_09.json",
"spawn_id": "",
"spawn": [
1754,
540
],
"requires_boss_dead": true
}
],
"ability_doors": [],
"room_w": 1600,
"room_h": 900,
"mist": false
}
File diff suppressed because it is too large Load Diff
@@ -4,13 +4,13 @@
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>e4b51f4c-e0be-4f9e-8a8e-d814a78b30c5</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>pygame_test.py</StartupFile>
<StartupFile>main.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>pygame test</Name>
<RootNamespace>pygame test</RootNamespace>
<Name>light_and_dark</Name>
<RootNamespace>light_and_dark</RootNamespace>
<TestFramework>unittest</TestFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -22,7 +22,7 @@
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<ItemGroup>
<Compile Include="pygame_test.py" />
<Compile Include="main.py" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
New skills
Tether
- hit an enemy, inflict a visual bind for a few seconds, end effect depends on which form the player is in when it ends
- light form: blind enemy (make them walk the other direction)
- shade form: instakill after a certain enemy HP threshold, slow otherwise
Reality Break
- light form: make all enemies double their drop rates
- shade form: instantly kill all enemies, halves drop rates
Shatterstep
- light form: for X seconds, create a platform below your feet when jumping, allowing for multiple jumps in the air
- shade form: when jumping, create aoe below you upon landing
Skill upgrades
Dash
- Refund cost upon successful stun/kill
- lower cooldown
- more range
- build up movement speed stacks with every dash, lose stacks upon getting hit
- more damage against targets that are stunned (inflict mark?)
- leave behind afterimage (enemy taunt)
Ranged Attack
- hold to charge, release for stronger attack (more damage and stun)
- faster speed, higher charge cost
- slower speed but more damage
- upgrade light form: pierce and slow
- upgrade shade form: aoe
- for every enemy kill collect stacks that increase damage, lose stacks upon not finishing enemies
Tether
- light form: higher hp threshold for kill; if used against boss: slower attacks
- shade form: longer fear; if used against boss: slower movement speed
How to obtain skill upgrades
- vendor (smith? trainer?) that can upgrade one spell (selection can be changed but requires paying for it every time)
- item (gem?) that can be slotted into armor to unlock a skill upgrade
- ring to go with necklaces (no synergy to not force those even more like armor already does)
Ascended armor
- select one awakened armor piece as the main piece and one as the sacrifice
- select X number of traits from the sacrifice to merge onto the main piece
- sacrifice gets destroyed in the process, creating an Ascended armor piece
- cost: many coins/rare resource?
Further ideas
Quests:
Repeatable
- usually has a loot pool of items but the player also gains resources to buy stuff from this loot pool (as some sort of pity system)
One-time
- fixed reward
- essential or useful for progression
Boss Tokens
- bosses drop an item (unique per boss) that can be "collected" (diary) to fight them again
- token can be brought to a specific NPC to upgrade it to fight harder bosses
Rework for Charge -> Flow, butterflies (for lore) instead of Lanterns
Regions
1st region: Shade mode is a thing, but not widely used. Player gets used to FLOW. First easy quest.
2nd region: Shade mode is needed for a handful of interactions, but not really for combat. First boss fight. First ability. Town 1.
3rd region: Shade mode is required to access areas or NPCs. New boss which unlocks region 4, but is optional for now.
4th region: Access through third area boss. Shade mode is required to traverse (unique environmental effects). Boss fight that has special targeting projectiles
if the player isn't in shade mode. New ability. Fast travel to 5th region/Town 2?
5th region: Alternate to 4th region in terms of progress. Light area. Town 2. Quest board/NPCs/player housing (for accessing the diary)
Ability changes (ideas)
Ranged:
- pierce
- less dmg for less flow cost
- faster/ramp up traveling speed/scale damage with speed
- more damage to specific enemies
- bigger but slower
- other directions (up/diagonal?)
- hold button to charge/release, first enemy hit will be marked for ? seconds, further shots will home in on marked enemy
Ground slam shade:
- knockback
- dmg immunity when hitting 1+ enemies
Light step:
- dmg up for ? seconds after/when touching platform
- speed increase after/when touching platform
Companion (fairy)
- interaction/guide
- ability: fairy bond teleport?
Fairy bond
- reinforce bond with fairy companion: allow fairy scout mode (fly and look but no interactions) and later on teleport to companion if it flies
ahead in this mode
- access to this bond mode takes a few seconds (channel) to not make the light step ability unneccessary
- fairy can make light at the expense of the player's flow
- fairy holes (environmental features/shortcuts) where player can teleport through with bond teleport
Fast travel
- items found all over the map (rune stones) which can be slotted into special devices to link those together as fast travel points
Enemies
design/behavior?
- slow patrol, move/jump in player direction
- slow patrol, doge backwards and leap at player
- stationary, shoot at player, burrow to somewhere else nearby
Quick recharge
- player can collect something in advance to refill charge mid-battle with a quick press of a button
Death
- what does the player lose?
- upgrades to lose less?
Lore
- player character is a being of light and dark, created by a forbidden love between a light and dark being
- gods? -> forbidden child <- banished and sleeping -> woken up long later by fairy companion (this is where the game starts)?
- parents of player character sent companion to support and guide the player back to their parents
- flow/charge is not inheritently dark or light, it is simply a resource that exists in the player character and a few other things in the world
World change
- mid game progress: unique weapon that the player can choose to take
- world changes if weapon is taken
- if weapon is not taken, player can still explore and return later to take the weapon unless they trigger a different event which would also change the world
(but differently compared to when they'd take the weapon) -> do they still get that weapon but in a different variant?
-5
View File
@@ -1,5 +0,0 @@
<Solution>
<Project Path="pygame test/pygame test.pyproj" Type="888888a0-9f3d-457c-b088-3a5042f75d52" Id="e4b51f4c-e0be-4f9e-8a8e-d814a78b30c5">
<Build Project="false" />
</Project>
</Solution>
-133
View File
@@ -1,133 +0,0 @@
# Light & Dark — Prototype
A 2D platformer built with **Pygame**. You switch between two forms — **Light** and **Shade** — each with different movement, combat, and ability behavior, all driven by a single shared resource: **Charge (Flow)**.
> Note: this is a prototype. All progress (quests, boss tokens, broken walls, defeated bosses) is **in-memory** and resets when you restart the game — intentional, for fast testing.
---
## Running the game
```
python pygame_test.py
```
Requires Python 3.10+ and `pygame`. Game data (rooms, items, sets, enemies, drops) lives in `data/`. Rooms are edited with the included browser tool: `level_editor_v43.html`.
---
## Controls
| Action | Key |
|---|---|
| Move | A / D |
| Jump | W |
| Move down / drop | S |
| Heal | Space (hold) |
| Melee attack | E |
| Ranged shot | Q |
| Dash | Tab |
| Shatterstep | R |
| Tether | X |
| Reality Break | C |
| Toggle Light/Shade | Left Shift |
| Interact / Talk | F |
| Inventory | I |
| Stats overlay | H |
| Pause / Settings | Esc |
---
## The two forms
**Light** — faster movement, higher jump, and charge **refills** over time. You take contact damage from enemies.
**Shade** — slower but tanky: **immune to enemy contact damage**, deals double melee damage, and can use shade-specific gear bonuses. But being in Shade **drains Charge** (and Shade is forced off when Charge hits 0). Shade also drains Charge **twice as fast while clipping inside an enemy**.
**Charge (Flow)** — base max 100, refills at 20/s in Light, drains at 40/s in Shade. Standing near a **lantern/light source** doubles Charge regeneration. Attacks, dash (Light), ranged shots, and healing all cost Charge.
---
## Abilities (by form)
| Ability | Key | Light | Shade |
|---|---|---|---|
| **Melee** | E | 1 damage, costs 1 Charge | **2 damage**, costs 1 Charge |
| **Dash** | Tab | Costs 10 Charge; **stuns** enemies hit | Costs 1 HP; deals **4 damage** to enemies hit |
| **Ranged** | Q | Costs 5 Charge, 0.45s cooldown, 0.5 damage; **breaks green ability walls** | Same, but 1.0 damage |
| **Shatterstep** | R | Spawns a **platform under your feet** while airborne (extra jumps) | On landing, creates a damaging **shockwave AOE** below you |
| **Tether** | X | **Binds** the enemy (3s) and **fears** it (runs away) | **Insta-kills** enemies below a HP threshold (normal ⅓, miniboss 20%, boss 5%); otherwise **slows** them |
| **Reality Break** | C | Doubles drop & coin rates and glows enemies | **Instantly kills all enemies** but halves drop & coin rates |
| **Heal** | Space | Spend Charge to heal over ~2s (slower while healing) | Unavailable |
---
## Combat notes
- Enemy contact damage only applies in **Light** form.
- Boss **shockwave attacks poison** you for 4 seconds: the health bar turns green and you lose 1 HP per second.
- Attacks and hits use invincibility frames and knockback; your melee hitbox covers ~30% into your body so close/overlapping hits always connect.
---
## Equipment & Armor Sets
Slots: **helmet, chest, pants, necklace**. Pieces roll stats (move speed, heal, charge, health, etc.).
**Rarities:** `tempered` (tier 1) → `attuned` (tier 2) → `awakened` (tier 3, unlocks ability slots). The **Upgrader** NPC turns 5 tempered copies of an item into 1 attuned copy.
**Sets** (3 pieces of the same set, attuned or better):
| Set | Summary |
|---|---|
| Dawnrunner | +HP; awakened adds Light move speed |
| Gravemark | +max Charge; awakened adds Charge regen |
| Vitalweave | +HP; awakened adds +heal |
| Duskwarrior | +Shade damage; awakened restores Charge on Shade hits |
| Twister | Removes Shade speed penalty; awakened gives move speed after switching forms |
| Paladin | Chance to negate incoming damage; awakened adds +HP |
| Luminal Growth | +Charge regen while standing still in Light; awakened also negates damage |
| Abyss Cleaver | Chance to heal on kill; awakened also gives move speed on kill |
| Umbraflow | Reduces Shade Charge drain |
| Adaptive Traveller | Move speed after switching forms; awakened restores Charge on switch |
The shade-focused sets (**Duskwarrior, Umbraflow, Abyss Cleaver, Adaptive Traveller**) are intentionally **excluded** from early boss and town-shop loot, so you can always complete the remaining sets.
---
## NPCs (Town 1: Gloomreach)
- **Vendor** — sells basic armor pieces.
- **Necklace Vendor** — sells necklaces.
- **Upgrader** — attunes 5× tempered → attuned.
- **Combat Master** — upgrades your boss token to unlock the **Hard** boss variant.
- **Boss Diary** — an interactable book: view your collected boss tokens in a grid, slot/unslot them, and start refights.
---
## Rooms & Progression
- `room_01` — tutorial. Contains a **quest object** (pink): accept the **"First Kills"** quest (kill 4 normal enemies → 5–15 light coins, repeatable).
- `room_02` — first enemies.
- `room_07` — hub, branching to the boss, reward room, and town.
- `room_boss_01` — **Gloom Warden** fight. Defeating him drops his **boss token**.
- `room_reward_01` — reward chest + the **Ranged** ability, locked behind green ability walls.
- `room_town_gloomreach` — Town 1 (NPCs above).
- `room_boss_refight_01` — diary refight room (same arena, no exits until the boss dies or you die).
---
## Boss Diary & Tokens
1. Beat the world boss → collect his **token** (teal pickup).
2. Bring it to the diary in town and **slot** it (or keep it in your inventory).
3. Slotting unlocks **Fight (Normal)**. Upgrading the token at the Combat Master unlocks **Fight (Hard)** — the boss moves and shoots twice as fast.
4. Dying in a refight returns you to the diary. Removing a token from the diary sends it back to your inventory, disabling the fight until you re-slot it.
---
## HUD & Stats
- Top-right: health bar (turns **green while poisoned**), Charge bar, coin counts.
- Ability icons with cooldown indicators (top-left).
- **H** toggles a detailed overlay: exact HP/Charge, move speeds (both forms), heal duration, attack rate, Charge regen, the lantern buff, and any stat bonuses from gear.
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
{
"charge_cost": 5.0,
"damage_mult": 0.5,
"cooldown": 0.45,
"projectile": {
"speed": 650.0,
"lifetime": 1.2,
"size": [14, 6]
}
}
-31
View File
@@ -1,31 +0,0 @@
{
"enemy": {
"rect": [40, 60],
"health": 3,
"speed": 2.2,
"air_speed": 2.0,
"detection_range": 240,
"contact_damage": 1,
"drop_profile": "normal"
},
"miniboss": {
"rect": [70, 95],
"health": 18,
"speed": 1.8,
"air_speed": 1.6,
"detection_range": 360,
"contact_damage": 2,
"drop_profile": "miniboss"
},
"boss": {
"rect": [110, 120],
"health": 10,
"speed": 1.6,
"air_speed": 1.4,
"detection_range": 500,
"contact_damage": 3,
"drop_profile": "boss"
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"objects": {
"lamp": {
"type": "light_source",
"charge_regen_range": 180
}
}
}
-143
View File
@@ -1,143 +0,0 @@
{
"meta": {
"id": "room_01",
"name": "Room 01 - Tutorial Steps"
},
"player_spawn": [
-260,
500
],
"bounds": {
"kill_y": 2000
},
"platforms": {
"ground": [
[
-500,
640,
2060,
460
],
[
-500,
560,
520,
80
],
[
1560,
220,
120,
880
],
[
-1100,
-520,
600,
1620
],
[
1680,
-100,
920,
1200
],
[
-500,
-520,
940,
840
],
[
440,
-520,
2160,
420
]
],
"floating": [
[
800,
220,
320,
40
],
[
880,
520,
240,
40
],
[
500,
400,
320,
40
],
[
440,
280,
200,
40
],
[
1220,
220,
340,
40
]
]
},
"enemies": [],
"exits": [
{
"rect": [
-420,
480,
90,
80
],
"to": "room_reward_01",
"spawn": [
120,
300
],
"hint": "Reward"
},
{
"rect": [
1560,
100,
120,
120
],
"to": "room_02",
"spawn": [
-420,
300
],
"hint": "",
"spawn_id": "entry_1"
}
],
"ability_doors": [],
"entry_spawns": {},
"props": [
{
"type": "light_source",
"pos": [
0,
560
],
"radius": 180
},
{
"type": "quest_giver",
"quest_id": "quest_first_kills",
"pos": [
300,
600
]
}
]
}
-205
View File
@@ -1,205 +0,0 @@
{
"meta": {
"id": "room_02",
"name": "Room 02 - First Enemies"
},
"player_spawn": [
-320,
300
],
"bounds": {
"kill_y": 2000
},
"platforms": {
"ground": [
[
-500,
360,
2440,
660
],
[
-760,
-580,
260,
1600
],
[
1940,
-580,
240,
1600
],
[
-760,
-820,
2940,
240
]
],
"floating": [
[
260,
260,
140,
18
],
[
520,
220,
140,
18
],
[
760,
180,
140,
18
],
[
940,
120,
170,
18
],
[
1200,
160,
170,
18
],
[
1460,
100,
170,
18
],
[
1200,
-20,
280,
40
],
[
40,
-140,
1100,
60
],
[
-500,
-140,
460,
60
],
[
-500,
-260,
260,
40
],
[
-500,
-380,
160,
40
],
[
-320,
-500,
1380,
40
],
[
1140,
-440,
340,
40
],
[
1560,
-380,
380,
40
]
]
},
"enemies": [
{
"type": "enemy",
"pos": [
240,
-200
]
},
{
"type": "enemy",
"pos": [
620,
140
]
},
{
"type": "enemy",
"pos": [
860,
-200
]
},
{
"type": "enemy",
"pos": [
480,
-530
]
},
{
"type": "enemy",
"pos": [
1740,
280
]
}
],
"exits": [
{
"rect": [
-500,
300,
80,
60
],
"to": "room_01",
"spawn": [
1560,
140
],
"hint": "Back"
},
{
"rect": [
1860,
-500,
80,
120
],
"to": "room_07",
"spawn": [
180,
240
],
"hint": ""
}
],
"ability_doors": [],
"entry_spawns": {
"entry_1": [
-420,
300
],
"entry_3": [
1840,
-440
]
}
}
-426
View File
@@ -1,426 +0,0 @@
{
"name": "room_07",
"display_name": "room_07",
"meta": {
"id": "room_07",
"name": "room_07"
},
"bounds": {
"kill_y": 3200
},
"player_spawn": [
200,
220
],
"entry_spawns": {
"entry_1": [
180,
240
],
"entry_2": [
4200,
2720
],
"entry_3": [
-320,
2720
]
},
"platforms": [
{
"x": 60,
"y": 320,
"w": 1460,
"h": 40,
"group": "ground"
},
{
"x": -1160,
"y": 2780,
"w": 5500,
"h": 120,
"group": "ground"
},
{
"x": -1160,
"y": -300,
"w": 1220,
"h": 2480,
"group": "ground"
},
{
"x": -1280,
"y": -300,
"w": 120,
"h": 3200,
"group": "ground"
},
{
"x": 2300,
"y": 1860,
"w": 2040,
"h": 320,
"group": "ground"
},
{
"x": 4340,
"y": -300,
"w": 100,
"h": 3200,
"group": "ground"
},
{
"x": 60,
"y": -300,
"w": 4280,
"h": 80,
"group": "ground"
},
{
"x": 60,
"y": 1860,
"w": 1600,
"h": 320,
"group": "ground"
},
{
"x": 1600,
"y": 440,
"w": 1060,
"h": 40,
"group": "ground"
},
{
"x": 2780,
"y": 320,
"w": 580,
"h": 40,
"group": "ground"
},
{
"x": 3280,
"y": 800,
"w": 1060,
"h": 60,
"group": "ground"
},
{
"x": 1660,
"y": 1920,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1940,
"y": 2040,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1660,
"y": 2160,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1940,
"y": 2280,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1660,
"y": 2400,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1940,
"y": 2520,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1660,
"y": 2640,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 1940,
"y": 2720,
"w": 360,
"h": 20,
"group": "ground"
},
{
"x": 2660,
"y": 880,
"w": 520,
"h": 40,
"group": "ground"
},
{
"x": 2140,
"y": 880,
"w": 380,
"h": 40,
"group": "ground"
},
{
"x": 1940,
"y": 880,
"w": 120,
"h": 40,
"group": "ground"
},
{
"x": 1680,
"y": 880,
"w": 100,
"h": 40,
"group": "ground"
},
{
"x": 1440,
"y": 880,
"w": 80,
"h": 40,
"group": "ground"
},
{
"x": 1220,
"y": 880,
"w": 80,
"h": 40,
"group": "ground"
},
{
"x": 940,
"y": 880,
"w": 180,
"h": 40,
"group": "ground"
},
{
"x": 660,
"y": 820,
"w": 180,
"h": 40,
"group": "ground"
},
{
"x": 300,
"y": 740,
"w": 260,
"h": 60,
"group": "ground"
},
{
"x": 260,
"y": 640,
"w": 40,
"h": 1100,
"group": "ground"
},
{
"x": 300,
"y": 1260,
"w": 4040,
"h": 40,
"group": "ground"
},
{
"x": 3240,
"y": 980,
"w": 60,
"h": 40,
"group": "ground"
},
{
"x": 3100,
"y": 1060,
"w": 60,
"h": 40,
"group": "ground"
},
{
"x": 3240,
"y": 1140,
"w": 60,
"h": 40,
"group": "ground"
},
{
"x": 3380,
"y": 440,
"w": 300,
"h": 40,
"group": "ground"
},
{
"x": 3700,
"y": 560,
"w": 300,
"h": 40,
"group": "ground"
},
{
"x": 4020,
"y": 680,
"w": 320,
"h": 40,
"group": "ground"
}
],
"enemies": [
{
"x": 2080,
"y": 360,
"type": "enemy"
},
{
"x": 280,
"y": 2680,
"type": "miniboss"
},
{
"x": 520,
"y": 2700,
"type": "enemy"
},
{
"x": 680,
"y": 2700,
"type": "enemy"
},
{
"x": 980,
"y": 2700,
"type": "enemy"
},
{
"x": 840,
"y": 2700,
"type": "enemy"
},
{
"x": 820,
"y": 1160,
"type": "enemy"
},
{
"x": 1040,
"y": 1160,
"type": "enemy"
},
{
"x": 1340,
"y": 1160,
"type": "enemy"
},
{
"x": 1700,
"y": 1160,
"type": "enemy"
},
{
"x": 2140,
"y": 1160,
"type": "enemy"
},
{
"x": 2480,
"y": 1160,
"type": "enemy"
},
{
"x": 2700,
"y": 1160,
"type": "enemy"
},
{
"x": 2740,
"y": 1720,
"type": "miniboss"
}
],
"props": [],
"exits": [
{
"x": -1160,
"y": 2640,
"w": 100,
"h": 140,
"to": "room_town_gloomreach",
"spawn_id": "",
"spawn": [
120,
300
],
"hint": ""
},
{
"x": -420,
"y": 2720,
"w": 80,
"h": 60,
"to": "room_boss_01",
"spawn_id": "entry_2",
"spawn": [
-360,
300
],
"hint": ""
},
{
"x": 4240,
"y": 2700,
"w": 100,
"h": 80,
"to": "room_reward_01",
"spawn_id": "",
"spawn": [
1760,
300
],
"hint": ""
},
{
"x": 60,
"y": 180,
"w": 100,
"h": 140,
"to": "room_02",
"spawn_id": "",
"spawn": [
1840,
-440
],
"hint": ""
}
],
"ability_doors": [
{
"x": -980,
"y": 2180,
"w": 80,
"h": 600
},
{
"x": 4100,
"y": 2180,
"w": 40,
"h": 600
}
],
"room_w": 1600,
"room_h": 900
}
@@ -1,71 +0,0 @@
{
"meta": {
"id": "room_boss_01",
"name": "room_boss_01"
},
"player_spawn": [
-240,
300
],
"bounds": {
"kill_y": 1400
},
"platforms": {
"ground": [
[
-500,
360,
1500,
240
]
]
},
"enemies": [
{
"type": "boss",
"pos": [
280,
200
]
}
],
"exits": [
{
"rect": [
-460,
300,
80,
60
],
"to": "room_07",
"spawn": [
-320,
2720
],
"hint": "Retreat",
"spawn_id": "entry_3"
},
{
"rect": [
880,
260,
120,
100
],
"to": "room_reward_01",
"spawn": [
100,
300
],
"hint": "Reward",
"spawn_id": "entry_2"
}
],
"ability_doors": [],
"entry_spawns": {
"entry_2": [
-360,
300
]
}
}
@@ -1,48 +0,0 @@
{
"meta": {
"id": "room_boss_refight_01",
"name": "room_boss_refight_01"
},
"player_spawn": [
-240,
300
],
"bounds": {
"kill_y": 1400
},
"platforms": {
"ground": [
[
-500,
360,
1500,
240
]
]
},
"enemies": [
{
"type": "boss",
"pos": [
280,
200
]
}
],
"exits": [
{
"rect": [
-460,
300,
80,
60
],
"to": "room_town_gloomreach",
"spawn_id": "entry_diary",
"hint": "Leave",
"requires_boss_dead": true
}
],
"ability_doors": [],
"entry_spawns": {}
}
@@ -1,116 +0,0 @@
{
"meta": {
"id": "room_reward_01",
"name": "room_reward_01"
},
"player_spawn": [
140,
300
],
"bounds": {
"kill_y": 2000
},
"platforms": {
"ground": [
[
-500,
360,
2400,
240
]
],
"floating": [
[
650,
260,
220,
18
],
[
1100,
260,
220,
18
]
]
},
"enemies": [],
"exits": [
{
"rect": [
0,
300,
80,
60
],
"to": "room_boss_01",
"spawn_id": "entry_2",
"spawn": [
110,
260
],
"hint": "Back"
},
{
"rect": [
1840,
300,
60,
60
],
"to": "room_07",
"spawn_id": "entry_2",
"spawn": [
4200,
2720
],
"hint": "Forward"
}
],
"ability_doors": [
{
"rect": [
1700,
60,
60,
300
]
},
{
"rect": [
1580,
60,
40,
300
]
}
],
"entry_spawns": {
"entry_2": [
100,
300
],
"entry_1": [
1800,
300
]
},
"props": [
{
"type": "reward_chest",
"pos": [
520,
330
],
"radius": 180
},
{
"type": "ability_pickup",
"pos": [
820,
210
],
"radius": 180
}
]
}
@@ -1,91 +0,0 @@
{
"meta": {
"id": "room_town_gloomreach",
"name": "room_town_gloomreach"
},
"player_spawn": [
80,
260
],
"bounds": {
"kill_y": 2000
},
"platforms": {
"ground": [
[
-500,
360,
2400,
240
]
]
},
"enemies": [],
"exits": [
{
"rect": [
10,
300,
80,
60
],
"to": "room_07",
"spawn": [
-1000,
2720
],
"hint": "Back"
}
],
"ability_doors": [],
"entry_spawns": {
"entry_1": [
120,
300
],
"entry_diary": [
180,
300
]
},
"props": [
{
"type": "diary",
"pos": [
180,
310
]
}
],
"npcs": [
{
"type": "vendor",
"pos": [
700,
306
]
},
{
"type": "upgrader",
"pos": [
860,
306
]
},
{
"type": "necklace_vendor",
"pos": [
1020,
306
]
},
{
"type": "combat_master",
"pos": [
400,
306
],
"trade_cost": 0
}
]
}
-50
View File
@@ -1,50 +0,0 @@
New skills
Tether
- hit an enemy, inflict a visual bind for a few seconds, end effect depends on which form the player is in when it ends
- light form: blind enemy (make them walk the other direction)
- shade form: instakill after a certain enemy HP threshold, slow otherwise
Reality Break
- light form: make all enemies double their drop rates
- shade form: instantly kill all enemies, halves drop rates
Shatterstep
- light form: for X seconds, create a platform below your feet when jumping, allowing for multiple jumps in the air
- shade form: when jumping, create aoe below you upon landing
Skill upgrades
Dash
- Refund cost upon successful stun/kill
- lower cooldown
- more range
- build up movement speed stacks with every dash, lose stacks upon getting hit
- more damage against targets that are stunned (inflict mark?)
- leave behind afterimage (enemy taunt)
Ranged Attack
- hold to charge, release for stronger attack (more damage and stun)
- faster speed, higher charge cost
- slower speed but more damage
- upgrade light form: pierce and slow
- upgrade shade form: aoe
- for every enemy kill collect stacks that increase damage, lose stacks upon not finishing enemies
Tether
- light form: higher hp threshold for kill; if used against boss: slower attacks
- shade form: longer fear; if used against boss: slower movement speed
How to obtain skill upgrades
- vendor (smith? trainer?) that can upgrade one spell (selection can be changed but requires paying for it every time)
- item (gem?) that can be slotted into armor to unlock a skill upgrade
Ascended armor
- select one awakened armor piece as the main piece and one as the sacrifice
- select X number of traits from the sacrifice to merge onto the main piece
- sacrifice gets destroyed in the process, creating an Ascended armor piece
- cost: many coins/rare resource?
+1
View File
@@ -0,0 +1 @@
pygame>=2.6