flying boss
boss addtion, small editor changes
This commit is contained in:
@@ -37,6 +37,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 +81,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 |
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -40,6 +40,51 @@
|
||||
"attack_cooldown": 1.8,
|
||||
"above_y": 40
|
||||
}
|
||||
},
|
||||
|
||||
"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",
|
||||
"flyer": {
|
||||
"flight_height": 220,
|
||||
"bob_amplitude": 20,
|
||||
"bob_speed": 2.2,
|
||||
"hover_speed": 2.6,
|
||||
"slam_cd": 6.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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 320 B |
@@ -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,9 @@
|
||||
"y": 520,
|
||||
"type": "save_point",
|
||||
"radius": 90,
|
||||
"object_id": "lamp"
|
||||
"object_id": "lamp",
|
||||
"layer": 0,
|
||||
"quest_id": ""
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
@@ -81,6 +95,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,74 @@
|
||||
{
|
||||
"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
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"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
|
||||
]
|
||||
}
|
||||
],
|
||||
"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>
|
||||
|
||||
@@ -288,6 +288,17 @@
|
||||
dash_duration: 0.7, attack_cooldown: 1.8, above_y: 40
|
||||
}
|
||||
},
|
||||
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",
|
||||
flyer: {
|
||||
flight_height: 220, bob_amplitude: 20, bob_speed: 2.2, hover_speed: 2.6,
|
||||
slam_cd: 6.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(){
|
||||
@@ -2317,6 +2328,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>
|
||||
` : ""}
|
||||
@@ -3783,14 +3795,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 +3857,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 +3867,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
|
||||
|
||||
+565
-3
@@ -433,6 +433,9 @@ MINIBOSS_HIT_COLOR = _color(["enemy", "miniboss_hit"])
|
||||
BOSS_COLOR = _color(["enemy", "boss"])
|
||||
BOSS_HIT_COLOR = _color(["enemy", "boss_hit"])
|
||||
|
||||
FLYER_BOSS_COLOR = _color(["enemy", "boss_flyer"], (150, 90, 220))
|
||||
FLYER_BOSS_HIT_COLOR = _color(["enemy", "boss_flyer_hit"], (210, 170, 255))
|
||||
|
||||
PLAYER_HIT_COLOR = (255, 255, 0)
|
||||
|
||||
HEAL_DARK = _color(["healing", "dark"])
|
||||
@@ -907,6 +910,49 @@ ENEMY_TYPES_DEFAULT = {
|
||||
"above_retreat": True
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"flyer": {
|
||||
"flight_height": 220,
|
||||
"bob_amplitude": 20,
|
||||
"bob_speed": 2.2,
|
||||
"hover_speed": 2.6,
|
||||
"slam_cd": 6.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"
|
||||
},
|
||||
}
|
||||
|
||||
ENEMY_DROPS_DEFAULT = {
|
||||
@@ -1453,6 +1499,15 @@ BOSS_TOKEN_SPRITE_RAW = _load_first_existing([
|
||||
OBJECTS_DIR / "Boss_Token.png",
|
||||
])
|
||||
|
||||
# Flying boss projectile sprite (faces RIGHT by default; flipped when fired left)
|
||||
ENEMY_SKILLS_DIR = DATA_ROOT / "sprites" / "enemy_skills"
|
||||
BOSS_PROJECTILE_RAW = _load_first_existing([
|
||||
ENEMY_SKILLS_DIR / "boss_ranged.png",
|
||||
ENEMY_SKILLS_DIR / "boss_ranged",
|
||||
ENEMY_SKILLS_DIR / "Boss_ranged.png",
|
||||
])
|
||||
BOSS_PROJECTILE_RAW_LEFT = pygame.transform.flip(BOSS_PROJECTILE_RAW, True, False) if BOSS_PROJECTILE_RAW is not None else None
|
||||
|
||||
def _scale_icon(img: pygame.Surface | None, size: int) -> pygame.Surface | None:
|
||||
if img is None:
|
||||
return None
|
||||
@@ -1628,6 +1683,123 @@ class Projectile:
|
||||
)
|
||||
|
||||
|
||||
class BossProjectile:
|
||||
"""Homing projectile fired by the flying boss. Slightly curves toward the
|
||||
player each frame. Uses boss_ranged.png (faces right; flipped when fired
|
||||
to the left)."""
|
||||
def __init__(self, x, y, dx, dy, damage, speed, lifetime, homing, player):
|
||||
self.rect = pygame.Rect(0, 0, 26, 26)
|
||||
self.rect.center = (int(x), int(y))
|
||||
self.dir_x = float(dx)
|
||||
self.dir_y = float(dy)
|
||||
d = math.hypot(self.dir_x, self.dir_y)
|
||||
if d > 0.0001:
|
||||
self.dir_x /= d
|
||||
self.dir_y /= d
|
||||
self.speed = float(speed)
|
||||
self.t = float(lifetime)
|
||||
self.damage = float(damage)
|
||||
self.homing = float(homing)
|
||||
self.player = player
|
||||
|
||||
def update(self, dt: float, platforms: list[pygame.Rect]):
|
||||
# slight homing: steer the direction toward the player
|
||||
px = self.player.rect.centerx - self.rect.centerx
|
||||
py = self.player.rect.centery - self.rect.centery
|
||||
plen = math.hypot(px, py)
|
||||
if plen > 1.0:
|
||||
tx = px / plen
|
||||
ty = py / plen
|
||||
k = min(1.0, self.homing * dt)
|
||||
self.dir_x += (tx - self.dir_x) * k
|
||||
self.dir_y += (ty - self.dir_y) * k
|
||||
d = math.hypot(self.dir_x, self.dir_y)
|
||||
if d > 0.0001:
|
||||
self.dir_x /= d
|
||||
self.dir_y /= d
|
||||
|
||||
self.rect.x += int(self.dir_x * self.speed * dt)
|
||||
self.rect.y += int(self.dir_y * self.speed * dt)
|
||||
self.t -= dt
|
||||
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
self.t = 0
|
||||
break
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
return self.t > 0
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
if BOSS_PROJECTILE_RAW is not None:
|
||||
if self.dir_x >= 0:
|
||||
img = BOSS_PROJECTILE_RAW
|
||||
angle = -math.degrees(math.atan2(self.dir_y, self.dir_x))
|
||||
else:
|
||||
img = BOSS_PROJECTILE_RAW_LEFT
|
||||
angle = math.degrees(math.atan2(self.dir_y, -self.dir_x))
|
||||
img = pygame.transform.rotate(img, angle)
|
||||
surface.blit(img, (self.rect.centerx - img.get_width() // 2 - cx,
|
||||
self.rect.centery - img.get_height() // 2 - cy))
|
||||
return
|
||||
# fallback: glowing bolt
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
(255, 120, 60),
|
||||
pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height),
|
||||
)
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
(0, 0, 0),
|
||||
pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
class BossAoe:
|
||||
"""Huge expanding circular blast: low damage, high knockback, one hit."""
|
||||
def __init__(self, x, y, max_radius, damage, duration,
|
||||
knock_strength=16.0, knock_duration=0.35, pop_up=-6.0):
|
||||
self.center = (int(x), int(y))
|
||||
self.max_radius = int(max_radius)
|
||||
self.radius = 0
|
||||
self.damage = int(damage)
|
||||
self.full_duration = max(0.05, float(duration))
|
||||
self.t = float(duration)
|
||||
self.hit = False
|
||||
self.knock_strength = float(knock_strength)
|
||||
self.knock_duration = float(knock_duration)
|
||||
self.pop_up = float(pop_up)
|
||||
|
||||
def update(self, dt: float):
|
||||
self.t -= dt
|
||||
progress = 1.0 - max(0.0, self.t / self.full_duration)
|
||||
self.radius = int(self.max_radius * progress)
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
return self.t > 0
|
||||
|
||||
def hits(self, rect: pygame.Rect) -> bool:
|
||||
if self.hit:
|
||||
return False
|
||||
cx, cy = self.center
|
||||
nx = max(rect.left, min(cx, rect.right))
|
||||
ny = max(rect.top, min(cy, rect.bottom))
|
||||
return math.hypot(cx - nx, cy - ny) <= self.radius
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
progress = 1.0 - max(0.0, self.t / self.full_duration)
|
||||
alpha = int(110 * (1.0 - progress * 0.65))
|
||||
px = self.center[0] - cx
|
||||
py = self.center[1] - cy
|
||||
ring = pygame.Surface((self.radius * 2 + 6, self.radius * 2 + 6), pygame.SRCALPHA)
|
||||
pygame.draw.circle(ring, (255, 120, 60, alpha), (self.radius + 3, self.radius + 3), self.radius, 4)
|
||||
pygame.draw.circle(ring, (255, 210, 130, alpha), (self.radius + 3, self.radius + 3), self.radius, 1)
|
||||
surface.blit(ring, (px - self.radius - 3, py - self.radius - 3))
|
||||
|
||||
|
||||
class TetherShot:
|
||||
def __init__(self, x, y, direction: int, player_ref: "Player"):
|
||||
w, h = TETHER_SIZE
|
||||
@@ -4430,6 +4602,51 @@ class Enemy:
|
||||
self.charge_timer = 0.0
|
||||
self.attack_cd = 0.0
|
||||
|
||||
# --- Flying boss ("flyer") behavior state ---
|
||||
flyer_cfg = (cfg.get("flyer", {}) if isinstance(cfg, dict) else {})
|
||||
if not isinstance(flyer_cfg, dict):
|
||||
flyer_cfg = {}
|
||||
self.flyer_flight_height = float(flyer_cfg.get("flight_height", 100))
|
||||
self.flyer_bob_amplitude = float(flyer_cfg.get("bob_amplitude", 20))
|
||||
self.flyer_bob_speed = float(flyer_cfg.get("bob_speed", 2.2))
|
||||
self.flyer_hover_speed = float(flyer_cfg.get("hover_speed", 2.6))
|
||||
self.flyer_slam_cooldown = float(flyer_cfg.get("slam_cd", 6.0))
|
||||
self.flyer_dive_vel = float(flyer_cfg.get("slam_dive_vel", 16.0))
|
||||
self.flyer_ground_time = float(flyer_cfg.get("ground_time", 2.0))
|
||||
self.flyer_summon_cooldown = float(flyer_cfg.get("summon_cd", 8.0))
|
||||
self.flyer_summon_max = int(flyer_cfg.get("summon_max", 3))
|
||||
self.flyer_summon_type = str(flyer_cfg.get("summon_type", "boss_minion"))
|
||||
self.flyer_summon_life = float(flyer_cfg.get("summon_life", 6.0))
|
||||
self.flyer_summon_heal = float(flyer_cfg.get("summon_heal", 4))
|
||||
self.flyer_summon_damage = float(flyer_cfg.get("summon_damage", 3))
|
||||
self.flyer_aoe_cooldown = float(flyer_cfg.get("aoe_cd", 9.0))
|
||||
self.flyer_aoe_radius = float(flyer_cfg.get("aoe_radius", 300))
|
||||
self.flyer_aoe_damage = int(flyer_cfg.get("aoe_damage", 1))
|
||||
self.flyer_aoe_duration = float(flyer_cfg.get("aoe_duration", 0.9))
|
||||
self.flyer_shot_cooldown = float(flyer_cfg.get("shot_cd", 5.0))
|
||||
self.flyer_shot_damage = float(flyer_cfg.get("shot_damage", 5))
|
||||
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_homing = float(flyer_cfg.get("shot_homing", 1.8))
|
||||
self.flyer_state = "hover" # hover | diving | grounded
|
||||
self.flyer_grounded_timer = 0.0
|
||||
self.flyer_bob_t = random.random() * 10.0
|
||||
self.flyer_last_ground_top = self.rect.bottom
|
||||
self.flyer_slam_cd = 0.0
|
||||
self.flyer_summon_cd = 0.0
|
||||
self.flyer_aoe_cd = 0.0
|
||||
self.flyer_shot_cd = 0.0
|
||||
|
||||
# --- summoned boss minion: expires after a short life, then heals the
|
||||
# boss; killing it early damages the boss. Color fades toward white. ---
|
||||
self.is_minion = (self.type_id == "boss_minion")
|
||||
self.minion_timer = 0.0
|
||||
self.minion_duration = 0.0
|
||||
self.minion_owner = None
|
||||
self.minion_heal = 0.0
|
||||
self.minion_damage = 0.0
|
||||
self.minion_expired = False
|
||||
|
||||
# --- anti-jiggle helpers ---
|
||||
self.turn_cooldown = 0.0 # prevents rapid flip-flop at edges/walls
|
||||
self.last_x = self.rect.x
|
||||
@@ -4478,6 +4695,15 @@ class Enemy:
|
||||
self.charge_timer = 0.0
|
||||
self.attack_cd = 0.0
|
||||
|
||||
self.flyer_state = "hover"
|
||||
self.flyer_grounded_timer = 0.0
|
||||
self.flyer_bob_t = random.random() * 10.0
|
||||
self.flyer_last_ground_top = self.rect.bottom
|
||||
self.flyer_slam_cd = 0.0
|
||||
self.flyer_summon_cd = 0.0
|
||||
self.flyer_aoe_cd = 0.0
|
||||
self.flyer_shot_cd = 0.0
|
||||
|
||||
self.turn_cooldown = 0.0
|
||||
self.last_x = self.rect.x
|
||||
self.stuck_timer = 0.0
|
||||
@@ -4617,6 +4843,11 @@ class Enemy:
|
||||
elif self.behavior == "charger":
|
||||
if self.attack_cd > 0:
|
||||
self.attack_cd -= dt
|
||||
elif self.behavior == "flyer":
|
||||
for attr in ("flyer_slam_cd", "flyer_summon_cd", "flyer_aoe_cd", "flyer_shot_cd"):
|
||||
v = getattr(self, attr)
|
||||
if v > 0:
|
||||
setattr(self, attr, max(0.0, v - dt))
|
||||
|
||||
def _behavior_update(self, dt, player, distance, dx, stunned):
|
||||
"""Choose/advance an attack per behavior. Runs before gravity/move."""
|
||||
@@ -4700,7 +4931,255 @@ class Enemy:
|
||||
return self.charge_dir * self.speed * self.dash_speed_mult
|
||||
return None
|
||||
|
||||
def update(self, dt, platforms, ground_platforms, player, shockwaves_list: list[Shockwave]):
|
||||
# --- Flying boss ("flyer") behavior ---
|
||||
|
||||
def _flyer_update(self, dt, platforms, player, shockwaves_list,
|
||||
projectiles_list, enemies_list, aoes_list):
|
||||
"""The flying boss hovers in the air (bobbing like wings), and uses its
|
||||
aerial skills: slam when the player is below, ground minion summoning,
|
||||
a huge low-damage/high-knockback AOE, and a slightly-homing shot."""
|
||||
# own timer ticks (the flyer bypasses the shared ground-based update body)
|
||||
if self.hit_timer > 0:
|
||||
self.hit_timer -= dt
|
||||
if self.bind_timer > 0.0:
|
||||
self.bind_timer = max(0.0, self.bind_timer - dt)
|
||||
if self.fear_timer > 0.0:
|
||||
self.fear_timer = max(0.0, self.fear_timer - dt)
|
||||
if self.slow_timer > 0.0:
|
||||
self.slow_timer = max(0.0, self.slow_timer - dt)
|
||||
if self.slow_timer == 0.0:
|
||||
self.slow_mult = 1.0
|
||||
self._behavior_tick(dt)
|
||||
|
||||
stunned = self.stun_timer > 0.0
|
||||
if self.stun_timer > 0.0:
|
||||
self.stun_timer = max(0.0, self.stun_timer - dt)
|
||||
|
||||
dx = player.rect.centerx - self.rect.centerx
|
||||
dy = player.rect.centery - self.rect.centery
|
||||
distance = math.hypot(dx, dy)
|
||||
chasing = (not player.shade) and (distance <= self.detection_range)
|
||||
|
||||
feared = self.fear_timer > 0.0
|
||||
if feared:
|
||||
chasing = False
|
||||
self.direction = -1 if dx > 0 else 1
|
||||
elif chasing and abs(dx) > 8:
|
||||
self.direction = 1 if dx > 0 else -1
|
||||
|
||||
speed = self.flyer_hover_speed
|
||||
if self.slow_timer > 0.0:
|
||||
speed *= self.slow_mult
|
||||
if feared:
|
||||
speed *= TETHER_FEAR_SPEED_MULT
|
||||
|
||||
if self.flyer_state == "diving":
|
||||
self._flyer_update_diving(dt, platforms, speed, shockwaves_list)
|
||||
elif self.flyer_state == "grounded":
|
||||
ground_speed = self.speed * (self.slow_mult if self.slow_timer > 0.0 else 1.0)
|
||||
if feared:
|
||||
ground_speed *= TETHER_FEAR_SPEED_MULT
|
||||
self._flyer_update_grounded(dt, platforms, player, ground_speed, feared)
|
||||
else:
|
||||
self._flyer_update_hover(dt, platforms, player, speed, chasing, feared,
|
||||
stunned, projectiles_list, enemies_list, aoes_list)
|
||||
|
||||
if self.rect.top > (ROOM.kill_y + 400):
|
||||
self.respawn()
|
||||
return
|
||||
self.was_chasing = chasing
|
||||
|
||||
def _flyer_update_hover(self, dt, platforms, player, speed, chasing, feared,
|
||||
stunned, projectiles_list, enemies_list, aoes_list):
|
||||
ground = self.support_below(platforms, 5000)
|
||||
if ground is not None:
|
||||
self.flyer_last_ground_top = ground.top
|
||||
ground_top = getattr(self, "flyer_last_ground_top", self.rect.bottom)
|
||||
|
||||
self.flyer_bob_t += dt * self.flyer_bob_speed
|
||||
bob = math.sin(self.flyer_bob_t) * self.flyer_bob_amplitude
|
||||
target_y = ground_top - self.flyer_flight_height + bob
|
||||
|
||||
if feared:
|
||||
self.vel_x = -self.direction * speed
|
||||
elif chasing and not stunned:
|
||||
self.vel_x = self.direction * speed
|
||||
else:
|
||||
self.vel_x *= 0.9
|
||||
if abs(self.vel_x) < 0.5:
|
||||
self.vel_x = 0.0
|
||||
|
||||
# ease vertically toward the hover target (stable, frame-rate-based like the rest)
|
||||
diff = target_y - self.rect.y
|
||||
self.rect.y += int(diff * min(1.0, 0.12))
|
||||
self.rect.x += int(self.vel_x)
|
||||
|
||||
# room bounds safety net (the flying boss ignores platform walls; it
|
||||
# hovers above the terrain and only needs to stay inside the room)
|
||||
lo = getattr(ROOM, "min_x", None)
|
||||
hi = getattr(ROOM, "max_x", None)
|
||||
if lo is not None:
|
||||
self.rect.left = max(self.rect.left, lo)
|
||||
if hi is not None:
|
||||
self.rect.right = min(self.rect.right, hi)
|
||||
room_h = getattr(ROOM, "room_h", 900)
|
||||
self.rect.y = max(20, min(self.rect.y, room_h - self.rect.height))
|
||||
|
||||
if chasing and (not stunned) and (not feared):
|
||||
self._flyer_cast_skills(dt, player, projectiles_list, enemies_list, aoes_list, platforms)
|
||||
|
||||
def _flyer_update_diving(self, dt, platforms, speed, shockwaves_list):
|
||||
# dive down until we hit a platform, then slam and stay grounded a moment.
|
||||
self.vel_y = min(self.flyer_dive_vel, self.vel_y + 1.0)
|
||||
self.rect.y += int(self.vel_y)
|
||||
self.rect.x += int(self.direction * speed * dt)
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
self.rect.bottom = p.top
|
||||
self.vel_y = 0.0
|
||||
self.flyer_state = "grounded"
|
||||
self.flyer_grounded_timer = self.flyer_ground_time
|
||||
self.flyer_last_ground_top = p.top
|
||||
spawn_boss_shockwave(self, shockwaves_list)
|
||||
break
|
||||
|
||||
def _flyer_update_grounded(self, dt, platforms, player, speed, feared):
|
||||
"""After a slam the boss stays grounded briefly: walks toward the player
|
||||
and can deal contact damage, then lifts back into the air."""
|
||||
self.flyer_grounded_timer -= dt
|
||||
dx = player.rect.centerx - self.rect.centerx
|
||||
if feared:
|
||||
self.direction = -1 if dx > 0 else 1
|
||||
elif abs(dx) > 8:
|
||||
self.direction = 1 if dx > 0 else -1
|
||||
|
||||
# gravity + vertical move/landing
|
||||
self.vel_y = min(24.0, self.vel_y + 0.5)
|
||||
self.rect.y += int(self.vel_y)
|
||||
landed = False
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
if self.vel_y > 0:
|
||||
self.rect.bottom = p.top
|
||||
self.vel_y = 0.0
|
||||
landed = True
|
||||
elif self.vel_y < 0:
|
||||
self.rect.top = p.bottom
|
||||
self.vel_y = 0.0
|
||||
self.on_ground = landed
|
||||
|
||||
# horizontal move + wall collision
|
||||
self.vel_x = self.direction * speed
|
||||
self.rect.x += int(self.vel_x)
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
if self.vel_x > 0:
|
||||
self.rect.right = p.left
|
||||
elif self.vel_x < 0:
|
||||
self.rect.left = p.right
|
||||
self.vel_x = 0.0
|
||||
break
|
||||
|
||||
# room bounds safety net
|
||||
lo = getattr(ROOM, "min_x", None)
|
||||
hi = getattr(ROOM, "max_x", None)
|
||||
if lo is not None:
|
||||
self.rect.left = max(self.rect.left, lo)
|
||||
if hi is not None:
|
||||
self.rect.right = min(self.rect.right, hi)
|
||||
|
||||
if self.flyer_grounded_timer <= 0.0:
|
||||
self.flyer_state = "hover"
|
||||
self.vel_y = 0.0
|
||||
self.vel_x = 0.0
|
||||
|
||||
def _flyer_cast_skills(self, dt, player, projectiles_list, enemies_list, aoes_list, platforms):
|
||||
# Slam takes priority whenever the player is below the boss.
|
||||
if self.flyer_slam_cd <= 0.0 and player.rect.centery > self.rect.centery + 20:
|
||||
self.flyer_state = "diving"
|
||||
self.vel_y = 6.0
|
||||
self.flyer_slam_cd = self.flyer_slam_cooldown
|
||||
return
|
||||
|
||||
# Otherwise pick from the ranged kit (shuffle so it isn't a fixed order).
|
||||
for skill in random.sample(("shot", "summon", "aoe"), 3):
|
||||
if skill == "shot" and self.flyer_shot_cd <= 0.0:
|
||||
self._flyer_cast_shot(player, projectiles_list)
|
||||
return
|
||||
if skill == "summon" and self.flyer_summon_cd <= 0.0:
|
||||
if self._flyer_cast_summon(enemies_list, platforms):
|
||||
return
|
||||
if skill == "aoe" and self.flyer_aoe_cd <= 0.0:
|
||||
self._flyer_cast_aoe(aoes_list)
|
||||
return
|
||||
|
||||
def _flyer_cast_shot(self, player, projectiles_list):
|
||||
sx = self.rect.centerx
|
||||
sy = self.rect.centery
|
||||
tx = player.rect.centerx - sx
|
||||
ty = player.rect.centery - sy
|
||||
length = math.hypot(tx, ty)
|
||||
if length < 1.0:
|
||||
tx, ty, length = 1.0, 0.0, 1.0
|
||||
projectiles_list.append(BossProjectile(
|
||||
sx, sy, tx / length, ty / length,
|
||||
self.flyer_shot_damage, self.flyer_shot_speed,
|
||||
self.flyer_shot_lifetime, self.flyer_shot_homing, player,
|
||||
))
|
||||
self.flyer_shot_cd = self.flyer_shot_cooldown
|
||||
|
||||
def _flyer_cast_summon(self, enemies_list, platforms) -> bool:
|
||||
alive = sum(
|
||||
1 for en in enemies_list
|
||||
if getattr(en, "type_id", "") == self.flyer_summon_type and en.health > 0
|
||||
)
|
||||
if alive >= self.flyer_summon_max:
|
||||
return False
|
||||
ground = self.support_below(platforms, 5000)
|
||||
if ground is not None:
|
||||
gx = self.rect.centerx
|
||||
gy = ground.top
|
||||
else:
|
||||
gx = self.rect.centerx
|
||||
gy = int(getattr(ROOM, "room_h", 900)) - 40
|
||||
s_cfg = ENEMY_TYPES.get(self.flyer_summon_type, ENEMY_TYPES.get("enemy", {}))
|
||||
sw = int(s_cfg.get("rect", [40, 60])[0]) if isinstance(s_cfg, dict) else 40
|
||||
sh = int(s_cfg.get("rect", [40, 60])[1]) if isinstance(s_cfg, dict) else 60
|
||||
minion = Enemy(gx - sw // 2, gy - sh, self.flyer_summon_type)
|
||||
minion.is_minion = True
|
||||
minion.minion_owner = self
|
||||
minion.minion_duration = self.flyer_summon_life
|
||||
minion.minion_timer = self.flyer_summon_life
|
||||
minion.minion_heal = self.flyer_summon_heal
|
||||
minion.minion_damage = self.flyer_summon_damage
|
||||
enemies_list.append(minion)
|
||||
self.flyer_summon_cd = self.flyer_summon_cooldown
|
||||
return True
|
||||
|
||||
def _flyer_cast_aoe(self, aoes_list):
|
||||
aoes_list.append(BossAoe(
|
||||
self.rect.centerx, self.rect.centery,
|
||||
self.flyer_aoe_radius, self.flyer_aoe_damage, self.flyer_aoe_duration,
|
||||
))
|
||||
self.flyer_aoe_cd = self.flyer_aoe_cooldown
|
||||
|
||||
def update(self, dt, platforms, ground_platforms, player, shockwaves_list: list[Shockwave],
|
||||
projectiles_list=None, enemies_list=None, aoes_list=None):
|
||||
# Summoned minions expire after their short life: no drops, heals the boss.
|
||||
if getattr(self, "is_minion", False) and self.minion_duration > 0:
|
||||
self.minion_timer -= dt
|
||||
if self.minion_timer <= 0.0:
|
||||
self.minion_timer = 0.0
|
||||
self.health = 0.0
|
||||
self.hazard_kill = True
|
||||
self.minion_expired = True
|
||||
|
||||
# Flying boss: completely replaces the ground-based AI body.
|
||||
if self.behavior == "flyer":
|
||||
self._flyer_update(dt, platforms, player, shockwaves_list,
|
||||
projectiles_list or [], enemies_list or [], aoes_list or [])
|
||||
return
|
||||
|
||||
if self.jump_cd_timer > 0:
|
||||
self.jump_cd_timer -= dt
|
||||
|
||||
@@ -5126,13 +5605,21 @@ class Enemy:
|
||||
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
if self.drop_profile == "boss":
|
||||
if self.behavior == "flyer":
|
||||
color = FLYER_BOSS_HIT_COLOR if self.hit_timer > 0 else FLYER_BOSS_COLOR
|
||||
elif self.drop_profile == "boss":
|
||||
color = BOSS_HIT_COLOR if self.hit_timer > 0 else BOSS_COLOR
|
||||
elif self.drop_profile == "miniboss":
|
||||
color = MINIBOSS_HIT_COLOR if self.hit_timer > 0 else MINIBOSS_COLOR
|
||||
else:
|
||||
color = ENEMY_HIT_COLOR if self.hit_timer > 0 else ENEMY_COLOR
|
||||
|
||||
# Summoned minions fade toward white as their life runs out.
|
||||
if getattr(self, "is_minion", False) and self.minion_duration > 0:
|
||||
frac = max(0.0, min(1.0, self.minion_timer / self.minion_duration))
|
||||
light = 1.0 - frac
|
||||
color = tuple(int(c + (255 - c) * light * 0.9) for c in color)
|
||||
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
color,
|
||||
@@ -6026,6 +6513,8 @@ shockwaves: list[Shockwave] = []
|
||||
coin_drops: list[CoinPickup] = []
|
||||
token_drops: list = []
|
||||
projectiles: list[Projectile] = []
|
||||
boss_projectiles: list[BossProjectile] = []
|
||||
boss_aoes: list[BossAoe] = []
|
||||
shatter_platforms: list[ShatterPlatform] = []
|
||||
shatter_aoes: list[ShatterAOE] = []
|
||||
tether_shots: list[TetherShot] = []
|
||||
@@ -6236,6 +6725,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
global platforms_dissolve
|
||||
global platform_layer
|
||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
||||
global boss_projectiles, boss_aoes
|
||||
global shatter_platforms, shatter_aoes, tether_shots
|
||||
global light_sources, save_points, butterfly_jars, houses
|
||||
global dust_puffs
|
||||
@@ -6273,6 +6763,8 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
coin_drops = []
|
||||
token_drops = []
|
||||
projectiles = []
|
||||
boss_projectiles = []
|
||||
boss_aoes = []
|
||||
shatter_platforms = []
|
||||
shatter_aoes = []
|
||||
tether_shots = []
|
||||
@@ -6310,6 +6802,7 @@ def reset_room_combat_state():
|
||||
"""Rebuild room enemies/NPCs/doors and clear transient combat objects."""
|
||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, token_drops, projectiles
|
||||
global shatter_platforms, shatter_aoes, dust_puffs, tether_shots, ability_doors
|
||||
global boss_projectiles, boss_aoes
|
||||
reset_trigger_wall_locks()
|
||||
enemies = spawn_enemies_from_room(ROOM)
|
||||
npcs = spawn_npcs_from_room(ROOM)
|
||||
@@ -6318,6 +6811,8 @@ def reset_room_combat_state():
|
||||
coin_drops = []
|
||||
token_drops = []
|
||||
projectiles = []
|
||||
boss_projectiles = []
|
||||
boss_aoes = []
|
||||
shatter_platforms = []
|
||||
shatter_aoes = []
|
||||
dust_puffs = []
|
||||
@@ -8113,8 +8608,12 @@ while running:
|
||||
t.draw(screen, camera_x, camera_y)
|
||||
for sw in shockwaves:
|
||||
sw.draw(screen, camera_x, camera_y)
|
||||
for a in boss_aoes:
|
||||
a.draw(screen, camera_x, camera_y)
|
||||
for pr in projectiles:
|
||||
pr.draw(screen, camera_x, camera_y)
|
||||
for bp in boss_projectiles:
|
||||
bp.draw(screen, camera_x, camera_y)
|
||||
for ts in tether_shots:
|
||||
ts.draw(screen, camera_x, camera_y)
|
||||
if corpse is not None and corpse.room_id == ROOM.id:
|
||||
@@ -8420,7 +8919,8 @@ while running:
|
||||
|
||||
# Update 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)
|
||||
|
||||
# Enemies that touch a hazard zone die instantly, dropping nothing.
|
||||
for en in enemies:
|
||||
@@ -8447,6 +8947,12 @@ while running:
|
||||
if any(box.colliderect(en.rect) for box in atk_boxes):
|
||||
player.swing_hit_ids.add(eid)
|
||||
en.take_damage(dmg)
|
||||
# Downward strike bounce: hitting an enemy from above while
|
||||
# swinging down bounces the player up (stomp/pogo).
|
||||
if player.attack_dir_y > 0.3 and player.rect.bottom <= en.rect.top + 6:
|
||||
player.vel_y = min(player.vel_y, -8.0)
|
||||
player.on_ground = False
|
||||
player.ground_platform = None
|
||||
|
||||
# Dash interactions
|
||||
if player.is_dashing():
|
||||
@@ -8485,6 +8991,11 @@ while running:
|
||||
pr.update(dt, collision_platforms, enemies, ability_doors)
|
||||
projectiles = [pr for pr in projectiles if pr.alive]
|
||||
|
||||
# Update flying-boss projectiles (homing shots)
|
||||
for bp in boss_projectiles:
|
||||
bp.update(dt, collision_platforms)
|
||||
boss_projectiles = [bp for bp in boss_projectiles if bp.alive]
|
||||
|
||||
# Update tether shots
|
||||
for ts in tether_shots:
|
||||
ts.update(dt, collision_platforms, enemies)
|
||||
@@ -8496,6 +9007,11 @@ while running:
|
||||
sw.update(dt, collision_platforms)
|
||||
shockwaves = [sw for sw in shockwaves if sw.alive]
|
||||
|
||||
# Update flying-boss AOE rings
|
||||
for a in boss_aoes:
|
||||
a.update(dt)
|
||||
boss_aoes = [a for a in boss_aoes if a.alive]
|
||||
|
||||
# Update shatter effects
|
||||
for sp in shatter_platforms:
|
||||
sp.update(dt)
|
||||
@@ -8516,6 +9032,27 @@ while running:
|
||||
player.apply_poison(BOSS_POISON_DURATION)
|
||||
sw.t = 0 # one-hit
|
||||
|
||||
# Flying-boss AOE rings hit the player (one hit per ring)
|
||||
for a in boss_aoes:
|
||||
if (not player.shade) and a.hits(player.rect):
|
||||
did_hit = player.take_damage(a.damage)
|
||||
if did_hit:
|
||||
player.apply_knockback(
|
||||
pygame.Rect(a.center[0] - 2, a.center[1] - 2, 4, 4),
|
||||
strength_px_per_frame=a.knock_strength,
|
||||
duration=a.knock_duration,
|
||||
pop_up_vel=a.pop_up,
|
||||
)
|
||||
a.hit = True
|
||||
|
||||
# Flying-boss homing projectiles hit the player
|
||||
for bp in boss_projectiles[:]:
|
||||
if (not player.shade) and player.rect.colliderect(bp.rect):
|
||||
did_hit = player.take_damage(bp.damage)
|
||||
if did_hit:
|
||||
player.apply_knockback(bp.rect, strength_px_per_frame=10.0, duration=0.18, pop_up_vel=-3.0)
|
||||
boss_projectiles.remove(bp)
|
||||
|
||||
|
||||
|
||||
# Update loot drops (gravity + landing)
|
||||
@@ -8541,8 +9078,20 @@ while running:
|
||||
if en.health > 0:
|
||||
alive.append(en)
|
||||
else:
|
||||
# Summoned minion expired on its own: heals the boss, no drops.
|
||||
if getattr(en, "minion_expired", False):
|
||||
owner = getattr(en, "minion_owner", None)
|
||||
if owner is not None and owner.health > 0:
|
||||
owner.health = min(owner.max_health, owner.health + getattr(en, "minion_heal", 0.0))
|
||||
continue
|
||||
if getattr(en, "hazard_kill", False):
|
||||
continue # hazard deaths drop nothing and give no quest credit
|
||||
# Player-killed minion: damages the boss, no drops.
|
||||
if getattr(en, "type_id", "") == "boss_minion":
|
||||
owner = getattr(en, "minion_owner", None)
|
||||
if owner is not None and owner.health > 0:
|
||||
owner.take_damage(getattr(en, "minion_damage", 0.0))
|
||||
continue
|
||||
loot_drops.extend(spawn_drops_from_enemy(en))
|
||||
etype = getattr(en, "type_id", "")
|
||||
eprof = getattr(en, "drop_profile", "")
|
||||
@@ -8551,6 +9100,13 @@ while running:
|
||||
bid = boss_id_for_room(ROOM.id)
|
||||
if bid and ACTIVE_REFIGHT is None:
|
||||
token_drops.append(TokenPickup(en.rect.centerx - 8, en.rect.top - 20, bid))
|
||||
# A defeated flying boss takes its summoned minions down with it.
|
||||
for other in enemies:
|
||||
if other is en:
|
||||
continue
|
||||
if getattr(other, "type_id", "") == "boss_minion" and other.health > 0:
|
||||
other.health = 0.0
|
||||
other.hazard_kill = True
|
||||
elif eprof == "miniboss":
|
||||
DEFEATED_MINIBOSSES.add(ROOM.id)
|
||||
|
||||
@@ -8680,9 +9236,15 @@ while running:
|
||||
for sw in shockwaves:
|
||||
sw.draw(screen, camera_x, camera_y)
|
||||
|
||||
# flying-boss AOE rings
|
||||
for a in boss_aoes:
|
||||
a.draw(screen, camera_x, camera_y)
|
||||
|
||||
# projectiles
|
||||
for pr in projectiles:
|
||||
pr.draw(screen, camera_x, camera_y)
|
||||
for bp in boss_projectiles:
|
||||
bp.draw(screen, camera_x, camera_y)
|
||||
for ts in tether_shots:
|
||||
ts.draw(screen, camera_x, camera_y)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user