""" Pixel Village JRPG A cozy little JRPG with town, forest, and turn-based combat! Requirements: pip install pygame Run: python jrpg_game.py Controls: Arrow Keys - Move E / Space - Interact / Confirm Escape - Cancel / Back """ import pygame import sys import random import math pygame.init() # ── Constants ────────────────────────────────────────────────────────────── W, H = 640, 480 TILE = 32 FPS = 60 # Bright pixel palette C = { "bg": (20, 20, 35), "sky": (100, 180, 255), "grass": (80, 200, 80), "dark_grass":(60, 160, 60), "path": (210, 180, 130), "water": (60, 160, 220), "tree": (40, 140, 40), "tree_top": (50, 200, 60), "trunk": (140, 90, 40), "house_w": (240, 220, 200), "house_r": (220, 60, 60), "store_w": (200, 220, 255), "store_r": (60, 100, 220), "door": (140, 80, 30), "window": (180, 230, 255), "sign": (240, 200, 100), "white": (255, 255, 255), "black": (0, 0, 0), "gray": (160, 160, 160), "dark": (30, 30, 50), "panel": (20, 20, 50), "panel2": (40, 40, 80), "border": (120, 100, 200), "gold": (255, 210, 50), "red": (240, 60, 60), "green": (80, 220, 100), "blue": (80, 160, 240), "pink": (255, 140, 200), "purple": (180, 100, 255), "yellow": (255, 240, 80), "orange": (255, 160, 40), "hp_red": (220, 50, 50), "mp_blue": (50, 100, 220), "xp_green": (50, 200, 80), } screen = pygame.display.set_mode((W, H), pygame.SCALED | pygame.RESIZABLE) pygame.display.set_caption("Pixel Village JRPG") clock = pygame.time.Clock() # Fonts try: font_lg = pygame.font.SysFont("couriernew", 22, bold=True) font_md = pygame.font.SysFont("couriernew", 16, bold=True) font_sm = pygame.font.SysFont("couriernew", 13) except: font_lg = pygame.font.SysFont(None, 26) font_md = pygame.font.SysFont(None, 18) font_sm = pygame.font.SysFont(None, 14) # ── Helpers ──────────────────────────────────────────────────────────────── def txt(surface, text, x, y, color, fnt=None): fnt = fnt or font_md surface.blit(fnt.render(str(text), True, color), (x, y)) def draw_bar(surf, x, y, w, h, val, mx, color, bg=(60,60,60)): pygame.draw.rect(surf, bg, (x, y, w, h)) fill = int(w * max(0, val) / max(1, mx)) pygame.draw.rect(surf, color, (x, y, fill, h)) pygame.draw.rect(surf, C["white"], (x, y, w, h), 1) def panel(surf, x, y, w, h, border=True): pygame.draw.rect(surf, C["panel"], (x, y, w, h)) if border: pygame.draw.rect(surf, C["border"], (x, y, w, h), 2) def flash_color(base, t, amp=40): v = int(amp * abs(math.sin(t * 4))) return tuple(min(255, c + v) for c in base) # ── Pixel Art Drawing Helpers ────────────────────────────────────────────── def draw_tree(surf, x, y): pygame.draw.rect(surf, C["trunk"], (x+12, y+28, 8, 12)) pygame.draw.polygon(surf, C["tree"], [(x+16,y),(x+2,y+28),(x+30,y+28)]) pygame.draw.polygon(surf, C["tree_top"], [(x+16,y+4),(x+4,y+24),(x+28,y+24)]) def draw_house(surf, x, y, color_w=None, color_r=None): cw = color_w or C["house_w"] cr = color_r or C["house_r"] pygame.draw.rect(surf, cw, (x, y+16, 48, 32)) pygame.draw.polygon(surf, cr, [(x,y+16),(x+24,y),(x+48,y+16)]) pygame.draw.rect(surf, C["door"], (x+18, y+32, 12, 16)) pygame.draw.rect(surf, C["window"], (x+4, y+22, 10, 10)) pygame.draw.rect(surf, C["window"], (x+34, y+22, 10, 10)) def draw_store(surf, x, y): draw_house(surf, x, y, C["store_w"], C["store_r"]) # sign pygame.draw.rect(surf, C["sign"], (x+8, y+12, 32, 8)) txt(surf, "SHOP", x+9, y+12, C["dark"], font_sm) def draw_player(surf, x, y, frame=0, facing=0): # Simple cute character bob = int(math.sin(frame * 0.3) * 2) bx, by = x, y + bob # body pygame.draw.rect(surf, C["blue"], (bx+5, by+10, 12, 12)) # head pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12)) # hair pygame.draw.rect(surf, C["orange"], (bx+4, by+2, 14, 5)) # eyes ex = bx+7 if facing != 2 else bx+11 pygame.draw.rect(surf, C["dark"], (ex, by+6, 2, 2)) pygame.draw.rect(surf, C["dark"], (ex+4, by+6, 2, 2)) # legs lx = bx+5 + (2 if frame % 20 < 10 else -1) pygame.draw.rect(surf, C["dark"], (lx, by+22, 4, 6)) pygame.draw.rect(surf, C["dark"], (lx+6, by+22, 4, 6)) def draw_npc_mitsuha(surf, x, y, frame=0): bob = int(math.sin(frame * 0.2) * 1.5) bx, by = x, y + bob pygame.draw.rect(surf, C["pink"], (bx+5, by+10, 12, 12)) pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12)) pygame.draw.rect(surf, C["purple"], (bx+4, by+2, 14, 5)) # ponytail pygame.draw.rect(surf, C["purple"], (bx+14, by+4, 4, 10)) pygame.draw.rect(surf, C["dark"], (bx+7, by+6, 2, 2)) pygame.draw.rect(surf, C["dark"], (bx+11, by+6, 2, 2)) pygame.draw.rect(surf, C["pink"], (bx+5, by+22, 4, 6)) pygame.draw.rect(surf, C["pink"], (bx+11, by+22, 4, 6)) def draw_slime(surf, x, y, frame=0, color=None): c = color or C["green"] bounce = int(abs(math.sin(frame * 0.15)) * 3) bx, by = x, y - bounce # body pygame.draw.ellipse(surf, c, (bx+2, by+10, 20, 16)) pygame.draw.ellipse(surf, tuple(min(255,v+40) for v in c), (bx+4, by+10, 16, 10)) # eyes pygame.draw.rect(surf, C["white"], (bx+6, by+13, 4, 4)) pygame.draw.rect(surf, C["white"], (bx+14, by+13, 4, 4)) pygame.draw.rect(surf, C["dark"], (bx+7, by+14, 2, 2)) pygame.draw.rect(surf, C["dark"], (bx+15, by+14, 2, 2)) def draw_fairy(surf, x, y, frame=0): bob = int(math.sin(frame * 0.2) * 4) bx, by = x, y + bob # wings wc = (200, 220, 255, 180) wing_surf = pygame.Surface((24, 14), pygame.SRCALPHA) pygame.draw.ellipse(wing_surf, (200,220,255,160), (0,0,10,10)) pygame.draw.ellipse(wing_surf, (200,220,255,160), (14,2,10,8)) surf.blit(wing_surf, (bx+4, by+8)) # body pygame.draw.circle(surf, C["yellow"], (bx+12, by+12), 7) pygame.draw.circle(surf, C["white"], (bx+12, by+12), 5) pygame.draw.rect(surf, C["dark"], (bx+10, by+10, 2, 2)) pygame.draw.rect(surf, C["dark"], (bx+13, by+10, 2, 2)) # ── Game Data ────────────────────────────────────────────────────────────── ITEMS = { "Potion": {"desc": "Restores 30 HP", "price": 50, "effect": ("hp", 30)}, "Hi-Potion": {"desc": "Restores 80 HP", "price": 120, "effect": ("hp", 80)}, "Ether": {"desc": "Restores 20 MP", "price": 60, "effect": ("mp", 20)}, "Elixir": {"desc": "Restores all HP", "price": 300, "effect": ("hp", 9999)}, } MONSTERS = [ {"name": "Green Slime", "hp": 20, "mp": 0, "atk": 5, "def": 2, "xp": 10, "gold": 8, "color": C["green"]}, {"name": "Blue Slime", "hp": 28, "mp": 0, "atk": 6, "def": 3, "xp": 14, "gold": 10, "color": C["blue"]}, {"name": "Pink Fairy", "hp": 18, "mp": 15, "atk": 8, "def": 1, "xp": 18, "gold": 14, "color": C["pink"]}, {"name": "Forest Wisp", "hp": 35, "mp": 20, "atk": 10, "def": 4, "xp": 25, "gold": 20, "color": C["purple"]}, ] MITSUHA_LINES = [ "Hi there! Beautiful day, isn't it?", "I heard there are cute little\nmonsters in the forest!", "Be careful out there, okay?\nBut also have fun!", "The shop has great potions~", "I love living in this village!", "Oh! You look stronger today!", ] HOME_LINES = [ "A cozy home. You feel rested!", "Sweet dreams are made of these~", "Home sweet home!", ] # ── Player ───────────────────────────────────────────────────────────────── class Player: def __init__(self): self.name = "Hero" self.x, self.y = 5, 6 self.hp, self.max_hp = 80, 80 self.mp, self.max_mp = 30, 30 self.atk = 15 self.dfn = 5 self.xp, self.xp_next = 0, 50 self.level = 1 self.gold = 100 self.inventory = {"Potion": 2} self.frame = 0 self.facing = 1 # 0=up 1=down 2=left 3=right self.px = self.x * TILE # pixel x for smooth movement self.py = self.y * TILE def gain_xp(self, amount): msgs = [] self.xp += amount while self.xp >= self.xp_next: self.xp -= self.xp_next self.level += 1 self.xp_next = int(self.xp_next * 1.4) self.max_hp += 15 self.hp = self.max_hp self.max_mp += 5 self.mp = self.max_mp self.atk += 3 self.dfn += 1 msgs.append(f"Level UP! Now Lv.{self.level}!") return msgs def add_item(self, item): self.inventory[item] = self.inventory.get(item, 0) + 1 def use_item(self, item): if self.inventory.get(item, 0) <= 0: return False, "No items left!" eff = ITEMS[item]["effect"] self.inventory[item] -= 1 if self.inventory[item] == 0: del self.inventory[item] if eff[0] == "hp": healed = min(eff[1], self.max_hp - self.hp) self.hp = min(self.max_hp, self.hp + eff[1]) return True, f"Restored {healed} HP!" elif eff[0] == "mp": self.mp = min(self.max_mp, self.mp + eff[1]) return True, f"Restored {eff[1]} MP!" return False, "Nothing happened." # ── Maps ─────────────────────────────────────────────────────────────────── # T=town, F=forest TOWN_MAP = [ "WWWWWWWWWWWWWWWWWWWW", "W..................W", "W..H.......S.....tW", "W..................W", "W...........ttt...W", "W....t............W", "W..................W", "W....P.....M......W", "W..................W", "W..........t......W", "W...##.....t......W", "W...##............W", "W.................W", "WWWWWWWWWFWWWWWWWWW", ] FOREST_MAP = [ "TTTTTTTTTTTTTTTTTTTT", "T.t....t....t.....T", "T....t.....t......T", "T.t........t....t.T", "T.....t...........T", "T....t...t........T", "T.........t.......T", "T...t.............T", "T....t....t.......T", "T.................T", "T.....t...........T", "T.....t...........T", "T.................T", "TTTTTTTTTTTTTTTTTTTT", ] # W=wall, H=home, S=store, t=tree, #=water, P=player start, M=Mitsuha, F=forest entrance, T=town exit # ── Camera ───────────────────────────────────────────────────────────────── class Camera: def __init__(self): self.ox = 0 self.oy = 0 def update(self, px, py, map_w, map_h): self.ox = px - W // 2 + TILE // 2 self.oy = py - H // 2 + TILE // 2 self.ox = max(0, min(self.ox, map_w * TILE - W)) self.oy = max(0, min(self.oy, map_h * TILE - H)) def world_to_screen(self, wx, wy): return wx - self.ox, wy - self.oy # ── Battle System ────────────────────────────────────────────────────────── class Battle: def __init__(self, player, monster_data): self.player = player self.mon = dict(monster_data) self.mon_hp = self.mon["hp"] self.mon_max_hp = self.mon["hp"] self.state = "player_turn" # player_turn, enemy_turn, win, lose self.log = [f"A {self.mon['name']} appeared!"] self.frame = 0 self.anim = None # ("shake_player"/"shake_enemy", timer) self.selected = 0 # menu cursor self.menu = "main" # main, items self.item_list = [] self.shake_player = 0 self.shake_enemy = 0 self.result_msgs = [] def add_log(self, msg): self.log.append(msg) if len(self.log) > 4: self.log.pop(0) def player_attack(self): dmg = max(1, self.player.atk - self.mon["def"] + random.randint(-3, 3)) self.mon_hp -= dmg self.add_log(f"You dealt {dmg} damage!") self.shake_enemy = 10 if self.mon_hp <= 0: self.mon_hp = 0 self.end_battle(True) else: self.state = "enemy_turn" def player_magic(self): if self.player.mp < 8: self.add_log("Not enough MP!") return self.player.mp -= 8 dmg = max(1, self.player.atk * 2 - self.mon["def"] + random.randint(-2, 5)) self.mon_hp -= dmg self.add_log(f"Magic! Dealt {dmg} dmg! (-8 MP)") self.shake_enemy = 12 if self.mon_hp <= 0: self.mon_hp = 0 self.end_battle(True) else: self.state = "enemy_turn" def player_defend(self): self.add_log("You brace for impact!") self.player.dfn += 5 self._defending = True self.state = "enemy_turn" def enemy_turn(self): if self.state != "enemy_turn": return dmg = max(1, self.mon["atk"] - self.player.dfn + random.randint(-2, 3)) self.player.hp -= dmg self.add_log(f"{self.mon['name']} dealt {dmg} dmg!") self.shake_player = 10 if hasattr(self, "_defending"): self.player.dfn -= 5 del self._defending if self.player.hp <= 0: self.player.hp = 0 self.end_battle(False) else: self.state = "player_turn" def end_battle(self, won): if won: xp = self.mon["xp"] gold = self.mon["gold"] self.player.gold += gold lvl_msgs = self.player.gain_xp(xp) self.add_log(f"Victory! +{xp} XP, +{gold}G") for m in lvl_msgs: self.add_log(m) self.result_msgs = [f"Victory!", f"+{xp} XP +{gold} Gold"] + lvl_msgs self.state = "win" else: self.player.hp = self.player.max_hp // 4 self.add_log("You were defeated...") self.result_msgs = ["You fainted!", "Recovered at home."] self.state = "lose" def draw(self, surf): self.frame += 1 # Background for yy in range(H): ratio = yy / H r = int(20 + ratio * 20) g = int(10 + ratio * 20) b = int(40 + ratio * 30) pygame.draw.line(surf, (r, g, b), (0, yy), (W, yy)) # Stars random.seed(42) for _ in range(40): sx = random.randint(0, W) sy = random.randint(0, H // 2) br = 150 + int(50 * math.sin(self.frame * 0.05 + sx)) pygame.draw.rect(surf, (br, br, br), (sx, sy, 2, 2)) random.seed() # Ground pygame.draw.rect(surf, (60, 160, 60), (0, H-80, W, 80)) pygame.draw.rect(surf, (40, 120, 40), (0, H-82, W, 4)) # Enemy ex = W * 3 // 4 - 24 ey = H // 2 - 40 sx = int(math.sin(self.shake_enemy * 0.8) * 5) if self.shake_enemy > 0 else 0 self.shake_enemy = max(0, self.shake_enemy - 1) if self.mon["name"] in ("Green Slime", "Blue Slime"): # Draw bigger slime c = self.mon["color"] for _ in range(3): bc = int(abs(math.sin(self.frame * 0.1 + _)) * 3) pygame.draw.ellipse(surf, c, (ex+sx+10, ey+bc+20, 60, 48)) pygame.draw.ellipse(surf, tuple(min(255,v+60) for v in c), (ex+sx+16, ey+24, 46, 30)) pygame.draw.rect(surf, C["white"], (ex+sx+22, ey+32, 10, 10)) pygame.draw.rect(surf, C["white"], (ex+sx+42, ey+32, 10, 10)) pygame.draw.rect(surf, C["dark"], (ex+sx+24, ey+34, 6, 6)) pygame.draw.rect(surf, C["dark"], (ex+sx+44, ey+34, 6, 6)) elif "Fairy" in self.mon["name"]: bob = int(math.sin(self.frame * 0.2) * 6) # Wings pygame.draw.ellipse(surf, (200, 230, 255), (ex+sx-10, ey+bob+30, 30, 20)) pygame.draw.ellipse(surf, (200, 230, 255), (ex+sx+60, ey+bob+30, 30, 20)) pygame.draw.circle(surf, C["yellow"], (ex+sx+40, ey+bob+40), 22) pygame.draw.circle(surf, C["white"], (ex+sx+40, ey+bob+40), 16) pygame.draw.rect(surf, C["dark"], (ex+sx+32, ey+bob+34, 6, 6)) pygame.draw.rect(surf, C["dark"], (ex+sx+44, ey+bob+34, 6, 6)) elif "Wisp" in self.mon["name"]: for ring in range(3): alpha_c = 120 - ring * 30 r2 = 25 + ring * 10 + int(math.sin(self.frame * 0.1 + ring) * 4) pygame.draw.circle(surf, C["purple"], (ex+sx+40, ey+40), r2, 2) pygame.draw.circle(surf, C["purple"], (ex+sx+40, ey+40), 18) pygame.draw.circle(surf, C["white"], (ex+sx+40, ey+40), 10) # HP bar over enemy draw_bar(surf, ex+sx+5, ey-12, 70, 8, self.mon_hp, self.mon_max_hp, C["hp_red"]) txt(surf, self.mon["name"], ex+sx-10, ey-28, C["white"], font_sm) # Player sprite (left side) psx = W // 4 - 12 psy = H // 2 pshake = int(math.sin(self.shake_player * 0.8) * 5) if self.shake_player > 0 else 0 self.shake_player = max(0, self.shake_player - 1) draw_player(surf, psx + pshake, psy, self.frame, 3) # Player stats panel panel(surf, 10, H-130, 180, 120) txt(surf, f"Lv.{self.player.level} {self.player.name}", 18, H-126, C["gold"]) draw_bar(surf, 18, H-108, 160, 10, self.player.hp, self.player.max_hp, C["hp_red"]) txt(surf, f"HP {self.player.hp}/{self.player.max_hp}", 18, H-96, C["white"], font_sm) draw_bar(surf, 18, H-82, 160, 10, self.player.mp, self.player.max_mp, C["mp_blue"]) txt(surf, f"MP {self.player.mp}/{self.player.max_mp}", 18, H-70, C["white"], font_sm) draw_bar(surf, 18, H-56, 160, 8, self.player.xp, self.player.xp_next, C["xp_green"]) txt(surf, f"XP {self.player.xp}/{self.player.xp_next}", 18, H-44, C["white"], font_sm) # Battle log panel(surf, W//2-160, H-130, 320, 70) for i, line in enumerate(self.log[-3:]): txt(surf, line, W//2-152, H-126 + i*22, C["white"], font_sm) # Menu / result if self.state in ("win", "lose"): panel(surf, W//2-120, H//2-60, 240, 120) color = C["gold"] if self.state == "win" else C["red"] txt(surf, self.result_msgs[0], W//2-80, H//2-50, color) for i, m in enumerate(self.result_msgs[1:]): txt(surf, m, W//2-100, H//2-20 + i*22, C["white"], font_sm) txt(surf, "Press E to continue", W//2-90, H//2+55, C["gray"], font_sm) elif self.state == "player_turn": if self.menu == "main": panel(surf, W-200, H-130, 190, 120) options = ["Attack", "Magic (8MP)", "Defend", "Items"] for i, opt in enumerate(options): color = C["yellow"] if i == self.selected else C["white"] prefix = "► " if i == self.selected else " " txt(surf, prefix + opt, W-192, H-122 + i*26, color) elif self.menu == "items": panel(surf, W-220, H-150, 210, 140) txt(surf, "Items:", W-212, H-146, C["gold"]) if not self.item_list: txt(surf, "No items!", W-200, H-120, C["gray"]) else: for i, (name, qty) in enumerate(self.item_list): color = C["yellow"] if i == self.selected else C["white"] prefix = "► " if i == self.selected else " " txt(surf, f"{prefix}{name} x{qty}", W-212, H-120 + i*24, color, font_sm) txt(surf, "ESC=Back", W-212, H-18, C["gray"], font_sm) def handle_input(self, event): if event.type != pygame.KEYDOWN: return None if self.state in ("win", "lose"): if event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): return self.state return None if self.state != "player_turn": return None if self.menu == "main": if event.key == pygame.K_UP: self.selected = (self.selected - 1) % 4 elif event.key == pygame.K_DOWN: self.selected = (self.selected + 1) % 4 elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): if self.selected == 0: self.player_attack() elif self.selected == 1: self.player_magic() elif self.selected == 2: self.player_defend() # enemy attacks immediately pygame.time.wait(400) self.enemy_turn() elif self.selected == 3: self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS] self.selected = 0 self.menu = "items" elif self.menu == "items": if event.key == pygame.K_ESCAPE: self.menu = "main" self.selected = 0 elif event.key == pygame.K_UP: self.selected = max(0, self.selected - 1) elif event.key == pygame.K_DOWN: self.selected = min(len(self.item_list)-1, self.selected + 1) elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): if self.item_list: name, _ = self.item_list[self.selected] ok, msg = self.player.use_item(name) self.add_log(msg) self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS] if ok: self.menu = "main" self.selected = 0 self.state = "enemy_turn" # Enemy responds if state changed if self.state == "enemy_turn": pygame.time.wait(300) self.enemy_turn() return None # ── Shop ─────────────────────────────────────────────────────────────────── class Shop: def __init__(self, player): self.player = player self.items = list(ITEMS.keys()) self.selected = 0 def draw(self, surf): panel(surf, W//2-180, 60, 360, 360) txt(surf, "= ITEM SHOP =", W//2-100, 72, C["gold"]) txt(surf, f"Gold: {self.player.gold}G", W//2+60, 72, C["yellow"], font_sm) pygame.draw.line(surf, C["border"], (W//2-175, 95), (W//2+175, 95), 1) for i, name in enumerate(self.items): info = ITEMS[name] y = 105 + i * 58 color = C["yellow"] if i == self.selected else C["white"] prefix = "► " if i == self.selected else " " txt(surf, prefix + name, W//2-170, y, color) txt(surf, info["desc"], W//2-155, y+20, C["gray"], font_sm) txt(surf, f"{info['price']}G", W//2+120, y, C["gold"], font_sm) txt(surf, "E=Buy ESC=Leave", W//2-80, 380, C["gray"], font_sm) def handle_input(self, event): if event.type != pygame.KEYDOWN: return False if event.key == pygame.K_ESCAPE: return True # close if event.key == pygame.K_UP: self.selected = (self.selected - 1) % len(self.items) elif event.key == pygame.K_DOWN: self.selected = (self.selected + 1) % len(self.items) elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): name = self.items[self.selected] price = ITEMS[name]["price"] if self.player.gold >= price: self.player.gold -= price self.player.add_item(name) # else: not enough gold (silent) return False # ── Dialogue ─────────────────────────────────────────────────────────────── class Dialogue: def __init__(self, lines): self.lines = lines if isinstance(lines, list) else [lines] self.idx = 0 @property def done(self): return self.idx >= len(self.lines) def advance(self): self.idx += 1 def draw(self, surf): if self.done: return panel(surf, 30, H-110, W-60, 100) line = self.lines[self.idx] for i, l in enumerate(line.split("\n")): txt(surf, l, 46, H-100 + i*22, C["white"]) txt(surf, "▼ E", W-70, H-24, C["gray"], font_sm) # ── World Renderer ───────────────────────────────────────────────────────── def tile_walkable(tile): return tile not in ("W", "t", "#", "T") def render_map(surf, map_data, cam, frame): rows = len(map_data) cols = len(map_data[0]) for row in range(rows): for col in range(col_count := cols): tile = map_data[row][col] if col < len(map_data[row]) else " " wx = col * TILE wy = row * TILE sx, sy = cam.world_to_screen(wx, wy) if sx > W+TILE or sy > H+TILE or sx < -TILE or sy < -TILE: continue # Base tile grass_c = C["dark_grass"] if (row + col) % 2 == 0 else C["grass"] pygame.draw.rect(surf, grass_c, (sx, sy, TILE, TILE)) if tile == "W": pygame.draw.rect(surf, (100, 80, 60), (sx, sy, TILE, TILE)) pygame.draw.rect(surf, (80, 60, 40), (sx, sy, TILE, TILE), 2) elif tile == "#": water_off = int(math.sin(frame * 0.05 + col * 0.5) * 2) pygame.draw.rect(surf, C["water"], (sx, sy+water_off, TILE, TILE)) pygame.draw.rect(surf, (100, 200, 255), (sx+4, sy+4+water_off, TILE-8, 4), 1) elif tile == ".": pass # just grass elif tile == "P": pass # just grass (player starts here) elif tile == "M": pass # NPC drawn separately elif tile == "t": draw_tree(surf, sx, sy - 8) elif tile == "F": # Forest entrance pygame.draw.rect(surf, C["dark_grass"], (sx, sy, TILE, TILE)) pygame.draw.rect(surf, C["green"], (sx+4, sy+4, TILE-8, TILE-8), 2) elif tile == "T": # Town exit pygame.draw.rect(surf, C["path"], (sx, sy, TILE, TILE)) elif tile == "H": draw_house(surf, sx-8, sy-16) elif tile == "S": draw_store(surf, sx-8, sy-16) # ── Main Game ────────────────────────────────────────────────────────────── class Game: TOWN = "town" FOREST = "forest" def __init__(self): self.player = Player() self.cam = Camera() self.scene = self.TOWN self.current_map = TOWN_MAP self.dialogue = None self.shop = None self.battle = None self.frame = 0 self.npc_frame = 0 self.message = None # (text, timer) self.monster_encounter_timer = 0 self.encounter_rate = 60 # frames between encounter checks self.mitsuha_dialogue_idx = 0 # Floating particles (sparkles) self.sparks = [] def show_message(self, text, duration=120): self.message = (text, duration) def add_spark(self, x, y, color=None): c = color or random.choice([C["yellow"], C["white"], C["gold"], C["pink"]]) self.sparks.append({ "x": x, "y": y, "vx": random.uniform(-1.5, 1.5), "vy": random.uniform(-3, -0.5), "life": 40, "color": c }) def get_tile(self, tx, ty): m = self.current_map if ty < 0 or ty >= len(m) or tx < 0 or tx >= len(m[ty]): return "W" return m[ty][tx] def try_move(self, dx, dy): nx = self.player.x + dx ny = self.player.y + dy if not tile_walkable(self.get_tile(nx, ny)): return # Check transitions if self.scene == self.TOWN and self.get_tile(nx, ny) == "F": self.scene = self.FOREST self.current_map = FOREST_MAP self.player.x, self.player.y = 10, 12 self.player.px = self.player.x * TILE self.player.py = self.player.y * TILE self.show_message("Entered the Forest!") return if self.scene == self.FOREST and self.get_tile(nx, ny) == "T": self.scene = self.TOWN self.current_map = TOWN_MAP self.player.x, self.player.y = 9, 12 self.player.px = self.player.x * TILE self.player.py = self.player.y * TILE self.show_message("Back in town!") return self.player.x = nx self.player.y = ny self.player.px = nx * TILE self.player.py = ny * TILE if dx > 0: self.player.facing = 3 elif dx < 0: self.player.facing = 2 elif dy > 0: self.player.facing = 1 else: self.player.facing = 0 # Random encounter in forest if self.scene == self.FOREST: if random.random() < 0.2: mon = random.choice(MONSTERS) self.battle = Battle(self.player, mon) def try_interact(self): # Check adjacent tiles for interactive objects fx, fy = self.player.x, self.player.y facing = self.player.facing if facing == 0: fy -= 1 elif facing == 1: fy += 1 elif facing == 2: fx -= 1 elif facing == 3: fx += 1 tile = self.get_tile(fx, fy) # Check if near Mitsuha (in town map) if self.scene == self.TOWN: for row_i, row in enumerate(TOWN_MAP): if "M" in row: mx, my = row.index("M"), row_i if abs(mx - self.player.x) + abs(my - self.player.y) <= 1: line = MITSUHA_LINES[self.mitsuha_dialogue_idx % len(MITSUHA_LINES)] self.mitsuha_dialogue_idx += 1 self.dialogue = Dialogue(line) return if tile == "H": # Home - rest self.player.hp = self.player.max_hp self.player.mp = self.player.max_mp self.dialogue = Dialogue(random.choice(HOME_LINES) + "\n(HP and MP restored!)") for _ in range(12): self.add_spark(self.player.px + random.randint(-20, 20), self.player.py + random.randint(-30, 10), C["gold"]) elif tile == "S": self.shop = Shop(self.player) def update(self): self.frame += 1 self.npc_frame += 1 self.player.frame += 1 # Update sparks for s in self.sparks[:]: s["x"] += s["vx"] s["y"] += s["vy"] s["vy"] += 0.1 s["life"] -= 1 if s["life"] <= 0: self.sparks.remove(s) if self.message: text, timer = self.message self.message = (text, timer - 1) if timer > 0 else None def draw_world(self, surf): surf.fill(C["sky"]) render_map(surf, self.current_map, self.cam, self.frame) # Draw Mitsuha NPC (in town) if self.scene == self.TOWN: for row_i, row in enumerate(TOWN_MAP): if "M" in row: mx, my = row.index("M"), row_i sx, sy = self.cam.world_to_screen(mx * TILE, my * TILE) draw_npc_mitsuha(surf, sx, sy, self.npc_frame) # Name tag panel(surf, sx-4, sy-20, 56, 16) txt(surf, "Mitsuha", sx-2, sy-19, C["pink"], font_sm) # Draw player px, py = self.cam.world_to_screen(self.player.px, self.player.py) draw_player(surf, px, py, self.player.frame, self.player.facing) # Sparks for s in self.sparks: alpha = max(0, s["life"] * 6) c = tuple(min(255, v) for v in s["color"]) pygame.draw.rect(surf, c, (int(s["x"]) - self.cam.ox, int(s["y"]) - self.cam.oy, 3, 3)) def draw_hud(self, surf): # Mini stats panel(surf, 8, 8, 200, 68) txt(surf, f"Lv.{self.player.level} {self.player.name}", 14, 12, C["gold"]) draw_bar(surf, 14, 32, 180, 8, self.player.hp, self.player.max_hp, C["hp_red"]) txt(surf, f"HP {self.player.hp}/{self.player.max_hp}", 14, 42, C["white"], font_sm) draw_bar(surf, 14, 54, 180, 8, self.player.mp, self.player.max_mp, C["mp_blue"]) txt(surf, f"MP {self.player.mp}/{self.player.max_mp}", 14, 64, C["white"], font_sm) # Gold panel(surf, W-140, 8, 132, 26) txt(surf, f"Gold: {self.player.gold}G", W-134, 12, C["gold"]) # Location loc = "** Village **" if self.scene == self.TOWN else "~~ Forest ~~" panel(surf, W//2-60, 8, 120, 26) txt(surf, loc, W//2-52, 12, C["white"], font_sm) # Controls hint txt(surf, "Arrows:Move E:Interact", 10, H-18, C["gray"], font_sm) # Floating message if self.message: text, timer = self.message alpha = min(255, timer * 4) panel(surf, W//2-120, H//2-20, 240, 40) fc = flash_color(C["white"], self.frame * 0.02) txt(surf, text, W//2-100, H//2-12, fc) def run(self): running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: if self.dialogue: self.dialogue = None elif self.shop: self.shop = None elif self.battle: pass # can't escape battle easily else: running = False # Battle input if self.battle: result = self.battle.handle_input(event) if result in ("win", "lose"): if result == "lose": self.scene = self.TOWN self.current_map = TOWN_MAP self.player.x, self.player.y = 5, 6 self.player.px = self.player.x * TILE self.player.py = self.player.y * TILE self.battle = None continue # Shop input if self.shop: if self.shop.handle_input(event): self.shop = None continue # Dialogue if self.dialogue: if event.type == pygame.KEYDOWN and event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): self.dialogue.advance() if self.dialogue.done: self.dialogue = None continue # Movement if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: self.try_move(0, -1) elif event.key == pygame.K_DOWN: self.try_move(0, 1) elif event.key == pygame.K_LEFT: self.try_move(-1, 0) elif event.key == pygame.K_RIGHT: self.try_move(1, 0) elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN): self.try_interact() self.update() self.cam.update(self.player.px, self.player.py, len(self.current_map[0]), len(self.current_map)) if self.battle: self.battle.draw(screen) else: self.draw_world(screen) self.draw_hud(screen) if self.shop: self.shop.draw(screen) if self.dialogue: self.dialogue.draw(screen) pygame.display.flip() clock.tick(FPS) pygame.quit() sys.exit() # ── Title Screen ─────────────────────────────────────────────────────────── def title_screen(): frame = 0 while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit(); sys.exit() if event.type == pygame.KEYDOWN: return frame += 1 screen.fill(C["bg"]) # Stars random.seed(7) for _ in range(60): sx = random.randint(0, W) sy = random.randint(0, H) br = 100 + int(80 * math.sin(frame * 0.03 + sx * 0.1)) pygame.draw.rect(screen, (br, br, int(br*0.8)), (sx, sy, 2, 2)) random.seed() # Animated title shimmer tc = flash_color(C["gold"], frame * 0.04, 30) title = font_lg.render(">> Pixel Village <<", True, tc) screen.blit(title, (W//2 - title.get_width()//2, 100)) sub = font_md.render("~ A Cozy Little JRPG ~", True, C["pink"]) screen.blit(sub, (W//2 - sub.get_width()//2, 140)) # Demo sprites draw_player(screen, W//2-80, 230, frame) draw_npc_mitsuha(screen, W//2-20, 230, frame) draw_slime(screen, W//2+60, 240, frame, C["green"]) draw_fairy(screen, W//2+100, 230, frame) # Features features = [ "[H] Cozy village with shop & home", "[?] Chat with neighbor Mitsuha", "[T] Forest full of cute monsters", "[!] Turn-based combat", "[*] Level up your hero!", ] for i, f in enumerate(features): fc = C["white"] if i % 2 == 0 else C["gray"] fs = font_sm.render(f, True, fc) screen.blit(fs, (W//2 - fs.get_width()//2, 310 + i*20)) blink = int(frame / 30) % 2 == 0 if blink: ps = font_md.render("Press any key to start!", True, C["yellow"]) screen.blit(ps, (W//2 - ps.get_width()//2, 420)) pygame.display.flip() clock.tick(FPS) if __name__ == "__main__": title_screen() game = Game() game.run()