This commit is contained in:
Joan
2026-02-23 15:42:21 +01:00
parent a725ae5836
commit d38d4cc288
102 changed files with 4511 additions and 4454 deletions

View File

@@ -329,6 +329,18 @@ character_quests = Table(
UniqueConstraint("character_id", "quest_id", name="uix_char_quest")
)
# Quests: Character History
character_quest_history = Table(
"character_quest_history",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("character_id", Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False),
Column("quest_id", String, nullable=False),
Column("started_at", Float, nullable=False),
Column("completed_at", Float, default=lambda: time.time()),
Column("rewards", JSON, default={}),
)
# Quests: Global progress
global_quests = Table(
"global_quests",
@@ -405,6 +417,8 @@ async def init_db():
# Quests
"CREATE INDEX IF NOT EXISTS idx_character_quests_char ON character_quests(character_id);",
"CREATE INDEX IF NOT EXISTS idx_character_quests_status ON character_quests(status);",
"CREATE INDEX IF NOT EXISTS idx_character_quest_history_char ON character_quest_history(character_id);",
"CREATE INDEX IF NOT EXISTS idx_character_quest_history_completed ON character_quest_history(completed_at);",
# Merchant Stock
"CREATE INDEX IF NOT EXISTS idx_merchant_stock_npc ON merchant_stock(npc_id);",
@@ -751,7 +765,9 @@ async def get_character_quests(character_id: int) -> List[Dict[str, Any]]:
"""Get all quests for a character"""
async with DatabaseSession() as session:
result = await session.execute(
select(character_quests).where(character_quests.c.character_id == character_id)
select(character_quests)
.where(character_quests.c.character_id == character_id)
.order_by(character_quests.c.started_at.desc())
)
rows = result.fetchall()
return [dict(row._mapping) for row in rows]
@@ -799,61 +815,139 @@ async def accept_quest(character_id: int, quest_id: str) -> Dict[str, Any]:
started_at=time.time(),
times_completed=0
).returning(character_quests)
result = await session.execute(stmt)
row = result.first()
await session.commit()
return dict(row._mapping) if row else None
async def update_quest_progress(character_id: int, quest_id: str, progress: Dict, status: Optional[str] = None) -> bool:
async def delete_character_quest(character_id: int, quest_id: str) -> bool:
"""Delete a character quest (used when completing or abandoning)"""
async with DatabaseSession() as session:
stmt = delete(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
)
await session.execute(stmt)
await session.commit()
return True
async def update_quest_progress(character_id: int, quest_id: str, progress: Dict, status: str = "active") -> bool:
"""Update quest progress"""
values = {"progress": progress}
if status:
values["status"] = status
async with DatabaseSession() as session:
# Check if we need to update timestamp
values = {
"progress": progress,
"status": status
}
if status == "completed":
values["completed_at"] = time.time()
values["last_completed_at"] = time.time()
# Increment times_completed
# We need to read first or use a raw update expression,
# simplest is to just increment in python for now or assume caller logic handles it
# But let's do it right:
# We can't easily do col + 1 in a simple update call without pulling in Table object to the values
# So we'll rely on a fetch-update pattern or standard SQL
pass
async with DatabaseSession() as session:
# If completing, increment counter
if status == "completed":
# We can use the column expression for atomic increment
values["times_completed"] = character_quests.c.times_completed + 1
# We need to do this carefully atomically or just fetch-update
# Doing fetch-update for simplicity as we are inside transaction block if we used one,
# but DatabaseSession is per-call here.
# Using specific update to increment
stmt = update(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
).values(**values)
# Also increment times_completed separately to avoid overwrite race with simple values
stmt2 = update(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
).values(times_completed=character_quests.c.times_completed + 1)
await session.execute(stmt)
await session.execute(stmt2)
else:
stmt = update(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
).values(**values)
await session.execute(stmt)
stmt = update(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
).values(**values)
await session.execute(stmt)
await session.commit()
return True
async def set_quest_cooldown(character_id: int, quest_id: str, cooldown_expires_at: float) -> bool:
"""Set cooldown for a quest"""
async def set_quest_cooldown(character_id: int, quest_id: str, expires_at: float) -> bool:
"""Set cooldown for a repeatable quest"""
async with DatabaseSession() as session:
stmt = update(character_quests).where(
and_(
character_quests.c.character_id == character_id,
character_quests.c.quest_id == quest_id
)
).values(cooldown_expires_at=cooldown_expires_at)
).values(cooldown_expires_at=expires_at)
await session.execute(stmt)
await session.commit()
return True
# Global Quests
async def log_quest_completion(character_id: int, quest_id: str, started_at: float, rewards: Dict) -> bool:
"""Log a quest completion to history"""
async with DatabaseSession() as session:
stmt = insert(character_quest_history).values(
character_id=character_id,
quest_id=quest_id,
started_at=started_at,
completed_at=time.time(),
rewards=rewards
)
await session.execute(stmt)
await session.commit()
return True
async def get_quest_history(character_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""Get quest history with pagination"""
offset = (page - 1) * page_size
async with DatabaseSession() as session:
# Get total count
count_stmt = select(character_quest_history.c.id).where(
character_quest_history.c.character_id == character_id
)
count_result = await session.execute(count_stmt)
total_count = len(count_result.fetchall())
# Get paged results
stmt = select(character_quest_history).where(
character_quest_history.c.character_id == character_id
).order_by(
character_quest_history.c.completed_at.desc()
).offset(offset).limit(page_size)
result = await session.execute(stmt)
rows = result.fetchall()
data = [dict(row._mapping) for row in rows]
return {
"data": data,
"total": total_count,
"page": page,
"pages": (total_count + page_size - 1) // page_size
}
# ========================================================================
# GLOBAL QUEST OPERATIONS
# ========================================================================
async def get_global_quest(quest_id: str) -> Optional[Dict[str, Any]]:
"""Get global quest progress"""
async with DatabaseSession() as session:
@@ -864,34 +958,63 @@ async def get_global_quest(quest_id: str) -> Optional[Dict[str, Any]]:
return dict(row._mapping) if row else None
async def update_global_quest(quest_id: str, progress: Dict, is_completed: bool = False) -> bool:
"""Update or create global quest progress"""
# Upsert logic
existing = await get_global_quest(quest_id)
async def update_global_quest(quest_id: str, progress: Dict) -> bool:
"""Update global quest progress"""
async with DatabaseSession() as session:
if existing:
stmt = update(global_quests).where(
global_quests.c.quest_id == quest_id
).values(
global_progress=progress,
is_completed=is_completed,
updated_at=time.time()
)
# Upsert
existing = await session.execute(
select(global_quests).where(global_quests.c.quest_id == quest_id)
)
if existing.first():
stmt = update(global_quests).where(
global_quests.c.quest_id == quest_id
).values(
global_progress=progress,
updated_at=time.time()
)
else:
stmt = insert(global_quests).values(
quest_id=quest_id,
global_progress=progress,
is_completed=is_completed,
updated_at=time.time()
)
stmt = insert(global_quests).values(
quest_id=quest_id,
global_progress=progress,
updated_at=time.time()
)
await session.execute(stmt)
await session.commit()
return True
# ========================================================================
async def get_completed_global_quests() -> List[str]:
"""Get list of IDs of all completed global quests"""
async with DatabaseSession() as session:
result = await session.execute(
select(global_quests.c.quest_id).where(global_quests.c.is_completed == True)
)
return [row[0] for row in result.fetchall()]
async def mark_global_quest_completed(quest_id: str) -> bool:
"""Mark a global quest as completed"""
async with DatabaseSession() as session:
stmt = update(global_quests).where(
global_quests.c.quest_id == quest_id
).values(
is_completed=True,
updated_at=time.time()
)
await session.execute(stmt)
await session.commit()
return True
async def get_all_quest_participants(quest_id: str) -> List[Dict[str, Any]]:
"""Get all characters who have this quest active or completed"""
async with DatabaseSession() as session:
result = await session.execute(
select(character_quests).where(character_quests.c.quest_id == quest_id)
)
return [dict(row._mapping) for row in result.fetchall()]
# MERCHANT OPERATIONS
# ========================================================================