room 3 changes
chest, tiny jump and run with moving platforms
This commit is contained in:
+96
-16
@@ -937,6 +937,10 @@ def normalize_room_json(room_json: dict) -> dict:
|
||||
obj["speed"] = p["speed"]
|
||||
if "name" in p:
|
||||
obj["name"] = p["name"]
|
||||
if "quest" in p:
|
||||
obj["quest"] = p["quest"]
|
||||
if "quest_complete" in p:
|
||||
obj["quest_complete"] = p["quest_complete"]
|
||||
if isinstance(p.get("respawn"), (list, tuple)) and len(p["respawn"]) >= 2:
|
||||
obj["respawn"] = p["respawn"]
|
||||
grouped.setdefault(group, []).append(obj)
|
||||
@@ -1068,7 +1072,12 @@ class Room:
|
||||
if isinstance(tp, dict):
|
||||
rect = pygame.Rect(int(tp.get("x", 0)), int(tp.get("y", 0)), int(tp.get("w", 0)), int(tp.get("h", 0)))
|
||||
self.platform_layer[id(rect)] = int(tp.get("layer", 0))
|
||||
self.platform_meta[id(rect)] = {"name": str(tp.get("name", ""))}
|
||||
meta = {"name": str(tp.get("name", ""))}
|
||||
if tp.get("quest"):
|
||||
meta["quest"] = str(tp.get("quest"))
|
||||
if tp.get("quest_complete"):
|
||||
meta["quest_complete"] = str(tp.get("quest_complete"))
|
||||
self.platform_meta[id(rect)] = meta
|
||||
self.platforms_trigger.append(rect)
|
||||
elif isinstance(tp, (list, tuple)) and len(tp) >= 4:
|
||||
rect = pygame.Rect(int(tp[0]), int(tp[1]), int(tp[2]), int(tp[3]))
|
||||
@@ -1931,11 +1940,12 @@ class RewardChest:
|
||||
|
||||
def open(self, player):
|
||||
if self.opened:
|
||||
return
|
||||
return False
|
||||
light_amt, dark_amt = self.coins
|
||||
player.coins_light += int(light_amt)
|
||||
player.coins_dark += int(dark_amt)
|
||||
self.opened = True
|
||||
return True
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
x = int(self.rect.x - cx)
|
||||
@@ -3248,6 +3258,7 @@ class Player:
|
||||
self.rect = pygame.Rect(int(x), int(y), PLAYER_HITBOX_W, PLAYER_HITBOX_H)
|
||||
self.vel_y = 0.0
|
||||
self.on_ground = False
|
||||
self.ground_platform = None
|
||||
self.shade = False
|
||||
|
||||
self.charge = float(BASE_MAX_CHARGE)
|
||||
@@ -3261,7 +3272,7 @@ class Player:
|
||||
# --- abilities ---
|
||||
self.has_ranged = False
|
||||
self.ranged_cd = 0.0
|
||||
self.reward_chest_opened = False
|
||||
self.opened_chests: set[str] = set()
|
||||
self.has_shatterstep = True
|
||||
self.shatterstep_timer = 0.0
|
||||
self.shatterstep_cd = 0.0
|
||||
@@ -3889,9 +3900,11 @@ class Player:
|
||||
self.vel_y += gravity
|
||||
dy = self.vel_y
|
||||
|
||||
# Horizontal collision
|
||||
# Horizontal collision (ignore the platform the player is standing on)
|
||||
self.rect.x += int(dx)
|
||||
for p in platforms:
|
||||
if p is self.ground_platform:
|
||||
continue
|
||||
if self.rect.colliderect(p):
|
||||
if dx > 0:
|
||||
self.rect.right = p.left
|
||||
@@ -3901,12 +3914,14 @@ class Player:
|
||||
# Vertical collision
|
||||
self.rect.y += int(dy)
|
||||
self.on_ground = False
|
||||
self.ground_platform = None
|
||||
for p in platforms:
|
||||
if self.rect.colliderect(p):
|
||||
if dy > 0:
|
||||
self.rect.bottom = p.top
|
||||
self.vel_y = 0
|
||||
self.on_ground = True
|
||||
self.ground_platform = p
|
||||
elif dy < 0:
|
||||
self.rect.top = p.bottom
|
||||
self.vel_y = 0
|
||||
@@ -4922,6 +4937,25 @@ class HouseObject:
|
||||
draw_house_shape(surface, cx, cy, self.x, self.y)
|
||||
|
||||
|
||||
def trigger_wall_blocking(rect) -> bool:
|
||||
"""A trigger wall blocks while its unlock condition is NOT met.
|
||||
- ``quest``: wall stays until the quest is accepted (becomes active).
|
||||
- ``quest_complete``: wall stays until the quest objective is done
|
||||
(is_complete, or the quest has been completed at least once).
|
||||
Walls with no condition are inert placeholders (never block)."""
|
||||
meta = platform_meta.get(id(rect), {})
|
||||
qid = meta.get("quest")
|
||||
if qid:
|
||||
return qid not in quest_manager.active
|
||||
qc = meta.get("quest_complete")
|
||||
if qc:
|
||||
return not (quest_manager.is_complete(qc) or qc in quest_manager.ever_completed)
|
||||
return False
|
||||
|
||||
|
||||
prev_moving_pos: dict[int, tuple[int, int]] = {}
|
||||
|
||||
|
||||
def update_moving_platforms(dt: float):
|
||||
"""Advance moving platforms along their waypoint paths (top-left follows the path)."""
|
||||
for rect in platforms_moving:
|
||||
@@ -4996,9 +5030,10 @@ def draw_layerable(surface, cx, cy):
|
||||
# hazard zones (red) layer alongside platforms
|
||||
for p in platforms_hazard:
|
||||
items.append((platform_layer.get(id(p), 0), 1, "hazard", p))
|
||||
# trigger placeholders (yellow, non-solid, no behavior yet)
|
||||
# trigger walls: only drawn while blocking (quest not yet accepted)
|
||||
for p in platforms_trigger:
|
||||
items.append((platform_layer.get(id(p), 0), 1, "trigger", p))
|
||||
if trigger_wall_blocking(p):
|
||||
items.append((platform_layer.get(id(p), 0), 1, "trigger", p))
|
||||
# ability doors
|
||||
for d in ability_doors:
|
||||
items.append((getattr(d, "layer", 0), 2, "door", d))
|
||||
@@ -5409,7 +5444,9 @@ def spawn_room_props(room: Room, player_obj: "Player"):
|
||||
coins = (int(coins[0]), int(coins[1]))
|
||||
else:
|
||||
coins = REWARD_CHEST_COINS
|
||||
reward_chest_obj = RewardChest(x, y, opened=player_obj.reward_chest_opened, coins=coins)
|
||||
cid = str(p.get("object_id") or "") or f"{room.id}@{int(x)},{int(y)}"
|
||||
reward_chest_obj = RewardChest(x, y, opened=cid in player_obj.opened_chests, coins=coins)
|
||||
reward_chest_obj.id = cid
|
||||
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"))
|
||||
@@ -5801,6 +5838,7 @@ class QuestManager:
|
||||
self.quests: dict[str, Quest] = {}
|
||||
self.active: dict[str, dict] = {} # quest_id -> {"progress": int}
|
||||
self.completed_one_time: set[str] = set()
|
||||
self.ever_completed: set[str] = set() # completed at least once (incl. repeatable)
|
||||
self.tokens = 0
|
||||
|
||||
def register(self, qdef: dict) -> Quest | None:
|
||||
@@ -5855,6 +5893,7 @@ class QuestManager:
|
||||
def _complete(self, quest_id: str):
|
||||
q = self.quests[quest_id]
|
||||
self.active.pop(quest_id, None)
|
||||
self.ever_completed.add(quest_id)
|
||||
if q.is_repeatable:
|
||||
self.tokens += q.token_count()
|
||||
else:
|
||||
@@ -6543,13 +6582,14 @@ def save_game(name: str = "auto") -> str:
|
||||
"coins_dark": player.coins_dark,
|
||||
"butterfly_jars": player.butterfly_jars,
|
||||
"has_ranged": player.has_ranged,
|
||||
"reward_chest_opened": player.reward_chest_opened,
|
||||
"opened_chests": sorted(player.opened_chests),
|
||||
"equipment": {s: it for s, it in player.equipment.items()},
|
||||
},
|
||||
"inventory": inventory.items,
|
||||
"quests": {
|
||||
"active": quest_manager.active,
|
||||
"completed_one_time": sorted(quest_manager.completed_one_time),
|
||||
"ever_completed": sorted(quest_manager.ever_completed),
|
||||
"tokens": quest_manager.tokens,
|
||||
},
|
||||
"boss_diary": {"slotted": boss_diary.slotted},
|
||||
@@ -6577,7 +6617,10 @@ def apply_save_data(data: dict):
|
||||
player.coins_dark = int(pdata.get("coins_dark", 0))
|
||||
player.butterfly_jars = int(pdata.get("butterfly_jars", 0))
|
||||
player.has_ranged = bool(pdata.get("has_ranged", False))
|
||||
player.reward_chest_opened = bool(pdata.get("reward_chest_opened", False))
|
||||
player.opened_chests = set(str(c) for c in (pdata.get("opened_chests") or []))
|
||||
# migrate the legacy single-flag save (only the reward-room default chest existed)
|
||||
if pdata.get("reward_chest_opened"):
|
||||
player.opened_chests.add(f"{REWARD_ROOM_ID}@{REWARD_CHEST_POS[0]},{REWARD_CHEST_POS[1]}")
|
||||
|
||||
eq = pdata.get("equipment") or {}
|
||||
player.equipment = {s: None for s in ("helmet", "chest", "pants", "necklace")}
|
||||
@@ -6592,6 +6635,7 @@ def apply_save_data(data: dict):
|
||||
qdata = data.get("quests") or {}
|
||||
quest_manager.active = {k: dict(v) for k, v in (qdata.get("active") or {}).items()}
|
||||
quest_manager.completed_one_time = set(qdata.get("completed_one_time") or [])
|
||||
quest_manager.ever_completed = set(qdata.get("ever_completed") or [])
|
||||
quest_manager.tokens = int(qdata.get("tokens", 0))
|
||||
|
||||
bdata = data.get("boss_diary") or {}
|
||||
@@ -6649,11 +6693,11 @@ def fresh_save_data() -> dict:
|
||||
"coins_dark": 0,
|
||||
"butterfly_jars": 0,
|
||||
"has_ranged": False,
|
||||
"reward_chest_opened": False,
|
||||
"opened_chests": [],
|
||||
"equipment": {},
|
||||
},
|
||||
"inventory": [],
|
||||
"quests": {"active": {}, "completed_one_time": [], "tokens": 0},
|
||||
"quests": {"active": {}, "completed_one_time": [], "ever_completed": [], "tokens": 0},
|
||||
"boss_diary": {"slotted": {}},
|
||||
"defeated_bosses": [],
|
||||
"broken_doors": [],
|
||||
@@ -7406,10 +7450,35 @@ while running:
|
||||
near_save_point = True
|
||||
break
|
||||
|
||||
# Move moving platforms before collision is computed so the player collides correctly.
|
||||
update_moving_platforms(dt)
|
||||
# Determine the moving platform the player is currently standing on (before it moves).
|
||||
support_moving = None
|
||||
if player.on_ground:
|
||||
for rect in platforms_moving:
|
||||
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
||||
player.rect.right > rect.left and player.rect.left < rect.right):
|
||||
support_moving = rect
|
||||
break
|
||||
|
||||
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open]
|
||||
# Move moving platforms; carry the player along with their support platform BEFORE
|
||||
# collision so they stay on it (no slipping off / bobbing when moving down).
|
||||
for rect in platforms_moving:
|
||||
prev_moving_pos[id(rect)] = (rect.x, rect.y)
|
||||
update_moving_platforms(dt)
|
||||
if support_moving is not None:
|
||||
px, py = prev_moving_pos.get(id(support_moving))
|
||||
if px is not None:
|
||||
dx = support_moving.x - px
|
||||
dy = support_moving.y - py
|
||||
if dx or dy:
|
||||
player.rect.x += dx
|
||||
player.rect.y += dy
|
||||
player.vel_y = 0.0
|
||||
player.ground_platform = support_moving
|
||||
|
||||
# Trigger walls: solid only while their unlock quest is not yet active.
|
||||
blocking_triggers = [t for t in platforms_trigger if trigger_wall_blocking(t)]
|
||||
|
||||
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open] + blocking_triggers
|
||||
player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
||||
|
||||
# Shade + clipping an enemy drains charge twice as fast (shade still avoids damage).
|
||||
@@ -7420,6 +7489,17 @@ while running:
|
||||
|
||||
player.update(keys, player_collision_platforms, dt)
|
||||
|
||||
# Glue the player to any moving platform they're standing on, so upward movement
|
||||
# doesn't leave a gap that makes on_ground flicker (bobbing / eventual fall-off).
|
||||
for rect in platforms_moving:
|
||||
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
||||
player.rect.right > rect.left and player.rect.left < rect.right):
|
||||
player.rect.bottom = rect.top
|
||||
player.vel_y = 0.0
|
||||
player.on_ground = True
|
||||
player.ground_platform = rect
|
||||
break
|
||||
|
||||
# Hazard zones: touching one deals 1 damage and teleports the player to its linked respawn
|
||||
# (defaults to the room's player spawn). A lethal hit falls through to the normal death respawn.
|
||||
for hz in platforms_hazard:
|
||||
@@ -7664,8 +7744,8 @@ while running:
|
||||
# Pickup loot (only when inventory closed)
|
||||
if not inv_ui.visible and not trade_ui.visible:
|
||||
if reward_chest and player.rect.colliderect(reward_chest.rect):
|
||||
reward_chest.open(player)
|
||||
player.reward_chest_opened = True
|
||||
if reward_chest.open(player):
|
||||
player.opened_chests.add(reward_chest.id)
|
||||
|
||||
if reward_ability and player.rect.colliderect(reward_ability.rect):
|
||||
if reward_ability.ability_id == "ranged":
|
||||
|
||||
Reference in New Issue
Block a user