# retoor <retoor@molodetz.nl>
from __future__ import annotations
import json
from datetime import datetime, timedelta
from .. import economy
from .common import (
PERK_COLUMN,
_iso_week,
_lvl,
_now,
_parse,
_parse_date,
_quests,
next_streak,
steal_cooldown_remaining,
)
from .cosmetics import owned_cosmetic_keys
from .era import active_era_name
from .farm import get_farm, get_plots
from .infrastructure import INFRA_COLUMN, owns_infrastructure
from .market import market_factor_for
from .mastery import MASTERY_COLUMN
from .quests import ensure_quests
def _watered_by(plot: dict) -> list[str]:
try:
value = json.loads(plot.get("watered_by") or "[]")
return value if isinstance(value, list) else []
except (ValueError, TypeError):
return []
def _plot_state(plot: dict, now: datetime) -> str:
if not plot.get("crop_key"):
return "empty"
ready_at = _parse(plot.get("ready_at", ""))
if ready_at and now >= ready_at:
return "ready"
return "growing"
def serialize_plot(
plot: dict,
*,
viewer_uid: str,
owner_uid: str,
now: datetime,
yield_level: int = 0,
prestige: int = 0,
growth_level: int = 0,
ci_tier: int = 1,
legacy_mult_level: int = 0,
legacy_speed_level: int = 0,
defense_level: int = 0,
steal_locked_until: int = 0,
steal_grace_bonus: int = 0,
steal_floor: float = 0.1,
steal_reduction: float = 0.0,
steal_cap: float = 1.0,
registry_boost: bool = False,
canary: bool = False,
underdog: bool = False,
contract_boost: bool = False,
) -> dict:
state = _plot_state(plot, now)
crop = economy.crop_for(plot.get("crop_key", ""))
ready_at = _parse(plot.get("ready_at", ""))
remaining = 0
if state == "growing" and ready_at:
remaining = max(0, int((ready_at - now).total_seconds()))
watered = _watered_by(plot)
is_owner = viewer_uid == owner_uid
golden = economy.is_golden(plot.get("uid", ""), plot.get("planted_at", ""))
can_water = (
state == "growing"
and not is_owner
and bool(viewer_uid)
and viewer_uid not in watered
and len(watered) < economy.MAX_WATERS_PER_PLOT
)
grace = economy.effective_steal_grace(defense_level, steal_grace_bonus)
protected = bool(ready_at) and now < ready_at + timedelta(seconds=grace)
immune = bool(crop and crop.steal_immune)
stripped = float(plot.get("raided_fraction") or 0.0) >= 1.0
eligible = (
state == "ready"
and not is_owner
and bool(viewer_uid)
and bool(crop)
and ready_at is not None
and not immune
and not stripped
)
can_steal = eligible and not protected and steal_locked_until <= 0
steal_reason = ""
if immune and state == "ready" and not is_owner:
steal_reason = "immune"
elif stripped and state == "ready" and not is_owner:
steal_reason = "stripped"
elif eligible and protected:
steal_reason = "protected"
elif eligible and steal_locked_until > 0:
steal_reason = "cooldown"
market_factor = market_factor_for(crop.key) if crop else 1.0
raided_fraction = float(plot.get("raided_fraction") or 0.0)
steal_share = min(
economy.effective_steal_fraction(
defense_level, steal_floor, steal_reduction, steal_cap
),
max(0.0, 1.0 - raided_fraction),
)
steal_coins = (
max(
1,
round(
economy.realizable_harvest_coins(
crop,
yield_level,
prestige,
legacy_mult_level,
market_factor,
golden,
contract_boost,
underdog,
)
* steal_share
),
)
if can_steal
else 0
)
fertilize_cost = 0
if state == "growing" and is_owner and crop:
full_grow = economy.grow_seconds_for(
crop, ci_tier, growth_level, legacy_speed_level, registry_boost
)
reduce_by = int(remaining * economy.FERTILIZE_FRACTION)
realizable = economy.realizable_harvest_coins(
crop,
yield_level,
prestige,
legacy_mult_level,
market_factor,
golden,
contract_boost,
underdog,
canary,
)
fertilize_cost = economy.fertilize_click_cost(realizable, reduce_by, full_grow)
return {
"slot": plot.get("slot_index", 0),
"state": state,
"crop_key": plot.get("crop_key", ""),
"crop_name": crop.name if crop else "",
"crop_icon": crop.icon if crop else "",
"reward_coins": crop.reward_coins if crop else 0,
"reward_xp": crop.reward_xp if crop else 0,
"ready_at": plot.get("ready_at", ""),
"remaining_seconds": remaining,
"watered_count": len(watered),
"max_waters": economy.MAX_WATERS_PER_PLOT,
"can_water": can_water,
"can_steal": can_steal,
"steal_coins": steal_coins,
"steal_cooldown_seconds": steal_locked_until if steal_reason == "cooldown" else 0,
"steal_reason": steal_reason,
"is_golden": golden and (is_owner or can_steal),
"fertilize_cost": fertilize_cost,
"raided_fraction": round(raided_fraction, 4),
"raided_pct": round(raided_fraction * 100),
}
def serialize_farm(
farm: dict, *, viewer: dict | None, owner: dict, now: datetime | None = None
) -> dict:
from .actions import _auto_harvest, boost_flags
from .defense import charge_upkeep
from .treasury import grant_status, treasury_balance
now = now or _now()
viewer_uid = viewer["uid"] if viewer else ""
owner_uid = owner["uid"]
is_owner = viewer_uid == owner_uid
auto = {"harvested": 0, "coins": 0, "xp": 0}
if is_owner and _lvl(farm, "legacy_autoharvest") > 0:
farm, auto = _auto_harvest(farm, owner_uid, now)
if is_owner:
farm = charge_upkeep(farm, now)
farm = _reset_harvests_week(farm, now)
prestige = _lvl(farm, "prestige")
growth_level = _lvl(farm, "perk_growth")
discount_level = _lvl(farm, "perk_discount")
yield_level = _lvl(farm, "perk_yield")
legacy_mult_level = _lvl(farm, "legacy_multiplier")
legacy_speed_level = _lvl(farm, "legacy_speed")
defense_level = _lvl(farm, "legacy_defense")
ci_tier = int(farm.get("ci_tier", 1))
steal_locked_until = (
steal_cooldown_remaining(viewer_uid, owner_uid, now)
if viewer_uid and not is_owner
else 0
)
building_defense_level = _lvl(farm, "defense_level")
defense_tier_info = economy.defense_tier(building_defense_level)
steal_floor = defense_tier_info.steal_fraction_floor
steal_cap = (
economy.OBSERVABILITY_STEAL_CAP
if owns_infrastructure(farm, "observability")
else 1.0
)
registry_boost = owns_infrastructure(farm, "registry")
underdog_active, contract_active = boost_flags(farm, now)
plots = get_plots(farm["uid"])
serialized_plots = [
serialize_plot(
plot,
viewer_uid=viewer_uid,
owner_uid=owner_uid,
now=now,
yield_level=yield_level,
prestige=prestige,
growth_level=growth_level,
ci_tier=ci_tier,
legacy_mult_level=legacy_mult_level,
legacy_speed_level=legacy_speed_level,
defense_level=defense_level,
steal_locked_until=steal_locked_until,
steal_grace_bonus=defense_tier_info.grace_bonus,
steal_floor=steal_floor,
steal_reduction=defense_tier_info.steal_reduction,
steal_cap=steal_cap,
registry_boost=registry_boost,
canary=owns_infrastructure(farm, "canary"),
underdog=underdog_active,
contract_boost=contract_active,
)
for plot in plots
]
progress = economy.level_progress(int(farm.get("xp", 0)))
level = progress["level"]
next_tier = economy.next_ci_tier(ci_tier)
ci_entry = economy.CI_BY_TIER.get(ci_tier)
streak = _lvl(farm, "streak")
daily_available = is_owner and _daily_available(farm, now)
mastery_earned = _lvl(farm, "mastery_points_earned_total")
era_name = active_era_name()
underdog_until = _parse(farm.get("underdog_boost_until") or "")
underdog_seconds = (
max(0, int((underdog_until - now).total_seconds())) if underdog_until else 0
)
contract_until = _parse(farm.get("contract_boost_until") or "")
contract_seconds = (
max(0, int((contract_until - now).total_seconds())) if contract_until else 0
)
income_scale = economy.social_reward_scale(prestige, legacy_mult_level)
effective_streak = next_streak(farm.get("last_daily_at") or "", streak, now)
coins = int(farm.get("coins", 0))
refactor_fee = economy.refactor_cost(prestige, coins)
carryover_level = _lvl(farm, "legacy_carryover")
grant = (
grant_status(farm, now)
if is_owner
else {"available": False, "amount": 0, "reason": ""}
)
return {
"owner_username": owner.get("username", ""),
"owner_uid": owner_uid,
"is_owner": is_owner,
"coins": int(farm.get("coins", 0)),
"xp": int(farm.get("xp", 0)),
"level": level,
"level_into": progress["into_level"],
"level_span": progress["span"],
"level_is_max": progress["is_max"],
"total_harvests": int(farm.get("total_harvests", 0)),
"plot_count": int(farm.get("plot_count", economy.STARTING_PLOTS)),
"max_plots": economy.MAX_PLOTS,
"next_plot_cost": (
economy.plot_cost(int(farm.get("plot_count", economy.STARTING_PLOTS)))
if int(farm.get("plot_count", economy.STARTING_PLOTS)) < economy.MAX_PLOTS
else 0
),
"ci_tier": ci_tier,
"ci_label": ci_entry.label if ci_entry else "",
"ci_speed": economy.ci_speed(ci_tier),
"ci_next_tier": next_tier.tier if next_tier else 0,
"ci_next_label": next_tier.label if next_tier else "",
"ci_next_cost": next_tier.upgrade_cost if next_tier else 0,
"plots": serialized_plots,
"crops": [
economy.crop_payload(
crop,
ci_tier,
level,
growth_level,
discount_level,
yield_level,
prestige,
legacy_mult_level,
legacy_speed_level,
market_factor_for(crop.key),
mastery_earned,
era_name,
registry_boost,
)
for crop in economy.CROPS
],
"prestige": prestige,
"prestige_multiplier": economy.prestige_multiplier(prestige),
"prestige_min_level": economy.PRESTIGE_MIN_LEVEL,
"prestige_available": is_owner and level >= economy.PRESTIGE_MIN_LEVEL,
"refactor_cost": refactor_fee,
"refactor_affordable": is_owner
and level >= economy.PRESTIGE_MIN_LEVEL
and coins >= refactor_fee,
"refactor_carryover_pct": round(
economy.refactor_carryover_fraction(carryover_level) * 100
),
"refactor_carryover_preview": economy.refactor_carryover(
coins, refactor_fee, carryover_level
),
"grant_available": grant["available"],
"grant_amount": grant["amount"],
"grant_reason": grant["reason"],
"treasury_balance": treasury_balance() if is_owner else 0,
"streak": streak,
"daily_available": daily_available,
"daily_streak_reset": daily_available and effective_streak == 1 and streak > 0,
"daily_reward": economy.daily_reward(
effective_streak if daily_available else max(1, streak), income_scale
),
"perks": _serialize_perks(farm) if is_owner else [],
"quests": _serialize_quests(owner_uid, now) if is_owner else [],
"stars": _lvl(farm, "stars"),
"legacy": _serialize_legacy(farm) if is_owner else [],
"steal_cooldown_seconds": steal_locked_until,
"mastery_points": _lvl(farm, "mastery_points"),
"mastery_points_earned_total": mastery_earned,
"mastery": _serialize_mastery(farm) if is_owner else [],
"infrastructure": _serialize_infrastructure(farm) if is_owner else [],
"defense_level": building_defense_level,
"defense_tier_name": defense_tier_info.name,
"defense_upkeep_daily": economy.daily_upkeep(defense_tier_info, int(farm.get("coins", 0))),
"defense_downgrade_available": is_owner and building_defense_level > 0,
"defense_next_cost": (
economy.next_defense_tier(building_defense_level).upgrade_cost
if economy.next_defense_tier(building_defense_level)
else 0
),
"cosmetics": _serialize_cosmetics(owner_uid) if is_owner else [],
"active_title": farm.get("active_title") or "",
"underdog_boost_seconds_remaining": underdog_seconds,
"contract_boost_seconds_remaining": contract_seconds,
"auto_harvested": int(auto.get("harvested") or 0),
"auto_harvest_coins": int(auto.get("coins") or 0),
"auto_harvest_xp": int(auto.get("xp") or 0),
"steal_max_per_victim_per_day": economy.STEAL_MAX_PER_VICTIM_PER_DAY,
"mastery_analytics_unlocked": bool(_lvl(farm, "mastery_analytics")),
"lifetime_coins_earned": _lvl(farm, "lifetime_coins_earned"),
"lifetime_harvests": _lvl(farm, "lifetime_harvests"),
"harvests_week": _lvl(farm, "harvests_week"),
"era_active": era_name is not None,
"era_name": era_name or "",
"era_coins": _lvl(farm, "era_coins"),
"era_harvests": _lvl(farm, "era_harvests"),
}
def _reset_harvests_week(farm: dict, now: datetime) -> dict:
from .common import _update_farm
current_week = _iso_week(now)
if farm.get("harvests_week_start") == current_week:
return farm
fields = {"harvests_week": 0, "harvests_week_start": current_week}
_update_farm(farm["uid"], fields)
return {**farm, **fields}
def _serialize_mastery(farm: dict) -> list[dict]:
upgrades = []
for m in economy.MASTERY_UPGRADES:
level = _lvl(farm, MASTERY_COLUMN[m.key])
maxed = level >= m.max_level
upgrades.append(
{
"key": m.key,
"name": m.name,
"icon": m.icon,
"description": m.description,
"level": level,
"max_level": m.max_level,
"cost": 0 if maxed else economy.mastery_cost(m, level),
"maxed": maxed,
"effect": m.description,
}
)
return upgrades
def _serialize_infrastructure(farm: dict) -> list[dict]:
rows = []
for infra in economy.INFRASTRUCTURE:
owned = bool(_lvl(farm, INFRA_COLUMN[infra.key]))
rows.append(
{
"key": infra.key,
"name": infra.name,
"icon": infra.icon,
"description": infra.description,
"cost": infra.cost,
"min_prestige": infra.min_prestige,
"owned": owned,
}
)
return rows
def _serialize_cosmetics(user_uid: str) -> list[dict]:
owned = owned_cosmetic_keys(user_uid)
return [
{
"key": c.key,
"name": c.name,
"icon": c.icon,
"description": c.description,
"cost_coins": c.cost_coins,
"kind": c.kind,
"owned": c.key in owned,
}
for c in economy.COSMETICS
if c.era_key is None
]
def _daily_available(farm: dict, now: datetime) -> bool:
last = _parse_date(farm.get("last_daily_at") or "")
return last != now.date()
def _serialize_perks(farm: dict) -> list[dict]:
perks = []
for perk in economy.PERKS:
level = _lvl(farm, PERK_COLUMN[perk.key])
maxed = level >= perk.max_level
perks.append(
{
"key": perk.key,
"name": perk.name,
"icon": perk.icon,
"description": perk.description,
"level": level,
"max_level": perk.max_level,
"cost": 0 if maxed else economy.perk_cost(perk, level),
"maxed": maxed,
"effect": economy.perk_value_text(perk, level),
}
)
return perks
def _serialize_legacy(farm: dict) -> list[dict]:
upgrades = []
for up in economy.LEGACY_UPGRADES:
level = _lvl(farm, f"legacy_{up.key}")
maxed = level >= up.max_level
upgrades.append(
{
"key": up.key,
"name": up.name,
"icon": up.icon,
"description": up.description,
"level": level,
"max_level": up.max_level,
"cost": 0 if maxed else economy.legacy_cost(up, level),
"maxed": maxed,
"effect": economy.legacy_value_text(up, level),
}
)
return upgrades
def _serialize_quests(user_uid: str, now: datetime) -> list[dict]:
from .quests import ensure_weekly_contract
farm = get_farm(user_uid)
if not farm:
return []
day = now.date().isoformat()
ensure_quests(farm, day)
rows = sorted(
_quests().find(farm_uid=farm["uid"], day=day, scope="daily"),
key=lambda row: row.get("slot_index", 0),
)
if _lvl(farm, "mastery_contracts"):
iso_week = _iso_week(now)
ensure_weekly_contract(farm, iso_week)
rows += sorted(
_quests().find(farm_uid=farm["uid"], day=iso_week, scope="weekly"),
key=lambda row: row.get("slot_index", 0),
)
quests = []
for row in rows:
goal = int(row.get("goal") or 0)
progress = min(goal, int(row.get("progress") or 0))
claimed = bool(row.get("claimed"))
quests.append(
{
"kind": row.get("kind", ""),
"label": row.get("label", ""),
"goal": goal,
"progress": progress,
"reward_coins": int(row.get("reward_coins") or 0),
"reward_xp": int(row.get("reward_xp") or 0),
"claimed": claimed,
"can_claim": (not claimed) and goal > 0 and progress >= goal,
"scope": row.get("scope", "daily"),
"reward_stars": int(row.get("reward_stars") or 0),
}
)
return quests