304 lines
13 KiB
Python
304 lines
13 KiB
Python
from collections import Counter
|
|
|
|
from assets import icon
|
|
from engine import collection, runes
|
|
from engine.battle import Battle
|
|
from engine.relics import RELIC_BY_KEY, grant_rune_rewards, roll_relics, rune_reward_count
|
|
from engine.roster import make_enemy_team, pick_team, player_team_level, player_team_stage
|
|
from engine.unit import TEAM_ENEMY, TEAM_PLAYER
|
|
|
|
|
|
def _pack_reward_items(pack_types):
|
|
"""Reward entries (label + pack sprite path) from a list of pack type names,
|
|
e.g. ['wildflower', 'wildflower'] -> [{'label': '2 Wildflower packs', ...}]."""
|
|
counts = Counter(pack_types)
|
|
items = []
|
|
for pt in ("general", "lesser", "tower", "radiant", "nocturnal", "ice", "garden", "wildflower"):
|
|
n = counts.get(pt, 0)
|
|
if n:
|
|
name = "Wildflower" if pt == "wildflower" else pt.capitalize()
|
|
items.append({
|
|
"label": f"{n} {name} pack{'s' if n > 1 else ''}",
|
|
"kind": "pack",
|
|
"img": icon("seed_packs/closed", f"{pt}_seedpack"),
|
|
})
|
|
return items
|
|
|
|
|
|
def _rune_reward_item(rune):
|
|
"""Reward entry for a rune drop (label + rune sprite path)."""
|
|
return {
|
|
"label": rune["name"],
|
|
"kind": "rune",
|
|
"img": icon("runes", runes.RUNE_ICON_FILES.get(rune["color"], "rune_brown")),
|
|
}
|
|
|
|
|
|
def _reward_labels(items):
|
|
"""Flatten reward entries (dicts or plain strings) into label text."""
|
|
return [it.get("label", "") if isinstance(it, dict) else it for it in items]
|
|
|
|
|
|
def _totals_txt(total_water, total_packs, run_relics):
|
|
"""Tower run totals line, including pending rune rewards from rune relics."""
|
|
parts = [f"{total_water} water", f"{total_packs} tower pack(s)"]
|
|
n = rune_reward_count(run_relics)
|
|
if n:
|
|
parts.append(f"{n} rune{'s' if n != 1 else ''}")
|
|
return ", ".join(parts)
|
|
|
|
|
|
def _battle_win_reward(floor):
|
|
"""Grant and describe a floor-battle win: a lesser pack, or on every 5th win
|
|
(5, 10, 15, ...) a better general/type-boosted pack instead. Advances the
|
|
floor. Runes can drop on any win regardless."""
|
|
collection.advance_floor()
|
|
wins = collection.record_win()
|
|
if wins % 5 == 0:
|
|
reward = _pack_reward_items(collection.grant_fifth_win_bonus())
|
|
else:
|
|
reward = _pack_reward_items(collection.add_pack("lesser"))
|
|
rune = collection.roll_battle_rune()
|
|
if rune:
|
|
reward.append(_rune_reward_item(rune))
|
|
return reward
|
|
|
|
|
|
def run_tower():
|
|
import ui.scaling as scaling
|
|
from ui.picker import TeamSelect
|
|
roster_pick = TeamSelect(tower_mode=True, pick_goal=8, min_goal=1, show_slots=False)
|
|
roster_res = roster_pick.run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
if roster_res is None or roster_res == "back" or not roster_res[0]:
|
|
return "back" # back to the battle/tower selection screen
|
|
roster = list(roster_res[0])
|
|
collection.mark_owned(roster)
|
|
from engine.roster import (
|
|
build_tower_enemy_team, roll_tower_enemy_team,
|
|
tower_enemy_level, tower_reward,
|
|
)
|
|
level = player_team_level(roster)
|
|
# Gate the tower to the highest stage the account qualifies for; clearing
|
|
# that stage finishes the run (the tower can't be pushed past the gate).
|
|
from engine.collection import tower_max_stage
|
|
max_stage = tower_max_stage()
|
|
|
|
def build_team(enemies=()):
|
|
el = tower_enemy_level(level, stage)
|
|
ts = TeamSelect(tower_mode=True, pick_goal=4, restrict_to=roster,
|
|
initial_enemies=list(enemies),
|
|
enemy_levels=[el] * len(enemies))
|
|
res = ts.run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
if res is None or res == "back":
|
|
return None
|
|
return list(res[0])
|
|
|
|
active = build_team()
|
|
if active == "quit":
|
|
return "quit"
|
|
if active is None:
|
|
return "back" # back to the battle/tower selection screen
|
|
run_relics = []
|
|
total_water = 0
|
|
total_packs = 0
|
|
stage = 1
|
|
rolled_stage = -1
|
|
enemy_recipe = None
|
|
while stage <= max_stage:
|
|
water, packs = tower_reward(stage)
|
|
reward_display = []
|
|
if water:
|
|
reward_display.append({"label": f"{water} water", "kind": "text"})
|
|
if packs:
|
|
reward_display.append({"label": f"{packs} tower pack{'s' if packs > 1 else ''}",
|
|
"kind": "pack",
|
|
"img": icon("seed_packs/closed", "tower_seedpack")})
|
|
if stage != rolled_stage:
|
|
# Roll the enemy team once per stage; retrying a lost stage (or
|
|
# swapping teams) rebuilds the exact same enemies instead of rerolling.
|
|
enemy_recipe = roll_tower_enemy_team(stage, tower_enemy_level(level, stage))
|
|
rolled_stage = stage
|
|
enemy_names, injections, enemy_runes = enemy_recipe
|
|
enemy_units = build_tower_enemy_team(enemy_names, injections, enemy_runes,
|
|
tower_enemy_level(level, stage), stage)
|
|
players = pick_team(active, TEAM_PLAYER) # fresh units: cooldowns/status reset each battle
|
|
battle = Battle(players, enemy_units, relics=run_relics)
|
|
from ui.main import Game
|
|
Game(battle, on_win=lambda d=reward_display: d, on_lose=lambda: [], tower=True,
|
|
relics=[RELIC_BY_KEY[k] for k in run_relics if k in RELIC_BY_KEY],
|
|
stage=stage).run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
totals_txt = _totals_txt(total_water, total_packs, run_relics)
|
|
from ui.tower import ConfirmScreen, TowerScreen
|
|
if battle.winner != TEAM_PLAYER:
|
|
while True:
|
|
choice = TowerScreen(stage, max_stage, "", totals_txt, mode="lost",
|
|
enemies=enemy_names).run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
if choice == "retry":
|
|
break # re-fight this stage
|
|
if choice == "change":
|
|
active = build_team(enemies=enemy_names)
|
|
if active == "quit":
|
|
return "quit"
|
|
if active is None:
|
|
# back in the team picker: ask before leaving the tower
|
|
if ConfirmScreen(
|
|
"Leave the tower?",
|
|
"You'll keep the rewards you've earned so far.").run():
|
|
TowerScreen(stage, max_stage, "", totals_txt,
|
|
mode="summary").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
grant_rune_rewards(run_relics)
|
|
return "back"
|
|
continue # stay: show the lost screen again
|
|
break # team changed, re-fight this stage
|
|
TowerScreen(stage, max_stage, "", totals_txt, mode="summary").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
grant_rune_rewards(run_relics)
|
|
return "back" # gave up -> back to the selection screen
|
|
continue
|
|
if water:
|
|
collection.add_water(water)
|
|
if packs:
|
|
collection.add_tower_packs(packs)
|
|
collection.grant_xp(active, 11 * stage, count_battle=False)
|
|
total_water += water
|
|
total_packs += packs
|
|
collection.set_tower_best(stage)
|
|
totals_txt = _totals_txt(total_water, total_packs, run_relics)
|
|
if stage >= max_stage:
|
|
TowerScreen(max_stage, max_stage, "", totals_txt, mode="cleared").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
break # cleared the highest available tower stage
|
|
from ui.relic_choice import RelicChoiceScreen
|
|
offered = roll_relics(3, exclude=run_relics, stage=stage)
|
|
if offered:
|
|
picked = RelicChoiceScreen(offered, stage, owned=run_relics).run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
if picked is not None:
|
|
run_relics.append(picked.key)
|
|
reward_txt = ", ".join(_reward_labels(reward_display)) if reward_display else "—"
|
|
while True:
|
|
choice = TowerScreen(stage, max_stage, reward_txt, totals_txt,
|
|
mode="battle").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
if choice == "exit":
|
|
TowerScreen(stage, max_stage, "", totals_txt, mode="summary").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
grant_rune_rewards(run_relics)
|
|
return "back"
|
|
if choice == "adjust":
|
|
active = build_team()
|
|
if active == "quit":
|
|
return "quit"
|
|
if active is None:
|
|
# back in the team picker: ask before leaving the tower
|
|
if ConfirmScreen(
|
|
"Leave the tower?",
|
|
"You'll keep the rewards you've earned so far.").run():
|
|
TowerScreen(stage, max_stage, "", totals_txt,
|
|
mode="summary").run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return "quit"
|
|
grant_rune_rewards(run_relics)
|
|
return "back"
|
|
continue # stay: show the stage screen again
|
|
continue # re-fight this stage with the new team
|
|
break # "next": advance to the next stage
|
|
stage += 1
|
|
grant_rune_rewards(run_relics)
|
|
return "back" # tower loop ended (cleared all available stages) -> selection screen
|
|
|
|
|
|
def main():
|
|
import pygame
|
|
import ui.scaling as scaling
|
|
try:
|
|
if not collection.save_exists():
|
|
from ui.starter import StarterScreen
|
|
if not StarterScreen().run():
|
|
return # closed the starter window without picking a pack
|
|
next_page = "menu"
|
|
while True:
|
|
from ui.menu import Menu
|
|
choice = Menu(page=next_page).run()
|
|
if scaling.QUIT_REQUESTED or choice is None:
|
|
return
|
|
next_page = "menu"
|
|
if choice == "tower":
|
|
result = run_tower()
|
|
if scaling.QUIT_REQUESTED or result == "quit":
|
|
return
|
|
if result == "back":
|
|
next_page = "battle_setup"
|
|
continue
|
|
floor = int(choice)
|
|
player_type = collection.unit_type("Pinwheel") if "Pinwheel" in collection.owned() \
|
|
else collection.unit_type(next(iter(collection.owned() or ["Pinwheel"])))
|
|
from engine.roster import (build_floor_enemies, floor_enemy_level,
|
|
floor_enemy_names, roll_enemy_types)
|
|
|
|
while True:
|
|
floor = collection.floor_progress()
|
|
enemy_names = floor_enemy_names(floor, player_type)
|
|
enemy_types = roll_enemy_types(enemy_names, player_type, floor)
|
|
enemy_levels = [floor_enemy_level(floor)] * len(enemy_names)
|
|
from ui.picker import TeamSelect
|
|
ts = TeamSelect(initial_enemies=enemy_names, difficulty=floor,
|
|
enemy_types=enemy_types, no_roll=True,
|
|
enemy_levels=enemy_levels)
|
|
result = ts.run()
|
|
if scaling.QUIT_REQUESTED or result is None:
|
|
return
|
|
if result == "back":
|
|
next_page = "battle_setup"
|
|
break # back to the battle/tower selection screen
|
|
|
|
allies, enemies = result
|
|
collection.mark_owned(allies)
|
|
collection.mark_found(enemies)
|
|
players = pick_team(allies, TEAM_PLAYER)
|
|
enemy_units = build_floor_enemies(enemy_names, enemy_types, floor)
|
|
battle = Battle(players, enemy_units)
|
|
|
|
from ui.main import Game
|
|
outcome = Game(battle,
|
|
on_win=lambda f=floor: _battle_win_reward(f),
|
|
on_lose=lambda: [], stage=floor).run()
|
|
if scaling.QUIT_REQUESTED:
|
|
return
|
|
|
|
won = battle.winner == TEAM_PLAYER
|
|
# Losses give nothing; wins scale XP gently with the floor.
|
|
xp = 40 + 15 * floor if won else 0
|
|
collection.grant_xp(allies, xp, count_battle=won,
|
|
fertilizer=floor if won else 0)
|
|
|
|
if outcome == "back":
|
|
next_page = "battle_setup"
|
|
break # back to the battle/tower selection screen
|
|
if outcome == "rematch":
|
|
# A win already advanced the floor (in _battle_win_reward), so
|
|
# the next loop iteration fights the new unbeaten floor.
|
|
# A loss retries the same floor.
|
|
continue
|
|
return
|
|
finally:
|
|
pygame.quit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |