room 3 changes
chest, tiny jump and run with moving platforms
This commit is contained in:
@@ -43,6 +43,7 @@ Proves the core loop is fun before scaling to the full vision.
|
||||
|---|---|---|
|
||||
| Shade-gated interactions (doors, an NPC, an area) | S-M | Room JSON + existing toggle. |
|
||||
| New enemy behaviors (patrol, dodge-and-leap, burrow-shooter) | M | Enemy system supports it. |
|
||||
| Enemy line-of-sight detection (radius -> raycast) | S-M | Replace/augment euclidean aggro (`main.py:4329`) with a `Rect.clipline` raycast against solid platforms so walls block vision. Per-type `"detection": radius/los` config in `enemy_types.json` + editor toggle. Edge cases: skip the enemy's own support platform, sample player center+feet for tolerance, apply LOS to boss slam `trigger_close/far`. |
|
||||
| ~~Quick recharge pickup~~ | S | **DONE (2026-09-29)**: butterfly jars — collect up to 3, press G to instantly refill Charge. Lost on death; respawn at their points after saving at a lantern in a different room or reloading. |
|
||||
| Ability tweaks: ranged upgrades, ground-slam knockback/iframe, light-step buffs | M | Picks up remaining notes ideas. |
|
||||
| Region-2 boss + ability pacing | M | Re-align pacing (Ranged already exists). |
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"room3_top_right": [
|
||||
1480,
|
||||
220
|
||||
],
|
||||
"room3_bottom_left": [
|
||||
140,
|
||||
800
|
||||
]
|
||||
},
|
||||
"platforms": [
|
||||
@@ -88,29 +92,149 @@
|
||||
"y": 460,
|
||||
"w": 20,
|
||||
"h": 160,
|
||||
"group": "trigger"
|
||||
"group": "trigger",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1100,
|
||||
"y": 620,
|
||||
"w": 320,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 800,
|
||||
"y": 320,
|
||||
"w": 160,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 1580,
|
||||
"y": 320,
|
||||
"w": 20,
|
||||
"h": 580,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 1220,
|
||||
"y": 760,
|
||||
"w": 360,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 880,
|
||||
"w": 1560,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 640,
|
||||
"w": 20,
|
||||
"h": 240,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"y": 640,
|
||||
"w": 20,
|
||||
"h": 240,
|
||||
"group": "trigger",
|
||||
"name": "first_quest_complete"
|
||||
},
|
||||
{
|
||||
"x": 580,
|
||||
"y": 640,
|
||||
"w": 20,
|
||||
"h": 120,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 740,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 400,
|
||||
"y": 400,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 220,
|
||||
"y": 400,
|
||||
"w": 80,
|
||||
"h": 20,
|
||||
"group": "moving",
|
||||
"path": [
|
||||
[
|
||||
220,
|
||||
400
|
||||
],
|
||||
[
|
||||
220,
|
||||
260
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"x": 40,
|
||||
"y": 220,
|
||||
"w": 140,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
},
|
||||
{
|
||||
"x": 1520,
|
||||
"y": 640,
|
||||
"w": 60,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
}
|
||||
],
|
||||
"enemies": [],
|
||||
"npcs": [],
|
||||
"props": [
|
||||
"enemies": [
|
||||
{
|
||||
"x": 1300,
|
||||
"y": 220,
|
||||
"type": "player",
|
||||
"radius": 180,
|
||||
"object_id": "player",
|
||||
"layer": 0,
|
||||
"quest_id": ""
|
||||
"x": 600,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 540,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
},
|
||||
{
|
||||
"x": 200,
|
||||
"y": 580,
|
||||
"type": "enemy"
|
||||
},
|
||||
{
|
||||
"x": 520,
|
||||
"y": 700,
|
||||
"type": "enemy"
|
||||
},
|
||||
{
|
||||
"x": 1520,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
}
|
||||
],
|
||||
"npcs": [],
|
||||
"props": [
|
||||
{
|
||||
"x": 960,
|
||||
"y": 580,
|
||||
"type": "quest_giver",
|
||||
"radius": 180,
|
||||
"object_id": "quest_point",
|
||||
"layer": 0,
|
||||
"quest_id": "first_kills"
|
||||
"quest_id": "quest_first_kills"
|
||||
}
|
||||
],
|
||||
"exits": [
|
||||
@@ -127,6 +251,18 @@
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 40,
|
||||
"y": 780,
|
||||
"w": 80,
|
||||
"h": 100,
|
||||
"to": "",
|
||||
"spawn_id": "",
|
||||
"spawn": [
|
||||
120,
|
||||
780
|
||||
]
|
||||
}
|
||||
],
|
||||
"ability_doors": [],
|
||||
|
||||
@@ -348,6 +348,8 @@
|
||||
if(Array.isArray(rect.path)) out.path = deepClone(rect.path);
|
||||
if(rect.speed !== undefined) out.speed = +rect.speed;
|
||||
if(rect.name) out.name = String(rect.name);
|
||||
if(rect.quest) out.quest = String(rect.quest);
|
||||
if(rect.quest_complete) out.quest_complete = String(rect.quest_complete);
|
||||
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
||||
return out;
|
||||
}
|
||||
@@ -466,7 +468,7 @@
|
||||
const type = p.type || p.kind || "light_source";
|
||||
const object_id = p.object_id || p.objectId || null;
|
||||
const radius = +p.radius || getDefaultRadiusForType(type, object_id);
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "" });
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "", coins: Array.isArray(p.coins) ? deepClone(p.coins) : undefined });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2133,6 +2135,14 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">name</div>
|
||||
<input id="ptname" type="text" value="${p.name||""}" placeholder="e.g. opens_bridge"/>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">unlock when quest accepted (id)</div>
|
||||
<input id="ptquest" type="text" value="${p.quest||""}" placeholder="e.g. quest_first_kills"/>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">unlock when quest complete (id)</div>
|
||||
<input id="ptqcomplete" type="text" value="${p.quest_complete||""}" placeholder="e.g. quest_first_kills"/>
|
||||
</div>` : ""}
|
||||
${(p.group||"ground") === "hazard" ? `
|
||||
<div style="margin-top:8px;">
|
||||
@@ -2358,6 +2368,14 @@ Cancel = Choose whether to remove without saving`);
|
||||
const v = ptname.value.trim();
|
||||
if(v){ p.name = v; } else { delete p.name; }
|
||||
}));
|
||||
const ptquest=q("ptquest"); if(ptquest) ptquest.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = ptquest.value.trim();
|
||||
if(v){ p.quest = v; } else { delete p.quest; }
|
||||
}));
|
||||
const ptqcomplete=q("ptqcomplete"); if(ptqcomplete) ptqcomplete.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = ptqcomplete.value.trim();
|
||||
if(v){ p.quest_complete = v; } else { delete p.quest_complete; }
|
||||
}));
|
||||
const ptspeed=q("ptspeed"); if(ptspeed) ptspeed.addEventListener("change", ()=>pushAnd(()=>{ p.speed = Math.max(1, num(ptspeed.value)); }));
|
||||
const hzrespawn=q("hzrespawn");
|
||||
if(hzrespawn){
|
||||
@@ -3259,6 +3277,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(p.speed !== undefined) obj.speed = p.speed;
|
||||
}
|
||||
if(p.name) obj.name = p.name;
|
||||
if(p.quest) obj.quest = p.quest;
|
||||
if(p.quest_complete) obj.quest_complete = p.quest_complete;
|
||||
if(Array.isArray(p.respawn)) obj.respawn = p.respawn;
|
||||
platforms[group].push(obj);
|
||||
} else {
|
||||
@@ -3311,6 +3331,7 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(p.object_id) out.object_id = p.object_id;
|
||||
if(p.layer) out.layer = p.layer;
|
||||
if(p.quest_id) out.quest_id = p.quest_id;
|
||||
if(Array.isArray(p.coins)) out.coins = p.coins;
|
||||
return out;
|
||||
});
|
||||
|
||||
|
||||
+95
-15
@@ -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,8 +5030,9 @@ 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:
|
||||
if trigger_wall_blocking(p):
|
||||
items.append((platform_layer.get(id(p), 0), 1, "trigger", p))
|
||||
# ability doors
|
||||
for d in ability_doors:
|
||||
@@ -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