game_state.py
"""Game state: manages party, inventory, battle progression."""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from data_types import *
from character import Character, create_player_character
from monster_unit import MonsterUnit
from monsters_db import get_encounter, MONSTER_DB
from items_db import ITEM_DB
from skills_db import SKILL_DB
from battle import Battle
from companions_db import summon_companion
# Starting inventory
DEFAULT_INVENTORY = {
1: 5, # Potion x5
6: 3, # Ether x3
31: 2, # Phoenix Feather x2
41: 2, # Antidote x2
86: 2, # Fire Bomb x2
}
# Battle milestones for companion rewards
COMPANION_MILESTONES = {
1: 2, # After battle 1: lv2 companion
5: 5, # After battle 5: lv5 companion
10: 7, # After battle 10: lv7-8 companion
20: 15, # After battle 20: lv15 companion
50: 40, # After battle 50: lv40 companion
75: 47, # After battle 75: lv45-50 companion
100:60, # After battle 100: lv60 companion
}
class GameState:
def __init__(self):
self.player: Optional[Character] = None
self.party: List[Character] = []
self.inventory: Dict[int, int] = dict(DEFAULT_INVENTORY)
self.battle_number: int = 0
self.game_over: bool = False
self.deceased_companions: List[Character] = []
# Records for game over screen
self.all_party_members: List[Character] = []
# ── Inventory ─────────────────────────────────────────────────────────
def add_item(self, item_id: int, count: int = 1):
self.inventory[item_id] = self.inventory.get(item_id, 0) + count
def display_inventory(self):
if not self.inventory:
print(" (Empty)")
return
print(" INVENTORY:")
for iid, count in sorted(self.inventory.items()):
if iid in ITEM_DB and count > 0:
item = ITEM_DB[iid]
print(f" {item.name} x{count} - {item.description}")
def use_item_outside_battle(self, item_id: int, target: Character) -> bool:
if item_id not in self.inventory or self.inventory[item_id] <= 0:
print(" You don't have that item!")
return False
item = ITEM_DB[item_id]
self.inventory[item_id] -= 1
if self.inventory[item_id] <= 0:
del self.inventory[item_id]
if item.item_type == ItemType.RECOVERY:
if item.effect_value == -100:
target.current_hp = target.max_hp
target.current_mp = target.max_mp
print(f" {target.name} fully restored!")
elif item.effect_value < 0:
pct = abs(item.effect_value)
amt = int(target.max_hp * pct / 100)
actual = target.heal(amt)
print(f" {target.name} recovers {actual} HP!")
elif item.id in (6,7,8,9,12,20):
mp = min(item.effect_value, target.max_mp - target.current_mp)
target.current_mp += mp
print(f" {target.name} recovers {mp} MP!")
else:
actual = target.heal(item.effect_value)
print(f" {target.name} recovers {actual} HP!")
elif item.item_type == ItemType.REVIVAL:
if target.is_ko and not target.is_dead:
target.revive(item.effect_value)
print(f" {target.name} revived with {item.effect_value}% HP!")
else:
print(f" {target.name} isn't KO'd.")
self.inventory[item_id] = self.inventory.get(item_id, 0) + 1 # refund
return False
elif item.item_type == ItemType.STATUS_CURE:
if item.status_cure:
for stype in item.status_cure:
target.remove_status(stype)
print(f" {target.name} cleansed!")
return True
# ── Party management ──────────────────────────────────────────────────
def display_party(self):
print("\n PARTY:")
for i, ch in enumerate(self.party):
rarity_str = f" {'★'*ch.rarity}" if ch.rarity > 0 else " [PLAYER]"
status = ch.short_status()
print(f" [{i+1}] {ch.name} Lv{ch.level} {ch.job.value}{rarity_str} - {status}")
print(f" HP:{ch.current_hp}/{ch.max_hp} MP:{ch.current_mp}/{ch.max_mp}")
print(f" PATK:{ch.patk} MATK:{ch.matk} PDEF:{ch.pdef} MDEF:{ch.mdef} SPD:{ch.spd}")
def display_character_detail(self, ch: Character):
print(f"\n ── {ch.name} ──────────────────────────────")
rarity_str = f"{'★'*ch.rarity}" if ch.rarity > 0 else "PLAYER"
print(f" Job: {ch.job.value} Rarity: {rarity_str}")
print(f" Weapon: {ch.weapon.value} Element: {ch.element.value}")
print(f" Level: {ch.level} EXP: {ch.exp}/{ch.exp_to_next}")
print(f" HP: {ch.current_hp}/{ch.max_hp} MP: {ch.current_mp}/{ch.max_mp}")
print(f" PATK:{ch.patk} MATK:{ch.matk} PDEF:{ch.pdef} MDEF:{ch.mdef}")
print(f" SPD:{ch.spd} EVA:{ch.base_stats.eva:.0%} ACC:{ch.base_stats.acc:.0%} CRIT:{ch.base_stats.crit:.0%}")
print(f" Skills:")
for sid in ch.skill_pool:
if sid in SKILL_DB:
sk = SKILL_DB[sid]
unlocked = "✓" if sid in ch.unlocked_skills else "✗"
unlock_lv = ""
if sk.tier == SkillTier.INTERMEDIATE:
unlock_lv = " (Lv5/10)"
elif sk.tier == SkillTier.ULTIMATE:
unlock_lv = " (Lv20)"
print(f" [{unlocked}] {sk.name} [{sk.tier.value}] MP:{sk.mp_cost}{unlock_lv}")
def offer_companion(self, companion: Character):
"""Offer a new companion to the player."""
print("\n" + "★"*65)
rarity_str = "★" * companion.rarity
print(f" A new companion has appeared! [{rarity_str}]")
print(f" {companion.name} Lv{companion.level} | {companion.job.value}")
print(f" HP:{companion.max_hp} PATK:{companion.base_stats.patk} MATK:{companion.base_stats.matk}")
print(f" SPD:{companion.base_stats.spd} Weapon:{companion.weapon.value} Element:{companion.element.value}")
alive_party = [ch for ch in self.party if not ch.is_dead]
if len(alive_party) < 4:
print(f"\n Adding {companion.name} to the party!")
self.party.append(companion)
self.all_party_members.append(companion)
input(" [Press Enter]")
else:
print(f"\n Your party is full (4/4). Dismiss a member to make room?")
self.display_party()
print(f" [1-{len(self.party)}] Dismiss member [0] Decline companion")
while True:
try:
choice = int(input(" > "))
if choice == 0:
print(f" Declined {companion.name}.")
input(" [Press Enter]")
return
idx = choice - 1
if 0 <= idx < len(self.party):
dismiss_target = self.party[idx]
if dismiss_target == self.player:
print(" Cannot dismiss player character!")
continue
self.party.remove(dismiss_target)
print(f" {dismiss_target.name} has left the party.")
self.party.append(companion)
self.all_party_members.append(companion)
print(f" {companion.name} joined the party!")
input(" [Press Enter]")
return
except ValueError:
pass
print(" Invalid choice.")
# ── Battle ────────────────────────────────────────────────────────────
def run_battle(self) -> bool:
"""Run a battle. Returns True if player survived."""
self.battle_number += 1
# Get encounter
templates = get_encounter(self.battle_number)
# Name duplicates
name_count: Dict[str, int] = {}
monster_units = []
for t in templates:
name_count[t.name] = name_count.get(t.name, 0) + 1
used: Dict[str, int] = {}
for t in templates:
used[t.name] = used.get(t.name, 0) + 1
label = t.name if name_count[t.name] == 1 else f"{t.name} {chr(64 + used[t.name])}"
mu = MonsterUnit(template=t, instance_name=label)
monster_units.append(mu)
alive_party = [ch for ch in self.party if not ch.is_dead]
battle = Battle(alive_party, monster_units)
# Inject item use capability into battle
battle._item_use_callback = self._battle_item_callback
battle._original_use_item_menu = battle._use_item_menu
battle._use_item_menu = lambda ch: self._battle_item_ui(ch, battle)
victory = battle.run()
if victory:
exp = battle.calculate_exp_reward()
print(f"\n ✓ VICTORY! Earned {exp} EXP.")
# Award loot
self._award_loot()
alive_after = [ch for ch in alive_party if not ch.is_dead]
for ch in alive_after:
msgs = ch.gain_exp(exp)
for m in msgs:
print(m)
input(" [Press Enter]")
# Handle companion death
for ch in alive_party:
if ch.is_dead and ch != self.player:
print(f"\n 💀 {ch.name} has permanently died and left the party.")
self.party.remove(ch)
self.deceased_companions.append(ch)
# Check milestone companions
self._check_companion_milestone()
else:
# Check if player character survived
player_dead = self.player.is_dead or self.player.is_ko
if player_dead:
self.game_over = True
print(f"\n ☠ {self.player.name} has fallen... GAME OVER")
return False
else:
print(f"\n DEFEAT! Some party members have fallen.")
for ch in alive_party:
if ch.is_dead and ch != self.player:
print(f" 💀 {ch.name} has permanently died.")
self.party.remove(ch)
self.deceased_companions.append(ch)
input(" [Press Enter]")
# Post-battle: reset KO state for surviving members
for ch in self.party:
if not ch.is_dead:
if ch.is_ko and ch.current_hp > 0:
ch.is_ko = False
ch.ko_turns = 0
# Partial HP restore (30% if KO'd but saved)
if ch.current_hp <= 0:
ch.current_hp = max(1, ch.max_hp // 5)
ch.is_ko = False
ch.ko_turns = 0
return True
def _battle_item_callback(self, ch, item_id: int, battle: Battle) -> bool:
return battle.use_item_in_battle(ch, item_id, self.inventory)
def _battle_item_ui(self, ch: Character, battle: Battle) -> bool:
if not self.inventory:
print(" Inventory is empty!")
return False
print(" INVENTORY (0=back):")
items = [(iid, cnt) for iid, cnt in sorted(self.inventory.items()) if cnt > 0]
for i, (iid, cnt) in enumerate(items):
if iid in ITEM_DB:
item = ITEM_DB[iid]
print(f" [{i+1}] {item.name} x{cnt} - {item.description}")
while True:
raw = input(" > ").strip()
if raw == "0": return False
try:
idx = int(raw) - 1
if 0 <= idx < len(items):
item_id = items[idx][0]
return battle.use_item_in_battle(ch, item_id, self.inventory)
except ValueError:
pass
print(" Invalid choice.")
def _award_loot(self):
"""Random item drops after victory."""
# Simple loot: random item from early pool
loot_pool = [1, 2, 6, 7, 31, 41, 42, 66, 67, 86, 87, 88]
advanced_pool = [3, 5, 8, 32, 56, 68]
rare_pool = [4, 10, 33, 57, 84, 85]
# Base drop
if random.random() < 0.7:
item_id = random.choice(loot_pool)
self.add_item(item_id)
print(f" Item found: {ITEM_DB[item_id].name}!")
# Extra drop for later battles
if self.battle_number > 20 and random.random() < 0.4:
item_id = random.choice(advanced_pool)
self.add_item(item_id)
print(f" Item found: {ITEM_DB[item_id].name}!")
if self.battle_number > 50 and random.random() < 0.2:
item_id = random.choice(rare_pool)
self.add_item(item_id)
print(f" Rare item found: {ITEM_DB[item_id].name}!")
def _check_companion_milestone(self):
if self.battle_number in COMPANION_MILESTONES:
target_lv = COMPANION_MILESTONES[self.battle_number]
if isinstance(target_lv, tuple):
target_lv = random.randint(target_lv[0], target_lv[1])
companion = summon_companion(target_lv)
self.offer_companion(companion)
# ── Between battles menu ──────────────────────────────────────────────
def between_battles_menu(self):
while True:
print("\n" + "─"*65)
print(f" ── Camp ── (Battle {self.battle_number} completed)")
print(" [1] View Party [2] Character Details [3] Use Item")
print(" [4] View Inventory [5] Next Battle [6] Quit")
choice = input(" > ").strip()
if choice == "1":
self.display_party()
elif choice == "2":
self.display_party()
try:
idx = int(input(" Choose character (0=back): ")) - 1
if 0 <= idx < len(self.party):
self.display_character_detail(self.party[idx])
except ValueError:
pass
elif choice == "3":
self._outside_battle_item_menu()
elif choice == "4":
self.display_inventory()
elif choice == "5":
return True
elif choice == "6":
return False
else:
print(" Invalid choice.")
def _outside_battle_item_menu(self):
self.display_inventory()
items = [(iid, cnt) for iid, cnt in sorted(self.inventory.items()) if cnt > 0]
if not items:
return
print(" Use item on (0=back):")
raw = input(" Item # > ").strip()
if raw == "0": return
try:
idx = int(raw) - 1
if 0 <= idx < len(items):
item_id = items[idx][0]
self.display_party()
tidx = int(input(" On character # > ")) - 1
if 0 <= tidx < len(self.party):
self.use_item_outside_battle(item_id, self.party[tidx])
except ValueError:
pass
# ── Game over screen ──────────────────────────────────────────────────
def show_game_over_screen(self):
print("\n" + "☠"*65)
print(" GAME OVER")
print("☠"*65)
print(f"\n {self.player.name} has fallen after {self.battle_number} battles.")
print(f"\n ── BATTLE RECORD ──")
print(f" Total Battles: {self.battle_number}")
print(f" Battles Won: {self.battle_number - 1}") # lost the last one
print(f"\n ── PARTY HISTORY ──")
all_chars = list({id(c): c for c in self.all_party_members + self.deceased_companions}.values())
if self.player not in all_chars:
all_chars.insert(0, self.player)
for ch in all_chars:
status = "💀 DECEASED" if ch.is_dead else "ALIVE"
rarity = f"{'★'*ch.rarity}" if ch.rarity else "PLAYER"
print(f"\n {ch.name} Lv{ch.level} {ch.job.value} [{rarity}] - {status}")
print(f" HP:{ch.max_hp} MP:{ch.max_mp} PATK:{ch.base_stats.patk} "
f"MATK:{ch.base_stats.matk} PDEF:{ch.base_stats.pdef} "
f"MDEF:{ch.base_stats.mdef} SPD:{ch.base_stats.spd}")
print(f" Weapon:{ch.weapon.value} Element:{ch.element.value}")
print(f" Skills: ", end="")
skill_names = []
for sid in ch.skill_pool:
if sid in SKILL_DB:
unlocked = "✓" if sid in ch.unlocked_skills else "✗"
skill_names.append(f"{SKILL_DB[sid].name}[{unlocked}]")
print(", ".join(skill_names))
print("\n" + "☠"*65)
input(" [Press Enter to exit]")