smol stuff again

added fairy sprite, unequip inventory, proper death recovery
This commit is contained in:
2026-09-29 09:41:05 +02:00
parent 2a86881108
commit db8aa53d3f
3 changed files with 219 additions and 52 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

+55 -6
View File
@@ -1208,6 +1208,18 @@ BUTTERFLY_RAW = _load_image_or_none(BUTTERFLY_PATH)
BUTTERFLY_JAR_PATH = OBJECTS_DIR / "flow_butterfly_jar1.png" BUTTERFLY_JAR_PATH = OBJECTS_DIR / "flow_butterfly_jar1.png"
BUTTERFLY_JAR_RAW = _load_image_or_none(BUTTERFLY_JAR_PATH) BUTTERFLY_JAR_RAW = _load_image_or_none(BUTTERFLY_JAR_PATH)
# --- Fairy companion (hovers behind the player's head) ---
FAIRY_PATH = DATA_ROOT / "sprites" / "npcs" / "fairy1.png"
FAIRY_RAW = _load_image_or_none(FAIRY_PATH)
FAIRY_SPRITE = None
if FAIRY_RAW is not None:
try:
fs = max(1, int(FAIRY_RAW.get_width() * PLAYER_SPRITE_SCALE * 0.55))
FAIRY_SPRITE = pygame.transform.smoothscale(FAIRY_RAW, (fs, fs))
except Exception:
FAIRY_SPRITE = FAIRY_RAW
FAIRY_SPRITE_FLIPPED = pygame.transform.flip(FAIRY_SPRITE, True, False) if FAIRY_SPRITE is not None else None
# ---------------------------- # ----------------------------
@@ -2357,6 +2369,8 @@ class InventoryUI:
self.equip_idx = (self.equip_idx - 1) % len(self.equip_slots) self.equip_idx = (self.equip_idx - 1) % len(self.equip_slots)
elif key == key_for("ui_down"): elif key == key_for("ui_down"):
self.equip_idx = (self.equip_idx + 1) % len(self.equip_slots) self.equip_idx = (self.equip_idx + 1) % len(self.equip_slots)
elif key in (key_for("ui_confirm"), key_for("ui_confirm_alt")):
self._unequip_selected()
def _equip_selected_swap(self): def _equip_selected_swap(self):
if len(self.inv.items) == 0: if len(self.inv.items) == 0:
@@ -2382,6 +2396,18 @@ class InventoryUI:
self._clamp_cursor() self._clamp_cursor()
def _unequip_selected(self):
"""Move the item in the selected equipment slot back to the inventory."""
slot = self.equip_slots[self.equip_idx]
item = self.player.equipment.get(slot)
if item is None:
return
self.player.equipment[slot] = None
self.inv.items.append(item)
self.player.recalc_bonuses()
self.player._clamp_resources()
self._clamp_cursor()
def _blit(self, surface, text, x, y, color=(230, 230, 230)): def _blit(self, surface, text, x, y, color=(230, 230, 230)):
surface.blit(self.font.render(text, True, color), (x, y)) surface.blit(self.font.render(text, True, color), (x, y))
@@ -2778,7 +2804,7 @@ class InventoryUI:
# Hint text (NOW hint_y exists) # Hint text (NOW hint_y exists)
self._blit( self._blit(
surface, surface,
"UP/DOWN move ENTER equip LEFT/RIGHT switch panel", "UP/DOWN move ENTER equip / unequip LEFT/RIGHT switch panel",
self.x + self.pad + 6, self.x + self.pad + 6,
hint_y + 4, hint_y + 4,
(160, 160, 160), (160, 160, 160),
@@ -3213,6 +3239,7 @@ class Player:
self.idle_frame = 0 self.idle_frame = 0
self.idle_anim_time = 0.0 self.idle_anim_time = 0.0
self.idle_bob_t = 0.0 self.idle_bob_t = 0.0
self.fairy_bob_t = random.random() * 10.0
self.idle_hold_time = 0.0 self.idle_hold_time = 0.0
self.ground_contact_timer = 0.0 self.ground_contact_timer = 0.0
self.is_idle = False self.is_idle = False
@@ -3661,6 +3688,7 @@ class Player:
self.just_jumped = False self.just_jumped = False
self.just_landed = False self.just_landed = False
self.just_air_jumped = False self.just_air_jumped = False
self.fairy_bob_t += dt
if self.invincibility_timer > 0: if self.invincibility_timer > 0:
self.invincibility_timer -= dt self.invincibility_timer -= dt
@@ -3839,6 +3867,20 @@ class Player:
self._clamp_resources() self._clamp_resources()
def _draw_fairy(self, surface, cx, cy, px, py, pw, ph):
"""Draw the fairy companion hovering behind/beside the player's head."""
img = FAIRY_SPRITE if self.facing_right else FAIRY_SPRITE_FLIPPED
if img is None or pw <= 0 or ph <= 0:
return
fw, fh = img.get_size()
# Trail behind the player (opposite the facing direction) at head height.
trail = max(1, (pw + fw) // 3)
off = -trail if self.facing_right else trail
fx = px + (pw - fw) // 2 + off
fairy_cy = py + int(ph * 0.18)
bob = int(math.sin(self.fairy_bob_t * 2.2) * 3)
surface.blit(img, (fx, fairy_cy - fh // 2 + bob))
def draw(self, surface, cx, cy): def draw(self, surface, cx, cy):
if self.healing: if self.healing:
dur = self.effective_heal_duration() dur = self.effective_heal_duration()
@@ -3855,6 +3897,7 @@ class Player:
if self.sprite_right is None: if self.sprite_right is None:
player_color = PLAYER_HIT_COLOR if self.invincibility_timer > 0 else (PLAYER_DARK if self.shade else PLAYER_LIGHT) player_color = PLAYER_HIT_COLOR if self.invincibility_timer > 0 else (PLAYER_DARK if self.shade else PLAYER_LIGHT)
pr = pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height) pr = pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height)
self._draw_fairy(surface, cx, cy, self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height)
pygame.draw.rect(surface, player_color, pr) pygame.draw.rect(surface, player_color, pr)
else: else:
use_idle = self.is_idle and self.idle_right_frames and (self.idle_hold_time >= PLAYER_IDLE_START_DELAY) use_idle = self.is_idle and self.idle_right_frames and (self.idle_hold_time >= PLAYER_IDLE_START_DELAY)
@@ -3879,6 +3922,7 @@ class Player:
draw_y = self.rect.y - cy - (img.get_height() - self.rect.height) + PLAYER_SPRITE_OFFSET_Y draw_y = self.rect.y - cy - (img.get_height() - self.rect.height) + PLAYER_SPRITE_OFFSET_Y
if self.is_idle and (not self.shade or not self.has_shade_sprite): if self.is_idle and (not self.shade or not self.has_shade_sprite):
draw_y -= int(math.sin(self.idle_bob_t) * PLAYER_IDLE_BOB_AMP) draw_y -= int(math.sin(self.idle_bob_t) * PLAYER_IDLE_BOB_AMP)
self._draw_fairy(surface, cx, cy, draw_x, draw_y, img.get_width(), img.get_height())
surface.blit(img, (draw_x, draw_y)) surface.blit(img, (draw_x, draw_y))
# Debug: show player hitbox # Debug: show player hitbox
@@ -5268,9 +5312,10 @@ def reset_room_combat_state():
def apply_death_penalty(): def apply_death_penalty():
"""On death: lose butterfly jars for good and drop coins into a corpse. """On death: lose butterfly jars for good and drop coins into a corpse.
A corpse holds 100% of carried coins on the first death, or 50% on a repeat A corpse holds 100% of carried coins on the first death. On a repeat death
death (the other 50% is lost forever and the old corpse vanishes). Void deaths (while a corpse is still unclaimed) only 50% of the remaining coins stay
(falling out of the world) leave no recoverable corpse. recoverable — the other 50% is lost forever and the old corpse vanishes.
Void deaths (falling out of the world) leave no recoverable corpse.
""" """
global corpse, JAR_DEATH_ROOM global corpse, JAR_DEATH_ROOM
player.butterfly_jars = 0 player.butterfly_jars = 0
@@ -5280,8 +5325,12 @@ def apply_death_penalty():
carried_l = player.coins_light carried_l = player.coins_light
carried_d = player.coins_dark carried_d = player.coins_dark
if corpse is not None: if corpse is not None:
lost_l = int(carried_l * 0.5) # "Remaining coins" = coins still held by the old corpse + anything carried
lost_d = int(carried_d * 0.5) # since then. The new corpse only keeps half of that pool.
pool_l = corpse.coins_light + carried_l
pool_d = corpse.coins_dark + carried_d
lost_l = int(pool_l * 0.5)
lost_d = int(pool_d * 0.5)
else: else:
lost_l = carried_l lost_l = carried_l
lost_d = carried_d lost_d = carried_d
+149 -31
View File
@@ -1,42 +1,51 @@
{ {
"version": 1, "version": 1,
"saved_at": "2026-09-28T10:53:55", "saved_at": "2026-09-29T09:39:49",
"player": { "player": {
"room_id": "room_town_gloomreach.json", "room_id": "room_town_gloomreach.json",
"x": 120, "x": 230,
"y": 360, "y": 360,
"health": 9, "health": 10,
"charge": 64.71, "charge": 125.0,
"shade": false, "shade": false,
"coins_light": 84, "coins_light": 0,
"coins_dark": 41, "coins_dark": 0,
"butterfly_jars": 2, "butterfly_jars": 0,
"has_ranged": true, "has_ranged": true,
"reward_chest_opened": true, "reward_chest_opened": true,
"equipment": { "equipment": {
"helmet": null, "helmet": {
"chest": null, "id": "adaptive_traveller_helm",
"pants": null, "name": "Adaptive Traveller's Helmet",
"necklace": null "set": "adaptive_traveller",
"slot": "helmet",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"charge": 10
} }
}, },
"inventory": [ "attuned": {
{ "extra_stats": {
"id": "necklace_placeholder", "charge": 15
"name": "Plain Necklace", }
"slot": "necklace",
"item_type": "necklace",
"base_stats": {
"health": 2
}, },
"rarity": "", "awakened": {
"tier": 0, "extra_stats": {
"abilities": [], "charge": 15,
"charge_regen_pct": 0.15
}
}
},
"rarity": "tempered",
"tier": 1,
"stats": { "stats": {
"health": 2 "charge": 10.0
}
}, },
{ "abilities": []
},
"chest": {
"id": "dawnrunner_chest", "id": "dawnrunner_chest",
"name": "Dawnrunner Chestplate", "name": "Dawnrunner Chestplate",
"set": "dawnrunner", "set": "dawnrunner",
@@ -70,24 +79,133 @@
}, },
"abilities": [] "abilities": []
}, },
"pants": {
"id": "dawnrunner_greaves",
"name": "Dawnrunner Greaves",
"set": "dawnrunner",
"slot": "pants",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"heal_speed_pct": 0.1
}
},
"attuned": {
"extra_stats": {
"heal_speed_pct": 0.1,
"move_speed_light_pct": 0.1
}
},
"awakened": {
"extra_stats": {
"heal_speed_pct": 0.15,
"move_speed_light_pct": 0.15
}
}
},
"rarity": "tempered",
"tier": 1,
"stats": {
"heal_speed_pct": 0.1
},
"abilities": []
},
"necklace": {
"id": "necklace_placeholder",
"name": "Plain Necklace",
"slot": "necklace",
"item_type": "necklace",
"base_stats": {
"health": 2
},
"rarity": "",
"tier": 0,
"abilities": [],
"stats": {
"health": 2
}
}
}
},
"inventory": [
{ {
"item_type": "boss_token", "id": "twister_helm",
"boss_id": "boss_01", "name": "Twister Helmet",
"name": "Gloom Warden's Token", "set": "twister",
"tier": 0 "slot": "helmet",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"attack_speed_pct": 0.1
}
},
"attuned": {
"extra_stats": {
"attack_speed_pct": 0.15
}
},
"awakened": {
"extra_stats": {
"attack_speed_pct": 0.15,
"charge_regen_pct": 0.15
}
}
},
"rarity": "tempered",
"tier": 1,
"stats": {
"attack_speed_pct": 0.1
},
"abilities": []
},
{
"id": "dawnrunner_helm",
"name": "Dawnrunner Helmet",
"set": "dawnrunner",
"slot": "helmet",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"move_speed_light_pct": 0.1
}
},
"attuned": {
"extra_stats": {
"move_speed_light_pct": 0.1,
"heal_speed_pct": 0.1
}
},
"awakened": {
"extra_stats": {
"move_speed_light_pct": 0.15,
"heal_speed_pct": 0.2
}
}
},
"rarity": "tempered",
"tier": 1,
"stats": {
"move_speed_light_pct": 0.1
},
"abilities": []
} }
], ],
"quests": { "quests": {
"active": { "active": {
"quest_first_kills": { "quest_first_kills": {
"progress": 12 "progress": 21
} }
}, },
"completed_one_time": [], "completed_one_time": [],
"tokens": 0 "tokens": 0
}, },
"boss_diary": { "boss_diary": {
"slotted": {} "slotted": {
"boss_01": 1
}
}, },
"defeated_bosses": [ "defeated_bosses": [
"room_boss_01.json" "room_boss_01.json"