Backup before cleanup

This commit is contained in:
Joan
2026-02-05 15:00:49 +01:00
parent e6747b1d05
commit 1b7ffd614d
60 changed files with 3013 additions and 460 deletions

View File

@@ -262,8 +262,12 @@ player_status_effects = Table(
Column("character_id", Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False),
Column("effect_name", String(50), nullable=False),
Column("effect_icon", String(10), nullable=False),
Column("effect_type", String(20), default="damage"), # 'damage', 'buff', 'debuff'
Column("damage_per_tick", Integer, nullable=False, default=0),
Column("value", Integer, default=0), # Generic value (buff %, damage, etc.)
Column("ticks_remaining", Integer, nullable=False),
Column("persist_after_combat", Boolean, default=False), # Keep after combat ends
Column("source", String(50), nullable=True), # 'item:molotov', 'action:defend'
Column("applied_at", Float, nullable=False),
)
@@ -2030,18 +2034,99 @@ async def remove_empty_npc_corpses() -> int:
# STATUS EFFECTS FUNCTIONS
# ============================================================================
async def get_player_status_effects(player_id: int):
async def add_effect(
player_id: int,
effect_name: str,
effect_icon: str,
ticks_remaining: int,
effect_type: str = "damage",
damage_per_tick: int = 0,
value: int = 0,
persist_after_combat: bool = False,
source: str = None
) -> int:
"""
Add a status effect to a player.
If the effect already exists, it refreshes the duration (ticks_remaining).
Returns the effect ID.
"""
async with DatabaseSession() as session:
# Check if effect already exists
result = await session.execute(
select(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.effect_name == effect_name
)
)
)
existing_effect = result.first()
if existing_effect:
# Refresh duration
await session.execute(
update(player_status_effects).where(
player_status_effects.c.id == existing_effect.id
).values(
ticks_remaining=ticks_remaining,
applied_at=time.time()
)
)
await session.commit()
return existing_effect.id
else:
# Insert new effect
stmt = insert(player_status_effects).values(
character_id=player_id,
effect_name=effect_name,
effect_icon=effect_icon,
effect_type=effect_type,
damage_per_tick=damage_per_tick,
value=value,
ticks_remaining=ticks_remaining,
persist_after_combat=persist_after_combat,
source=source,
applied_at=time.time()
).returning(player_status_effects.c.id)
result = await session.execute(stmt)
row = result.first()
await session.commit()
return row[0] if row else None
async def get_player_effects(player_id: int, min_ticks: int = 1) -> List[Dict[str, Any]]:
"""Get all active status effects for a player."""
async with DatabaseSession() as session:
result = await session.execute(
select(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.ticks_remaining > 0
player_status_effects.c.ticks_remaining >= min_ticks
)
)
)
return [row._asdict() for row in result.fetchall()]
return [dict(row._mapping) for row in result.fetchall()]
# Alias for backward compatibility
async def get_player_status_effects(player_id: int, min_ticks: int = 1):
"""Alias for get_player_effects for backward compatibility."""
return await get_player_effects(player_id, min_ticks)
async def remove_effect(player_id: int, effect_name: str) -> bool:
"""Remove a specific effect from a player by name."""
async with DatabaseSession() as session:
await session.execute(
delete(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.effect_name == effect_name
)
)
)
await session.commit()
return True
async def remove_all_status_effects(player_id: int):
@@ -2052,36 +2137,141 @@ async def remove_all_status_effects(player_id: int):
)
await session.commit()
async def decrement_all_status_effect_ticks():
"""
Decrement ticks for all active status effects and return affected player IDs.
Used by background processor.
"""
async def clean_expired_status_effects():
"""Remove all status effects with <= 0 ticks."""
async with DatabaseSession() as session:
# Get player IDs with effects before updating
from sqlalchemy import distinct
result = await session.execute(
select(distinct(player_status_effects.c.character_id)).where(
player_status_effects.c.ticks_remaining > 0
await session.execute(
delete(player_status_effects).where(player_status_effects.c.ticks_remaining <= 0)
)
await session.commit()
async def remove_non_persistent_effects(player_id: int):
"""Remove effects where persist_after_combat is False. Called when combat ends."""
async with DatabaseSession() as session:
await session.execute(
delete(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.persist_after_combat == False
)
)
)
affected_players = [row[0] for row in result.fetchall()]
await session.commit()
async def tick_player_effects(player_id: int) -> List[Dict[str, Any]]:
"""
Decrement ticks and return effects that were applied this tick.
Used during combat when player receives a turn.
Returns list of effects with their current state (before tick was applied).
"""
async with DatabaseSession() as session:
# Get effects before decrementing
result = await session.execute(
select(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.ticks_remaining > 0
)
)
)
effects = [dict(row._mapping) for row in result.fetchall()]
if not effects:
return []
# Decrement ticks
await session.execute(
update(player_status_effects).where(
player_status_effects.c.ticks_remaining > 0
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.ticks_remaining > 0
)
).values(ticks_remaining=player_status_effects.c.ticks_remaining - 1)
)
# Remove expired effects
await session.execute(
delete(player_status_effects).where(player_status_effects.c.ticks_remaining <= 0)
delete(player_status_effects).where(
and_(
player_status_effects.c.character_id == player_id,
player_status_effects.c.ticks_remaining <= 0
)
)
)
await session.commit()
return affected_players
return effects
async def decrement_all_status_effect_ticks():
"""
Decrement ticks for all active status effects and return affected player IDs.
Used by background processor. Only processes players NOT in combat.
"""
async with DatabaseSession() as session:
from sqlalchemy import distinct
# Get all players with active effects
result = await session.execute(
select(distinct(player_status_effects.c.character_id)).where(
player_status_effects.c.ticks_remaining > 0
)
)
all_players = [row[0] for row in result.fetchall()]
# Filter out players in combat - they process effects on turn
players_to_process = []
for pid in all_players:
if not await is_player_in_combat(pid):
players_to_process.append(pid)
if not players_to_process:
return []
# Decrement ticks only for players not in combat
for pid in players_to_process:
await session.execute(
update(player_status_effects).where(
and_(
player_status_effects.c.character_id == pid,
player_status_effects.c.ticks_remaining > 0
)
).values(ticks_remaining=player_status_effects.c.ticks_remaining - 1)
)
# NOTE: We do NOT remove expired effects here anymore.
# They will be processed by the background task (to apply final tick)
# and then cleaned up via clean_expired_status_effects()
await session.commit()
return players_to_process
async def is_player_in_combat(player_id: int) -> bool:
"""Check if player is in any active combat (PvE or PvP)."""
async with DatabaseSession() as session:
# Check PvE combat
pve = await session.execute(
select(active_combats.c.id).where(active_combats.c.character_id == player_id)
)
if pve.first():
return True
# Check PvP combat
pvp = await session.execute(
select(pvp_combats.c.id).where(
or_(
pvp_combats.c.attacker_character_id == player_id,
pvp_combats.c.defender_character_id == player_id
)
)
)
if pvp.first():
return True
return False
# ============================================================================