Guest User

Untitled

a guest
Oct 25th, 2025
26
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 30.34 KB | None | 0 0
  1. # ancient_poke_side_v2.py
  2. """
  3. Ancient Pokémon — side-scroller prototype v2
  4.  
  5. Features:
  6. - Runtime placeholder sprite generation (PNG files) under ./sprites/
  7. - Sprite loading + animation system with graceful fallback
  8. - Pokemon AI states: idle, wander, follow, guard, attack, flee
  9. - Simple in-world combat: attack cooldown, damage, faint
  10. - Start menu, save/load, parallax, camera, multiple ownership types
  11. - Uses pygame only (no external libs required)
  12. """
  13.  
  14. import pygame
  15. import json
  16. import os
  17. import random
  18. from pathlib import Path
  19. import math
  20. import time
  21.  
  22. # ---- Basic setup ----
  23. pygame.init()
  24. WIDTH, HEIGHT = 1200, 640
  25. SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
  26. pygame.display.set_caption("Ancient Pokémon — Prototype v2")
  27. CLOCK = pygame.time.Clock()
  28. FONT = pygame.font.SysFont("arial", 16)
  29. BIG_FONT = pygame.font.SysFont("serif", 44)
  30.  
  31. SAVE_PATH = Path("savegame_v2.json")
  32. SPRITE_ROOT = Path("sprites")
  33. FPS = 60
  34.  
  35. # ---- Game constants ----
  36. GRAVITY = 1400
  37. GROUND_Y = HEIGHT - 90
  38. PLAYER_SPEED = 320
  39. POKEMON_BASE_SPEED = 160
  40. SCROLL_EDGE = WIDTH * 0.4
  41.  
  42. # ---- Utility helpers ----
  43. def ensure_dir(p: Path):
  44. if not p.exists():
  45. p.mkdir(parents=True, exist_ok=True)
  46.  
  47. def clamp(v, a, b):
  48. return max(a, min(b, v))
  49.  
  50. # ---- Placeholder sprite generation ----
  51. def generate_placeholder_sprite(path: Path, text: str, size=(64, 64), bgcolor=(150, 150, 150), fg=(20, 20, 20)):
  52. """
  53. Create a simple PNG using pygame surfaces and save it.
  54. """
  55. ensure_dir(path.parent)
  56. surf = pygame.Surface(size, pygame.SRCALPHA)
  57. surf.fill(bgcolor)
  58. # border
  59. pygame.draw.rect(surf, (0,0,0), surf.get_rect(), 2)
  60. # text
  61. small = pygame.font.SysFont("arial", max(10, size[1] // 6))
  62. lines = text.split("\n")
  63. # center multiline
  64. total_h = sum(small.size(line)[1] for line in lines)
  65. y = (size[1] - total_h) // 2
  66. for line in lines:
  67. txt = small.render(line, True, fg)
  68. surf.blit(txt, ((size[0] - txt.get_width())//2, y))
  69. y += txt.get_height()
  70. try:
  71. pygame.image.save(surf, str(path))
  72. return True
  73. except Exception as e:
  74. print("Could not save placeholder sprite:", e)
  75. return False
  76.  
  77. def ensure_default_sprites():
  78. """
  79. Create a small set of placeholder sprites for:
  80. - player: idle/run frames
  81. - pokemon species: idle/run/attack frames
  82. """
  83. species_list = ["scarabeet", "sphinxling", "desertlark", "mummat", "ra-hound", "mini-scarab", "sandpup"]
  84. # Player frames
  85. player_dir = SPRITE_ROOT / "player"
  86. ensure_dir(player_dir)
  87. # create idle frames and run frames
  88. for action, frames in [("idle", 3), ("run", 4), ("attack", 3)]:
  89. ad = player_dir / action
  90. ensure_dir(ad)
  91. for i in range(frames):
  92. p = ad / f"{i}.png"
  93. if not p.exists():
  94. generate_placeholder_sprite(p, f"player\n{action}\n{i}", size=(48,72), bgcolor=(180,160,120))
  95. # Create species frames
  96. for spec in species_list:
  97. sdir = SPRITE_ROOT / spec
  98. for action, frames in [("idle", 3), ("run", 4), ("attack", 3), ("faint", 2)]:
  99. ad = sdir / action
  100. ensure_dir(ad)
  101. for i in range(frames):
  102. p = ad / f"{i}.png"
  103. if not p.exists():
  104. bg = (200,180,120) if "scarab" in spec or "sand" in spec else (180,120,160)
  105. generate_placeholder_sprite(p, f"{spec}\n{action}\n{i}", size=(56,56), bgcolor=bg)
  106.  
  107. # ---- Sprite / Animation loading system ----
  108. class AnimatedSprite:
  109. """
  110. Loads action folders under sprites/<name>/<action>/<frame>.png
  111. action_frames: dict action -> [Surface,..]
  112. """
  113. def __init__(self, name, size_hint=(48,48)):
  114. self.name = name
  115. self.base = SPRITE_ROOT / name
  116. self.actions = {}
  117. self.size_hint = size_hint
  118. self.loaded = False
  119. self.load_all_actions()
  120.  
  121. def load_all_actions(self):
  122. # scan base folder for subfolders (actions)
  123. if not self.base.exists():
  124. self.loaded = False
  125. return
  126. for action_dir in self.base.iterdir():
  127. if action_dir.is_dir():
  128. frames = []
  129. # load sequentially numbered PNGs: 0.png,1.png,...
  130. i = 0
  131. while True:
  132. p = action_dir / f"{i}.png"
  133. if p.exists():
  134. try:
  135. img = pygame.image.load(str(p)).convert_alpha()
  136. frames.append(img)
  137. except Exception:
  138. # ignore bad frames
  139. pass
  140. i += 1
  141. else:
  142. break
  143. if frames:
  144. self.actions[action_dir.name] = frames
  145. self.loaded = len(self.actions) > 0
  146.  
  147. def get_action_frames(self, action):
  148. # fallback: idle or rectangle placeholder
  149. if action in self.actions:
  150. return self.actions[action]
  151. if "idle" in self.actions:
  152. return self.actions["idle"]
  153. return None
  154.  
  155. # ---- Entity base classes ----
  156. class Entity:
  157. def __init__(self, x, y, w, h):
  158. self.x = float(x)
  159. self.y = float(y)
  160. self.vx = 0.0
  161. self.vy = 0.0
  162. self.w = w
  163. self.h = h
  164. self.on_ground = False
  165.  
  166. @property
  167. def rect(self):
  168. return pygame.Rect(int(self.x), int(self.y), self.w, self.h)
  169.  
  170. def apply_gravity(self, dt):
  171. if not self.on_ground:
  172. self.vy += GRAVITY * dt
  173. self.y += self.vy * dt
  174.  
  175. def ground_collision(self):
  176. if self.y + self.h >= GROUND_Y:
  177. self.y = GROUND_Y - self.h
  178. self.vy = 0
  179. self.on_ground = True
  180. else:
  181. self.on_ground = False
  182.  
  183. # ---- Character (Player/NPC) ----
  184. class Character(Entity):
  185. def __init__(self, name, x, y, is_player=False):
  186. super().__init__(x, y, 36, 64)
  187. self.name = name
  188. self.is_player = is_player
  189. self.facing = 1
  190. self.speed = PLAYER_SPEED
  191. self.items = {"healing_potion": 2}
  192. self.pokemon_party = [] # list of Pokemon instances
  193. self.sprite = AnimatedSprite("player", size_hint=(48,72))
  194. # Animation state
  195. self.action = "idle"
  196. self.frame_idx = 0
  197. self.frame_timer = 0.0
  198. self.frame_duration = 0.12
  199.  
  200. def player_input(self, dt, keys):
  201. self.vx = 0
  202. moving = False
  203. if keys[pygame.K_LEFT] or keys[pygame.K_a]:
  204. self.vx = -self.speed
  205. self.facing = -1
  206. moving = True
  207. if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
  208. self.vx = self.speed
  209. self.facing = 1
  210. moving = True
  211. if (keys[pygame.K_SPACE] or keys[pygame.K_w] or keys[pygame.K_UP]) and self.on_ground:
  212. self.vy = -520
  213. self.on_ground = False
  214. if moving:
  215. self.action = "run"
  216. else:
  217. self.action = "idle"
  218. # Item: H heal first pokemon
  219. if keys[pygame.K_h] and self.items.get("healing_potion",0) > 0:
  220. if self.pokemon_party:
  221. target = self.pokemon_party[0]
  222. target.heal(30)
  223. self.items["healing_potion"] -= 1
  224. self.x += self.vx * dt
  225.  
  226. def update(self, dt, keys):
  227. if self.is_player:
  228. self.player_input(dt, keys)
  229. # update physics
  230. self.apply_gravity(dt)
  231. self.ground_collision()
  232. # animation frame
  233. self.frame_timer += dt
  234. if self.frame_timer >= self.frame_duration:
  235. self.frame_timer -= self.frame_duration
  236. self.frame_idx = (self.frame_idx + 1) % max(1, len(self.sprite.get_action_frames(self.action) or [0]))
  237.  
  238. def draw(self, surface, camera):
  239. dest = camera.apply(self.rect)
  240. frames = self.sprite.get_action_frames(self.action)
  241. if frames:
  242. frame = frames[self.frame_idx % len(frames)]
  243. # flip if facing left
  244. if self.facing < 0:
  245. frame = pygame.transform.flip(frame, True, False)
  246. surface.blit(frame, (dest.x, dest.y))
  247. else:
  248. # fallback rectangle
  249. pygame.draw.rect(surface, (50,120,200) if self.is_player else (160,120,80), dest)
  250. surface.blit(FONT.render(self.name, True, (0,0,0)), (dest.x, dest.y - 18))
  251.  
  252. # ---- Pokemon class with AI and combat ----
  253. class Pokemon(Entity):
  254. def __init__(self, species, x, y, owner=None):
  255. super().__init__(x, y, 48, 48)
  256. self.species = species.lower()
  257. self.owner = owner # None | Character | Pokemon
  258. self.personality = random.choice(["brave", "timid", "curious"])
  259. self.state = "idle" # idle, wander, follow, guard, attack, flee
  260. self.health = 100.0
  261. self.max_health = 100.0
  262. self.speed = POKEMON_BASE_SPEED * (1.1 if self.personality=="brave" else 0.9 if self.personality=="timid" else 1.0)
  263. self.wander_timer = random.uniform(0.4, 2.5)
  264. self.target = None
  265. # combat
  266. self.attack_range = 90
  267. self.attack_damage = 18 if self.personality=="brave" else 12
  268. self.attack_cooldown = 1.2
  269. self.attack_timer = 0.0
  270. self.is_fainted = False
  271. # sprites + animation
  272. self.sprite = AnimatedSprite(self.species, size_hint=(56,56))
  273. self.action = "idle"
  274. self.frame_idx = 0
  275. self.frame_timer = 0.0
  276. self.frame_duration = 0.14
  277.  
  278. def heal(self, amount):
  279. if self.is_fainted:
  280. return
  281. self.health = clamp(self.health + amount, 0, self.max_health)
  282.  
  283. def take_damage(self, amount, attacker=None):
  284. if self.is_fainted:
  285. return
  286. self.health -= amount
  287. # reactive: if owner exists and is player, might call to assist
  288. if self.health <= 0:
  289. self.health = 0
  290. self.is_fainted = True
  291. self.state = "faint"
  292. self.action = "faint"
  293. self.vx = 0
  294. self.attack_timer = 0
  295. else:
  296. # small flinch / possible flee
  297. if self.personality == "timid" and random.random() < 0.6:
  298. self.state = "flee"
  299. elif self.owner is not None:
  300. # guard or counterattack
  301. if random.random() < 0.6:
  302. self.state = "attack"
  303. self.target = attacker
  304.  
  305. def _resolve_human_owner(self):
  306. owner = self.owner
  307. visited = set()
  308. while owner and not isinstance(owner, Character):
  309. if id(owner) in visited:
  310. break
  311. visited.add(id(owner))
  312. owner = getattr(owner, "owner", None)
  313. return owner if isinstance(owner, Character) else None
  314.  
  315. def _distance_to(self, ent):
  316. return abs((self.x + self.w/2) - (ent.x + ent.w/2))
  317.  
  318. def update_animation(self, dt):
  319. self.frame_timer += dt
  320. if self.frame_timer >= self.frame_duration:
  321. self.frame_timer -= self.frame_duration
  322. frames = self.sprite.get_action_frames(self.action) or []
  323. if frames:
  324. self.frame_idx = (self.frame_idx + 1) % len(frames)
  325. else:
  326. self.frame_idx = 0
  327.  
  328. def update(self, dt, world):
  329. if self.is_fainted:
  330. # fainting: slowly sink a little
  331. self.vx = 0
  332. self.vy += GRAVITY * dt * 0.2
  333. self.y += self.vy * dt
  334. self.ground_collision()
  335. self.update_animation(dt)
  336. return
  337.  
  338. # Reduce attack timer
  339. if self.attack_timer > 0:
  340. self.attack_timer = max(0.0, self.attack_timer - dt)
  341.  
  342. # State logic
  343. if self.owner is None:
  344. self._wild_ai(dt, world)
  345. else:
  346. self._owned_ai(dt, world)
  347.  
  348. # movement
  349. self.x += self.vx * dt
  350. self.apply_gravity(dt)
  351. self.ground_collision()
  352.  
  353. # animation choice
  354. if self.state in ("attack","assist"):
  355. self.action = "attack"
  356. elif abs(self.vx) > 10:
  357. self.action = "run"
  358. elif self.is_fainted:
  359. self.action = "faint"
  360. else:
  361. self.action = "idle"
  362. self.update_animation(dt)
  363.  
  364. def _wild_ai(self, dt, world):
  365. # wild behavior: wander usually; might attack player-side pokemon if brave
  366. nearest = world.find_nearest_interesting(self)
  367. if nearest and self._distance_to(nearest) < 200:
  368. if self.personality == "timid":
  369. self.state = "flee"
  370. self._flee_from(nearest, dt)
  371. return
  372. else:
  373. # brave or curious: maybe attack
  374. if random.random() < 0.6:
  375. self.state = "attack"
  376. self.target = nearest
  377. self._approach_target(dt)
  378. self.try_attack(world)
  379. return
  380. # wander idle
  381. self.state = "wander"
  382. self.wander_timer -= dt
  383. if self.wander_timer <= 0:
  384. self.wander_timer = random.uniform(1.0, 3.0)
  385. self.vx = random.choice([-1,0,1]) * self.speed * 0.5
  386. self.vx *= 0.88
  387.  
  388. def _owned_ai(self, dt, world):
  389. human_owner = self._resolve_human_owner()
  390. # guard behavior: if close to owner position and enemy nearby -> attack
  391. if self.state == "guard" or (self.owner and isinstance(self.owner, Pokemon) and getattr(self.owner, "state", None) == "guard"):
  392. # stay near owner
  393. follow_ent = self.owner if self.owner else human_owner
  394. if follow_ent:
  395. dx = (follow_ent.x + follow_ent.w/2) - (self.x + self.w/2)
  396. if abs(dx) > 70:
  397. self.vx = self.speed * (1 if dx > 0 else -1)
  398. else:
  399. self.vx = 0
  400. # look for enemies
  401. enemy = world.find_nearest_enemy_for(self)
  402. if enemy and self._distance_to(enemy) < 160:
  403. self.state = "attack"
  404. self.target = enemy
  405. self._approach_target(dt)
  406. self.try_attack(world)
  407. return
  408. return
  409.  
  410. # assist/attack commanded by owner
  411. if self.state in ("assist","attack") and self.target:
  412. if self.target.is_fainted:
  413. self.state = "follow"
  414. self.target = None
  415. return
  416. if self._distance_to(self.target) > 18 and self._distance_to(self.target) < 800:
  417. self._approach_target(dt)
  418. else:
  419. self.vx = 0
  420. self.try_attack(world)
  421. return
  422.  
  423. # follow owner
  424. if isinstance(self.owner, Character) or isinstance(self.owner, Pokemon):
  425. follow_ent = self.owner
  426. dx = (follow_ent.x + follow_ent.w/2 - 40) - self.x
  427. if abs(dx) > 60:
  428. self.vx = self.speed if dx > 0 else -self.speed
  429. else:
  430. self.vx = 0
  431. # look for nearby enemies if guard-ish or brave
  432. nearby = world.find_nearest_enemy_for(self)
  433. if nearby and self._distance_to(nearby) < 160:
  434. if self.personality != "timid":
  435. self.state = "attack"
  436. self.target = nearby
  437. return
  438.  
  439. # default fallback
  440. self.state = "idle"
  441. self.vx *= 0.8
  442.  
  443. def _approach_target(self, dt):
  444. if not self.target:
  445. return
  446. dx = (self.target.x + self.target.w/2) - (self.x + self.w/2)
  447. if abs(dx) > 6:
  448. self.vx = self.speed if dx > 0 else -self.speed
  449. else:
  450. self.vx = 0
  451.  
  452. def _flee_from(self, ent, dt):
  453. dx = (self.x + self.w/2) - (ent.x + ent.w/2)
  454. self.vx = self.speed if dx > 0 else -self.speed
  455. self.x += self.vx * dt
  456.  
  457. def try_attack(self, world):
  458. # if in range and cooldown expired, perform attack
  459. if not self.target or self.attack_timer > 0:
  460. return
  461. if self._distance_to(self.target) <= self.attack_range:
  462. # perform attack
  463. self.attack_timer = self.attack_cooldown
  464. # damage
  465. # simple damage: attack_damage +/- 25%
  466. dmg = self.attack_damage * random.uniform(0.75, 1.25)
  467. self.target.take_damage(dmg, attacker=self)
  468. # slight recoil or animation handled via action
  469. self.action = "attack"
  470.  
  471. def draw(self, surface, camera):
  472. if self.is_fainted:
  473. color = (90,90,90)
  474. elif self.owner is None:
  475. color = (200,70,70)
  476. elif isinstance(self.owner, Character) and self._resolve_human_owner() and self._resolve_human_owner().is_player:
  477. color = (80,200,120)
  478. else:
  479. color = (200,180,80)
  480.  
  481. dest = camera.apply(self.rect)
  482. frames = self.sprite.get_action_frames(self.action)
  483. if frames:
  484. frame = frames[self.frame_idx % len(frames)]
  485. # flip based on velocity
  486. if self.vx < -10:
  487. frame = pygame.transform.flip(frame, True, False)
  488. surface.blit(frame, (dest.x, dest.y))
  489. else:
  490. pygame.draw.ellipse(surface, color, dest)
  491.  
  492. # health bar
  493. hb_w = self.w
  494. hb_h = 6
  495. hp_ratio = max(0.0, self.health / self.max_health)
  496. hp_rect = pygame.Rect(dest.x, dest.y - 10, int(hb_w * hp_ratio), hb_h)
  497. bg_rect = pygame.Rect(dest.x, dest.y - 10, hb_w, hb_h)
  498. pygame.draw.rect(surface, (80,80,80), bg_rect)
  499. pygame.draw.rect(surface, (80,200,120), hp_rect)
  500.  
  501. # name + state
  502. txt = FONT.render(f"{self.species} [{self.state}]", True, (0,0,0))
  503. surface.blit(txt, (dest.x, dest.y - 24))
  504.  
  505. # ---- Camera & Parallax ----
  506. class Camera:
  507. def __init__(self):
  508. self.x = 0.0
  509. def apply(self, rect: pygame.Rect):
  510. return rect.move(-int(self.x), 0)
  511. def update(self, player: Character):
  512. target_x = player.x - SCROLL_EDGE
  513. if target_x > self.x:
  514. self.x = target_x
  515. # clamp min
  516. self.x = max(0, self.x)
  517.  
  518. class ParallaxLayer:
  519. def __init__(self, color, height, y_offset, speed):
  520. self.color = color
  521. self.height = height
  522. self.y_offset = y_offset
  523. self.speed = speed
  524. def draw(self, surf, camera_x):
  525. w = surf.get_width()
  526. tile_w = 480
  527. offset = int((camera_x * self.speed) % tile_w)
  528. for i in range(-1, w//tile_w + 2):
  529. r = pygame.Rect(i*tile_w - offset, self.y_offset, tile_w - 40, self.height)
  530. pygame.draw.rect(surf, self.color, r)
  531.  
  532. # ---- World class ----
  533. class World:
  534. def __init__(self):
  535. self.camera = Camera()
  536. self.bg_layers = [
  537. ParallaxLayer((210,190,140), 200, 40, 0.12),
  538. ParallaxLayer((160,140,100), 120, 140, 0.28),
  539. ]
  540. self.foreground = []
  541. self.characters = []
  542. self.pokemons = []
  543. self.populate_demo()
  544.  
  545. def populate_demo(self):
  546. # player
  547. player = Character("Player", 220, GROUND_Y - 64, is_player=True)
  548. self.player = player
  549. self.characters.append(player)
  550. # npc
  551. npc = Character("Merchant", 760, GROUND_Y - 64, is_player=False)
  552. self.characters.append(npc)
  553. # foreground objects
  554. for i in range(10):
  555. rx = i * 300 + 120
  556. self.foreground.append(pygame.Rect(rx, GROUND_Y - random.randint(10,40) - 60, 90, random.randint(40,120)))
  557. # pokemons
  558. p1 = Pokemon("scarabeet", 260, GROUND_Y - 48, owner=player)
  559. p2 = Pokemon("sphinxling", 300, GROUND_Y - 48, owner=player)
  560. w1 = Pokemon("desertlark", 920, GROUND_Y - 48, owner=None)
  561. w2 = Pokemon("mummat", 1100, GROUND_Y - 48, owner=None)
  562. p3 = Pokemon("ra-hound", 420, GROUND_Y - 48, owner=None)
  563. sub = Pokemon("mini-scarab", 440, GROUND_Y - 48, owner=p3)
  564. # set some personalities for variety
  565. p1.personality = "brave"
  566. p2.personality = "curious"
  567. w2.personality = "timid"
  568. p3.personality = "brave"
  569. self.pokemons.extend([p1,p2,w1,w2,p3,sub])
  570. player.pokemon_party.extend([p1,p2])
  571.  
  572. def update(self, dt, keys_pressed):
  573. for ch in self.characters:
  574. ch.update(dt, keys_pressed)
  575. for pk in self.pokemons:
  576. pk.update(dt, self)
  577. # simple removal of fainted pokemons from active list after a delay? keep for now
  578. self.camera.update(self.player)
  579.  
  580. def draw(self, surf):
  581. surf.fill((180,220,255))
  582. for layer in self.bg_layers:
  583. layer.draw(surf, self.camera.x)
  584. pygame.draw.rect(surf, (220,200,160), pygame.Rect(0, GROUND_Y, WIDTH, HEIGHT - GROUND_Y))
  585. # foreground objects
  586. for r in self.foreground:
  587. drawr = self.camera.apply(r)
  588. pygame.draw.rect(surf, (120,100,70), drawr)
  589. # characters
  590. for ch in self.characters:
  591. ch.draw(surf, self.camera)
  592. # pokemons
  593. for pk in self.pokemons:
  594. pk.draw(surf, self.camera)
  595. # UI
  596. self.draw_ui(surf)
  597.  
  598. def draw_ui(self, surf):
  599. x = 8; y = 8
  600. p = self.player
  601. items_txt = f"Items: {p.items}"
  602. surf.blit(FONT.render(items_txt, True, (0,0,0)), (x,y))
  603. y += 22
  604. party_txt = "Party: " + ", ".join([f"{pk.species}({int(pk.health)})" for pk in p.pokemon_party])
  605. surf.blit(FONT.render(party_txt, True, (0,0,0)), (x,y))
  606. # instructions
  607. instr = "Arrows/A-D move, Space jump, H heal, 1 attack, 2 follow, C spawn wild, F5 save, F9 load"
  608. surf.blit(FONT.render(instr, True, (0,0,0)), (8, HEIGHT - 24))
  609.  
  610. # world queries
  611. def find_nearest_interesting(self, pk):
  612. candidates = []
  613. for ch in self.characters:
  614. candidates.append(ch)
  615. for p in self.pokemons:
  616. if p is not pk:
  617. candidates.append(p)
  618. if not candidates:
  619. return None
  620. nearest = min(candidates, key=lambda c: abs((pk.x+pk.w/2)-(c.x+c.w/2)))
  621. return nearest
  622.  
  623. def find_nearest_enemy_for(self, pk):
  624. my_owner = pk._resolve_human_owner()
  625. enemies = []
  626. for p in self.pokemons:
  627. if p is pk or p.is_fainted:
  628. continue
  629. other_owner = p._resolve_human_owner()
  630. # enemy if owners differ (player vs non-player) or wild vs player's
  631. if my_owner is None and other_owner is not None:
  632. enemies.append(p)
  633. elif my_owner is not None and other_owner is not my_owner:
  634. enemies.append(p)
  635. if not enemies:
  636. return None
  637. return min(enemies, key=lambda e: abs((pk.x+pk.w/2)-(e.x+e.w/2)))
  638.  
  639. # serialization
  640. def serialize(self):
  641. data = {
  642. "player": {"x": self.player.x, "y": self.player.y, "items": self.player.items},
  643. "pokemons": []
  644. }
  645. for pk in self.pokemons:
  646. data["pokemons"].append({
  647. "species": pk.species,
  648. "x": pk.x,
  649. "y": pk.y,
  650. "health": pk.health,
  651. "personality": pk.personality,
  652. "owner": self._owner_to_ref(pk.owner),
  653. "is_fainted": pk.is_fainted
  654. })
  655. return data
  656.  
  657. def _owner_to_ref(self, owner):
  658. if owner is None:
  659. return None
  660. if isinstance(owner, Character):
  661. return {"type":"player"} if owner.is_player else {"type":"npc","name":owner.name,"x":owner.x,"y":owner.y}
  662. if isinstance(owner, Pokemon):
  663. try:
  664. return {"type":"pokemon","index":self.pokemons.index(owner)}
  665. except ValueError:
  666. return None
  667. return None
  668.  
  669. def load_from(self, data):
  670. if "player" in data:
  671. pd = data["player"]
  672. self.player.x = pd.get("x", self.player.x)
  673. self.player.y = pd.get("y", self.player.y)
  674. self.player.items = pd.get("items", self.player.items)
  675. if "pokemons" in data:
  676. self.pokemons.clear()
  677. # create instances
  678. for pkd in data["pokemons"]:
  679. pk = Pokemon(pkd["species"], pkd.get("x",300), pkd.get("y", GROUND_Y-48), owner=None)
  680. pk.health = pkd.get("health", 100)
  681. pk.personality = pkd.get("personality", "curious")
  682. pk.is_fainted = pkd.get("is_fainted", False)
  683. self.pokemons.append(pk)
  684. # resolve owners
  685. for i, pkd in enumerate(data["pokemons"]):
  686. ref = pkd.get("owner")
  687. if not ref:
  688. self.pokemons[i].owner = None
  689. elif ref["type"] == "player":
  690. self.pokemons[i].owner = self.player
  691. self.player.pokemon_party.append(self.pokemons[i])
  692. elif ref["type"] == "pokemon":
  693. idx = ref.get("index")
  694. if idx is not None and 0 <= idx < len(self.pokemons):
  695. self.pokemons[i].owner = self.pokemons[idx]
  696. elif ref["type"] == "npc":
  697. existing = next((c for c in self.characters if c.name == ref["name"]), None)
  698. if not existing:
  699. newnpc = Character(ref["name"], x=ref.get("x",700), y=ref.get("y",GROUND_Y-64), is_player=False)
  700. self.characters.append(newnpc)
  701. existing = newnpc
  702. self.pokemons[i].owner = existing
  703.  
  704. # ---- Start menu / game loop ----
  705. def start_screen():
  706. clock = pygame.time.Clock()
  707. options = ["New Game", "Load Game", "Quit"]
  708. selected = 0
  709. while True:
  710. for ev in pygame.event.get():
  711. if ev.type == pygame.QUIT:
  712. return None
  713. if ev.type == pygame.KEYDOWN:
  714. if ev.key == pygame.K_UP:
  715. selected = (selected - 1) % len(options)
  716. if ev.key == pygame.K_DOWN:
  717. selected = (selected + 1) % len(options)
  718. if ev.key == pygame.K_RETURN:
  719. return options[selected]
  720. SCREEN.fill((10,10,40))
  721. title = BIG_FONT.render("Ancient Pokémon — Prototype v2", True, (255,230,160))
  722. SCREEN.blit(title, (WIDTH//2 - title.get_width()//2, 80))
  723. for i,opt in enumerate(options):
  724. col = (255,255,200) if i == selected else (200,200,200)
  725. s = FONT.render(opt, True, col)
  726. SCREEN.blit(s, (WIDTH//2 - s.get_width()//2, 260 + i*40))
  727. pygame.display.flip()
  728. clock.tick(30)
  729.  
  730. def game_loop(world):
  731. running = True
  732. paused = False
  733. last_save_time = 0
  734. while running:
  735. dt = CLOCK.tick(FPS) / 1000.0
  736. keys = pygame.key.get_pressed()
  737. for ev in pygame.event.get():
  738. if ev.type == pygame.QUIT:
  739. running = False
  740. if ev.type == pygame.KEYDOWN:
  741. if ev.key == pygame.K_F5:
  742. save_json(SAVE_PATH, world.serialize())
  743. last_save_time = time.time()
  744. print("Saved.")
  745. if ev.key == pygame.K_F9:
  746. data = load_json(SAVE_PATH)
  747. if data:
  748. world.load_from(data)
  749. print("Loaded.")
  750. if ev.key == pygame.K_ESCAPE:
  751. running = False
  752. if ev.key == pygame.K_1:
  753. if world.player.pokemon_party:
  754. pk = world.player.pokemon_party[0]
  755. pk.state = "attack"
  756. pk.target = world.find_nearest_enemy_for(pk)
  757. if ev.key == pygame.K_2:
  758. if world.player.pokemon_party:
  759. pk = world.player.pokemon_party[0]
  760. pk.state = "follow"
  761. pk.target = None
  762. if ev.key == pygame.K_c:
  763. spawn_x = world.camera.x + WIDTH * 0.85
  764. neww = Pokemon(random.choice(["sandpup","mini-scarab","desertlark"]), spawn_x, GROUND_Y-48, owner=None)
  765. world.pokemons.append(neww)
  766. world.update(dt, keys)
  767. world.draw(SCREEN)
  768. # overlay
  769. fps = int(CLOCK.get_fps())
  770. SCREEN.blit(FONT.render(f"FPS: {fps}", True, (0,0,0)), (WIDTH - 100, 8))
  771. if last_save_time and time.time() - last_save_time < 2.5:
  772. SCREEN.blit(FONT.render("Saved!", True, (0,0,0)), (WIDTH - 180, 28))
  773. pygame.display.flip()
  774. # on exit save
  775. try:
  776. save_json(SAVE_PATH, world.serialize())
  777. except Exception as e:
  778. print("Error saving on exit:", e)
  779.  
  780. # ---- Simple JSON load/save helpers ----
  781. def load_json(path: Path):
  782. if path.exists():
  783. try:
  784. with open(path, "r") as f:
  785. return json.load(f)
  786. except Exception as e:
  787. print("Load failed:", e)
  788. return None
  789.  
  790. def save_json(path: Path, data):
  791. try:
  792. with open(path, "w") as f:
  793. json.dump(data, f, indent=2)
  794. return True
  795. except Exception as e:
  796. print("Save failed:", e)
  797. return False
  798.  
  799. # ---- Main entry ----
  800. def main():
  801. # generate placeholder assets if missing
  802. ensure_default_sprites()
  803. # load sprites for all species used in demo (ensures AnimatedSprite picks them up)
  804. world = World()
  805. # ensure each Character/Pokemon AnimatedSprite tries loading (they load during construction)
  806. while True:
  807. choice = start_screen()
  808. if choice is None or choice == "Quit":
  809. break
  810. if choice == "New Game":
  811. world = World()
  812. game_loop(world)
  813. elif choice == "Load Game":
  814. data = load_json(SAVE_PATH)
  815. if data:
  816. world = World()
  817. world.load_from(data)
  818. game_loop(world)
  819. else:
  820. # fallback to new
  821. world = World()
  822. game_loop(world)
  823. pygame.quit()
  824.  
  825. if __name__ == "__main__":
  826. main()
  827.  
Advertisement
Add Comment
Please, Sign In to add comment