stepstead-backend/routers/harvest.py
jts ea049b8ccd İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:24 +03:00

160 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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