crude sprites
bad sprites for enemies, some fairy changes
This commit is contained in:
@@ -24,7 +24,9 @@
|
||||
"boss": [240, 220, 80],
|
||||
"boss_hit": [255, 245, 160],
|
||||
"boss_flyer": [150, 90, 220],
|
||||
"boss_flyer_hit": [210, 170, 255]
|
||||
"boss_flyer_hit": [210, 170, 255],
|
||||
"swooper": [90, 180, 220],
|
||||
"swooper_hit": [170, 225, 255]
|
||||
},
|
||||
|
||||
"healing": {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"shade": "Ooh, shiny. Even the dark likes this."
|
||||
},
|
||||
"dust_2": {
|
||||
"light": "Another pinch of dust - keep going!",
|
||||
"light": "A pinch of dust - keep going!",
|
||||
"shade": "The dark tastes sweeter with this."
|
||||
},
|
||||
"dust_3": {
|
||||
@@ -50,7 +50,7 @@
|
||||
"shade": "All 5! My sting is yours. Press the Fairy Mode key."
|
||||
},
|
||||
"quest_return": {
|
||||
"light": "That's all 5! Return to me at the fairy spot - I have something to show you!",
|
||||
"light": "That's all 5! Return to the fairy spot - I have something to show you!",
|
||||
"shade": "All 5 gathered. Come back to my spot - I am ready to change."
|
||||
},
|
||||
"quest_unlock": {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"size": [14, 6]
|
||||
},
|
||||
"fairy": {
|
||||
"charge_cost": 5.0,
|
||||
"cooldown": 0.7,
|
||||
"charge_cost": 10.0,
|
||||
"cooldown": 1.4,
|
||||
"range": 560.0,
|
||||
"voice_every": 3,
|
||||
"damage_mult": 0.5,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"pad_y": 6,
|
||||
"line_gap": 4,
|
||||
"x": 14,
|
||||
"center_lift": 60,
|
||||
"y_offset_from_center": -10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,26 @@
|
||||
"detection_range": 320,
|
||||
"contact_damage": 1,
|
||||
"drop_profile": "normal"
|
||||
},
|
||||
|
||||
"swooper": {
|
||||
"rect": [40, 40],
|
||||
"health": 3,
|
||||
"speed": 2.0,
|
||||
"air_speed": 2.0,
|
||||
"detection_range": 340,
|
||||
"contact_damage": 1,
|
||||
"drop_profile": "normal",
|
||||
"behavior": "swooper",
|
||||
"swooper": {
|
||||
"bob_amplitude": 6,
|
||||
"bob_speed": 2.4,
|
||||
"charge_speed_mult": 3.4,
|
||||
"charge_duration": 0.8,
|
||||
"charge_cooldown": 1.8,
|
||||
"return_speed": 2.2,
|
||||
"windup": 0.0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 421 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 376 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 505 B |
Binary file not shown.
|
After Width: | Height: | Size: 883 B |
@@ -22,6 +22,10 @@
|
||||
"room9_bottom_right": [
|
||||
2660,
|
||||
600
|
||||
],
|
||||
"entry_diary": [
|
||||
1774,
|
||||
540
|
||||
]
|
||||
},
|
||||
"platforms": [
|
||||
|
||||
@@ -303,6 +303,13 @@
|
||||
}
|
||||
},
|
||||
boss_minion: { rect: [34, 50], health: 4, speed: 2.4, air_speed: 2.2, detection_range: 320, contact_damage: 1, drop_profile: "normal" },
|
||||
swooper: {
|
||||
rect: [40, 40], health: 3, speed: 2.0, air_speed: 2.0, detection_range: 340, contact_damage: 1, drop_profile: "normal", behavior: "swooper",
|
||||
swooper: {
|
||||
bob_amplitude: 6, bob_speed: 2.4, charge_speed_mult: 3.4,
|
||||
charge_duration: 0.8, charge_cooldown: 1.8, return_speed: 2.2, windup: 0.0
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function seedDefaultObjectCatalog(){
|
||||
|
||||
+297
-11
@@ -450,6 +450,9 @@ 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))
|
||||
|
||||
SWOOPER_COLOR = _color(["enemy", "swooper"], (90, 180, 220))
|
||||
SWOOPER_HIT_COLOR = _color(["enemy", "swooper_hit"], (170, 225, 255))
|
||||
|
||||
PLAYER_HIT_COLOR = (255, 255, 0)
|
||||
|
||||
HEAL_DARK = _color(["healing", "dark"])
|
||||
@@ -521,8 +524,8 @@ PROJECTILE_LIFETIME = float(_ranged(["projectile", "lifetime"], PROJECTILE_LIFET
|
||||
PROJECTILE_SIZE = tuple(_ranged(["projectile", "size"], list(PROJECTILE_SIZE)))
|
||||
|
||||
# --- Fairy combat (granted by the Fairy Dust quest) ---
|
||||
FAIRY_SHOT_CHARGE_COST = float(_ranged(["fairy", "charge_cost"], RANGED_CHARGE_COST))
|
||||
FAIRY_SHOT_COOLDOWN = float(_ranged(["fairy", "cooldown"], 0.7))
|
||||
FAIRY_SHOT_CHARGE_COST = float(_ranged(["fairy", "charge_cost"], RANGED_CHARGE_COST * 2.0))
|
||||
FAIRY_SHOT_COOLDOWN = float(_ranged(["fairy", "cooldown"], 1.4))
|
||||
FAIRY_SHOT_RANGE = float(_ranged(["fairy", "range"], 560.0))
|
||||
FAIRY_VOICE_EVERY = max(1, int(_ranged(["fairy", "voice_every"], 3)))
|
||||
FAIRY_DAMAGE_MULT = float(_ranged(["fairy", "damage_mult"], RANGED_DAMAGE_MULT))
|
||||
@@ -983,6 +986,25 @@ ENEMY_TYPES_DEFAULT = {
|
||||
"contact_damage": 1,
|
||||
"drop_profile": "normal"
|
||||
},
|
||||
"swooper": {
|
||||
"rect": [40, 40],
|
||||
"health": 3,
|
||||
"speed": 2.0,
|
||||
"air_speed": 2.0,
|
||||
"detection_range": 340,
|
||||
"contact_damage": 1,
|
||||
"drop_profile": "normal",
|
||||
"behavior": "swooper",
|
||||
"swooper": {
|
||||
"bob_amplitude": 6,
|
||||
"bob_speed": 2.4,
|
||||
"charge_speed_mult": 3.4,
|
||||
"charge_duration": 0.8,
|
||||
"charge_cooldown": 1.8,
|
||||
"return_speed": 2.2,
|
||||
"windup": 0.0
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ENEMY_DROPS_DEFAULT = {
|
||||
@@ -1564,6 +1586,42 @@ BOSS_PROJECTILE_RAW = _load_first_existing([
|
||||
])
|
||||
BOSS_PROJECTILE_RAW_LEFT = pygame.transform.flip(BOSS_PROJECTILE_RAW, True, False) if BOSS_PROJECTILE_RAW is not None else None
|
||||
|
||||
# --- Enemy sprites (authored at each enemy's collision-rect size) ---
|
||||
ENEMIES_SPRITES_DIR = DATA_ROOT / "sprites" / "enemies"
|
||||
|
||||
def _load_enemy_sprite(name: str):
|
||||
return _load_image_or_none(ENEMIES_SPRITES_DIR / name)
|
||||
|
||||
ENEMY_SPRITES = {
|
||||
"ground_enemy": _load_enemy_sprite("ground_enemy_1.png"),
|
||||
"ground_miniboss": _load_enemy_sprite("ground_miniboss_1.png"),
|
||||
"ground_boss": _load_enemy_sprite("ground_boss_1.png"),
|
||||
"flying_enemy": _load_enemy_sprite("flying_enemy_1.png"),
|
||||
"flying_boss": _load_enemy_sprite("flying_boss_1.png"),
|
||||
"flying_boss_minion": _load_enemy_sprite("boss_flyer_minion.png"),
|
||||
}
|
||||
|
||||
def enemy_sprite_info(type_id: str, behavior: str):
|
||||
"""Return ``(raw_surface | None, source_facing)`` for an enemy.
|
||||
|
||||
``source_facing`` is ``"right"`` / ``"left"`` for art that must be mirrored
|
||||
to match the enemy's facing, or ``None`` for art with no direction (drawn
|
||||
as authored). A ``None`` surface means "no sprite yet" -> the entity keeps
|
||||
its colored-rectangle placeholder."""
|
||||
if behavior == "flyer":
|
||||
return ENEMY_SPRITES["flying_boss"], None
|
||||
if behavior == "swooper":
|
||||
return ENEMY_SPRITES["flying_enemy"], None
|
||||
if behavior == "charger":
|
||||
return ENEMY_SPRITES["ground_boss"], "left"
|
||||
if behavior == "shockwave":
|
||||
return None, None # shockwave boss art not made yet
|
||||
if type_id == "boss_minion":
|
||||
return ENEMY_SPRITES["flying_boss_minion"], None
|
||||
if type_id == "miniboss":
|
||||
return ENEMY_SPRITES["ground_miniboss"], "right"
|
||||
return ENEMY_SPRITES["ground_enemy"], "right"
|
||||
|
||||
def _scale_icon(img: pygame.Surface | None, size: int) -> pygame.Surface | None:
|
||||
if img is None:
|
||||
return None
|
||||
@@ -1763,6 +1821,9 @@ class FairyProjectile(Projectile):
|
||||
w = max(1, int(img_raw.get_width() * scale))
|
||||
h = max(1, int(img_raw.get_height() * scale))
|
||||
img = pygame.transform.scale(img_raw, (w, h))
|
||||
# Raw fairy sprite points LEFT; flip it so it points right, then rotate
|
||||
# to match the actual flight direction (same fix as the ranged bolt).
|
||||
img = pygame.transform.flip(img, True, False)
|
||||
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,
|
||||
@@ -3748,6 +3809,9 @@ class PickupFeed:
|
||||
self.pad_x = int(_ui(["pickup_feed", "pad_x"], 10))
|
||||
self.pad_y = int(_ui(["pickup_feed", "pad_y"], 6))
|
||||
self.line_gap = int(_ui(["pickup_feed", "line_gap"], 4))
|
||||
# Lift center "splash" messages above the normal feed line so they do
|
||||
# not cover the fairy's speech bubble.
|
||||
self.center_lift = int(_ui(["pickup_feed", "center_lift"], 60))
|
||||
|
||||
|
||||
def push(self, name: str, rarity: str = ""):
|
||||
@@ -3830,7 +3894,7 @@ class PickupFeed:
|
||||
box_w = min(WIDTH - 80, w + pad_x * 2)
|
||||
box_h = h + pad_y * 2
|
||||
box_x = (WIDTH - box_w) // 2
|
||||
box_y = y
|
||||
box_y = y - self.center_lift
|
||||
|
||||
box = pygame.Surface((box_w, box_h), pygame.SRCALPHA)
|
||||
box.fill((0, 0, 0, int(alpha * 0.8)))
|
||||
@@ -4995,6 +5059,29 @@ class Enemy:
|
||||
self.flyer_cycle_casts_left = 0
|
||||
self.flyer_hard = False
|
||||
|
||||
# --- Hovering flyer ("swooper") state: the enemy idles in the air and,
|
||||
# the moment it spots the player, dives straight at the point the player
|
||||
# occupied when the charge began (that point stays frozen mid-dive). ---
|
||||
swoop_cfg = (cfg.get("swooper", {}) if isinstance(cfg, dict) else {})
|
||||
if not isinstance(swoop_cfg, dict):
|
||||
swoop_cfg = {}
|
||||
self.swooper_bob_amplitude = float(swoop_cfg.get("bob_amplitude", 6))
|
||||
self.swooper_bob_speed = float(swoop_cfg.get("bob_speed", 2.4))
|
||||
self.swooper_charge_speed_mult = float(swoop_cfg.get("charge_speed_mult", 3.4))
|
||||
self.swooper_charge_duration = float(swoop_cfg.get("charge_duration", 0.8))
|
||||
self.swooper_charge_cooldown = float(swoop_cfg.get("charge_cooldown", 1.8))
|
||||
self.swooper_return_speed = float(swoop_cfg.get("return_speed", 2.2))
|
||||
self.swooper_windup = float(swoop_cfg.get("windup", 0.0))
|
||||
self.swooper_state = "idle" # idle | windup | charging | recover
|
||||
self.swooper_timer = 0.0
|
||||
self.swooper_bob_t = random.random() * 10.0
|
||||
self.swooper_home_x = self.rect.centerx
|
||||
self.swooper_home_y = self.rect.centery
|
||||
self.swooper_dir_x = 0.0
|
||||
self.swooper_dir_y = 0.0
|
||||
self.swooper_target_x = float(self.rect.centerx)
|
||||
self.swooper_target_y = float(self.rect.centery)
|
||||
|
||||
# --- 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")
|
||||
@@ -5068,6 +5155,16 @@ class Enemy:
|
||||
self.flyer_startup_timer = self.flyer_startup_delay
|
||||
self.flyer_cycle_casts_left = 0
|
||||
|
||||
self.swooper_state = "idle"
|
||||
self.swooper_timer = 0.0
|
||||
self.swooper_bob_t = random.random() * 10.0
|
||||
self.swooper_home_x = self.rect.centerx
|
||||
self.swooper_home_y = self.rect.centery
|
||||
self.swooper_dir_x = 0.0
|
||||
self.swooper_dir_y = 0.0
|
||||
self.swooper_target_x = float(self.rect.centerx)
|
||||
self.swooper_target_y = float(self.rect.centery)
|
||||
|
||||
self.turn_cooldown = 0.0
|
||||
self.last_x = self.rect.x
|
||||
self.stuck_timer = 0.0
|
||||
@@ -5559,6 +5656,157 @@ class Enemy:
|
||||
))
|
||||
self.flyer_aoe_cd = self.flyer_aoe_cooldown
|
||||
|
||||
# --- Hovering flyer ("swooper") behavior ---
|
||||
|
||||
def _swooper_update(self, dt, platforms, player):
|
||||
"""Idle in the air bobbing, then -- the instant the player is spotted --
|
||||
lock onto where the player stood at that moment and dive at that frozen
|
||||
point. After the dive it recovers, drifts back to its perch and can
|
||||
charge again once the cooldown elapses."""
|
||||
if self.hit_timer > 0.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 stunned:
|
||||
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)
|
||||
sees_player = (not player.shade) and (distance <= self.detection_range)
|
||||
feared = self.fear_timer > 0.0
|
||||
speed = self.air_speed
|
||||
if self.slow_timer > 0.0:
|
||||
speed *= self.slow_mult
|
||||
|
||||
if feared:
|
||||
# Flee straight away from the player and abort any dive.
|
||||
if distance > 0.001:
|
||||
self._swooper_try_move(-dx / distance * TETHER_FEAR_SPEED_MULT * speed,
|
||||
-dy / distance * TETHER_FEAR_SPEED_MULT * speed,
|
||||
platforms)
|
||||
self.swooper_state = "idle"
|
||||
self.swooper_timer = 0.0
|
||||
self.was_chasing = False
|
||||
return
|
||||
|
||||
if stunned:
|
||||
self.swooper_state = "idle"
|
||||
self.swooper_timer = 0.0
|
||||
self.was_chasing = False
|
||||
return
|
||||
|
||||
if self.swooper_state == "idle":
|
||||
self._swooper_hover(dt, platforms)
|
||||
if sees_player:
|
||||
self._swooper_start_charge(player)
|
||||
elif self.swooper_state == "windup":
|
||||
self._swooper_hover(dt, platforms)
|
||||
self.swooper_timer -= dt
|
||||
if self.swooper_timer <= 0.0:
|
||||
self.swooper_state = "charging"
|
||||
self.swooper_timer = self.swooper_charge_duration
|
||||
elif self.swooper_state == "charging":
|
||||
charge_speed = speed * self.swooper_charge_speed_mult
|
||||
blocked = self._swooper_try_move(
|
||||
self.swooper_dir_x * charge_speed,
|
||||
self.swooper_dir_y * charge_speed,
|
||||
platforms,
|
||||
)
|
||||
self.swooper_timer -= dt
|
||||
# Remaining vector to the frozen target; once it points behind us
|
||||
# (or we hit something / ran out of time) the dive is over.
|
||||
rem_x = self.swooper_target_x - self.rect.centerx
|
||||
rem_y = self.swooper_target_y - self.rect.centery
|
||||
passed_target = (rem_x * self.swooper_dir_x + rem_y * self.swooper_dir_y) <= 0.0
|
||||
if blocked or self.swooper_timer <= 0.0 or passed_target:
|
||||
self.swooper_state = "recover"
|
||||
self.swooper_timer = self.swooper_charge_cooldown
|
||||
else: # recover
|
||||
self._swooper_hover(dt, platforms)
|
||||
self.swooper_timer -= dt
|
||||
if self.swooper_timer <= 0.0:
|
||||
self.swooper_state = "idle"
|
||||
|
||||
self.was_chasing = sees_player
|
||||
|
||||
def _swooper_hover(self, dt, platforms):
|
||||
"""Ease back toward the perch while bobbing gently on the y axis."""
|
||||
self.swooper_bob_t += dt * self.swooper_bob_speed
|
||||
bob = math.sin(self.swooper_bob_t) * self.swooper_bob_amplitude
|
||||
to_x = self.swooper_home_x - self.rect.centerx
|
||||
to_y = (self.swooper_home_y + bob) - self.rect.centery
|
||||
dist = math.hypot(to_x, to_y)
|
||||
if dist < 0.001:
|
||||
return
|
||||
hover_speed = self.swooper_return_speed
|
||||
if self.slow_timer > 0.0:
|
||||
hover_speed *= self.slow_mult
|
||||
step = min(hover_speed, dist)
|
||||
self._swooper_try_move(to_x / dist * step, to_y / dist * step, platforms)
|
||||
|
||||
def _swooper_start_charge(self, player):
|
||||
"""Freeze the player's position and begin the dive toward it."""
|
||||
self.swooper_target_x = float(player.rect.centerx)
|
||||
self.swooper_target_y = float(player.rect.centery)
|
||||
dx = self.swooper_target_x - self.rect.centerx
|
||||
dy = self.swooper_target_y - self.rect.centery
|
||||
length = math.hypot(dx, dy)
|
||||
if length < 1.0:
|
||||
dx, dy, length = 1.0, 0.0, 1.0
|
||||
self.swooper_dir_x = dx / length
|
||||
self.swooper_dir_y = dy / length
|
||||
self.direction = 1 if self.swooper_dir_x >= 0 else -1
|
||||
if self.swooper_windup > 0.0:
|
||||
self.swooper_state = "windup"
|
||||
self.swooper_timer = self.swooper_windup
|
||||
else:
|
||||
self.swooper_state = "charging"
|
||||
self.swooper_timer = self.swooper_charge_duration
|
||||
|
||||
def _swooper_try_move(self, dx, dy, platforms) -> bool:
|
||||
"""Move by a fractional per-frame delta, stopping on platforms and
|
||||
clamping to the room. Returns True if a platform blocked the move."""
|
||||
blocked = False
|
||||
if dx:
|
||||
self.rect.x += int(dx)
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
if dx > 0:
|
||||
self.rect.right = p.left
|
||||
else:
|
||||
self.rect.left = p.right
|
||||
blocked = True
|
||||
break
|
||||
if dy:
|
||||
self.rect.y += int(dy)
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
if dy > 0:
|
||||
self.rect.bottom = p.top
|
||||
else:
|
||||
self.rect.top = p.bottom
|
||||
blocked = True
|
||||
break
|
||||
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))
|
||||
return blocked
|
||||
|
||||
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.
|
||||
@@ -5580,6 +5828,11 @@ class Enemy:
|
||||
)
|
||||
return
|
||||
|
||||
# Hovering flyer ("swooper"): also fully aerial; replaces the ground body.
|
||||
if self.behavior == "swooper":
|
||||
self._swooper_update(dt, platforms, player)
|
||||
return
|
||||
|
||||
if self.jump_cd_timer > 0:
|
||||
self.jump_cd_timer -= dt
|
||||
|
||||
@@ -6012,6 +6265,8 @@ class Enemy:
|
||||
def draw(self, surface, cx, cy):
|
||||
if self.behavior == "flyer":
|
||||
color = FLYER_BOSS_HIT_COLOR if self.hit_timer > 0 else FLYER_BOSS_COLOR
|
||||
elif self.behavior == "swooper":
|
||||
color = SWOOPER_HIT_COLOR if self.hit_timer > 0 else SWOOPER_COLOR
|
||||
elif self.drop_profile == "boss":
|
||||
color = BOSS_HIT_COLOR if self.hit_timer > 0 else BOSS_COLOR
|
||||
elif self.drop_profile == "miniboss":
|
||||
@@ -6020,11 +6275,33 @@ class Enemy:
|
||||
color = ENEMY_HIT_COLOR if self.hit_timer > 0 else ENEMY_COLOR
|
||||
|
||||
# Summoned minions fade toward white as their life runs out.
|
||||
fade = 0.0
|
||||
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)
|
||||
fade = (1.0 - frac) * 0.9
|
||||
|
||||
raw, source_facing = enemy_sprite_info(self.type_id, self.behavior)
|
||||
if raw is not None:
|
||||
img = raw
|
||||
if source_facing == "right" and self.direction < 0:
|
||||
img = pygame.transform.flip(img, True, False)
|
||||
elif source_facing == "left" and self.direction > 0:
|
||||
img = pygame.transform.flip(img, True, False)
|
||||
draw_x = self.rect.centerx - img.get_width() // 2 - cx
|
||||
draw_y = self.rect.bottom - img.get_height() - cy
|
||||
surface.blit(img, (draw_x, draw_y))
|
||||
if self.hit_timer > 0:
|
||||
flash = img.copy()
|
||||
flash.fill((140, 140, 140, 0), special_flags=pygame.BLEND_RGBA_ADD)
|
||||
surface.blit(flash, (draw_x, draw_y))
|
||||
if fade > 0.0:
|
||||
add = int(255 * fade)
|
||||
glow = img.copy()
|
||||
glow.fill((add, add, add, 0), special_flags=pygame.BLEND_RGBA_ADD)
|
||||
surface.blit(glow, (draw_x, draw_y))
|
||||
else:
|
||||
if fade > 0.0:
|
||||
color = tuple(int(c + (255 - c) * fade) for c in color)
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
color,
|
||||
@@ -8466,8 +8743,12 @@ diary_ui = DiaryUI(inventory, player, font)
|
||||
# ----------------------------
|
||||
TOWN_NPC_TYPES = ("vendor", "upgrader", "necklace_vendor", "combat_master")
|
||||
|
||||
# The current first town (where the Boss Diary lives).
|
||||
TOWN_ROOM_ID = "room_09.json"
|
||||
|
||||
TOWN_DISPLAY_NAMES = {
|
||||
"room_town_gloomreach.json": "Gloomreach",
|
||||
"room_09.json": "Gloomreach",
|
||||
}
|
||||
|
||||
|
||||
@@ -9749,11 +10030,12 @@ while running:
|
||||
|
||||
# Respawn on death/void
|
||||
if player.rect.top > KILL_Y or player.health <= 0:
|
||||
apply_death_penalty()
|
||||
if ACTIVE_REFIGHT is not None:
|
||||
# Died in a diary refight -> return to the diary in town.
|
||||
# Died in a diary refight. Refights are consequence-free practice:
|
||||
# skip the death penalty entirely (no coin/jar loss, no corpse) and
|
||||
# return the player to the diary in town.
|
||||
ACTIVE_REFIGHT = None
|
||||
room = room_manager.load_room("room_town_gloomreach.json")
|
||||
room = room_manager.load_room(TOWN_ROOM_ID)
|
||||
diary_spawn = room.entry_spawns.get("entry_diary") or room.player_spawn
|
||||
room.player_spawn = [int(diary_spawn[0]), int(diary_spawn[1])]
|
||||
player.respawn(int(diary_spawn[0]), int(diary_spawn[1]))
|
||||
@@ -9761,6 +10043,7 @@ while running:
|
||||
player.shatterstep_landing_pending = False
|
||||
player.shatterstep_landing_damage = 0.0
|
||||
else:
|
||||
apply_death_penalty()
|
||||
# Normal death respawn: latest lantern save point, else the room spawn.
|
||||
if LANTERN_RESPAWN_ROOM and LANTERN_RESPAWN_POS:
|
||||
lx, ly = LANTERN_RESPAWN_POS
|
||||
@@ -10155,12 +10438,15 @@ while running:
|
||||
# Fairy companion bubble: active line, or a talk prompt when at a fairy spot.
|
||||
fairy_rect = player.fairy_screen_rect
|
||||
if fairy_rect is not None:
|
||||
# Anchor the bubble above the player rather than the fairy: the fairy
|
||||
# swaps sides when the player flips, which made the text jump.
|
||||
bubble_x = player.rect.centerx - camera_x
|
||||
if fairy_speech_text:
|
||||
draw_speech_bubble(screen, fairy_speech_text, fairy_rect.centerx, fairy_rect.top)
|
||||
draw_speech_bubble(screen, fairy_speech_text, bubble_x, fairy_rect.top)
|
||||
elif get_near_fairy_dust(player, fairy_dusts):
|
||||
draw_speech_bubble(screen, "(Press F to collect)", fairy_rect.centerx, fairy_rect.top)
|
||||
draw_speech_bubble(screen, "(Press F to collect)", bubble_x, fairy_rect.top)
|
||||
elif get_near_fairy_spot(player, fairy_spots):
|
||||
draw_speech_bubble(screen, "(Press F to talk)", fairy_rect.centerx, fairy_rect.top)
|
||||
draw_speech_bubble(screen, "(Press F to talk)", bubble_x, fairy_rect.top)
|
||||
|
||||
# NPC greeting bubble (shown near the player)
|
||||
greet_npc = get_near_npc(player, npcs)
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"version": 1,
|
||||
"saved_at": "2026-10-06T09:06:07",
|
||||
"player": {
|
||||
"room_id": "room_09.json",
|
||||
"x": 1460,
|
||||
"y": 660,
|
||||
"health": 20,
|
||||
"charge": 115.0,
|
||||
"shade": true,
|
||||
"coins_light": 92,
|
||||
"coins_dark": 22,
|
||||
"butterfly_jars": 1,
|
||||
"butterfly_jar_seen": true,
|
||||
"has_ranged": true,
|
||||
"has_fairy_combat": true,
|
||||
"opened_chests": [
|
||||
"room_03.json@90,194",
|
||||
"room_07.json@440,494"
|
||||
],
|
||||
"equipment": {
|
||||
"helmet": {
|
||||
"id": "gravemark_helm",
|
||||
"name": "Gravemark Helmet",
|
||||
"set": "gravemark",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge": 10
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"charge_regen_pct": 0.1
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"charge_regen_pct": 0.15,
|
||||
"health": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "awakened",
|
||||
"tier": 3,
|
||||
"stats": {
|
||||
"charge": 15.0,
|
||||
"charge_regen_pct": 0.15,
|
||||
"health": 5.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"chest": {
|
||||
"id": "gravemark_chest",
|
||||
"name": "Gravemark Chestplate",
|
||||
"set": "gravemark",
|
||||
"slot": "chest",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.1
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.1,
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.2,
|
||||
"health": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"charge_regen_pct": 0.1
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"pants": {
|
||||
"id": "vitalweave_greaves",
|
||||
"name": "Vitalweave Greaves",
|
||||
"set": "vitalweave",
|
||||
"slot": "pants",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.1
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.15
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.35
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "attuned",
|
||||
"tier": 2,
|
||||
"stats": {
|
||||
"charge_regen_pct": 0.15
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
"necklace": {
|
||||
"id": "necklace_vigor",
|
||||
"name": "Charm of Vigor",
|
||||
"slot": "necklace",
|
||||
"item_type": "necklace",
|
||||
"base_stats": {
|
||||
"health": 5
|
||||
},
|
||||
"rarity": "",
|
||||
"tier": 0,
|
||||
"abilities": [],
|
||||
"stats": {
|
||||
"health": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"id": "vitalweave_helm",
|
||||
"name": "Vitalweave Helmet",
|
||||
"set": "vitalweave",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"health": 10
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"health": 15
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "attuned",
|
||||
"tier": 2,
|
||||
"stats": {
|
||||
"health": 10.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
{
|
||||
"id": "necklace_haste",
|
||||
"name": "Charm of Haste",
|
||||
"slot": "necklace",
|
||||
"item_type": "necklace",
|
||||
"base_stats": {
|
||||
"move_speed_pct": 0.1
|
||||
},
|
||||
"rarity": "",
|
||||
"tier": 0,
|
||||
"abilities": [],
|
||||
"stats": {
|
||||
"move_speed_pct": 0.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "necklace_haste",
|
||||
"name": "Charm of Haste",
|
||||
"slot": "necklace",
|
||||
"item_type": "necklace",
|
||||
"base_stats": {
|
||||
"move_speed_pct": 0.1
|
||||
},
|
||||
"rarity": "",
|
||||
"tier": 0,
|
||||
"abilities": [],
|
||||
"stats": {
|
||||
"move_speed_pct": 0.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "adaptive_traveller_helm",
|
||||
"name": "Adaptive Traveller's Helmet",
|
||||
"set": "adaptive_traveller",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge": 10
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge": 15
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge": 15,
|
||||
"charge_regen_pct": 0.15
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"charge": 10.0
|
||||
},
|
||||
"abilities": []
|
||||
},
|
||||
{
|
||||
"id": "luminal_growth_helm",
|
||||
"name": "Luminal Growth Helmet",
|
||||
"set": "luminal_growth",
|
||||
"slot": "helmet",
|
||||
"base_stats": {},
|
||||
"rarity_overrides": {
|
||||
"tempered": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.1
|
||||
}
|
||||
},
|
||||
"attuned": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.1,
|
||||
"health": 5
|
||||
}
|
||||
},
|
||||
"awakened": {
|
||||
"extra_stats": {
|
||||
"charge_regen_pct": 0.15,
|
||||
"health": 10,
|
||||
"negate_damage_chance": 0.05
|
||||
}
|
||||
}
|
||||
},
|
||||
"rarity": "tempered",
|
||||
"tier": 1,
|
||||
"stats": {
|
||||
"charge_regen_pct": 0.1
|
||||
},
|
||||
"abilities": []
|
||||
}
|
||||
],
|
||||
"quests": {
|
||||
"active": {
|
||||
"first_kills_shade": {
|
||||
"progress": 0
|
||||
},
|
||||
"quest_combat_master_tokens": {
|
||||
"progress": 1
|
||||
}
|
||||
},
|
||||
"completed_one_time": [
|
||||
"quest_fairy_dust"
|
||||
],
|
||||
"ever_completed": [
|
||||
"quest_fairy_dust",
|
||||
"quest_first_kills"
|
||||
],
|
||||
"tokens": 0
|
||||
},
|
||||
"boss_diary": {
|
||||
"slotted": {
|
||||
"boss_02": 1
|
||||
}
|
||||
},
|
||||
"defeated_bosses": [
|
||||
"room_05.json",
|
||||
"room_08.json"
|
||||
],
|
||||
"broken_doors": [],
|
||||
"pending_token_drops": {},
|
||||
"opened_miniboss_walls": [
|
||||
"room_04.json:1340,540,20,160",
|
||||
"room_04.json:460,540,20,160"
|
||||
],
|
||||
"fairy_dust_collected": [
|
||||
"room_01.json:600,140",
|
||||
"room_03.json:440,400",
|
||||
"room_05.json:740,180",
|
||||
"room_07.json:1100,520",
|
||||
"room_09.json:840,560"
|
||||
],
|
||||
"lantern_respawn": {
|
||||
"room_id": "room_09.json",
|
||||
"x": 1460,
|
||||
"y": 660
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user