201 lines
5.5 KiB
Python
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"])
|