İ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
160
routers/harvest.py
Normal file
160
routers/harvest.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"""
|
||||
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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue