""" 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, )