|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
|
|
from devplacepy.utils import generate_uid
|
|
|
|
from .. import economy
|
|
from .common import (
|
|
GameError,
|
|
PERK_COLUMN,
|
|
_iso,
|
|
_lvl,
|
|
_now,
|
|
_parse,
|
|
_parse_date,
|
|
_plots,
|
|
_quests,
|
|
_steals,
|
|
_today,
|
|
_update_farm,
|
|
steal_cooldown_remaining,
|
|
)
|
|
from .farm import _create_plot, _plot_at, ensure_farm, get_farm, get_plots
|
|
from .quests import advance_quests, ensure_quests
|
|
from .serialize import _daily_available, _plot_state, _watered_by
|
|
|
|
|
|
def plant(user: dict, slot: int, crop_key: str) -> dict:
|
|
farm = ensure_farm(user["uid"])
|
|
crop = economy.crop_for(crop_key)
|
|
if not crop:
|
|
raise GameError("Unknown crop type.")
|
|
progress = economy.level_progress(int(farm.get("xp", 0)))
|
|
if crop.min_level > progress["level"]:
|
|
raise GameError(f"{crop.name} unlocks at level {crop.min_level}.")
|
|
plot = _plot_at(farm["uid"], slot)
|
|
if not plot:
|
|
raise GameError("That plot does not exist.")
|
|
if plot.get("crop_key"):
|
|
raise GameError("That plot is already in use.")
|
|
coins = int(farm.get("coins", 0))
|
|
cost = economy.effective_plant_cost(crop, _lvl(farm, "perk_discount"))
|
|
if coins < cost:
|
|
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"), _lvl(farm, "legacy_speed")
|
|
)
|
|
ready_at = now + timedelta(seconds=grow)
|
|
_plots().update(
|
|
{
|
|
"uid": plot["uid"],
|
|
"crop_key": crop.key,
|
|
"planted_at": _iso(now),
|
|
"ready_at": _iso(ready_at),
|
|
"watered_by": "[]",
|
|
"updated_at": _iso(now),
|
|
},
|
|
["uid"],
|
|
)
|
|
_update_farm(farm["uid"], {"coins": coins - cost})
|
|
advance_quests(user["uid"], "plant", 1)
|
|
return {"slot": slot, "crop": crop.key, "spent": cost}
|
|
|
|
|
|
def harvest(user: dict, slot: int) -> dict:
|
|
farm = ensure_farm(user["uid"])
|
|
plot = _plot_at(farm["uid"], slot)
|
|
if not plot or not plot.get("crop_key"):
|
|
raise GameError("That plot is empty.")
|
|
now = _now()
|
|
if _plot_state(plot, now) != "ready":
|
|
raise GameError("That build is still running.")
|
|
crop = economy.crop_for(plot.get("crop_key", ""))
|
|
if not crop:
|
|
raise GameError("Unknown crop type.")
|
|
_plots().update(
|
|
{
|
|
"uid": plot["uid"],
|
|
"crop_key": "",
|
|
"planted_at": "",
|
|
"ready_at": "",
|
|
"watered_by": "[]",
|
|
"updated_at": _iso(now),
|
|
},
|
|
["uid"],
|
|
)
|
|
prestige = _lvl(farm, "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(
|
|
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)) + 1,
|
|
},
|
|
)
|
|
advance_quests(user["uid"], "harvest", 1)
|
|
advance_quests(user["uid"], "earn", coins_gain)
|
|
return {
|
|
"slot": slot,
|
|
"crop": crop.key,
|
|
"coins": coins_gain,
|
|
"xp": xp_gain,
|
|
"golden": golden,
|
|
}
|
|
|
|
|
|
def water(visitor: dict, owner: dict, slot: int) -> dict:
|
|
if visitor["uid"] == owner["uid"]:
|
|
raise GameError("You cannot water your own build.")
|
|
farm = ensure_farm(owner["uid"])
|
|
plot = _plot_at(farm["uid"], slot)
|
|
if not plot or not plot.get("crop_key"):
|
|
raise GameError("That plot is empty.")
|
|
now = _now()
|
|
if _plot_state(plot, now) != "growing":
|
|
raise GameError("That build is not running.")
|
|
watered = _watered_by(plot)
|
|
if visitor["uid"] in watered:
|
|
raise GameError("You already watered this build.")
|
|
if len(watered) >= economy.MAX_WATERS_PER_PLOT:
|
|
raise GameError("This build has been watered enough.")
|
|
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"), _lvl(farm, "legacy_speed")
|
|
)
|
|
new_ready = max(now, ready_at - timedelta(seconds=bonus))
|
|
watered.append(visitor["uid"])
|
|
_plots().update(
|
|
{
|
|
"uid": plot["uid"],
|
|
"ready_at": _iso(new_ready),
|
|
"watered_by": json.dumps(watered),
|
|
"updated_at": _iso(now),
|
|
},
|
|
["uid"],
|
|
)
|
|
visitor_farm = ensure_farm(visitor["uid"])
|
|
new_xp = int(visitor_farm.get("xp", 0)) + economy.WATER_REWARD_XP
|
|
_update_farm(
|
|
visitor_farm["uid"],
|
|
{
|
|
"coins": int(visitor_farm.get("coins", 0)) + economy.WATER_REWARD_COINS,
|
|
"xp": new_xp,
|
|
"level": economy.level_for_xp(new_xp),
|
|
},
|
|
)
|
|
advance_quests(visitor["uid"], "water", 1)
|
|
return {
|
|
"slot": slot,
|
|
"bonus_seconds": bonus,
|
|
"reward_coins": economy.WATER_REWARD_COINS,
|
|
"reward_xp": economy.WATER_REWARD_XP,
|
|
}
|
|
|
|
|
|
def steal(thief: dict, owner: dict, slot: int) -> dict:
|
|
if thief["uid"] == owner["uid"]:
|
|
raise GameError("You cannot steal from your own farm.")
|
|
farm = ensure_farm(owner["uid"])
|
|
plot = _plot_at(farm["uid"], slot)
|
|
if not plot or not plot.get("crop_key"):
|
|
raise GameError("That plot is empty.")
|
|
now = _now()
|
|
if _plot_state(plot, now) != "ready":
|
|
raise GameError("That build is not ready.")
|
|
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
|
|
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"],
|
|
"crop_key": "",
|
|
"planted_at": "",
|
|
"ready_at": "",
|
|
"watered_by": "[]",
|
|
"updated_at": _iso(now),
|
|
},
|
|
["uid"],
|
|
)
|
|
coins_gain = economy.steal_reward_coins(
|
|
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,
|
|
"coins": coins_gain,
|
|
"owner_uid": owner["uid"],
|
|
}
|
|
|
|
|
|
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 claim_quest(user: dict, kind: str) -> dict:
|
|
farm = ensure_farm(user["uid"])
|
|
day = _today()
|
|
ensure_quests(farm, day)
|
|
row = _quests().find_one(farm_uid=farm["uid"], day=day, kind=kind)
|
|
if not row:
|
|
raise GameError("No such quest today.")
|
|
if row.get("claimed"):
|
|
raise GameError("Quest already claimed.")
|
|
goal = int(row.get("goal") or 0)
|
|
if int(row.get("progress") or 0) < goal:
|
|
raise GameError("Quest not complete yet.")
|
|
reward_coins = int(row.get("reward_coins") or 0)
|
|
reward_xp = int(row.get("reward_xp") or 0)
|
|
_quests().update(
|
|
{"uid": row["uid"], "claimed": 1, "updated_at": _iso(_now())}, ["uid"]
|
|
)
|
|
new_xp = int(farm.get("xp", 0)) + reward_xp
|
|
_update_farm(
|
|
farm["uid"],
|
|
{
|
|
"coins": int(farm.get("coins", 0)) + reward_coins,
|
|
"xp": new_xp,
|
|
"level": economy.level_for_xp(new_xp),
|
|
},
|
|
)
|
|
return {"kind": kind, "reward_coins": reward_coins, "reward_xp": reward_xp}
|
|
|
|
|
|
def claim_daily(user: dict) -> dict:
|
|
farm = ensure_farm(user["uid"])
|
|
now = _now()
|
|
if not _daily_available(farm, now):
|
|
raise GameError("Daily bonus already claimed today.")
|
|
last = _parse_date(farm.get("last_daily_at") or "")
|
|
streak = _lvl(farm, "streak") + 1 if last == now.date() - timedelta(days=1) else 1
|
|
reward = economy.daily_reward(streak)
|
|
_update_farm(
|
|
farm["uid"],
|
|
{
|
|
"coins": int(farm.get("coins", 0)) + reward,
|
|
"streak": streak,
|
|
"last_daily_at": _iso(now),
|
|
},
|
|
)
|
|
return {"reward": reward, "streak": streak}
|
|
|
|
|
|
def upgrade_perk(user: dict, perk_key: str) -> dict:
|
|
perk = economy.perk_for(perk_key)
|
|
if not perk:
|
|
raise GameError("Unknown perk.")
|
|
farm = ensure_farm(user["uid"])
|
|
column = PERK_COLUMN[perk.key]
|
|
level = _lvl(farm, column)
|
|
if level >= perk.max_level:
|
|
raise GameError("That perk is maxed out.")
|
|
cost = economy.perk_cost(perk, level)
|
|
if int(farm.get("coins", 0)) < cost:
|
|
raise GameError("Not enough coins for that upgrade.")
|
|
_update_farm(farm["uid"], {"coins": int(farm.get("coins", 0)) - cost, column: level + 1})
|
|
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)))
|
|
if level < economy.PRESTIGE_MIN_LEVEL:
|
|
raise GameError(
|
|
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)) >= base_plots:
|
|
_plots().delete(uid=plot["uid"])
|
|
else:
|
|
kept_slots.add(int(plot.get("slot_index", 0)))
|
|
_plots().update(
|
|
{
|
|
"uid": plot["uid"],
|
|
"crop_key": "",
|
|
"planted_at": "",
|
|
"ready_at": "",
|
|
"watered_by": "[]",
|
|
"updated_at": now,
|
|
},
|
|
["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": 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, "stars_awarded": stars_award}
|
|
|
|
|
|
def fertilize(user: dict, slot: int) -> dict:
|
|
farm = ensure_farm(user["uid"])
|
|
plot = _plot_at(farm["uid"], slot)
|
|
if not plot or not plot.get("crop_key"):
|
|
raise GameError("That plot is empty.")
|
|
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()))
|
|
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.")
|
|
new_ready = max(now, ready_at - timedelta(seconds=reduce_by))
|
|
_plots().update(
|
|
{"uid": plot["uid"], "ready_at": _iso(new_ready), "updated_at": _iso(now)},
|
|
["uid"],
|
|
)
|
|
_update_farm(farm["uid"], {"coins": int(farm.get("coins", 0)) - cost})
|
|
return {"slot": slot, "spent": cost, "reduced_seconds": reduce_by}
|