forked from retoor/devplacepy
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:
@@ -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)
|
||||
|
||||
@@ -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)},
|
||||
|
||||
Reference in New Issue
Block a user