251 lines
8.5 KiB
Python
251 lines
8.5 KiB
Python
"""
|
||
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,
|
||
)
|