İ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
251
routers/craft.py
Normal file
251
routers/craft.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"""
|
||||
Crafting: tarifleri listele, üret.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/craft", tags=["craft"])
|
||||
|
||||
|
||||
class IngredientOut(BaseModel):
|
||||
item_code: str
|
||||
item_name: str
|
||||
qty: int
|
||||
have: int
|
||||
|
||||
|
||||
class RecipeOut(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
name: str
|
||||
output_item_code: str
|
||||
output_item_name: str
|
||||
output_qty: int
|
||||
skill: str
|
||||
required_level: int
|
||||
xp_reward: int
|
||||
step_cost: int
|
||||
ingredients: list[IngredientOut]
|
||||
unlocked: bool
|
||||
craftable: bool
|
||||
|
||||
|
||||
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 _inventory_map(db: AsyncSession, char_id: int) -> dict[str, int]:
|
||||
rows = (await db.execute(text("""
|
||||
SELECT i.code, SUM(inv.qty)::int AS qty
|
||||
FROM inventory inv JOIN items i ON i.id = inv.item_id
|
||||
WHERE inv.character_id = :cid GROUP BY i.code
|
||||
"""), {"cid": char_id})).all()
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
async def _skill_levels(db: AsyncSession, char_id: int) -> dict[str, int]:
|
||||
rows = (await db.execute(text(
|
||||
"SELECT skill_name, level FROM skills WHERE character_id = :cid"
|
||||
), {"cid": char_id})).all()
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
@router.get("/recipes", response_model=list[RecipeOut])
|
||||
async def list_recipes(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
inv = await _inventory_map(db, char.id)
|
||||
skl = await _skill_levels(db, char.id)
|
||||
bal = char.step_balance
|
||||
|
||||
rows = (await db.execute(text("""
|
||||
SELECT r.id, r.code, r.name, r.output_item_id, r.output_qty,
|
||||
r.skill, r.required_level, r.xp_reward, r.step_cost, r.ingredients,
|
||||
oi.code, oi.name
|
||||
FROM recipes r JOIN items oi ON oi.id = r.output_item_id
|
||||
ORDER BY r.skill, r.required_level
|
||||
"""))).all()
|
||||
|
||||
out: list[RecipeOut] = []
|
||||
for r in rows:
|
||||
ings: list[IngredientOut] = []
|
||||
all_ok = True
|
||||
for ing in r[9]: # JSONB
|
||||
code = ing["item_code"]
|
||||
qty = int(ing["qty"])
|
||||
item_row = (await db.execute(text("SELECT name FROM items WHERE code=:c"), {"c": code})).first()
|
||||
have = inv.get(code, 0)
|
||||
if have < qty:
|
||||
all_ok = False
|
||||
ings.append(IngredientOut(item_code=code, item_name=item_row[0] if item_row else code, qty=qty, have=have))
|
||||
|
||||
unlocked = skl.get(r[5], 1) >= r[6]
|
||||
affordable_steps = bal >= r[8]
|
||||
craftable = unlocked and all_ok and affordable_steps
|
||||
|
||||
out.append(RecipeOut(
|
||||
id=r[0], code=r[1], name=r[2],
|
||||
output_item_code=r[10], output_item_name=r[11], output_qty=r[4],
|
||||
skill=r[5], required_level=r[6], xp_reward=r[7], step_cost=r[8],
|
||||
ingredients=ings, unlocked=unlocked, craftable=craftable,
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
class CraftIn(BaseModel):
|
||||
recipe_code: str
|
||||
qty: int = Field(1, ge=1, le=50)
|
||||
|
||||
|
||||
class CraftOut(BaseModel):
|
||||
output_item_code: str
|
||||
output_item_name: str
|
||||
output_qty: int
|
||||
skill: str
|
||||
xp_gained: int
|
||||
skill_level: int
|
||||
skill_level_up: bool
|
||||
character_level: int
|
||||
character_level_up: bool
|
||||
step_balance: int
|
||||
|
||||
|
||||
@router.post("", response_model=CraftOut)
|
||||
async def craft(c: CraftIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
char = await _char(db, user)
|
||||
|
||||
rcp = (await db.execute(text("""
|
||||
SELECT id, output_item_id, output_qty, skill, required_level, xp_reward, step_cost, ingredients
|
||||
FROM recipes WHERE code = :c
|
||||
"""), {"c": c.recipe_code})).first()
|
||||
if not rcp:
|
||||
raise HTTPException(404, "Tarif yok")
|
||||
|
||||
skill_name = rcp[3]
|
||||
required_level = rcp[4]
|
||||
step_cost_per = rcp[6]
|
||||
xp_per = rcp[5]
|
||||
ingredients = rcp[7]
|
||||
|
||||
# Skill seviye
|
||||
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()
|
||||
if skill.level < required_level:
|
||||
raise HTTPException(400, f"{skill_name} {required_level} gerekli (mevcut {skill.level})")
|
||||
|
||||
# Toplam adım
|
||||
total_step = step_cost_per * c.qty
|
||||
if char.step_balance < total_step:
|
||||
raise HTTPException(400, f"Yetersiz adım: {total_step} gerekli, {char.step_balance} var")
|
||||
|
||||
# Malzeme yeterli mi
|
||||
inv = await _inventory_map(db, char.id)
|
||||
for ing in ingredients:
|
||||
need = int(ing["qty"]) * c.qty
|
||||
have = inv.get(ing["item_code"], 0)
|
||||
if have < need:
|
||||
raise HTTPException(400, f"{ing['item_code']} yetersiz: {need} gerekli, {have} var")
|
||||
|
||||
# Malzemeleri tüket (FIFO slotlardan)
|
||||
for ing in ingredients:
|
||||
need = int(ing["qty"]) * c.qty
|
||||
item_id_row = (await db.execute(text("SELECT id FROM items WHERE code=:c"), {"c": ing["item_code"]})).first()
|
||||
if not item_id_row:
|
||||
raise HTTPException(500, f"Item bulunamadı: {ing['item_code']}")
|
||||
item_id = item_id_row[0]
|
||||
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()
|
||||
remaining = need
|
||||
for st in stacks:
|
||||
if remaining <= 0:
|
||||
break
|
||||
take = min(remaining, st[1])
|
||||
if take == st[1]:
|
||||
await db.execute(text("DELETE FROM inventory WHERE id = :id"), {"id": st[0]})
|
||||
else:
|
||||
await db.execute(text("UPDATE inventory SET qty = qty - :t WHERE id = :id"), {"t": take, "id": st[0]})
|
||||
remaining -= take
|
||||
if remaining > 0:
|
||||
raise HTTPException(500, "İç tutarsızlık: malzeme eksildi ama bulunamadı")
|
||||
|
||||
# Output ekle (stack ya da yeni slot)
|
||||
out_qty = int(rcp[2]) * c.qty
|
||||
out_item_id = rcp[1]
|
||||
out_row = (await db.execute(text("SELECT code, name, stack_max FROM items WHERE id = :i"), {"i": out_item_id})).first()
|
||||
out_code = out_row[0]
|
||||
out_name = out_row[1]
|
||||
stack_max = int(out_row[2])
|
||||
|
||||
remaining = out_qty
|
||||
# Önce mevcut stack'lere ekle (stack_max'a kadar)
|
||||
existing = (await db.execute(text("""
|
||||
SELECT id, qty FROM inventory WHERE character_id = :cid AND item_id = :iid ORDER BY slot
|
||||
"""), {"cid": char.id, "iid": out_item_id})).all()
|
||||
for ex in existing:
|
||||
if remaining <= 0:
|
||||
break
|
||||
space = stack_max - ex[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": ex[0]})
|
||||
remaining -= add
|
||||
# Yeni slot(lar)
|
||||
while remaining > 0:
|
||||
add = min(stack_max, remaining)
|
||||
slot_row = (await db.execute(text(
|
||||
"SELECT COALESCE(MAX(slot), -1) + 1 FROM inventory WHERE character_id = :cid"
|
||||
), {"cid": char.id})).first()
|
||||
next_slot = slot_row[0]
|
||||
await db.execute(text("""
|
||||
INSERT INTO inventory (character_id, item_id, qty, slot)
|
||||
VALUES (:cid, :iid, :q, :s)
|
||||
"""), {"cid": char.id, "iid": out_item_id, "q": add, "s": next_slot})
|
||||
remaining -= add
|
||||
|
||||
# XP + adım
|
||||
xp_gain = xp_per * c.qty
|
||||
old_sl = skill.level
|
||||
skill.xp += xp_gain
|
||||
skill.level = skill_level_from_xp(skill.xp)
|
||||
skill_up = skill.level > old_sl
|
||||
|
||||
old_cl = char.level
|
||||
char.total_xp += xp_gain
|
||||
char.level = character_level_from_total_xp(char.total_xp)
|
||||
char_up = char.level > old_cl
|
||||
|
||||
char.step_balance -= total_step
|
||||
char.last_sync_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
return CraftOut(
|
||||
output_item_code=out_code,
|
||||
output_item_name=out_name,
|
||||
output_qty=out_qty,
|
||||
skill=skill_name,
|
||||
xp_gained=xp_gain,
|
||||
skill_level=skill.level,
|
||||
skill_level_up=skill_up,
|
||||
character_level=char.level,
|
||||
character_level_up=char_up,
|
||||
step_balance=char.step_balance,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue