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)
|
||||
|
||||
Reference in New Issue
Block a user