Backup before cleanup
This commit is contained in:
226
api/database.py
226
api/database.py
@@ -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
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user