İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
ea049b8ccd
45 changed files with 6751 additions and 0 deletions
536
routers/travel.py
Normal file
536
routers/travel.py
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
"""
|
||||
Lokasyon-graf dünya: statik harita üzerinde noktalar (şehir/orman/maden...),
|
||||
yollar (adım maliyeti), lokasyonda kaynak toplama ve yolda D&D tarzı olaylar.
|
||||
|
||||
Eski tile/chunk sistemi kaldırıldı (2026-07-04). Akış:
|
||||
- GET /world/map → tüm lokasyonlar + yollar + benim yerim
|
||||
- GET /world/location → bulunduğum yer: kaynaklar (cooldown/tool durumu), buradaki oyuncular, bekleyen olay
|
||||
- POST /world/travel → yol varsa adım öde, git; şansa bağlı olay kartı döner
|
||||
- POST /world/gather → lokasyon kaynağını topla (tool şartı + cooldown + adım + XP)
|
||||
- POST /world/event/resolve → olay kartında seçim yap (skill check d20 + skill seviyesi)
|
||||
"""
|
||||
import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from deps import get_current_user
|
||||
from game_logic import character_level_from_total_xp, skill_level_from_xp
|
||||
from models import Character, Skill, User, get_db
|
||||
from routers.harvest import SKILL_TOOLS, TOOL_LABEL, _has_any_tool, consume_items
|
||||
|
||||
router = APIRouter(prefix="/world", tags=["travel"])
|
||||
|
||||
# Kamp: lokasyonda kişisel kamp → o lokasyonda gather adım maliyeti -%25
|
||||
CAMP_COST_ITEMS = {"log_oak": 5, "stone_small": 2, "vine": 1}
|
||||
CAMP_STEP_COST = 100
|
||||
CAMP_XP = 40 # construction
|
||||
CAMP_DISCOUNT = 0.25
|
||||
|
||||
|
||||
def _camp_cost(step_cost: int, has_camp: bool) -> int:
|
||||
if not has_camp:
|
||||
return step_cost
|
||||
return max(1, int(step_cost * (1.0 - CAMP_DISCOUNT)))
|
||||
|
||||
|
||||
async def _has_camp(db: AsyncSession, char_id: int, location_id: int | None) -> bool:
|
||||
if location_id is None:
|
||||
return False
|
||||
row = (await db.execute(text(
|
||||
"SELECT 1 FROM camps WHERE character_id=:c AND location_id=:l"
|
||||
), {"c": char_id, "l": location_id})).first()
|
||||
return row is not None
|
||||
|
||||
|
||||
async def _char(db: AsyncSession, user: User) -> Character:
|
||||
char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
|
||||
if not char:
|
||||
raise HTTPException(404, "Karakter yok")
|
||||
return char
|
||||
|
||||
|
||||
async def _add_items(db: AsyncSession, char_id: int, item_code: str, qty: int) -> None:
|
||||
"""Envantere stack mantığıyla item ekle."""
|
||||
row = (await db.execute(
|
||||
text("SELECT id, stack_max FROM items WHERE code = :c"), {"c": item_code}
|
||||
)).first()
|
||||
if not row:
|
||||
return
|
||||
item_id, stack_max = row[0], int(row[1])
|
||||
remaining = qty
|
||||
stacks = (await db.execute(text(
|
||||
"SELECT id, qty FROM inventory WHERE character_id=:cid AND item_id=:iid ORDER BY slot"
|
||||
), {"cid": char_id, "iid": item_id})).all()
|
||||
for st in stacks:
|
||||
if remaining <= 0:
|
||||
return
|
||||
space = stack_max - st[1]
|
||||
if space <= 0:
|
||||
continue
|
||||
add = min(space, remaining)
|
||||
await db.execute(text("UPDATE inventory SET qty = qty + :a WHERE id = :id"), {"a": add, "id": st[0]})
|
||||
remaining -= add
|
||||
while remaining > 0:
|
||||
add = min(stack_max, remaining)
|
||||
slot = (await db.execute(text(
|
||||
"SELECT COALESCE(MAX(slot), -1) + 1 FROM inventory WHERE character_id=:cid"
|
||||
), {"cid": char_id})).first()[0]
|
||||
await db.execute(text(
|
||||
"INSERT INTO inventory (character_id, item_id, qty, slot) VALUES (:cid, :iid, :q, :s)"
|
||||
), {"cid": char_id, "iid": item_id, "q": add, "s": slot})
|
||||
remaining -= add
|
||||
|
||||
|
||||
def _grant_xp(char: Character, skill: Skill, xp: int) -> dict:
|
||||
old_sl, old_cl = skill.level, char.level
|
||||
skill.xp += xp
|
||||
skill.level = skill_level_from_xp(skill.xp)
|
||||
char.total_xp += xp
|
||||
char.level = character_level_from_total_xp(char.total_xp)
|
||||
return {"skill_level_up": skill.level > old_sl, "character_level_up": char.level > old_cl}
|
||||
|
||||
|
||||
# ---------- Harita ----------
|
||||
|
||||
class LocationOut(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
type: str
|
||||
description: Optional[str] = None
|
||||
map_x: int
|
||||
map_y: int
|
||||
|
||||
|
||||
class RoadOut(BaseModel):
|
||||
a: str
|
||||
b: str
|
||||
steps: int
|
||||
waypoints: list[list[float]] = [] # normalize 0-1 ara noktalar (editörde çizilir)
|
||||
|
||||
|
||||
class MapOut(BaseModel):
|
||||
locations: list[LocationOut]
|
||||
roads: list[RoadOut]
|
||||
my_location: Optional[str] = None
|
||||
pending_event: Optional[dict] = None
|
||||
|
||||
|
||||
@router.get("/map", response_model=MapOut)
|
||||
async def world_map(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
locs = (await db.execute(text(
|
||||
"SELECT id, code, name, type, description, map_x, map_y FROM locations ORDER BY id"
|
||||
))).all()
|
||||
id2code = {r[0]: r[1] for r in locs}
|
||||
roads = (await db.execute(text("SELECT loc_a, loc_b, step_cost, waypoints FROM roads"))).all()
|
||||
return MapOut(
|
||||
locations=[LocationOut(code=r[1], name=r[2], type=r[3], description=r[4], map_x=r[5], map_y=r[6]) for r in locs],
|
||||
roads=[RoadOut(a=id2code[r[0]], b=id2code[r[1]], steps=r[2], waypoints=r[3] or []) for r in roads],
|
||||
my_location=id2code.get(char.location_id),
|
||||
pending_event=char.pending_event,
|
||||
)
|
||||
|
||||
|
||||
# ---------- Lokasyon detay ----------
|
||||
|
||||
class ResourceOut(BaseModel):
|
||||
item_code: str
|
||||
item_name: str
|
||||
skill: str
|
||||
step_cost: int
|
||||
xp: int
|
||||
cooldown_remaining: int # saniye, 0 = hazır
|
||||
tool_ok: bool
|
||||
tool_needed: Optional[str] = None
|
||||
|
||||
|
||||
class LocationDetailOut(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
type: str
|
||||
description: Optional[str] = None
|
||||
resources: list[ResourceOut]
|
||||
players_here: list[str]
|
||||
pending_event: Optional[dict] = None
|
||||
has_camp: bool = False
|
||||
camp_cost_items: dict = {} # kamp yoksa: kurulum malzemeleri {code: qty}
|
||||
camp_step_cost: int = CAMP_STEP_COST
|
||||
|
||||
|
||||
@router.get("/location", response_model=LocationDetailOut)
|
||||
async def location_detail(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
if char.location_id is None:
|
||||
raise HTTPException(400, "Konumun yok — sunucu seed'i eksik olabilir")
|
||||
loc = (await db.execute(text(
|
||||
"SELECT id, code, name, type, description FROM locations WHERE id = :i"
|
||||
), {"i": char.location_id})).first()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
rows = (await db.execute(text("""
|
||||
SELECT lr.id, lr.item_code, i.name, lr.skill, lr.step_cost, lr.xp, gc.ready_at
|
||||
FROM location_resources lr
|
||||
JOIN items i ON i.code = lr.item_code
|
||||
LEFT JOIN gather_cooldowns gc ON gc.resource_id = lr.id AND gc.character_id = :cid
|
||||
WHERE lr.location_id = :lid
|
||||
ORDER BY lr.step_cost
|
||||
"""), {"cid": char.id, "lid": char.location_id})).all()
|
||||
|
||||
has_camp = await _has_camp(db, char.id, char.location_id)
|
||||
resources = []
|
||||
for r in rows:
|
||||
tool_codes = SKILL_TOOLS.get(r[3])
|
||||
tool_ok = True
|
||||
if tool_codes:
|
||||
tool_ok = await _has_any_tool(db, char.id, tool_codes)
|
||||
cd = 0
|
||||
if r[6] is not None and r[6] > now:
|
||||
cd = int((r[6] - now).total_seconds())
|
||||
resources.append(ResourceOut(
|
||||
item_code=r[1], item_name=r[2], skill=r[3],
|
||||
step_cost=_camp_cost(int(r[4]), has_camp), xp=r[5],
|
||||
cooldown_remaining=cd, tool_ok=tool_ok,
|
||||
tool_needed=TOOL_LABEL.get(r[3]),
|
||||
))
|
||||
|
||||
names = (await db.execute(text(
|
||||
"SELECT name FROM characters WHERE location_id = :lid AND id != :cid ORDER BY name LIMIT 50"
|
||||
), {"lid": char.location_id, "cid": char.id})).all()
|
||||
|
||||
return LocationDetailOut(
|
||||
code=loc[1], name=loc[2], type=loc[3], description=loc[4],
|
||||
resources=resources,
|
||||
players_here=[n[0] for n in names],
|
||||
pending_event=char.pending_event,
|
||||
has_camp=has_camp,
|
||||
camp_cost_items={} if has_camp else CAMP_COST_ITEMS,
|
||||
)
|
||||
|
||||
|
||||
# ---------- Seyahat ----------
|
||||
|
||||
class TravelIn(BaseModel):
|
||||
to_code: str
|
||||
|
||||
|
||||
class TravelOut(BaseModel):
|
||||
location_code: str
|
||||
location_name: str
|
||||
step_cost: int
|
||||
step_balance: int
|
||||
event: Optional[dict] = None # {code, name, text, choices:[{text}]} — çözülmesi gerekir
|
||||
|
||||
|
||||
@router.post("/travel", response_model=TravelOut)
|
||||
async def travel(t: TravelIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
if char.pending_event:
|
||||
raise HTTPException(400, "Önce bekleyen olayı çöz")
|
||||
|
||||
dest = (await db.execute(text(
|
||||
"SELECT id, code, name FROM locations WHERE code = :c"
|
||||
), {"c": t.to_code})).first()
|
||||
if not dest:
|
||||
raise HTTPException(404, "Böyle bir yer yok")
|
||||
if dest[0] == char.location_id:
|
||||
raise HTTPException(400, "Zaten oradasın")
|
||||
|
||||
road = (await db.execute(text("""
|
||||
SELECT step_cost, event_chance FROM roads
|
||||
WHERE (loc_a = :a AND loc_b = :b) OR (loc_a = :b AND loc_b = :a)
|
||||
"""), {"a": char.location_id, "b": dest[0]})).first()
|
||||
if not road:
|
||||
raise HTTPException(400, "Oraya buradan yol yok")
|
||||
|
||||
cost = int(road[0])
|
||||
if char.step_balance < cost:
|
||||
raise HTTPException(400, f"Yetersiz adım: {cost} gerekli, {char.step_balance} var")
|
||||
|
||||
char.step_balance -= cost
|
||||
char.location_id = dest[0]
|
||||
char.last_sync_at = datetime.now(timezone.utc)
|
||||
|
||||
# Yolda olay? (d100 < şans). %50 önce varış lokasyonuna özel havuz denenir,
|
||||
# yoksa/diğer yarıda genel + özel karışık havuzdan gelir.
|
||||
event_payload = None
|
||||
if random.random() < float(road[1]):
|
||||
ev = None
|
||||
if random.random() < 0.5:
|
||||
ev = (await db.execute(text(
|
||||
"SELECT code, name, text, choices FROM events "
|
||||
"WHERE :dest = ANY(location_codes) ORDER BY random() LIMIT 1"
|
||||
), {"dest": dest[1]})).first()
|
||||
if ev is None:
|
||||
ev = (await db.execute(text(
|
||||
"SELECT code, name, text, choices FROM events "
|
||||
"WHERE location_codes IS NULL OR :dest = ANY(location_codes) "
|
||||
"ORDER BY random() LIMIT 1"
|
||||
), {"dest": dest[1]})).first()
|
||||
if ev:
|
||||
# Oyuncuya sadece seçim metinleri gider; sonuçlar sunucuda kalır
|
||||
choices_client = [{"text": c["text"]} for c in ev[3]]
|
||||
event_payload = {"code": ev[0], "name": ev[1], "text": ev[2], "choices": choices_client}
|
||||
# Tam kart saklanır ki istemci reconnect'te de render edebilsin
|
||||
char.pending_event = event_payload
|
||||
|
||||
await db.commit()
|
||||
|
||||
# WS yayını
|
||||
from routers.ws import publish_event, set_online
|
||||
payload = {"char_id": char.id, "name": char.name, "level": char.level, "location_code": dest[1]}
|
||||
await set_online(char.id, payload)
|
||||
await publish_event({"type": "player_moved", **payload})
|
||||
|
||||
return TravelOut(
|
||||
location_code=dest[1], location_name=dest[2],
|
||||
step_cost=cost, step_balance=char.step_balance,
|
||||
event=event_payload,
|
||||
)
|
||||
|
||||
|
||||
# ---------- Kaynak toplama ----------
|
||||
|
||||
class GatherIn(BaseModel):
|
||||
item_code: str
|
||||
|
||||
|
||||
class GatherOut(BaseModel):
|
||||
item_code: str
|
||||
item_name: str
|
||||
qty_gained: int
|
||||
skill: str
|
||||
xp_gained: int
|
||||
skill_level: int
|
||||
skill_level_up: bool
|
||||
total_xp: int
|
||||
character_level: int
|
||||
character_level_up: bool
|
||||
step_balance: int
|
||||
cooldown_s: int
|
||||
|
||||
|
||||
@router.post("/gather", response_model=GatherOut)
|
||||
async def gather(g: GatherIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
if char.pending_event:
|
||||
raise HTTPException(400, "Önce bekleyen olayı çöz")
|
||||
|
||||
res = (await db.execute(text("""
|
||||
SELECT lr.id, lr.item_code, i.name, lr.skill, lr.step_cost, lr.xp, lr.cooldown_s
|
||||
FROM location_resources lr JOIN items i ON i.code = lr.item_code
|
||||
WHERE lr.location_id = :lid AND lr.item_code = :c
|
||||
"""), {"lid": char.location_id, "c": g.item_code})).first()
|
||||
if not res:
|
||||
raise HTTPException(404, "Bu kaynak burada yok")
|
||||
|
||||
# Tool şartı
|
||||
tool_codes = SKILL_TOOLS.get(res[3])
|
||||
if tool_codes and not await _has_any_tool(db, char.id, tool_codes):
|
||||
raise HTTPException(400, f"{TOOL_LABEL[res[3]]} gerekli — envanterinde yok")
|
||||
|
||||
# Cooldown
|
||||
now = datetime.now(timezone.utc)
|
||||
cd = (await db.execute(text(
|
||||
"SELECT ready_at FROM gather_cooldowns WHERE character_id=:cid AND resource_id=:rid"
|
||||
), {"cid": char.id, "rid": res[0]})).first()
|
||||
if cd and cd[0] > now:
|
||||
raise HTTPException(400, f"Bu kaynak yenileniyor ({int((cd[0] - now).total_seconds())}s)")
|
||||
|
||||
step_cost = _camp_cost(int(res[4]), await _has_camp(db, char.id, char.location_id))
|
||||
if char.step_balance < step_cost:
|
||||
raise HTTPException(400, f"Yetersiz adım: {step_cost} gerekli, {char.step_balance} var")
|
||||
|
||||
skill = (await db.execute(
|
||||
select(Skill).where(Skill.character_id == char.id, Skill.skill_name == res[3])
|
||||
)).scalar_one_or_none()
|
||||
if skill is None:
|
||||
skill = Skill(character_id=char.id, skill_name=res[3], xp=0, level=1)
|
||||
db.add(skill)
|
||||
await db.flush()
|
||||
|
||||
char.step_balance -= step_cost
|
||||
ups = _grant_xp(char, skill, int(res[5]))
|
||||
await _add_items(db, char.id, res[1], 1)
|
||||
|
||||
await db.execute(text("""
|
||||
INSERT INTO gather_cooldowns (character_id, resource_id, ready_at)
|
||||
VALUES (:cid, :rid, :ra)
|
||||
ON CONFLICT (character_id, resource_id) DO UPDATE SET ready_at = EXCLUDED.ready_at
|
||||
"""), {"cid": char.id, "rid": res[0], "ra": now + timedelta(seconds=int(res[6]))})
|
||||
|
||||
char.last_sync_at = now
|
||||
await db.commit()
|
||||
|
||||
return GatherOut(
|
||||
item_code=res[1], item_name=res[2], qty_gained=1,
|
||||
skill=res[3], xp_gained=int(res[5]),
|
||||
skill_level=skill.level, skill_level_up=ups["skill_level_up"],
|
||||
total_xp=char.total_xp, character_level=char.level,
|
||||
character_level_up=ups["character_level_up"],
|
||||
step_balance=char.step_balance, cooldown_s=int(res[6]),
|
||||
)
|
||||
|
||||
|
||||
# ---------- Kamp kurma ----------
|
||||
|
||||
class CampBuildOut(BaseModel):
|
||||
location_code: str
|
||||
step_cost: int
|
||||
xp_gained: int
|
||||
skill: str = "construction"
|
||||
skill_level: int
|
||||
skill_level_up: bool
|
||||
character_level: int
|
||||
character_level_up: bool
|
||||
step_balance: int
|
||||
|
||||
|
||||
@router.post("/camp/build", response_model=CampBuildOut)
|
||||
async def build_camp(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
if char.pending_event:
|
||||
raise HTTPException(400, "Önce bekleyen olayı çöz")
|
||||
if char.location_id is None:
|
||||
raise HTTPException(400, "Konumun yok")
|
||||
if await _has_camp(db, char.id, char.location_id):
|
||||
raise HTTPException(400, "Burada zaten kampın var")
|
||||
if char.step_balance < CAMP_STEP_COST:
|
||||
raise HTTPException(400, f"Yetersiz adım: {CAMP_STEP_COST} gerekli, {char.step_balance} var")
|
||||
|
||||
# Malzemeler (hepsi birden — yetersizse consume_items 400 fırlatır, rollback olur)
|
||||
for code, qty in CAMP_COST_ITEMS.items():
|
||||
item = (await db.execute(text(
|
||||
"SELECT id, name FROM items WHERE code=:c"), {"c": code})).first()
|
||||
await consume_items(db, char.id, item[0], qty, item[1])
|
||||
|
||||
skill = (await db.execute(
|
||||
select(Skill).where(Skill.character_id == char.id, Skill.skill_name == "construction")
|
||||
)).scalar_one_or_none()
|
||||
if skill is None:
|
||||
skill = Skill(character_id=char.id, skill_name="construction", xp=0, level=1)
|
||||
db.add(skill)
|
||||
await db.flush()
|
||||
|
||||
char.step_balance -= CAMP_STEP_COST
|
||||
ups = _grant_xp(char, skill, CAMP_XP)
|
||||
await db.execute(text(
|
||||
"INSERT INTO camps (character_id, location_id) VALUES (:c, :l)"
|
||||
), {"c": char.id, "l": char.location_id})
|
||||
|
||||
loc_code = (await db.execute(text(
|
||||
"SELECT code FROM locations WHERE id=:i"), {"i": char.location_id})).first()[0]
|
||||
char.last_sync_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
return CampBuildOut(
|
||||
location_code=loc_code, step_cost=CAMP_STEP_COST, xp_gained=CAMP_XP,
|
||||
skill_level=skill.level, skill_level_up=ups["skill_level_up"],
|
||||
character_level=char.level, character_level_up=ups["character_level_up"],
|
||||
step_balance=char.step_balance,
|
||||
)
|
||||
|
||||
|
||||
# ---------- Olay çözümü ----------
|
||||
|
||||
class ResolveIn(BaseModel):
|
||||
choice: int = Field(ge=0, le=5)
|
||||
|
||||
|
||||
class ResolveOut(BaseModel):
|
||||
event_code: str
|
||||
choice_text: str
|
||||
skill_check: Optional[dict] = None # {skill, dc, roll, skill_level, success}
|
||||
result_text: str
|
||||
steps_delta: int
|
||||
gold_delta: int = 0
|
||||
items_gained: list[dict]
|
||||
xp_gained: Optional[dict] = None # {skill, xp}
|
||||
step_balance: int
|
||||
gold: int = 0
|
||||
|
||||
|
||||
@router.post("/event/resolve", response_model=ResolveOut)
|
||||
async def resolve_event(r: ResolveIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
if not char.pending_event:
|
||||
raise HTTPException(400, "Bekleyen olay yok")
|
||||
|
||||
ev = (await db.execute(text(
|
||||
"SELECT code, name, choices FROM events WHERE code = :c"
|
||||
), {"c": char.pending_event.get("code")})).first()
|
||||
if not ev:
|
||||
char.pending_event = None
|
||||
await db.commit()
|
||||
raise HTTPException(500, "Olay bulunamadı, temizlendi")
|
||||
|
||||
choices = ev[2]
|
||||
if r.choice >= len(choices):
|
||||
raise HTTPException(400, "Geçersiz seçim")
|
||||
ch = choices[r.choice]
|
||||
|
||||
# Skill check'li mi?
|
||||
skill_check = None
|
||||
if "skill" in ch:
|
||||
skill = (await db.execute(
|
||||
select(Skill).where(Skill.character_id == char.id, Skill.skill_name == ch["skill"])
|
||||
)).scalar_one_or_none()
|
||||
lvl = skill.level if skill else 1
|
||||
roll = random.randint(1, 20)
|
||||
success = roll + lvl >= int(ch["dc"])
|
||||
skill_check = {"skill": ch["skill"], "dc": int(ch["dc"]), "roll": roll,
|
||||
"skill_level": lvl, "success": success}
|
||||
outcome = ch["success"] if success else ch["fail"]
|
||||
else:
|
||||
outcome = ch["outcome"]
|
||||
|
||||
# Sonucu uygula
|
||||
steps_delta = int(outcome.get("steps", 0))
|
||||
if steps_delta < 0:
|
||||
steps_delta = -min(-steps_delta, char.step_balance) # bakiye eksiye düşmez
|
||||
char.step_balance += steps_delta
|
||||
|
||||
gold_delta = int(outcome.get("gold", 0))
|
||||
if gold_delta < 0:
|
||||
gold_delta = -min(-gold_delta, char.gold)
|
||||
char.gold += gold_delta
|
||||
|
||||
items_gained = []
|
||||
for it in outcome.get("items", []):
|
||||
await _add_items(db, char.id, it["code"], int(it["qty"]))
|
||||
name = (await db.execute(text("SELECT name FROM items WHERE code=:c"), {"c": it["code"]})).first()
|
||||
items_gained.append({"code": it["code"], "name": name[0] if name else it["code"], "qty": int(it["qty"])})
|
||||
|
||||
xp_gained = None
|
||||
if "xp" in outcome:
|
||||
skill_name = outcome["xp"]["skill"]
|
||||
skill = (await db.execute(
|
||||
select(Skill).where(Skill.character_id == char.id, Skill.skill_name == skill_name)
|
||||
)).scalar_one_or_none()
|
||||
if skill is None:
|
||||
skill = Skill(character_id=char.id, skill_name=skill_name, xp=0, level=1)
|
||||
db.add(skill)
|
||||
await db.flush()
|
||||
_grant_xp(char, skill, int(outcome["xp"]["amount"]))
|
||||
xp_gained = {"skill": skill_name, "xp": int(outcome["xp"]["amount"])}
|
||||
|
||||
char.pending_event = None
|
||||
char.last_sync_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
return ResolveOut(
|
||||
event_code=ev[0],
|
||||
choice_text=ch["text"],
|
||||
skill_check=skill_check,
|
||||
result_text=outcome.get("text", ""),
|
||||
steps_delta=steps_delta,
|
||||
gold_delta=gold_delta,
|
||||
items_gained=items_gained,
|
||||
xp_gained=xp_gained,
|
||||
step_balance=char.step_balance,
|
||||
gold=char.gold,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue