""" Pixel Art Sprite Generator - Charmander-style blocky pixel art Tiny, blocky, simple rectangles - matches charmander.png reference Based on draw_charmander_test method """ import numpy as np import math import random from PIL import Image, ImageDraw from typing import Tuple from .sprite_generator_base import SpriteGeneratorBase from .sprite_generator import Sprite from util import PokemonColorPalette class PixelArtSpriteGenerator(SpriteGeneratorBase): """Pixel art sprite generator - Charmander-style blocky pixel art""" # Pure black outline color OUTLINE_COLOR = (0, 0, 0) def generate_smurf_sprites(self, color_tint: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = None, size_scale: float = 1.0) -> dict: """Generate pixel art style dwarf humanoid monster sprites""" from entities.enums import AnimationType # Generate random color pair if not provided - use Pokemon color palette if color_tint is None: color_tint = PokemonColorPalette.get_random_color_pair() main_color, accent_color = color_tint return { AnimationType.IDLE.value: self._gen_smurf_idle(main_color, accent_color, size_scale), AnimationType.WALK.value: self._gen_smurf_walk(main_color, accent_color, size_scale), AnimationType.GATHER.value: self._gen_smurf_gather(main_color, accent_color, size_scale), AnimationType.HAPPY.value: self._gen_smurf_happy(main_color, accent_color, size_scale), AnimationType.ANGRY.value: self._gen_smurf_angry(main_color, accent_color, size_scale), AnimationType.CHAT.value: self._gen_smurf_chat(main_color, accent_color, size_scale), AnimationType.PLAY.value: self._gen_smurf_play(main_color, accent_color, size_scale) } @staticmethod def _quantize_color(color: Tuple[int, int, int], levels: int = 8) -> Tuple[int, int, int]: """Quantize color to pixel art style""" return tuple((c // levels) * levels for c in color) @staticmethod def _draw_character(draw: ImageDraw, x: int, y: int, scale: float, body_color: Tuple[int, int, int], belly_color: Tuple[int, int, int], expression: str = 'neutral', x_offset: int = 0, y_offset: int = 0, leg_offset_left: int = 0, leg_offset_right: int = 0, arm_offset_left: int = 0, arm_offset_right: int = 0): """ Draw a charmander-style character - TINY pixel art style Based on draw_charmander_test - simple blocky pixels, not smooth shapes! Args: draw: ImageDraw object x, y: Base position scale: Scale factor body_color: RGB color for body (main color) belly_color: RGB color for belly (accent color) expression: 'neutral', 'happy', 'angry', 'chatting' x_offset, y_offset: Position offsets for animation leg_offset_left, leg_offset_right: Leg position offsets for walking arm_offset_left, arm_offset_right: Arm position offsets for animation """ s = scale black = PixelArtSpriteGenerator.OUTLINE_COLOR # Quantize colors for pixel art style body_color = PixelArtSpriteGenerator._quantize_color(body_color) belly_color = PixelArtSpriteGenerator._quantize_color(belly_color) # Head (large, blocky) - top 2/3 of sprite # Original: 10x8 at scale 4, so base is 10x8 head_x = x + int(x_offset * s) head_y = y + int(y_offset * s) head_w = int(10 * s) head_h = int(8 * s) draw.rectangle([head_x, head_y, head_x + head_w, head_y + head_h], fill=body_color, outline=black, width=1) # Eyes - just TWO BLACK PIXELS, not white circles! eye_size = int(1 * s) eye_y = head_y + int(3 * s) if expression == 'angry': # Angry eyes - slightly closer together, lower eye_y = head_y + int(4 * s) draw.rectangle([head_x + int(3*s), eye_y, head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black) draw.rectangle([head_x + int(6*s), eye_y, head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black) # Angry eyebrows (blocky rectangles above eyes) draw.rectangle([head_x + int(2*s), head_y + int(2*s), head_x + int(4*s), head_y + int(3*s)], fill=black) draw.rectangle([head_x + int(6*s), head_y + int(2*s), head_x + int(8*s), head_y + int(3*s)], fill=black) elif expression == 'happy': # Happy eyes - normal position draw.rectangle([head_x + int(3*s), eye_y, head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black) draw.rectangle([head_x + int(6*s), eye_y, head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black) # Happy mouth (blocky U-shape) mouth_y = head_y + int(6 * s) # Base of smile draw.rectangle([head_x + int(2*s), mouth_y, head_x + int(8*s), mouth_y + int(1*s)], fill=black) # Left curve draw.rectangle([head_x + int(1*s), mouth_y + int(1*s), head_x + int(2*s), mouth_y + int(2*s)], fill=black) # Right curve draw.rectangle([head_x + int(8*s), mouth_y + int(1*s), head_x + int(9*s), mouth_y + int(2*s)], fill=black) elif expression == 'chatting': # Normal eyes draw.rectangle([head_x + int(3*s), eye_y, head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black) draw.rectangle([head_x + int(6*s), eye_y, head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black) # Chatting mouth (open - blocky rectangle) mouth_y = head_y + int(5 * s) draw.rectangle([head_x + int(4*s), mouth_y, head_x + int(6*s), mouth_y + int(2*s)], fill=black) else: # neutral # Normal eyes draw.rectangle([head_x + int(3*s), eye_y, head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black) draw.rectangle([head_x + int(6*s), eye_y, head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black) # Simple mouth (horizontal line) mouth_y = head_y + int(6 * s) draw.rectangle([head_x + int(3*s), mouth_y, head_x + int(7*s), mouth_y + int(1*s)], fill=black) # Body (smaller, with lighter belly) body_x = head_x + int(1 * s) + int(x_offset * s) body_y = head_y + int(8 * s) + int(y_offset * s) body_w = int(8 * s) body_h = int(6 * s) draw.rectangle([body_x, body_y, body_x + body_w, body_y + body_h], fill=body_color, outline=black, width=1) # Belly patch (lighter color) belly_x = body_x + int(2 * s) belly_y = body_y + int(3 * s) belly_w = int(4 * s) belly_h = int(3 * s) draw.rectangle([belly_x, belly_y, belly_x + belly_w, belly_y + belly_h], fill=belly_color) # Arms (tiny, with offsets for animation) # Left arm arm_left_x1 = body_x - int(2*s) + int(arm_offset_left * s) arm_left_x2 = body_x + int(arm_offset_left * s) draw.rectangle([arm_left_x1, body_y + int(1*s), arm_left_x2, body_y + int(3*s)], fill=body_color, outline=black, width=1) # Right arm arm_right_x1 = body_x + body_w + int(arm_offset_right * s) arm_right_x2 = body_x + body_w + int(2*s) + int(arm_offset_right * s) draw.rectangle([arm_right_x1, body_y + int(1*s), arm_right_x2, body_y + int(3*s)], fill=body_color, outline=black, width=1) # Legs (tiny, with offsets for walking) leg_y = body_y + body_h # Left leg leg_left_x1 = body_x + int(1*s) + int(leg_offset_left * s) leg_left_x2 = body_x + int(3*s) + int(leg_offset_left * s) leg_left_y2 = leg_y + int(2*s) + int(leg_offset_left * s) draw.rectangle([leg_left_x1, leg_y, leg_left_x2, leg_left_y2], fill=body_color, outline=black, width=1) # Right leg leg_right_x1 = body_x + int(5*s) + int(leg_offset_right * s) leg_right_x2 = body_x + int(7*s) + int(leg_offset_right * s) leg_right_y2 = leg_y + int(2*s) + int(leg_offset_right * s) draw.rectangle([leg_right_x1, leg_y, leg_right_x2, leg_right_y2], fill=body_color, outline=black, width=1) def _gen_smurf_idle(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Idle animation - slight breathing""" frames = [] base_size = int(32 * scale) for i in range(2): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Slight vertical breathing motion y_offset = 0.3 if i == 0 else 0 # Center character on 32x32 canvas # Character base size is ~10x14, so center it char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) + int(y_offset * scale) self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral', y_offset=int(y_offset)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=15) def _gen_smurf_walk(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Walking animation""" frames = [] base_size = int(32 * scale) for i in range(4): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Walking cycle: legs alternate, slight body bob leg_cycle = i % 2 leg_offset = 1.5 if leg_cycle == 0 else -1.5 y_bob = 0.5 if i % 2 == 0 else 0 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) + int(y_bob * scale) # Alternate leg positions leg_left = leg_offset if i < 2 else -leg_offset leg_right = -leg_offset if i < 2 else leg_offset self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral', y_offset=int(y_bob), leg_offset_left=int(leg_left), leg_offset_right=int(leg_right)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=8) def _gen_smurf_gather(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Gathering animation - bending down""" frames = [] base_size = int(32 * scale) for i in range(4): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Bend down progressively bend = i * 1.2 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) + int(bend * scale) # Arms reach down arm_offset = i * 0.5 self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral', y_offset=int(bend), arm_offset_left=int(arm_offset), arm_offset_right=int(arm_offset)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=10) def _gen_smurf_happy(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Happy animation - jumping""" frames = [] base_size = int(32 * scale) for i in range(6): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Jump up and down jump = abs(math.sin(i * math.pi / 3)) * 3 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) - int(jump * scale) self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'happy', y_offset=-int(jump)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=5) def _gen_smurf_angry(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Angry animation - shaking""" frames = [] base_size = int(32 * scale) # Red-tint the colors when angry angry_main = (min(255, main_color[0] + 40), max(0, main_color[1] - 20), max(0, main_color[2] - 20)) angry_accent = (min(255, accent_color[0] + 40), max(0, accent_color[1] - 20), max(0, accent_color[2] - 20)) for i in range(4): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Shake left and right shake = (i % 2) * 2 - 1 # -1 or 1 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) self._draw_character(draw, char_x, char_y, scale, angry_main, angry_accent, 'angry', x_offset=shake) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=8) def _gen_smurf_chat(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Chatting animation""" frames = [] base_size = int(32 * scale) for i in range(4): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Slight nod nod = (i % 2) * 0.5 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) + int(nod * scale) # Expression alternates between neutral and chatting (mouth open/closed) expression = 'chatting' if i % 2 == 0 else 'neutral' self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, expression, y_offset=int(nod)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=12) def _gen_smurf_play(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite: """Playing animation - bouncing""" frames = [] base_size = int(32 * scale) for i in range(6): img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Bounce up and down bounce = abs(math.sin(i * math.pi / 3)) * 2.5 # Center character char_x = int((base_size - 10 * scale) / 2) char_y = int((base_size - 14 * scale) / 2) - int(bounce * scale) self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'happy', y_offset=-int(bounce)) frames.append(np.array(img)) return Sprite(frames=frames, frame_speed=10) @staticmethod def draw_charmander_test(img: Image, draw: ImageDraw, x: int, y: int, scale: int = 4): """ Draw a static charmander-like monster - TINY pixel art style (10x14 pixels) Mimics charmander.png exactly - simple blocky pixels, not smooth shapes! """ s = scale orange = (255, 140, 0) yellow = (255, 220, 0) black = (0, 0, 0) brown = (139, 69, 19) # Head (large, blocky, orange) - top 2/3 of sprite # Draw as rectangles/pixels, not ellipses! # This should be maybe 8-10 pixels wide, 8-10 pixels tall head_x = x head_y = y head_w = int(10 * s) head_h = int(8 * s) draw.rectangle([head_x, head_y, head_x + head_w, head_y + head_h], fill=orange, outline=black, width=1) # Eyes - just TWO BLACK PIXELS, not white circles! eye_size = int(1 * s) eye_y = head_y + int(3 * s) draw.rectangle([head_x + int(3*s), eye_y, head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black) draw.rectangle([head_x + int(6*s), eye_y, head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black) # Body (smaller, orange with yellow belly) body_x = head_x + int(1 * s) body_y = head_y + int(8 * s) body_w = int(8 * s) body_h = int(6 * s) draw.rectangle([body_x, body_y, body_x + body_w, body_y + body_h], fill=orange, outline=black, width=1) # Yellow belly patch belly_x = body_x + int(2 * s) belly_y = body_y + int(3 * s) belly_w = int(4 * s) belly_h = int(3 * s) draw.rectangle([belly_x, belly_y, belly_x + belly_w, belly_y + belly_h], fill=yellow) # Arms (tiny, orange) # Left arm draw.rectangle([body_x - int(2*s), body_y + int(1*s), body_x, body_y + int(3*s)], fill=orange, outline=black, width=1) # Right arm draw.rectangle([body_x + body_w, body_y + int(1*s), body_x + body_w + int(2*s), body_y + int(3*s)], fill=orange, outline=black, width=1) # Legs (tiny, orange) leg_y = body_y + body_h draw.rectangle([body_x + int(1*s), leg_y, body_x + int(3*s), leg_y + int(2*s)], fill=orange, outline=black, width=1) draw.rectangle([body_x + int(5*s), leg_y, body_x + int(7*s), leg_y + int(2*s)], fill=orange, outline=black, width=1) # Brown pixel on right side (tail detail) draw.rectangle([body_x + body_w + int(1*s), body_y + int(4*s), body_x + body_w + int(2*s), body_y + int(5*s)], fill=brown)