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)