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