flyer boss, town
flyer boss adjustments, town addition
This commit is contained in:
@@ -3,13 +3,13 @@
|
|||||||
## Project
|
## Project
|
||||||
"Light & Dark" — a 2D light/shade platformer prototype (Python 3.10 + pygame).
|
"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)
|
- 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`
|
- 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`
|
- Data/config: `light_and_dark/data/config/*.json`, `data/items/*.json`, `data/objects/*.json`, `data/enemies/*.json`
|
||||||
|
|
||||||
## Standard session start
|
## 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.
|
2. Read the specific file(s) the request touches before editing; follow existing style.
|
||||||
3. After edits, always verify (see below).
|
3. After edits, always verify (see below).
|
||||||
|
|
||||||
@@ -25,7 +25,11 @@
|
|||||||
|
|
||||||
## Key systems (in main.py unless noted)
|
## 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.
|
- **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).
|
- **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").
|
- **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.
|
- **Minibosses**: stay defeated per room (`DEFEATED_MINIBOSSES`) until a lantern save in a different room, then respawn.
|
||||||
|
|||||||
+19
-11
@@ -92,7 +92,8 @@ When you die you lose your **butterfly jars for good** and drop your coins. A **
|
|||||||
## Combat notes
|
## Combat notes
|
||||||
|
|
||||||
- Enemy contact damage only applies in **Light** form.
|
- 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.
|
- Attacks and hits use invincibility frames and knockback; your melee hitbox covers ~30% into your body so close/overlapping hits always connect.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -122,7 +123,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.
|
- **Vendor** — sells basic armor pieces.
|
||||||
- **Necklace Vendor** — sells necklaces.
|
- **Necklace Vendor** — sells necklaces.
|
||||||
@@ -134,22 +137,27 @@ The shade-focused sets (**Duskwarrior, Umbraflow, Abyss Cleaver, Adaptive Travel
|
|||||||
|
|
||||||
## Rooms & Progression
|
## 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_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_02` — first enemies.
|
||||||
- `room_07` — hub, branching to the boss, reward room, and town.
|
- `room_04` — **miniboss** arena; the walls seal until the miniboss is slain.
|
||||||
- `room_boss_01` — **Gloom Warden** fight. Defeating him drops his **boss token**.
|
- `room_05` — **charger boss** arena (seals until slain).
|
||||||
- `room_reward_01` — reward chest + the **Ranged** ability, locked behind green ability walls.
|
- `room_07` — hub, branching to the boss arena and back.
|
||||||
- `room_town_gloomreach` — Town 1 (NPCs above).
|
- `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_boss_refight_01` — diary refight room (same arena, no exits until the boss dies or you die).
|
- `room_09` — the area past Sky Terror's right door.
|
||||||
|
- `room_boss_fly_refight` — Sky Terror **diary refight** arena.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Boss Diary & Tokens
|
## Boss Diary & Tokens
|
||||||
|
|
||||||
1. Beat the world boss → collect his **token** (teal pickup).
|
1. Beat a world boss → collect its **token** (a distinct pickup per boss).
|
||||||
2. Bring it to the diary in town and **slot** it (or keep it in your inventory).
|
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)** — the boss moves and shoots twice as fast.
|
3. Slotting unlocks **Fight (Normal)**. Upgrading the token at the **Combat Master** unlocks **Fight (Hard)**.
|
||||||
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.
|
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.
|
- **Vendor** (smith/trainer) gives direct linear power upgrades. Upgrades one selected spell; changing selection costs coins.
|
||||||
- **Gems / rings** give alternate behaviors / sidegrades.
|
- **Gems / rings** give alternate behaviors / sidegrades.
|
||||||
- **Ranged identity**: keep the bolt, add upgrades (no charge-up / homing rework).
|
- **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+.
|
- **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.
|
- **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. |
|
| ~~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. |
|
| ~~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. |
|
| More quests (repeatable + one-time; system exists) | S | Just content. |
|
||||||
| Lore dialogue for fairy / NPCs | S | Needs written copy. |
|
| Lore dialogue for fairy / NPCs | S | Needs written copy. |
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ 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. |
|
| 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. |
|
| 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. |
|
| ~~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. |
|
| 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). |
|
| Region-2 boss + ability pacing | M | Re-align pacing (Ranged already exists). |
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
"boss_flyer": {
|
"boss_flyer": {
|
||||||
"rect": [120, 90],
|
"rect": [120, 90],
|
||||||
"health": 26,
|
"health": 51,
|
||||||
"speed": 3.0,
|
"speed": 3.0,
|
||||||
"air_speed": 3.0,
|
"air_speed": 3.0,
|
||||||
"detection_range": 620,
|
"detection_range": 620,
|
||||||
@@ -52,11 +52,14 @@
|
|||||||
"drop_profile": "boss",
|
"drop_profile": "boss",
|
||||||
"behavior": "flyer",
|
"behavior": "flyer",
|
||||||
"flyer": {
|
"flyer": {
|
||||||
"flight_height": 220,
|
"flight_height": 280,
|
||||||
"bob_amplitude": 20,
|
"bob_amplitude": 20,
|
||||||
"bob_speed": 2.2,
|
"bob_speed": 2.2,
|
||||||
"hover_speed": 2.6,
|
"hover_speed": 2.6,
|
||||||
"slam_cd": 6.0,
|
"startup_delay": 3.0,
|
||||||
|
"phase2_hp_pct": 0.33,
|
||||||
|
"hard_phase3_hp_pct": 0.20,
|
||||||
|
"slam_cd": 9.0,
|
||||||
"slam_dive_vel": 16.0,
|
"slam_dive_vel": 16.0,
|
||||||
"ground_time": 2.0,
|
"ground_time": 2.0,
|
||||||
"summon_cd": 8.0,
|
"summon_cd": 8.0,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 395 B |
@@ -33,14 +33,6 @@
|
|||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"x": 20,
|
|
||||||
"y": 0,
|
|
||||||
"w": 1580,
|
|
||||||
"h": 20,
|
|
||||||
"group": "ground",
|
|
||||||
"layer": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"x": 540,
|
"x": 540,
|
||||||
"y": 140,
|
"y": 140,
|
||||||
@@ -159,13 +151,15 @@
|
|||||||
"x": 1480,
|
"x": 1480,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"behavior": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 340,
|
"x": 340,
|
||||||
"y": 580,
|
"y": 580,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"behavior": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"npcs": [],
|
"npcs": [],
|
||||||
@@ -193,10 +187,12 @@
|
|||||||
240
|
240
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"requires_boss_dead": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
"room_w": 1600,
|
"room_w": 1600,
|
||||||
"room_h": 900
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
}
|
}
|
||||||
@@ -49,22 +49,6 @@
|
|||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"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,
|
"x": 1060,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
@@ -81,19 +65,11 @@
|
|||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"x": 1260,
|
|
||||||
"y": 180,
|
|
||||||
"w": 260,
|
|
||||||
"h": 20,
|
|
||||||
"group": "ground",
|
|
||||||
"layer": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"x": 1500,
|
"x": 1500,
|
||||||
"y": 200,
|
"y": 100,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 440,
|
"h": 540,
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
@@ -203,13 +179,15 @@
|
|||||||
"x": 1160,
|
"x": 1160,
|
||||||
"y": 320,
|
"y": 320,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"behavior": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1160,
|
"x": 1160,
|
||||||
"y": 580,
|
"y": 580,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"behavior": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"npcs": [],
|
"npcs": [],
|
||||||
@@ -219,7 +197,9 @@
|
|||||||
"y": 320,
|
"y": 320,
|
||||||
"type": "save_point",
|
"type": "save_point",
|
||||||
"radius": 90,
|
"radius": 90,
|
||||||
"object_id": "lamp"
|
"object_id": "lamp",
|
||||||
|
"layer": 0,
|
||||||
|
"quest_id": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exits": [
|
"exits": [
|
||||||
@@ -235,7 +215,8 @@
|
|||||||
680
|
680
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"requires_boss_dead": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 20,
|
"x": 20,
|
||||||
@@ -249,10 +230,12 @@
|
|||||||
220
|
220
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"requires_boss_dead": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
"room_w": 1600,
|
"room_w": 1600,
|
||||||
"room_h": 900
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"y": 720,
|
"y": 720,
|
||||||
"type": "boss",
|
"type": "boss",
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"behavior": "charger"
|
"behavior": "shockwave"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"npcs": [],
|
"npcs": [],
|
||||||
@@ -151,7 +151,8 @@
|
|||||||
640
|
640
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"requires_boss_dead": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1440,
|
"x": 1440,
|
||||||
@@ -165,7 +166,8 @@
|
|||||||
460
|
460
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"requires_boss_dead": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
"room8_bottom_left": [
|
"room8_bottom_left": [
|
||||||
300,
|
300,
|
||||||
400
|
400
|
||||||
|
],
|
||||||
|
"room8_bottom_right": [
|
||||||
|
1400,
|
||||||
|
400
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"platforms": [
|
"platforms": [
|
||||||
@@ -46,7 +50,7 @@
|
|||||||
{
|
{
|
||||||
"x": 820,
|
"x": 820,
|
||||||
"y": 220,
|
"y": 220,
|
||||||
"type": "boss",
|
"type": "boss_flyer",
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"behavior": "flyer"
|
"behavior": "flyer"
|
||||||
}
|
}
|
||||||
@@ -64,7 +68,25 @@
|
|||||||
"spawn": [
|
"spawn": [
|
||||||
1040,
|
1040,
|
||||||
460
|
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": [],
|
"ability_doors": [],
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 620,
|
||||||
|
"y": 500,
|
||||||
|
"w": 120,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 800,
|
||||||
|
"y": 560,
|
||||||
|
"w": 80,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 80,
|
||||||
|
"y": 660,
|
||||||
|
"w": 2699,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1580,
|
||||||
|
"y": 540,
|
||||||
|
"w": 240,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1980,
|
||||||
|
"y": 540,
|
||||||
|
"w": 100,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2260,
|
||||||
|
"y": 660,
|
||||||
|
"w": 140,
|
||||||
|
"h": 20,
|
||||||
|
"group": "moving",
|
||||||
|
"path": [
|
||||||
|
[
|
||||||
|
2260,
|
||||||
|
660
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2260,
|
||||||
|
340
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2460,
|
||||||
|
"y": 300,
|
||||||
|
"w": 320,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enemies": [],
|
||||||
|
"npcs": [
|
||||||
|
{
|
||||||
|
"x": 1296,
|
||||||
|
"y": 606,
|
||||||
|
"type": "necklace_vendor",
|
||||||
|
"layer": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2012,
|
||||||
|
"y": 486,
|
||||||
|
"type": "combat_master"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"x": 800,
|
||||||
|
"y": 460,
|
||||||
|
"type": "player",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "player"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1240,
|
||||||
|
"y": 660,
|
||||||
|
"type": "house",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "house"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1660,
|
||||||
|
"y": 540,
|
||||||
|
"type": "house",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "house"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1754,
|
||||||
|
"y": 490,
|
||||||
|
"type": "diary",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "diary",
|
||||||
|
"layer": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"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": "",
|
||||||
|
"spawn_id": "",
|
||||||
|
"spawn": [
|
||||||
|
1460,
|
||||||
|
360
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
|
}
|
||||||
@@ -289,10 +289,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
boss_flyer: {
|
boss_flyer: {
|
||||||
rect: [120, 90], health: 26, speed: 3.0, air_speed: 3.0, detection_range: 620, contact_damage: 3, drop_profile: "boss", behavior: "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: {
|
flyer: {
|
||||||
flight_height: 220, bob_amplitude: 20, bob_speed: 2.2, hover_speed: 2.6,
|
flight_height: 280, bob_amplitude: 20, bob_speed: 2.2, hover_speed: 2.6,
|
||||||
slam_cd: 6.0, slam_dive_vel: 16.0, ground_time: 2.0,
|
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,
|
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,
|
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
|
shot_cd: 5.0, shot_damage: 5, shot_speed: 300, shot_lifetime: 2.2, shot_homing: 1.8
|
||||||
@@ -510,7 +512,8 @@
|
|||||||
spawn_id: ex.spawn_id || "",
|
spawn_id: ex.spawn_id || "",
|
||||||
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
||||||
hint: ex.hint || "",
|
hint: ex.hint || "",
|
||||||
layer: +(ex.layer||0)
|
layer: +(ex.layer||0),
|
||||||
|
requires_boss_dead: !!ex.requires_boss_dead
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// editor-native exit
|
// editor-native exit
|
||||||
@@ -520,7 +523,8 @@
|
|||||||
spawn_id: ex.spawn_id || "",
|
spawn_id: ex.spawn_id || "",
|
||||||
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
spawn: Array.isArray(ex.spawn) ? [ +ex.spawn[0]||0, +ex.spawn[1]||0 ] : null,
|
||||||
hint: ex.hint || "",
|
hint: ex.hint || "",
|
||||||
layer: +(ex.layer||0)
|
layer: +(ex.layer||0),
|
||||||
|
requires_boss_dead: !!ex.requires_boss_dead
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2271,6 +2275,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">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 class="mini">spawn_id</div><input id="exsid" type="text" value="${ex.spawn_id||""}" placeholder="from_room_01"/></div>
|
||||||
</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="muted" style="margin-top:8px;">Tip: use the Linking panel to set these safely.</div>
|
||||||
<div class="row" style="margin-top:8px;">
|
<div class="row" style="margin-top:8px;">
|
||||||
<button id="delSel" class="danger">Delete</button>
|
<button id="delSel" class="danger">Delete</button>
|
||||||
@@ -2581,6 +2588,9 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const exto=q("exto"); if(exto) exto.addEventListener("change", ()=>pushAnd(()=>{ ex.to = exto.value.trim(); }));
|
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 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 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(()=>{
|
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
||||||
room.exits.splice(sel.index,1);
|
room.exits.splice(sel.index,1);
|
||||||
state.selection=null;
|
state.selection=null;
|
||||||
@@ -3451,7 +3461,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
return out;
|
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 exits = (d.exits || []).map(ex => {
|
||||||
const out = {
|
const out = {
|
||||||
rect: [ex.x, ex.y, ex.w, ex.h],
|
rect: [ex.x, ex.y, ex.w, ex.h],
|
||||||
@@ -3461,6 +3471,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
};
|
};
|
||||||
if(ex.spawn_id) out.spawn_id = ex.spawn_id;
|
if(ex.spawn_id) out.spawn_id = ex.spawn_id;
|
||||||
if(ex.layer) out.layer = ex.layer;
|
if(ex.layer) out.layer = ex.layer;
|
||||||
|
if(ex.requires_boss_dead) out.requires_boss_dead = true;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+145
-43
@@ -1499,6 +1499,28 @@ BOSS_TOKEN_SPRITE_RAW = _load_first_existing([
|
|||||||
OBJECTS_DIR / "Boss_Token.png",
|
OBJECTS_DIR / "Boss_Token.png",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
BOSS_TOKEN_SPRITE_CACHE: dict[str, pygame.Surface | None] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def boss_token_sprite(boss_id) -> pygame.Surface | None:
|
||||||
|
"""Per-boss token sprite. Uses ``token_sprite`` from BOSS_DEFS_DEFAULT when
|
||||||
|
set, else ``<boss_id>_token.png``; falls back to the shared token sprite."""
|
||||||
|
key = str(boss_id or "")
|
||||||
|
if key in BOSS_TOKEN_SPRITE_CACHE:
|
||||||
|
return BOSS_TOKEN_SPRITE_CACHE[key]
|
||||||
|
bd = BOSS_DEFS_DEFAULT.get(key, {}) if key else {}
|
||||||
|
candidates: list[Path] = []
|
||||||
|
if isinstance(bd, dict) and bd.get("token_sprite"):
|
||||||
|
candidates.append(TOKENS_DIR / str(bd["token_sprite"]))
|
||||||
|
if key:
|
||||||
|
candidates.append(TOKENS_DIR / f"{key}_token.png")
|
||||||
|
img = _load_first_existing(candidates) if candidates else None
|
||||||
|
if img is None:
|
||||||
|
img = BOSS_TOKEN_SPRITE_RAW
|
||||||
|
BOSS_TOKEN_SPRITE_CACHE[key] = img
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
# Flying boss projectile sprite (faces RIGHT by default; flipped when fired left)
|
# Flying boss projectile sprite (faces RIGHT by default; flipped when fired left)
|
||||||
ENEMY_SKILLS_DIR = DATA_ROOT / "sprites" / "enemy_skills"
|
ENEMY_SKILLS_DIR = DATA_ROOT / "sprites" / "enemy_skills"
|
||||||
BOSS_PROJECTILE_RAW = _load_first_existing([
|
BOSS_PROJECTILE_RAW = _load_first_existing([
|
||||||
@@ -1569,12 +1591,13 @@ def roll_from_table(table: dict) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
class Shockwave:
|
class Shockwave:
|
||||||
def __init__(self, x, y, direction: int, speed: float, lifetime: float, height: int, damage: int):
|
def __init__(self, x, y, direction: int, speed: float, lifetime: float, height: int, damage: int, poison: bool = True):
|
||||||
self.rect = pygame.Rect(int(x), int(y), 26, int(height))
|
self.rect = pygame.Rect(int(x), int(y), 26, int(height))
|
||||||
self.dir = 1 if direction >= 0 else -1
|
self.dir = 1 if direction >= 0 else -1
|
||||||
self.speed = float(speed)
|
self.speed = float(speed)
|
||||||
self.t = float(lifetime)
|
self.t = float(lifetime)
|
||||||
self.damage = int(damage)
|
self.damage = int(damage)
|
||||||
|
self.poison = bool(poison)
|
||||||
|
|
||||||
def update(self, dt: float, platforms: list[pygame.Rect]):
|
def update(self, dt: float, platforms: list[pygame.Rect]):
|
||||||
self.rect.x += int(self.dir * self.speed * dt)
|
self.rect.x += int(self.dir * self.speed * dt)
|
||||||
@@ -2105,16 +2128,16 @@ class Corpse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def spawn_boss_shockwave(boss, shockwaves_list: list[Shockwave]):
|
def spawn_boss_shockwave(boss, shockwaves_list: list[Shockwave], poison: bool = True):
|
||||||
y = boss.rect.bottom - boss.shockwave_height
|
y = boss.rect.bottom - boss.shockwave_height
|
||||||
x = boss.rect.centerx
|
x = boss.rect.centerx
|
||||||
shockwaves_list.append(Shockwave(
|
shockwaves_list.append(Shockwave(
|
||||||
x, y, -1,
|
x, y, -1,
|
||||||
boss.shockwave_speed, boss.shockwave_lifetime, boss.shockwave_height, boss.shockwave_damage
|
boss.shockwave_speed, boss.shockwave_lifetime, boss.shockwave_height, boss.shockwave_damage, poison
|
||||||
))
|
))
|
||||||
shockwaves_list.append(Shockwave(
|
shockwaves_list.append(Shockwave(
|
||||||
x, y, 1,
|
x, y, 1,
|
||||||
boss.shockwave_speed, boss.shockwave_lifetime, boss.shockwave_height, boss.shockwave_damage
|
boss.shockwave_speed, boss.shockwave_lifetime, boss.shockwave_height, boss.shockwave_damage, poison
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
@@ -2780,20 +2803,23 @@ class InventoryUI:
|
|||||||
|
|
||||||
self.slot_icons = load_slot_icons()
|
self.slot_icons = load_slot_icons()
|
||||||
self.slot_icon_cache = {} # (slot, rarity) -> Surface
|
self.slot_icon_cache = {} # (slot, rarity) -> Surface
|
||||||
self.token_icon = None # lazily cached scaled boss-token icon
|
self.token_icons = {} # boss_id -> lazily cached scaled boss-token icon
|
||||||
self.icon_size = int(_ui(["inventory_ui", "icons", "size"], 16))
|
self.icon_size = int(_ui(["inventory_ui", "icons", "size"], 16))
|
||||||
self.icon_gap = int(_ui(["inventory_ui", "icons", "gap"], 6))
|
self.icon_gap = int(_ui(["inventory_ui", "icons", "gap"], 6))
|
||||||
|
|
||||||
|
|
||||||
def _get_token_icon(self) -> pygame.Surface | None:
|
def _get_token_icon(self, boss_id=None) -> pygame.Surface | None:
|
||||||
if self.token_icon is None:
|
key = str(boss_id or "")
|
||||||
if BOSS_TOKEN_SPRITE_RAW is not None:
|
if key not in self.token_icons:
|
||||||
|
raw = boss_token_sprite(boss_id)
|
||||||
|
icon = None
|
||||||
|
if raw is not None:
|
||||||
try:
|
try:
|
||||||
self.token_icon = pygame.transform.smoothscale(
|
icon = pygame.transform.smoothscale(raw, (self.icon_size, self.icon_size))
|
||||||
BOSS_TOKEN_SPRITE_RAW, (self.icon_size, self.icon_size))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.token_icon = None
|
icon = None
|
||||||
return self.token_icon
|
self.token_icons[key] = icon
|
||||||
|
return self.token_icons[key]
|
||||||
|
|
||||||
|
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
@@ -3111,7 +3137,8 @@ class InventoryUI:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self._blit(surface, f"Inventory (I to close) — {room_name}",
|
title = f"Inventory (I to close) — {room_name}"
|
||||||
|
self._blit(surface, self._ellipsize(title, self.w - self.pad * 2),
|
||||||
self.x + self.pad, self.y + self.pad, (240, 240, 240))
|
self.x + self.pad, self.y + self.pad, (240, 240, 240))
|
||||||
|
|
||||||
# --- Detail areas (reserve bottom space FIRST) ---
|
# --- Detail areas (reserve bottom space FIRST) ---
|
||||||
@@ -3181,7 +3208,7 @@ class InventoryUI:
|
|||||||
# --- slot icon (tinted by rarity), or token icon for boss tokens ---
|
# --- slot icon (tinted by rarity), or token icon for boss tokens ---
|
||||||
icon = None
|
icon = None
|
||||||
if it.get("item_type") == "boss_token":
|
if it.get("item_type") == "boss_token":
|
||||||
icon = self._get_token_icon()
|
icon = self._get_token_icon(it.get("boss_id"))
|
||||||
if icon is None:
|
if icon is None:
|
||||||
icon = self._get_slot_icon(slot, rarity)
|
icon = self._get_slot_icon(slot, rarity)
|
||||||
icon_y = y + (self.line_h - self.icon_size) // 2
|
icon_y = y + (self.line_h - self.icon_size) // 2
|
||||||
@@ -3227,13 +3254,16 @@ class InventoryUI:
|
|||||||
surface.blit(icon, (eq_x, icon_y))
|
surface.blit(icon, (eq_x, icon_y))
|
||||||
|
|
||||||
text_x = eq_x + (self.icon_size + self.icon_gap)
|
text_x = eq_x + (self.icon_size + self.icon_gap)
|
||||||
|
avail = (self.x + self.w - self.pad) - text_x
|
||||||
|
|
||||||
if not it:
|
if not it:
|
||||||
self._blit(surface, f"{slot}: (empty)", text_x, eq_y, (170, 170, 170))
|
self._blit(surface, self._ellipsize(f"{slot}: (empty)", max(0, avail)),
|
||||||
|
text_x, eq_y, (170, 170, 170))
|
||||||
else:
|
else:
|
||||||
name = it.get("name", "?")
|
name = it.get("name", "?")
|
||||||
left = f"{slot}: {name} "
|
|
||||||
tag = f"[{rarity}]" if rarity else ""
|
tag = f"[{rarity}]" if rarity else ""
|
||||||
|
tag_w = self.font.size(tag)[0] if tag else 0
|
||||||
|
left = self._ellipsize(f"{slot}: {name} ", max(0, avail - tag_w))
|
||||||
self._blit(surface, left, text_x, eq_y, (230, 230, 230))
|
self._blit(surface, left, text_x, eq_y, (230, 230, 230))
|
||||||
if tag:
|
if tag:
|
||||||
self._blit(surface, tag, text_x + self.font.size(left)[0], eq_y, self._rarity_color(rarity))
|
self._blit(surface, tag, text_x + self.font.size(left)[0], eq_y, self._rarity_color(rarity))
|
||||||
@@ -3290,7 +3320,10 @@ class InventoryUI:
|
|||||||
# Hint text (NOW hint_y exists)
|
# Hint text (NOW hint_y exists)
|
||||||
self._blit(
|
self._blit(
|
||||||
surface,
|
surface,
|
||||||
|
self._ellipsize(
|
||||||
"UP/DOWN move ENTER equip / unequip LEFT/RIGHT switch panel",
|
"UP/DOWN move ENTER equip / unequip LEFT/RIGHT switch panel",
|
||||||
|
self.w - self.pad * 2 - 6,
|
||||||
|
),
|
||||||
self.x + self.pad + 6,
|
self.x + self.pad + 6,
|
||||||
hint_y + 4,
|
hint_y + 4,
|
||||||
(160, 160, 160),
|
(160, 160, 160),
|
||||||
@@ -3328,12 +3361,15 @@ class InventoryUI:
|
|||||||
|
|
||||||
name = selected_item.get("name", selected_item.get("id", "<?>"))
|
name = selected_item.get("name", selected_item.get("id", "<?>"))
|
||||||
rarity = selected_item.get("rarity", "?")
|
rarity = selected_item.get("rarity", "?")
|
||||||
self._blit(surface, f"{name} ", self.x + self.pad + 6, stats_y + 24, (230, 230, 230))
|
name_x = self.x + self.pad + 6
|
||||||
if rarity:
|
stats_avail = (self.x + self.w - self.pad) - name_x
|
||||||
self._blit(surface, f"[{rarity}]",
|
tag = f"[{rarity}]" if rarity else ""
|
||||||
self.x + self.pad + 6 + self.font.size(f"{name} ")[0],
|
tag_w = self.font.size(tag)[0] if tag else 0
|
||||||
stats_y + 24,
|
name_fit = self._ellipsize(f"{name} ", max(0, stats_avail - tag_w))
|
||||||
self._rarity_color(rarity))
|
self._blit(surface, name_fit, name_x, stats_y + 24, (230, 230, 230))
|
||||||
|
if tag:
|
||||||
|
self._blit(surface, tag, name_x + self.font.size(name_fit)[0],
|
||||||
|
stats_y + 24, self._rarity_color(rarity))
|
||||||
|
|
||||||
|
|
||||||
compare_item = None
|
compare_item = None
|
||||||
@@ -3386,10 +3422,11 @@ class InventoryUI:
|
|||||||
if bonus_lines:
|
if bonus_lines:
|
||||||
y_bonus = y_stats if not had_stats else (y_stats + 2)
|
y_bonus = y_stats if not had_stats else (y_stats + 2)
|
||||||
max_y = stats_bottom
|
max_y = stats_bottom
|
||||||
|
bonus_avail = (self.x + self.w - self.pad) - stats_x
|
||||||
for line in bonus_lines[:2]:
|
for line in bonus_lines[:2]:
|
||||||
if y_bonus > max_y:
|
if y_bonus > max_y:
|
||||||
break
|
break
|
||||||
self._blit(surface, line, stats_x, y_bonus, (200, 200, 200))
|
self._blit(surface, self._ellipsize(line, max(0, bonus_avail)), stats_x, y_bonus, (200, 200, 200))
|
||||||
y_bonus += 16
|
y_bonus += 16
|
||||||
# --- Scroll hint + tiny scrollbar ---
|
# --- Scroll hint + tiny scrollbar ---
|
||||||
total = len(self.inv.items)
|
total = len(self.inv.items)
|
||||||
@@ -4628,6 +4665,9 @@ class Enemy:
|
|||||||
self.flyer_shot_speed = float(flyer_cfg.get("shot_speed", 300))
|
self.flyer_shot_speed = float(flyer_cfg.get("shot_speed", 300))
|
||||||
self.flyer_shot_lifetime = float(flyer_cfg.get("shot_lifetime", 2.2))
|
self.flyer_shot_lifetime = float(flyer_cfg.get("shot_lifetime", 2.2))
|
||||||
self.flyer_shot_homing = float(flyer_cfg.get("shot_homing", 1.8))
|
self.flyer_shot_homing = float(flyer_cfg.get("shot_homing", 1.8))
|
||||||
|
self.flyer_startup_delay = float(flyer_cfg.get("startup_delay", 1.5))
|
||||||
|
self.flyer_phase2_hp_pct = float(flyer_cfg.get("phase2_hp_pct", 0.30))
|
||||||
|
self.flyer_hard_phase3_hp_pct = float(flyer_cfg.get("hard_phase3_hp_pct", 0.20))
|
||||||
self.flyer_state = "hover" # hover | diving | grounded
|
self.flyer_state = "hover" # hover | diving | grounded
|
||||||
self.flyer_grounded_timer = 0.0
|
self.flyer_grounded_timer = 0.0
|
||||||
self.flyer_bob_t = random.random() * 10.0
|
self.flyer_bob_t = random.random() * 10.0
|
||||||
@@ -4636,6 +4676,9 @@ class Enemy:
|
|||||||
self.flyer_summon_cd = 0.0
|
self.flyer_summon_cd = 0.0
|
||||||
self.flyer_aoe_cd = 0.0
|
self.flyer_aoe_cd = 0.0
|
||||||
self.flyer_shot_cd = 0.0
|
self.flyer_shot_cd = 0.0
|
||||||
|
self.flyer_startup_timer = self.flyer_startup_delay
|
||||||
|
self.flyer_cycle_casts_left = 0
|
||||||
|
self.flyer_hard = False
|
||||||
|
|
||||||
# --- summoned boss minion: expires after a short life, then heals the
|
# --- summoned boss minion: expires after a short life, then heals the
|
||||||
# boss; killing it early damages the boss. Color fades toward white. ---
|
# boss; killing it early damages the boss. Color fades toward white. ---
|
||||||
@@ -4657,6 +4700,10 @@ class Enemy:
|
|||||||
self.last_chase_dir = self.direction
|
self.last_chase_dir = self.direction
|
||||||
self.aggro_drop_timer = 0.0
|
self.aggro_drop_timer = 0.0
|
||||||
|
|
||||||
|
# Bosses pull aggro once (first light-form entry into their range) and
|
||||||
|
# then track the player permanently, ignoring range and shade form.
|
||||||
|
self.boss_engaged = False
|
||||||
|
|
||||||
# When aggro breaks near an edge: keep walking off naturally, then fall
|
# When aggro breaks near an edge: keep walking off naturally, then fall
|
||||||
self.stepoff_timer = 0.0
|
self.stepoff_timer = 0.0
|
||||||
self.stepoff_dir = self.direction
|
self.stepoff_dir = self.direction
|
||||||
@@ -4703,6 +4750,8 @@ class Enemy:
|
|||||||
self.flyer_summon_cd = 0.0
|
self.flyer_summon_cd = 0.0
|
||||||
self.flyer_aoe_cd = 0.0
|
self.flyer_aoe_cd = 0.0
|
||||||
self.flyer_shot_cd = 0.0
|
self.flyer_shot_cd = 0.0
|
||||||
|
self.flyer_startup_timer = self.flyer_startup_delay
|
||||||
|
self.flyer_cycle_casts_left = 0
|
||||||
|
|
||||||
self.turn_cooldown = 0.0
|
self.turn_cooldown = 0.0
|
||||||
self.last_x = self.rect.x
|
self.last_x = self.rect.x
|
||||||
@@ -4951,6 +5000,9 @@ class Enemy:
|
|||||||
self.slow_mult = 1.0
|
self.slow_mult = 1.0
|
||||||
self._behavior_tick(dt)
|
self._behavior_tick(dt)
|
||||||
|
|
||||||
|
if self.flyer_startup_timer > 0.0:
|
||||||
|
self.flyer_startup_timer = max(0.0, self.flyer_startup_timer - dt)
|
||||||
|
|
||||||
stunned = self.stun_timer > 0.0
|
stunned = self.stun_timer > 0.0
|
||||||
if self.stun_timer > 0.0:
|
if self.stun_timer > 0.0:
|
||||||
self.stun_timer = max(0.0, self.stun_timer - dt)
|
self.stun_timer = max(0.0, self.stun_timer - dt)
|
||||||
@@ -4958,6 +5010,11 @@ class Enemy:
|
|||||||
dx = player.rect.centerx - self.rect.centerx
|
dx = player.rect.centerx - self.rect.centerx
|
||||||
dy = player.rect.centery - self.rect.centery
|
dy = player.rect.centery - self.rect.centery
|
||||||
distance = math.hypot(dx, dy)
|
distance = math.hypot(dx, dy)
|
||||||
|
if self.drop_profile == "boss":
|
||||||
|
if (not self.boss_engaged) and (not player.shade) and distance <= self.detection_range:
|
||||||
|
self.boss_engaged = True
|
||||||
|
chasing = self.boss_engaged
|
||||||
|
else:
|
||||||
chasing = (not player.shade) and (distance <= self.detection_range)
|
chasing = (not player.shade) and (distance <= self.detection_range)
|
||||||
|
|
||||||
feared = self.fear_timer > 0.0
|
feared = self.fear_timer > 0.0
|
||||||
@@ -5040,7 +5097,7 @@ class Enemy:
|
|||||||
self.flyer_state = "grounded"
|
self.flyer_state = "grounded"
|
||||||
self.flyer_grounded_timer = self.flyer_ground_time
|
self.flyer_grounded_timer = self.flyer_ground_time
|
||||||
self.flyer_last_ground_top = p.top
|
self.flyer_last_ground_top = p.top
|
||||||
spawn_boss_shockwave(self, shockwaves_list)
|
spawn_boss_shockwave(self, shockwaves_list, poison=False)
|
||||||
break
|
break
|
||||||
|
|
||||||
def _flyer_update_grounded(self, dt, platforms, player, speed, feared):
|
def _flyer_update_grounded(self, dt, platforms, player, speed, feared):
|
||||||
@@ -5093,25 +5150,49 @@ class Enemy:
|
|||||||
self.vel_y = 0.0
|
self.vel_y = 0.0
|
||||||
self.vel_x = 0.0
|
self.vel_x = 0.0
|
||||||
|
|
||||||
|
def _flyer_max_casts(self) -> int:
|
||||||
|
"""Number of ranged skills the boss may cast in the upcoming cycle.
|
||||||
|
Normal: 1, or 2 at/below phase2_hp_pct.
|
||||||
|
Hard: 2 (starts as if already in phase 2), or all 3 at/below hard_phase3_hp_pct."""
|
||||||
|
frac = (self.health / self.max_health) if self.max_health > 0 else 0.0
|
||||||
|
if getattr(self, "flyer_hard", False):
|
||||||
|
return 3 if frac <= self.flyer_hard_phase3_hp_pct else 2
|
||||||
|
return 2 if frac <= self.flyer_phase2_hp_pct else 1
|
||||||
|
|
||||||
def _flyer_cast_skills(self, dt, player, projectiles_list, enemies_list, aoes_list, platforms):
|
def _flyer_cast_skills(self, dt, player, projectiles_list, enemies_list, aoes_list, platforms):
|
||||||
# Slam takes priority whenever the player is below the boss.
|
# Give the boss a calm intro instead of opening with a slam.
|
||||||
if self.flyer_slam_cd <= 0.0 and player.rect.centery > self.rect.centery + 20:
|
if self.flyer_startup_timer > 0.0:
|
||||||
self.flyer_state = "diving"
|
|
||||||
self.vel_y = 6.0
|
|
||||||
self.flyer_slam_cd = self.flyer_slam_cooldown
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Otherwise pick from the ranged kit (shuffle so it isn't a fixed order).
|
# Start a new attack cycle whenever the slam comes off cooldown. Each
|
||||||
|
# cycle the boss casts a limited number of ranged skills (see
|
||||||
|
# _flyer_max_casts); it never chains more than that.
|
||||||
|
if self.flyer_slam_cd <= 0.0:
|
||||||
|
self.flyer_cycle_casts_left = self._flyer_max_casts()
|
||||||
|
self.flyer_slam_cd = self.flyer_slam_cooldown
|
||||||
|
# Slam takes priority whenever the player is below the boss.
|
||||||
|
if player.rect.centery > self.rect.centery + 20:
|
||||||
|
self.flyer_state = "diving"
|
||||||
|
self.vel_y = 6.0
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.flyer_cycle_casts_left <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
casts = 0
|
||||||
for skill in random.sample(("shot", "summon", "aoe"), 3):
|
for skill in random.sample(("shot", "summon", "aoe"), 3):
|
||||||
|
if casts >= self.flyer_cycle_casts_left:
|
||||||
|
break
|
||||||
if skill == "shot" and self.flyer_shot_cd <= 0.0:
|
if skill == "shot" and self.flyer_shot_cd <= 0.0:
|
||||||
self._flyer_cast_shot(player, projectiles_list)
|
self._flyer_cast_shot(player, projectiles_list)
|
||||||
return
|
casts += 1
|
||||||
if skill == "summon" and self.flyer_summon_cd <= 0.0:
|
elif skill == "summon" and self.flyer_summon_cd <= 0.0:
|
||||||
if self._flyer_cast_summon(enemies_list, platforms):
|
if self._flyer_cast_summon(enemies_list, platforms):
|
||||||
return
|
casts += 1
|
||||||
if skill == "aoe" and self.flyer_aoe_cd <= 0.0:
|
elif skill == "aoe" and self.flyer_aoe_cd <= 0.0:
|
||||||
self._flyer_cast_aoe(aoes_list)
|
self._flyer_cast_aoe(aoes_list)
|
||||||
return
|
casts += 1
|
||||||
|
self.flyer_cycle_casts_left -= casts
|
||||||
|
|
||||||
def _flyer_cast_shot(self, player, projectiles_list):
|
def _flyer_cast_shot(self, player, projectiles_list):
|
||||||
sx = self.rect.centerx
|
sx = self.rect.centerx
|
||||||
@@ -5176,8 +5257,12 @@ class Enemy:
|
|||||||
|
|
||||||
# Flying boss: completely replaces the ground-based AI body.
|
# Flying boss: completely replaces the ground-based AI body.
|
||||||
if self.behavior == "flyer":
|
if self.behavior == "flyer":
|
||||||
self._flyer_update(dt, platforms, player, shockwaves_list,
|
self._flyer_update(
|
||||||
projectiles_list or [], enemies_list or [], aoes_list or [])
|
dt, platforms, player, shockwaves_list,
|
||||||
|
projectiles_list if projectiles_list is not None else [],
|
||||||
|
enemies_list if enemies_list is not None else [],
|
||||||
|
aoes_list if aoes_list is not None else [],
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.jump_cd_timer > 0:
|
if self.jump_cd_timer > 0:
|
||||||
@@ -5216,6 +5301,11 @@ class Enemy:
|
|||||||
dx = player.rect.centerx - self.rect.centerx
|
dx = player.rect.centerx - self.rect.centerx
|
||||||
dy = player.rect.centery - self.rect.centery
|
dy = player.rect.centery - self.rect.centery
|
||||||
distance = math.hypot(dx, dy)
|
distance = math.hypot(dx, dy)
|
||||||
|
if self.drop_profile == "boss":
|
||||||
|
if (not self.boss_engaged) and (not player.shade) and distance <= self.detection_range:
|
||||||
|
self.boss_engaged = True
|
||||||
|
chasing = self.boss_engaged
|
||||||
|
else:
|
||||||
chasing = (not player.shade) and (distance <= self.detection_range)
|
chasing = (not player.shade) and (distance <= self.detection_range)
|
||||||
|
|
||||||
feared = self.fear_timer > 0.0
|
feared = self.fear_timer > 0.0
|
||||||
@@ -7232,6 +7322,14 @@ BOSS_DEFS_DEFAULT = {
|
|||||||
"room": "room_boss_01.json",
|
"room": "room_boss_01.json",
|
||||||
"refight_room": "room_boss_refight_01.json",
|
"refight_room": "room_boss_refight_01.json",
|
||||||
"token_name": "Gloom Warden's Token",
|
"token_name": "Gloom Warden's Token",
|
||||||
|
"token_sprite": "Boss1_token.png",
|
||||||
|
},
|
||||||
|
"boss_02": {
|
||||||
|
"name": "Sky Terror",
|
||||||
|
"room": "room_08.json",
|
||||||
|
"refight_room": "room_boss_fly_refight.json",
|
||||||
|
"token_name": "Sky Terror's Token",
|
||||||
|
"token_sprite": "Boss_fly_token.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7320,6 +7418,8 @@ def start_refight(boss_id: str, variant: str = "normal") -> bool:
|
|||||||
en.dash_speed_mult *= 1.5
|
en.dash_speed_mult *= 1.5
|
||||||
en.retreat_speed_mult *= 1.5
|
en.retreat_speed_mult *= 1.5
|
||||||
en.attack_cooldown = max(0.6, en.attack_cooldown * 0.75)
|
en.attack_cooldown = max(0.6, en.attack_cooldown * 0.75)
|
||||||
|
elif getattr(en, "behavior", "") == "flyer":
|
||||||
|
en.flyer_hard = True
|
||||||
else:
|
else:
|
||||||
en.shockwave_speed *= 2.0
|
en.shockwave_speed *= 2.0
|
||||||
en.slam_cooldown_base = max(0.25, en.slam_cooldown_base / 2.0)
|
en.slam_cooldown_base = max(0.25, en.slam_cooldown_base / 2.0)
|
||||||
@@ -7363,8 +7463,9 @@ class TokenPickup:
|
|||||||
bob_y = int(math.sin(self.bob_t) * LOOT_BOB_AMP) if self.on_ground else 0
|
bob_y = int(math.sin(self.bob_t) * LOOT_BOB_AMP) if self.on_ground else 0
|
||||||
x = int(self.rect.x - cx)
|
x = int(self.rect.x - cx)
|
||||||
y = int(self.rect.y - cy + bob_y)
|
y = int(self.rect.y - cy + bob_y)
|
||||||
if BOSS_TOKEN_SPRITE_RAW is not None:
|
raw = boss_token_sprite(self.boss_id)
|
||||||
img = pygame.transform.scale(BOSS_TOKEN_SPRITE_RAW, (self.rect.width, self.rect.height))
|
if raw is not None:
|
||||||
|
img = pygame.transform.scale(raw, (self.rect.width, self.rect.height))
|
||||||
surface.blit(img, (x, y))
|
surface.blit(img, (x, y))
|
||||||
return
|
return
|
||||||
pygame.draw.circle(surface, (60, 200, 190), (x + 8, y + 8), 7)
|
pygame.draw.circle(surface, (60, 200, 190), (x + 8, y + 8), 7)
|
||||||
@@ -7618,10 +7719,10 @@ class DiaryUI:
|
|||||||
icon_x = cell.centerx - 16
|
icon_x = cell.centerx - 16
|
||||||
icon_y = cell.y + 8
|
icon_y = cell.y + 8
|
||||||
has_token = (tier >= 0 or carried is not None)
|
has_token = (tier >= 0 or carried is not None)
|
||||||
if BOSS_TOKEN_SPRITE_RAW is not None and has_token:
|
raw_token = boss_token_sprite(boss_id)
|
||||||
icon = BOSS_TOKEN_SPRITE_RAW
|
if raw_token is not None and has_token:
|
||||||
|
icon = pygame.transform.scale(raw_token, (32, 32))
|
||||||
if tier < 0:
|
if tier < 0:
|
||||||
icon = icon.copy()
|
|
||||||
icon.set_alpha(120)
|
icon.set_alpha(120)
|
||||||
surface.blit(icon, (icon_x, icon_y))
|
surface.blit(icon, (icon_x, icon_y))
|
||||||
if tier >= 1:
|
if tier >= 1:
|
||||||
@@ -8920,7 +9021,7 @@ while running:
|
|||||||
# Update enemies
|
# Update enemies
|
||||||
for en in enemies:
|
for en in enemies:
|
||||||
en.update(dt, enemy_collision_platforms, ground_platforms, player, shockwaves,
|
en.update(dt, enemy_collision_platforms, ground_platforms, player, shockwaves,
|
||||||
projectiles, enemies, boss_aoes)
|
boss_projectiles, enemies, boss_aoes)
|
||||||
|
|
||||||
# Enemies that touch a hazard zone die instantly, dropping nothing.
|
# Enemies that touch a hazard zone die instantly, dropping nothing.
|
||||||
for en in enemies:
|
for en in enemies:
|
||||||
@@ -9029,6 +9130,7 @@ while running:
|
|||||||
did_hit = player.take_damage(sw.damage)
|
did_hit = player.take_damage(sw.damage)
|
||||||
if did_hit:
|
if did_hit:
|
||||||
player.apply_knockback(sw.rect, strength_px_per_frame=14.0, duration=0.22, pop_up_vel=-4.5)
|
player.apply_knockback(sw.rect, strength_px_per_frame=14.0, duration=0.22, pop_up_vel=-4.5)
|
||||||
|
if sw.poison:
|
||||||
player.apply_poison(BOSS_POISON_DURATION)
|
player.apply_poison(BOSS_POISON_DURATION)
|
||||||
sw.t = 0 # one-hit
|
sw.t = 0 # one-hit
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"saved_at": "2026-10-01T14:40:38",
|
"saved_at": "2026-10-05T09:57:55",
|
||||||
"player": {
|
"player": {
|
||||||
"room_id": "room_07.json",
|
"room_id": "room_07.json",
|
||||||
"x": 620,
|
"x": 680,
|
||||||
"y": 520,
|
"y": 520,
|
||||||
"health": 10,
|
"health": 10,
|
||||||
"charge": 120.0,
|
"charge": 120.0,
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"coins_light": 40,
|
"coins_light": 40,
|
||||||
"coins_dark": 20,
|
"coins_dark": 20,
|
||||||
"butterfly_jars": 0,
|
"butterfly_jars": 0,
|
||||||
"has_ranged": false,
|
"has_ranged": true,
|
||||||
"opened_chests": [
|
"opened_chests": [
|
||||||
"room_03.json@90,194",
|
"room_03.json@90,194",
|
||||||
"room_07.json@440,494"
|
"room_07.json@440,494"
|
||||||
|
|||||||
Reference in New Issue
Block a user