level editor overhaul
more objects, NPCs, better toolbar and snapping
This commit is contained in:
+200
-138
@@ -868,15 +868,27 @@ ROOM_DEFAULT = {
|
||||
}
|
||||
|
||||
|
||||
def rects_from_list(list_of_rects) -> list[pygame.Rect]:
|
||||
def rects_from_list(list_of_rects, layer_map: dict[int, int] | None = None) -> list[pygame.Rect]:
|
||||
out = []
|
||||
if not isinstance(list_of_rects, list):
|
||||
return out
|
||||
for r in list_of_rects:
|
||||
if isinstance(r, (list, tuple)) and len(r) == 4:
|
||||
out.append(pygame.Rect(int(r[0]), int(r[1]), int(r[2]), int(r[3])))
|
||||
if isinstance(r, (list, tuple)) and len(r) >= 4:
|
||||
rect = pygame.Rect(int(r[0]), int(r[1]), int(r[2]), int(r[3]))
|
||||
out.append(rect)
|
||||
if layer_map is not None and len(r) >= 5:
|
||||
try:
|
||||
layer_map[id(rect)] = int(r[4])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
elif isinstance(r, dict):
|
||||
out.append(pygame.Rect(int(r.get("x", 0)), int(r.get("y", 0)), int(r.get("w", 0)), int(r.get("h", 0))))
|
||||
rect = pygame.Rect(int(r.get("x", 0)), int(r.get("y", 0)), int(r.get("w", 0)), int(r.get("h", 0)))
|
||||
out.append(rect)
|
||||
if layer_map is not None:
|
||||
try:
|
||||
layer_map[id(rect)] = int(r.get("layer", 0))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
@@ -985,13 +997,14 @@ class Room:
|
||||
|
||||
self.kill_y = int((room_json.get("bounds", {}) or {}).get("kill_y", 1400))
|
||||
|
||||
self.platform_layer: dict[int, int] = {}
|
||||
platforms = room_json.get("platforms", {}) if isinstance(room_json, dict) else {}
|
||||
if isinstance(platforms, list):
|
||||
self.platforms_ground = rects_from_list(platforms)
|
||||
self.platforms_ground = rects_from_list(platforms, self.platform_layer)
|
||||
self.platforms_floating = []
|
||||
elif isinstance(platforms, dict):
|
||||
self.platforms_ground = rects_from_list(platforms.get("ground", []))
|
||||
self.platforms_floating = rects_from_list(platforms.get("floating", []))
|
||||
self.platforms_ground = rects_from_list(platforms.get("ground", []), self.platform_layer)
|
||||
self.platforms_floating = rects_from_list(platforms.get("floating", []), self.platform_layer)
|
||||
else:
|
||||
self.platforms_ground = []
|
||||
self.platforms_floating = []
|
||||
@@ -1036,7 +1049,13 @@ class Room:
|
||||
rect = list(d)
|
||||
if rect is None:
|
||||
continue
|
||||
normalized_ability_doors.append({"rect": rect})
|
||||
nd = {"rect": rect}
|
||||
if isinstance(d, dict):
|
||||
try:
|
||||
nd["layer"] = int(d.get("layer", 0))
|
||||
except (TypeError, ValueError):
|
||||
nd["layer"] = 0
|
||||
normalized_ability_doors.append(nd)
|
||||
self.ability_doors = normalized_ability_doors
|
||||
|
||||
self.exit_triggers: list[dict] = []
|
||||
@@ -1057,6 +1076,7 @@ class Room:
|
||||
"hint": str(ex.get("hint", "")),
|
||||
"requires_boss_dead": bool(ex.get("requires_boss_dead", False)),
|
||||
"requires_ranged": bool(ex.get("requires_ranged", False)),
|
||||
"layer": int(ex.get("layer", 0)),
|
||||
})
|
||||
|
||||
|
||||
@@ -1553,7 +1573,7 @@ class LightSource:
|
||||
|
||||
|
||||
class SavePoint:
|
||||
"""Lantern save point: auto-saves and becomes the respawn point when touched."""
|
||||
"""Lantern save point: pressing interact nearby saves the game and sets the respawn point."""
|
||||
def __init__(self, x: int, y: int, radius: int = 90):
|
||||
self.x = int(x)
|
||||
self.y = int(y)
|
||||
@@ -1568,13 +1588,6 @@ class SavePoint:
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
self.t += 0.03
|
||||
pulse = 0.10 + 0.05 * (0.5 + 0.5 * math.sin(self.t))
|
||||
r = self.radius
|
||||
overlay = pygame.Surface((r * 2, r * 2), pygame.SRCALPHA)
|
||||
alpha = int(255 * pulse)
|
||||
pygame.draw.circle(overlay, (150, 220, 255, alpha), (r, r), r, 2)
|
||||
surface.blit(overlay, (self.x - r - cx, self.y - r - cy))
|
||||
|
||||
if self.img is not None:
|
||||
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy))
|
||||
else:
|
||||
@@ -1808,6 +1821,7 @@ def spawn_ability_doors(room) -> list[AbilityDoor]:
|
||||
rect = pygame.Rect(*map(int, d.get("rect", [0, 0, 0, 0])))
|
||||
door = AbilityDoor(rect, room_id=room.id)
|
||||
door.open = is_door_broken(room.id, rect)
|
||||
door.layer = int(d.get("layer", 0))
|
||||
out.append(door)
|
||||
return out
|
||||
|
||||
@@ -1862,13 +1876,14 @@ class AbilityPickup:
|
||||
class NPC:
|
||||
def __init__(self, x, y, npc_type: str = "vendor", trade_cost: int | None = None,
|
||||
offers=None, greet_light: str | None = None, greet_shade: str | None = None,
|
||||
greet_hint: str | None = None):
|
||||
greet_hint: str | None = None, layer: int = 0):
|
||||
self.type = npc_type
|
||||
self.trade_cost = int(trade_cost) if trade_cost is not None else None
|
||||
self.offers = offers if isinstance(offers, list) else []
|
||||
self.greet_light = greet_light
|
||||
self.greet_shade = greet_shade
|
||||
self.greet_hint = greet_hint
|
||||
self.layer = int(layer)
|
||||
self.rect = pygame.Rect(int(x), int(y), 36, 54)
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
@@ -4597,7 +4612,9 @@ def spawn_enemies_from_room(room: Room):
|
||||
w, h = cfg.get("rect", [40, 60])
|
||||
x0 = int(pos[0] - (w / 2))
|
||||
y0 = int(pos[1] - (h / 2))
|
||||
out.append(Enemy(x0, y0, t))
|
||||
enemy = Enemy(x0, y0, t)
|
||||
enemy.layer = int(ent.get("layer", 0))
|
||||
out.append(enemy)
|
||||
return out
|
||||
|
||||
|
||||
@@ -4630,7 +4647,8 @@ def spawn_npcs_from_room(room: Room):
|
||||
greet_hint = _npc(["defaults", t, "greet_hint"], None)
|
||||
|
||||
out.append(NPC(pos[0], pos[1], t, trade_cost=trade_cost, offers=offers,
|
||||
greet_light=greet_light, greet_shade=greet_shade, greet_hint=greet_hint))
|
||||
greet_light=greet_light, greet_shade=greet_shade, greet_hint=greet_hint,
|
||||
layer=int(ent.get("layer", 0))))
|
||||
return out
|
||||
|
||||
|
||||
@@ -4759,12 +4777,12 @@ def draw_speech_bubble(surface, text: str, center_x: int, bottom_y: int, hint_te
|
||||
surface.blit(hint, (hint_x, hint_y))
|
||||
|
||||
|
||||
def draw_npc_house(surface, npc: NPC, cx, cy):
|
||||
# Simple backdrop house behind the NPC
|
||||
def draw_house_shape(surface, cx, cy, center_x: int, bottom_y: int):
|
||||
"""Draw the procedural house with its bottom-center anchored at (center_x, bottom_y)."""
|
||||
house_w = 140
|
||||
house_h = 90
|
||||
base_x = int(npc.rect.centerx - house_w // 2 - cx)
|
||||
base_y = int(npc.rect.bottom - house_h + 6 - cy)
|
||||
base_x = int(center_x - house_w // 2 - cx)
|
||||
base_y = int(bottom_y - house_h - cy)
|
||||
|
||||
# wall
|
||||
pygame.draw.rect(surface, (180, 170, 150), (base_x, base_y + 30, house_w, house_h - 30))
|
||||
@@ -4787,6 +4805,84 @@ def draw_npc_house(surface, npc: NPC, cx, cy):
|
||||
pygame.draw.polygon(surface, (210, 170, 110), roof_pts)
|
||||
pygame.draw.polygon(surface, (135, 100, 65), roof_pts, 2)
|
||||
|
||||
|
||||
class HouseObject:
|
||||
"""A standalone house prop placed in a room (bottom-center anchored)."""
|
||||
def __init__(self, x: int, y: int, layer: int = 0):
|
||||
self.x = int(x)
|
||||
self.y = int(y)
|
||||
self.layer = int(layer)
|
||||
self.rect = pygame.Rect(int(x) - 70, int(y) - 90, 140, 90)
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
draw_house_shape(surface, cx, cy, self.x, self.y)
|
||||
|
||||
|
||||
def draw_layerable(surface, cx, cy):
|
||||
"""Draw all static room objects back-to-front by (layer, category).
|
||||
|
||||
Lower layer = further back. Ties use category order so default rooms keep a
|
||||
sensible look: props/houses (0) < platforms (1) < ability doors (2) < exits (3)
|
||||
< enemies (4) < NPCs (5). Any object's layer can be raised/lowered to push it
|
||||
into the foreground or background.
|
||||
"""
|
||||
items = []
|
||||
# props / houses
|
||||
for ls in light_sources:
|
||||
items.append((getattr(ls, "layer", 0), 0, "light", ls))
|
||||
for sp in save_points:
|
||||
items.append((getattr(sp, "layer", 0), 0, "savepoint", sp))
|
||||
for bj in butterfly_jars:
|
||||
items.append((getattr(bj, "layer", 0), 0, "jar", bj))
|
||||
for h in houses:
|
||||
items.append((getattr(h, "layer", 0), 0, "house", h))
|
||||
for qg in quest_givers:
|
||||
items.append((getattr(qg, "layer", 0), 0, "questgiver", qg))
|
||||
if diary_obj is not None:
|
||||
items.append((getattr(diary_obj, "layer", 0), 0, "diary", diary_obj))
|
||||
if reward_chest is not None:
|
||||
items.append((getattr(reward_chest, "layer", 0), 0, "rewardchest", reward_chest))
|
||||
if reward_ability is not None:
|
||||
items.append((getattr(reward_ability, "layer", 0), 0, "rewardability", reward_ability))
|
||||
# platforms
|
||||
for p in platforms:
|
||||
items.append((platform_layer.get(id(p), 0), 1, "platform", p))
|
||||
# ability doors
|
||||
for d in ability_doors:
|
||||
items.append((getattr(d, "layer", 0), 2, "door", d))
|
||||
# exits (respect locked-exit hiding)
|
||||
for ex in ROOM.exit_triggers:
|
||||
if ex.get("requires_boss_dead", False) and boss_alive_in_room():
|
||||
continue
|
||||
if ex.get("requires_ranged", False) and not player.has_ranged:
|
||||
continue
|
||||
items.append((ex.get("layer", 0), 3, "exit", ex))
|
||||
# enemies
|
||||
for en in enemies:
|
||||
items.append((getattr(en, "layer", 0), 4, "enemy", en))
|
||||
# npcs
|
||||
for n in npcs:
|
||||
items.append((getattr(n, "layer", 0), 5, "npc", n))
|
||||
|
||||
items.sort(key=lambda it: (it[0], it[1]))
|
||||
for _z, _cat, kind, obj in items:
|
||||
if kind in ("light", "savepoint", "jar", "house", "questgiver", "diary", "rewardchest", "rewardability", "door", "enemy", "npc"):
|
||||
obj.draw(surface, cx, cy)
|
||||
elif kind == "platform":
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
PLATFORM_COLOR,
|
||||
pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height),
|
||||
)
|
||||
elif kind == "exit":
|
||||
rr = obj["rect"]
|
||||
pygame.draw.rect(
|
||||
surface,
|
||||
(140, 220, 140),
|
||||
pygame.Rect(rr.x - cx, rr.y - cy, rr.width, rr.height),
|
||||
2,
|
||||
)
|
||||
|
||||
def draw_ability_icon_bg(surface, x: int, y: int, size: int, shade: bool):
|
||||
pad = 4
|
||||
w = size + pad * 2
|
||||
@@ -5020,6 +5116,7 @@ platforms_ground = ROOM.platforms_ground
|
||||
platforms_floating = ROOM.platforms_floating
|
||||
ground_platforms = ROOM.ground_platforms
|
||||
platforms = ROOM.platforms
|
||||
platform_layer = ROOM.platform_layer
|
||||
|
||||
|
||||
# ----------------------------
|
||||
@@ -5046,6 +5143,7 @@ tether_shots: list[TetherShot] = []
|
||||
light_sources: list[LightSource] = []
|
||||
save_points: list[SavePoint] = []
|
||||
butterfly_jars: list[ButterflyJar] = []
|
||||
houses: list[HouseObject] = []
|
||||
dust_puffs: list[DustPuff] = []
|
||||
class QuestGiver:
|
||||
"""Interactable environment object that hands out a quest."""
|
||||
@@ -5119,6 +5217,7 @@ def spawn_room_props(room: Room, player_obj: "Player"):
|
||||
lights: list[LightSource] = []
|
||||
save_points: list[SavePoint] = []
|
||||
butterfly_jars: list[ButterflyJar] = []
|
||||
houses: list[HouseObject] = []
|
||||
quest_givers: list[QuestGiver] = []
|
||||
diary_obj = None
|
||||
|
||||
@@ -5145,27 +5244,40 @@ def spawn_room_props(room: Room, player_obj: "Player"):
|
||||
else:
|
||||
coins = REWARD_CHEST_COINS
|
||||
reward_chest_obj = RewardChest(x, y, opened=player_obj.reward_chest_opened, coins=coins)
|
||||
reward_chest_obj.layer = int(p.get("layer", 0))
|
||||
elif p_type == "ability_pickup" and reward_ability_obj is None:
|
||||
ability_id = str(p.get("ability", "ranged"))
|
||||
if ability_id == "ranged" and player_obj.has_ranged:
|
||||
continue
|
||||
reward_ability_obj = AbilityPickup(x, y, ability_id=ability_id)
|
||||
reward_ability_obj.layer = int(p.get("layer", 0))
|
||||
elif p_type == "light_source":
|
||||
radius = int(p.get("radius", 180))
|
||||
lights.append(LightSource(x, y, radius))
|
||||
ls = LightSource(x, y, radius)
|
||||
ls.layer = int(p.get("layer", 0))
|
||||
lights.append(ls)
|
||||
elif p_type == "save_point":
|
||||
radius = int(p.get("radius", 90))
|
||||
save_points.append(SavePoint(x, y, radius))
|
||||
sp = SavePoint(x, y, radius)
|
||||
sp.layer = int(p.get("layer", 0))
|
||||
save_points.append(sp)
|
||||
elif p_type == "butterfly_jar":
|
||||
key = jar_key(room.id, x, y)
|
||||
if key not in COLLECTED_JARS:
|
||||
butterfly_jars.append(ButterflyJar(x, y, key=key))
|
||||
bj = ButterflyJar(x, y, key=key)
|
||||
bj.layer = int(p.get("layer", 0))
|
||||
butterfly_jars.append(bj)
|
||||
elif p_type == "quest_giver":
|
||||
quest_givers.append(QuestGiver(x, y, str(p.get("quest_id", ""))))
|
||||
qg = QuestGiver(x, y, str(p.get("quest_id", "")))
|
||||
qg.layer = int(p.get("layer", 0))
|
||||
quest_givers.append(qg)
|
||||
elif p_type == "diary":
|
||||
diary_obj = DiaryObject(x, y)
|
||||
diary_obj.layer = int(p.get("layer", 0))
|
||||
elif p_type == "house":
|
||||
houses.append(HouseObject(x, y, layer=int(p.get("layer", 0))))
|
||||
|
||||
return reward_chest_obj, reward_ability_obj, lights, save_points, butterfly_jars, quest_givers, diary_obj
|
||||
return reward_chest_obj, reward_ability_obj, lights, save_points, butterfly_jars, houses, quest_givers, diary_obj
|
||||
|
||||
|
||||
def rebuild_room_butterfly_jars():
|
||||
@@ -5186,7 +5298,7 @@ def rebuild_room_butterfly_jars():
|
||||
butterfly_jars.append(ButterflyJar(x, y, key=key))
|
||||
|
||||
|
||||
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, houses, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||
|
||||
def grant_full_set(set_id: str, rarity: str, equip: bool = True) -> bool:
|
||||
slots = ["helmet", "chest", "pants"]
|
||||
@@ -5231,9 +5343,10 @@ def grant_five_tempered() -> dict | None:
|
||||
def apply_room(room: Room, set_player_spawn=True):
|
||||
"""Updates room-dependent lists + respawns enemies. Keeps inventory/equipment."""
|
||||
global ROOM, KILL_Y, platforms_ground, platforms_floating, ground_platforms, platforms
|
||||
global platform_layer
|
||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
||||
global shatter_platforms, shatter_aoes, tether_shots
|
||||
global light_sources, save_points, butterfly_jars
|
||||
global light_sources, save_points, butterfly_jars, houses
|
||||
global dust_puffs
|
||||
global dust_puffs
|
||||
global reward_chest, reward_ability, quest_givers, diary_obj
|
||||
@@ -5248,6 +5361,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
platforms_floating = ROOM.platforms_floating
|
||||
ground_platforms = ROOM.ground_platforms
|
||||
platforms = ROOM.platforms
|
||||
platform_layer = ROOM.platform_layer
|
||||
|
||||
if is_town_room(ROOM):
|
||||
town_title.trigger(ROOM.id, town_display_name(ROOM))
|
||||
@@ -5267,6 +5381,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
light_sources = []
|
||||
save_points = []
|
||||
butterfly_jars = []
|
||||
houses = []
|
||||
dust_puffs = []
|
||||
dust_puffs = []
|
||||
ROOM_DROP_MULT = 1.0
|
||||
@@ -5278,7 +5393,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
reward_chest = None
|
||||
reward_ability = None
|
||||
diary_obj = None
|
||||
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, houses, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||
|
||||
if set_player_spawn:
|
||||
player.start_x = int(ROOM.player_spawn[0])
|
||||
@@ -5845,6 +5960,37 @@ def get_near_diary(player_obj, diary_obj) -> bool:
|
||||
return math.hypot(dx, dy) <= DiaryObject.INTERACT_RANGE
|
||||
|
||||
|
||||
def get_near_lantern(player_obj, save_point_list, range_px: int | None = None) -> SavePoint | None:
|
||||
"""Return the nearest lantern the player can interact with, or None."""
|
||||
best = None
|
||||
best_d = None
|
||||
px, py = player_obj.rect.midbottom
|
||||
for sp in save_point_list:
|
||||
r = range_px if range_px is not None else sp.radius
|
||||
if r <= 0:
|
||||
continue
|
||||
dx = px - sp.x
|
||||
dy = py - sp.y
|
||||
d2 = dx * dx + dy * dy
|
||||
if d2 <= (r * r):
|
||||
if best is None or d2 < best_d:
|
||||
best = sp
|
||||
best_d = d2
|
||||
return best
|
||||
|
||||
|
||||
def save_at_lantern(sp: SavePoint):
|
||||
"""Set the lantern as the respawn point and save the game."""
|
||||
global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS
|
||||
LANTERN_RESPAWN_ROOM = ROOM.id
|
||||
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
||||
_do_save_game()
|
||||
pickup_feed.push_center("Lantern - game saved", duration=2.0)
|
||||
# Butterfly jars lost on death respawn after saving at a lantern in a different room.
|
||||
if respawn_jars_if_elsewhere():
|
||||
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
||||
|
||||
|
||||
def _ellipsize_text(text: str, fnt, max_w: int) -> str:
|
||||
if fnt.size(text)[0] <= max_w:
|
||||
return text
|
||||
@@ -6783,7 +6929,6 @@ pause_menu_index = 0
|
||||
settings_index = 0
|
||||
settings_wait_action: str | None = None
|
||||
pause_use_mouse = False
|
||||
prev_near_save_point = False
|
||||
while running:
|
||||
dt = clock.tick(60) / 1000.0
|
||||
|
||||
@@ -6895,7 +7040,10 @@ while running:
|
||||
pickup_feed.push_center(f"Debug: +5 {inst.get('name', 'Item')} (tempered)", duration=3.0)
|
||||
|
||||
if key_is("interact", e.key) and not inv_ui.visible and not was_trade_visible and not diary_ui.visible:
|
||||
if get_near_diary(player, diary_obj):
|
||||
near_lantern = get_near_lantern(player, save_points)
|
||||
if near_lantern:
|
||||
save_at_lantern(near_lantern)
|
||||
elif get_near_diary(player, diary_obj):
|
||||
diary_ui.toggle()
|
||||
else:
|
||||
near_npc = get_near_npc(player, npcs)
|
||||
@@ -6961,16 +7109,9 @@ while running:
|
||||
# draw last frame + pause overlay
|
||||
screen.fill(BG)
|
||||
|
||||
# NPC backdrop houses (behind platforms)
|
||||
for npc in npcs:
|
||||
if npc.type == "vendor":
|
||||
draw_npc_house(screen, npc, camera_x, camera_y)
|
||||
# Layerable room objects (props, houses, platforms, doors, exits, enemies, NPCs)
|
||||
draw_layerable(screen, camera_x, camera_y)
|
||||
|
||||
for p in platforms:
|
||||
pygame.draw.rect(
|
||||
screen, PLATFORM_COLOR,
|
||||
pygame.Rect(p.x - camera_x, p.y - camera_y, p.width, p.height)
|
||||
)
|
||||
for sp in shatter_platforms:
|
||||
sp.draw(screen, camera_x, camera_y)
|
||||
for aoe in shatter_aoes:
|
||||
@@ -6978,20 +7119,6 @@ while running:
|
||||
for dp in dust_puffs:
|
||||
dp.draw(screen, camera_x, camera_y)
|
||||
|
||||
for ex in ROOM.exit_triggers:
|
||||
if ex.get("requires_boss_dead", False) and boss_alive_in_room():
|
||||
continue
|
||||
if ex.get("requires_ranged", False) and not player.has_ranged:
|
||||
continue
|
||||
|
||||
rr = ex["rect"]
|
||||
pygame.draw.rect(
|
||||
screen,
|
||||
(140, 220, 140),
|
||||
pygame.Rect(rr.x - camera_x, rr.y - camera_y, rr.width, rr.height),
|
||||
2,
|
||||
)
|
||||
|
||||
for drop in loot_drops:
|
||||
drop.draw(screen, camera_x, camera_y)
|
||||
for c in coin_drops:
|
||||
@@ -7004,25 +7131,10 @@ while running:
|
||||
pr.draw(screen, camera_x, camera_y)
|
||||
for ts in tether_shots:
|
||||
ts.draw(screen, camera_x, camera_y)
|
||||
for ls in light_sources:
|
||||
ls.draw(screen, camera_x, camera_y)
|
||||
for sp in save_points:
|
||||
sp.draw(screen, camera_x, camera_y)
|
||||
for bj in butterfly_jars:
|
||||
bj.draw(screen, camera_x, camera_y)
|
||||
if corpse is not None and corpse.room_id == ROOM.id:
|
||||
corpse.draw(screen, camera_x, camera_y)
|
||||
|
||||
player.draw(screen, camera_x, camera_y)
|
||||
for npc in npcs:
|
||||
npc.draw(screen, camera_x, camera_y)
|
||||
for en in enemies:
|
||||
en.draw(screen, camera_x, camera_y)
|
||||
|
||||
if reward_chest:
|
||||
reward_chest.draw(screen, camera_x, camera_y)
|
||||
if reward_ability:
|
||||
reward_ability.draw(screen, camera_x, camera_y)
|
||||
|
||||
if VIGNETTE_LIGHT and VIGNETTE_SHADE:
|
||||
if player.shade:
|
||||
@@ -7108,7 +7220,8 @@ while running:
|
||||
player.charge_regen_mult = 2.0
|
||||
break
|
||||
|
||||
# Lantern save point: touching a lantern sets the respawn point + auto-saves.
|
||||
# Lantern save point: being near a lantern enables the "Press F to save" prompt;
|
||||
# the actual save + respawn point update happens when the player presses interact.
|
||||
near_save_point = False
|
||||
for sp in save_points:
|
||||
px, py = player.rect.midbottom
|
||||
@@ -7116,16 +7229,7 @@ while running:
|
||||
dy = py - sp.y
|
||||
if (dx * dx + dy * dy) <= (sp.radius * sp.radius):
|
||||
near_save_point = True
|
||||
LANTERN_RESPAWN_ROOM = ROOM.id
|
||||
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
||||
break
|
||||
if near_save_point and not prev_near_save_point:
|
||||
_do_save_game()
|
||||
pickup_feed.push_center("Lantern - game saved", duration=2.0)
|
||||
# Butterfly jars lost on death respawn after saving at a lantern in a different room.
|
||||
if respawn_jars_if_elsewhere():
|
||||
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
||||
prev_near_save_point = near_save_point
|
||||
|
||||
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open]
|
||||
player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
||||
@@ -7403,9 +7507,9 @@ while running:
|
||||
f"Butterfly jar collected ({player.butterfly_jars}/{BUTTERFLY_JARS_MAX})",
|
||||
duration=2.5,
|
||||
)
|
||||
butterfly_jars.remove(bj)
|
||||
else:
|
||||
pickup_feed.push_center("You can't carry any more butterfly jars!", duration=2.0)
|
||||
butterfly_jars.remove(bj)
|
||||
for t in token_drops[:]:
|
||||
if player.rect.colliderect(t.rect):
|
||||
token = make_boss_token(t.boss_id)
|
||||
@@ -7434,23 +7538,9 @@ while running:
|
||||
# ----------------------------
|
||||
screen.fill(BG)
|
||||
|
||||
# NPC backdrop houses (behind platforms)
|
||||
for npc in npcs:
|
||||
if npc.type == "vendor":
|
||||
draw_npc_house(screen, npc, camera_x, camera_y)
|
||||
# Layerable room objects (props, houses, platforms, doors, exits, enemies, NPCs)
|
||||
draw_layerable(screen, camera_x, camera_y)
|
||||
|
||||
for ls in light_sources:
|
||||
ls.draw(screen, camera_x, camera_y)
|
||||
for sp in save_points:
|
||||
sp.draw(screen, camera_x, camera_y)
|
||||
for bj in butterfly_jars:
|
||||
bj.draw(screen, camera_x, camera_y)
|
||||
|
||||
for p in platforms:
|
||||
pygame.draw.rect(
|
||||
screen, PLATFORM_COLOR,
|
||||
pygame.Rect(p.x - camera_x, p.y - camera_y, p.width, p.height)
|
||||
)
|
||||
for sp in shatter_platforms:
|
||||
sp.draw(screen, camera_x, camera_y)
|
||||
for aoe in shatter_aoes:
|
||||
@@ -7458,26 +7548,6 @@ while running:
|
||||
for dp in dust_puffs:
|
||||
dp.draw(screen, camera_x, camera_y)
|
||||
|
||||
# ability doors (ranged-locked)
|
||||
for d in ability_doors:
|
||||
d.draw(screen, camera_x, camera_y)
|
||||
|
||||
# exits / doorways
|
||||
for ex in ROOM.exit_triggers:
|
||||
# hide locked exits
|
||||
if ex.get("requires_boss_dead", False) and boss_alive_in_room():
|
||||
continue
|
||||
if ex.get("requires_ranged", False) and not player.has_ranged:
|
||||
continue
|
||||
|
||||
rr = ex["rect"]
|
||||
pygame.draw.rect(
|
||||
screen,
|
||||
(140, 220, 140),
|
||||
pygame.Rect(rr.x - camera_x, rr.y - camera_y, rr.width, rr.height),
|
||||
2,
|
||||
)
|
||||
|
||||
# loot
|
||||
for drop in loot_drops:
|
||||
drop.draw(screen, camera_x, camera_y)
|
||||
@@ -7490,21 +7560,6 @@ while running:
|
||||
for t in token_drops:
|
||||
t.draw(screen, camera_x, camera_y)
|
||||
|
||||
# reward room props
|
||||
if reward_chest:
|
||||
reward_chest.draw(screen, camera_x, camera_y)
|
||||
if reward_ability:
|
||||
reward_ability.draw(screen, camera_x, camera_y)
|
||||
|
||||
# quest giver objects
|
||||
for qg in quest_givers:
|
||||
qg.draw(screen, camera_x, camera_y)
|
||||
|
||||
# diary object (boss token diary)
|
||||
if diary_obj:
|
||||
diary_obj.draw(screen, camera_x, camera_y)
|
||||
|
||||
|
||||
# shockwaves
|
||||
for sw in shockwaves:
|
||||
sw.draw(screen, camera_x, camera_y)
|
||||
@@ -7516,14 +7571,10 @@ while running:
|
||||
ts.draw(screen, camera_x, camera_y)
|
||||
|
||||
|
||||
# entities
|
||||
for npc in npcs:
|
||||
npc.draw(screen, camera_x, camera_y)
|
||||
# entities (NPCs/enemies are drawn by draw_layerable for layer support)
|
||||
if corpse is not None and corpse.room_id == ROOM.id:
|
||||
corpse.draw(screen, camera_x, camera_y)
|
||||
player.draw(screen, camera_x, camera_y)
|
||||
for en in enemies:
|
||||
en.draw(screen, camera_x, camera_y)
|
||||
|
||||
# NPC greeting bubble (shown near the player)
|
||||
greet_npc = get_near_npc(player, npcs)
|
||||
@@ -7576,6 +7627,17 @@ while running:
|
||||
int(diary_obj.rect.top - camera_y),
|
||||
)
|
||||
|
||||
# Lantern save hint bubble (shown near the player)
|
||||
if not inv_ui.visible and not trade_ui.visible and not diary_ui.visible:
|
||||
near_lantern = get_near_lantern(player, save_points)
|
||||
if near_lantern:
|
||||
draw_speech_bubble(
|
||||
screen,
|
||||
"Press F - Save at lantern",
|
||||
int(near_lantern.rect.centerx - camera_x),
|
||||
int(near_lantern.rect.top - camera_y),
|
||||
)
|
||||
|
||||
# Boss health bar (if boss exists)
|
||||
boss = next((en for en in enemies if getattr(en, "type_id", "") == "boss"), None)
|
||||
if boss is not None:
|
||||
|
||||
Reference in New Issue
Block a user