feat: add per-user avatar seed regeneration with irreversible random avatar replacement

Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
This commit is contained in:
2026-06-27 22:31:34 +00:00
parent e773067106
commit 6f340a6818
60 changed files with 1626 additions and 97 deletions
+175 -16
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
import hashlib
import math
from dataclasses import dataclass
STARTING_COINS = 50
@@ -17,6 +19,10 @@ WATER_REWARD_XP = 3
STEAL_GRACE_SECONDS = 60
STEAL_FRACTION = 0.5
STEAL_COOLDOWN_SECONDS = 3600
GOLDEN_CHANCE = 0.05
GOLDEN_MULTIPLIER = 5
@dataclass(frozen=True)
@@ -77,16 +83,32 @@ def next_ci_tier(tier: int) -> CiTier | None:
return CI_BY_TIER.get(tier + 1)
def farm_speed(ci_tier: int, growth_level: int = 0) -> float:
return ci_speed(ci_tier) * (1 + PERK_BY_KEY["growth"].step * growth_level)
def farm_speed(ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0) -> float:
return (
ci_speed(ci_tier)
* (1 + PERK_BY_KEY["growth"].step * growth_level)
* (1 + LEGACY_SPEED_STEP * legacy_speed_level)
)
def grow_seconds_for(crop: Crop, ci_tier: int, growth_level: int = 0) -> int:
return max(1, round(crop.grow_seconds / farm_speed(ci_tier, growth_level)))
def grow_seconds_for(
crop: Crop, ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0
) -> int:
return max(
1, round(crop.grow_seconds / farm_speed(ci_tier, growth_level, legacy_speed_level))
)
def water_bonus_seconds(crop: Crop, ci_tier: int, growth_level: int = 0) -> int:
return max(1, round(grow_seconds_for(crop, ci_tier, growth_level) * WATER_BONUS_PCT))
def water_bonus_seconds(
crop: Crop, ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0
) -> int:
return max(
1,
round(
grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level)
* WATER_BONUS_PCT
),
)
def plot_cost(current_plot_count: int) -> int:
@@ -168,16 +190,20 @@ def crop_payload(
discount_level: int = 0,
yield_level: int = 0,
prestige: int = 0,
legacy_mult_level: int = 0,
legacy_speed_level: int = 0,
) -> dict:
return {
"key": crop.key,
"name": crop.name,
"icon": crop.icon,
"cost": effective_plant_cost(crop, discount_level),
"reward_coins": effective_reward_coins(crop, yield_level, prestige),
"reward_coins": effective_reward_coins(
crop, yield_level, prestige, legacy_mult_level
),
"reward_xp": crop.reward_xp,
"min_level": crop.min_level,
"grow_seconds": grow_seconds_for(crop, ci_tier, growth_level),
"grow_seconds": grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level),
"locked": crop.min_level > level,
}
@@ -206,13 +232,118 @@ PERK_BY_KEY = {perk.key: perk for perk in PERKS}
PRESTIGE_MIN_LEVEL = 10
PRESTIGE_BONUS = 0.25
STAR_BASE = 1
LEGACY_SPEED_STEP = 0.05
LEGACY_MULT_STEP = 0.10
LEGACY_DEFENSE_GRACE = 30
LEGACY_DEFENSE_FRACTION = 0.05
@dataclass(frozen=True)
class LegacyUpgrade:
key: str
name: str
icon: str
description: str
max_level: int
base_cost: int
cost_growth: float
LEGACY_UPGRADES: tuple[LegacyUpgrade, ...] = (
LegacyUpgrade(
"autoharvest",
"CI Bot",
"🤖",
"Auto-collect ready builds when you open your farm",
1,
3,
1.0,
),
LegacyUpgrade(
"multiplier",
"Tech Debt Payoff",
"💎",
"+10% coins per level, stacks with prestige",
10,
1,
1.6,
),
LegacyUpgrade(
"speed", "Bare-Metal", "🏎️", "+5% base build speed per level", 8, 1, 1.7
),
LegacyUpgrade(
"plots", "Monorepo", "🗂️", "+1 starting plot after refactor per level", 4, 3, 2.0
),
LegacyUpgrade(
"defense",
"Branch Protection",
"🛡️",
"+30s steal grace and -5% steal loss per level",
5,
2,
1.8,
),
)
LEGACY_BY_KEY = {up.key: up for up in LEGACY_UPGRADES}
def legacy_for(key: str) -> LegacyUpgrade | None:
return LEGACY_BY_KEY.get(key)
def legacy_cost(up: LegacyUpgrade, level: int) -> int:
return round(up.base_cost * (up.cost_growth ** level))
def legacy_multiplier(level: int) -> float:
return 1 + LEGACY_MULT_STEP * max(0, level)
def stars_for_refactor(level: int, prestige: int) -> int:
return STAR_BASE + level // 5 + max(0, prestige)
def effective_steal_grace(defense_level: int = 0) -> int:
return STEAL_GRACE_SECONDS + LEGACY_DEFENSE_GRACE * max(0, defense_level)
def effective_steal_fraction(defense_level: int = 0) -> float:
return max(0.1, STEAL_FRACTION - LEGACY_DEFENSE_FRACTION * max(0, defense_level))
def prestige_base_plots(legacy_plots_level: int = 0) -> int:
return STARTING_PLOTS + max(0, legacy_plots_level)
def legacy_value_text(up: LegacyUpgrade, level: int) -> str:
if up.key == "autoharvest":
return "Active" if level > 0 else "Inactive"
if up.key == "multiplier":
return f"+{round(LEGACY_MULT_STEP * level * 100)}% coins"
if up.key == "speed":
return f"+{round(LEGACY_SPEED_STEP * level * 100)}% build speed"
if up.key == "plots":
return f"+{level} starting plots"
grace = LEGACY_DEFENSE_GRACE * level
loss = round(LEGACY_DEFENSE_FRACTION * level * 100)
return f"+{grace}s grace, -{loss}% steal loss"
def is_golden(plot_uid: str, planted_at: str) -> bool:
if not plot_uid or not planted_at:
return False
digest = hashlib.sha256(f"{plot_uid}:{planted_at}".encode()).hexdigest()
return (int(digest, 16) % 1000) < round(GOLDEN_CHANCE * 1000)
DAILY_BASE = 20
DAILY_STREAK_STEP = 12
DAILY_STREAK_CAP = 7
FERTILIZE_FRACTION = 0.5
FERTILIZE_COIN_PER_SECOND = 0.3
FERTILIZE_MIN_COST = 5
FERTILIZE_TAX = 1.05
def perk_for(key: str) -> Perk | None:
@@ -242,8 +373,14 @@ def effective_plant_cost(crop: Crop, discount_level: int = 0) -> int:
return max(1, round(crop.cost * factor))
def effective_reward_coins(crop: Crop, yield_level: int = 0, prestige: int = 0) -> int:
factor = (1 + PERK_BY_KEY["yield"].step * yield_level) * prestige_multiplier(prestige)
def effective_reward_coins(
crop: Crop, yield_level: int = 0, prestige: int = 0, legacy_mult_level: int = 0
) -> int:
factor = (
(1 + PERK_BY_KEY["yield"].step * yield_level)
* prestige_multiplier(prestige)
* legacy_multiplier(legacy_mult_level)
)
return round(crop.reward_coins * factor)
@@ -251,8 +388,21 @@ def effective_reward_xp(crop: Crop, xp_level: int = 0) -> int:
return round(crop.reward_xp * (1 + PERK_BY_KEY["xp"].step * xp_level))
def steal_reward_coins(crop: Crop, yield_level: int = 0, prestige: int = 0) -> int:
return max(1, round(effective_reward_coins(crop, yield_level, prestige) * STEAL_FRACTION))
def steal_reward_coins(
crop: Crop,
yield_level: int = 0,
prestige: int = 0,
legacy_mult_level: int = 0,
defense_level: int = 0,
) -> int:
fraction = effective_steal_fraction(defense_level)
return max(
1,
round(
effective_reward_coins(crop, yield_level, prestige, legacy_mult_level)
* fraction
),
)
def daily_reward(streak: int) -> int:
@@ -260,8 +410,17 @@ def daily_reward(streak: int) -> int:
return DAILY_BASE + DAILY_STREAK_STEP * (effective - 1)
def fertilize_cost(remaining_seconds: int) -> int:
return max(FERTILIZE_MIN_COST, round(remaining_seconds * FERTILIZE_COIN_PER_SECOND))
def fertilize_click_cost(
effective_reward_coins: int, reduce_seconds: int, full_grow_seconds: int
) -> int:
if reduce_seconds < 1 or full_grow_seconds < 1:
return 0
return max(
1,
math.ceil(
effective_reward_coins * reduce_seconds / full_grow_seconds * FERTILIZE_TAX
),
)
@dataclass(frozen=True)
+236 -19
View File
@@ -53,6 +53,27 @@ 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:
latest = None
for row in _steals().find(thief_uid=thief_uid, owner_uid=owner_uid):
stamp = _parse(row.get("stolen_at", ""))
if stamp and (latest is None or stamp > latest):
latest = stamp
return latest
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 _lvl(farm: dict, key: str) -> int:
return int(farm.get(key) or 0)
@@ -141,6 +162,12 @@ def serialize_plot(
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,
) -> dict:
state = _plot_state(plot, now)
crop = economy.crop_for(plot.get("crop_key", ""))
@@ -150,6 +177,7 @@ def serialize_plot(
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
@@ -157,17 +185,38 @@ def serialize_plot(
and viewer_uid not in watered
and len(watered) < economy.MAX_WATERS_PER_PLOT
)
can_steal = (
grace = economy.effective_steal_grace(defense_level)
protected = bool(ready_at) and now < ready_at + timedelta(seconds=grace)
eligible = (
state == "ready"
and not is_owner
and bool(viewer_uid)
and bool(crop)
and ready_at is not None
and now >= ready_at + timedelta(seconds=economy.STEAL_GRACE_SECONDS)
)
can_steal = eligible and not protected and steal_locked_until <= 0
steal_reason = ""
if eligible and protected:
steal_reason = "protected"
elif eligible and steal_locked_until > 0:
steal_reason = "cooldown"
steal_coins = (
economy.steal_reward_coins(crop, yield_level, prestige) if can_steal else 0
economy.steal_reward_coins(
crop, yield_level, prestige, legacy_mult_level, defense_level
)
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
)
reduce_by = int(remaining * economy.FERTILIZE_FRACTION)
eff_reward = economy.effective_reward_coins(
crop, yield_level, prestige, legacy_mult_level
)
fertilize_cost = economy.fertilize_click_cost(eff_reward, reduce_by, full_grow)
return {
"slot": plot.get("slot_index", 0),
"state": state,
@@ -183,9 +232,10 @@ def serialize_plot(
"can_water": can_water,
"can_steal": can_steal,
"steal_coins": steal_coins,
"fertilize_cost": (
economy.fertilize_cost(remaining) if state == "growing" and is_owner else 0
),
"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,
}
@@ -195,10 +245,22 @@ def serialize_farm(
now = now or _now()
viewer_uid = viewer["uid"] if viewer else ""
owner_uid = owner["uid"]
is_owner = viewer_uid == owner_uid
if is_owner and _lvl(farm, "legacy_autoharvest") > 0:
farm = _auto_harvest(farm, owner_uid, 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
)
plots = get_plots(farm["uid"])
serialized_plots = [
serialize_plot(
@@ -208,15 +270,19 @@ def serialize_farm(
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,
)
for plot in plots
]
progress = economy.level_progress(int(farm.get("xp", 0)))
level = progress["level"]
ci_tier = int(farm.get("ci_tier", 1))
next_tier = economy.next_ci_tier(ci_tier)
ci_entry = economy.CI_BY_TIER.get(ci_tier)
is_owner = viewer_uid == owner_uid
streak = _lvl(farm, "streak")
daily_available = is_owner and _daily_available(farm, now)
return {
@@ -246,7 +312,15 @@ def serialize_farm(
"plots": serialized_plots,
"crops": [
economy.crop_payload(
crop, ci_tier, level, growth_level, discount_level, yield_level, prestige
crop,
ci_tier,
level,
growth_level,
discount_level,
yield_level,
prestige,
legacy_mult_level,
legacy_speed_level,
)
for crop in economy.CROPS
],
@@ -259,6 +333,9 @@ def serialize_farm(
"daily_reward": economy.daily_reward(streak + 1 if daily_available else streak),
"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,
}
@@ -286,7 +363,9 @@ def plant(user: dict, slot: int, crop_key: str) -> dict:
raise GameError("Not enough coins to plant that.")
now = _now()
ci_tier = int(farm.get("ci_tier", 1))
grow = economy.grow_seconds_for(crop, ci_tier, _lvl(farm, "perk_growth"))
grow = economy.grow_seconds_for(
crop, ci_tier, _lvl(farm, "perk_growth"), _lvl(farm, "legacy_speed")
)
ready_at = now + timedelta(seconds=grow)
_plots().update(
{
@@ -327,7 +406,12 @@ def harvest(user: dict, slot: int) -> dict:
["uid"],
)
prestige = _lvl(farm, "prestige")
coins_gain = economy.effective_reward_coins(crop, _lvl(farm, "perk_yield"), prestige)
golden = economy.is_golden(plot.get("uid", ""), plot.get("planted_at", ""))
coins_gain = economy.effective_reward_coins(
crop, _lvl(farm, "perk_yield"), prestige, _lvl(farm, "legacy_multiplier")
)
if golden:
coins_gain *= economy.GOLDEN_MULTIPLIER
xp_gain = economy.effective_reward_xp(crop, _lvl(farm, "perk_xp"))
new_xp = int(farm.get("xp", 0)) + xp_gain
_update_farm(
@@ -346,6 +430,7 @@ def harvest(user: dict, slot: int) -> dict:
"crop": crop.key,
"coins": coins_gain,
"xp": xp_gain,
"golden": golden,
}
@@ -397,7 +482,7 @@ def water(visitor: dict, owner: dict, slot: int) -> dict:
crop = economy.crop_for(plot.get("crop_key", ""))
ready_at = _parse(plot.get("ready_at", "")) or now
bonus = economy.water_bonus_seconds(
crop, int(farm.get("ci_tier", 1)), _lvl(farm, "perk_growth")
crop, int(farm.get("ci_tier", 1)), _lvl(farm, "perk_growth"), _lvl(farm, "legacy_speed")
)
new_ready = max(now, ready_at - timedelta(seconds=bonus))
watered.append(visitor["uid"])
@@ -442,9 +527,18 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
crop = economy.crop_for(plot.get("crop_key", ""))
if not crop:
raise GameError("Unknown crop type.")
defense_level = _lvl(farm, "legacy_defense")
ready_at = _parse(plot.get("ready_at", "")) or now
if now < ready_at + timedelta(seconds=economy.STEAL_GRACE_SECONDS):
grace = economy.effective_steal_grace(defense_level)
if now < ready_at + timedelta(seconds=grace):
raise GameError("That harvest is still protected.")
cooldown = steal_cooldown_remaining(thief["uid"], owner["uid"], now)
if cooldown > 0:
minutes = max(1, (cooldown + 59) // 60)
raise GameError(
f"You can only raid {owner.get('username', 'this farmer')} "
f"once an hour. Try again in {minutes} min."
)
_plots().update(
{
"uid": plot["uid"],
@@ -457,13 +551,29 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
["uid"],
)
coins_gain = economy.steal_reward_coins(
crop, _lvl(farm, "perk_yield"), _lvl(farm, "prestige")
crop,
_lvl(farm, "perk_yield"),
_lvl(farm, "prestige"),
_lvl(farm, "legacy_multiplier"),
defense_level,
)
thief_farm = ensure_farm(thief["uid"])
_update_farm(
thief_farm["uid"],
{"coins": int(thief_farm.get("coins", 0)) + coins_gain},
)
_steals().insert(
{
"uid": generate_uid(),
"thief_uid": thief["uid"],
"owner_uid": owner["uid"],
"slot_index": slot,
"crop_key": crop.key,
"coins": coins_gain,
"stolen_at": _iso(now),
"created_at": _iso(now),
}
)
return {
"slot": slot,
"crop": crop.key,
@@ -529,6 +639,76 @@ def _serialize_perks(farm: dict) -> list[dict]:
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 _auto_harvest(farm: dict, owner_uid: str, now: datetime) -> dict:
yield_level = _lvl(farm, "perk_yield")
xp_level = _lvl(farm, "perk_xp")
prestige = _lvl(farm, "prestige")
mult_level = _lvl(farm, "legacy_multiplier")
coins_gain = 0
xp_gain = 0
harvested = 0
for plot in get_plots(farm["uid"]):
if _plot_state(plot, now) != "ready":
continue
crop = economy.crop_for(plot.get("crop_key", ""))
if not crop:
continue
golden = economy.is_golden(plot.get("uid", ""), plot.get("planted_at", ""))
_plots().update(
{
"uid": plot["uid"],
"crop_key": "",
"planted_at": "",
"ready_at": "",
"watered_by": "[]",
"updated_at": _iso(now),
},
["uid"],
)
coins = economy.effective_reward_coins(crop, yield_level, prestige, mult_level)
if golden:
coins *= economy.GOLDEN_MULTIPLIER
coins_gain += coins
xp_gain += economy.effective_reward_xp(crop, xp_level)
harvested += 1
if not harvested:
return farm
new_xp = int(farm.get("xp", 0)) + xp_gain
_update_farm(
farm["uid"],
{
"coins": int(farm.get("coins", 0)) + coins_gain,
"xp": new_xp,
"level": economy.level_for_xp(new_xp),
"total_harvests": int(farm.get("total_harvests", 0)) + harvested,
},
)
advance_quests(owner_uid, "harvest", harvested)
advance_quests(owner_uid, "earn", coins_gain)
return get_farm(owner_uid) or farm
def ensure_quests(farm: dict, day: str) -> None:
if _quests().find_one(farm_uid=farm["uid"], day=day):
return
@@ -670,6 +850,24 @@ def upgrade_perk(user: dict, perk_key: str) -> dict:
return {"perk": perk.key, "level": level + 1, "spent": cost}
def upgrade_legacy(user: dict, key: str) -> dict:
upgrade = economy.legacy_for(key)
if not upgrade:
raise GameError("Unknown legacy upgrade.")
farm = ensure_farm(user["uid"])
column = f"legacy_{upgrade.key}"
level = _lvl(farm, column)
if level >= upgrade.max_level:
raise GameError("That legacy upgrade is maxed out.")
cost = economy.legacy_cost(upgrade, level)
if _lvl(farm, "stars") < cost:
raise GameError("Not enough stars for that legacy upgrade.")
_update_farm(
farm["uid"], {"stars": _lvl(farm, "stars") - cost, column: level + 1}
)
return {"key": upgrade.key, "level": level + 1, "spent": cost}
def prestige(user: dict) -> dict:
farm = ensure_farm(user["uid"])
level = economy.level_for_xp(int(farm.get("xp", 0)))
@@ -678,11 +876,15 @@ def prestige(user: dict) -> dict:
f"Reach level {economy.PRESTIGE_MIN_LEVEL} to refactor (prestige)."
)
new_prestige = _lvl(farm, "prestige") + 1
stars_award = economy.stars_for_refactor(level, _lvl(farm, "prestige"))
base_plots = economy.prestige_base_plots(_lvl(farm, "legacy_plots"))
now = _iso(_now())
kept_slots = set()
for plot in get_plots(farm["uid"]):
if int(plot.get("slot_index", 0)) >= economy.STARTING_PLOTS:
if int(plot.get("slot_index", 0)) >= base_plots:
_plots().delete(uid=plot["uid"])
else:
kept_slots.add(int(plot.get("slot_index", 0)))
_plots().update(
{
"uid": plot["uid"],
@@ -694,18 +896,22 @@ def prestige(user: dict) -> dict:
},
["uid"],
)
for slot_index in range(base_plots):
if slot_index not in kept_slots:
_create_plot(farm["uid"], user["uid"], slot_index, now)
reset = {
"coins": economy.STARTING_COINS,
"xp": 0,
"level": 1,
"ci_tier": 1,
"plot_count": economy.STARTING_PLOTS,
"plot_count": base_plots,
"prestige": new_prestige,
"stars": _lvl(farm, "stars") + stars_award,
}
for perk in economy.PERKS:
reset[PERK_COLUMN[perk.key]] = 0
_update_farm(farm["uid"], reset)
return {"prestige": new_prestige}
return {"prestige": new_prestige, "stars_awarded": stars_award}
def fertilize(user: dict, slot: int) -> dict:
@@ -716,12 +922,23 @@ def fertilize(user: dict, slot: int) -> dict:
now = _now()
if _plot_state(plot, now) != "growing":
raise GameError("That build is not running.")
crop = economy.crop_for(plot.get("crop_key", ""))
if not crop:
raise GameError("Unknown crop type.")
ready_at = _parse(plot.get("ready_at", "")) or now
remaining = max(1, int((ready_at - now).total_seconds()))
cost = economy.fertilize_cost(remaining)
reduce_by = int(remaining * economy.FERTILIZE_FRACTION)
if reduce_by < 1:
raise GameError("That build is almost ready already.")
full_grow = economy.grow_seconds_for(
crop, int(farm.get("ci_tier", 1)), _lvl(farm, "perk_growth"), _lvl(farm, "legacy_speed")
)
eff_reward = economy.effective_reward_coins(
crop, _lvl(farm, "perk_yield"), _lvl(farm, "prestige"), _lvl(farm, "legacy_multiplier")
)
cost = economy.fertilize_click_cost(eff_reward, reduce_by, full_grow)
if int(farm.get("coins", 0)) < cost:
raise GameError("Not enough coins to fertilize.")
reduce_by = int(remaining * economy.FERTILIZE_FRACTION)
new_ready = max(now, ready_at - timedelta(seconds=reduce_by))
_plots().update(
{"uid": plot["uid"], "ready_at": _iso(new_ready), "updated_at": _iso(now)},