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_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)
elif key == key_for("ui_down"):
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):
if len(self.inv.items) == 0:
@@ -2382,6 +2396,18 @@ class InventoryUI:
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)):
surface.blit(self.font.render(text, True, color), (x, y))
@@ -2778,7 +2804,7 @@ class InventoryUI:
# Hint text (NOW hint_y exists)
self._blit(
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,
hint_y + 4,
(160, 160, 160),
@@ -3213,6 +3239,7 @@ class Player:
self.idle_frame = 0
self.idle_anim_time = 0.0
self.idle_bob_t = 0.0
self.fairy_bob_t = random.random() * 10.0
self.idle_hold_time = 0.0
self.ground_contact_timer = 0.0
self.is_idle = False
@@ -3661,6 +3688,7 @@ class Player:
self.just_jumped = False
self.just_landed = False
self.just_air_jumped = False
self.fairy_bob_t += dt
if self.invincibility_timer > 0:
self.invincibility_timer -= dt
@@ -3839,6 +3867,20 @@ class Player:
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):
if self.healing:
dur = self.effective_heal_duration()
@@ -3855,6 +3897,7 @@ class Player:
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)
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)
else:
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
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)
self._draw_fairy(surface, cx, cy, draw_x, draw_y, img.get_width(), img.get_height())
surface.blit(img, (draw_x, draw_y))
# Debug: show player hitbox
@@ -5268,9 +5312,10 @@ def reset_room_combat_state():
def apply_death_penalty():
"""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
death (the other 50% is lost forever and the old corpse vanishes). Void deaths
(falling out of the world) leave no recoverable corpse.
A corpse holds 100% of carried coins on the first death. On a repeat death
(while a corpse is still unclaimed) only 50% of the remaining coins stay
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
player.butterfly_jars = 0
@@ -5280,8 +5325,12 @@ def apply_death_penalty():
carried_l = player.coins_light
carried_d = player.coins_dark
if corpse is not None:
lost_l = int(carried_l * 0.5)
lost_d = int(carried_d * 0.5)
# "Remaining coins" = coins still held by the old corpse + anything carried
# 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:
lost_l = carried_l
lost_d = carried_d
+164 -46
View File
@@ -1,93 +1,211 @@
{
"version": 1,
"saved_at": "2026-09-28T10:53:55",
"saved_at": "2026-09-29T09:39:49",
"player": {
"room_id": "room_town_gloomreach.json",
"x": 120,
"x": 230,
"y": 360,
"health": 9,
"charge": 64.71,
"health": 10,
"charge": 125.0,
"shade": false,
"coins_light": 84,
"coins_dark": 41,
"butterfly_jars": 2,
"coins_light": 0,
"coins_dark": 0,
"butterfly_jars": 0,
"has_ranged": true,
"reward_chest_opened": true,
"equipment": {
"helmet": null,
"chest": null,
"pants": null,
"necklace": null
"helmet": {
"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": []
},
"chest": {
"id": "dawnrunner_chest",
"name": "Dawnrunner Chestplate",
"set": "dawnrunner",
"slot": "chest",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"charge": 10
}
},
"attuned": {
"extra_stats": {
"charge": 15,
"move_speed_light_pct": 0.05
}
},
"awakened": {
"extra_stats": {
"charge": 15,
"move_speed_light_pct": 0.05,
"heal_plus": 1
}
}
},
"rarity": "attuned",
"tier": 2,
"stats": {
"charge": 15.0,
"move_speed_light_pct": 0.05
},
"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": [
{
"id": "necklace_placeholder",
"name": "Plain Necklace",
"slot": "necklace",
"item_type": "necklace",
"base_stats": {
"health": 2
},
"rarity": "",
"tier": 0,
"abilities": [],
"stats": {
"health": 2
}
},
{
"id": "dawnrunner_chest",
"name": "Dawnrunner Chestplate",
"set": "dawnrunner",
"slot": "chest",
"id": "twister_helm",
"name": "Twister Helmet",
"set": "twister",
"slot": "helmet",
"base_stats": {},
"rarity_overrides": {
"tempered": {
"extra_stats": {
"charge": 10
"attack_speed_pct": 0.1
}
},
"attuned": {
"extra_stats": {
"charge": 15,
"move_speed_light_pct": 0.05
"attack_speed_pct": 0.15
}
},
"awakened": {
"extra_stats": {
"charge": 15,
"move_speed_light_pct": 0.05,
"heal_plus": 1
"attack_speed_pct": 0.15,
"charge_regen_pct": 0.15
}
}
},
"rarity": "attuned",
"tier": 2,
"rarity": "tempered",
"tier": 1,
"stats": {
"charge": 15.0,
"move_speed_light_pct": 0.05
"attack_speed_pct": 0.1
},
"abilities": []
},
{
"item_type": "boss_token",
"boss_id": "boss_01",
"name": "Gloom Warden's Token",
"tier": 0
"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": {
"active": {
"quest_first_kills": {
"progress": 12
"progress": 21
}
},
"completed_one_time": [],
"tokens": 0
},
"boss_diary": {
"slotted": {}
"slotted": {
"boss_01": 1
}
},
"defeated_bosses": [
"room_boss_01.json"