Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |
| } | |
| 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) | |
| 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) | |
| 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) | |