""" Envanter + consumable kullanımı + tool tanımları. NOT (2026-07-04): Node hasat sistemi kaldırıldı — kaynak toplama artık lokasyon bazlı (routers/travel.py: POST /world/gather). SKILL_TOOLS burada tanımlı kalır, travel.py buradan import eder. """ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import bindparam, select, text from sqlalchemy.ext.asyncio import AsyncSession from deps import get_current_user from models import Character, User, get_db router = APIRouter(prefix="/world", tags=["inventory"]) # Tool şartı: bu skill'lerle toplama için envanterde listedeki tool'lardan biri olmalı. # Foraging (elle toplama) serbest. İlk tool'lar elle toplanan taş/sopa/sarmaşıktan craft edilir. SKILL_TOOLS = { "woodcutting": ["axe_stone", "axe_copper", "axe_iron"], "mining": ["pickaxe_stone", "pickaxe_copper", "pickaxe_iron"], "fishing": ["fishing_rod", "fishing_rod_fine", "fishing_rod_master"], } TOOL_LABEL = {"woodcutting": "Balta", "mining": "Kazma", "fishing": "Olta"} async def _has_any_tool(db: AsyncSession, char_id: int, codes: list[str]) -> bool: stmt = text(""" SELECT 1 FROM inventory inv JOIN items i ON i.id = inv.item_id WHERE inv.character_id = :cid AND i.code IN :codes LIMIT 1 """).bindparams(bindparam("codes", expanding=True)) row = (await db.execute(stmt, {"cid": char_id, "codes": codes})).first() return row is not None async def consume_items(db: AsyncSession, char_id: int, item_id: int, qty: int, item_name: str) -> None: """Envanterden FIFO tüket — market/camp/use-item ortak paterni. Yetersizse HTTPException(400) fırlatır.""" 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() have = sum(s[1] for s in stacks) if have < qty: raise HTTPException(400, f"{item_name} yetersiz: {qty} gerekli, {have} var") remaining = qty 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 class InventoryItem(BaseModel): slot: int item_code: str item_name: str rarity: str qty: int icon: str | None = None item_type: str = "material" effect: dict | None = None @router.get("/inventory", response_model=list[InventoryItem]) async def inventory(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: return [] rows = (await db.execute(text(""" SELECT inv.slot, i.code, i.name, i.rarity, inv.qty, i.icon, i.type, i.effect FROM inventory inv JOIN items i ON i.id = inv.item_id WHERE inv.character_id = :cid ORDER BY inv.slot """), {"cid": char.id})).all() return [ InventoryItem(slot=r[0], item_code=r[1], item_name=r[2], rarity=r[3], qty=r[4], icon=r[5], item_type=r[6], effect=r[7]) for r in rows ] class UseItemIn(BaseModel): item_code: str qty: int = Field(1, ge=1, le=10) class UseItemOut(BaseModel): item_code: str item_name: str qty_used: int effect_type: str effect_amount: int step_balance: int @router.post("/use-item", response_model=UseItemOut) async def use_item(u: UseItemIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none() if not char: raise HTTPException(404, "Karakter yok") item = (await db.execute( text("SELECT id, name, type, effect FROM items WHERE code = :c"), {"c": u.item_code} )).first() if not item: raise HTTPException(404, "Item yok") item_id, item_name, item_type, effect = item[0], item[1], item[2], item[3] if item_type != "consumable" or not effect: raise HTTPException(400, f"{item_name} kullanılabilir bir item değil") if effect.get("type") != "steps": raise HTTPException(400, f"Bilinmeyen etki türü: {effect.get('type')}") # Envanterde yeterli mi (FIFO tüket — craft ile aynı patern) 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() have = sum(s[1] for s in stacks) if have < u.qty: raise HTTPException(400, f"{item_name} yetersiz: {u.qty} gerekli, {have} var") remaining = u.qty 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 # Etki: adım bakiyesi yenile (lifetime'a sayılmaz — gerçek yürüyüş değil) gained = int(effect["amount"]) * u.qty char.step_balance += gained await db.execute( text("INSERT INTO step_log (character_id, steps_delta, source, accepted, reason) " "VALUES (:cid, :d, 'consumable', true, :r)"), {"cid": char.id, "d": gained, "r": f"{u.item_code} x{u.qty}"}, ) char.last_sync_at = datetime.now(timezone.utc) await db.commit() return UseItemOut( item_code=u.item_code, item_name=item_name, qty_used=u.qty, effect_type="steps", effect_amount=gained, step_balance=char.step_balance, )