Compare commits
3
Commits
c7558a3947
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
002d1e83bf | ||
|
|
d56540785f | ||
|
|
73cfd870f6 |
@@ -3,13 +3,13 @@
|
||||
## Project
|
||||
"Light & Dark" — a 2D light/shade platformer prototype (Python 3.10 + pygame).
|
||||
|
||||
- Main game: `light_and_dark/main.py` (single large file, ~8000 lines, no tests)
|
||||
- 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 (rooms 4-6 are untracked/WIP).
|
||||
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).
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
|
||||
## 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`.
|
||||
- **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.
|
||||
|
||||
+25
-12
@@ -30,6 +30,7 @@ Requires Python 3.10+ and `pygame`. Game data (rooms, items, sets, enemies, drop
|
||||
| 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 |
|
||||
@@ -37,6 +38,10 @@ Requires Python 3.10+ and `pygame`. Game data (rooms, items, sets, enemies, drop
|
||||
| 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
|
||||
@@ -77,7 +82,7 @@ When you die you lose your **butterfly jars for good** and drop your coins. A **
|
||||
|---|---|---|---|
|
||||
| **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 |
|
||||
| **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 |
|
||||
@@ -88,7 +93,8 @@ When you die you lose your **butterfly jars for good** and drop your coins. A **
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
---
|
||||
@@ -118,7 +124,9 @@ The shade-focused sets (**Duskwarrior, Umbraflow, Abyss Cleaver, Adaptive Travel
|
||||
|
||||
---
|
||||
|
||||
## NPCs (Town 1: Gloomreach)
|
||||
## 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.
|
||||
@@ -130,22 +138,27 @@ The shade-focused sets (**Duskwarrior, Umbraflow, Abyss Cleaver, Adaptive Travel
|
||||
|
||||
## 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_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).
|
||||
- `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 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Proves the core loop is fun before scaling to the full vision.
|
||||
- **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.
|
||||
|
||||
@@ -28,6 +29,7 @@ Proves the core loop is fun before scaling to the full vision.
|
||||
|---|---|---|
|
||||
| ~~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. |
|
||||
|
||||
@@ -43,12 +45,13 @@ Proves the core loop is fun before scaling to the full vision.
|
||||
|---|---|---|
|
||||
| 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 euclidean aggro (`main.py:4329`) 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`. |
|
||||
| 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
|
||||
@@ -57,6 +60,27 @@ Proves the core loop is fun before scaling to the full vision.
|
||||
### 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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,5 +6,16 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,54 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
},
|
||||
"quest_point": {
|
||||
"type": "quest_giver"
|
||||
},
|
||||
"fairy_spot": {
|
||||
"type": "fairy_spot"
|
||||
},
|
||||
"fairy_dust": {
|
||||
"type": "fairy_dust"
|
||||
}
|
||||
}
|
||||
}
|
||||
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: 437 B |
Binary file not shown.
|
After Width: | Height: | Size: 395 B |
@@ -33,14 +33,6 @@
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 0,
|
||||
"w": 1580,
|
||||
"h": 20,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 540,
|
||||
"y": 140,
|
||||
@@ -152,6 +144,23 @@
|
||||
"h": 260,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 760,
|
||||
"w": 140,
|
||||
"h": 20,
|
||||
"group": "moving",
|
||||
"path": [
|
||||
[
|
||||
20,
|
||||
760
|
||||
],
|
||||
[
|
||||
20,
|
||||
400
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"enemies": [
|
||||
@@ -159,13 +168,15 @@
|
||||
"x": 1480,
|
||||
"y": 360,
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"behavior": ""
|
||||
},
|
||||
{
|
||||
"x": 340,
|
||||
"y": 580,
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"behavior": ""
|
||||
}
|
||||
],
|
||||
"npcs": [],
|
||||
@@ -177,7 +188,20 @@
|
||||
"radius": 180,
|
||||
"object_id": "flow_butterfly_jar",
|
||||
"layer": 0,
|
||||
"quest_id": ""
|
||||
"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": [
|
||||
@@ -193,10 +217,12 @@
|
||||
240
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"requires_boss_dead": false
|
||||
}
|
||||
],
|
||||
"ability_doors": [],
|
||||
"room_w": 1600,
|
||||
"room_h": 900
|
||||
"room_h": 900,
|
||||
"mist": false
|
||||
}
|
||||
@@ -49,22 +49,6 @@
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 120,
|
||||
"w": 1240,
|
||||
"h": 20,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1240,
|
||||
"y": 140,
|
||||
"w": 20,
|
||||
"h": 60,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1060,
|
||||
"y": 360,
|
||||
@@ -81,19 +65,11 @@
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1260,
|
||||
"y": 180,
|
||||
"w": 260,
|
||||
"h": 20,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1500,
|
||||
"y": 200,
|
||||
"y": 100,
|
||||
"w": 20,
|
||||
"h": 440,
|
||||
"h": 540,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
@@ -203,13 +179,15 @@
|
||||
"x": 1160,
|
||||
"y": 320,
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"behavior": ""
|
||||
},
|
||||
{
|
||||
"x": 1160,
|
||||
"y": 580,
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"behavior": ""
|
||||
}
|
||||
],
|
||||
"npcs": [],
|
||||
@@ -219,7 +197,9 @@
|
||||
"y": 320,
|
||||
"type": "save_point",
|
||||
"radius": 90,
|
||||
"object_id": "lamp"
|
||||
"object_id": "lamp",
|
||||
"layer": 0,
|
||||
"quest_id": ""
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
@@ -235,7 +215,8 @@
|
||||
680
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"requires_boss_dead": false
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
@@ -249,10 +230,12 @@
|
||||
220
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"requires_boss_dead": false
|
||||
}
|
||||
],
|
||||
"ability_doors": [],
|
||||
"room_w": 1600,
|
||||
"room_h": 900
|
||||
"room_h": 900,
|
||||
"mist": false
|
||||
}
|
||||
@@ -277,6 +277,15 @@
|
||||
"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": [
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"y": 720,
|
||||
"type": "boss",
|
||||
"layer": 0,
|
||||
"behavior": "charger"
|
||||
"behavior": "shockwave"
|
||||
}
|
||||
],
|
||||
"npcs": [],
|
||||
@@ -136,6 +136,15 @@
|
||||
"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": [
|
||||
@@ -151,7 +160,8 @@
|
||||
640
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"requires_boss_dead": false
|
||||
},
|
||||
{
|
||||
"x": 1440,
|
||||
@@ -165,7 +175,8 @@
|
||||
460
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"requires_boss_dead": false
|
||||
}
|
||||
],
|
||||
"ability_doors": [],
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"room7_top_left": [
|
||||
180,
|
||||
460
|
||||
],
|
||||
"room7_bottom_right": [
|
||||
1040,
|
||||
460
|
||||
]
|
||||
},
|
||||
"platforms": [
|
||||
@@ -28,10 +32,18 @@
|
||||
{
|
||||
"x": 80,
|
||||
"y": 520,
|
||||
"w": 920,
|
||||
"w": 1080,
|
||||
"h": 20,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1160,
|
||||
"y": 320,
|
||||
"w": 20,
|
||||
"h": 220,
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
}
|
||||
],
|
||||
"enemies": [],
|
||||
@@ -64,7 +76,18 @@
|
||||
"y": 520,
|
||||
"type": "save_point",
|
||||
"radius": 90,
|
||||
"object_id": "lamp"
|
||||
"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": [
|
||||
@@ -81,6 +104,20 @@
|
||||
],
|
||||
"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": [],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -88,10 +88,10 @@
|
||||
<div class="row">
|
||||
<button id="downloadActiveBtn" class="good">Download Active</button>
|
||||
<button id="downloadCheckedBtn">Download Checked</button>
|
||||
<button id="downloadAllBtn">Download All</button>
|
||||
<button id="downloadAllBtn">Download Modified</button>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px;">
|
||||
Downloads are per-room. Checked download saves one file per checked room; Download All saves every loaded room.
|
||||
Downloads are per-room. Checked download saves one file per checked room; Download Modified saves every room you've changed.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
</div> <div class="row" style="margin-bottom:8px;">
|
||||
<label class="muted" style="display:flex; align-items:center; gap:8px;">
|
||||
<input type="checkbox" id="showLinksChk"/>
|
||||
Draw link lines (auto-enables Compare)
|
||||
Draw link lines
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -271,6 +271,8 @@
|
||||
diary: { type: "diary" },
|
||||
player: { type: "player" },
|
||||
quest_point: { type: "quest_giver" },
|
||||
fairy_spot: { type: "fairy_spot" },
|
||||
fairy_dust: { type: "fairy_dust" },
|
||||
};
|
||||
// Type fallbacks used when no object def provides a radius.
|
||||
const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 };
|
||||
@@ -288,6 +290,19 @@
|
||||
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" },
|
||||
};
|
||||
|
||||
function seedDefaultObjectCatalog(){
|
||||
@@ -483,7 +498,7 @@
|
||||
const type = p.type || p.kind || "light_source";
|
||||
const object_id = p.object_id || p.objectId || null;
|
||||
const radius = +p.radius || getDefaultRadiusForType(type, object_id);
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "", coins: Array.isArray(p.coins) ? deepClone(p.coins) : undefined });
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "", dialogue_id: p.dialogue_id || p.dialogueId || "", auto: !!p.auto, coins: Array.isArray(p.coins) ? deepClone(p.coins) : undefined });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,7 +514,8 @@
|
||||
spawn_id: ex.spawn_id || "",
|
||||
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
||||
hint: ex.hint || "",
|
||||
layer: +(ex.layer||0)
|
||||
layer: +(ex.layer||0),
|
||||
requires_boss_dead: !!ex.requires_boss_dead
|
||||
});
|
||||
} else {
|
||||
// editor-native exit
|
||||
@@ -509,7 +525,8 @@
|
||||
spawn_id: ex.spawn_id || "",
|
||||
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
||||
hint: ex.hint || "",
|
||||
layer: +(ex.layer||0)
|
||||
layer: +(ex.layer||0),
|
||||
requires_boss_dead: !!ex.requires_boss_dead
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2260,6 +2277,9 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div><div class="mini">to</div><input id="exto" type="text" value="${ex.to||""}" placeholder="room_02"/></div>
|
||||
<div><div class="mini">spawn_id</div><input id="exsid" type="text" value="${ex.spawn_id||""}" placeholder="from_room_01"/></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<label><input id="exbossdead" type="checkbox" ${ex.requires_boss_dead?"checked":""}/> Appears/active only after the room's boss is slain</label>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:8px;">Tip: use the Linking panel to set these safely.</div>
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<button id="delSel" class="danger">Delete</button>
|
||||
@@ -2317,6 +2337,7 @@ Cancel = Choose whether to remove without saving`);
|
||||
<option value="" ${beh===""?"selected":""}>default (from enemy type)</option>
|
||||
<option value="shockwave" ${beh==="shockwave"?"selected":""}>shockwave (slam + shockwave)</option>
|
||||
<option value="charger" ${beh==="charger"?"selected":""}>charger (retreat-charge + dash)</option>
|
||||
<option value="flyer" ${beh==="flyer"?"selected":""}>flyer (air boss: slam, minions, aoe, homing shot)</option>
|
||||
</select>
|
||||
</div>
|
||||
` : ""}
|
||||
@@ -2358,8 +2379,9 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(!p) return "<div class='muted'>Selection missing.</div>";
|
||||
const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null);
|
||||
const objId = p.object_id || p.objectId || "";
|
||||
const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)", quest_giver:"Quest point (name it below)"})[p.type||"light_source"] || "";
|
||||
const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)", quest_giver:"Quest point (name it below)", fairy_spot:"Fairy spot (press F to hear the fairy; set quest id for a quest)", fairy_dust:"Fairy dust (collect with F)"})[p.type||"light_source"] || "";
|
||||
const isQuest = (p.type||"") === "quest_giver";
|
||||
const isFairySpot = (p.type||"") === "fairy_spot";
|
||||
return `
|
||||
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
||||
<div class="hr"></div>
|
||||
@@ -2371,10 +2393,19 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div class="mini">type</div>
|
||||
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
||||
</div>
|
||||
${isQuest ? `
|
||||
${(isQuest || isFairySpot) ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">quest id (name)</div>
|
||||
<input id="prqid" type="text" value="${p.quest_id||""}" placeholder="e.g. quest_save_the_well"/>
|
||||
<input id="prqid" type="text" value="${p.quest_id||""}" placeholder="e.g. quest_fairy_dust"/>
|
||||
</div>` : ""}
|
||||
${isFairySpot ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">dialogue id (from data/config/fairy.json)</div>
|
||||
<input id="prdialogue" type="text" value="${p.dialogue_id||""}" placeholder="e.g. spot_greeting"/>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">auto trigger</div>
|
||||
<input id="prauto" type="checkbox" ${p.auto ? "checked" : ""}/>
|
||||
</div>` : ""}
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">object id</div>
|
||||
@@ -2569,6 +2600,9 @@ Cancel = Choose whether to remove without saving`);
|
||||
const exto=q("exto"); if(exto) exto.addEventListener("change", ()=>pushAnd(()=>{ ex.to = exto.value.trim(); }));
|
||||
const exsid=q("exsid"); if(exsid) exsid.addEventListener("change", ()=>pushAnd(()=>{ ex.spawn_id = exsid.value.trim(); }));
|
||||
const exlayer=q("exlayer"); if(exlayer) exlayer.addEventListener("change", ()=>pushAnd(()=>{ ex.layer = Math.round(num(exlayer.value)); }));
|
||||
const exbossdead=q("exbossdead"); if(exbossdead) exbossdead.addEventListener("change", ()=>pushAnd(()=>{
|
||||
if(exbossdead.checked){ ex.requires_boss_dead = true; } else { delete ex.requires_boss_dead; }
|
||||
}));
|
||||
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
||||
room.exits.splice(sel.index,1);
|
||||
state.selection=null;
|
||||
@@ -2626,6 +2660,14 @@ Cancel = Choose whether to remove without saving`);
|
||||
const v = prqid.value.trim();
|
||||
if(v){ p.quest_id = v; } else { delete p.quest_id; }
|
||||
}));
|
||||
const prdialogue=q("prdialogue"), prauto=q("prauto");
|
||||
if(prdialogue) prdialogue.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = prdialogue.value.trim();
|
||||
if(v){ p.dialogue_id = v; } else { delete p.dialogue_id; }
|
||||
}));
|
||||
if(prauto) prauto.addEventListener("change", ()=>pushAnd(()=>{
|
||||
if(prauto.checked){ p.auto = true; } else { delete p.auto; }
|
||||
}));
|
||||
if(probjid) probjid.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = probjid.value.trim();
|
||||
if(v){ p.object_id = v; } else { delete p.object_id; }
|
||||
@@ -3439,7 +3481,7 @@ Cancel = Choose whether to remove without saving`);
|
||||
return out;
|
||||
});
|
||||
|
||||
// exits back to {rect:[x,y,w,h], to, spawn_id?, spawn:[x,y], hint} (+ layer when set)
|
||||
// exits back to {rect:[x,y,w,h], to, spawn_id?, spawn:[x,y], hint} (+ layer / requires_boss_dead when set)
|
||||
const exits = (d.exits || []).map(ex => {
|
||||
const out = {
|
||||
rect: [ex.x, ex.y, ex.w, ex.h],
|
||||
@@ -3449,6 +3491,7 @@ Cancel = Choose whether to remove without saving`);
|
||||
};
|
||||
if(ex.spawn_id) out.spawn_id = ex.spawn_id;
|
||||
if(ex.layer) out.layer = ex.layer;
|
||||
if(ex.requires_boss_dead) out.requires_boss_dead = true;
|
||||
return out;
|
||||
});
|
||||
|
||||
@@ -3469,6 +3512,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(p.object_id) out.object_id = p.object_id;
|
||||
if(p.layer) out.layer = p.layer;
|
||||
if(p.quest_id) out.quest_id = p.quest_id;
|
||||
if(p.dialogue_id) out.dialogue_id = p.dialogue_id;
|
||||
if(p.auto) out.auto = true;
|
||||
if(Array.isArray(p.coins)) out.coins = p.coins;
|
||||
return out;
|
||||
});
|
||||
@@ -3783,14 +3828,9 @@ Cancel = Choose whether to remove without saving`);
|
||||
refreshUI();
|
||||
});
|
||||
|
||||
els.showLinksChk.checked = false; // always start unchecked (no auto Compare)
|
||||
els.showLinksChk.addEventListener("change", ()=>{
|
||||
if(els.showLinksChk.checked){
|
||||
if(!state.compareEnabled){
|
||||
state.compareEnabled = true;
|
||||
const other = [...state.rooms.keys()].find(k => k !== state.activeId);
|
||||
state.compareId = other || state.activeId;
|
||||
}
|
||||
}
|
||||
// Lines only draw while Compare is on too (see drawLinkLinesForCanvas).
|
||||
// overlay updates in render loop
|
||||
refreshUI();
|
||||
});
|
||||
@@ -3850,8 +3890,8 @@ els.compareRoomSelect.addEventListener("change", ()=>{
|
||||
});
|
||||
|
||||
els.downloadAllBtn.addEventListener("click", async ()=>{
|
||||
const rooms = [...state.rooms.values()];
|
||||
if(!rooms.length){ setStatus("No rooms loaded."); return; }
|
||||
const rooms = [...state.rooms.values()].filter(r=>r.dirty);
|
||||
if(!rooms.length){ setStatus("No modified rooms."); return; }
|
||||
let i=0;
|
||||
for(const r of rooms){
|
||||
downloadText(r.filename || (r.data.name + ".json"), serializeRoom(r));
|
||||
@@ -3860,7 +3900,7 @@ els.compareRoomSelect.addEventListener("change", ()=>{
|
||||
await new Promise(res=>setTimeout(res, 250));
|
||||
}
|
||||
refreshRoomsList();
|
||||
setStatus(`Downloaded ${i} room(s).`);
|
||||
setStatus(`Downloaded ${i} modified room(s).`);
|
||||
});
|
||||
|
||||
// canvas pointer handlers
|
||||
|
||||
+1756
-286
File diff suppressed because it is too large
Load Diff
@@ -1,260 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"saved_at": "2026-10-01T14:40:38",
|
||||
"player": {
|
||||
"room_id": "room_07.json",
|
||||
"x": 620,
|
||||
"y": 520,
|
||||
"health": 10,
|
||||
"charge": 120.0,
|
||||
"shade": false,
|
||||
"coins_light": 40,
|
||||
"coins_dark": 20,
|
||||
"butterfly_jars": 0,
|
||||
"has_ranged": false,
|
||||
"opened_chests": [
|
||||
"room_03.json@90,194",
|
||||
"room_07.json@440,494"
|
||||
],
|
||||
"equipment": {
|
||||
"helmet": {
|
||||
"id": "duskwarrior_helm",
|
||||
"name": "Duskwarrior Helmet",
|
||||
"set": "duskwarrior",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge": 10
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge": 10,
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"health": 5,
|
||||
"charge_regen_pct": 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"charge": 10.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"chest": {
|
||||
"id": "vitalweave_chest",
|
||||
"name": "Vitalweave Chestplate",
|
||||
"set": "vitalweave",
|
||||
"slot": "chest",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"heal_speed_pct": 0.1
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"heal_speed_pct": 0.15
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"heal_speed_pct": 0.15,
|
||||
"health": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "attuned",
|
||||
"tier": 2,
|
||||
"stats": {
|
||||
"heal_speed_pct": 0.15
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"pants": {
|
||||
"id": "gravemark_greaves",
|
||||
"name": "Gravemark Greaves",
|
||||
"set": "gravemark",
|
||||
"slot": "pants",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge": 10
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge": 10,
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge": 20,
|
||||
"health": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"charge": 10.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"necklace": null
|
||||
}
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"id": "dawnrunner_helm",
|
||||
"name": "Dawnrunner Helmet",
|
||||
"set": "dawnrunner",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"move_speed_light_pct": 0.1
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"move_speed_light_pct": 0.1,
|
||||
"heal_speed_pct": 0.1
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"move_speed_light_pct": 0.15,
|
||||
"heal_speed_pct": 0.2
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "attuned",
|
||||
"tier": 2,
|
||||
"stats": {
|
||||
"move_speed_light_pct": 0.1,
|
||||
"heal_speed_pct": 0.1
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
{
|
||||
"id": "paladin_helm",
|
||||
"name": "Paladin Helmet",
|
||||
"set": "paladin",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"health": 10,
|
||||
"negate_damage_chance": 0.05
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"health": 15,
|
||||
"negate_damage_chance": 0.1,
|
||||
"knockback_resistance": 0.25
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"health": 5.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
{
|
||||
"id": "dawnrunner_chest",
|
||||
"name": "Dawnrunner Chestplate",
|
||||
"set": "dawnrunner",
|
||||
"slot": "chest",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge": 10
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"move_speed_light_pct": 0.05
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"move_speed_light_pct": 0.05,
|
||||
"heal_plus": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "attuned",
|
||||
"tier": 2,
|
||||
"stats": {
|
||||
"charge": 15.0,
|
||||
"move_speed_light_pct": 0.05
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
{
|
||||
"id": "necklace_flow",
|
||||
"name": "Charm of Flow",
|
||||
"slot": "necklace",
|
||||
"item_type": "necklace",
|
||||
"base_stats": {
|
||||
"charge": 10
|
||||
},
|
||||
"rarity": "",
|
||||
"tier": 0,
|
||||
"abilities": [],
|
||||
"stats": {
|
||||
"charge": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
"quests": {
|
||||
"active": {
|
||||
"first_kills_shade": {
|
||||
"progress": 1
|
||||
}
|
||||
},
|
||||
"completed_one_time": [],
|
||||
"ever_completed": [
|
||||
"quest_first_kills"
|
||||
],
|
||||
"tokens": 0
|
||||
},
|
||||
"boss_diary": {
|
||||
"slotted": {}
|
||||
},
|
||||
"defeated_bosses": [
|
||||
"room_05.json"
|
||||
],
|
||||
"broken_doors": [],
|
||||
"lantern_respawn": {
|
||||
"room_id": "room_07.json",
|
||||
"x": 640,
|
||||
"y": 520
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user