Files
devplacepy/devplacepy/services/game/store/common.py
T
2026-07-26 16:46:41 +02:00

201 lines
5.5 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from devplacepy.database import conditional_update_row, get_table
from .. import economy
class GameError(Exception):
pass
def _now() -> datetime:
return datetime.now(timezone.utc)
def _iso(value: datetime) -> str:
return value.isoformat()
def _parse(value: str) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value)
except ValueError:
return None
def _today() -> str:
return _now().date().isoformat()
def _iso_week(value: datetime | None = None) -> str:
value = value or _now()
year, week, _ = value.isocalendar()
return f"{year}-W{week:02d}"
def _parse_date(value: str):
parsed = _parse(value)
return parsed.date() if parsed else None
def _farms():
return get_table("game_farms")
def _plots():
return get_table("game_plots")
def _quests():
return get_table("game_quests")
def _steals():
return get_table("game_steals")
def last_steal_at(thief_uid: str, owner_uid: str) -> datetime | None:
from devplacepy.database import db
rows = db.query(
"SELECT stolen_at FROM game_steals WHERE thief_uid = :thief AND owner_uid = :owner "
"ORDER BY stolen_at DESC LIMIT 1",
thief=thief_uid,
owner=owner_uid,
)
for row in rows:
return _parse(row.get("stolen_at") or "")
return None
def steal_cooldown_remaining(thief_uid: str, owner_uid: str, now: datetime) -> int:
latest = last_steal_at(thief_uid, owner_uid)
if not latest:
return 0
elapsed = (now - latest).total_seconds()
return max(0, int(economy.STEAL_COOLDOWN_SECONDS - elapsed))
def raids_against_today(owner_uid: str, now: datetime) -> int:
from devplacepy.database import db
cutoff = _iso(now - timedelta(days=1))
rows = db.query(
"SELECT COUNT(*) AS raids FROM game_steals "
"WHERE owner_uid = :owner AND stolen_at >= :cutoff",
owner=owner_uid,
cutoff=cutoff,
)
for row in rows:
return int(row.get("raids") or 0)
return 0
def next_streak(last_daily_at: str, streak: int, now: datetime) -> int:
return streak + 1 if _parse_date(last_daily_at or "") == now.date() - timedelta(days=1) else 1
def _lvl(farm: dict, key: str) -> int:
return int(farm.get(key) or 0)
PERK_COLUMN = {perk.key: f"perk_{perk.key}" for perk in economy.PERKS}
def _update_farm(farm_uid: str, fields: dict) -> None:
fields = {**fields, "uid": farm_uid, "updated_at": _iso(_now())}
_farms().update(fields, ["uid"])
def conditional_update_farm(farm_uid: str, set_clause: str, where_clause: str, params: dict) -> int:
return conditional_update_row("game_farms", farm_uid, set_clause, where_clause, params)
def clear_plot(plot_uid: str, expected_planted_at: str) -> int:
return conditional_update_row(
"game_plots",
plot_uid,
"crop_key = '', planted_at = '', ready_at = '', watered_by = '[]', raided_fraction = 0",
"crop_key != '' AND planted_at = :expected_planted",
{"expected_planted": expected_planted_at},
)
def refund_farm(farm_uid: str, coins: int) -> None:
if coins <= 0:
return
conditional_update_farm(
farm_uid, "coins = COALESCE(coins, 0) + :refund", "1 = 1", {"refund": coins}
)
def _refresh_level(farm_uid: str) -> dict:
for _ in range(4):
row = _farms().find_one(uid=farm_uid)
if not row:
return {}
xp_seen = int(row.get("xp") or 0)
level = economy.level_for_xp(xp_seen)
if int(row.get("level") or 1) == level:
return row
rows = conditional_update_farm(
farm_uid,
"level = :level",
"COALESCE(xp, 0) = :xp_seen",
{"level": level, "xp_seen": xp_seen},
)
if rows:
return {**row, "level": level}
return _farms().find_one(uid=farm_uid) or {}
def credit_farm(
farm: dict,
*,
coins: int = 0,
xp: int = 0,
harvests: int = 0,
stars: int = 0,
era_active: bool = False,
extra: dict | None = None,
) -> dict:
set_parts = [
"coins = MAX(0, COALESCE(coins, 0) + :coins_delta)",
"xp = COALESCE(xp, 0) + :xp_delta",
"total_harvests = COALESCE(total_harvests, 0) + :harvests_delta",
"lifetime_coins_earned = COALESCE(lifetime_coins_earned, 0) + :earned_delta",
"lifetime_harvests = COALESCE(lifetime_harvests, 0) + :harvests_delta",
]
params = {
"coins_delta": coins,
"xp_delta": xp,
"harvests_delta": harvests,
"earned_delta": max(0, coins),
}
if stars:
set_parts.append("stars = COALESCE(stars, 0) + :stars_delta")
params["stars_delta"] = stars
if harvests:
set_parts.append(
"harvests_week = CASE WHEN harvests_week_start = :week "
"THEN COALESCE(harvests_week, 0) + :harvests_delta ELSE :harvests_delta END"
)
set_parts.append("harvests_week_start = :week")
params["week"] = _iso_week()
if era_active:
set_parts.append("era_coins = COALESCE(era_coins, 0) + :earned_delta")
set_parts.append("era_harvests = COALESCE(era_harvests, 0) + :harvests_delta")
if extra:
for index, (key, value) in enumerate(extra.items()):
set_parts.append(f"{key} = :extra_{index}")
params[f"extra_{index}"] = value
conditional_update_farm(farm["uid"], ", ".join(set_parts), "1 = 1", params)
return _refresh_level(farm["uid"])