main.py
"""Main game entry point: title screen, character creation, game loop."""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from data_types import *
from character import create_player_character, JOB_BASE_STATS
from game_state import GameState
from skills_db import SKILL_DB, JOB_SKILL_POOL
def clear():
os.system('clear' if os.name == 'posix' else 'cls')
def print_title():
clear()
print("""
╔══════════════════════════════════════════════════════════════╗
║ ║
║ ✦ CHRONICLES OF THE ETERNAL TURN ✦ ║
║ A JRPG CLI ADVENTURE ║
║ ║
╚══════════════════════════════════════════════════════════════╝
""")
def create_character() -> tuple:
clear()
print("╔══════════════════════════════════════════════════════════════╗")
print("║ CHARACTER CREATION ║")
print("╚══════════════════════════════════════════════════════════════╝")
while True:
name = input("\n Enter your hero's name: ").strip()
if name:
break
print(" Please enter a name.")
weapons = list(WeaponType)
weapon_descs = {
WeaponType.SWORD: "Balanced. Warriors, Knights, Paladins",
WeaponType.SPEAR: "Long reach. Spearman, Dragoon",
WeaponType.AXE: "High power. Berserker, Monk",
WeaponType.DAGGER: "Fast & precise. Assassin",
WeaponType.BOW: "Ranged. Ranger, Hunter",
WeaponType.STAFF: "Magical focus. All Mages and Healers",
}
print("\n ── Choose Your Weapon ──")
for i, w in enumerate(weapons):
print(f" [{i+1}] {w.value:10s} - {weapon_descs[w]}")
while True:
try:
wi = int(input(" > ")) - 1
if 0 <= wi < len(weapons): break
except ValueError: pass
print(" Invalid.")
weapon = weapons[wi]
elements = [e for e in Element if e != Element.NONE]
elem_descs = {
Element.FIRE: "Offensive burn effects",
Element.ICE: "Freeze and slow foes",
Element.LIGHTNING: "Chain damage, high speed",
Element.WIND: "Evasive, speed-based",
Element.EARTH: "Defense-break, sturdy",
Element.LIGHT: "Holy power, vs Undead/Dark",
Element.DARK: "Debuff and drain",
}
print("\n ── Choose Your Element ──")
for i, e in enumerate(elements):
print(f" [{i+1}] {e.value:10s} - {elem_descs[e]}")
while True:
try:
ei = int(input(" > ")) - 1
if 0 <= ei < len(elements): break
except ValueError: pass
print(" Invalid.")
element = elements[ei]
all_jobs = list(JobClass)
job_weapon_affinity = {
WeaponType.SWORD: [JobClass.WARRIOR, JobClass.KNIGHT, JobClass.PALADIN,
JobClass.ASSASSIN, JobClass.BERSERKER, JobClass.DRAGOON,
JobClass.DARK_MAGE, JobClass.LIGHT_MAGE],
WeaponType.SPEAR: [JobClass.SPEARMAN, JobClass.DRAGOON, JobClass.WARRIOR,
JobClass.KNIGHT, JobClass.RANGER, JobClass.HUNTER],
WeaponType.AXE: [JobClass.BERSERKER, JobClass.WARRIOR, JobClass.MONK,
JobClass.EARTH_MAGE, JobClass.DRAGOON, JobClass.KNIGHT],
WeaponType.DAGGER: [JobClass.ASSASSIN, JobClass.RANGER, JobClass.HUNTER,
JobClass.WIND_MAGE, JobClass.DARK_MAGE, JobClass.MONK],
WeaponType.BOW: [JobClass.RANGER, JobClass.HUNTER, JobClass.ASSASSIN,
JobClass.STORM_MAGE, JobClass.WIND_MAGE, JobClass.LIGHT_MAGE],
WeaponType.STAFF: [JobClass.CLERIC, JobClass.PRIEST, JobClass.FIRE_MAGE,
JobClass.ICE_MAGE, JobClass.STORM_MAGE, JobClass.WIND_MAGE,
JobClass.EARTH_MAGE, JobClass.DARK_MAGE, JobClass.LIGHT_MAGE,
JobClass.ARCANE_SAGE, JobClass.PALADIN],
}
suggested = job_weapon_affinity.get(weapon, all_jobs)
job_info = {
JobClass.WARRIOR: "Melee fighter, high PATK, balanced",
JobClass.KNIGHT: "Tank, high HP/DEF, Shield Bash",
JobClass.PALADIN: "Holy warrior, heal & attack",
JobClass.BERSERKER: "Rage fighter, highest PATK, low DEF",
JobClass.ASSASSIN: "Stealth, high CRIT/EVA, poison",
JobClass.RANGER: "Bow specialist, multi-hit, area",
JobClass.HUNTER: "Traps, dragon slayer, beast lore",
JobClass.SPEARMAN: "Spear master, sweep attacks",
JobClass.DRAGOON: "Dragon knight, jump attacks",
JobClass.MONK: "Unarmed master, combo strikes",
JobClass.CLERIC: "Healer & smiter, Light magic",
JobClass.PRIEST: "Pure healer, mass heals, revival",
JobClass.FIRE_MAGE: "Fire magic, burn effects",
JobClass.ICE_MAGE: "Ice magic, freeze/slow",
JobClass.STORM_MAGE: "Lightning magic, chain effects",
JobClass.WIND_MAGE: "Wind magic, speed buffs",
JobClass.EARTH_MAGE: "Earth magic, high power AOE",
JobClass.DARK_MAGE: "Dark magic, debuffs, drain",
JobClass.LIGHT_MAGE: "Light magic, barriers, blind",
JobClass.ARCANE_SAGE:"All-element magic, time manipulation",
}
print(f"\n ── Choose Your Job (Weapon: {weapon.value}) ──")
print(f" Recommended jobs shown. Enter A for all jobs.")
print()
for i, job in enumerate(suggested):
print(f" [{i+1:2d}] {job.value:15s} - {job_info.get(job,'')}")
print(f" [ A] Show ALL {len(all_jobs)} jobs")
job = None
while True:
raw = input(" > ").strip().upper()
if raw == "A":
print("\n ALL JOBS:")
for i, j in enumerate(all_jobs):
print(f" [{i+1:2d}] {j.value:15s} - {job_info.get(j,'')}")
while True:
try:
ji = int(input(" > ")) - 1
if 0 <= ji < len(all_jobs):
job = all_jobs[ji]
break
except ValueError: pass
print(" Invalid.")
break
else:
try:
idx = int(raw) - 1
if 0 <= idx < len(suggested):
job = suggested[idx]
break
except ValueError: pass
print(" Invalid choice.")
# Skill preview
print(f"\n ── Skill Preview for {job.value} ──")
pool = JOB_SKILL_POOL[job]
basics = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.BASIC][:4]
inters = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.INTERMEDIATE][:4]
ultims = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.ULTIMATE][:2]
print(" Basic (start with 2):")
for sid in basics:
sk = SKILL_DB[sid]
print(f" • {sk.name:20s} MP:{sk.mp_cost:3d} PWR:{sk.power} - {sk.description}")
print(" Intermediate (unlock lv5/10):")
for sid in inters:
sk = SKILL_DB[sid]
print(f" • {sk.name:20s} MP:{sk.mp_cost:3d} PWR:{sk.power} - {sk.description}")
print(" Ultimate (unlock lv20):")
for sid in ultims:
sk = SKILL_DB[sid]
print(f" • {sk.name:20s} MP:{sk.mp_cost:3d} PWR:{sk.power} - {sk.description}")
# Summary
stats = JOB_BASE_STATS[job]
print(f"\n ── Summary ──")
print(f" Name: {name} | Job: {job.value} | Weapon: {weapon.value} | Element: {element.value}")
print(f" HP:{stats.hp} MP:{stats.mp} PATK:{stats.patk} MATK:{stats.matk} "
f"PDEF:{stats.pdef} MDEF:{stats.mdef} SPD:{stats.spd}")
confirm = input("\n Confirm? (Y/N) > ").strip().upper()
if confirm != "Y":
return create_character()
return name, job, weapon, element
def main():
print_title()
print("""
Welcome to Chronicles of the Eternal Turn!
OBJECTIVE: Survive as many battles as you can.
Collect companions, level up, grow powerful.
COMBAT TIPS:
• Attack enemies' Weaknesses to reduce their Shield Points
• When Shield = 0, enemy is BROKEN (50% more damage, can't act)
• Use Defend to reduce incoming damage and gain turn priority
• If a party member is KO'd, revive within 3 turns or they DIE
• If YOUR character dies → GAME OVER
COMPANION SYSTEM:
• Win battles to earn companions (3★/4★/5★)
• Party maximum: 4 members
• Companions can permanently die in battle
""")
input(" [Press Enter to begin]")
name, job, weapon, element = create_character()
clear()
gs = GameState()
player = create_player_character(name, job, weapon, element)
gs.player = player
gs.party = [player]
gs.all_party_members = [player]
print(f"\n ✦ {player.name} the {player.job.value} sets forth!")
print(f" Starting skills: " +
", ".join(SKILL_DB[s].name for s in player.unlocked_skills if s in SKILL_DB))
input("\n [Press Enter to begin your adventure]")
# Main game loop
while not gs.game_over:
print(f"\n{'═'*65}")
print(f" ✦ BATTLE {gs.battle_number + 1}")
if gs.battle_number > 0:
cont = gs.between_battles_menu()
if not cont:
print("\n Thanks for playing! Farewell, hero.")
print(f" Final record: {gs.battle_number} battles | {gs.player.name} Lv{gs.player.level}")
break
alive = [ch for ch in gs.party if not ch.is_dead]
if not alive:
gs.game_over = True
break
survived = gs.run_battle()
if not survived or gs.game_over:
gs.show_game_over_screen()
break
if not gs.game_over and gs.battle_number > 0:
print(f"\n Journey ended. {gs.battle_number} battles completed.")
print(f" {gs.player.name} reached Level {gs.player.level}. Well done!")
if __name__ == "__main__":
main()