From 97a27f9a426bc5730817ffcb7091faf437d9e890 Mon Sep 17 00:00:00 2001 From: Gabrieal Jimmy Date: Thu, 1 Oct 2026 15:15:20 -0500 Subject: [PATCH] init --- .gitignore | 36 + AI_PIXEL_ART_GUIDE.md | 108 +++ README.md | 66 ++ SPRITES.md | 319 +++++++++ build_parts.py | 172 +++++ business_pokemon_type.py | 76 ++ generate_sprite.py | 729 ++++++++++++++++++++ legacy/parts/body/avian_perched.png | Bin 0 -> 418 bytes legacy/parts/body/avian_round.png | Bin 0 -> 333 bytes legacy/parts/body/biped_bulky.png | Bin 0 -> 617 bytes legacy/parts/body/biped_small.png | Bin 0 -> 367 bytes legacy/parts/body/biped_tall.png | Bin 0 -> 573 bytes legacy/parts/body/blob_drip.png | Bin 0 -> 535 bytes legacy/parts/body/blob_floating.png | Bin 0 -> 480 bytes legacy/parts/body/blob_round.png | Bin 0 -> 356 bytes legacy/parts/body/fish_body.png | Bin 0 -> 556 bytes legacy/parts/body/insect_beetle.png | Bin 0 -> 529 bytes legacy/parts/body/insect_larva.png | Bin 0 -> 522 bytes legacy/parts/body/insect_mantis.png | Bin 0 -> 532 bytes legacy/parts/body/quad_long.png | Bin 0 -> 379 bytes legacy/parts/body/quad_slim.png | Bin 0 -> 393 bytes legacy/parts/body/quad_stocky.png | Bin 0 -> 433 bytes legacy/parts/body/seal_body.png | Bin 0 -> 381 bytes legacy/parts/body/serpent_coiled.png | Bin 0 -> 608 bytes legacy/parts/body/serpent_rising.png | Bin 0 -> 662 bytes legacy/parts/eyes/eyes_angry.png | Bin 0 -> 145 bytes legacy/parts/eyes/eyes_beady.png | Bin 0 -> 106 bytes legacy/parts/eyes/eyes_big_shiny.png | Bin 0 -> 157 bytes legacy/parts/eyes/eyes_compound.png | Bin 0 -> 160 bytes legacy/parts/eyes/eyes_cyclops.png | Bin 0 -> 138 bytes legacy/parts/eyes/eyes_glowing.png | Bin 0 -> 137 bytes legacy/parts/eyes/eyes_happy_closed.png | Bin 0 -> 100 bytes legacy/parts/eyes/eyes_masked.png | Bin 0 -> 181 bytes legacy/parts/eyes/eyes_round.png | Bin 0 -> 148 bytes legacy/parts/eyes/eyes_sharp.png | Bin 0 -> 126 bytes legacy/parts/eyes/eyes_sleepy.png | Bin 0 -> 133 bytes legacy/parts/head/head_bird.png | Bin 0 -> 251 bytes legacy/parts/head/head_bulky.png | Bin 0 -> 335 bytes legacy/parts/head/head_canine.png | Bin 0 -> 298 bytes legacy/parts/head/head_feline.png | Bin 0 -> 300 bytes legacy/parts/head/head_flat.png | Bin 0 -> 277 bytes legacy/parts/head/head_ghost.png | Bin 0 -> 313 bytes legacy/parts/head/head_insect.png | Bin 0 -> 246 bytes legacy/parts/head/head_reptile.png | Bin 0 -> 316 bytes legacy/parts/head/head_rock.png | Bin 0 -> 329 bytes legacy/parts/head/head_round.png | Bin 0 -> 269 bytes legacy/parts/snout/beak_duck.png | Bin 0 -> 115 bytes legacy/parts/snout/beak_hooked.png | Bin 0 -> 127 bytes legacy/parts/snout/beak_short.png | Bin 0 -> 120 bytes legacy/parts/snout/fangs.png | Bin 0 -> 136 bytes legacy/parts/snout/fish_lips.png | Bin 0 -> 139 bytes legacy/parts/snout/mandibles.png | Bin 0 -> 159 bytes legacy/parts/snout/muzzle_long.png | Bin 0 -> 156 bytes legacy/parts/snout/muzzle_small.png | Bin 0 -> 137 bytes legacy/parts/snout/reptile_jaw.png | Bin 0 -> 157 bytes legacy/parts/snout/snout_pig.png | Bin 0 -> 132 bytes legacy/parts/snout/tongue_out.png | Bin 0 -> 148 bytes legacy/parts/snout/trunk_short.png | Bin 0 -> 174 bytes legacy/parts/snout/tusks.png | Bin 0 -> 153 bytes legacy/parts/snout/whiskers.png | Bin 0 -> 157 bytes legacy/parts_src/body/avian_perched.txt | 32 + legacy/parts_src/body/avian_round.txt | 28 + legacy/parts_src/body/biped_bulky.txt | 39 ++ legacy/parts_src/body/biped_small.txt | 31 + legacy/parts_src/body/biped_tall.txt | 45 ++ legacy/parts_src/body/blob_drip.txt | 38 + legacy/parts_src/body/blob_floating.txt | 40 ++ legacy/parts_src/body/blob_round.txt | 31 + legacy/parts_src/body/fish_body.txt | 35 + legacy/parts_src/body/insect_beetle.txt | 30 + legacy/parts_src/body/insect_larva.txt | 25 + legacy/parts_src/body/insect_mantis.txt | 35 + legacy/parts_src/body/quad_long.txt | 20 + legacy/parts_src/body/quad_slim.txt | 20 + legacy/parts_src/body/quad_stocky.txt | 26 + legacy/parts_src/body/seal_body.txt | 27 + legacy/parts_src/body/serpent_coiled.txt | 36 + legacy/parts_src/body/serpent_rising.txt | 47 ++ legacy/parts_src/eyes/eyes_angry.txt | 7 + legacy/parts_src/eyes/eyes_beady.txt | 3 + legacy/parts_src/eyes/eyes_big_shiny.txt | 9 + legacy/parts_src/eyes/eyes_compound.txt | 8 + legacy/parts_src/eyes/eyes_cyclops.txt | 9 + legacy/parts_src/eyes/eyes_glowing.txt | 7 + legacy/parts_src/eyes/eyes_happy_closed.txt | 3 + legacy/parts_src/eyes/eyes_masked.txt | 8 + legacy/parts_src/eyes/eyes_round.txt | 8 + legacy/parts_src/eyes/eyes_sharp.txt | 6 + legacy/parts_src/eyes/eyes_sleepy.txt | 5 + legacy/parts_src/head/head_bird.txt | 21 + legacy/parts_src/head/head_bulky.txt | 24 + legacy/parts_src/head/head_canine.txt | 22 + legacy/parts_src/head/head_feline.txt | 22 + legacy/parts_src/head/head_flat.txt | 18 + legacy/parts_src/head/head_ghost.txt | 22 + legacy/parts_src/head/head_insect.txt | 20 + legacy/parts_src/head/head_reptile.txt | 20 + legacy/parts_src/head/head_rock.txt | 22 + legacy/parts_src/head/head_round.txt | 22 + legacy/parts_src/snout/beak_duck.txt | 6 + legacy/parts_src/snout/beak_hooked.txt | 7 + legacy/parts_src/snout/beak_short.txt | 6 + legacy/parts_src/snout/fangs.txt | 10 + legacy/parts_src/snout/fish_lips.txt | 9 + legacy/parts_src/snout/mandibles.txt | 10 + legacy/parts_src/snout/muzzle_long.txt | 9 + legacy/parts_src/snout/muzzle_small.txt | 7 + legacy/parts_src/snout/reptile_jaw.txt | 10 + legacy/parts_src/snout/snout_pig.txt | 8 + legacy/parts_src/snout/tongue_out.txt | 11 + legacy/parts_src/snout/trunk_short.txt | 14 + legacy/parts_src/snout/tusks.txt | 9 + legacy/parts_src/snout/whiskers.txt | 7 + parts/aura/aura_bubbles.png | Bin 0 -> 524 bytes parts/aura/aura_dust.png | Bin 0 -> 765 bytes parts/aura/aura_embers.png | Bin 0 -> 491 bytes parts/aura/aura_gusts.png | Bin 0 -> 648 bytes parts/aura/aura_leaves.png | Bin 0 -> 654 bytes parts/aura/aura_pebbles.png | Bin 0 -> 786 bytes parts/aura/aura_power.png | Bin 0 -> 696 bytes parts/aura/aura_rings.png | Bin 0 -> 507 bytes parts/aura/aura_snowflakes.png | Bin 0 -> 605 bytes parts/aura/aura_sparkle.png | Bin 0 -> 669 bytes parts/aura/aura_sparks.png | Bin 0 -> 522 bytes parts/aura/aura_spores.png | Bin 0 -> 658 bytes parts/aura/aura_toxic.png | Bin 0 -> 768 bytes parts/aura/aura_wisps.png | Bin 0 -> 659 bytes parts/back/cloud_puffs.png | Bin 0 -> 272 bytes parts/back/coil_spikes.png | Bin 0 -> 254 bytes parts/back/crystal_cluster.png | Bin 0 -> 321 bytes parts/back/dorsal_fin.png | Bin 0 -> 222 bytes parts/back/flame_mane.png | Bin 0 -> 416 bytes parts/back/flower_back.png | Bin 0 -> 451 bytes parts/back/ghost_tatters.png | Bin 0 -> 273 bytes parts/back/icicle_spikes.png | Bin 0 -> 271 bytes parts/back/leaf_cape.png | Bin 0 -> 396 bytes parts/back/mane_fluffy.png | Bin 0 -> 570 bytes parts/back/mushroom_back.png | Bin 0 -> 338 bytes parts/back/plant_bulb.png | Bin 0 -> 376 bytes parts/back/rock_plates.png | Bin 0 -> 387 bytes parts/back/sail_fin.png | Bin 0 -> 318 bytes parts/back/shell_round.png | Bin 0 -> 428 bytes parts/back/shell_spiked.png | Bin 0 -> 460 bytes parts/back/spikes_back.png | Bin 0 -> 277 bytes parts/back/wings_bat.png | Bin 0 -> 483 bytes parts/back/wings_bug.png | Bin 0 -> 497 bytes parts/back/wings_dragon.png | Bin 0 -> 582 bytes parts/back/wings_feather.png | Bin 0 -> 482 bytes parts/back/wings_feather_small.png | Bin 0 -> 273 bytes parts/body/base_axolotl.png | Bin 0 -> 672 bytes parts/body/base_bee.png | Bin 0 -> 723 bytes parts/body/base_beetle.png | Bin 0 -> 598 bytes parts/body/base_blob.png | Bin 0 -> 496 bytes parts/body/base_brawler.png | Bin 0 -> 783 bytes parts/body/base_bunny.png | Bin 0 -> 685 bytes parts/body/base_chick.png | Bin 0 -> 596 bytes parts/body/base_cub.png | Bin 0 -> 687 bytes parts/body/base_dino.png | Bin 0 -> 660 bytes parts/body/base_drake.png | Bin 0 -> 680 bytes parts/body/base_eel.png | Bin 0 -> 1018 bytes parts/body/base_fawn.png | Bin 0 -> 744 bytes parts/body/base_fox.png | Bin 0 -> 676 bytes parts/body/base_grub.png | Bin 0 -> 605 bytes parts/body/base_guppy.png | Bin 0 -> 627 bytes parts/body/base_hawk.png | Bin 0 -> 638 bytes parts/body/base_imp.png | Bin 0 -> 779 bytes parts/body/base_noodle.png | Bin 0 -> 736 bytes parts/body/base_otter.png | Bin 0 -> 610 bytes parts/body/base_owl.png | Bin 0 -> 693 bytes parts/body/base_pebble.png | Bin 0 -> 541 bytes parts/body/base_penguin.png | Bin 0 -> 616 bytes parts/body/base_pixie.png | Bin 0 -> 795 bytes parts/body/base_pup.png | Bin 0 -> 688 bytes parts/body/base_salamander.png | Bin 0 -> 694 bytes parts/body/base_slime.png | Bin 0 -> 563 bytes parts/body/base_wisp.png | Bin 0 -> 641 bytes parts/body/base_wyrm.png | Bin 0 -> 818 bytes parts/head_top/antennae.png | Bin 0 -> 180 bytes parts/head_top/cloud_tuft.png | Bin 0 -> 185 bytes parts/head_top/crest_feather.png | Bin 0 -> 195 bytes parts/head_top/crest_flame.png | Bin 0 -> 205 bytes parts/head_top/crystal_spikes.png | Bin 0 -> 174 bytes parts/head_top/ears_bolt.png | Bin 0 -> 238 bytes parts/head_top/ears_fin.png | Bin 0 -> 268 bytes parts/head_top/ears_floppy.png | Bin 0 -> 288 bytes parts/head_top/ears_long.png | Bin 0 -> 276 bytes parts/head_top/ears_pointed.png | Bin 0 -> 174 bytes parts/head_top/ears_round.png | Bin 0 -> 241 bytes parts/head_top/flower_bloom.png | Bin 0 -> 247 bytes parts/head_top/gem_forehead.png | Bin 0 -> 114 bytes parts/head_top/headband.png | Bin 0 -> 168 bytes parts/head_top/horn_single.png | Bin 0 -> 169 bytes parts/head_top/horns_dragon.png | Bin 0 -> 252 bytes parts/head_top/horns_ram.png | Bin 0 -> 289 bytes parts/head_top/ice_crown.png | Bin 0 -> 190 bytes parts/head_top/leaf_sprout.png | Bin 0 -> 227 bytes parts/head_top/mushroom_cap.png | Bin 0 -> 197 bytes parts/head_top/spiky_hair.png | Bin 0 -> 203 bytes parts/head_top/water_spout.png | Bin 0 -> 241 bytes parts/head_top/wisp_flame.png | Bin 0 -> 180 bytes parts/tail/tail_club.png | Bin 0 -> 296 bytes parts/tail/tail_crystal.png | Bin 0 -> 274 bytes parts/tail/tail_curly.png | Bin 0 -> 196 bytes parts/tail/tail_dragon.png | Bin 0 -> 263 bytes parts/tail/tail_feather_fan.png | Bin 0 -> 310 bytes parts/tail/tail_fish_fin.png | Bin 0 -> 224 bytes parts/tail/tail_flame_tip.png | Bin 0 -> 258 bytes parts/tail/tail_fluffy.png | Bin 0 -> 273 bytes parts/tail/tail_ice_shard.png | Bin 0 -> 288 bytes parts/tail/tail_leaf.png | Bin 0 -> 275 bytes parts/tail/tail_lightning.png | Bin 0 -> 264 bytes parts/tail/tail_long_thin.png | Bin 0 -> 189 bytes parts/tail/tail_rattle.png | Bin 0 -> 193 bytes parts/tail/tail_spiked.png | Bin 0 -> 297 bytes parts/tail/tail_stinger.png | Bin 0 -> 269 bytes parts/tail/tail_stub.png | Bin 0 -> 188 bytes parts/tail/tail_wispy.png | Bin 0 -> 256 bytes parts_src/aura/aura_bubbles.txt | 84 +++ parts_src/aura/aura_dust.txt | 84 +++ parts_src/aura/aura_embers.txt | 84 +++ parts_src/aura/aura_gusts.txt | 84 +++ parts_src/aura/aura_leaves.txt | 84 +++ parts_src/aura/aura_pebbles.txt | 84 +++ parts_src/aura/aura_power.txt | 84 +++ parts_src/aura/aura_rings.txt | 84 +++ parts_src/aura/aura_snowflakes.txt | 84 +++ parts_src/aura/aura_sparkle.txt | 84 +++ parts_src/aura/aura_sparks.txt | 84 +++ parts_src/aura/aura_spores.txt | 84 +++ parts_src/aura/aura_toxic.txt | 84 +++ parts_src/aura/aura_wisps.txt | 84 +++ parts_src/back/cloud_puffs.txt | 20 + parts_src/back/coil_spikes.txt | 24 + parts_src/back/crystal_cluster.txt | 23 + parts_src/back/dorsal_fin.txt | 19 + parts_src/back/flame_mane.txt | 26 + parts_src/back/flower_back.txt | 26 + parts_src/back/ghost_tatters.txt | 24 + parts_src/back/icicle_spikes.txt | 24 + parts_src/back/leaf_cape.txt | 20 + parts_src/back/mane_fluffy.txt | 35 + parts_src/back/mushroom_back.txt | 22 + parts_src/back/plant_bulb.txt | 26 + parts_src/back/rock_plates.txt | 25 + parts_src/back/sail_fin.txt | 24 + parts_src/back/shell_round.txt | 26 + parts_src/back/shell_spiked.txt | 28 + parts_src/back/spikes_back.txt | 18 + parts_src/back/wings_bat.txt | 26 + parts_src/back/wings_bug.txt | 28 + parts_src/back/wings_dragon.txt | 32 + parts_src/back/wings_feather.txt | 30 + parts_src/back/wings_feather_small.txt | 16 + parts_src/body/base_axolotl.txt | 30 + parts_src/body/base_bee.txt | 35 + parts_src/body/base_beetle.txt | 30 + parts_src/body/base_blob.txt | 33 + parts_src/body/base_brawler.txt | 43 ++ parts_src/body/base_bunny.txt | 45 ++ parts_src/body/base_chick.txt | 35 + parts_src/body/base_cub.txt | 34 + parts_src/body/base_dino.txt | 44 ++ parts_src/body/base_drake.txt | 38 + parts_src/body/base_eel.txt | 46 ++ parts_src/body/base_fawn.txt | 35 + parts_src/body/base_fox.txt | 36 + parts_src/body/base_grub.txt | 27 + parts_src/body/base_guppy.txt | 33 + parts_src/body/base_hawk.txt | 37 + parts_src/body/base_imp.txt | 40 ++ parts_src/body/base_noodle.txt | 40 ++ parts_src/body/base_otter.txt | 30 + parts_src/body/base_owl.txt | 36 + parts_src/body/base_pebble.txt | 32 + parts_src/body/base_penguin.txt | 38 + parts_src/body/base_pixie.txt | 39 ++ parts_src/body/base_pup.txt | 34 + parts_src/body/base_salamander.txt | 27 + parts_src/body/base_slime.txt | 34 + parts_src/body/base_wisp.txt | 40 ++ parts_src/body/base_wyrm.txt | 44 ++ parts_src/head_top/antennae.txt | 16 + parts_src/head_top/cloud_tuft.txt | 11 + parts_src/head_top/crest_feather.txt | 14 + parts_src/head_top/crest_flame.txt | 16 + parts_src/head_top/crystal_spikes.txt | 14 + parts_src/head_top/ears_bolt.txt | 16 + parts_src/head_top/ears_fin.txt | 15 + parts_src/head_top/ears_floppy.txt | 17 + parts_src/head_top/ears_long.txt | 20 + parts_src/head_top/ears_pointed.txt | 14 + parts_src/head_top/ears_round.txt | 12 + parts_src/head_top/flower_bloom.txt | 15 + parts_src/head_top/gem_forehead.txt | 6 + parts_src/head_top/headband.txt | 10 + parts_src/head_top/horn_single.txt | 14 + parts_src/head_top/horns_dragon.txt | 19 + parts_src/head_top/horns_ram.txt | 13 + parts_src/head_top/ice_crown.txt | 14 + parts_src/head_top/leaf_sprout.txt | 16 + parts_src/head_top/mushroom_cap.txt | 12 + parts_src/head_top/spiky_hair.txt | 13 + parts_src/head_top/water_spout.txt | 14 + parts_src/head_top/wisp_flame.txt | 16 + parts_src/tail/tail_club.txt | 18 + parts_src/tail/tail_crystal.txt | 22 + parts_src/tail/tail_curly.txt | 16 + parts_src/tail/tail_dragon.txt | 17 + parts_src/tail/tail_feather_fan.txt | 22 + parts_src/tail/tail_fish_fin.txt | 19 + parts_src/tail/tail_flame_tip.txt | 23 + parts_src/tail/tail_fluffy.txt | 23 + parts_src/tail/tail_ice_shard.txt | 23 + parts_src/tail/tail_leaf.txt | 24 + parts_src/tail/tail_lightning.txt | 23 + parts_src/tail/tail_long_thin.txt | 15 + parts_src/tail/tail_rattle.txt | 18 + parts_src/tail/tail_spiked.txt | 21 + parts_src/tail/tail_stinger.txt | 23 + parts_src/tail/tail_stub.txt | 15 + parts_src/tail/tail_wispy.txt | 19 + preview.py | 270 ++++++++ requirements.txt | 3 + sample.csv | 46 ++ sketch.py | 243 +++++++ sprite_parts.py | 423 ++++++++++++ validate_parts.py | 189 +++++ 327 files changed, 7075 insertions(+) create mode 100644 .gitignore create mode 100644 AI_PIXEL_ART_GUIDE.md create mode 100644 README.md create mode 100644 SPRITES.md create mode 100755 build_parts.py create mode 100644 business_pokemon_type.py create mode 100644 generate_sprite.py create mode 100644 legacy/parts/body/avian_perched.png create mode 100644 legacy/parts/body/avian_round.png create mode 100644 legacy/parts/body/biped_bulky.png create mode 100644 legacy/parts/body/biped_small.png create mode 100644 legacy/parts/body/biped_tall.png create mode 100644 legacy/parts/body/blob_drip.png create mode 100644 legacy/parts/body/blob_floating.png create mode 100644 legacy/parts/body/blob_round.png create mode 100644 legacy/parts/body/fish_body.png create mode 100644 legacy/parts/body/insect_beetle.png create mode 100644 legacy/parts/body/insect_larva.png create mode 100644 legacy/parts/body/insect_mantis.png create mode 100644 legacy/parts/body/quad_long.png create mode 100644 legacy/parts/body/quad_slim.png create mode 100644 legacy/parts/body/quad_stocky.png create mode 100644 legacy/parts/body/seal_body.png create mode 100644 legacy/parts/body/serpent_coiled.png create mode 100644 legacy/parts/body/serpent_rising.png create mode 100644 legacy/parts/eyes/eyes_angry.png create mode 100644 legacy/parts/eyes/eyes_beady.png create mode 100644 legacy/parts/eyes/eyes_big_shiny.png create mode 100644 legacy/parts/eyes/eyes_compound.png create mode 100644 legacy/parts/eyes/eyes_cyclops.png create mode 100644 legacy/parts/eyes/eyes_glowing.png create mode 100644 legacy/parts/eyes/eyes_happy_closed.png create mode 100644 legacy/parts/eyes/eyes_masked.png create mode 100644 legacy/parts/eyes/eyes_round.png create mode 100644 legacy/parts/eyes/eyes_sharp.png create mode 100644 legacy/parts/eyes/eyes_sleepy.png create mode 100644 legacy/parts/head/head_bird.png create mode 100644 legacy/parts/head/head_bulky.png create mode 100644 legacy/parts/head/head_canine.png create mode 100644 legacy/parts/head/head_feline.png create mode 100644 legacy/parts/head/head_flat.png create mode 100644 legacy/parts/head/head_ghost.png create mode 100644 legacy/parts/head/head_insect.png create mode 100644 legacy/parts/head/head_reptile.png create mode 100644 legacy/parts/head/head_rock.png create mode 100644 legacy/parts/head/head_round.png create mode 100644 legacy/parts/snout/beak_duck.png create mode 100644 legacy/parts/snout/beak_hooked.png create mode 100644 legacy/parts/snout/beak_short.png create mode 100644 legacy/parts/snout/fangs.png create mode 100644 legacy/parts/snout/fish_lips.png create mode 100644 legacy/parts/snout/mandibles.png create mode 100644 legacy/parts/snout/muzzle_long.png create mode 100644 legacy/parts/snout/muzzle_small.png create mode 100644 legacy/parts/snout/reptile_jaw.png create mode 100644 legacy/parts/snout/snout_pig.png create mode 100644 legacy/parts/snout/tongue_out.png create mode 100644 legacy/parts/snout/trunk_short.png create mode 100644 legacy/parts/snout/tusks.png create mode 100644 legacy/parts/snout/whiskers.png create mode 100644 legacy/parts_src/body/avian_perched.txt create mode 100644 legacy/parts_src/body/avian_round.txt create mode 100644 legacy/parts_src/body/biped_bulky.txt create mode 100644 legacy/parts_src/body/biped_small.txt create mode 100644 legacy/parts_src/body/biped_tall.txt create mode 100644 legacy/parts_src/body/blob_drip.txt create mode 100644 legacy/parts_src/body/blob_floating.txt create mode 100644 legacy/parts_src/body/blob_round.txt create mode 100644 legacy/parts_src/body/fish_body.txt create mode 100644 legacy/parts_src/body/insect_beetle.txt create mode 100644 legacy/parts_src/body/insect_larva.txt create mode 100644 legacy/parts_src/body/insect_mantis.txt create mode 100644 legacy/parts_src/body/quad_long.txt create mode 100644 legacy/parts_src/body/quad_slim.txt create mode 100644 legacy/parts_src/body/quad_stocky.txt create mode 100644 legacy/parts_src/body/seal_body.txt create mode 100644 legacy/parts_src/body/serpent_coiled.txt create mode 100644 legacy/parts_src/body/serpent_rising.txt create mode 100644 legacy/parts_src/eyes/eyes_angry.txt create mode 100644 legacy/parts_src/eyes/eyes_beady.txt create mode 100644 legacy/parts_src/eyes/eyes_big_shiny.txt create mode 100644 legacy/parts_src/eyes/eyes_compound.txt create mode 100644 legacy/parts_src/eyes/eyes_cyclops.txt create mode 100644 legacy/parts_src/eyes/eyes_glowing.txt create mode 100644 legacy/parts_src/eyes/eyes_happy_closed.txt create mode 100644 legacy/parts_src/eyes/eyes_masked.txt create mode 100644 legacy/parts_src/eyes/eyes_round.txt create mode 100644 legacy/parts_src/eyes/eyes_sharp.txt create mode 100644 legacy/parts_src/eyes/eyes_sleepy.txt create mode 100644 legacy/parts_src/head/head_bird.txt create mode 100644 legacy/parts_src/head/head_bulky.txt create mode 100644 legacy/parts_src/head/head_canine.txt create mode 100644 legacy/parts_src/head/head_feline.txt create mode 100644 legacy/parts_src/head/head_flat.txt create mode 100644 legacy/parts_src/head/head_ghost.txt create mode 100644 legacy/parts_src/head/head_insect.txt create mode 100644 legacy/parts_src/head/head_reptile.txt create mode 100644 legacy/parts_src/head/head_rock.txt create mode 100644 legacy/parts_src/head/head_round.txt create mode 100644 legacy/parts_src/snout/beak_duck.txt create mode 100644 legacy/parts_src/snout/beak_hooked.txt create mode 100644 legacy/parts_src/snout/beak_short.txt create mode 100644 legacy/parts_src/snout/fangs.txt create mode 100644 legacy/parts_src/snout/fish_lips.txt create mode 100644 legacy/parts_src/snout/mandibles.txt create mode 100644 legacy/parts_src/snout/muzzle_long.txt create mode 100644 legacy/parts_src/snout/muzzle_small.txt create mode 100644 legacy/parts_src/snout/reptile_jaw.txt create mode 100644 legacy/parts_src/snout/snout_pig.txt create mode 100644 legacy/parts_src/snout/tongue_out.txt create mode 100644 legacy/parts_src/snout/trunk_short.txt create mode 100644 legacy/parts_src/snout/tusks.txt create mode 100644 legacy/parts_src/snout/whiskers.txt create mode 100644 parts/aura/aura_bubbles.png create mode 100644 parts/aura/aura_dust.png create mode 100644 parts/aura/aura_embers.png create mode 100644 parts/aura/aura_gusts.png create mode 100644 parts/aura/aura_leaves.png create mode 100644 parts/aura/aura_pebbles.png create mode 100644 parts/aura/aura_power.png create mode 100644 parts/aura/aura_rings.png create mode 100644 parts/aura/aura_snowflakes.png create mode 100644 parts/aura/aura_sparkle.png create mode 100644 parts/aura/aura_sparks.png create mode 100644 parts/aura/aura_spores.png create mode 100644 parts/aura/aura_toxic.png create mode 100644 parts/aura/aura_wisps.png create mode 100644 parts/back/cloud_puffs.png create mode 100644 parts/back/coil_spikes.png create mode 100644 parts/back/crystal_cluster.png create mode 100644 parts/back/dorsal_fin.png create mode 100644 parts/back/flame_mane.png create mode 100644 parts/back/flower_back.png create mode 100644 parts/back/ghost_tatters.png create mode 100644 parts/back/icicle_spikes.png create mode 100644 parts/back/leaf_cape.png create mode 100644 parts/back/mane_fluffy.png create mode 100644 parts/back/mushroom_back.png create mode 100644 parts/back/plant_bulb.png create mode 100644 parts/back/rock_plates.png create mode 100644 parts/back/sail_fin.png create mode 100644 parts/back/shell_round.png create mode 100644 parts/back/shell_spiked.png create mode 100644 parts/back/spikes_back.png create mode 100644 parts/back/wings_bat.png create mode 100644 parts/back/wings_bug.png create mode 100644 parts/back/wings_dragon.png create mode 100644 parts/back/wings_feather.png create mode 100644 parts/back/wings_feather_small.png create mode 100644 parts/body/base_axolotl.png create mode 100644 parts/body/base_bee.png create mode 100644 parts/body/base_beetle.png create mode 100644 parts/body/base_blob.png create mode 100644 parts/body/base_brawler.png create mode 100644 parts/body/base_bunny.png create mode 100644 parts/body/base_chick.png create mode 100644 parts/body/base_cub.png create mode 100644 parts/body/base_dino.png create mode 100644 parts/body/base_drake.png create mode 100644 parts/body/base_eel.png create mode 100644 parts/body/base_fawn.png create mode 100644 parts/body/base_fox.png create mode 100644 parts/body/base_grub.png create mode 100644 parts/body/base_guppy.png create mode 100644 parts/body/base_hawk.png create mode 100644 parts/body/base_imp.png create mode 100644 parts/body/base_noodle.png create mode 100644 parts/body/base_otter.png create mode 100644 parts/body/base_owl.png create mode 100644 parts/body/base_pebble.png create mode 100644 parts/body/base_penguin.png create mode 100644 parts/body/base_pixie.png create mode 100644 parts/body/base_pup.png create mode 100644 parts/body/base_salamander.png create mode 100644 parts/body/base_slime.png create mode 100644 parts/body/base_wisp.png create mode 100644 parts/body/base_wyrm.png create mode 100644 parts/head_top/antennae.png create mode 100644 parts/head_top/cloud_tuft.png create mode 100644 parts/head_top/crest_feather.png create mode 100644 parts/head_top/crest_flame.png create mode 100644 parts/head_top/crystal_spikes.png create mode 100644 parts/head_top/ears_bolt.png create mode 100644 parts/head_top/ears_fin.png create mode 100644 parts/head_top/ears_floppy.png create mode 100644 parts/head_top/ears_long.png create mode 100644 parts/head_top/ears_pointed.png create mode 100644 parts/head_top/ears_round.png create mode 100644 parts/head_top/flower_bloom.png create mode 100644 parts/head_top/gem_forehead.png create mode 100644 parts/head_top/headband.png create mode 100644 parts/head_top/horn_single.png create mode 100644 parts/head_top/horns_dragon.png create mode 100644 parts/head_top/horns_ram.png create mode 100644 parts/head_top/ice_crown.png create mode 100644 parts/head_top/leaf_sprout.png create mode 100644 parts/head_top/mushroom_cap.png create mode 100644 parts/head_top/spiky_hair.png create mode 100644 parts/head_top/water_spout.png create mode 100644 parts/head_top/wisp_flame.png create mode 100644 parts/tail/tail_club.png create mode 100644 parts/tail/tail_crystal.png create mode 100644 parts/tail/tail_curly.png create mode 100644 parts/tail/tail_dragon.png create mode 100644 parts/tail/tail_feather_fan.png create mode 100644 parts/tail/tail_fish_fin.png create mode 100644 parts/tail/tail_flame_tip.png create mode 100644 parts/tail/tail_fluffy.png create mode 100644 parts/tail/tail_ice_shard.png create mode 100644 parts/tail/tail_leaf.png create mode 100644 parts/tail/tail_lightning.png create mode 100644 parts/tail/tail_long_thin.png create mode 100644 parts/tail/tail_rattle.png create mode 100644 parts/tail/tail_spiked.png create mode 100644 parts/tail/tail_stinger.png create mode 100644 parts/tail/tail_stub.png create mode 100644 parts/tail/tail_wispy.png create mode 100644 parts_src/aura/aura_bubbles.txt create mode 100644 parts_src/aura/aura_dust.txt create mode 100644 parts_src/aura/aura_embers.txt create mode 100644 parts_src/aura/aura_gusts.txt create mode 100644 parts_src/aura/aura_leaves.txt create mode 100644 parts_src/aura/aura_pebbles.txt create mode 100644 parts_src/aura/aura_power.txt create mode 100644 parts_src/aura/aura_rings.txt create mode 100644 parts_src/aura/aura_snowflakes.txt create mode 100644 parts_src/aura/aura_sparkle.txt create mode 100644 parts_src/aura/aura_sparks.txt create mode 100644 parts_src/aura/aura_spores.txt create mode 100644 parts_src/aura/aura_toxic.txt create mode 100644 parts_src/aura/aura_wisps.txt create mode 100644 parts_src/back/cloud_puffs.txt create mode 100644 parts_src/back/coil_spikes.txt create mode 100644 parts_src/back/crystal_cluster.txt create mode 100644 parts_src/back/dorsal_fin.txt create mode 100644 parts_src/back/flame_mane.txt create mode 100644 parts_src/back/flower_back.txt create mode 100644 parts_src/back/ghost_tatters.txt create mode 100644 parts_src/back/icicle_spikes.txt create mode 100644 parts_src/back/leaf_cape.txt create mode 100644 parts_src/back/mane_fluffy.txt create mode 100644 parts_src/back/mushroom_back.txt create mode 100644 parts_src/back/plant_bulb.txt create mode 100644 parts_src/back/rock_plates.txt create mode 100644 parts_src/back/sail_fin.txt create mode 100644 parts_src/back/shell_round.txt create mode 100644 parts_src/back/shell_spiked.txt create mode 100644 parts_src/back/spikes_back.txt create mode 100644 parts_src/back/wings_bat.txt create mode 100644 parts_src/back/wings_bug.txt create mode 100644 parts_src/back/wings_dragon.txt create mode 100644 parts_src/back/wings_feather.txt create mode 100644 parts_src/back/wings_feather_small.txt create mode 100644 parts_src/body/base_axolotl.txt create mode 100644 parts_src/body/base_bee.txt create mode 100644 parts_src/body/base_beetle.txt create mode 100644 parts_src/body/base_blob.txt create mode 100644 parts_src/body/base_brawler.txt create mode 100644 parts_src/body/base_bunny.txt create mode 100644 parts_src/body/base_chick.txt create mode 100644 parts_src/body/base_cub.txt create mode 100644 parts_src/body/base_dino.txt create mode 100644 parts_src/body/base_drake.txt create mode 100644 parts_src/body/base_eel.txt create mode 100644 parts_src/body/base_fawn.txt create mode 100644 parts_src/body/base_fox.txt create mode 100644 parts_src/body/base_grub.txt create mode 100644 parts_src/body/base_guppy.txt create mode 100644 parts_src/body/base_hawk.txt create mode 100644 parts_src/body/base_imp.txt create mode 100644 parts_src/body/base_noodle.txt create mode 100644 parts_src/body/base_otter.txt create mode 100644 parts_src/body/base_owl.txt create mode 100644 parts_src/body/base_pebble.txt create mode 100644 parts_src/body/base_penguin.txt create mode 100644 parts_src/body/base_pixie.txt create mode 100644 parts_src/body/base_pup.txt create mode 100644 parts_src/body/base_salamander.txt create mode 100644 parts_src/body/base_slime.txt create mode 100644 parts_src/body/base_wisp.txt create mode 100644 parts_src/body/base_wyrm.txt create mode 100644 parts_src/head_top/antennae.txt create mode 100644 parts_src/head_top/cloud_tuft.txt create mode 100644 parts_src/head_top/crest_feather.txt create mode 100644 parts_src/head_top/crest_flame.txt create mode 100644 parts_src/head_top/crystal_spikes.txt create mode 100644 parts_src/head_top/ears_bolt.txt create mode 100644 parts_src/head_top/ears_fin.txt create mode 100644 parts_src/head_top/ears_floppy.txt create mode 100644 parts_src/head_top/ears_long.txt create mode 100644 parts_src/head_top/ears_pointed.txt create mode 100644 parts_src/head_top/ears_round.txt create mode 100644 parts_src/head_top/flower_bloom.txt create mode 100644 parts_src/head_top/gem_forehead.txt create mode 100644 parts_src/head_top/headband.txt create mode 100644 parts_src/head_top/horn_single.txt create mode 100644 parts_src/head_top/horns_dragon.txt create mode 100644 parts_src/head_top/horns_ram.txt create mode 100644 parts_src/head_top/ice_crown.txt create mode 100644 parts_src/head_top/leaf_sprout.txt create mode 100644 parts_src/head_top/mushroom_cap.txt create mode 100644 parts_src/head_top/spiky_hair.txt create mode 100644 parts_src/head_top/water_spout.txt create mode 100644 parts_src/head_top/wisp_flame.txt create mode 100644 parts_src/tail/tail_club.txt create mode 100644 parts_src/tail/tail_crystal.txt create mode 100644 parts_src/tail/tail_curly.txt create mode 100644 parts_src/tail/tail_dragon.txt create mode 100644 parts_src/tail/tail_feather_fan.txt create mode 100644 parts_src/tail/tail_fish_fin.txt create mode 100644 parts_src/tail/tail_flame_tip.txt create mode 100644 parts_src/tail/tail_fluffy.txt create mode 100644 parts_src/tail/tail_ice_shard.txt create mode 100644 parts_src/tail/tail_leaf.txt create mode 100644 parts_src/tail/tail_lightning.txt create mode 100644 parts_src/tail/tail_long_thin.txt create mode 100644 parts_src/tail/tail_rattle.txt create mode 100644 parts_src/tail/tail_spiked.txt create mode 100644 parts_src/tail/tail_stinger.txt create mode 100644 parts_src/tail/tail_stub.txt create mode 100644 parts_src/tail/tail_wispy.txt create mode 100755 preview.py create mode 100644 requirements.txt create mode 100644 sample.csv create mode 100755 sketch.py create mode 100644 sprite_parts.py create mode 100755 validate_parts.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cd5d55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +.env/ + +# Secrets (OPENROUTER_API_KEY is read from the environment) +.env +.env.* +*.key +secrets* + +# Generated sprites (generate_sprite.py --out, default sprites/) +sprites/ +sample_out/ +test_out/ +test_out_colors/ +out/ + +# Generated contact sheets (preview.py, $PREVIEW_OUT defaults to preview/) +preview/ + +# Scratch reports +size_changes.txt + +# Real business logos used for local testing +test_logos/ +logos/ + +# Editor / OS +.vscode/ +.idea/ +*.swp +.DS_Store +Thumbs.db diff --git a/AI_PIXEL_ART_GUIDE.md b/AI_PIXEL_ART_GUIDE.md new file mode 100644 index 0000000..fc47532 --- /dev/null +++ b/AI_PIXEL_ART_GUIDE.md @@ -0,0 +1,108 @@ +# How to make good pixel art as an AI (no image model) + +This guide is for AI coding agents. It describes the method used to build the Henry pixel-art loading animations (`~/Documents/henry_pixel_art_animations`). No diffusion or image generation was involved: every pixel is written as code, rendered, looked at, and fixed. + +The reference implementation is in `henry_pixel_art_animations/src/lib/henry/`: `engine/` holds the drawing engine, `henry.ts` the mascot sprite, and `scenes/` one file per animation. + +--- + +## 1. Constraints first + +Hard limits are what make the result look like real pixel art instead of blurry clip art. Decide them before drawing anything. + +- **Tiny canvas.** Everything is drawn at a low logical resolution (Henry uses 160×120, 4:3), then upscaled. Never draw at screen resolution. +- **Whole pixels only.** Round every coordinate. Sub-pixel positions smear when scaled. +- **One fixed palette.** Use about 16 colours plus a few named extras. Henry uses Sweetie 16 by GrafxKid. Every scene uses only these colours, which is why they all look like one set. Ban ad-hoc hex values, or give each extra colour a name. +- **1px dark outline** (`ink`, the palette's darkest colour) around characters and props, so they read against any background. +- **Upscale with nearest-neighbour.** In canvas, set `imageSmoothingEnabled = false`; in CSS, use `image-rendering: pixelated`. For fractional display scales, use a "sharp bilinear" upscale: scale with nearest-neighbour to the next whole multiple, then do one smooth downscale to the exact size. Pixels then stay evenly sized. + +## 2. Sprites are text grids + +Write each sprite as an array of equal-length strings. Each character maps to a palette colour, and `.` is transparent. + +```ts +const PAL = { k: C.ink, w: C.down, h: C.downLight, s: C.downShade, e: C.ink, o: C.bill, O: C.billShade, f: C.bill }; + +const STAND = [ + '....kkkkk.....', + '...kwwwwwk....', + '..khwwwwwwk...', + '..khwwwwwwwk..', + '..kwwwwwwewk..', // e = eye + '..kwwwwwwewkk.', + '..kwwwwwwwwook', // o = bill + '..kswwwwwwwOk.', + // ... + '.....f..f.....', // feet + '.....ff.ff....', +]; +``` + +- **Validate.** Throw if any row has a different width. This catches most authoring mistakes. +- **Bake once.** Render each sprite once to a small offscreen canvas, plus a horizontally flipped copy, and `drawImage` those every frame. +- **Make new poses by editing rows, not redrawing.** A blink changes the eye cell. Breathing drops one row so the body squashes 1px. A crouch drops two rows. A stride swaps the feet rows. Keeping the head rows identical keeps the character on-model across every pose. +- **Reason row by row** about silhouette and symmetry while writing the grid. Get the outline right first, then add the fill, then one highlight colour and one shade colour. +- **Keep sprites small.** A 14×18 character is plenty; small sprites read more clearly and are easier to get right. + +## 3. Draw scenery procedurally + +Backgrounds are drawing commands, not grids. + +- **Primitives:** `rect`, `px`, a scanline `disc` (a filled circle with no antialiasing), `pill` (a rounded rectangle with 1px corner notches), `clip`, and `sprite`. +- **Dithering:** at colour band edges, use a checkerboard. For example, at a sky band edge, paint the previous band's colour where `(x + y) % 2 === 0`, fading over 2–4 rows. +- **Seeded randomness** for skylines, asphalt specks and windows, with a small PRNG such as `mulberry32(seed)`, so the scenery is identical every run. +- **Pre-render static layers** (background, foreground props) once into offscreen canvases. Each frame, draw only the moving things between them. +- **Layer order:** background, then far movers (clouds, birds, distant cars), then big actors (bus), then foreground props, then the character, then speech bubbles and effects. +- **Ambient life** keeps loops from feeling static: drifting clouds, a passing bird, cars, blinking, flickering lights. + +## 4. Animate with timers and state machines + +- **Phases.** Each animation is a small state machine, e.g. `intro → waiting (loops forever) → resolving → finished`. Each phase has its own timer. +- **Idle scheduler.** While waiting, the character randomly picks short "acts" (look around, tap foot, hop, think bubble), never the same act twice in a row, with random rests between. This prevents a visibly repeating loop. +- **Motion:** use easing functions (`easeOutCubic`, `easeOutBack`) and a parabolic `hop(t, height)`. Step frame animations by time (`Math.floor(t / 0.16) % 2`) or by distance travelled (wheels, walk cycles). +- **Cartoon cues:** a 1-frame pop on bubbles, dust puffs on landing, a squash before a jump, sparkles on success, wobble or shake for impact. +- **Clamp `dt`** to a range like 0–0.05 s, so a backgrounded tab doesn't fast-forward, and a negative first-frame delta can't break array indexing. +- **Transitions:** the iris (circle) wipe paints everything outside a circle with the palette's darkest colour, one scanline at a time so the edges stay crisp. It closes on the character's position and opens the next scene on its character. + +## 5. Render, look, fix — the step that matters most + +You cannot see the art while writing the character grids, so you must render it and inspect it. Never ship unviewed art. + +1. Run the app (e.g. a Vite dev server) and open it in a headless browser. +2. Grab frames from the canvas **inside one in-page script**, because headless tabs may freeze between tool calls: + ```js + // in page.evaluate: sample the canvas every 250 ms, trigger UI via setTimeout in the same script + const out = []; const c = document.querySelector('canvas'); + const id = setInterval(() => { out.push(c.toDataURL('image/png')); if (out.length > 40) { clearInterval(id); resolve(out); } }, 250); + ``` + For exact, repeatable checks, import the scene module directly in the page and step it with a fixed `dt = 1/60`, drawing to an offscreen canvas. +3. Write the frames as PNGs, then tile them into a contact sheet with ffmpeg. Always use nearest-neighbour scaling: + ```sh + ffmpeg -framerate 1 -i f%03d.png -vf "scale=160:120:flags=neighbor,crop=W:H:X:Y,scale=320:-1:flags=neighbor,tile=6x4" -frames:v 1 sheet.png + ``` +4. **Look at the image.** Crop and zoom to 4–10× on details such as the character, icons and contact points. Check whether: + - the silhouette reads, and the icon is recognisable; + - the motion reads across frames; + - things line up (feet on the ground, a bill touching a button, nothing clipping through a wall); + - the character is still visible when the iris closes. +5. Fix and repeat. Expect several rounds on anything important. Real fixes from this project: the bill was one pixel too narrow; the "walk" icon was a blob until it got a gap between head and body; a button was 2px too low for the bill to touch; a peeking pose showed the tail instead of the face until it was flipped. +6. For small icons, preview the grid by itself on a plain background at about 14× before placing it in the scene. + +## 6. Scaling up with multiple agents + +If several agents each build a scene, keep the style consistent: + +- **Write a shared guide first.** Cover the engine API, palette, layout constants, character sprite and helpers, the scene contract, style rules (outlines, dithering, ambient life), and the render-and-look verification loop. +- **Freeze the shared files** (engine, palette, character). Agents put scene-specific colours and poses in their own files, and build new poses from the shared character rows so the design never drifts. +- **Give each agent a way to open its scene directly,** e.g. `?scene=`, and a unique temp directory and browser tab. +- **Review every finished scene yourself.** Step each one with fixed timesteps across early, middle and late "done" triggers to catch crashes and scenes that never finish, then look at the contact sheets. + +## Checklist + +- [ ] Low fixed resolution, whole pixels, nearest-neighbour upscale +- [ ] One named palette; no stray colours +- [ ] 1px dark outlines on characters and props +- [ ] Sprites as text grids with validated widths; poses made by editing rows +- [ ] Procedural backgrounds with checkerboard dithering and seeded randomness +- [ ] State-machine animation, varied idle acts, ambient motion, clamped `dt` +- [ ] Rendered, sampled, contact-sheeted, zoomed and **looked at** — then fixed and checked again diff --git a/README.md b/README.md new file mode 100644 index 0000000..6344c6d --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# sprite_gen + +Procedural 96x96 creature sprites for businesses. You give it a business name and logo. It picks a type, builds a creature from parts and recolors it with the logo's brand colors. + +## Setup + +```bash +pip install -r requirements.txt +``` + +You only need an [OpenRouter](https://openrouter.ai) API key if you want the type picked automatically. Skip the key by passing `--type`. + +## Generate a sprite + +```bash +# Type picked from the business name +OPENROUTER_API_KEY= python generate_sprite.py "Rosas Landscaping" logos/rosas.png + +# Add a description to help pick the type +OPENROUTER_API_KEY= python generate_sprite.py "Rosas Landscaping" logos/rosas.png --desc "Lawn care and garden design" + +# Set the type yourself (no API key needed) +python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass + +# Set the colors yourself instead of using a logo (main color first) +python generate_sprite.py "Rosas Landscaping" --type grass --colors "#2E7D32,#FBC02D" + +# A different creature for the same business +python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass --reroll 1 +``` + +Each run writes `sprites/.png` (96x96), `sprites/@4x.png` and `sprites/.json` (type, parts, colors). With `--reroll N`, the file names end in `-rN`. + +| Option | Meaning | +|---|---| +| `--type T` | Skip the classifier. One of: normal fire water grass electric ice fighting poison ground flying psychic bug rock ghost dragon | +| `--desc TEXT` | Description sent to the classifier (default: the name) | +| `--colors "#main,#accent"` | Manual colors; overrides the logo | +| `--reroll N` | Alternate creature; the name alone always gives the same sprite | +| `--out DIR` | Output folder (default `sprites`) | +| `--scale N` | Size of the upscaled copy (default 4, `1` disables it) | + +Logo colors: transparent pixels, soft edge pixels and a solid white/black/gray background are ignored, and colored brand colors are used before white, black or gray. A logo with only white, black or gray in it still uses those colors. + +## Batch + +Use a CSV with the columns `name,logo,type,description`. Only `name` is required, and rows with no `type` use the classifier. + +```bash +OPENROUTER_API_KEY= python generate_sprite.py --batch businesses.csv --out sprites +``` + +## Editing the art + +The parts are text grids in `parts_src//.txt`, compiled to `parts//.png`. Weights, compatibility and deny lists live in `sprite_parts.py`. See `AI_PIXEL_ART_GUIDE.md` for the palette and anchor rules. + +```bash +python build_parts.py # compile all grids (or pass part names) +python validate_parts.py --sample 3000 # check colors, anchors, canvas fit on random composites +python preview.py bases # contact sheets in preview/ +python preview.py part back plant_bulb # one part on every base it fits +python preview.py zoom body=base_fox tail=tail_leaf +python generate_sprite.py --sprite-list SPRITES.md # regenerate the part list and weight report +``` + +Do not run `--make-placeholders --overwrite`, because it replaces the real art with test placeholders. diff --git a/SPRITES.md b/SPRITES.md new file mode 100644 index 0000000..e068153 --- /dev/null +++ b/SPRITES.md @@ -0,0 +1,319 @@ +# Sprite checklist + +**104 sprites** across 5 slots. Save each as `parts//.png`. + +## Art rules + +- Final canvas is 96x96. Sizes below are suggested part canvases; crop tight with transparency. +- Characters face left in a 3/4 view, light from the top-left. 1px outline, no anti-aliasing, no semi-transparent pixels. +- Draw only with the palette keys below. Any other color (eye white, pupils) is left as-is. + +| Key | Hex | Use | +|---|---|---| +| main_light | #FF8080 | body / most fur and skin | +| main_base | #FF0000 | body / most fur and skin | +| main_dark | #800000 | body / most fur and skin | +| accent_light | #80FF80 | horns, wings, beaks, crests, shells | +| accent_base | #00FF00 | horns, wings, beaks, crests, shells | +| accent_dark | #008000 | horns, wings, beaks, crests, shells | +| type_light | #8080FF | elemental bits: flames, leaves, sparks, ice, water | +| type_base | #0000FF | elemental bits: flames, leaves, sparks, ice, water | +| type_dark | #000080 | elemental bits: flames, leaves, sparks, ice, water | +| outline | #000000 | outline (recolored) | +| pupil | #141414 | not recolored | +| eye white | #FFFFFF | not recolored | + +### Anchor marker pixels + +One pixel each. `pivot` is where the part attaches to its parent; the others are where children attach. + +| Marker | Hex | Needed in | +|---|---|---| +| pivot | #FF00FF | every part (bodies: bottom center between the feet) | +| head_top | #FA00FF | all bodies (top of the head) | +| tail | #FD00FF | all bodies | +| back | #FC00FF | all bodies | +| aura | #FB00FF | all bodies (center of mass; belly patch centre) | + +## body (28) + +| Done | File | Size | Palette | Plans | Description | Most likely for | +|---|---|---|---|---|---|---| +| [ ] | `body/base_fox.png` | 42x36 | main | quadruped | Chibi fox/cat critter with cheek tufts, big pointed ears and a fluffy bib | fire, electric, psychic | +| [ ] | `body/base_cub.png` | 40x34 | main | quadruped | Chunky bear-cub on all fours, round ears, sturdy paws | ground, fighting, rock | +| [ ] | `body/base_pup.png` | 42x34 | main | quadruped | Bouncy puppy with floppy ears and a happy open mouth | normal, fire, fighting | +| [ ] | `body/base_salamander.png` | 47x27 | main | quadruped | Low chibi salamander/newt with a wide smile and frilly cheeks | fire, water, poison | +| [ ] | `body/base_drake.png` | 42x38 | main | quadruped | Cool baby dragon on four legs, nub horns, confident grin | dragon, fire, rock | +| [ ] | `body/base_fawn.png` | 42x35 | main | quadruped | Dainty chibi fawn in 3/4 view: big leaf ears, gentle eyes, spotted back | grass, ice, psychic | +| [ ] | `body/base_imp.png` | 40x40 | main | biped | Upright mouse-imp with big round ears and a cheeky fanged grin | electric, normal, psychic | +| [ ] | `body/base_brawler.png` | 42x43 | main | biped | Stout little fighter with big fists and a determined face | fighting, ground, rock | +| [ ] | `body/base_bunny.png` | 36x45 | main | biped | Upright chibi bunny with long ears and a fluffy chest | normal, grass, ice | +| [ ] | `body/base_dino.png` | 36x44 | main | biped | Little upright dino with a big head, tiny arms and a toothy grin | dragon, fire, ground | +| [ ] | `body/base_pixie.png` | 42x39 | main | biped | Leaf-elf pixie with sweeping pointed ears, round torso, stubby legs and a mischievous grin | psychic, ghost, grass | +| [ ] | `body/base_chick.png` | 36x35 | main | avian | Egg-round chick with a feather tuft, wing nubs and a tiny beak | normal, ice, water | +| [ ] | `body/base_owl.png` | 36x36 | main | avian | Round owlet with huge eyes and ear tufts | psychic, flying, ghost | +| [ ] | `body/base_hawk.png` | 38x37 | main | avian | Cool young hawk, puffed chest, sharp brow and hooked beak | flying, fire, fighting | +| [ ] | `body/base_penguin.png` | 36x38 | main | avian | Chubby penguin chick with flipper wings and a bib | ice, water, normal | +| [ ] | `body/base_guppy.png` | 46x33 | main | aquatic | Round bubbly fish with big eyes and frilly fins | water, ice, poison | +| [ ] | `body/base_otter.png` | 49x30 | main | aquatic | Chubby seal/otter pup lying on its flippers | water, ice, normal | +| [ ] | `body/base_axolotl.png` | 51x30 | main | aquatic | Smiling axolotl with feathery gill fronds | water, poison, dragon | +| [ ] | `body/base_noodle.png` | 44x40 | main | serpentine | Cute coiled snake with a big round head and a sly smile | poison, ground, normal | +| [ ] | `body/base_eel.png` | 45x46 | main | serpentine | Rising sea-serpent with fin frills and big eyes | water, ice, electric | +| [ ] | `body/base_wyrm.png` | 43x44 | main | serpentine | Cool coiled young wyrm with whisker barbels and a proud look | dragon, ghost, ice | +| [ ] | `body/base_blob.png` | 40x33 | main | blob | Squishy mochi blob with a curl on top and a big smile | normal, grass, ice | +| [ ] | `body/base_slime.png` | 40x34 | main | blob | Drippy goo blob with a cheeky tongue-out face | poison, water, ghost | +| [ ] | `body/base_wisp.png` | 36x40 | main | blob | Floating ghost wisp with a curly tail-end and a spooky-cute grin | ghost, psychic, fire | +| [ ] | `body/base_pebble.png` | 38x32 | main | blob | Round rock lump creature with a sturdy little face and stubby feet | rock, ground, fighting | +| [ ] | `body/base_beetle.png` | 42x30 | main | insectoid | Round shiny beetle bug with a horn and stubby legs | bug, ground, rock | +| [ ] | `body/base_grub.png` | 44x27 | main | insectoid | Plump caterpillar with a big happy face and tufted segments | bug, grass, poison | +| [ ] | `body/base_bee.png` | 38x35 | main | insectoid | Fuzzy round bee/bug with tiny wings and antennae | bug, fighting, flying | + +## head_top (23) + +| Done | File | Size | Palette | Plans | Description | Most likely for | +|---|---|---|---|---|---|---| +| [ ] | `head_top/ears_pointed.png` | 16x14 | main | all | Pair of tall pointed ears | electric, normal, fire | +| [ ] | `head_top/ears_round.png` | 16x14 | main | all | Pair of round mouse/bear ears | normal, electric, ice | +| [ ] | `head_top/ears_long.png` | 18x20 | main | all | Long rabbit ears (drawn behind the creature) | normal, psychic, grass | +| [ ] | `head_top/ears_floppy.png` | 26x12 | main | all | Droopy hanging ears (drawn behind the creature) | normal, water, ground | +| [ ] | `head_top/ears_bolt.png` | 16x14 | main | all | Zigzag lightning-bolt ears, type-colored tips | electric, normal, fire | +| [ ] | `head_top/ears_fin.png` | 16x14 | accent | all | Fin-shaped ear frills | water, ice, dragon | +| [ ] | `head_top/horn_single.png` | 10x14 | accent | all | One forehead horn (rhino/beetle) | ground, bug, rock | +| [ ] | `head_top/horns_ram.png` | 26x14 | accent | all | Curled ram horns | ground, rock, fighting | +| [ ] | `head_top/horns_dragon.png` | 22x16 | accent | all | Swept-back dragon horns | dragon, fire, ghost | +| [ ] | `head_top/antennae.png` | 16x16 | main | all | Two thin antennae with tips | bug, electric, psychic | +| [ ] | `head_top/crest_feather.png` | 16x14 | accent | all | Swept feather crest | flying, fire, fighting | +| [ ] | `head_top/crest_flame.png` | 16x14 | type | all | Flickering flame on top of head | fire, normal, water | +| [ ] | `head_top/leaf_sprout.png` | 14x14 | type | all | Leaf/sprout growing from head | grass, normal, fire | +| [ ] | `head_top/flower_bloom.png` | 16x14 | accent | all | Small flower on head | grass, poison, psychic | +| [ ] | `head_top/mushroom_cap.png` | 20x12 | accent | all | Mushroom cap worn like a hat | poison, grass, bug | +| [ ] | `head_top/ice_crown.png` | 18x12 | type | all | Ring of small icicles | ice, normal, fire | +| [ ] | `head_top/crystal_spikes.png` | 16x14 | accent | all | Cluster of crystal shards | rock, ice, psychic | +| [ ] | `head_top/gem_forehead.png` | 8x8 | accent | all | Gem set in forehead | psychic, dragon, rock | +| [ ] | `head_top/spiky_hair.png` | 20x12 | main | all | Spiky tuft of fur/hair | electric, fighting, poison | +| [ ] | `head_top/headband.png` | 24x10 | accent | all | Tied headband with trailing ends | fighting, normal, fire | +| [ ] | `head_top/wisp_flame.png` | 16x14 | type | all | Ghostly floating flame wisp | ghost, fire, psychic | +| [ ] | `head_top/water_spout.png` | 16x14 | type | all | Water droplet/fountain crest | water, ice, normal | +| [ ] | `head_top/cloud_tuft.png` | 20x12 | type | all | Puffy cloud tuft | flying, electric, ice | + +## back (22) + +| Done | File | Size | Palette | Plans | Description | Most likely for | +|---|---|---|---|---|---|---| +| [ ] | `back/wings_feather.png` | 44x30 | accent | avian, biped, quadruped | Full feathered wings, spread | flying, fire, normal | +| [ ] | `back/wings_feather_small.png` | 30x14 | accent | avian, biped, quadruped, blob | Tiny stubby wings | flying, normal, ice | +| [ ] | `back/wings_bat.png` | 44x26 | accent | biped, quadruped, avian, blob, serpentine | Membrane bat wings | poison, ghost, flying | +| [ ] | `back/wings_bug.png` | 40x28 | accent | insectoid, biped, blob | Translucent veined insect wings | bug, grass, poison | +| [ ] | `back/wings_dragon.png` | 48x32 | accent | quadruped, biped, serpentine | Large clawed dragon wings | dragon, fire, flying | +| [ ] | `back/shell_round.png` | 40x26 | accent | quadruped, biped, aquatic | Domed turtle shell | water, ground, rock | +| [ ] | `back/shell_spiked.png` | 40x28 | accent | quadruped, biped, insectoid | Armored shell with spikes | rock, ground, water | +| [ ] | `back/spikes_back.png` | 36x14 | accent | quadruped, biped, serpentine, aquatic, blob | Row of spikes along the spine | ground, poison, rock | +| [ ] | `back/sail_fin.png` | 34x20 | accent | quadruped, biped, serpentine | Tall dimetrodon-style back sail | fire, dragon, water | +| [ ] | `back/dorsal_fin.png` | 20x16 | accent | aquatic, serpentine, quadruped | Shark-style dorsal fin | water, ice, dragon | +| [ ] | `back/plant_bulb.png` | 30x26 | type | quadruped, biped, blob | Large seed pod/bulb with leaves | grass, poison, normal | +| [ ] | `back/leaf_cape.png` | 38x20 | type | quadruped, biped, serpentine, insectoid | Overlapping leaves along the back | grass, bug, flying | +| [ ] | `back/flower_back.png` | 32x26 | accent | quadruped, biped, blob | Big blooming flower | grass, poison, psychic | +| [ ] | `back/mushroom_back.png` | 30x22 | accent | quadruped, insectoid, blob | Cluster of mushrooms growing on back | grass, poison, bug | +| [ ] | `back/flame_mane.png` | 36x28 | type | quadruped, biped | Mane/collar of fire | fire, dragon, normal | +| [ ] | `back/crystal_cluster.png` | 30x24 | accent | quadruped, biped, blob, serpentine | Crystals jutting from back | rock, ice, psychic | +| [ ] | `back/rock_plates.png` | 38x20 | accent | quadruped, biped, serpentine | Boulder armor plates | rock, ground, fighting | +| [ ] | `back/icicle_spikes.png` | 29x24 | type | quadruped, biped, aquatic, avian, blob | Tight fan of icicles rising from the back | ice, water, normal | +| [ ] | `back/mane_fluffy.png` | 34x26 | main | quadruped, biped | Big fluffy fur collar/mane | normal, fighting, electric | +| [ ] | `back/coil_spikes.png` | 26x24 | type | quadruped, biped, blob, insectoid | Chunky coil/battery prongs with a spark | electric, normal, fire | +| [ ] | `back/cloud_puffs.png` | 30x20 | type | blob, avian, quadruped, serpentine | Fluffy cloud hugging the back | flying, ice, psychic | +| [ ] | `back/ghost_tatters.png` | 26x24 | accent | biped, blob, serpentine, avian | Short ghostly shroud draped from the shoulders | ghost, poison, psychic | + +## tail (17) + +| Done | File | Size | Palette | Plans | Description | Most likely for | +|---|---|---|---|---|---|---| +| [ ] | `tail/tail_stub.png` | 10x10 | main | quadruped, biped, avian | Short round stub tail | normal, ice, ground | +| [ ] | `tail/tail_fluffy.png` | 26x20 | main | quadruped, biped | Big fluffy fox/squirrel tail | normal, ice, electric | +| [ ] | `tail/tail_long_thin.png` | 30x12 | main | quadruped, biped | Long thin tail with tuft | psychic, normal, electric | +| [ ] | `tail/tail_curly.png` | 16x16 | main | quadruped, biped | Spiral curly tail | normal, psychic, grass | +| [ ] | `tail/tail_flame_tip.png` | 28x20 | type | quadruped, biped, serpentine | Tail ending in a flame | fire, normal, water | +| [ ] | `tail/tail_lightning.png` | 26x22 | type | quadruped, biped | Zigzag lightning-bolt tail | electric, normal, fire | +| [ ] | `tail/tail_leaf.png` | 24x16 | type | quadruped, biped, insectoid, serpentine | Tail ending in a big leaf | grass, bug, normal | +| [ ] | `tail/tail_fish_fin.png` | 22x22 | accent | aquatic, serpentine, quadruped, biped | Forked fish/mermaid tail fin | water, ice, dragon | +| [ ] | `tail/tail_club.png` | 28x16 | main | quadruped, biped | Heavy club-ended tail | rock, ground, fighting | +| [ ] | `tail/tail_spiked.png` | 28x14 | main | quadruped, biped, serpentine | Tail with spikes | ground, rock, dragon | +| [ ] | `tail/tail_stinger.png` | 22x18 | accent | insectoid, serpentine, quadruped, biped | Curved stinger tail | poison, bug, normal | +| [ ] | `tail/tail_feather_fan.png` | 22x22 | accent | avian, biped | Fanned tail feathers | flying, fire, psychic | +| [ ] | `tail/tail_wispy.png` | 24x16 | main | blob, serpentine, biped, avian | Fading smoky wisp tail | ghost, psychic, fire | +| [ ] | `tail/tail_ice_shard.png` | 24x16 | type | quadruped, biped, aquatic, serpentine | Tail ending in an ice shard | ice, normal, fire | +| [ ] | `tail/tail_dragon.png` | 30x16 | main | quadruped, biped, serpentine | Thick tapered tail with small fin | dragon, fire, ground | +| [ ] | `tail/tail_rattle.png` | 14x12 | accent | serpentine | Rattlesnake rattle tip | poison, ground, normal | +| [ ] | `tail/tail_crystal.png` | 24x16 | accent | quadruped, biped, serpentine | Tail tipped with a crystal | psychic, rock, ice | + +## aura (14) + +| Done | File | Size | Palette | Plans | Description | Most likely for | +|---|---|---|---|---|---|---| +| [ ] | `aura/aura_embers.png` | 84x84 | type | all | Floating embers | fire | +| [ ] | `aura/aura_sparks.png` | 84x84 | type | all | Crackling sparks | electric | +| [ ] | `aura/aura_bubbles.png` | 84x84 | type | all | Rising bubbles | water, poison | +| [ ] | `aura/aura_leaves.png` | 84x84 | type | all | Drifting leaves | grass, flying, bug | +| [ ] | `aura/aura_snowflakes.png` | 84x84 | type | all | Snowflakes | ice | +| [ ] | `aura/aura_wisps.png` | 84x84 | type | all | Ghost wisps | ghost, psychic | +| [ ] | `aura/aura_rings.png` | 84x84 | type | all | Psychic rings/ripples | psychic | +| [ ] | `aura/aura_dust.png` | 84x84 | type | all | Dust cloud at the feet | ground, rock, fighting | +| [ ] | `aura/aura_toxic.png` | 84x84 | type | all | Toxic bubbles and drips | poison | +| [ ] | `aura/aura_sparkle.png` | 84x84 | type | all | Twinkling sparkles | dragon, normal, psychic | +| [ ] | `aura/aura_gusts.png` | 84x84 | type | all | Wind swirls | flying, dragon | +| [ ] | `aura/aura_pebbles.png` | 84x84 | type | all | Floating pebbles | rock, ground, psychic | +| [ ] | `aura/aura_power.png` | 84x84 | type | all | Power-up energy lines | fighting | +| [ ] | `aura/aura_spores.png` | 84x84 | type | all | Floating spores | bug, grass, poison | + +## Simulated pick rates per type + +From 3,000 random seeds per type. Use this to spot parts that never show up or show up too much. + +### Normal + +- **Body plan:** quadruped 41%, biped 32%, avian 15%, blob 8%, serpentine 4% +- **body:** base_pup 18%, base_bunny 14%, base_imp 12%, base_chick 9%, base_fox 8%, base_cub 8%, base_blob 7%, base_fawn 4% +- **head_top:** none 61%, spiky_hair 9%, horns_ram 8%, headband 6%, horn_single 3%, ears_round 2%, ears_floppy 2%, flower_bloom 1% +- **back:** none 47%, mane_fluffy 17%, wings_feather_small 13%, wings_feather 8%, cloud_puffs 6%, coil_spikes 2%, spikes_back 2% +- **tail:** none 36%, tail_stub 23%, tail_fluffy 15%, tail_curly 12%, tail_long_thin 7%, tail_feather_fan 3%, tail_rattle 1% +- **aura:** none 100% +- **markings:** none 42%, belly 31%, spots 15%, speckles 12% + +### Fire + +- **Body plan:** quadruped 43%, biped 26%, avian 12%, serpentine 11%, blob 8% +- **body:** base_dino 18%, base_pup 11%, base_salamander 11%, base_fox 10%, base_drake 10%, base_hawk 9%, base_wisp 5%, base_wyrm 4% +- **head_top:** crest_flame 53%, none 15%, wisp_flame 10%, horns_dragon 5%, spiky_hair 5%, crest_feather 4%, ears_pointed 2%, horn_single 1% +- **back:** none 34%, flame_mane 19%, sail_fin 14%, wings_dragon 10%, spikes_back 7%, wings_feather 6%, mane_fluffy 5%, wings_feather_small 2% +- **tail:** none 68%, tail_flame_tip 15%, tail_feather_fan 5%, tail_dragon 3%, tail_wispy 3%, tail_stub 2%, tail_fluffy 2%, tail_long_thin 1% +- **aura:** none 100% +- **markings:** none 58%, belly 29%, spots 9%, speckles 5% + +### Water + +- **Body plan:** aquatic 43%, serpentine 22%, quadruped 16%, biped 12%, blob 8% +- **body:** base_eel 19%, base_guppy 16%, base_otter 14%, base_axolotl 12%, base_salamander 8%, base_slime 6%, base_dino 6%, base_pup 4% +- **head_top:** water_spout 48%, none 26%, ears_fin 11%, horn_single 3%, spiky_hair 2%, horns_ram 1%, horns_dragon 1%, flower_bloom 1% +- **back:** none 41%, dorsal_fin 37%, icicle_spikes 6%, shell_round 6%, sail_fin 4%, shell_spiked 2%, spikes_back 1%, wings_feather_small 1% +- **tail:** none 72%, tail_fish_fin 14%, tail_dragon 7%, tail_rattle 1% +- **aura:** none 100% +- **markings:** none 48%, spots 25%, belly 23%, speckles 4% + +### Grass + +- **Body plan:** quadruped 38%, biped 28%, blob 18%, insectoid 9%, serpentine 8% +- **body:** base_fawn 19%, base_bunny 15%, base_blob 14%, base_cub 9%, base_pixie 9%, base_grub 8%, base_noodle 6%, base_salamander 4% +- **head_top:** leaf_sprout 42%, flower_bloom 26%, mushroom_cap 15%, none 10%, ears_long 1% +- **back:** leaf_cape 31%, plant_bulb 29%, flower_back 17%, none 9%, mushroom_back 9%, wings_bug 4% +- **tail:** none 87%, tail_leaf 11%, tail_curly 1% +- **aura:** none 100% +- **markings:** none 48%, spots 37%, belly 12%, speckles 4% + +### Electric + +- **Body plan:** quadruped 38%, biped 28%, blob 18%, insectoid 9%, avian 8% +- **body:** base_fox 26%, base_imp 22%, base_blob 12%, base_bee 6%, base_chick 5%, base_drake 2%, base_fawn 2%, base_pup 2% +- **head_top:** none 23%, cloud_tuft 23%, spiky_hair 22%, antennae 15%, ears_pointed 4%, horn_single 2%, ears_bolt 2%, ears_round 2% +- **back:** coil_spikes 33%, none 33%, mane_fluffy 10%, cloud_puffs 9%, wings_bug 5%, sail_fin 4%, wings_feather_small 2%, wings_feather 1% +- **tail:** none 73%, tail_lightning 13%, tail_long_thin 5%, tail_fluffy 4%, tail_stub 2%, tail_curly 1% +- **aura:** none 100% +- **markings:** none 54%, spots 29%, belly 14%, speckles 4% + +### Ice + +- **Body plan:** quadruped 28%, aquatic 19%, biped 18%, avian 13%, blob 13%, serpentine 9% +- **body:** base_fawn 14%, base_otter 12%, base_bunny 12%, base_blob 10%, base_fox 9%, base_penguin 8%, base_guppy 7%, base_eel 6% +- **head_top:** ice_crown 52%, none 13%, cloud_tuft 10%, crystal_spikes 10%, water_spout 5%, horn_single 1%, ears_fin 1%, ears_round 1% +- **back:** icicle_spikes 50%, none 18%, crystal_cluster 12%, spikes_back 6%, cloud_puffs 5%, dorsal_fin 4%, mane_fluffy 2%, wings_feather_small 1% +- **tail:** none 77%, tail_ice_shard 14%, tail_stub 3%, tail_crystal 2%, tail_fluffy 1% +- **aura:** none 100% +- **markings:** none 49%, belly 24%, speckles 20%, spots 6% + +### Fighting + +- **Body plan:** biped 75%, quadruped 20%, insectoid 5% +- **body:** base_brawler 61%, base_cub 10%, base_pup 7%, base_dino 4%, base_bee 4%, base_bunny 4%, base_imp 3%, base_pixie 3% +- **head_top:** headband 38%, none 20%, horns_ram 16%, horn_single 15%, spiky_hair 7% +- **back:** none 42%, mane_fluffy 23%, shell_spiked 11%, rock_plates 10%, spikes_back 2%, shell_round 2%, wings_feather 2%, wings_feather_small 2% +- **tail:** none 72%, tail_club 12%, tail_stub 4%, tail_long_thin 3%, tail_fluffy 3%, tail_curly 2%, tail_spiked 1% +- **aura:** none 100% +- **markings:** none 58%, belly 29%, spots 9%, speckles 5% + +### Poison + +- **Body plan:** blob 32%, serpentine 24%, quadruped 15%, biped 15%, insectoid 14% +- **body:** base_slime 27%, base_noodle 21%, base_salamander 10%, base_grub 10%, base_pixie 9%, base_beetle 3%, base_wisp 2%, base_dino 2% +- **head_top:** mushroom_cap 29%, none 19%, flower_bloom 15%, horn_single 12%, spiky_hair 12%, antennae 5%, ears_pointed 1%, horns_ram 1% +- **back:** none 30%, spikes_back 15%, mushroom_back 11%, plant_bulb 10%, flower_back 10%, wings_bat 10%, wings_bug 8%, ghost_tatters 2% +- **tail:** none 72%, tail_stinger 13%, tail_rattle 7%, tail_spiked 4%, tail_long_thin 2% +- **aura:** none 100% +- **markings:** none 48%, spots 37%, belly 12%, speckles 4% + +### Ground + +- **Body plan:** quadruped 48%, biped 29%, serpentine 18%, insectoid 4% +- **body:** base_cub 32%, base_noodle 16%, base_brawler 15%, base_dino 10%, base_drake 7%, base_beetle 3%, base_salamander 2%, base_fawn 2% +- **head_top:** horn_single 35%, none 34%, horns_ram 18%, spiky_hair 2%, flower_bloom 2%, mushroom_cap 1%, leaf_sprout 1% +- **back:** spikes_back 26%, none 22%, rock_plates 19%, shell_spiked 15%, shell_round 14% +- **tail:** none 62%, tail_club 13%, tail_spiked 12%, tail_stub 3%, tail_dragon 3%, tail_rattle 3% +- **aura:** none 100% +- **markings:** none 42%, speckles 33%, belly 20%, spots 5% + +### Flying + +- **Body plan:** avian 71%, insectoid 10%, quadruped 10%, serpentine 9% +- **body:** base_hawk 35%, base_owl 21%, base_chick 14%, base_bee 8%, base_eel 3%, base_wyrm 3%, base_noodle 3%, base_drake 2% +- **head_top:** cloud_tuft 51%, none 35%, crest_feather 5%, flower_bloom 2%, leaf_sprout 1%, horn_single 1% +- **back:** cloud_puffs 46%, wings_feather_small 24%, none 15%, wings_feather 9%, leaf_cape 4% +- **tail:** none 72%, tail_feather_fan 24%, tail_stub 2% +- **aura:** none 100% +- **markings:** none 69%, belly 17%, spots 9%, speckles 5% + +### Psychic + +- **Body plan:** biped 39%, quadruped 29%, blob 18%, avian 14% +- **body:** base_pixie 18%, base_wisp 14%, base_fox 13%, base_fawn 12%, base_owl 12%, base_bunny 9%, base_imp 9%, base_cub 2% +- **head_top:** gem_forehead 44%, none 14%, flower_bloom 12%, antennae 7%, wisp_flame 6%, crystal_spikes 4%, ears_long 3%, ears_pointed 2% +- **back:** none 31%, crystal_cluster 28%, cloud_puffs 9%, flower_back 7%, ghost_tatters 6%, wings_bug 5%, wings_bat 5%, wings_feather_small 3% +- **tail:** none 72%, tail_crystal 6%, tail_long_thin 6%, tail_curly 5%, tail_wispy 4%, tail_fluffy 3%, tail_stub 2%, tail_feather_fan 1% +- **aura:** none 100% +- **markings:** none 69%, belly 17%, spots 9%, speckles 5% + +### Bug + +- **Body plan:** insectoid 83%, serpentine 11%, avian 6% +- **body:** base_grub 29%, base_beetle 28%, base_bee 26%, base_eel 4%, base_noodle 4%, base_wyrm 3%, base_penguin 2%, base_hawk 1% +- **head_top:** antennae 31%, mushroom_cap 22%, none 22%, horn_single 20% +- **back:** wings_bug 31%, none 29%, mushroom_back 21%, leaf_cape 18% +- **tail:** none 86%, tail_stinger 9%, tail_leaf 4% +- **aura:** none 100% +- **markings:** none 54%, spots 29%, belly 14%, speckles 4% + +### Rock + +- **Body plan:** quadruped 32%, biped 31%, blob 19%, serpentine 19% +- **body:** base_brawler 22%, base_cub 16%, base_pebble 16%, base_drake 10%, base_wyrm 7%, base_noodle 6%, base_eel 6%, base_pixie 2% +- **head_top:** none 23%, horn_single 22%, horns_ram 17%, gem_forehead 17%, crystal_spikes 12%, spiky_hair 2% +- **back:** crystal_cluster 27%, rock_plates 19%, spikes_back 18%, none 15%, shell_spiked 11%, shell_round 6%, wings_dragon 1% +- **tail:** none 73%, tail_spiked 9%, tail_crystal 8%, tail_club 7%, tail_stub 1% +- **aura:** none 100% +- **markings:** none 46%, speckles 36%, belly 11%, spots 6% + +### Ghost + +- **Body plan:** blob 65%, serpentine 20%, biped 10%, avian 5% +- **body:** base_wisp 50%, base_wyrm 17%, base_slime 10%, base_pixie 6%, base_owl 4%, base_pebble 3%, base_blob 3%, base_imp 2% +- **head_top:** wisp_flame 44%, none 21%, mushroom_cap 8%, gem_forehead 7%, horns_dragon 6%, ears_pointed 3%, ears_round 2%, horn_single 1% +- **back:** ghost_tatters 40%, none 29%, wings_bat 26%, mushroom_back 3% +- **tail:** none 84%, tail_wispy 14% +- **aura:** none 100% +- **markings:** none 69%, belly 17%, spots 9%, speckles 5% + +### Dragon + +- **Body plan:** serpentine 39%, quadruped 29%, biped 27%, avian 4% +- **body:** base_wyrm 25%, base_dino 21%, base_drake 18%, base_eel 12%, base_salamander 8%, base_hawk 2%, base_brawler 2%, base_imp 2% +- **head_top:** none 30%, gem_forehead 29%, horns_dragon 23%, horn_single 7%, ears_fin 2%, spiky_hair 2%, flower_bloom 2% +- **back:** none 28%, wings_dragon 25%, sail_fin 14%, spikes_back 14%, wings_bat 10%, dorsal_fin 3%, flame_mane 1% +- **tail:** none 57%, tail_dragon 23%, tail_spiked 11%, tail_fish_fin 3%, tail_stub 2% +- **aura:** none 100% +- **markings:** none 58%, belly 29%, spots 9%, speckles 5% diff --git a/build_parts.py b/build_parts.py new file mode 100755 index 0000000..494e9c4 --- /dev/null +++ b/build_parts.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Compile text-grid part sources into palette-key PNGs. + +Sources live in parts_src//.txt and compile to parts//.png. +Every non-blank line is one pixel row; lines starting with '#' are comments. All rows +must be the same width. + +Legend + . transparent o outline + 1 main_light 2 main_base 3 main_dark + a accent_light b accent_base c accent_dark + x type_light y type_base z type_dark + w white p pupil + P pivot T tail B back A aura ^ head_top + +Layout sources (parts_src//.layout) assemble stamps onto a fixed canvas, +used for the 84x84 auras: + size 84 84 + stamp [flip] # parts_src//stamps/.txt, top-left at x,y + marker # e.g. "marker P 42 42" +Later stamps paint over earlier ones; '.' in a stamp never paints. + +Usage + python build_parts.py # build everything + python build_parts.py body/quad_slim # build some parts + python build_parts.py mirror FILE # print FILE + its horizontal mirror (shared center column) + python build_parts.py mirror FILE --gap N # mirror with N transparent columns between halves + python build_parts.py flip FILE # print FILE flipped horizontally +""" +import os +import sys +from pathlib import Path + +from PIL import Image + +from generate_sprite import KEYS, MARKERS, PUPIL, EYE_WHITE + +# PARTS_SRC / PARTS_DIR let experiments build into a separate library (e.g. proto/). +SRC = Path(os.environ.get("PARTS_SRC", "parts_src")) +OUT = Path(os.environ.get("PARTS_DIR", "parts")) + +LEGEND = { + "o": KEYS["outline"], + "1": KEYS["main_light"], "2": KEYS["main_base"], "3": KEYS["main_dark"], + "a": KEYS["accent_light"], "b": KEYS["accent_base"], "c": KEYS["accent_dark"], + "x": KEYS["type_light"], "y": KEYS["type_base"], "z": KEYS["type_dark"], + "w": EYE_WHITE, "p": PUPIL, + "P": MARKERS["pivot"], "T": MARKERS["tail"], "B": MARKERS["back"], + "A": MARKERS["aura"], "^": MARKERS["head_top"], +} + + +class SourceError(Exception): + pass + + +def read_grid(path): + rows = [] + for n, line in enumerate(Path(path).read_text().splitlines(), 1): + line = line.rstrip() + if not line or line.startswith("#"): + continue + bad = sorted({c for c in line if c != "." and c not in LEGEND}) + if bad: + raise SourceError(f"{path}:{n}: unknown characters {bad}") + rows.append(line) + if not rows: + raise SourceError(f"{path}: empty grid") + widths = {len(r) for r in rows} + if len(widths) != 1: + for n, r in enumerate(rows): + if len(r) != len(rows[0]): + raise SourceError(f"{path}: row {n} is {len(r)} wide, row 0 is {len(rows[0])}") + return rows + + +def paint(canvas, rows, x0, y0, flip=False): + for y, row in enumerate(rows): + if flip: + row = row[::-1] + for x, c in enumerate(row): + if c == ".": + continue + px, py = x0 + x, y0 + y + if not (0 <= px < len(canvas[0]) and 0 <= py < len(canvas)): + raise SourceError(f"stamp pixel ({px},{py}) outside canvas") + canvas[py][px] = c + + +def read_layout(path): + path = Path(path) + canvas = None + for n, line in enumerate(path.read_text().splitlines(), 1): + parts = line.split("#")[0].split() + if not parts: + continue + cmd, args = parts[0], parts[1:] + try: + if cmd == "size": + w, h = int(args[0]), int(args[1]) + canvas = [["."] * w for _ in range(h)] + elif cmd == "stamp": + rows = read_grid(path.parent / "stamps" / f"{args[0]}.txt") + paint(canvas, rows, int(args[1]), int(args[2]), flip=len(args) > 3 and args[3] == "flip") + elif cmd == "marker": + paint(canvas, [args[0]], int(args[1]), int(args[2])) + else: + raise SourceError(f"unknown command {cmd!r}") + except (IndexError, ValueError, TypeError) as e: + raise SourceError(f"{path}:{n}: {e}") + if canvas is None: + raise SourceError(f"{path}: missing 'size'") + return ["".join(r) for r in canvas] + + +def to_image(rows): + h, w = len(rows), len(rows[0]) + img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + px = img.load() + for y, row in enumerate(rows): + for x, c in enumerate(row): + if c != ".": + px[x, y] = LEGEND[c] + (255,) + return img + + +def sources(): + for p in sorted(SRC.glob("*/*")): + if p.suffix in (".txt", ".layout"): + yield p + + +def build(path): + rows = read_layout(path) if path.suffix == ".layout" else read_grid(path) + out = OUT / path.parent.name / f"{path.stem}.png" + out.parent.mkdir(parents=True, exist_ok=True) + to_image(rows).save(out) + return out, len(rows[0]), len(rows) + + +def main(argv): + if argv and argv[0] in ("mirror", "flip"): + rows = read_grid(argv[1]) + if argv[0] == "flip": + print("\n".join(r[::-1] for r in rows)) + else: + gap = int(argv[3]) if len(argv) > 3 and argv[2] == "--gap" else None + for r in rows: + print(r + "." * gap + r[::-1] if gap is not None else r + r[-2::-1]) + return 0 + wanted = set(argv) + errors = 0 + built = 0 + for path in sources(): + key = f"{path.parent.name}/{path.stem}" + if wanted and key not in wanted: + continue + try: + out, w, h = build(path) + built += 1 + if wanted: + print(f"{out} {w}x{h}") + except SourceError as e: + print(f"ERROR {e}", file=sys.stderr) + errors += 1 + print(f"built {built} part(s), {errors} error(s)") + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/business_pokemon_type.py b/business_pokemon_type.py new file mode 100644 index 0000000..92e3a75 --- /dev/null +++ b/business_pokemon_type.py @@ -0,0 +1,76 @@ +import os +import sys +import json +import requests + +OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") +if not OPENROUTER_API_KEY: + sys.exit("Set OPENROUTER_API_KEY first: export OPENROUTER_API_KEY='sk-or-v1-...'") + +# All 15 Gen 1 types, tuned for home service businesses. Each trade is listed under +# exactly one type, and Rock is limited to stone/roofing work so "construction-y" +# businesses spread out by what they actually do. +POKEMON_TYPES = { + "normal": "General or multi-trade home services: general contractors, remodeling, handymen, painters, drywall, flooring, cabinets, house cleaning and maid services, home inspectors", + "fire": "Heat and engines: heating and furnace repair, fireplaces, chimney sweeps, outdoor kitchens and grills, welding, auto repair and mechanics", + "water": "Water: plumbing, leak detection, water heaters, water damage restoration, pools and spas, irrigation and sprinklers, pressure washing, car washes and auto detailing", + "grass": "Plants and yards: landscaping, lawn care and mowing, tree service, sod, gardening, nurseries, mulch", + "electric": "Electricity and tech: electricians, lighting, solar, generators, EV chargers, smart home, home theater, alarm and camera systems, appliance repair", + "ice": "Cooling: air conditioning repair and install, refrigeration, insulation, ice machines, cold storage", + "fighting": "Protection and heavy labor: fencing and gates, security, demolition, moving companies, hauling, furniture assembly", + "poison": "Waste and contaminants: junk removal, dumpster rental, septic, mold remediation, asbestos and lead abatement, hazardous waste", + "ground": "Earth and dirt work: foundation repair, excavation, grading, drainage and French drains, concrete, driveways, paving, patios and hardscaping", + "flying": "Air and high places: gutters, air duct cleaning, dryer vents, indoor air quality, window cleaning, drones, couriers", + "psychic": "Knowledge and planning: interior design, architects, real estate, property management, home warranties, insurance, consulting, marketing", + "bug": "Insects and critters: pest control, exterminators, termite treatment, wildlife and rodent removal, beekeeping", + "rock": "Stone only: roofing, masonry, brick, stone veneer, granite and quartz countertops, tile, retaining walls", + "ghost": "Hidden spaces and the unseen: attics, crawl spaces, basement waterproofing, locksmiths, estate cleanouts, trauma and crime scene cleanup, funeral homes", + "dragon": "Premium or rare: luxury custom homes, high-end remodels, custom pools, exotic or performance auto shops, elite luxury brands", +} + + +def get_business_type(description: str) -> dict: + response = requests.post( + url="https://openrouter.ai/api/alpha/decisions", + headers={ + "Authorization": f"Bearer {OPENROUTER_API_KEY}", + "Content-Type": "application/json", + }, + data=json.dumps({ + "model": "typesafe/jev-1.13", + "state": f"Business description: {description}", + "questions": { + "pokemon_type": { + "type": "choice", + "instructions": ( + "Which Gen 1 Pokémon type best matches this business? Decide by its " + "specific trade or main service, not by the fact that it is construction, " + "home service, or physical work in general. Being construction-related is " + "not a reason to choose Rock; choose Rock only for roofing or stone work. " + "If the description is only a business name, use the trade the name implies." + ), + "criteria": POKEMON_TYPES, + } + }, + }), + timeout=30, + ) + response.raise_for_status() + return response.json()["answers"]["pokemon_type"] + + +if __name__ == "__main__": + desc = " ".join(sys.argv[1:]) or "Residential lawn mowing, hedge trimming, and flower bed installation" + answer = get_business_type(desc) + + choice = answer["choice"] + probs = answer["probabilities"] + top3 = sorted(probs.items(), key=lambda kv: kv[1], reverse=True)[:3] + + print(f"Business: {desc}") + print(f"Type: {choice.upper()} ({probs[choice]:.0%})") + print("Top 3: " + ", ".join(f"{t} {p:.0%}" for t, p in top3)) + + # Low confidence usually means a vague description or a true dual-type business + if probs[choice] < 0.5: + print(f"Low confidence. Possible dual type: {top3[0][0]}/{top3[1][0]}") diff --git a/generate_sprite.py b/generate_sprite.py new file mode 100644 index 0000000..e52c8a2 --- /dev/null +++ b/generate_sprite.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python3 +""" +Generate a Pokémon-style sprite for a business. + +Pipeline: Jev picks a type -> seeded RNG picks a base creature and its variation parts +(weighted by type) -> parts snap together via anchor pixels -> markings painted on -> +palette keys swapped for colors pulled from the business logo. + +Logo colors: transparent pixels, anti-aliased edges and a solid white/black/gray background +(the colour filling the image border) are ignored; hued brand colours win over white, black +or gray text. A logo that is only white/black (or only its background) still uses that. + +Usage + python generate_sprite.py "Rosas Landscaping" logos/rosas.png # Jev picks the type + python generate_sprite.py "Rosas Landscaping" logos/rosas.png --type grass + python generate_sprite.py "Rosas Landscaping" --type grass --colors "#2E7D32,#FBC02D" + python generate_sprite.py --batch businesses.csv # columns: name,logo,type,description + python generate_sprite.py --make-placeholders # test art for every part + python generate_sprite.py --sprite-list SPRITES.md # art checklist + weight report + +Requires: pip install pillow numpy requests +""" +import argparse +import colorsys +import csv +import hashlib +import json +import random +import re +import sys +from collections import Counter +from pathlib import Path + +import numpy as np +from PIL import Image, ImageDraw + +import sprite_parts as cfg + +CANVAS = 96 # final sprite size (square) +GROUND_MARGIN = 4 # empty rows under the feet + +# Palette keys: draw every part using ONLY these exact colors (plus pass-through colors +# like eye white). They get swapped for logo-derived colors at render time. +KEYS = { + "main_light": (255, 128, 128), "main_base": (255, 0, 0), "main_dark": (128, 0, 0), + "accent_light": (128, 255, 128), "accent_base": (0, 255, 0), "accent_dark": (0, 128, 0), + "type_light": (128, 128, 255), "type_base": (0, 0, 255), "type_dark": (0, 0, 128), + "outline": (0, 0, 0), +} +PUPIL = (20, 20, 20) # near-black that is NOT recolored +EYE_WHITE = (255, 255, 255) + +# Anchor marker pixels. Place one pixel of the color where a child part attaches. +# They are read, then replaced with the surrounding color. +MARKERS = { + "pivot": (255, 0, 255), "tail": (253, 0, 255), "back": (252, 0, 255), + "aura": (251, 0, 255), "head_top": (250, 0, 255), +} + +PICK_ORDER = ["body", "head_top", "back", "tail", "aura"] +DEFAULT_Z = {"aura": 0, "back": 10, "tail": 20, "body": 30, "head_top": 50} + + +def warn(msg): + print(f" ! {msg}", file=sys.stderr) + + +# ============================================================ seeded picking + +def make_rng(name, reroll=0): + key = f"{name.strip().lower()}|{reroll}" + return random.Random(int(hashlib.sha256(key.encode()).hexdigest()[:16], 16)) + + +def weighted_pick(rng, items): + items = [(it, w) for it, w in items if w > 0] + total = sum(w for _, w in items) + if total <= 0: + return None + r = rng.random() * total + acc = 0.0 + for it, w in items: + acc += w + if r < acc: + return it + return items[-1][0] + + +def part_weight(p, ptype): + return p["types"].get(ptype, p["other"]) + + +def candidates(slot, plan, ptype, tags, excluded, denied=()): + out = [] + for p in cfg.SLOTS[slot]: + if plan not in p["plans"] or p["name"] in denied: + continue + if p["requires"] and not set(p["requires"]) & tags: + continue + if set(p["tags"]) & excluded or set(p["excludes"]) & tags: + continue + out.append((p, part_weight(p, ptype))) + return out + + +def motif_factor(p, motifs): + """Keep one creature's features on a theme: same motif boosted, clashing motifs damped.""" + m = cfg.MOTIFS.get(p["name"]) + if m is None or not motifs: + return 1.0 + if m in motifs: + return cfg.MOTIF_SAME + if all(frozenset((m, o)) in cfg.MOTIF_PAIRS for o in motifs): + return 1.0 + return cfg.MOTIF_CLASH + + +def pick_parts(ptype, rng): + plan = weighted_pick(rng, cfg.PLAN_WEIGHTS[ptype].items()) + chosen, tags, excluded, denied, motifs = {}, {f"plan:{plan}"}, set(), set(), set() + features = 0 + for slot in PICK_ORDER: + if slot in cfg.DISABLED_SLOTS: + continue + cands = candidates(slot, plan, ptype, tags, excluded, denied) + if slot in cfg.FEATURE_SLOTS: + if features >= cfg.MAX_FEATURES: + cands = [(p, w) for p, w in cands if p["name"] == "none"] + cands = [(p, w * motif_factor(p, motifs)) for p, w in cands] + p = weighted_pick(rng, cands) + if p is None: + continue + chosen[slot] = p + tags |= set(p["tags"]) + excluded |= set(p["excludes"]) + denied |= set(p.get("deny", ())) + if p["name"] != "none" and slot in cfg.FEATURE_SLOTS: + features += 1 + if cfg.MOTIFS.get(p["name"]): + motifs.add(cfg.MOTIFS[p["name"]]) + markings = weighted_pick(rng, [(m, w.get(ptype, w.get("_other", 0))) for m, w in cfg.MARKINGS.items()]) + return plan, chosen, markings, tags + + +def attributes_from(tags): + """Public attributes: drop compatibility plumbing (*_head family tags, _internal tags, plan).""" + return sorted(t for t in tags if not t.endswith("_head") and not t.startswith(("_", "plan:"))) + + +# ============================================================ loading + compositing + +_part_cache = {} + + +def load_part(parts_dir, slot, name): + path = Path(parts_dir) / slot / f"{name}.png" + if path in _part_cache: + return _part_cache[path] + if not path.exists(): + _part_cache[path] = None + return None + arr = np.array(Image.open(path).convert("RGBA")) + rgb, alpha = arr[:, :, :3], arr[:, :, 3] > 0 + anchors, marker_mask = {}, np.zeros(alpha.shape, bool) + for mname, col in MARKERS.items(): + m = np.all(rgb == col, axis=-1) & alpha + ys, xs = np.nonzero(m) + if len(xs): + anchors[mname] = (int(round(xs.mean())), int(round(ys.mean()))) + marker_mask |= m + # Replace marker pixels with the most common opaque, non-marker 4-neighbor + h, w = alpha.shape + for y, x in zip(*np.nonzero(marker_mask)): + neigh = [tuple(arr[ny, nx]) for ny, nx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1)) + if 0 <= ny < h and 0 <= nx < w and arr[ny, nx, 3] > 0 and not marker_mask[ny, nx]] + arr[y, x] = Counter(neigh).most_common(1)[0][0] if neigh else (0, 0, 0, 0) + result = (Image.fromarray(arr), anchors) + _part_cache[path] = result + return result + + +def key_mask(arr, key): + return np.all(arr[:, :, :3] == KEYS[key], axis=-1) & (arr[:, :, 3] > 0) + + +def apply_markings(img, kind, rng, center=None): + """Paint accent markings onto main-ramp pixels. + + center: the body's aura anchor (torso centre). When given, markings stay on the body + below the face (rows from a little above the torso centre down) and the belly patch + is centred there.""" + if kind in (None, "none"): + return img + arr = np.array(img) + h, w = arr.shape[:2] + main = key_mask(arr, "main_light") | key_mask(arr, "main_base") | key_mask(arr, "main_dark") + ys, xs = np.mgrid[0:h, 0:w] + if center: + main &= ys >= center[1] - h * 0.12 + if not main.any(): + return img + if kind == "spots": + pat = np.zeros((h, w), bool) + pts = np.argwhere(main) + for _ in range(rng.randint(4, 8)): + y, x = pts[rng.randrange(len(pts))] + r = rng.randint(1, 3) + pat |= (xs - x) ** 2 + (ys - y) ** 2 <= r * r + elif kind == "speckles": + pat = np.random.default_rng(rng.randrange(2 ** 32)).random((h, w)) < 0.1 + elif kind == "belly": + oy, ox = np.nonzero(arr[:, :, 3] > 0) + x0, x1, y0, y1 = ox.min(), ox.max(), oy.min(), oy.max() + cx, cy = center if center else ((x0 + x1) / 2, y0 + (y1 - y0) * 0.62) + rx, ry = max((x1 - x0) * 0.2, 1), max((y1 - y0) * 0.17, 1) + pat = ((xs - cx) / rx) ** 2 + ((ys - cy) / ry) ** 2 <= 1 + else: + return img + mask = pat & main + for src, dst in (("main_light", "accent_light"), ("main_base", "accent_base"), ("main_dark", "accent_dark")): + arr[mask & key_mask(arr, src), :3] = KEYS[dst] + return Image.fromarray(arr) + + +def outline_behind(canvas, body, pos): + """Where parts drawn behind the body (back, tail, rear ears) touch the body's silhouette, + add a second outline pixel so the creature's own shape stays readable against them.""" + arr = np.array(canvas) + h, w = arr.shape[:2] + m = np.zeros((h, w), bool) + x, y = pos + m[y:y + body.height, x:x + body.width] = np.array(body)[:, :, 3] > 0 + ring = np.zeros_like(m) + ring[1:] |= m[:-1] + ring[:-1] |= m[1:] + ring[:, 1:] |= m[:, :-1] + ring[:, :-1] |= m[:, 1:] + hit = ring & ~m & (arr[:, :, 3] > 0) + arr[hit] = KEYS["outline"] + (255,) + return Image.fromarray(arr) + + +def compose(parts_dir, chosen, markings, rng): + body = chosen["body"] + loaded = load_part(parts_dir, "body", body["name"]) + if loaded is None: + raise FileNotFoundError(f"Missing body sprite: {parts_dir}/body/{body['name']}.png") + bimg, banch = loaded + bimg = apply_markings(bimg, markings, rng, banch.get("aura")) + bpiv = banch.get("pivot", (bimg.width // 2, bimg.height - 1)) + bpos = (-bpiv[0], -bpiv[1]) + + layers = [(body["z"] or DEFAULT_Z["body"], 0, bimg, bpos)] + for i, slot in enumerate(PICK_ORDER[1:], 1): + p = chosen.get(slot) + if not p or p["name"] == "none": + continue + loaded = load_part(parts_dir, slot, p["name"]) + if loaded is None: + warn(f"missing sprite {slot}/{p['name']}.png, skipped") + continue + img, anch = loaded + a = banch.get(slot) + if a is None: + warn(f"no '{slot}' anchor on {body['name']}, {p['name']} skipped") + continue + piv = anch.get("pivot", (img.width // 2, img.height // 2)) + pos = (bpos[0] + a[0] - piv[0], bpos[1] + a[1] - piv[1]) + layers.append((p["z"] if p["z"] is not None else DEFAULT_Z[slot], i, img, pos)) + + layers.sort(key=lambda l: (l[0], l[1])) + minx = min(l[3][0] for l in layers) + miny = min(l[3][1] for l in layers) + maxx = max(l[3][0] + l[2].width for l in layers) + maxy = max(l[3][1] + l[2].height for l in layers) + canvas = Image.new("RGBA", (maxx - minx, maxy - miny), (0, 0, 0, 0)) + for _, i, img, (x, y) in layers: + if i == 0: + canvas = outline_behind(canvas, img, (x - minx, y - miny)) + canvas.alpha_composite(img, (x - minx, y - miny)) + return canvas.crop(canvas.getbbox()) + + +def place_on_canvas(sprite): + w, h = sprite.size + size = max(CANVAS, w + 2, h + GROUND_MARGIN + 1) + if size > CANVAS: + warn(f"sprite is {w}x{h}, canvas expanded to {size}px") + out = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + out.alpha_composite(sprite, ((size - w) // 2, size - GROUND_MARGIN - h)) + return out + + +def recolor(img, palette): + arr = np.array(img) + masks = {k: key_mask(arr, k) for k in palette} # compute all before writing + for k, rgb in palette.items(): + arr[masks[k], :3] = rgb + return Image.fromarray(arr) + + +# ============================================================ colors + +def hex_to_rgb(h): + h = h.strip().lstrip("#") + return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + + +def rgb_to_hex(c): + return "#{:02X}{:02X}{:02X}".format(*c) + + +def to_hls(rgb): + return colorsys.rgb_to_hls(*(c / 255 for c in rgb)) + + +def from_hls(h, l, s): + return tuple(int(round(c * 255)) for c in colorsys.hls_to_rgb(h % 1, min(max(l, 0), 1), min(max(s, 0), 1))) + + +def hue_toward(h, target, amount): + d = ((target - h + 0.5) % 1.0) - 0.5 + return (h + max(-amount, min(amount, d))) % 1.0 + + +def color_dist(a, b): + rmean = (a[0] + b[0]) / 2 + dr, dg, db = a[0] - b[0], a[1] - b[1], a[2] - b[2] + return ((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db) ** 0.5 + + +def rotate_hue(rgb, amount): + h, l, s = to_hls(rgb) + return from_hls(h + amount, l, s) + + +def ramp(rgb): + """light/base/dark with hue shifting: highlights lean yellow, shadows lean blue.""" + h, l, s = to_hls(rgb) + l = min(max(l, 0.25), 0.75) + light = from_hls(hue_toward(h, 1 / 6, 0.03), min(l + 0.17, 0.95), s * 0.92) + base = from_hls(h, l, s) + dark = from_hls(hue_toward(h, 2 / 3, 0.035), max(l - 0.2, 0.06), s * 1.08) + return light, base, dark + + +def outline_color(rgb): + h, l, s = to_hls(rgb) + return from_hls(hue_toward(h, 2 / 3, 0.05), min(l * 0.35, 0.16), s * 0.8) + + +def _dist_to(px, rgb): + """color_dist from many pixels (N,3 float) to one colour.""" + r, g, b = rgb + rmean = (px[:, 0] + r) / 2 + dr, dg, db = px[:, 0] - r, px[:, 1] - g, px[:, 2] - b + return np.sqrt((2 + rmean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rmean) / 256) * db * db) + + +def is_neutral(rgb): + """White, black or gray: carries no brand hue.""" + mx, mn = max(rgb), min(rgb) + light, chroma = (mx + mn) / 510, (mx - mn) / 255 + return chroma < 0.12 or light > 0.93 or light < 0.08 + + +def logo_background(arr): + """The solid background colour of a logo, if its border is mostly one opaque colour.""" + h, w = arr.shape[:2] + border = np.concatenate([arr[0], arr[-1], arr[:, 0], arr[:, -1]]) + opaque = border[border[:, 3] >= 200][:, :3].astype(float) + if len(opaque) < 0.4 * len(border): + return None # transparent or busy border: no solid background + best = None + for c in opaque[:: max(1, len(opaque) // 64)]: + share = np.mean(_dist_to(opaque, c) < 40) + if best is None or share > best[1]: + best = (c, share) + if best[1] < 0.4: + return None + near = opaque[_dist_to(opaque, best[0]) < 40] + return tuple(int(v) for v in near.mean(0)) + + +def _quantize(px, n): + q = Image.fromarray(px.astype(np.uint8).reshape(1, -1, 3), "RGB").quantize(colors=n, method=Image.Quantize.MEDIANCUT) + pal, counts = q.getpalette(), q.getcolors() + total = sum(c for c, _ in counts) + merged = [] + for rgb, frac in sorted(((tuple(pal[i * 3:i * 3 + 3]), c / total) for c, i in counts), key=lambda cf: -cf[1]): + for m in merged: + if color_dist(m[0], rgb) < 40: + m[1] += frac + break + else: + merged.append([rgb, frac]) + return [(tuple(c), f) for c, f in merged] + + +def _interior(arr, tol=24): + """Opaque pixels whose 4 neighbours are opaque and nearly the same colour. Drops the + anti-aliased blends at shape edges, which would otherwise show up as muddy extra colours.""" + rgb = arr[:, :, :3].astype(float) + ok = arr[:, :, 3] >= 200 + keep = ok.copy() + for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)): + nb = np.roll(np.roll(rgb, dy, 0), dx, 1) + nok = np.roll(np.roll(ok, dy, 0), dx, 1) + keep &= nok & (np.abs(rgb - nb).max(-1) < tol) + keep[0, :] = keep[-1, :] = keep[:, 0] = keep[:, -1] = False + return keep + + +def extract_logo_colors(path, n=6): + """Return [(rgb, fraction)] of a logo's brand colours, most important first. + + - Transparent pixels and anti-aliased edge blends are ignored. + - A solid white/black/gray background (the colour filling most of the image border) is + removed. A coloured background is kept: it is part of the brand. + - Brand hues come first; neutral colours (white, black, gray) that are part of the logo + itself rank after every hued colour. + - If nothing but the background is left (e.g. a plain logo on its own colour), the + background colour is used rather than nothing. + """ + img = Image.open(path).convert("RGBA") + img.thumbnail((256, 256)) + arr = np.array(img) + opaque = arr[:, :, 3] >= 200 + if not opaque.any(): + return [] + inner = _interior(arr) + if inner.sum() < 0.25 * opaque.sum(): # tiny or noisy logo: use every opaque pixel + inner = opaque + px = arr[inner][:, :3].astype(float) + bg = logo_background(arr) + if bg is not None and is_neutral(bg): + fg = px[_dist_to(px, bg) >= 60] + if len(fg) < max(20, 0.002 * len(px)): + return [(bg, 1.0)] + px = fg + found = _quantize(px, n) + hued = [cf for cf in found if not is_neutral(cf[0])] + neutral = [cf for cf in found if is_neutral(cf[0])] + return sorted(hued, key=lambda cf: -cf[1]) + sorted(neutral, key=lambda cf: -cf[1]) + + +def pick_main_accent(colors, ptype): + type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype]) + if not colors: + main = type_rgb + accent = rotate_hue(type_rgb, 0.5) + else: + main = colors[0][0] + cands = [(c, f) for c, f in colors[1:] if f >= 0.04 and color_dist(c, main) >= 90] + if cands: + # hued brand colours beat white/black/gray text for the accent + accent = max(cands, key=lambda cf: color_dist(cf[0], main) * cf[1] ** 0.5 + * (0.6 if is_neutral(cf[0]) else 1.0))[0] + elif color_dist(type_rgb, main) >= 90: + accent = type_rgb + else: + accent = rotate_hue(main, 0.5) + # Keep main and accent readable against each other + (_, lm, _), (ha, la, sa) = to_hls(main), to_hls(accent) + if abs(lm - la) < 0.12 and color_dist(main, accent) < 140: + la = la + 0.22 if lm < 0.55 else la - 0.22 + accent = from_hls(ha, la, sa) + return main, accent + + +def build_palette(main, accent, ptype): + type_rgb = hex_to_rgb(cfg.TYPE_COLORS[ptype]) + # Elemental parts (leaves, flames, sparks) must read against the body: if the type colour + # is close to the main colour, push its lightness away from the main colour. + if color_dist(type_rgb, main) < 150: + (_, lm, _), (ht, lt, st) = to_hls(main), to_hls(type_rgb) + type_rgb = from_hls(ht, lm + 0.24 if lm < 0.5 else lm - 0.24, st) + pal = {} + for group, rgb in (("main", main), ("accent", accent), ("type", type_rgb)): + pal[f"{group}_light"], pal[f"{group}_base"], pal[f"{group}_dark"] = ramp(rgb) + pal["outline"] = outline_color(main) + return pal + + +# ============================================================ generate + +def slugify(s): + return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "business" + + +def classify(description): + try: + from business_pokemon_type import get_business_type # imported lazily so --type works offline + except ImportError: + sys.exit("business_pokemon_type.py not found next to this script; pass --type instead.") + except SystemExit: + sys.exit("No --type given and OPENROUTER_API_KEY is not set: export it or pass --type .") + ans = get_business_type(description) + return ans["choice"].lower(), ans.get("probabilities") + + +def generate(name, parts_dir, out_dir, ptype=None, description=None, logo=None, colors=None, + reroll=0, scale=4): + probs = None + if not ptype: + ptype, probs = classify(description or name) + ptype = ptype.lower() + if ptype not in cfg.PLAN_WEIGHTS: + sys.exit(f"Unknown type '{ptype}'. Options: {', '.join(cfg.TYPES)}") + + rng = make_rng(name, reroll) + plan, chosen, markings, tags = pick_parts(ptype, rng) + + if colors: + logo_colors = [(hex_to_rgb(c), 1.0) for c in colors] + elif logo: + logo_colors = extract_logo_colors(logo) + if not logo_colors: + warn("no usable colors in logo, falling back to type color") + else: + logo_colors = [] + main, accent = pick_main_accent(logo_colors, ptype) + palette = build_palette(main, accent, ptype) + + sprite = recolor(place_on_canvas(compose(parts_dir, chosen, markings, rng)), palette) + + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + slug = slugify(name) + (f"-r{reroll}" if reroll else "") + sprite.save(out_dir / f"{slug}.png") + if scale > 1: + sprite.resize((sprite.width * scale, sprite.height * scale), Image.NEAREST).save(out_dir / f"{slug}@{scale}x.png") + + info = { + "name": name, "type": ptype, "type_probabilities": probs, "plan": plan, + "parts": {s: p["name"] for s, p in chosen.items()}, "markings": markings, + "attributes": attributes_from(tags), + "colors": {"main": rgb_to_hex(main), "accent": rgb_to_hex(accent), + "type": cfg.TYPE_COLORS[ptype], "outline": rgb_to_hex(palette["outline"])}, + } + (out_dir / f"{slug}.json").write_text(json.dumps(info, indent=2)) + print(f"{name}: {ptype.upper()} {plan} | {', '.join(info['attributes']) or 'no attributes'} | " + f"main {info['colors']['main']} accent {info['colors']['accent']} -> {out_dir / slug}.png") + return info + + +# ============================================================ placeholders + +BASE_ANCHORS = {"pivot": (0.5, 1.0), "head_top": (0.35, 0.08), "back": (0.7, 0.35), "tail": (0.9, 0.7), + "aura": (0.5, 0.55)} +CHILD_ANCHORS = { + "head_top": {"pivot": (0.5, 1.0)}, "back": {"pivot": (0.5, 0.85)}, "tail": {"pivot": (0.05, 0.6)}, + "aura": {"pivot": (0.5, 0.5)}, +} +DEFAULT_SIZE = {"head_top": (16, 14), "back": (40, 30), "tail": (26, 14), "aura": (84, 84)} + + +def anchors_for(slot, p): + return BASE_ANCHORS if slot == "body" else CHILD_ANCHORS[slot] + + +def size_for(slot, p): + return tuple(p["size"] or DEFAULT_SIZE[slot]) + + +def _shaded(d, box, pal): + x0, y0, x1, y1 = [int(v) for v in box] + w, h = x1 - x0, y1 - y0 + d.ellipse((x0, y0, x1, y1), fill=KEYS[f"{pal}_dark"], outline=KEYS["outline"]) + if w > 4 and h > 4: + d.ellipse((x0 + 1, y0 + 1, x1 - max(2, int(w * 0.2)), y1 - max(2, int(h * 0.25))), fill=KEYS[f"{pal}_base"]) + d.ellipse((x0 + int(w * 0.2), y0 + int(h * 0.15), x0 + int(w * 0.4), y0 + int(h * 0.3)), fill=KEYS[f"{pal}_light"]) + + +def _rect(d, box, pal): + d.rectangle([int(v) for v in box], fill=KEYS[f"{pal}_base"], outline=KEYS["outline"]) + + +def draw_placeholder(slot, p): + w, h = size_for(slot, p) + img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + pal = p["palette"] + if slot == "body": # base creature: round body with a big head and two dot eyes + _shaded(d, (w * 0.3, h * 0.45, w - 1, h - 1), pal) + _shaded(d, (0, 0, w * 0.65, h * 0.7), pal) + for ex in (0.2, 0.42): + d.rectangle((int(w * ex), int(h * 0.3), int(w * ex) + 1, int(h * 0.3) + 2), fill=PUPIL) + elif slot == "head_top": + d.polygon([(0, h - 1), (w // 2, 0), (w - 1, h - 1)], fill=KEYS[f"{pal}_base"], outline=KEYS["outline"]) + d.line([(w // 2, 2), (w // 2, h - 3)], fill=KEYS[f"{pal}_light"]) + elif slot == "aura": + rng = random.Random(p["name"]) + for _ in range(14): + x, y = rng.randrange(4, w - 8), rng.randrange(4, h - 8) + r = rng.randint(2, 3) + d.ellipse((x, y, x + r * 2, y + r * 2), fill=KEYS["type_base"], outline=KEYS["type_dark"]) + else: + _shaded(d, (0, 0, w - 1, h - 1), pal) + + for mname, (fx, fy) in anchors_for(slot, p).items(): + img.putpixel((min(int(fx * w), w - 1), min(int(fy * h), h - 1)), MARKERS[mname] + (255,)) + return img + + +def make_placeholders(parts_dir, overwrite=False): + made = 0 + for slot, parts in cfg.SLOTS.items(): + (Path(parts_dir) / slot).mkdir(parents=True, exist_ok=True) + for p in parts: + if p["name"] == "none": + continue + path = Path(parts_dir) / slot / f"{p['name']}.png" + if path.exists() and not overwrite: + continue + draw_placeholder(slot, p).save(path) + made += 1 + print(f"Wrote {made} placeholder sprites to {parts_dir}/") + + +# ============================================================ sprite list + weight report + +def simulate(ptype, n=3000): + plans, slots, marks = Counter(), {s: Counter() for s in PICK_ORDER}, Counter() + for i in range(n): + plan, chosen, m, _ = pick_parts(ptype, random.Random(i)) + plans[plan] += 1 + marks[m] += 1 + for s in PICK_ORDER: + slots[s][chosen[s]["name"] if s in chosen else "none"] += 1 + return plans, slots, marks + + +def pct(counter, n, top=None): + return ", ".join(f"{k} {v / n:.0%}" for k, v in counter.most_common(top) if v / n >= 0.01) + + +def write_sprite_list(path): + L = ["# Sprite checklist", ""] + total = sum(1 for parts in cfg.SLOTS.values() for p in parts if p["name"] != "none") + L += [f"**{total} sprites** across {len(cfg.SLOTS)} slots. Save each as `parts//.png`.", "", + "## Art rules", "", + f"- Final canvas is {CANVAS}x{CANVAS}. Sizes below are suggested part canvases; crop tight with transparency.", + "- Characters face left in a 3/4 view, light from the top-left. 1px outline, no anti-aliasing, no semi-transparent pixels.", + "- Draw only with the palette keys below. Any other color (eye white, pupils) is left as-is.", ""] + L += ["| Key | Hex | Use |", "|---|---|---|"] + uses = {"main": "body / most fur and skin", "accent": "horns, wings, beaks, crests, shells", + "type": "elemental bits: flames, leaves, sparks, ice, water"} + for k, v in KEYS.items(): + L.append(f"| {k} | {rgb_to_hex(v)} | {'outline (recolored)' if k == 'outline' else uses[k.split('_')[0]]} |") + L += [f"| pupil | {rgb_to_hex(PUPIL)} | not recolored |", f"| eye white | {rgb_to_hex(EYE_WHITE)} | not recolored |", ""] + L += ["### Anchor marker pixels", "", + "One pixel each. `pivot` is where the part attaches to its parent; the others are where children attach.", "", + "| Marker | Hex | Needed in |", "|---|---|---|", + f"| pivot | {rgb_to_hex(MARKERS['pivot'])} | every part (bodies: bottom center between the feet) |", + f"| head_top | {rgb_to_hex(MARKERS['head_top'])} | all bodies (top of the head) |", + f"| tail | {rgb_to_hex(MARKERS['tail'])} | all bodies |", + f"| back | {rgb_to_hex(MARKERS['back'])} | all bodies |", + f"| aura | {rgb_to_hex(MARKERS['aura'])} | all bodies (center of mass; belly patch centre) |", ""] + + for slot in ["body", "head_top", "back", "tail", "aura"]: + parts = [p for p in cfg.SLOTS[slot] if p["name"] != "none"] + L += [f"## {slot} ({len(parts)})", "", "| Done | File | Size | Palette | Plans | Description | Most likely for |", + "|---|---|---|---|---|---|---|"] + for p in parts: + top = sorted(cfg.TYPES, key=lambda t: -part_weight(p, t))[:3] + top = ", ".join(t for t in top if part_weight(p, t) > 0) + plans = "all" if p["plans"] == cfg.ALL else ", ".join(p["plans"]) + w, h = size_for(slot, p) + L.append(f"| [ ] | `{slot}/{p['name']}.png` | {w}x{h} | {p['palette']} | {plans} | {p['desc']} | {top} |") + L.append("") + + L += ["## Simulated pick rates per type", "", + "From 3,000 random seeds per type. Use this to spot parts that never show up or show up too much.", ""] + for t in cfg.TYPES: + n = 3000 + plans, slots, marks = simulate(t, n) + L += [f"### {t.capitalize()}", "", f"- **Body plan:** {pct(plans, n)}"] + for s in PICK_ORDER: + L.append(f"- **{s}:** {pct(slots[s], n, 8)}") + L += [f"- **markings:** {pct(marks, n)}", ""] + Path(path).write_text("\n".join(L)) + print(f"Wrote {path}") + + +# ============================================================ CLI + +def main(): + ap = argparse.ArgumentParser(description="Generate Pokémon-style business sprites") + ap.add_argument("name", nargs="?", help="Business name (also the random seed)") + ap.add_argument("logo_file", nargs="?", help="Logo image to pull colors from (same as --logo)") + ap.add_argument("--desc", help="Business description for Jev (defaults to the name)") + ap.add_argument("--type", help="Skip Jev and use this type") + ap.add_argument("--logo", help="Logo image to pull colors from") + ap.add_argument("--colors", help='Manual colors, e.g. "#2E7D32,#FBC02D" (main first)') + ap.add_argument("--reroll", type=int, default=0, help="Alternate creature for the same business") + ap.add_argument("--parts", default="parts", help="Parts folder") + ap.add_argument("--out", default="sprites", help="Output folder") + ap.add_argument("--scale", type=int, default=4, help="Also save an upscaled copy (1 to disable)") + ap.add_argument("--batch", help="CSV with columns name,logo,type,description (only name required)") + ap.add_argument("--make-placeholders", action="store_true", help="Create test art for every part") + ap.add_argument("--overwrite", action="store_true", help="With --make-placeholders, replace existing files") + ap.add_argument("--sprite-list", metavar="PATH", help="Write the art checklist and weight report") + a = ap.parse_args() + + if a.make_placeholders: + make_placeholders(a.parts, a.overwrite) + if a.sprite_list: + write_sprite_list(a.sprite_list) + colors = a.colors.split(",") if a.colors else None + if a.batch: + with open(a.batch, newline="", encoding="utf-8") as f: + for row in csv.DictReader(f): + if not (row.get("name") or "").strip(): + continue + generate(row["name"].strip(), a.parts, a.out, ptype=(row.get("type") or "").strip() or None, + description=(row.get("description") or "").strip() or None, + logo=(row.get("logo") or "").strip() or None, scale=a.scale) + elif a.name: + logo = a.logo or a.logo_file + if logo and not Path(logo).exists(): + sys.exit(f"Logo not found: {logo}") + generate(a.name, a.parts, a.out, a.type, a.desc, logo, colors, a.reroll, a.scale) + elif not (a.make_placeholders or a.sprite_list): + ap.print_help() + + +if __name__ == "__main__": + main() diff --git a/legacy/parts/body/avian_perched.png b/legacy/parts/body/avian_perched.png new file mode 100644 index 0000000000000000000000000000000000000000..e982f03f01c9e585dbccdafe31802dde1053b3d3 GIT binary patch literal 418 zcmV;T0bTxyP)1C1Cbu2ajzJa|Bq@b8FqkK=16m<=u+tp@Fd_!dn;g+g zD&zIT7sA_$R~2B>LrQS&CvbQu*DWPti+G4!L6i5CAV7S!?vVEEkP1|j&+OO}NG?+b z=O)B|mmA1ipysifA#bd;(h$o;nUQf9R0`NS1$4S2yO805*bSzj-`$YI0irBRUSFgY z;BH^DR9LAGOM#%^?e!YZz+nSbB)dln1YLtO6!MZ1 z2iCY!;YD6~D^4Pz)^qJmx|KJ*2KXT?XzgJ&ofGzT4V{?I^~-Y_dpLJDp=MOwjI54TK; zPeL)U1nWDvHqX)rov!Z72wPoBJ>c+_oqgCnp(G>&et=<7oVwEOo&o#d3Dx!j?O?yI zRzQ84YXC?wY*juSpEbV=dW&NXkYYIffh|VAUUTJe3tyosYTZ4o`z6f>-gXATC{$|Txciw)1!WOi1N7|@L}dyk2HF=U zdh<503#q_j9ybB%0M3C3!Ljl6Q|}0E_q))g@DWr}%yYmXq4=Uaq~GfU%!XRE{4Em> z71R!p2Uda4tNnesa+d!yu4Me5^~rn)+;JtxpgquGC1xp%3UFxAdF^{xhTd%o@Twfn zC9r6@-D2^aZlkQsRyYqVWXi*St;#E>?#P0rm_+&ZJgD_C>)}J?NXk4N@M6GjZH!>` zsPtNyP)>3R18n=AT25*z&xQ=81hu2Qr0V}`zNhOZuoYZQgG@dvAT8FPSZ)Ivux+Wy z?ON^3zq6rrP6Ou%Wd1x+TZ8B7V)v^m+(BQAB6JqGplCP;+0TtH1e}a1l$1h8fM(X9 zXVGDBaY(PuyaqkR&s*6ii-Y#}n)YbqQ8*lIuVLNgM@exrQ)0%UP&)c*DN#>Aw`q^& zBL#51(01&VnlEJ9Ny ziw?pd3`HsW+eVDPZE_8Ukq%2+I!#P$vSnk?(-&na{hLr=4L+fZR)G$xOK0{%Awa=K z*!P)jJ0UN0C#A^Gd!=*)j2%>fMB&*0+feIU01Q{MX0|jP5%5PeQe&b3f@T{6`Kx;n5 zDJ;j_2o*e=D0zD3pK-l;Yp0IpxorRd N002ovPDHLkV1j^6oHzgg literal 0 HcmV?d00001 diff --git a/legacy/parts/body/biped_tall.png b/legacy/parts/body/biped_tall.png new file mode 100644 index 0000000000000000000000000000000000000000..b7157df8b3f0ff017699f93ea91d407aa4ab43eb GIT binary patch literal 573 zcmV-D0>b@?P)%6Wa*{GvFSQgw6)Z$7~=Fn#&CC2LoFqR<)Whruq>!py(ArEUAf=7{) zXD6x~%SWti8pyO@%>J^&XrCFYqlN@{fh~aDNg1kZz3$v+l73`fLGVIOYqC>oC zVpd-NkLb3FfJnpwV5*tLOR*q+BRbB8wz{%-r3hG&TB_@q52x3Ey(y)RnQwj{^fn}D zj6ED%0M=&261M=FCIPg)fcZ#{ZpkK@n?B)=XKyE@g?l={3=1&Xw%-OAwE+JOV0{+= zIpzB;0JALoDS*vu9Q}@P1z^1-aUX!;g|@VWSy=*6=lM?fJplJVLjKrbeLspz)r*84 zz}E97%n-`ftLKe~nmFBf4bK38R^oX(AR*io@9NNEU|_(rn{K~9Qj6x2(5Plw00000 LNkvXXu0mjf^dA3P literal 0 HcmV?d00001 diff --git a/legacy/parts/body/blob_drip.png b/legacy/parts/body/blob_drip.png new file mode 100644 index 0000000000000000000000000000000000000000..39b0fc38e3f24a7f55b0f029fe16193e3cf11b03 GIT binary patch literal 535 zcmV+y0_gpTP)d&F5oOM~pDjP8@cKJN z4-_ST6S;cvCCvoNQ6MFl>qF6GQPkHn89s3}wJ!}U^fXn`_pH)jDtn5l-{oLIX zPFhF#FHwKJGS8nuW!!5M1x3Y8$*OE@TTMEu_&~WWL;+aCXWK0kf)@&~`c%WGg}(Jk z2Ag$SuuqagpQ|vu7|5|ps#7gTxhM?1bzaL;wjPVvI;3hTib#-8sMV?UNX<6Unyf_9 zvY%{$+Y?mXFFbL(WVzvJpkSNtOO-z?rX78{sQiMZ708Ft6a&MrSf4BH3tLM6RIoUoqV5kXFl4*ADVAmM z!6CsY0)ZzOu|SCZ&*OmL0rndT2LMiRn+XcXK|gR$&nS;8_k_ofzQ7BrD`CzkbGI<4 zq{|%FPaK$^aPCWix&s52vM85f9Tuth=Y)NruSDrd=}^94IL=TDRkFqD<`MJ__xT11 z$r#>YUTT#%YQ8IR4Q8~AwytzkZhr4|WxS6WGZg~%DIAKlQ0`#cxf<^sz#i1=;EQl> z=T>J~`2VGa#2O<)JVwU!9<5u!(D|9c=EpaU!&@@OO)P((b`cBIjKKmJ>a8s2UyJZ=Evl=G zwPt&EO9C`@YwJ^DfJv~28E2sjXX|Wb6NhVr1)mFft^|v}HOqO_9rw`daqIyLTm`vy zgJu(7fXr literal 0 HcmV?d00001 diff --git a/legacy/parts/body/blob_round.png b/legacy/parts/body/blob_round.png new file mode 100644 index 0000000000000000000000000000000000000000..c61c4e9d9495782f78ed20690f8839efa6888e99 GIT binary patch literal 356 zcmV-q0h|7bP)6XSRL<#U zP?d1us+0or)_Nz_xQwfKa*~?@S}1EC(hIFuW<#wgOEp5Z`powsZJO z&ol_x7k0Ph!Yo8C3aL)|mZn;5gf7ra1$E;Qhb!*+O(^UV4M)wmbH=8?vl?jw=@b!7 ze5|EIYPt7{8h}{%&2zzKYgLq>_TcEhCg;{oIg7~UKID$LOlVcY_eP?UM{mWD+{rb`MVrGe2S zO2tpoK5I27&G{Y3y-{pM1>RwK*G?e0qCLRC2@ooGMco@=&Q=~Zy{th5?$m1fKjJ<+ zr6pws=v!gX_&sc0ZHhLetqta^=rQY`(OuW_>}r9My`a44 z=d^QOv3H4%1rgg;t6Ar}rB(A@H#GWd~;NeXGZ{Zkq_8clce5md% uVGm6WV{w;ABi}5*bHu#vq5l?*yAyxEL93|YACQay0000AaZN1CmBiOnF9U%rL3Ga zmWF5Fj)%yBPb!Y)47K`yCkz5cxTEqgT+RD}!3l4N9*EW8VHjGO5C&fW6Fw4t?a8N2 zz+q=}6m($Vb+Hw{mV@EE`(>4`>9Wv8Vi)Kw2fEh+akRE;?gb?L)1LG;VqRt?pJ`jdhl4}U$E2Q$4?|Z)?bDB* zl99XHEaj_m)u63X*_CfFV2swcsX;~1Y~s+5^40*_>nc#0^1S);%_iF2cAul?r~E`- z3)%y}l#7HmCNce8zrt_)y=WxLGPDQ}pv@>1%O3^!=_L$Nto5%cekl>pXgYojvub)% zo|CfHIZ2+wJ@5;drM3rmPI7q_U_a1P-I`}Lx(ceQiNtL2pgKwJ`~ofBI)C@pcigP)X1^@s6ccHC$0005eNklf3*{tLJb*YR(a1%TKlqtGFn3Pg>tshxS~9WcKy0eGwk1({w%{#Zte2blTIAXU zT>c{XVmZK1EJ@YJzPFF{PSE*ovWT%Hxvy5IJpjj>A2caeLeV(B6Qwml$6L5!dE$R0 zN=vOn!?qTJ;3Vjqxp|@`_y|t3vJ;r*SAt;_Emz6P!VB1Jv<4_RZf$Du-fstxjRe#x zWKLEIi6tRVRbsVdJ-%U_3Z6Wv0Ps`G+J$2mPYqrWHu0f-0({IcMtb%d~jAr_SMm#s>i5^PlwlHzR z0#A`E!?N4y^|=I6gP{mkeO31P=z?BojP~gydeJ?NhNV$=#&=2Y7mu(qajw52fB*mh M07*qoM6N<$g1_MHi2wiq literal 0 HcmV?d00001 diff --git a/legacy/parts/body/insect_mantis.png b/legacy/parts/body/insect_mantis.png new file mode 100644 index 0000000000000000000000000000000000000000..2cc674c7a70cd148841315c3295c225ca07448f6 GIT binary patch literal 532 zcmV+v0_**WP)QG^@LCG)x zWPteoM}Vk%)L<(4C!jQW>F=~Rxfo(qiKt}_A5}=7636+!^iNZ`cYBH3<@Rc3vhcVBA(V1G-cipkM$RXTQu=hD-fgklJX@tK_lH zLkp>P&OOvI!29GL{sjz=#j~^=&}^+WD?cJ>>N3eN+TO%Laqi~z&}C7ZKZU8Q&|*74 ztZcbS%G9%t_P>ZIT!LHbUmOdvNEE5+tg1xHEW1L@n2is~ezO8Ks`=m}I`4`U`eg5;}c@q}VFZZ3WT%!JgN(+WrBG W!x~HPGEpu700000fhdEP)`_8@FNp))p5pNER#9-e9 zx=-7l0SZ*0m{qm<)LUrP*Nuqx>E^dRK+oZFlGM);SwYzZDvbxL(Qf|8#?1=p7X zdFD0G2-`6G06IfQ!=J8o{m=ittPlTt%7XFueJpPtD)gt5%RoIGd04?12;lPrJi7)D Zwtvsqf3%mJ#%cfn002ovPDHLkV1i%;rab@v literal 0 HcmV?d00001 diff --git a/legacy/parts/body/quad_slim.png b/legacy/parts/body/quad_slim.png new file mode 100644 index 0000000000000000000000000000000000000000..e08eefdc4b40d853f22ea70c8cd1f6f4f1528a6f GIT binary patch literal 393 zcmV;40e1e0P)e))feTl#1n^ctjtFGS0PrASI$lFaCHqtD@!;U>b>rLxf@==9 zcrMCbvJ2{9szvy4q7?utasfJoEOhJhmeUx@CJZBvfKQbc<9qP8!!1VBR| z_Pyar9FRTJR<*sBEC9&ti6M-VNFWtlUlrE?R(qNc{`bs+7zYvvqHr~bzoN;bRa+$^ zo4N8ge>)%!4gjp&9L)h3V=^2&;m88W%zk#T663{7tcE~qr4EuR7EkYRFGo;RYyOfc nd5+#+TRYCcjMu@c=6L82XKtJ;;kbsw00000NkvXXu0mjff8?rI literal 0 HcmV?d00001 diff --git a/legacy/parts/body/quad_stocky.png b/legacy/parts/body/quad_stocky.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5bb03689d857898768626f6b5aec14c82202e4 GIT binary patch literal 433 zcmV;i0Z#sjP)(neFv|5XNMiFW*B)<)6gHb4IfVcfoWfK@|ojEAT`F&dW}p)*D@d^ zqzUWtB$Em<^ID2-AZ7rHK+3oEZrh2K$Z%km_UDRk3|xunmu4OldqPQy&ZVqa%nyXYra7B~^Z}iJ1@A b-YWb7q&T6dLCizs00000NkvXXu0mjf3COiP literal 0 HcmV?d00001 diff --git a/legacy/parts/body/seal_body.png b/legacy/parts/body/seal_body.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2267d9171b520b7e80904e6f704b946e4adac7 GIT binary patch literal 381 zcmV-@0fPRCP)UNcX~NBy0_M z+Wg>LS~+OO3sm4Upi|?sJYk!`=O!P`P;a0c_=3*J1P5DbG?r&{M-Ty9X_TQen~3cW zzA*3OeM=p41nfptOF}UhF&42b=T4O=S_q!j#j(7K2&zM<1MnW+;6vyJLC`2RvL}Ey z$^v!=FNdJ6;l8U-^00000NkvXXu0mjff%2nx literal 0 HcmV?d00001 diff --git a/legacy/parts/body/serpent_coiled.png b/legacy/parts/body/serpent_coiled.png new file mode 100644 index 0000000000000000000000000000000000000000..49c087103092ea3d6c7bb9abe327142fa9fc0edb GIT binary patch literal 608 zcmV-m0-ybfP)vt1&BTYArxL7@aIvgD7BWAS&&SybsQ-WgXyr;PnIs7>AEe_{}lC z2Ct<EP)#JC8yzYfv3-6Prpre7w(o@~EYP9q5 zswY&7%V2c+p7;KUKPQ!*N>LI%-af{b2Y0J`wv!*tKY`$DpK3GuA8&bQV8O8e;Hae& ze9vyPugK)=hiU=pfTzmX>QQ`q=Cbs|?v~v?Tihutk1l&x1L^xR~NkC(Uz#w$`ytVDZ|KyBl> zEm{HYgZ2Hf0f}&vJTyQNQn9K$vv6xk5JA}m*l+=w09ZynMjy;ScLqmvo4+gf#Cr-?X(2lhYuO9 z;hSjF?ULRSIwZVPN^UR=kE2IA7RB5j%naUY$c1%=q6Cj6uX*~{<4_wMfTeOI0`_b@ zK=?o<7%jl)DUdhVB7=d&b-38))zKxy^1}Pyhe`07*qoM6N<$g4H%J_y7O^ literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_angry.png b/legacy/parts/eyes/eyes_angry.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ecd886ac96d9bbc53dc277d6705ed96d0c6f6c GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^{6Ngk!3HF+w~DI+sR&OO$B>FSQztNTF(`7dl)vZw z_rKXLiGQinlUEu6SJLisFql00*0EW``CsP-#?pudc^{^9PySFG*rBQ^bY_jU&JLr= u+~U1#@A!;*jamJkBp6QJ{d2=VrF}6u>dVg6S6%?x!QkoY=d#Wzp$PzAY%?1F>u+P7|d*iw78>4iKU$1cg|AdjXKuyZAZetSAJ_b)$KbLh* G2~7Y43pZ~7 literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_compound.png b/legacy/parts/eyes/eyes_compound.png new file mode 100644 index 0000000000000000000000000000000000000000..c5c8317d25a09b16d2c3c24f4c701a48dfcf66a7 GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQt6&9jv*CuN-r|Jt z@#$=dq*D$)CfwVt3KhFnZ0-A+vTgSsM8bg K=d#Wzp$PzQ(mSUB literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_cyclops.png b/legacy/parts/eyes/eyes_cyclops.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d5439f839002d6413db6961724f10451f23aae GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^AT}ol8<6B)wpSQP1$w$ThE&W+PH|}X&vS!Oc@Bf- zumAG9t=!x?tlVZO1qKF+u&$5a?-qE5C8w_B@RgUF(~a*=+js?`5n-322WQ%mvv4FO#nF4EBXKc literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_glowing.png b/legacy/parts/eyes/eyes_glowing.png new file mode 100644 index 0000000000000000000000000000000000000000..265be7603a25b57446a4899abcf9fd3341c26c7c GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^d_c_3!3HEP+-oF(RDh?8V@SoEsS|GV9Z=wLN{l}@ ze{;#@BiwqD{5Ouw3sQ~SIoD-c`>eZr3bfxGiG3<$r~1~Ge@4RL>BHF%SR% literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_happy_closed.png b/legacy/parts/eyes/eyes_happy_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..d112d17283944e2654f62fb87b33ea0f25bf1e4c GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp@K+Md+1|&Tmw(l<*Py_|Z2egH z!QMyJ5BW`{{SIG|*n7aq?WGh8!$rYA%~5K)tw*K*WH{6t+WY=EWn(engyrI`&oq0R z{^~4Wc8zP|M{|>TM}0JJ+Euaq-jyYCdFnlRq0C7g1#@1_nz2lJ?X>?Xh0`Cr+Q0ml hg@MPripw|tvVEN9_^#C^JO$_u22WQ%mvv4FO#qS6N&Nr- literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_round.png b/legacy/parts/eyes/eyes_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf4bf895b3c3bd1bc4d8d0433448ff9c8eba980 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~c!3HEhl+{lMQqi6+jv*Cul2aNQ{`0J85bW*u z<&BV4_Hc4>VR8L(cDDJ8wuBXpE0dWFb%7 literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_sharp.png b/legacy/parts/eyes/eyes_sharp.png new file mode 100644 index 0000000000000000000000000000000000000000..48b7b93c88a6f4fb9a854e3fb936e90421a87af7 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp@K+ML$1|%hPMD_zIS5Ftmkcv4|`vbWc6gZr>pIh+V z{?#Fc&O;gp`S0EiU=aA{b;oDTfzk}~jkkCwtIW9RruFc`qGs>OyX8N!tPcL%KJ(Wn aVe2o;1V6@F?e7H|$l&Sf=d#Wzp$Pyce=S@9 literal 0 HcmV?d00001 diff --git a/legacy/parts/eyes/eyes_sleepy.png b/legacy/parts/eyes/eyes_sleepy.png new file mode 100644 index 0000000000000000000000000000000000000000..abf85f830003915cebdcea2ec15234b6aaf9fe86 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^d_c^~!3HEt`|kFSQ~OT~9&q4rTFB15 zExub}bKaac$GvwLeqd8w*5ZFeS9acFm7V^t71Q{0003NNklU%uKY+)dquj(M;bUa`{e&&%u>q3a00X=jI4}V1wN@T@D6dGE)0k zBM4PjX|AC+8x~FJS)VJkq%{L)o!Wp7weO+~Y@Q$hEnp0mSWQbk3D#P}xQO5rs?@Ft z>>BzlOswjis`5|k*LBC1kH_2H=_C;&VQmxRl?I)t8_0^2NNJ}97+x1rHv&nSzzl3w z06Lj}E2Rw<5mnx?dH)g8B+p@Y@X0tsR{$eeH>fHL&Giynv=7buP;0*)9j6m8%)*75 hYx++8wFPwQ?+f5n$E#*0rceL?002ovPDHLkV1h^sjMo4F literal 0 HcmV?d00001 diff --git a/legacy/parts/head/head_canine.png b/legacy/parts/head/head_canine.png new file mode 100644 index 0000000000000000000000000000000000000000..ba796b5040bbeac3e1af8ddd63a77cdbe8da5274 GIT binary patch literal 298 zcmV+_0oDGAP)5gn8TGyX_f6J20`qxUqpms;>H?AUc@5+hk~OZt^h>!SQ|3! z%@Bns!&-SCex2R{r?Ze}BanH8>)UN<3Wcko3oZ5+Y6<}3Xvn-hC3n5h#YB6^^0{Pz z;E*@uda_N?>=(nxof6zSU6DOWVl2?vYe=mg1O%k0gl~>iShy&ZWR9j^8kQ1obD=do zdH};Ud=#$Ioh5T*dNc}YcU%n^p8++h=5fb+a*ymFBr5eNS`3&3+x6_4 zSVjUKs|&gakj3uB%*G=0B&COQ$3qU=y12|ydoepO4-&N$pyDF0AiL5=IRceod5J&P z;qvy6E{bq1_nbRXm;m?Q0T#AkNm0qWw2kYSEOns~&RfE?{uXKPIg y^BrjrvpwRS!G)AU3a?k+GlTy3K-Hr@|JMf;1XmZE)0>$90000ymyvq9Ro7lj-XpNBZsa}6(91Th=6>a8W#F8SwyiJbB|EwXqtrAV+fmlyWXU%}6^QIg2UkcLS*-FC^3w8L}fl90VG{RZ^&$C+Nm4qArmlfSXJPoEk(DN4lyssYJ b^f&PZjMsxiq9?m!00000NkvXXu0mjf1V3~! literal 0 HcmV?d00001 diff --git a/legacy/parts/head/head_ghost.png b/legacy/parts/head/head_ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..3735d018681cbd0e349364ab953279ef99d4a1a8 GIT binary patch literal 313 zcmV-90mlA`P)v;hb_Ghfv^RViZV1E zhG^$?n7|gZbRsHW9|OZlgtmOv0N9nG z2b)oP2#CDcOi9KM0!+2|WLVDuld==6Fzz<%F&BD1ihYvb^3pwaF* z8Pt~mKcDskWEUPIRP*`?fG^hv4)iR9*nv26hx`0V4&%kJaBKF))z#bR~ zK)f{#e`KI9+3s~?57eN+Q!TQ$fx%Z5v#$|3&`(r|P~1ae931t%lMVh6rp1v~90Xe3 zD}SpxAW`$y1hgu3E27Z9fcLC| z3C=O3Sn=Pkr5kn@<`}VosCy}WN?|ulWN;->m7rYFRMMpzh9@lhm6rmi`yuGHZOxF) z57H(2(h6^ijy9Gh04|VSTE)y2HNhx>Y&OyesF4@;F4o)PfZYWQSxlF4G=ZsobS;s^Tct zO6@TP%BpVsi4CG8%c%yA^kD<@Fc^FRo7u0x5F88fa3*;7jepfsRo@>=@neRaqowBn O0000H{VlXz8wnsh5x#pm)6%JECi-ZCyPhb^=@`6~y|kIXiJD%hl5w zmIxB8`VO#FBS(8M8DbWJHaW6CIv*SzA^^K15qcWh>eRlYrMn05NyNCp41zKr(kP=C z+a9e!V9!n>8YwE7DIn86dDbwnoTNC*` z4*smXHtT@1VQQO6(Z#d~7FvM!d9Z`j#?VX*6){t#(!zD2kqFv2TIC4N;EA}KlMiPE b_c6Wz$ZTj0sE!si00000NkvXXu0mjfpcI4W literal 0 HcmV?d00001 diff --git a/legacy/parts/head/head_round.png b/legacy/parts/head/head_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c494fa4b289477f2f09d52fc9dbc37a4af25b1e GIT binary patch literal 269 zcmV+o0rLKdP)W zX#pH1U(UBv+Y023`}tC_+hY_(;iycg;-kyBg+K-}D@(Tu8SvjivT5kZ$~MDq!gZpL zMtg?S@z4TfHK!(RRe?LFBLchDUpw7JV>wYT9y5_mza%vA;dN$&0mR_dWh} cV)BU@`!zIQJo^;86=))Zr>mdKI;Vst0Gwklr~m)} literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/beak_short.png b/legacy/parts/snout/beak_short.png new file mode 100644 index 0000000000000000000000000000000000000000..26f827b08dbcfe4f1dbc1128f07cc5e6215c8efc GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^96-#*!3HG%vEKg;r0hLi978JRl=cO3F(`02o4FSt^SN$42m4Zh5X;+ z_GJ{NUApKnBgd~**Pt-OX_xw4o_Ck#P0ld(>pZ+6**I8x#`OA%*Szfg7nFKJMEeZx k2r(Raz4)i>iR~TbVjpGOL?tu?fkrcUy85}Sb4q9e08E1{zyJUM literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/fish_lips.png b/legacy/parts/snout/fish_lips.png new file mode 100644 index 0000000000000000000000000000000000000000..8faf4011f40c00027f763be9452ab288a31b7fa3 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORFJ2OV@SoE(*BEr2NXD*7aAKF zygSWT@!+Ddj8@4kn??XH6-Y@%OqId|P}x2V+2 zWO9AYnr2bjudzo$qiWOT)0+}^+_3mG>C%;l9oDb@t*91w&Auwc`W63u8D5}`44$rj JF6*2UngGo3I_dxb literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/muzzle_long.png b/legacy/parts/snout/muzzle_long.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba8bf13f7c5bc48af5b577f5a15bc759195372d GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^d_c^}!3HFmd}T9%RI;awV@SoEsS_Bv8VopGO|6=a z-%)(C=Ph3w^Yoo2RR%wDZ1(dcx+<_wPiQg_zt`L8+Ly~9{v7* z7_0cPElyU4o>!QQhJNZ@UnG+-X_M+AKACj3?1St!m=G zH~;;{NdZ+(i3XOdZ%60k%l9fU>9cWoq`mIyu+v<)c)^3Oi=|x`CnQhWzRgvmR(B!) l;|CJP$+}N literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/snout_pig.png b/legacy/parts/snout/snout_pig.png new file mode 100644 index 0000000000000000000000000000000000000000..0a7d1bba3048b9b7d8ec658f33f6be2a25c72ef2 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CB!3HGHK9Tzfq`W;{978JRv<5_TF*tHuvaBpF zVxOk!_3Wa5M4IG9=eAC7$F3K?hZ#-?@8nzgpfKppihA+wjp059wkPgBTlQc0vY(f+ g-#SaPJj$jbPC2GCdrPgg&ebxsLQ02lc!x&QzG literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/tongue_out.png b/legacy/parts/snout/tongue_out.png new file mode 100644 index 0000000000000000000000000000000000000000..bf63e3c5332c360abb190a487a9053d01b4a8fa5 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#8<2F%laT;Y(Vi}jAr*6aFL-kuFyLSb(6!xR zyHoLF^J3kSX}^pTW*B(MKkN!g;o5lfE4%Asm&X&OaxS~QcydnYsF>nOZn^dgRSqZJ wtP6P--FUX=;PxNEZS^gib0+`h;w^s_SusVrzrlL-8=y4|p00i_>zopr02}ExKmY&$ literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/trunk_short.png b/legacy/parts/snout/trunk_short.png new file mode 100644 index 0000000000000000000000000000000000000000..8093a252c549eadeebe43ed7309f7cdec388a2c5 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CH!3HFy_x^nYq)I(q978JROr6xobwGi`<@`qe z^7wZKo0fiNv+gj6Pmg5M^KN`KqqUJSqO!h7kIAt`cdE9H%#=Z>|2Ky47}=fj3`~Nmbqa z5A18rmu%g0SDx`sCez(n+`V`1n27Av4p_VC`bUP@{mKkXMpBkQyBIuO{an^LB{Ts5 D#hf$9 literal 0 HcmV?d00001 diff --git a/legacy/parts/snout/whiskers.png b/legacy/parts/snout/whiskers.png new file mode 100644 index 0000000000000000000000000000000000000000..4307e04ae283c5f6cfc7c25b025c211dfccf32e2 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~p!3HE570x;VsT5Ba$B>FSQztO;H7M{fTR#?l zR{xOyPq|xRz;lz0oScpKG6Wgg^bQ8seJ|txX)t@Lk^j2AKF_}BGk!{4)wOo3&LzfK z)z?21>G>JmIJb1Kg_8ezyL`2Mp&v5-tUR>W>F&eS7u(C)A4)f!%5uK}w2#5l)z4*} HQ$iB}MHW0U literal 0 HcmV?d00001 diff --git a/legacy/parts_src/body/avian_perched.txt b/legacy/parts_src/body/avian_perched.txt new file mode 100644 index 0000000..ec35895 --- /dev/null +++ b/legacy/parts_src/body/avian_perched.txt @@ -0,0 +1,32 @@ +...............oooooo........... +.............oo222222oo......... +............o2222222222o........ +.........ooo2H2222222222o....... +........o222221111122B222o...... +......oo222221111112222222o..... +....oo22221111111111222222o..... +...o22222111111111111122223o.... +.oo222221111111111111222223o.... +o22222211111111111111122222o.... +.ooo222111111111111111222233o... +..o2222222111111111212222233o... +.o22222221111112222222222233o... +o222222222211122222222222233o... +.oooo2222222222222A222222333o... +...o222222222222222222222233o... +...o223322222222222222222233o... +....oooo22222222222222222223o... +........o2222222222222222223o... +........o22222222222333332333o.. +.........o22222222333333333T3o.. +.........o23333333333333333333o. +..........o23333333333333333332o +...........o3333333333333333333o +............o3obco33333oooooooo. +.............oobco333ooo........ +..............obcoooocco........ +..............obco..occo........ +.............oobcoo.occo........ +............oabbbcooobcoo....... +............obbbccoobbcco....... +............ooooooPoooooo....... diff --git a/legacy/parts_src/body/avian_round.txt b/legacy/parts_src/body/avian_round.txt new file mode 100644 index 0000000..02bff5a --- /dev/null +++ b/legacy/parts_src/body/avian_round.txt @@ -0,0 +1,28 @@ +..............ooo............. +..........oooo222oooo......... +........oo22222222222oo....... +.......o222222222222222o...... +......o22221111112222222o..... +.....o222111111112B222222o.... +....o222111111111112222222o... +...o22211H11111111122222222o.. +..o222111111111111122222222o.. +..o2221111111111112222222223o. +.o22222111111111222222222223o. +.o22222111111122222222222233o. +.o222221111112222222222222T3o. +.o2222221112222A222222222333o. +.o22222222222222222222233333o. +.o22222222222222222222333333o. +.o2222222222222222223333333o.. +..o222222222222222233333333o.. +..o22222222222222333333333o... +...o222222222223333333333o.... +....o3222222233333333333o..... +.....o33333333333333333o...... +......o333333333333333o....... +.......oo33333333333oo........ +.........ooco333ocoo.......... +..........ocoooooco........... +.........obbco.obbco.......... +..........ooo.P.ooo........... diff --git a/legacy/parts_src/body/biped_bulky.txt b/legacy/parts_src/body/biped_bulky.txt new file mode 100644 index 0000000..f675f93 --- /dev/null +++ b/legacy/parts_src/body/biped_bulky.txt @@ -0,0 +1,39 @@ +.................oooooooooo................. +.............oooo2222222222oooo............. +...........oo222222222222222222oo........... +.........oo22oooooooHooo222222222oo......... +........o111111111122222oo222oooo22o........ +.......o211111111111121122o2o2111222o....... +.......o221111111111111122oo221111222o...... +......o2o22212211122211222oo2111111222o..... +......o22o222221222222222oo22111122223o..... +.....o2o2o22223o22223o222oo22111122223o..... +.....o2o22o223o1o223o1o23oo22111122223o..... +....o22o22o23o111o2o111oo2o22211122233o..... +....o22o222oo1111o3o122222o22221112233o..... +....o222o22oo21111o12222222o22B211223o...... +....o222o2222211112222222222o222122223o..... +...o2223o22222112222222222222o22122223o..... +...o222o222222122222222222222o22222223o..... +...o222o222222222222222222222o212222223o.... +..o2222o222222222222222222222o222222223o.... +..o2222o2222222222222A2222222o222222223o.... +..o2222o222222222222222222223o2212222233o... +.o22222o222222222222222222233o2212222233o... +.o222223o22222222222222222333o22122222333o.. +o2222333o22222222222222233333o22222T23333o.. +o22223333o2222222222233333333o22222233333o.. +o22233332o2222222223333333333o22222233333o.. +.o33333333o222222333333333333o22222233333o.. +.o33333333oo33323333333333333o22222333333o.. +..oo3333oo32oo33333333333333ooo222333333o... +....oooo.o2223ooo33333333ooo112oo333333o.... +.........o2322333oo33333o22222222oo33oo..... +.........o222233333oooooo2222222223oo....... +........oo222333333o....o2222222223o........ +.......o2222333333o....o222222222233o....... +......o222223333333o...o222222233333o....... +......o233333333333o...o222223333333o....... +.......o3333333333o....o222233333333o....... +........oo333333oo......ooo333333ooo........ +..........oooooo......P....oooooo........... diff --git a/legacy/parts_src/body/biped_small.txt b/legacy/parts_src/body/biped_small.txt new file mode 100644 index 0000000..c1c2aa0 --- /dev/null +++ b/legacy/parts_src/body/biped_small.txt @@ -0,0 +1,31 @@ +.............................. +.............................. +............oooooo............ +..........oo222222oo.......... +.........o22222H2222o......... +........o222211122222o........ +.......o22211111122222o....... +.......o22111111112222o....... +......o2221111111122223o...... +......o2221111111122223o...... +......o2221111111122223o...... +......o222211111122ooo3o...... +.....o2o2211111122o1B2oo...... +....o22o2111111222o1222o...... +...o22o22211112222o122o3o..... +..o22o22221112222o2223o32o.... +..o22o222211222A2o222o333o.... +..o23o22221222222o123o333o.... +...o3o2222122222o222o3333o.... +....o22222222222o222o3333o.... +....o22222222222o223o33T3o.... +....o222222222222ooo33333o.... +.....o222222222233333333o..... +......o2222222233333333o...... +.......oo3333333333333o....... +.....oo22ooo333322233o........ +....o2222222o111112222o....... +....o2223333o2111222222o...... +....o33333333o22222232o....... +.....oo33333oo2333333o........ +.......ooooo..Poooooo......... diff --git a/legacy/parts_src/body/biped_tall.txt b/legacy/parts_src/body/biped_tall.txt new file mode 100644 index 0000000..2923527 --- /dev/null +++ b/legacy/parts_src/body/biped_tall.txt @@ -0,0 +1,45 @@ +............oooooooo............ +..........oo22222222oo.......... +.........o2ooooooooo22o......... +........o1o2111H1122oo2oo....... +.......o1111111111112oo22o...... +......o2211111111211o21122o..... +......oo22222111222oo11122o..... +......o2o2222o22223o2111233o.... +......o2o222o1o223o2o21223o..... +.....o2o2o2o111o2o22o21223o..... +.....o22ooo2211o3o223o1222o..... +.....o22ooo22212o2233o22223o.... +.....o233o222222222333o2223o.... +....o22333o2222222233Bo1223o.... +....o22333o2222222233o21223o.... +...oo2233oo2222222233o212233ooo. +..o2o2233oo2222222233o221233222o +.o2222233oo2222222233oo21223ooo. +o32222233o222122A23333o22223o... +.oo223333o222122233333oT2223o... +..o233333o222222333333oo12233o.. +..o233333o22222233333oo212233o.. +...o3333ooo2222233333o3o22333o.. +....oooo.o2oo333333oo23o2233o... +.........o223oooooo2223o333o.... +.........o223333o222233oooo..... +.........o223333o222233o........ +.........o22333o222223o......... +.........o22333o222223o......... +.........o22333o222233o......... +.........o22333o222233o......... +.........o22333o222233o......... +.........o22333o222223o......... +.........o2233o.o22223o......... +.........o2233o.o22223o......... +.........o2233o.o22223o......... +.........o2333o.o21223o......... +.........o2233o.o21223o......... +........oo2233o.o21223o......... +......oo22233o.oo11223o......... +.....o22222333o1111222o......... +.....o23333333o21122333o........ +....o33333333o222222333o........ +.....oooooooo22223333oo......... +.............oooPoooo........... diff --git a/legacy/parts_src/body/blob_drip.txt b/legacy/parts_src/body/blob_drip.txt new file mode 100644 index 0000000..092fa35 --- /dev/null +++ b/legacy/parts_src/body/blob_drip.txt @@ -0,0 +1,38 @@ +.....................oooo................. +...................oo2222o................ +.................oo22222o................. +..............ooo22222222ooo.............. +............oo22222222222222oo............ +..........oo222222112222222222oo.......... +.........o2222211111122222222222o......... +........o222211111111^12222222222o........ +.......o22211111111111111222222222o....... +......o2222111111111111112222oo2222o...... +......o222211111111111111112o12o222o...... +.....o2222111111111111111122o22o2223o..... +.....o22221111111111111112222oB22223o..... +.....o222211111111111112222222222222o..... +....o22222211111111E12222222222222233o.... +....o22222211111111222222222222222233o.... +....o22222222111222222222222222222333o.... +....o22222222112222222222222222223333o.... +....o22222222222222222222222222233333o.... +...o222222222222222222222222222333333oo... +..o22222222222222222222S222222233333332o.. +..o222222222222222222A22222222233333332o.. +..o222222222222222222222222222233333333o.. +..o222o2222222222222222222222223333o333o.. +..o212oo2222222oo2222oo22222o2233T3o233o.. +.o2123o2o21223o11o23o12o223o2o21223o223o.. +.o2123o2o21223o11o23o22o223o2o21223o223o.. +.o2223o2o22223o22o23o22o223o2o22223o233o.. +.o22223o222233o22oooo22o223o2o22223oooo... +..o223oo222233o22222222o223o2o22223o...... +...ooo22o22223o222222222ooo22o2223o2ooo... +..o11222o22223o22222222222233o2223oo222o.. +.o222222o22223o222222233333333oooo11o122o. +.o2222222o223o2223333333333333333o22o222o. +..o2222222ooo333333333333333333333oo333o.. +...ooo333333333333333333333333333333ooo... +......oooooo333333333333333333oooooo...... +............oooooooooPoooooooo............ diff --git a/legacy/parts_src/body/blob_floating.txt b/legacy/parts_src/body/blob_floating.txt new file mode 100644 index 0000000..10918da --- /dev/null +++ b/legacy/parts_src/body/blob_floating.txt @@ -0,0 +1,40 @@ +...................................... +...............oooooooo............... +............ooo22222222ooo............ +..........oo22222222222222oo.......... +.........o222222111222222222o......... +........o22221111111222222222o........ +.......o22211111111^1222222222o....... +......o222111111111111122222222o...... +.....o22211111111111111122222222o..... +.....o22211111111111111112222222o..... +....o2222211111111111112222222223o.... +....o2222211111111111222222B22223o.... +....o2222211111111122222222222233o.... +....o222222111111E222222222222233o.... +....o2222221111222222222222222233o.... +....o2222221112222222222222222333o.... +....o2222222222222222222222223333o.... +....o2222222222222222222222233333o.... +....o22222222222222A2222222333333o.... +....o2222222222222222S222233333T3o.... +.....o22222222222222222233333333oo.... +....o1ooo2222222222222233333333o22o... +...o2111o222222222222233333333o2223o.. +...o2122o22222222222223333333o22333o.. +...o2223o33222222222223333333o23333o.. +...o2233oo322222222222233333o.ooooo... +....oooo..oo222222222223333o.......... +...........o22222222222333o........... +............o3222222222233o........... +.............o322222222223o........... +..............o332222222233o.......... +..............o233233333333o.......... +...............o33333333333o....o..... +................o33333333333o.oo2o.... +.................o33333333332o2222o... +..................oo3333333331222o.... +....................o33333333222o..... +.....................o333333333o...... +......................ooooo333o....... +...........................oPo........ diff --git a/legacy/parts_src/body/blob_round.txt b/legacy/parts_src/body/blob_round.txt new file mode 100644 index 0000000..816cfcf --- /dev/null +++ b/legacy/parts_src/body/blob_round.txt @@ -0,0 +1,31 @@ +...................................... +.............oooooooooooo............. +..........ooo222222222222ooo.......... +........oo222222222222222222oo........ +.......o22222222222^2222222222o....... +......o222222111111122222222222o...... +.....o22211111111111122222222222o..... +....o2221111111111111112222222222o.... +...o222111111111111111112222222222o... +..o22211111111111111111112222222222o.. +..o2222111111111111111111122B222222o.. +.o2222211111111111111111111222222223o. +.o2222221111111111111122222222222233o. +.o222222111111111E222222222222222233o. +.o2222222111111222222222222222222233o. +.o2222222111122222222222222222222233o. +.o22222222122222222A2222222222223333o. +.o2222222222222222222222222222223333o. +.o2222222222222222222222222222223333o. +.o22222222222222222222S2222222223333o. +.o2222222222222222222222222222333333o. +..o22222222222222222222222223333333o.. +..o222222222222222222222333333333T3o.. +.o2222222222222222233333333333333333o. +.o2222222222333333333333333333333333o. +..o22223223333333333333333333333333o.. +...o323333333333333333333333333333o... +....oo33333333333333333333333333oo.... +......oooo333333333333333333oooo...... +..........oooooooooooooooooo.......... +...................P.................. diff --git a/legacy/parts_src/body/fish_body.txt b/legacy/parts_src/body/fish_body.txt new file mode 100644 index 0000000..1c58ed0 --- /dev/null +++ b/legacy/parts_src/body/fish_body.txt @@ -0,0 +1,35 @@ +.................................................. +............ooooooo............................... +.........ooo2222222ooooooooo...................... +........o222222222222o222222oooo.................. +......oo22222112222222o222222222oo................ +.....o22221111111222222oo222B22222oo.............. +....o2222111111^112222222o2222222222oo............ +...o2221111111111122222222o22222222222oo.......... +...o2221111111111111222222o2222222222222ooo....... +..o222111111111111111222222o222222222222222ooo.... +..o222211111111111112222222o222222222222222222o... +.o22222111111111112222222222o222222222222222222o.. +.o22222111111111122222222223o222222222222222222o.. +.o2222211111E112222222222223o222222222222222T23o.. +.o22222211111122222222222223o222222222222222233o.. +.o2222221111222222222222ooo3o222222222333333333o.. +.o2222221122222222222A22oaaoo22222333333333333o... +.o2222222122222222222222oaaaaooo233333333333oo.... +.o2222222222222222222222obaaaaaaooo33333oooo...... +.o2222S22222222222222223obbbaaaaaaaoo3oo.......... +.o22222222222222222222333obbbbbbaaaaaoo........... +..o2222222222222222223333obbcbbbbbbbbbbo.......... +..o22222222222222223333333obbccbbbbbbboo.......... +...o2222222222222233333333obbbbccccccoo........... +...o2222222222233333333333oobbbbbbbbo............. +....o23222233333333333333o3obbbbbbbbbo............ +.....o33333333333333333oo333obcbbbbbbbo........... +......oo33333333333333o33333obbccbbbccco.......... +........o333333333333o333333oobbbcccoo............ +.........ooo3333333ooooooooo..ooooo............... +............ooooooo............................... +.............obbbbco.............................. +..............obbbbco............................. +...............obbcco............................. +................oPoo.............................. diff --git a/legacy/parts_src/body/insect_beetle.txt b/legacy/parts_src/body/insect_beetle.txt new file mode 100644 index 0000000..360f51a --- /dev/null +++ b/legacy/parts_src/body/insect_beetle.txt @@ -0,0 +1,30 @@ +.............................................. +....................oooooooooooooo............ +.................ooo22222222222222ooo......... +................o2222222222B222222222o........ +..............oo2222211112222222222222oo...... +.............o22221111111111122222222222o..... +............o2222111111111111111222222222o.... +...........o2211111oooooooooooooo222222222o... +......ooooo22211ooo22222222222222ooo2222222o.. +.....o22222ooooo21111111111222222222oo22222o.. +....o221111122o11111111111111222222222o22223o. +...o22111H11222o11111111111111112222222o2223o. +..o2211111111222o11111111111111111222222o233o. +..o2221111111122o111111222222222222222222T33o. +..o2221111112222o122222222222222222222222o33o. +..o2221111222222o22222222A222222222222222o33o. +..o2222222222222o222222222222222222222333o3o.. +..o2222222222223o222222222222222222223333o3o.. +...o22222222223o222222222222222222223333o3o... +....o22222222oo22333333333333333333oo33o3o.... +.....o3oooooo12o33333333oo33333333o12oo3o..... +......o2233o1122oooooooo12oooooooo1122oo...... +.....o2223o111222o3333o1122o3333o211122o...... +.....o223o2112233o333o211122o33oo2211222o..... +....o2233o211223oo222o211123oooo2o2122222o.... +....o223o2212233oo223o211223o...oo2222223o.... +.....o33o222223o.o233o222223o....oo222233o.... +......o3o222233o..o33o222233o.....o233333o.... +.......ooo2233o....o33o2233o.......o3333o..... +..........o33o......oo.oP3o.........oooo...... diff --git a/legacy/parts_src/body/insect_larva.txt b/legacy/parts_src/body/insect_larva.txt new file mode 100644 index 0000000..4034125 --- /dev/null +++ b/legacy/parts_src/body/insect_larva.txt @@ -0,0 +1,25 @@ +................................................ +................................................ +................................................ +.......................o........................ +......................o2o.......o............... +......oooooooo.......o22o......o2o......o....... +.....o22222222o.....o122o.....o22o.....o2o...... +....o2211112222o...o1112o....o122o....o22o...... +...o22111H122222ooooooooo...oo212o...o122o...... +..o22111111122222o222222o.ooo2oooo..o2212o...... +..o22111111112222o1111222o2222222ooooooooo...... +.o2221111122222223o1112222o11B12222o22222oooo... +.o2221111222222223o11122222o1111222o111222o22o.. +.o2221112222222223o11122223o11111222o111222o22o. +.o2222222222222333o22222223o11122223o111223o122o +.o2222222222223333o22222A233o2222223o222222T223o +..o22222222223333o222222223o22222223o22oo233o23o +..o2222222oooo333o222ooo333o22oooo33o2o12o3o233o +...o22223o1122o3o222o112o33o2o1122o3oo2112oo333o +....o3333o2112oo222o21122oo22o2112oo2o2122oo33o. +.....o333o1122o2333o21222o332o1122oo3o1123o33o.. +......ooo21223oo333o21233o33o21223o3o21223ooo... +........o21223o.oooo2223o.ooo21223ooo21223o..... +........o22233o....o2223o...o22233o.o22233o..... +.........o233o.....o2233P....o233o...o233o...... diff --git a/legacy/parts_src/body/insect_mantis.txt b/legacy/parts_src/body/insect_mantis.txt new file mode 100644 index 0000000..69dbba4 --- /dev/null +++ b/legacy/parts_src/body/insect_mantis.txt @@ -0,0 +1,35 @@ +...............oooo................. +.......o......o2222o................ +......obo....o221222o............... +......obbo..o2211H222o.............. +......obbboo2oooo11222o............. +..o...obbbcoo1122o1222o............. +.obo..obbbboo1112o22223o............ +.obao..obbco21222o22223o............ +.obaao.obcco21233o22B23o............ +.obaabo.oco22223o22222o2oo.......... +.obbaao.obo21233o22223o222o......... +..obabaooo22223o222223o1222o........ +..obbbbbboooo33o2222233o122o........ +...obbbbbbbbboo2222223o11223o....... +...obcbcbbaaboo2222223o11122o....... +....occccbbbbo12222223o111223o...... +.....oocccccco22222233o222222o...... +.......oooooo22222A233o2222222o..... +..........o22222222333o22222222o.... +...........o222222333o322222222o.... +...........o2oooo3333o333322T223o... +............o1122o33o3o333333333o... +............o1112o3o22ooo33333333o.. +...........o21122oo2233o.ooo333332o. +...........o21223o22233o....oo3333o. +...........o21223o22233o......oooo.. +...........o21233oo2233o............ +...........o2223o.o22333o........... +..........o22223o..o2333o........... +..........o21233o..o2333o........... +..........o2123o...o23333o.......... +..........o2223o....o2333o.......... +..........o2223o....o2333o.......... +.........o22233o....o2333o.......... +..........o223o..P...o333o.......... diff --git a/legacy/parts_src/body/quad_long.txt b/legacy/parts_src/body/quad_long.txt new file mode 100644 index 0000000..42436a0 --- /dev/null +++ b/legacy/parts_src/body/quad_long.txt @@ -0,0 +1,20 @@ +.................oooooooooooooo...... +.......oooooooooo22222222222222o..... +....ooo22222222222222222B2222222o.... +...o1111111111111111111111oooooo2o... +..o11111H111111111111111oo111112o2o.. +.oaaaaaaaaab11111111111o111111111T2o. +.oaaaaaaaabb11111111111o11111111122o. +.o2baaaaabb11111111111o2111111111222o +o221baaabb111111111111o2111111122223o +o22221bb1112222222A222o2111111222223o +.o22111112222222222222o2111122222223o +.o22211112222222222222o2211222222233o +.o222111222222222222222o21222222233o. +..o22111232222222222222o22222222333o. +...o212223o3333333333333o22222333oo.. +...o2aaaa3ooooooo3333333oaaaaa3oo3o.. +...oaaaaabo223333oooooooaaaaaao333o.. +...obbbbbboo3333o......obbaaabo33o... +....obbbbo.o2333o.......obbbbo333o... +.....oooo...oooo.P.......oooooooo.... diff --git a/legacy/parts_src/body/quad_slim.txt b/legacy/parts_src/body/quad_slim.txt new file mode 100644 index 0000000..eb4b791 --- /dev/null +++ b/legacy/parts_src/body/quad_slim.txt @@ -0,0 +1,20 @@ +.......oooo.oooooooooooo......... +.....oo1222o222222222222oo....... +....o111112111111122222B22o...... +...o11111111111111111222222o..... +..o11111bH111111111111ooooo2o.... +.o2aaaaaaaaabb1111111o11112oTo... +.oaaaaaaaaaabb111111o1111112oo... +.obaaaaaaaaab111111o1111111122o.. +o2bbaaaaaaabb222222o1111111222o.. +.o2bbaaaaabb2222A2o211111122222o. +.o222ooobbb2222222o211111222223o. +.o22o111obb2222222o221112222233o. +..o2o111o2b33333333o2112222233o.. +...o2112o3333333333o2212222333o.. +...o21123333333ooooo22222233ooo.. +...o11223332233o....o222223o222o. +..o211223o22333o...o222223o22233o +..o222223o233333o..o222233o3333o. +...o22233oo3333o....o22333ooooo.. +....ooooo..oooo.P....ooooo....... diff --git a/legacy/parts_src/body/quad_stocky.txt b/legacy/parts_src/body/quad_stocky.txt new file mode 100644 index 0000000..b93a771 --- /dev/null +++ b/legacy/parts_src/body/quad_stocky.txt @@ -0,0 +1,26 @@ +............oooooooo.............. +........oooo22222222oooo.......... +......oo2222211122222B22oo........ +.....o22211111111222222222o....... +....o2211111111111111222222o...... +...o221111111111111111122222o..... +..o2211H111111111111111122222o.... +.oaaaaaaaaaab1111111oooooooo22o... +.oaaaaaaaaabb11111oo11111122oTo... +o22aaaaaaabb11111o1111111111222o.. +o222baaaabb111111o1111111111122o.. +o2222baabb111111o211111111111222o. +o22221bb11111122A211111111112223o. +o222211111112222o111111111122222o. +o22111122122222o22111111222222233o +.o21111122222222o221112222222223o. +.o21111122222222o222112222222233o. +.o21111122222222o222122222222233o. +.o2111122o2222222o2222222222233o.. +.o2111222o2222222o222222222333oo.. +.o2112222o33333333o222222333oo3o.. +.o2aaaaab3oo3333333oaaaaa3oo333o.. +.oaaaaaaao23ooo3333oaaaaao23333o.. +.obbbbbbbo23333oooobbaaabo23333o.. +..obbbbbbo3333o....obbbbbo3333o... +...oooooo.o33o.P....ooooo.o33o.... diff --git a/legacy/parts_src/body/seal_body.txt b/legacy/parts_src/body/seal_body.txt new file mode 100644 index 0000000..f93754a --- /dev/null +++ b/legacy/parts_src/body/seal_body.txt @@ -0,0 +1,27 @@ +............oooooo.............................. +..........oo222222oo............................ +.........o2222222222o........................... +........o2222H1222222o.......................... +.......o22211111122222o......................... +......o2221111111222222oo....................... +.....o2221111111112222222oo..................... +...oo2221111111111122222222oo................... +..o22222111111111111122222222oo................. +.o2222211111111111111122222B222oo............... +..oo22221111111111111112222222222o.............. +..o2222211111111222222222222222222oo............ +.o2222211111112222222222222222222222o........... +..oo222222222222222222222222222222222o.......... +..o22222222222222222222222222222222222oo........ +.o2222222222222222222A222222222222222222oo...... +..oooo222222222222222222222222222222222222oo.... +.....o22oooooo222222222222222222222222222222oo.. +....oooo112222o2222222222222222222222222222222o. +...o1111112222o3333333333333332222222222222222o. +..o1111112223o3333333333333333333333322232T2232o +.o2211122223o333333333333333333333333333333333o. +.o221222223o3333333333333333333333333333333333o. +o2222222333oooooooo33ooooooooo333333333333oooo.. +o22o22333oo........oo233333333o333333ooooo...... +.o2o33ooo...........o3333333ooooooooo........... +..ooooo..............Poooooo.................... diff --git a/legacy/parts_src/body/serpent_coiled.txt b/legacy/parts_src/body/serpent_coiled.txt new file mode 100644 index 0000000..26eea87 --- /dev/null +++ b/legacy/parts_src/body/serpent_coiled.txt @@ -0,0 +1,36 @@ +..........oooooo.............................. +.........o222222o............................. +........o22111222o.....ooo.................... +........o2111H122o...ooaao.................... +.......o221111223o.ooaabbo.................... +.......o221122223ooaabbco..................... +......o2221122223oabbcco...................... +......o2211122223obccoo....................... +......o2211112233ooooooo...................... +.....o22111111233oaabbbco..................... +.....o2211111123ooabbbccco.................... +....o22211111123oobccccoo..................... +....o22211111122ooooooooooooooooooo........... +....o222111111222oo2222222222222222ooo........ +....o222111112222o22122222222B11112222oo...... +....o2222111122222o112222222222211111222o..... +....o22222111222222o113333333333221112222o.... +.....o22221222222222o133333333333211222222o... +......o22222222222222o23333333333122222222o... +......o222222222222222o22333333211222222233o.. +.......oo2222222222332o2222222111112222233o... +.....oo22o222222223333o2211111111112333333o... +...oo22211o22222233333o22221111112222333ooo... +..o2221111o22223333333o333222222222233oo222o.. +.o221111111o2333333333o33A332222223ooo222222o. +o22211111111o33333333oooooooooooooo2222222222o +o222211111111o333333o22222222222222222222T223o +o2222222222222oooooo2222222222222222222222233o +o22222222222222222222222222222222222222332333o +.o222222222222222222222222222222222223333333o. +..o3332222222222222222222222222222333333333o.. +...oo333333333222222222233333333333333333oo... +.....oo33333333333333333333333333333333oo..... +.......ooo33333333333333333333333333ooo....... +..........oooooo33333333333333oooooo.......... +................oooooooPoooooo................ diff --git a/legacy/parts_src/body/serpent_rising.txt b/legacy/parts_src/body/serpent_rising.txt new file mode 100644 index 0000000..bdd131c --- /dev/null +++ b/legacy/parts_src/body/serpent_rising.txt @@ -0,0 +1,47 @@ +............oooo.......................... +...........o2222o......................... +..........o222122oo....................... +.........o222111222o...................... +.........o2211111222o..................... +........o22211H111222o.................... +.........o221111111222o................... +.........o222111111222o................... +..........o222111222223o.................. +...........o22211222222o.................. +............o22222222223o................. +.............o2222222223o................. +.............o2222222223o................. +..............o2222222333o................ +..............o2222223333ooo.............. +.............o21122223333obboo............ +...........oo211122233333obbbco........... +..........o22211112333B3ooobccco.......... +.........o22211112233333oobbccco.......... +.........o22111122223333obbccco........... +.......ooo2211122222333obcccoo............ +.....ooaao2221222223333oooo............... +...ooaaabo221122223333o................... +.ooaaaabbo2211122333oo.................... +oaaaabbbbo221111233o...................... +.oooabbbco22111113o....................... +.oaabbbcco22111123o....................... +oabbbbccco22111122o....................... +.ooobccco2221111222o...................... +....oooo2221111222A2o..................... +.......o2222112222222o.................... +.......o22221122222222ooooo............... +........o2222222222222o2222oooo........... +......oo2o2222222222222o2111222oo......... +....oo2221o122222222222o222111122oo....... +...o2221111o223222222332o3322111222o...... +..o221111111o22222222333333321122222ooooo. +.o2221111112o2222223333333332222222222222o +.o2222111112oo232333333333311222222222222o +.o222222122222oo333333333o11112222T222222o +.o22222222222222ooooooooo11111222233333oo. +..o2222222222222111111111111122233333oo... +...o3332222222222211111112222223333oo..... +....oo3333333333322222222222223333o....... +......oo33333333333222222233333ooo........ +........oooo333333333333333oooo........... +............ooooooooPoooooo............... diff --git a/legacy/parts_src/eyes/eyes_angry.txt b/legacy/parts_src/eyes/eyes_angry.txt new file mode 100644 index 0000000..a67c109 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_angry.txt @@ -0,0 +1,7 @@ +oo............o +.ooo........ooo +...oo.....ooo.. +.ooo......oooo. +owwpo....owwppo +oppwo..P.opppwo +.ooo......oooo. diff --git a/legacy/parts_src/eyes/eyes_beady.txt b/legacy/parts_src/eyes/eyes_beady.txt new file mode 100644 index 0000000..bb87714 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_beady.txt @@ -0,0 +1,3 @@ +oo........oo +pp...P....pp +pp........pp diff --git a/legacy/parts_src/eyes/eyes_big_shiny.txt b/legacy/parts_src/eyes/eyes_big_shiny.txt new file mode 100644 index 0000000..d13829f --- /dev/null +++ b/legacy/parts_src/eyes/eyes_big_shiny.txt @@ -0,0 +1,9 @@ +.oooo......ooooo. +oppppo....opppppo +owwppo....owwpppo +owwppo....owwpppo +oppppo..P.opppppo +oppppo....opppppo +opppwo....oppppwo +opwwpo....oppwwpo +.oooo......ooooo. diff --git a/legacy/parts_src/eyes/eyes_compound.txt b/legacy/parts_src/eyes/eyes_compound.txt new file mode 100644 index 0000000..55e5142 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_compound.txt @@ -0,0 +1,8 @@ +.oooo.....ooooo. +owabbo...owabbbo +oabcbo...oabcbbo +obcbco.P.obcbcbo +obbcco...obbcbco +ocbcco...ocbcbco +.occo.....occco. +..oo.......ooo.. diff --git a/legacy/parts_src/eyes/eyes_cyclops.txt b/legacy/parts_src/eyes/eyes_cyclops.txt new file mode 100644 index 0000000..7c9d89d --- /dev/null +++ b/legacy/parts_src/eyes/eyes_cyclops.txt @@ -0,0 +1,9 @@ +..oooooo.. +.owwwwwwo. +owppppwwwo +opwwpppwwo +opwwPppwwo +opppppwwwo +owppppwwwo +.owwwwwwo. +..oooooo.. diff --git a/legacy/parts_src/eyes/eyes_glowing.txt b/legacy/parts_src/eyes/eyes_glowing.txt new file mode 100644 index 0000000..daafe0a --- /dev/null +++ b/legacy/parts_src/eyes/eyes_glowing.txt @@ -0,0 +1,7 @@ +.oo......ooo.. +oyyo....oyyyo. +oxyo....oxxyo. +oxxo..P.oxxyo. +oxyo....oxyyo. +oyzo....oyyzo. +.oo......ooo.. diff --git a/legacy/parts_src/eyes/eyes_happy_closed.txt b/legacy/parts_src/eyes/eyes_happy_closed.txt new file mode 100644 index 0000000..fd4ebe5 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_happy_closed.txt @@ -0,0 +1,3 @@ +.oo......ooo. +oooo..P.ooooo +o..o....oo.oo diff --git a/legacy/parts_src/eyes/eyes_masked.txt b/legacy/parts_src/eyes/eyes_masked.txt new file mode 100644 index 0000000..b9db96a --- /dev/null +++ b/legacy/parts_src/eyes/eyes_masked.txt @@ -0,0 +1,8 @@ +oo..............oo +oaoo..........oobo +oaoooooooooooooobo +oaowpoaaabbowpwobo +oboppobbPbboppwoco +oboooobbbbboooooco +occccbboooobbcccco +.ooooooo..oooooo.. diff --git a/legacy/parts_src/eyes/eyes_round.txt b/legacy/parts_src/eyes/eyes_round.txt new file mode 100644 index 0000000..b90fe8d --- /dev/null +++ b/legacy/parts_src/eyes/eyes_round.txt @@ -0,0 +1,8 @@ +.oooo.....ooooo. +oppppo...opppppo +owwppo...owwpppo +owwppo...owwpppo +oppppo.P.opppppo +oppppo...opppppo +opppwo...oppppwo +.oooo.....ooooo. diff --git a/legacy/parts_src/eyes/eyes_sharp.txt b/legacy/parts_src/eyes/eyes_sharp.txt new file mode 100644 index 0000000..af90fc1 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_sharp.txt @@ -0,0 +1,6 @@ +o...........o +ooo......oooo +oooo....ooooo +owpo..P.oppwo +oppo....oppwo +.oo......ooo. diff --git a/legacy/parts_src/eyes/eyes_sleepy.txt b/legacy/parts_src/eyes/eyes_sleepy.txt new file mode 100644 index 0000000..3252920 --- /dev/null +++ b/legacy/parts_src/eyes/eyes_sleepy.txt @@ -0,0 +1,5 @@ +.ooo.....oooo. +o112o...o1122o +ooooo..Poooooo +oppwo...opppwo +.ooo.....oooo. diff --git a/legacy/parts_src/head/head_bird.txt b/legacy/parts_src/head/head_bird.txt new file mode 100644 index 0000000..d2b2734 --- /dev/null +++ b/legacy/parts_src/head/head_bird.txt @@ -0,0 +1,21 @@ +...................... +.........oooooooo..... +......ooo22222222o.... +.....o222212^22222o... +....o22111111122222o.. +...o221111111112223o.. +..o2211111111111222o.. +.o222111111111222223o. +.o222211111112222223o. +.o222211112E22222223o. +.o222211122222222233o. +.o222211222222222233o. +.o222222222222222333o. +.o2S2222222222223333o. +.o222222222222233333o. +..o2222222222233333o.. +...o22222223333333o... +....o333333333333o.... +.....o333333P333o..... +......ooo3333ooo...... +.........oooo......... diff --git a/legacy/parts_src/head/head_bulky.txt b/legacy/parts_src/head/head_bulky.txt new file mode 100644 index 0000000..89e5e4d --- /dev/null +++ b/legacy/parts_src/head/head_bulky.txt @@ -0,0 +1,24 @@ +...........ooooooo.......... +........ooo2222222oo........ +.......o222222222222o....... +.....oo22222111222222o...... +...oo22ooooooo^1222222o..... +..o22111111122oooo22222o.... +.o2211111111122112oo222o.... +o22222222222222222oo2223o... +.o2222222222223ooo222223o... +..o2ooooooooooo112222223o... +...o22211111112222222222o... +..o2221111112222222222223o.. +..o222111112E222222222222o.. +.o222211122222222222222223o. +o2222221222222222222222333o. +o222222222222222222223oo333o +o2222S22222222222223oo33333o +o22222222222222233oo3333333o +o2222222222222233333333333o. +.o22222222223333333333333o.. +..o333323333333333333333o... +...oo33333333333333333oo.... +.....ooo3333333P333ooo...... +........ooooooooooo......... diff --git a/legacy/parts_src/head/head_canine.txt b/legacy/parts_src/head/head_canine.txt new file mode 100644 index 0000000..597f6fe --- /dev/null +++ b/legacy/parts_src/head/head_canine.txt @@ -0,0 +1,22 @@ +...........ooooo.......... +.........oo22222oo........ +........o222112222o....... +.......o22111122222oo..... +......o2211111^222222oooo. +.....o2221111112222222222o +.....o221111111112222222o. +.....o22111111112222233o.. +....o22111111112222222ooo. +...oo2111111E222222232322o +.oo222111111222222222332o. +o2222211112222222222233o.. +o222222111222222222223ooo. +o22222S222222222222333322o +o22222222222222222333332o. +.o2oooo2222222223333333o.. +..oo332222222223333333o... +....oo322233333333333o.... +......o3333333333333o..... +.......o3333333P33oo...... +........oooooooooo........ +.......................... diff --git a/legacy/parts_src/head/head_feline.txt b/legacy/parts_src/head/head_feline.txt new file mode 100644 index 0000000..cac24cb --- /dev/null +++ b/legacy/parts_src/head/head_feline.txt @@ -0,0 +1,22 @@ +..........oooooo.......... +........oo222222oo........ +......oo2222122222oo...... +.....o22221112222222o..... +....o22111111^1222222o.... +....o2211111111112222o.... +...o222111111111222223o... +...o222111111111122223o... +...o222211111122222223o... +...o221111112222222223o... +..oo2222112E2222222233oo.. +.o2222221222222222222332o. +o222222122222222222223322o +.ooo222222222222222233ooo. +..o22222222222222233333o.. +.o22222S2222222333333332o. +o222222222222333333333332o +.ooo222323333333333333ooo. +....oo33333333333333oo.... +......ooo33333P33ooo...... +.........oooooooo......... +.......................... diff --git a/legacy/parts_src/head/head_flat.txt b/legacy/parts_src/head/head_flat.txt new file mode 100644 index 0000000..edcb0ae --- /dev/null +++ b/legacy/parts_src/head/head_flat.txt @@ -0,0 +1,18 @@ +.......................... +.......................... +............ooooo......... +....oooo...o22^22o....oo.. +...o2222o.o2222222o.oobbo. +..o221122o22112222oobbbbo. +.o2211122121111222obbbbo.. +.o22111111111112222oooooo. +.o221111111E1112222222obbo +.o2111112221112222222obbbo +o222222222221222222222obo. +o2222222222222o22223333o.. +oooooSoooooooo22223333obo. +.o2222222222222333333obbo. +..o22323333333333333oboo.. +...o3333333333333333oo.... +....ooo3333333P33ooo...... +.......oooooooooo......... diff --git a/legacy/parts_src/head/head_ghost.txt b/legacy/parts_src/head/head_ghost.txt new file mode 100644 index 0000000..a06dfe7 --- /dev/null +++ b/legacy/parts_src/head/head_ghost.txt @@ -0,0 +1,22 @@ +..........................o. +........................oo2o +........oooooooo......oo222o +......oo22222222oooooo2222o. +.....o22212^2222222222222o.. +....o22211111222222222222o.. +...o2211ooo1111112111222o... +..o22ooooooooo111111223o.... +.o22ooooooooooo11122223o.... +.o2ooooooooooooo1222223o.... +o2oooooooooooooo2222223o.... +o2ooooooooooooooo222223o.... +o2o333333E333333o222233o.... +o2o3333333333333o222233o.... +o22o33333333333o2222233o.... +o22o33333333333o2322233o.... +.o22ooS333333oo23333333o.... +.o2222ooooooo2233333333o.... +.o222322222223P33333333o.... +.o22333o33333o33333o333o.... +..o233o.o333o.o333o.o3o..... +...ooo...ooo...ooo...o...... diff --git a/legacy/parts_src/head/head_insect.txt b/legacy/parts_src/head/head_insect.txt new file mode 100644 index 0000000..ab71436 --- /dev/null +++ b/legacy/parts_src/head/head_insect.txt @@ -0,0 +1,20 @@ +.......ooooooooo...... +.....oo222222222oo.... +....o2221112222222o... +...o221111111222222o.. +..o22111111^11122222o. +..o22111111111112222o. +.o2221111111112222223o +.o2222111112222222223o +.o2222112222222222223o +.o2222222E22222222233o +.o2222222222222222333o +.o2222222222222223333o +.o2222222222222233333o +.o2222oo22222223333oo. +.o222o22ooo2233333o... +.o222oS222oooooooo.... +..o2o222222223333o.... +...o2222223P3333o..... +....oo3333333333o..... +......oooooooooo...... diff --git a/legacy/parts_src/head/head_reptile.txt b/legacy/parts_src/head/head_reptile.txt new file mode 100644 index 0000000..d75abb7 --- /dev/null +++ b/legacy/parts_src/head/head_reptile.txt @@ -0,0 +1,20 @@ +.................oo.....oo.. +...............oobbo..oobbo. +..........oo..oaaabo.oabbbo. +.........obboobbaaboobbabbo. +........obbbo2ooooo22obbbbo. +.......obboo2222222222ooco.. +......oooo21111^11222222oo.. +...ooo22211111111111222222o. +..o22221111111111111122223o. +.o211111111111111122222223o. +o2222111111111E12222222223o. +o2222211111122222222222233o. +o222222222222oo22222222333o. +oooooSooooooo222222222333o.. +.o22222222222222222223333o.. +..o3333332222222222333333o.. +...ooo333333333333333333o... +......oooo3333333333333o.... +..........ooo33P33333oo..... +.............oooooooo....... diff --git a/legacy/parts_src/head/head_rock.txt b/legacy/parts_src/head/head_rock.txt new file mode 100644 index 0000000..74fc444 --- /dev/null +++ b/legacy/parts_src/head/head_rock.txt @@ -0,0 +1,22 @@ +...........oo.....oo...... +....ooooooo22o.ooo22oo.... +...o222221o222o2222222o... +...o21111o22222o2222222o.. +..o22111111^2222o2222223o. +.o221111111112222oo22233o. +.o2211111111112222o22333o. +o22211111111222222o22o333o +o22211112222222222o333o33o +o222211222E2222223o333o33o +o23222222222222223o33o333o +o22322222222222223o33o333o +o22322222222222233o333333o +o22222222222222333o333333o +.o2222oooooooo3333o333333o +.ooooo11111122ooooo33333o. +.o211S111111o111122o333o.. +.o22111122222o221222o3o... +.o22222222222o2222233o.... +..o22222233333P33233o..... +...oo33333333333333o...... +.....oooooooooooooo....... diff --git a/legacy/parts_src/head/head_round.txt b/legacy/parts_src/head/head_round.txt new file mode 100644 index 0000000..153eaa9 --- /dev/null +++ b/legacy/parts_src/head/head_round.txt @@ -0,0 +1,22 @@ +........oooooooooo........ +......oo2222222222oo...... +.....o22211111112222o..... +....o2211111111122222o.... +...o221111111^11122222o... +..o22111111111111122222o.. +.o2211111111111111122222o. +.o2111111111111111112222o. +o222111111111111111122222o +o222111111111111111222223o +o222111111111111122222223o +o222111111111122222222223o +o2221111111E1112222222223o +o221111111111111222222223o +o211111111111111122222233o +o211111111111111122222333o +o2111S1111111111122223333o +o2211111o1o11222222333333o +.o2211111o22222333333333o. +..o22212222223333333333o.. +...oo33333oooooooo333oo... +.....ooooo....P...ooo..... diff --git a/legacy/parts_src/snout/beak_duck.txt b/legacy/parts_src/snout/beak_duck.txt new file mode 100644 index 0000000..4a33898 --- /dev/null +++ b/legacy/parts_src/snout/beak_duck.txt @@ -0,0 +1,6 @@ +.ooooooo. +oaaaaaabo +oabbbbbbP +obbbbbbco +.occcccco +..oooooo. diff --git a/legacy/parts_src/snout/beak_hooked.txt b/legacy/parts_src/snout/beak_hooked.txt new file mode 100644 index 0000000..7f709ce --- /dev/null +++ b/legacy/parts_src/snout/beak_hooked.txt @@ -0,0 +1,7 @@ +...oooooo +..oaaaabo +.oaaabbbo +oabbbbbcP +obbcoccco +obco.ooo. +.oo...... diff --git a/legacy/parts_src/snout/beak_short.txt b/legacy/parts_src/snout/beak_short.txt new file mode 100644 index 0000000..f736b06 --- /dev/null +++ b/legacy/parts_src/snout/beak_short.txt @@ -0,0 +1,6 @@ +....ooo. +..ooaabo +.oaaabbo +oaabbbbP +.oobccco +...oooo. diff --git a/legacy/parts_src/snout/fangs.txt b/legacy/parts_src/snout/fangs.txt new file mode 100644 index 0000000..8228718 --- /dev/null +++ b/legacy/parts_src/snout/fangs.txt @@ -0,0 +1,10 @@ +..ooo... +.o1111.. +o1111122 +o111222P +o2222233 +.ooooooo +.owwoww. +.owwoww. +..owoow. +...o..o. diff --git a/legacy/parts_src/snout/fish_lips.txt b/legacy/parts_src/snout/fish_lips.txt new file mode 100644 index 0000000..c3c6e09 --- /dev/null +++ b/legacy/parts_src/snout/fish_lips.txt @@ -0,0 +1,9 @@ +...oooo. +..o1112o +.o111122 +ooppo22P +opppo223 +ooppo223 +.o222233 +..o2333o +...oooo. diff --git a/legacy/parts_src/snout/mandibles.txt b/legacy/parts_src/snout/mandibles.txt new file mode 100644 index 0000000..d03220e --- /dev/null +++ b/legacy/parts_src/snout/mandibles.txt @@ -0,0 +1,10 @@ +..ooo..oooo. +.obbbooaaabo +obbbcoaaabbo +obbcooabbbbP +obco..obbbco +obco...obbco +occo...obcco +.occo.obcco. +..oco.obco.. +...o...oo... diff --git a/legacy/parts_src/snout/muzzle_long.txt b/legacy/parts_src/snout/muzzle_long.txt new file mode 100644 index 0000000..24f889a --- /dev/null +++ b/legacy/parts_src/snout/muzzle_long.txt @@ -0,0 +1,9 @@ +....ooooooo... +..oo1111111111 +.oppp111111111 +opppp11111122P +oppp2222222222 +.ooo2o22222223 +....oo22222233 +.....ooo222333 +........ooo33. diff --git a/legacy/parts_src/snout/muzzle_small.txt b/legacy/parts_src/snout/muzzle_small.txt new file mode 100644 index 0000000..a9b0b73 --- /dev/null +++ b/legacy/parts_src/snout/muzzle_small.txt @@ -0,0 +1,7 @@ +...oooo... +..o11112.. +.pp111222. +opp111222P +o21112222. +.oo222223. +...oooo33. diff --git a/legacy/parts_src/snout/reptile_jaw.txt b/legacy/parts_src/snout/reptile_jaw.txt new file mode 100644 index 0000000..c11b707 --- /dev/null +++ b/legacy/parts_src/snout/reptile_jaw.txt @@ -0,0 +1,10 @@ +....oooooooo +..oo11111222 +.o1111111122 +o11pp111122P +o111p1122222 +ooooooooo222 +.owowowowo22 +.o2222222333 +..o3333333oo +...ooooooo.. diff --git a/legacy/parts_src/snout/snout_pig.txt b/legacy/parts_src/snout/snout_pig.txt new file mode 100644 index 0000000..9c58344 --- /dev/null +++ b/legacy/parts_src/snout/snout_pig.txt @@ -0,0 +1,8 @@ +...oooo..... +..o1111oooo. +.o111111o222 +.o1o11o1o22P +.o1o11o1o223 +.o222222o233 +..o2222ooo3. +...oooo..... diff --git a/legacy/parts_src/snout/tongue_out.txt b/legacy/parts_src/snout/tongue_out.txt new file mode 100644 index 0000000..acd59fa --- /dev/null +++ b/legacy/parts_src/snout/tongue_out.txt @@ -0,0 +1,11 @@ +...ooooooo +..o1111122 +.o11111222 +o11112222P +oooooooo22 +ooaaaabo23 +.oaabbbo33 +.oabbcbo3o +.oabbcboo. +..obbbo... +...ooo.... diff --git a/legacy/parts_src/snout/trunk_short.txt b/legacy/parts_src/snout/trunk_short.txt new file mode 100644 index 0000000..5ecb286 --- /dev/null +++ b/legacy/parts_src/snout/trunk_short.txt @@ -0,0 +1,14 @@ +......oooo11 +....oo111111 +...o11111112 +..o11111112P +..o111122222 +.o1112222223 +.o112222oooo +o112222o.... +o12222o..oo. +o12222o.o22o +o22222oo223o +.o222222233o +..o3333333o. +...ooooooo.. diff --git a/legacy/parts_src/snout/tusks.txt b/legacy/parts_src/snout/tusks.txt new file mode 100644 index 0000000..c967bb0 --- /dev/null +++ b/legacy/parts_src/snout/tusks.txt @@ -0,0 +1,9 @@ +.o.....o.... +oao...obo... +oaao..obbo.P +oabo..obbco. +oabbo.obbcco +.oabboobbcco +.oaabbbocco. +..obbccooo.. +...oooo..... diff --git a/legacy/parts_src/snout/whiskers.txt b/legacy/parts_src/snout/whiskers.txt new file mode 100644 index 0000000..5ba81a7 --- /dev/null +++ b/legacy/parts_src/snout/whiskers.txt @@ -0,0 +1,7 @@ +oo.....ooo...... +..ooo.o11122..oo +.....opp11222o.. +oooooopp1222Pooo +......oo22233o.. +..ooo...ooo33.oo +oo.............. diff --git a/parts/aura/aura_bubbles.png b/parts/aura/aura_bubbles.png new file mode 100644 index 0000000000000000000000000000000000000000..77dac424eb60ddf870a636b12696104f06a2881b GIT binary patch literal 524 zcmV+n0`vWeP)MzZ<({6LU`L$LA5=j~WEE5+qs7$DnQlE9Rn( ziRb&yX->qc$h7>NVoYthhF5!+%qioUk;k02jQ4ZQ)#n^*6ANJ1*ZnbRHBV@S2|K9@ z`$u)Gu~V5?rVKswIcoa3Y9xv35q1LlcH28%ih+{B^OQS8Cw0pOeHp}GY>0*ia&-G-#^ z{Mp6l=P^2h8t>Rf&|t2szs#Y9cWkp_3S{tm>x?9c*=DizC!%^WZ+BsCW&r>$@;ezBj)ZoivU4>UpDo5Khc>n+a00000shK~YFuVi+YKLC{ O0000p literal 0 HcmV?d00001 diff --git a/parts/aura/aura_dust.png b/parts/aura/aura_dust.png new file mode 100644 index 0000000000000000000000000000000000000000..31a768ca7fe71e0ce48c769c02f4223c4923bc96 GIT binary patch literal 765 zcmeAS@N?(olHy`uVBq!ia0vp^At21b1|(&&1r9PWFdgx9aSW-L^L9q}q-F&k*NvP1 z>(*74tqD4$EHGi+x_ictZ)mmMIUU@~2-Jcb>`>SuU*6dFs#134om>8Y%MGTUyXJgM zK=SeCAJ1c)&c~kFvwB{7VbapZ&Ay-||^6 zD|Y?M{`ll!?TPu<#p4gYSkzl&vOcZeZB53L@MGRHt(4a5)$?s%wDXx+c1YZ#&%x;i zzB;R}1n$1?oxe9K*4h64THTya?vpNr2z_1ZpE_&D&OK2jqM3#D>#s;;|8@O$JLjQV z?V8ly3$u>D*r8myvQgiteVJ`#Oyf@}ztFhm zb?b}mzvb%I*>f9%gbI}{)UxKM-N>xIx8?YS=Nq#=+Xi2`wpD}G`17@|U6&wa3-c`1-Utiyzv3ZfV`=nd(tspLZr^ zW=&6)+;7|T1(%J}t-ABR&i}IZ)&JAQy(hUi{)<0fTx;@VuUGHSmKjkzU)7!!J-c=2 z+@n)JHobKEn&o|HZsqwW`#*CWUcRN>XC6PR{wqxdQ$9&=rzc;v~+nHu0f2w{h^7Pn@yZt-(W|rjX`1^mYIrUXc{qvWH6_@iC zcUm7?8F)0Ab#CFYyU&07ujJ{TDBbF+eotroV4nQ#8ZJ)h5c;+dV3fOM0aRGaes*ITc= ty8NtG%H+rGXYn87KZ6o56g+$PhiTit*?ngUJ3oPhJYD@<);T3K0RZCxa^?U4 literal 0 HcmV?d00001 diff --git a/parts/aura/aura_embers.png b/parts/aura/aura_embers.png new file mode 100644 index 0000000000000000000000000000000000000000..521daefd966db3d1319df023565272c38611c241 GIT binary patch literal 491 zcmV7{xl& z)TepB*(3z1Ipf<0OKo4Du&)2o_c!<1jXVynD$vxvHO#t`={C&xGs?_H*uP_F?BC-W z`q|QWX>0pz?srWrAs4SjsehH3;$nm@A;k3hmPz0-_8+MhQ+{!{HFB$U3W?IzC~MkV zUYu%ZtxICC&OZOX+b}npBTJNyX?X6>Ism{Wxw0O5H`@*;z5Yg^^F<+R+R*iK7WrnEK*yd1QzxY$__IW4J&}r4`Lu1{T$a3}u zwoN0h8#BqdMJr0TbOB%nTznd=qdglr8n#FsFy&_;kK&9c-i~KzsbEB?8mPW?Ky9M~ z)==CK!@AJBiv7&DjjLtc0wGk@KrOzCGEtyS-+;poHn1Hq)q&g0Ql1<+SF|4|Q02!s z2~_!EIReeXeq0Ot@DA9A16gFooO)g5g&$_o4O5&yM(*{v4vFV5t$q|sn*^V{&z>O( hVFCaE004k3fiF%39?rO;0T=)P002ovPDHLkV1oXCwggT*CgNwmnFcAO%0000;fT)@J+X+nsQFSJ>Zj(T=EsYr?pf>1R0@h2 zzU4**1h7iosbeoeHMmvkfm2;7*<(Mr7R@X}I3#zcy0uXL6?5Uf(N2bGAnsObJ>zbp z3sZx~@>S%NR^Ckyrq1zjt~blG_vZZVvdA~E&MHIU2`yC1e9zbVDW`qwwbS0z%=K9{ z10jI=g3=b7-V+&s}vd9~~)h#1Qm=(p?P} zRpTSJ#}x?E_!I|$D&B`a?^8#79P7a_7OJGm#M2Jcv|z-IXC4H7dnJ;0)P$f7EVLwo zjDa~2A|Y(I$J|?`cceyJaKBXfKJ+X<36zHK9t8|=$D})TRTfel4~-zEG)!JNYCx1V zRK5C=W1+l^UbH+*&{>% literal 0 HcmV?d00001 diff --git a/parts/aura/aura_leaves.png b/parts/aura/aura_leaves.png new file mode 100644 index 0000000000000000000000000000000000000000..3b5e4e311b7712c9253109013e474c173a99f35e GIT binary patch literal 654 zcmV;90&)F`P)2*oSB#eM={G+$#-ib_9lV&4T zT`tVvASe?JZ5DwpMRSyHW3eT|q`vni^jylZTk5mEDkpt9p&ryxK~a1Ypn zFYke3H~Dp1mi)5nYwv>++RjZb=1E-)Zr#_853to4w$#@ywV`8T+Tk|mhomnE}Zi}$crPCXzDmZLC9NQilq^2;%<09uJX0D%+C{>{ZRC` z>Vp2(0lrn|++Lx|3xHU1b_*MPc^PpQcxbh^{dowg;6LEuvneX7eC5a&2&#kU6Z+~k zapAoI0002sOs zN1R6QZQr{cR*r|t0qK@)h5dGdm78b%O!uh^V!2U3znR^_t|-#n;Q3}2xtX^Sg~Vl; on9Z%pBWxxB000000D#)_2Z2Syq7xQ~KmY&$07*qoM6N<$f{pw%#sB~S literal 0 HcmV?d00001 diff --git a/parts/aura/aura_pebbles.png b/parts/aura/aura_pebbles.png new file mode 100644 index 0000000000000000000000000000000000000000..7643935c9ed072ad14c74ae3476d5b1e339129a0 GIT binary patch literal 786 zcmV+t1MU2YP)vDu3425B5-@ELSr{f)UmurGt&NtH^Z8hQ%Bq2ei00000&?j2hJ~3Tqvy)?st1V6> zjgv9UJs6|2ggET%X7odC@$wR~OJ@e2C{<}@tz z{90#ds~4w>8aZsf4u@DfwPFc6@pt(*ub^LMTb1j|G$bnTkoSnG?Dv~i?X%u&+JyS9 z!6mAURlL0yvAa!{>tb^i)9;Q%T(en%-=Sws;>0xhp<^?I4tcGVw*0|~lV9N#^I$q} z{$rP-#=JvUQSIN$gpMsyAW|ZZ(6JfB7Dw){3Y8l=0t=E{G6Nlhp%;&HVuqk`cztTA z6iAN*rDGsy0xq8#L!Q~Emi9w?0zS16@Tma+KnS1PG)(G(3mqC6be4k`;}srek$B}= zuY%sh93_3hfeuR>kCDFMz>!BED;E@qcanBaN|$mJ-|?c5BGO7f`7q~BwpGpis>X9I z-|01nzMc4J&(x2Ld9H6<=b76C?OP9hT*$ik-0DWJK&(P#j^O9A-89w-OL&ZkirM;; zBJTF*Bep=703z*A6}0H?Gy%lgpRp138c8)3MTxclm5~8#Rm`f}k4XuDrl{h* zQ7K9JGziMPr!{cnSJd$>lf9?)6&_7A8xe&Pu&31@0Jy5}hTF8ov8OetoN<(_r}?^g z9CzQIoYHWKI?CA7>g}N;7MSY9J+0vfl=>o9Wz_nap`BqKhSZJHsD)+{+iGa!LNjhb zsnSBTV#|!&Ly(vY%~-xnR_e=)5FA-x@&Em=WJxJ~-uBpSz8B7>>fjUh%MMhs56OacM`00000091qg0|U`huW!vDu3426N&2WOu=9Yt3htAHe2&-bI#ZYfC)SA$e30000008j{0<3PC;m|O>}r4&a< zm4}345fOAH(h=ftIFVSJd%0{_1dV_Whl`0yyP1*=`MGySX-+Y%Ra=!y)yof-)5gW7 zv$2TjY&st_Yh1B)Edo$vi*99u<;nYfY~8t}*j!^skNaBocE~di6YaI8|GgwqHXrBP z>^am8<*(|W?-olRi_$dY3aZPD-cqcwum=>o(4788^$*3DyI;V#hWrTKR7+L7_0DxE z8=>cMvuPvSL&1ZfgDo`eEMOS!1+<|-8ia((Ce5KK={Ks7s%s*SAN{^m#>t4)joFASg2sw4$JmCOi6k`N5~kf<$g{C~p>kX_)D~CbpOj)64!Nh)NtW zKd$lt12rxp)V|;lJPG}DbP+-IaKXXwoF6rDZp3unly}aQbEf1Q>`_ROSORlsPSs-) z(ge@)%s|ji1SQoz)Jt}r60Z2w;;oOtEWX%~x9vVbv0dd(5p>erXDs*oR1@CtLfI5e zLPH+iG*J`5IaD=^q;J@VHUf40LIg#=z(A#X=txOr_85)8wG`dQT6Vkj$+{L~gs#l! zhPPZ#z7@H8^Ltb~|6>AbeXh)yTaOrzxZ`3pV_li)D-b4TZ0Pdvet9lQqpT_vkQ@wpa zC8&t1$S2ja+lz77o;DS#8aLK*d)6ONaoRu+sJU(J_kb2CVFCaE0C2_I?WC4m4^3;6 zZbH?)>#&alVLGJm7xNYS7{l=G`Qkt*b*O2L)6J6u5?KU;ytIQ%#p*&R%wbU5VaOL`-u=VVL2v$=-8!tT-U;lEr9 zG}i+i^v+T4)Zcem$PGpwrcs(1fU<)V(TBM2&#qVBpXYR>hu(6k2MKpwVs`D9Q{EVXUZmH1bZg`U z0000WPaD2-Fl+n6vV0}BS(=kdZ(?Yf9gd59y^hFpA9x>>zvSJ5}Epeq;RB^zFm xt7Iz?+~vnw%d>Uu@R$Gq000000001_6MrFs`4H6#GL!%S002ovPDHLkV1gIR>Z1Sv literal 0 HcmV?d00001 diff --git a/parts/aura/aura_snowflakes.png b/parts/aura/aura_snowflakes.png new file mode 100644 index 0000000000000000000000000000000000000000..7753631ba7bec3b9fae73d137cc3093b9fd27af8 GIT binary patch literal 605 zcmV-j0;2tiP)SqHoT`WmsmL|`*f9UO#i}`FE_vBgwsqXCQTK+0hNf_!@ou9dlgl=XJ{nVN zjS}7H$YSMAQVpjfe?EViuazv{&%aR(T>NgT-2QZS&V}mJvLj_FcP_Ke2t7)4E2S9q zWm*uo(UErNP|Llpk9CVvll2W)uAC<1p%PhBRLw}KC%lJnbGhcZ_;$T@#l9{OG3kdH zAo#Go_^urg`>*M)7IWxpd{lRNas3$I>~`ozn*W1d@{05R{Sfk38)l;}#tlY>41zJE zm_1yFXMW|g>##E3Y}DzR;zH~sy3HA(C$XM8?Mh!2$$BWS=FC{~vMqlZ(tw}8oRI%E z)UXEJEf^C30000=vn#k^2_*dwXDjuMW#w#!6xp?1BX)`GTLSa^VKV~qBvXsJoFcLx zlBWjbu*i-)RX}8CblyQ^F90Gt0000a&|tGx-@KFW*a>ldz>~ctdqfUv9IduwTjdOm zBX1|fzAQ*3VDMJ*cZ0 zTKoK@R#9P%wQlX;{G{&o$E*GGlg^0>EB#r_$z#+NP8KUw;NH&aDk{{<=O^W=LOng@ rrKTThbV}(P#h3s900000AY<|m?odc|9cWR~Dtwl5mQU33)>)hHHBE$eOArL|cA%u7U&)1RfnJ_5Jhor2O zvOQ7%JsYEH8WhqENooA~Q>8=USe%EFT>Nt!Ps)rT#6@nbxS%T)#*2|8boq#eV5}SR z>Q&P(Y7P0M%j+$lx}<}u!HC6%Ok-DBro`XBl>(Oo2(C?G0@D^Wyhabv*53LiVk@{hG1Q-{=M}VUuL| zW5H98QLo%H59zkYp!q6oeM~mWk(0;Zma2(EwL#y4oM2oY(3s?Qe02{Dn}k=}N(QB~uH5h&yllTZ z=WWfjeywyBqcNP%c{ukPW5JDWng$2Q>r?+H<%fp~w0S@^qgL%qF2q6YQ zI&A9Mf1zewc#lE3&|Crcl!W<~&WJBLp?64KRLO~b)a8ZRJUrDHwVYV7yz>j9niJc5 zLKSM-KXjPF=z?B*=LmOy73jG`5kmB#j}e^9bMZvbpyowVymRJhDdgL^Xxny`jnt~*82*Ysd!A?A zrAbVa+1j4Fyq027XUK8?3qh8yf^I?xA%qY@NRa0jHW#43!S$k?00000NkvXXu0mjf DaHL3z literal 0 HcmV?d00001 diff --git a/parts/aura/aura_sparks.png b/parts/aura/aura_sparks.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a64a1c3448cfe9d4749dc50fc58761a307dcf7 GIT binary patch literal 522 zcmV+l0`>igP)Xp3`n!v9@todhHplT?O91ONb_2F={wy1$iL+gPL@4dK~W zP267BkF|ojwpX={qMg;W4BhsNe_!3-+SUrSYs$|Drbc z%yfO*ubtZi>9(UShME$$e0XXn94YY_$6%>k6rR!7 z7%bKUh0wgH;YqPaFz+R=yJn{*7J|+XbR1-i0l!#)Q=o#KLOw6ssRezdxvOT$%YdT}`p z=F-F&GB>NHe$POy+aH0*3o7p!yp+tSsWLN1$spW5m;e9(03cob0nGHALH!?lZ2$lO M07*qoM6N<$f`A_KB>(^b literal 0 HcmV?d00001 diff --git a/parts/aura/aura_spores.png b/parts/aura/aura_spores.png new file mode 100644 index 0000000000000000000000000000000000000000..dea1df28a63d1345df67ab765aaf073652a1771f GIT binary patch literal 658 zcmV;D0&V??P)Y!E zACH4xKfW6iViWSH&0OfB{YE`8H|~o1Zp4UNokBb)rRSgclWH!u74!Mk{$>q*1`)mc zlp7Nvx+EEN7>u>1;?RKW>sKq7F2%H-hC|NR z*P91z>TG?z#y)KySP=SIU+-o;6%|BHt%G%!z#+!M{yRPB>NlECcx(nKt;pk^2v=fq ssqCHX6ET&TcYsg;000000Dz#vKNXvzP$9Au8~^|S07*qoM6N<$g1>etGXMYp literal 0 HcmV?d00001 diff --git a/parts/aura/aura_toxic.png b/parts/aura/aura_toxic.png new file mode 100644 index 0000000000000000000000000000000000000000..6eaf870455b48dc91afaf4080f3ee8abe14c3e53 GIT binary patch literal 768 zcmV+b1ONPqP)C{njE<>#f zN&@4_+TxDRVp{8(^?Xas;RG!o*7)@|yQ*bvDSoXY%!XU*IuR1}i7szb#N7V(%8jVC zMUY-hapSmP6c@ZPVhdU}=Xsyz5P$bLY>N7H*Bz*mmYF$xmF;m!-Jt;v{w>1B32JKZ z4ll5dVZTK~n5QfB-cl!(Uq9*B-tJ^q6z_4~mU)UX)L~1FCOU@h9Ab>4rHMUn4IGST zj&r|>&gIQ=uCG_jhI3~MOr$u+%lLNg-nC{*Inw6;;}NNK6}j_UaE(DVvdQq2pE0X3 zt;Ns3Fs4YGyByhMuYkPr?F5LAYE9BOi$1ne-G!9`Dq zD&WIS(>0v^yat=Vafae<~XkeCxY!+!)%B%v^erm)RWnfE#_z3{elOr}D)?^g4hc}(c`JQZh$_aYd zkbvH55Z?OE#y2D&VI61GYn?xeyGR|ZKhd?0z&W(|Xl{H@D(}84V3;`@V=u0000P)CmjwU-0000q4(-QK*h=-%K~Ea# zhDr#UVw71wp*W3lL#>QqNq5^+64l$hnGlR{7c-Py zzjQxHWq0{=D|72_g>QxsRR_ddLEqhIKU|D2J(P(mmu%}+&t~h}?#*`DdX12w`&Sox z&f~?1`?Jrw-uAsddWLgy{#3$;n$6H?G5!Djp-L%NqkbWkIrOGik;MVA2p002OXU^btD70gwdjZ2NL z=9CC3Kl86fmrGA9HE+3Xa8^23X>cBUZ#ONJ*3e-)>#jr z;I0Qlu&msuPd^Gl~*!j!3kqcL}$w6M!?=w7}k5zypF_vp)Sj z56HYHz}kY+(mq*x3$z5KKWRQ)kTPY|7<8z^{i03pEg=8W6y?^LTbH*t%I?x#p2Ffl zb@?ihMRh4N6XT37hlAv*sbicQc*p4O=Sp>eXnWrb22@wJgG?rKc2TsX&cL tSrgNMLLiH<0C-vp00000003B``2kAP6k5=)ab*Ai002ovPDHLkV1mJtB|-oI literal 0 HcmV?d00001 diff --git a/parts/back/cloud_puffs.png b/parts/back/cloud_puffs.png new file mode 100644 index 0000000000000000000000000000000000000000..ecef2d94a2aeab3c4f47c40563f37d6c15d21f0a GIT binary patch literal 272 zcmV+r0q_2aP)f&NgF{xH0#a>wmulJgBBqMGWs#Net97Ud6xq~pOh2k%v<4RiTsXC z>WN0u58Aev!JzGgTp}j`=^nQ;o|6{HIVs~ccx(m)F7ZzNmhEQC$4UJR7oYL+S}NWl zFW_x8Q}9|wy5kwpD;u9s5~o?Y4T=j9$5b2vHY3IL2T(I=#^-@UIUPh{?fqM)$r2AWP=lL%v%8EyCG`DvG6L*(Zi z-f1hJI|eW#xN>4p<>@_PxN;JuO?J{IJe}YQv=l!Kq{5;X3?&uFXQqrsj)KR$2V4+%JIp;cF3%m4rY07*qoM6N<$ Eg6#8U9smFU literal 0 HcmV?d00001 diff --git a/parts/back/crystal_cluster.png b/parts/back/crystal_cluster.png new file mode 100644 index 0000000000000000000000000000000000000000..708fac19183f449846c327c45e4a251ba32b38ab GIT binary patch literal 321 zcmV-H0lxl;P)v3`C3Zzo}n78{{IXfVGg_ox}&Lq=)H+XiiAaw6-Y-5D^!kpMZc7307%a0>Lr< zO%SYh!U|Hv$p-QzFy+ej4{pp(&0Q0&tOaQcD?wl=OI&u)!R+E-kRxrQ_LTvM4x%@X z$Sn!o@lGPBdMD?BbmSVvco-UYZVAU%C|`4_Pj?+*uj|ht=+iCP2s7doeRk$cj_%`= zlwoW-^gY&|8gEECDhrEJG(fk&J;9xIF1tCG&w+OCuY<3v07%jx@CFxplZ~LN7RFCrmMEK9ykm}(+l@JBMFx8rGFC{{4vgl|j;6nR6Ea@fS zc=N-GS5n^Gu&Ijq0jl|Uoxl%48=1P;Jvucs>RllL=}GIgSkM_d*hdq+8o|Fco&yv4 Y0;O+Fek5}$6aWAK07*qoM6N<$f;&B2Bme*a literal 0 HcmV?d00001 diff --git a/parts/back/flame_mane.png b/parts/back/flame_mane.png new file mode 100644 index 0000000000000000000000000000000000000000..7f014e974dfc378b9e7315d2a305f8211a00cf1d GIT binary patch literal 416 zcmV;R0bl-!P)DvLLm(^j=_eNSbKjVz3^W!Q&pyw zun{1zGl+ic6Fa^)2l{&~g2*6%3Ufb1moYhOLpW?2;t|y6$A`NJD8f-{MSq}?7lCYgo2zO%0~Z89 zBm%Yp*aIN(G4#bVARasd9(ar@$>ytU<2UaU3jd=7!4n5(ApKSH3s!bX!M>NQr!rj( z0#XW#4hrm^Ca_ldsuhRF^1i#sb;$Pq5g38?idoGQFh@`+Y$S{tp?c+9d$XWAh!jCw zhkLO9Ig(21{aP#djYJvNLZ!z7pEU&;8XIbF=SQoaUK(uL=`e(&?*pvu|kKZ;J6%+}2lEl9vY6|`XQAZXYV44PW z*RBVcegFnIv1=G7Ut}z5mO%u(mcb@Bn^F_+sEMvP)AFW58*3ikvJgecxY@MHqjFOB zoVMazx4IP{Yk7;D-i>v(<@41C(L>~EFq^+g-O<&@Va~SS#>kpNCch!t{5vL~YT4n-;=;m_>;RW;b VV+e$HH002ovPDHLkV1hL;bjJVy literal 0 HcmV?d00001 diff --git a/parts/back/leaf_cape.png b/parts/back/leaf_cape.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b3df09f5c7bfcb3d4d60dc15a02447398e463b GIT binary patch literal 396 zcmV;70dxL|P) zI`^Q@LYRsos_*Vq*|&tL>}XLz1mg!fOKpziS<4>^dj5_2bCFhENY5ATOYezH0jD<1RXgD z>ocwE%E_U55Rfr?tYY+?Grh$b^Yvucpw(gmNX!lgL#kv0j*EKzj>frZQ@kzTPIthW qwKV=W@CP4q-Iw6OJjAgy{5QVG_0VGQh49`00000%A_P)()FnIVxq5`2wsW^$>_Yb z5~cJ2$*Ak$ynlXR;+H!gcnT7rs3fWZY@7g|S)#v1TgV`R;d? zY`Gee1UtP)h3?m-@P5JVAJT_ZiE2!}{mC>e2iznPZSbWAo{8k_vXt#=hiImNm%;d0=k|XsX zxKI<|h#V_YVolbfEoO%t7NwWE=;99fa@9XoW+A$&RJONsfxLnhaW_mVp{=??A@gug zJ|dpJ64pB$JIVwqCgEN`v3jV!ww^;{d7Q_fQ~SAhiFc5SK%BTrT5&!;d3dhBnRod5s; literal 0 HcmV?d00001 diff --git a/parts/back/plant_bulb.png b/parts/back/plant_bulb.png new file mode 100644 index 0000000000000000000000000000000000000000..ed29097aee4407254c9eb2aa1354457f0788dedb GIT binary patch literal 376 zcmV-;0f+vHP)mn zHmucm?{svzV}juM9~%zIP6I zVbK$EO;8Efs{0zg@c=~UqjHGgOA_io^`2r^IYBTXi3nq+=5pJ#-VdGyesDrt$!qsd zy^#3omYl_yJb)??e4vO3BOjd^*flKgyF?!QpFfJc_XKU>f_V7~B|)=3e|*1*XZ!#v W?t&Nyq|k5x0000X1^@s6veP%{0003>NklZ&%)6b0oz%7o!YVNg_*q245aM}) z*9tKgl5dIN>f;8i5W_|_jM%MzRba6qoKQ|B)}pgf=_+(o}Vi+;)IU}x%br2j$ z`veMypoMCK5Rg$mG}*4*Ny;VR>n#i$=MFwSrVhohtnSE&tX5o>4M ztUb3?eJpJ&vk6r>k(lGxPWc(Qf36l9&w8^9xw8NO002ovPDHLkV1gP$tu6on literal 0 HcmV?d00001 diff --git a/parts/back/sail_fin.png b/parts/back/sail_fin.png new file mode 100644 index 0000000000000000000000000000000000000000..68cdec5f275f79a00b2ff1c778f31f89b1890ae5 GIT binary patch literal 318 zcmV-E0m1%>P)ZB)LXuKIUYN@GW{>x<=?~uufyCsV423xx-`o1+-3T&5X`zTazxGTrlL)q-z2SuLuQeg8i~rIGmZ5mc1HJ*sL#V#D zDW5Z&6P5x;fvR7pN4yExz*#WlfxJEA4ZxIAgg|UNr$emm1Fx6O5d+-ycJ0(4>7YP6 zwZQuT@V0Am{J7u@Fcq|jy0$kzk7%r6+QLJ{NOE{jXT+6f-DXaSPqgk4@lN!zPV>~a z&hAfg$?mO-V9iQijJZ9J6Ne`2UkF1cr;P!;(L5@zjSV$3uz*nlMkrB-%l9G?my4QLVNhk8d?9@+QMu6-}XAa}dHy7SXd; zia&9VI~)`31R;wlIh}d35d}9QD@JO~!!@{*bRlOeN(`JLJ&==+I&EvEZG^Nwi>BU3 zp0vn@AH3>@|FZDJmDg~8Z4?72NX_0nJbsjgXA!+Cp$18m4@K*G5;GhGN&ik_)bZX09Xy}_0000kh&o4228vy;q+67^S%bAYAk228WpVCqTH% zof#l#I}&sUD}lh+i?&dsJD+QqS$GRb6n0KTNA+#SIT}#k4XlLDxLU335-gy;I0kv? z)95gvJBJbXK(P^Qj#HcAUa{*94h;#Q#9yD}j0*Ms6f$2^C5}PCgqtI zT6!v&kfZMKL{YI3$Jt~8B}J_Leq3A*XxjyUYnqX{r|2OhP~zHOK&#{$fuxHDNR6p5 bC-Xf4dYfx>Ubfi*00000NkvXXu0mjfsy}nE literal 0 HcmV?d00001 diff --git a/parts/back/wings_bat.png b/parts/back/wings_bat.png new file mode 100644 index 0000000000000000000000000000000000000000..d682a204cc7b0d2c739d56ddbd3626f1120a278d GIT binary patch literal 483 zcmV<90UZ8`P);=H`x4ggo;L>IYS$|!OsVomZM1D0XG)F{j6 z8M379TsDk%D2nnsX?k>BccW`5k?s?HR1BkBW@7((;DQ;-QRLT zp{@I#X0)rpZRN^~4ztfQ*8qJ~N3w|jE>jNWna3*)OW9aH`Ya)%diK#R&)nC~7Izv{ ztG#KHzAQlFyUWA1!vq3Z^m2eoRv59T%aUN33^mLhCNvZQIrkdv7Y>8uIiAeRSZ2_4 z)N+uJ4n0y4EMipxvIbdVK;}}zz1vx`u+I+UMEp=!q1W}(+9P#18BANS1z z4eLA`+Fc;+6J}r9r+z+~w)g<$+1#+mJ+w*P*tYd{oYXnOwK?%s;z!s=l#1_8j^f^f15l{jGX?+XXt%e34o3PQxUSo!;VYB`J ZhhIO2h)2+P4lDow002ovPDHLkV1fZwEO{!I9{ED3q#0Hf%9NBb{;OX+~k!jps24+&*Ua|}NM%-85S4FF3NO1fgWOZV z)P&UbL>C#s2HC=fxDWYIYDtY$=I3BL+!I`6o9jAo2r!169BLkb>q*MS?%cK$1kB;x zwmjCK5B$EUMP2*joz!#ez@=)@&=%+7Cs$XrSct*Suk|huccY}TaN!u@>T~Mn;MH_( z)3Gn$1rl>?y%-(1ZR~zcS`K2TtfS&ynubbfa}qXiFj0e4WC3wigFq3a?9~o($HFNX zst3zhqp@N^L2P;n77Ki?EzZyyxXsOhibmxh6Bn)soDNuZ(ao>T6|Ui`Yv`UQ6}178 z5HuUe0|vo0|EO03aECmPeXR$iTS{)B*8XLza`A9I3EZPC?OzE=QqCKr{g=e7QapdVG!Pn^*0D4l`!@V}d00000NkvXXu0mjfUR>!= literal 0 HcmV?d00001 diff --git a/parts/back/wings_dragon.png b/parts/back/wings_dragon.png new file mode 100644 index 0000000000000000000000000000000000000000..1091626a90a812db5e399bd660482ad2af949c47 GIT binary patch literal 582 zcmV-M0=fN(P)+$ zf>ny{$+!^}O%w>{y*5TK;?cli$2+sYw6DpQ(UK0^E-PJJdSnO)$ue~*s^WlZ1aK$` z>=??tQcjP}8fvD9@dnIP)z~_KrPrOeDqgt)V#9dNz##j$YXB2x@>Y*@-hP8MW`s`_ z;Th{{7_u$Nb+4#38~G_%5>&-d{#E2bGZ9cyrVwP)6~lTk7=`BSMNRW#G`t9t8xTDt zQ)VCs69Rg?67Qc7ix-VJfjLnvh1hyc*`ehA$vd`XQ843|~9)icVgW>%a^dxsOqb zIq`BYHFuaoZ5^;%3%mdCdcT40+)qkMg%?sMt~gSTZv|=_u{dz)MsVvYlRY%;i~v@-=p!D0Umy9boHcs}PR_yi&eV_8K%91a z`*`-g{O-%3|8Xe2zZ9Q^veZ+Z!DoN_-~%$_A+wxi@S7u$<~Kf1{tdkL>kxhwe-}$1 UX}bW5O8@`>07*qoM6N<$f>}QUY5)KL literal 0 HcmV?d00001 diff --git a/parts/back/wings_feather.png b/parts/back/wings_feather.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e8b2ad393d3ef48fca1fa47d386647073db5a4 GIT binary patch literal 482 zcmV<80UiE{P)! z(UQX;2t~tA|9kPv*)b+n5fF{G4|_9pnuu~l#k6pIPh`}=aK&~&GAu!V@(Exn$~k39 z(13aOvjZa}G!5{u>p8`XJ1EN1kOtwOM^#Y?Osaz#_rB9-hxF*-NJOJTeG6S!O`t-b zeQ_}KfHJtE@!RW9(?Dy~V&#&-x&eEBv4$;|1e{ZC`6y#aRflxdRzLc2+!lKyDyt>mLUyt@ofeH>p-!3=u26&W9BmE| zJew|pNt%IWk`5FT_}b+1rt@Zs5e0p4BN;{CbSo9=+o=qa%-|)ilwIH`NB4ww92&9U zz~kV(^&4zE5f*)USkl&rW9(3VBU$#^8^Pn|%%7AJoEH5+A@_X^cPaZMr=Q%5x(->;vvaH~3#A zcL1j&ogC8~^uu68dzJ)`ZYZ8V+&i;bf=BtSm}U)F`0$@MrsTYQtUUx)k5>NOaQtKa Y0TYJp@X+7l^#A|>07*qoM6N<$f?%WEKmY&$ literal 0 HcmV?d00001 diff --git a/parts/back/wings_feather_small.png b/parts/back/wings_feather_small.png new file mode 100644 index 0000000000000000000000000000000000000000..1662c32738af77a0719bdf7e6e9e56b321d1fe6d GIT binary patch literal 273 zcmV+s0q*{ZP)8f=10+>&~&m~c_V zF>zdSI1>5Rz z)U&O+9dX%+X+g23XNP4ju1!>O?!gVv($n700000NkvXXu0mjfg{gH2 literal 0 HcmV?d00001 diff --git a/parts/body/base_axolotl.png b/parts/body/base_axolotl.png new file mode 100644 index 0000000000000000000000000000000000000000..2b00b4ea090e60666d6ec9d1d4c7263387d9243d GIT binary patch literal 672 zcmV;R0$=@!P)@laQsm>GyzeBN5S^JCYgO8MdjRJX0irHKq4h zt?_{9-vv@%dxPYSj9nJLkFu)jidd6|oz0SAJ3Ago%?%>m3@2ouS1fIno~41Hs#)(| zj0s5|eyE683qobv7cji7mQwpSEpQT4Eiw|j&{5U0?(C4r{Z^1SiFk#ol;_^`CLv*{ zo~wuB1w>#nkBgG8tbT$Q!)9m)5u!$6@Z>Q2ejXh(1Eok#UJ}&S%L}K8R1XvKtU+6N zJOonTCor5HP(o>YFZQmmb@2->8XW5F*Ji(%E3{_s-zX)*Q6M{{^=iA15UF6N;?)da z>6rqs_@jcWcoEO76<>@PTbyxn)HBt}hr!7%Nl-IgQZYsV{TnhGr00;G8H@Gs;VZzb z#1g_-ZL_oxfY2IiNpKQzDLAE>0Q7te%%}_88q=s?H{}N;$vGSwn|V0^0000H*mytcsvE&paKm|}3;cGJ2QoD_$ zkvVsLaH@{tCyDI=W9jQT`F4xBH&g=WvdgTO*O<1LP{o$h)2g7xS1pS$KRENR-2p1T zauxSvU}r&5A+|7ZO^H@#yEH}tY@ACCu9-1i+Fdr;*#>hzpJ1jT>}8*WY|R&xjmPxjb{cO z|Jpb6{+LA_Em}&yUPbSgh|j=<(%$!Da^Vot_}$w}Y8t5KNw8oMRBaqfz&w61(zVs(O#%3+QHgSk3H2kyV2R(9i1>(oL1;BLOLT+Srf@koG(1*>d6sKwM=;t|2~JXi$Ff$B@Y3Np>TC`g_H zo66=Df>_Vl#vGojT(*A!sYnxSV-;dAHTO|zSmv4Lm{Q;9@(=1&fjSVk+wIhsfN0}c z$Giv5ZCk*Qui2Rb z;5lX+AS9RrP>jAqxyC5CE0T1uPX>Sq0E@1oZj@N>ly!a?|d%*wZ;c-p(WT-YWjtq k^8=wS`B-mftLb}r0Z+IYM`avduK)l507*qoM6N<$f@ZrL+5i9m literal 0 HcmV?d00001 diff --git a/parts/body/base_blob.png b/parts/body/base_blob.png new file mode 100644 index 0000000000000000000000000000000000000000..44d87d820417c975508caf2da4a9b78e0a73c29e GIT binary patch literal 496 zcmV{ zKz;;#Cm`>^9F#mt(^vy^6!{tRM;Lf1{PzJ;6wV;rOS|6@E)rQ_4@T`Fz6uZlgOI+p zNUZoD=VaAhS|nGf_D)$UN0f~6gQ>^GW-t=^9Z~I|*aMJ@U^!*%QNe&}I3?IO&G1P= z<4as$g2bM1Ds8@gfZ-!7d8`3Ez+nW*0yGNeBs=MbQ;?{IdpsV2kufG1T(XhuY9ZBq za9vk-oV9_CD$d$TzBsC*Vr)6~T#H+#~eXDmAEQNh&iV`y=#&9haTj~QVx2Ya>| zD$itG$(Vv$wiU5mc=Doyu@YN~^*4um7LKOo7Z?UWmxr4NOy;146E{B*2yVU4tj4)Z z!FlC;ekGn6Vh+wo?hgHtgEU45H!qO%ncK1?L)^6*N#9nS`HrnzBo%rB!LE2I+a@oVUHf&dMtoAch!YNUEJDMKekLdDz&&0e1$Ga}JJs_U}hP7C(=s zHe#|s%-G!^W?c2Lw0_t(=QbC~BKqY@E4MBHYJyyOHyCIVO=LY9ghyD?iKTxY|3Sm% z0xcywVSa#~tR#(xjP~5Ca<;(2=|!V7Ohq*KOC|*8BOuOVBf(Y^vPV;``hvBQq(?#0BQPoTX`<<=ME#x|A*n)$?8r_sMHGCsU)Z0J^qCDG)e(!` zV&cX*S^q-lk-HOWF%@V;wI&QLnFev|tGbnXuD{eJ%l zF(MHEa=9eidW-oUVQEjULo{FEK4M+ha9LzlL5^Q&JoK2V{x@J55q>_Ox5x~>J^x=s z_fJQZBn$ht@bE1NOH_J_tT(;ar`Jw_HdpkM#oZ+eGcnWB3MHp zoOKQv8QAuBKheV}a-vtG>b@Ph(%d3GJE`)(8zphN{I4hG3LpsMYEU($EMKP z#9_2io$!N6(u0z`K14$CzJ3<;uZ$(grUi}UK_XF(`^}R2cyLTh)YAWrn_KE>R3OZf zFZ(;lvdv7k?=n4-Duk~mC%^UjBGLY#gegvwM5B(9g!7j<$>zNSrTxSrq`p=?%lG;$ zphJvn&7z>{c^x$fY%5Et7s;m%Aa#Ol7x`T~OxZ@-^zAIdOlNF2XU!6IJT?x1BVWt0dEx4vjWd z1ZS|^@}1LAPP@B?#}$%B=g?ZgCx$zZTu{QGR@fm&1ZRFl>mQw+{sA%+Lj(}6v55cx N002ovPDHLkV1n4CeB=NC literal 0 HcmV?d00001 diff --git a/parts/body/base_bunny.png b/parts/body/base_bunny.png new file mode 100644 index 0000000000000000000000000000000000000000..20af3e6ad6858d39cb34948d686a4b5db13644d2 GIT binary patch literal 685 zcmV;e0#f~nP)gci1r>g9Z?c`#NbGgZcsrFNr}0B0$v;xAUm<5yvWD=K$PhJZq;%D8?IGSA!O= zj7PW+6i4mF%F_0aT4bkFza{U+_ zjAUGE2nP~kBR5RYY|nf$lAW18nhxsgp*v(U2K7`oF^62-Y}KG*>h^M8E<)f1L6QLY!A!(*?&%EV%fT5bwq*P! zscrfWB48bV5sg_wh)OE5u}G=xD5CuXV6Kd?Z5aQ95L*+t$Oq06;okBX%Y~D^PF^xR zaNNA{#byx5r``k^auR9idGcyoMdpjHB_Ts!ZFRQ_TpAe*?YzPnLOU{88P+|hG-gGm z5v>*}^dvA6!TEeHl!PY1J+pfuUPF_N8-vxvQ}$~SgIs2QCaqg)#43At7_ojhb%D=h&LcXO$t zazwpWDL3X%!^qHM*{pphiuo~5k`dCdEWO6PR-f}WR}(QxNDp%X{OIK6_O1aInLHZ} zqEnw0!72c*gx5C+Ml@Q5l^WtxwaX=&i8v5J(AN`0z=D@_Ukfi;PuePl1#o`+au_*|NP) z516Zw4$>oCe~Gwt$TlWLxad=xS&ij{+y}h~dB|+;uT{_zoe6NrHhZ--bmlb8>DdyY z_{+huSI|{kODvg1pFvOuIyl}+@*Y=u!*vk~)ohEDl_?G~Gq13?Go@RMIci-enImZZ itdbD!eqbH>qxK7)RWrIPnCut;00006wE*vB~L_mB1BGRg@B5%b=Cdv%}w(x`w z)=~Qx2_9@EkSQ4vpwmzJ7EcQ1M-wNIwn)Lrz;7tXhAf7w)8!aM1c(u%8G2w}bc0W- zUERcTFbvO0m1ZL3jK?e?vL8=95k3V7XW^u%u}|dFX3!%|7Z&Y&3$P^*7D&bOO#{+#=Pu7E+@?i5AomIXMLSkgYf^@5ynZf~$KCPGcb1MsYpfYDR z9z=dLs}k6lV)JxZjDM9U+dODUfGYM-Z_)s)Tq!reRqtsp4r8$A4N4V7*4gYr$Obdu ze$yRqy-;gnk=v3fdQmBo5s;Pl7Pe1Ohnh(|JkS6dTmBK#<1F{(BcQyVjyCP3a*jLKthDtF3?Jf(UR-tU zT<5Q8I4=&D0Jq?do0>Xpd8ihP^a%BAq^}L5oo5(>avzpLY6)+aPHu*c&^Hsi<2*P{ zZkS(LC0KE88XvaH<|_lXA)#0C%uU#k2OJbVeYH2s002ovPDHLkV1ibaE;axF literal 0 HcmV?d00001 diff --git a/parts/body/base_dino.png b/parts/body/base_dino.png new file mode 100644 index 0000000000000000000000000000000000000000..00dac64f1190659f46d9eba564d496e6c33a0c29 GIT binary patch literal 660 zcmV;F0&D$=P)~i^FA=u_q!VgZD-$j3m;rX(T}#^*{HB%U`$c&)gpEJ7Z@W_ zG9|$S1I#E@0J)B;*N7B&V9w|BQ{nM=6wahhO;18`pw^IOj^=q@jwrFuqoL_&dW*;? zvgY)sc~cX^Zg1Hya=Bc>2pvn_|1CP$XYW*3x+lT zRT3E4Fnox<0)@8#3lQ#{sr@q5Qz*jF-i@LuJZe*01z9@{2f~gyD5cxh=TX@UM1^w! z@=Q?ewIx6*L3F%2u>GA!I?oI=9Hc5c9j+{0fE@*l7M;yH`Em$#s9N~FI_}K{8BQ3g uX7lw?MO+Yl|HNoqW0N%z)NM-}ncgq^5~agQxTH$}0000ah@FCal0r5C~JU~iq8$1|P< z39?o2p5=_$@qq9G?4t4Hx~{1>7zrp!Y-4UnpsH2B@4GK$=Pj)Jy{@Z4L|-BxB{TDN zxiT-mDGOQKKt7*OUo`UMbm@Vsx7P>h%}1=#eK;E^M(v#Zx z85fpD_K|K4IP?BN|A_1$u{R65ohI=4PetiQj4>Jm2&(v>?Vbp5=Ku0>if}DmSE;AU@4)^uR*gI zNOo24Oxom*S`cmkMZJSO|8mC|FLNp&6&`!&NpWjzr!;oqN~aQd*`o-q1k8m38$kw5 zm;+VLy*DueX;7S~7E?7xiwlR@M2kRADJtY%O)*E=^yK%qcsSsCkmN*Gcw9AXiI}{? zcggQsN?3L1wEblVt|u>ZHY#_gKoC*?zToYy*2|Qqt+rS1YxR zXQK)(z48M}I;88VLsi{Yl;d@_zr-Euo1tE|S?k?$Lw&ljyEjqT6y*!yJ%L$Pcvk_IR{{aEVDsqIdTpD);?`G0bPS69a0K?&J0G55fsyGBbHNk|8zXS*jvY3Z>1Db2q{ez`0$V ze_*yT!fB;kNStsYWDzz<$9`(_X2OeVvkin3(n^(w2f_w^0~$nGT&&GSLiu_x*PUP{ zjQb3)h{?4}+@B$A0{G;aRDe#nF2wgX)Q}(R-h*oJ-qqw5!ou@uT=2@65vfe)(scTe za#T$3SoD1WP7+JGz2#8u@0QD}>JUu5{tRHhsX?+>-#sVoohLetePuJ;?+}iOKvI#? zoTN()4>`Qty-PZqo9io8oV=%cNsUYL6i_^fA?d0j17VFr(`judN|G*`h<3o_9h5G{ ztC~shw1g{yAoY6haL3Il(~E-HTVpOUT^w{8VVf4%po`vfWo z?71jF*J&Mwq*qjd_6b_>gl!7YakLDpw!|dH9GCsHFC4>Id70hi@# zfbhZi>!N!kuUk_uIuc63RwV6fQYsP3_EMIvwT~fz6Ymw%Iorv5_SgZx?uL4Kz_TMwZ9;jfUcP?*MUhkeg|SDJY?7PI+RXl zB7%Spf8G!;5gtezP@}wFv|&bgi=<7$hLpq5UkIGwTLnl#hZ$kVXE)<$+=vP{UopfL z!hc+3goh1^22FW`Xt8qqRHvDgS0wz<_6X&6ONx7GCf;F)jcg8eO6@`-EJ3bpxY&X& z9ATEzRrN5vF(VA}0P)?(n00087NklehkXHp0TM)Q-#&|s_;`dsP;g^}rVj442($b!A{ek-6&&IZ zVyfkG#1A6^zhoYl>LFX##c`3L;b$yh3;rAlWMKY-ki?R4E=B-VWWH(ph!5mO;Se^5 z8$TR7Ur;4rvFRq4`5EhSUDZ|37e1vA5*(c(xX7bG%HuzQ40@t+PwgBAjp$ZLke)JP zzT+2Y!wj@(;VI}qJ!;|%u?*^@Q1ZfA6ds?7?5k=m+$0|~qtQsFfVGGm)As7~W0BQO z!&}I!hJiP~adBVRj7G7S-YqldJAs|H(H4_JVP>lLfT)LoE=1KNr`~Dt`Vt_jVIZ$P z@ILxJptcRA%kZogb#@A^NDv*g3#47yilAVSTho6gMk}FYbidy#gLPetSQ6Q`7aP~Z zvqy1DFq}Ru!B&iRcs`%;I@>P6BDXrG5J7amd1y_Bn-^;0;I81C`a)Zi-#9+dJ0CD! z64OE-k4L;Lk!t$|loOlC^%_e4zY5<8*iXY0ugfd8CyGU=jtS>%B&KR`BjftrOZ79cMbj(+f#dBaYo(e(s*Aqp~d;}6gm*5ffoXYj6cZ32mfOp zRlG%5_Co=3r$P0w_)-$yad4KaeljMZGBpOS;ri+6?57<(wx7QLRb&Fvo831BII%tL zdI2|^(NvB;a99t{rO@#U4ouoG!Z1tdm_CQ~u_a5Ziabh a4Dk;nT`U>itd=AI0000_y_Y!nz-@@@wuMLP!R7YmjFG{FYp z23!F$2w6tVT~`4!OKa#Oq)z}0QUg?AdE-cH8NvsKJ}W^U0Dj>De=)sPQC@|vyy@O* zhDXz6JM*F@#C=D!P9q%f=&kjDu0S^G@x^IX6LgBO!14t)3)IaDMN$EKWY~`?RGYpu z;`?K^(S{mJG!ku10o_499*@f4{eD-l0aDszJ1Ani-IT?&8~WGl6|W=p*uaIM*Y?q~ z2w7y@5tafKr4@0M7{y4%-dfexPjY&&QF}Z`0Yb){T>aF z+JYVTsPEqnh;e=c!JQuj6|Z_F%Q`=W7KQOBu*){2!ef+rNNJMbs2~O0ku0DYRv!8? z927}FRVk9m;(c@T=BVt6XpvwHpmpw;0|oGigw?|&$){;;^&F=#Cp*H)ObNv)n*hwC37<_%sbp)|Sx0000< KMNUMnLSTZkB{t9i literal 0 HcmV?d00001 diff --git a/parts/body/base_grub.png b/parts/body/base_grub.png new file mode 100644 index 0000000000000000000000000000000000000000..af0c8fab78ced319e18f692c16b55e8eaa68a0ac GIT binary patch literal 605 zcmV-j0;2tiP)?MxXcToKVhm;*QD2ww)ykep=u74q1Svmr_3 zw`)U(lg2JthnRur^9|CcO=R`p>Sq&JJucxXvOjb?nluv=)XE!Q%TRvAK$6a!-m*?W{_e4gu@P zIg|1dR!3MZuhk({zJj0lKgn_hI?F*pc)46ELuFuUYkicjYj>BCT;53(@`-e?j{vO8 z8r<*q__Q$6Yoe6NF7@)sNyIk$tEbbnitz5h`fpx^KVUc`)@h;o{?iRkLKx=j624xq zzVkHGrrrU2EOmoZ@RF33h6MA*7fBfob&KGmI?jHzB%a<>3V6>C8TFe>b%e_qF=t#WIwqbZO{%c zp}iDvY{e^LNmX#h>%3z(fj7Nsc~?Qdh{zA6ep)8mxsPmjFngE4T@rQ?)<{M3Np|S# rh&Nm|WS}8Cj>Fqvwq<>|+%tFpyJrtp9(wC#00000NkvXXu0mjf5&9qn literal 0 HcmV?d00001 diff --git a/parts/body/base_guppy.png b/parts/body/base_guppy.png new file mode 100644 index 0000000000000000000000000000000000000000..726d5ae51bb18cacef4de625585cd0dc9ba84eeb GIT binary patch literal 627 zcmV-(0*w8MP)iK&>nZHqaC-o!8Um!gaY!TWMU;dX`(cbBb?%=6cJ`x_>>Q?$23do# zYmd$*C?kxxmC)L@#=u~+rOQCNLC!Vgz-G6I%aNqN@sS?9tRhcYwI6V}r!}y?zmD98 zgJI9Z+eue+K%0X(RE{NG&_h{e1N3GO5Qa(fphix%;W3`C6KAAvw_7GO&vQmflhNcB zi04k?krwo3l?Bh|b19=TJHwBV(D@Pva4~N(BfyX%W!PnvPI^@l$X|PH-tYJQG93<} zR}6%TH5Z1{P5cZ{BA*%xXvetXt*$sW$Z?Ru7aa=~2nnMa;|v?Y-gLwlcqAS8XjhMF*7Z*TTZVL6MBX`jE_|AUQnV+**Rk@6R!Q)iM>-sG3iwXyca|PA45niG~zBZ7W(#;0crn>FW z+X*$VhbyNQ3MjF}gc1jK8W=w5OCwAn2BJ%y&$$`_>$u`~3h-X*5SVndxg^qda!7Z^%z!jm@_6eY|mE&ADogz3o;p_>h{ z)XpbFSu0Jf9-o+^ttEKN|0<)0TwJt>wUk(xzJM&JLmz?5j93Ryl1H9hP68QA-j_s- zADev}#Dmeexa589i^kOl)`gM3!mA!vY{{V>5Ou{F!WA6X} N002ovPDHLkV1lHIB2fSU literal 0 HcmV?d00001 diff --git a/parts/body/base_hawk.png b/parts/body/base_hawk.png new file mode 100644 index 0000000000000000000000000000000000000000..4fba0a79a629c3670cc13d5fa8bd2e8a8cdff097 GIT binary patch literal 638 zcmV-^0)hRBP)3r@Xr}eTWrI_AfvM zA;i9<_RP)-Eb<4bfzsfW>j2mVW=Os89YhX>;fUBqZvV+aH;f4kNcCLUOpKzl zBfN(^6C5xP#FXUBkpi_jR~DoSC5fO1+lwW71Z~AKA_pTD~({T%+SSlH;Io1e5c-H<2evtx9~Go=Oo- zf^4lnii1*h0aKg%l@oGA)P_VK{wZB$Qdto(v7bQUOWE6M^;m1UwHk)E3!iFZ099r6 z@4$w(k18pz5bwn9?I}kY#9NoyoS%^@cEt&wSj08FK1q2)TmW%j`dZjThhB5d-`9pc z<+ Y|Bpu%*($RO4*&oF07*qoM6N<$f-M9icmMzZ literal 0 HcmV?d00001 diff --git a/parts/body/base_imp.png b/parts/body/base_imp.png new file mode 100644 index 0000000000000000000000000000000000000000..20f8328d07b89a89f1d9ecdae13141c669a5890b GIT binary patch literal 779 zcmV+m1N8ifP)5N7$`#3nK;z8gzYAnWWMnPeg=3=p&UoguO@!@p$5UYpAgY z9a24Zu#KEXd9O)IgQ>FXz?z|eREH^Y1Crn+r!3R0QMqNO$i=td6)>dOE<4-#>XCzV zTpDGVtdqp0$;$WnE(l6;)!w_EK+#Rv6V2>9u1uEeaIj?Jx4KM-nF#$|#LGLj!+MY4 zL^X0p%s}06q(`cbc#5neJ=pUeNqLC8zgK?=}DZx=ZOG%n<|KHo`4mq&P5D>=Cb z#&8TqaVI^;LvRP;gzSi5PE(^L{*+j|1ItCnrnr!BjDN-=RU?aO1{cVMnQCU1P(loG!^ZOV z7&(#GIk+Ml)2wk_=Pc~A`6JQ zezoLanOyNmGNg_x?gw)>F>mpp z8=gKlpOXKJU^XrYg+YyHf4N<^I}P)oPc1L~rFgpD_y?EB8~A_tsd@ka002ov JPDHLkV1lpSaa{la literal 0 HcmV?d00001 diff --git a/parts/body/base_noodle.png b/parts/body/base_noodle.png new file mode 100644 index 0000000000000000000000000000000000000000..07cf6dac8036664de89e0ff320822c5058698ea6 GIT binary patch literal 736 zcmV<60w4W}P)p%GW9juPP`h5#Kf^YThNxX{Lu%KRiPOtjUi zZvqtYY$ZK2>%g@y`!1-m9LCzMy|0Arfc~I0f6!^Y4O&Ry-RrPTAufZyeI2&)eym$N zcDumqU>8HTJiHEep&nXTVE4XLUf7`AaWXs?d6>XFpU>i?fKoZPkz~ypV9&i5^nSm$ zv!-S_);)}aajf$2H-&@g?V<5*(ChVTR@5tBr5jIWE&`|PYB_Sw=oAt*f~KpaRxGz{ zoVhMl42LlJd_MUEfFiB&a>>Rmh+l4;XE*1MhU}zU9L-sPOJJ+O;bCp?iIpIp>|0|1 z>kR$2*u4)K(Zf;D0#6geX4ConNx=#|nsCr|wD6hBq>2EGEZ(YtXZs##YGb<+KL>E_ zl5_j|xn&>`Ax)jZi`E+BoI3Skf5CIPe44A#GB&7Ewia%W;}pTY;qsTrDK(*PcUq`HTy$*U=w zQrIlP!=~!pv^4-qKfiPC38WEShN~#g50B_$zW&;YJL=vRJz^JX#Cg>w4U%o5Fl*)F zHMA1i{)g7wO1BnSqJ&Ltq0zIw@ri>eyNgH=*}NaDzDR^+KVO+AWl7YEDZ{ zH}R{D;}Efv*l3w_2d004XBB-#sNfnM!exp!*Mt*1SO2rq9GhXy0{tD&kp2U-W_=Ro Sw^beh0000+O61xS|@M+wAZO$jd3$i{H zogM;qjtH;Egi{Ki!~9;6AjEf@U&Bd>9e@F1=_I{+d^=VX0S{mn=VFkvc)Q(-vFGzy z5Sa+nI!RAikoyMeYhQJW@_0OA+pCi!>rii=Lg<&L&4XFH(0~_x5*rKKR0Lxo$qX2@)lD522c``18gbOHAJy9E<08j>G z>2rr$9YUrjP$!pw(q!wc8=sIJb~ROLkp#Nx=O8!&$u^^J2}q#k!X}k2*&~W@2^>AZ zjQ<7-SI5B&5e>6Qc5%?Gs6;#!M@~ZfT}0> zln@r;#*esXkF)Jqg~YYX#9r6e4hqgtw#c$Odq|80NW&PD*d>8n-dZeS&DAYrMq%6$ z&YN)Ji7dWtBtos%4o1)s6Fdp2R$Af5SnvI%jxt8`0_du18+~AicWytHyYE6H9(Kv% w`ZUXNF{keP1-fP_BL2D}`7AvAj}uG&0ecDs5IzuRl>h($07*qoM6N<$f-JoamH+?% literal 0 HcmV?d00001 diff --git a/parts/body/base_owl.png b/parts/body/base_owl.png new file mode 100644 index 0000000000000000000000000000000000000000..e482e2778f27398657b7589c552922b2efeb6071 GIT binary patch literal 693 zcmV;m0!safP)0c@lS>TkHh z#h+3<-$N6=)>5jwy}pzw+*Od*Jlq+dMBGq}i9 z%p`3>SN+9-WkO1C8Gs7Uvq)+(i!r%n;6GN!Mka?p+AC!Z!m|2Z>o@FJq)bBTU|?(T z{5+Cn6dBbwS|v%QG|YFz(nOYiX4~4&GE&x(-|48PPXMr1hi40jA;#FTUe~o{UcEAw zys5*hmOVr97>51Gc#N^U%kYrVIue#&b>&AeeBtqUl;*`q{XIh^h4)O}sJ|v{T zm&hqG#$6K?V`mp@lTOW^RyyRdaRkWksZS>SXt{1FS9RrB?V!pTi(*ys z9L5%*HaRz;3C_qj*>Rl?9QK#X4J2&N`CR<1aM##Pc;FUY9`dYG%5G?ptC#$=k>t9l zGO~4bL+A+2LNN`=rQH6 zxdJEQQy)fZAB3)40CO?C{~b`Nh0005xNkl$)evJ|Y;Xv|^Te4x>~%JrfZ({r z7xbWvyPI_B00JgIbw&O@(SwG_hmKn!iBAOrfQbn1CA+O34RdR$T+`&AUV-|ddjeO2 z3Qo23)~J_t6FY2C61jp8tZ-lzDyS3AIyRSDRH5D=%&-jq4OLomdbzNsk?iT73^qe4 zS{RyfLX4WGkIaVLW??mAad|+cT%f$KJj}NNA8R{NMvF5O<|4`ssq~{`58Hpl(xUe_ zuGeeQ=bSr4Qrhfx1~3T~gHWqEWUPl3H{|V!3Dah3|OU;t9f(_Pzyg}UN+tS?ZnxS6yF~Upow^VmbW;s;^ z$tAvS0swfN;C1~1!=>_KwgAOx%z^E2XEZh1OQ^#|BoiAo0#tmUd5!tfkvb3<*bO0t zU5oz!6tsS9t5GAF19=!LsGLgcfy$z=qhci=<&7V_*{1~E>A_F_G*}(4Ij_BmGRDl9 fzUE4P)Q9>5r6<#|o~1o(00000NkvXXu0mjfSZMm8 literal 0 HcmV?d00001 diff --git a/parts/body/base_penguin.png b/parts/body/base_penguin.png new file mode 100644 index 0000000000000000000000000000000000000000..7a0e6bbd7a538280dc1c8db1ef07e21d29d6a927 GIT binary patch literal 616 zcmV-u0+;=XP)#*<1Llk|rUf`A3@6LwfGu+k#s3Ke z3d92J7TjUO)r~)B_xL>qY(fy(_dCI&id=vw$CcP%Az#-n7;1oc1I#qb+p(nP2+TJP zX2dfP zr8LDl`LixB$(&^?sbK?Xy>ff8wi!h8eST4`BU+N_B5`e1Y;Va@4Av;+NDOkCVvbV@ zNGiq>KC&%RbIWf56*3@I&T<`U<9{9w+2Lec%A^dXF2D>nb^^ z0TM_T;TD{fT*;3}_FqM4i=Or-x0vlGFnoz>qXSXwvgtT_#a|jiB#gDl*L;vl9eJ>F zDriRtja-li>~i|AjG^CU$j3qQPH&y|t*yaD`Osj2olumVfX_(R#YfVZyP0Vj%MH?lU&t>Zoq_}kmVRv-=Z}BdcFMr z^Z9Jndrjy-gqzq1hM*c}r`1d27e@+Rk7}ZNsepVAJh>ywpjX9g)^pL*wxqE*t7k5> zH4`gV>#8W6G)nC}ysPW|jzu=^`AAbU&9bzKbs^SWgJX|a(rA|Hf#P=swMO435O zyVubSIkjDquES2Iw}XabaU&8>;?7&uFx)~)VS6_PhK*xz`-Cv#;= zChxiHU^~?v9`y#21uT!tg=s%Nflf#hbd0in;>IpDyPoj@FV6p}m?j*}b)Rep^ry{L#0Bm9BPB+5lY%O0a2KLPFNP3398#=sah^T0EEE*|XnJ Z{sRRGai%?737`M~002ovPDHLkV1f)ef^Pr- literal 0 HcmV?d00001 diff --git a/parts/body/base_pup.png b/parts/body/base_pup.png new file mode 100644 index 0000000000000000000000000000000000000000..36b2af1be9be910dc4378cf38f1ff57db3d63f55 GIT binary patch literal 688 zcmV;h0#E&kP)N*}~7LH4Ms zw`r1*j3D%lUp9GxOq{MbdYz;~u;3FH#sv3wJ>UYjJ>RS(XMpSiWd}ct56`9`h?4Y0 z<%X&X8#Y{_vz=+2jc_7e7lT_rrRxZNotLlFD>kUuX{_`X+gntf!Fs4HRx|?1JK>p!$0B>@t)0XQP~CC68OGjcW_Z zMjEh5Mii1$!Hrn$snLMR#}Qa_B_6MC(Az)LZyK9EKsvjU^p5?39ocjCF0&@&)`7r_N8Hy)4jjja1v~z@L1L>38-=L^AX_Nc$ zaurbyX_WGllOG+aTxVgrD49_R_pa>3Pn_e5PHckgvkZ4$%q!~HVClPx>$-!zX26Rr z_zT0Mi-6lc>iRlp6;yf3smmmb1{_aM#XKkLxn$h_3`U|@W W>55259765@0000fd1g zPE3#bxmh5<^n{tmzgx%dAVv*Q88v+_9e4+NBS23Y5Ikl>!Lb#iK-urUnQ)9RGeRA6 zd0HPQ(gyKgUJfz7`tl>{~3eGZnw zd6`aBVAt!l+~4kZ+P1A5gv3M~(T>x^1?gdltfkz2LE;(ItXOZ4$0J_!A*PV`T!`<_ z(*2qyGhgX5kazKRP1nd~bw8-@W*1U&$>dj}C-3vC_Xat!{k9Ya!+1vxde)NAatO{f z2H^@3<6u&xC?>?Igv|@m_=V?}6D!Y3iUE+Ya}1TOOJ!Xj9%vqrpV76(tT{@6JF-Nk?fH;}5 zvCZWaXx>s%`3#NLFnBgMxOzC+GhZct7!Wo$x*e$3_t@DiASfQNb?^Gf;*Vm!8+R|eSCvKJlYnrCnY1S<_z^Err5 zxq;aFuQ?3anIjnXg{HhUX3JjzE(y1MB-QJKlx+ug$mjT69aD1fXa$pSt%s(AICz;R z;n-J~EqijjYnz3$_5KH+1k2ate5rGGtfM%(bq=owPFssFK{5b*0XGe!23LGg8fFGvbbv|*+f_Lm!bjhT z6@)q;=5e_?rWih|C6s2<4JeV4Lw1Ea89%e>PkRT5WfumCZE|)NljW(6r=>X{nqe4% z4(qxGgGn}rU1m_L5@s^47_wzq+@+z$rWW_4y@221Dp(@4t2BANBXr0ha zn6D(XRL+w#^9f3Y5wvvn*(mfQMVVZNC4`VRQ#5D@Dhnm%@6F7g`-C!~BxJsGz!>WY zNh8Gm7)URSh_1*{)_}5^WakV36hpNUOm=L+bATsvG-OB(LN&H{Eid=^HGT!vS;{6( z#@oc<3)iwtXvp?hSNGi$^F&-oc6X)HXYcTF9|B-L=DuSOT>UPGC?ZocUxI<+^}}M{$rO)40)hf3DK!cATyB?E-ji>;$o{K6 zQhJi0sB@N3GqH&i63AS&$%lPOD8h1ZJ5ePYu285;Exzd9NyzMYztxdR+&Oj;L|dh( bI=cP?7)2%TQ4x3v00000NkvXXu0mjfG7laA literal 0 HcmV?d00001 diff --git a/parts/body/base_wyrm.png b/parts/body/base_wyrm.png new file mode 100644 index 0000000000000000000000000000000000000000..822a39bb62186a23476bd5a759032baaf0fd5ff8 GIT binary patch literal 818 zcmV-21I_%2P)X@4~Vs#cWd}<6h3`z~(Z0O-wzgFGFpi+!Fv%rtlpg;7^9~kvQo5Sx~ z{F+6-VBuy^@Gbd;G$I%lUnUC%j3X<5FQ{~C>QEhW#JY7@(UG>4hPJHF@mT@b#|RpL zExUmN-L`GNEI9c+>TTP0K40PUBZ)B9Kf#uDzxy8KN10fy(%?Z5&%fb)q7pn5Zy^o@<8+Tk{(0h)P^ zf6wAM@fQW{9`PCzvgg6SY(j~7g5zgrzrybr6b|5$-xQ*YW%0{1nu>sC$d~U~%VK^I z=NZ-ECP|xGAhdQn)V&qId=aZTTJv(yf$fH)dy9AOY1MF#yNTz}35*W8A`)3Hf!qh2 zqFQ14qdcpSozqAjc)^Ub=!fOm?GYlAMXR- zIcKw#16UHLJ$*$Z0Pyn3pVh zhq`D`bB`t%s|kcxR%e1;f_Y~qWUXA3Wqhqyc7g^0ZduG46xKmnWtZy1;db4|yx3lm wY38`R=+doehnoy5|C!=NBs?q<pGftHS*ZJ^V{gbes{k|-+_yZ84um!arrps*zMKZ+7iBeR7w-reBsoO2b)S7 zPk6IV;6CHyrTnACLp*cd={e5VR@6;xW@zkv{9B8E;e76%8E+Nm{+CyNF`KFMSn+{# g@2|;za4Kiz-I2Fv^3CQ3pfeaeUHx3vIVCg!04Nqrxc~qF literal 0 HcmV?d00001 diff --git a/parts/head_top/cloud_tuft.png b/parts/head_top/cloud_tuft.png new file mode 100644 index 0000000000000000000000000000000000000000..f484c83f7b007b410acc58478f4d478d05c23686 GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^VnEEz!3HF&|J2q2sRmCM$B>FSQzu9A9Z=wKS^Dze za`odL*+vTlPqlMf@fS=CGv+Nm$r8lKG>_Y3RX?{^UGIU6c!t~3mEz8fb8jvUIOTl5 zuFp0iTjAU{krfd|yr4`@~YdBwCyl|}qVCWI<;{FG$1bLAxl&rkinFPLg zSemBn0s>J?H$oVRj3gauB@<@N?ulKblz1i*zQm+k`ph@Y(0;VD;ry=Z*M3H^lJ+H< x(gFhZgp{eKcitmM@=w&zip)FSb0;?PH7IbntdxKM z>r?jH01=_ywb$6}?#(ZC(^+zp{V|d4`#Qx54H^aL-thsNjJMH2=M*!W&;OXk;vd$@? F2>>umPJaLZ literal 0 HcmV?d00001 diff --git a/parts/head_top/crystal_spikes.png b/parts/head_top/crystal_spikes.png new file mode 100644 index 0000000000000000000000000000000000000000..1c457d4c89a4028c079cc999ac609ae794b4da97 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3HFiZux8nQl*|Qjv*Cu_D;IU*Py`RdQktk z{{CJsah{_wuk-9W)7d9z@V_(mJ*mKyDeEAmM&V5_jUfn&7BTeOQ-L3VCMVZveJ^_ Y^)l_g@@xN8fDT~rboFyt=akR{02B>8m;e9( literal 0 HcmV?d00001 diff --git a/parts/head_top/ears_bolt.png b/parts/head_top/ears_bolt.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ede2d558ab3972138edb0dc63444b62560ee56 GIT binary patch literal 238 zcmVG(oCiURB7HgpspcaMh0i!yv`jQz`W9Q~R zh-{?9(APM-o)B|F6cBkThK$IOWYs1~(0q3Ibr@#tsz)b6BoPE`1YJRMlp7oCjBj08 z>}wyr)P7&kS&QR9@-?q>;B@Hj1O}nc>++12wwuvSQL0)7Ggp2dUAy^(7@{Jf@%|SO obd}n&cT~NOuN1;sC-)cm0@L3OG?>iZk^lez07*qoM6N<$f;viK!~g&Q literal 0 HcmV?d00001 diff --git a/parts/head_top/ears_fin.png b/parts/head_top/ears_fin.png new file mode 100644 index 0000000000000000000000000000000000000000..e3684a7b2dee57026dd26faf8c3518f6bac01196 GIT binary patch literal 268 zcmV+n0rUQeP);)kx ze%#_F6Bm=G(*fA2gSGYI#KWai-Wze2-SUtw>~})5t`0O@u&`;pi1$sj#XD}PMO7Ef z1nvC4zn}1+Ozd@G%$XKP=eRa7H_>uDM#XuRlbWiNdVZ(e+tj_UM6ErIZ(KeQg1RnV z5rJ^ztT4QCBMz@c&A`%D_y^M7`<^A&a1e0K7zRL)=b7Ft+_%v(V}+OZtDfG9F+m{n SJB{W50000+0&#^9&Jn1B%z$Jls4$X2j2%GdWu?0aMKB)^`85J`5Eu~Q z`@<;i!h@|Be-@NdQCIw}$#huC^|Le5_oQhmVu^UhSrQl*snhXi5K?JQOM1_|AU+i4 zWr}-J5!2xy1mlWur50 z*u*934SqoL#8Lz`cTnFK aPisH_%w%|%KbE!t0000cR+y0)%s@j z>v@0lQk#})tM0b137IRyxk%K;^`weRbj!>O zc`bH{t=rGmy9tL~$njBTW#bicSIGAZda>AL>9xO$=gPc%dn|G88#_a--uXHyZ8yuA Y+a*n#gATi%13G}g)78&qol`;+0JQ==vj6}9 literal 0 HcmV?d00001 diff --git a/parts/head_top/ears_round.png b/parts/head_top/ears_round.png new file mode 100644 index 0000000000000000000000000000000000000000..30439649770a10c03f7ae18e55b1ee22726ad04d GIT binary patch literal 241 zcmV4+uqA}8KRUih?mRNEQ+qHEkR|3$qbceupPH>qj$zy42UoTwp?YA3ZT>5 zv+kXli%baIl8LCG*0tU^Gk$sVxPflod=_Bh3DD*5F{CjGnODYRF7MG_QHE8>8ZCz7 z%b{qzck->rC$s%Ue8!PS9s(zg&h#wC6IZ4p(A)`+tI7yNBk2m)l40CSn@}}gQMB3p r2~A|kWO)daBY5Me|I(?6lWFb;@?nO$$x1Lw00000NkvXXu0mjf0>Ek= literal 0 HcmV?d00001 diff --git a/parts/head_top/flower_bloom.png b/parts/head_top/flower_bloom.png new file mode 100644 index 0000000000000000000000000000000000000000..61d7a994ec979e165033b272ea79954c82848ea9 GIT binary patch literal 247 zcmV4u*Y0002KNklszf}2P&gbm`s{1Xt7PK1P=Vyia@ac* z7e)oT=OW7(&)_oom@h?)D|V9>Wc0KA%A7w@+U`e?fY!*lK@9^F>>Ez*OE#kn5?He8 z$QiaN;a~4zOzaJv8X9KhzB^sU22(AxkTJvAY{@>nK6b@JtL>) xypL{gVdNRCd56SY=;KfS>T#?{lk)db`vH_DSO6YCC8Db?Rs z{5jG?ncGXO=*?ni_vA!ipT96F07#P}c1-!MCqU)bKcm zaQn=g7Bg7xYjc(tc*J?=t%*-q!(8J1;qTl8)uifePdT|O7s{I7nsjK7uKcALDO(OS S3S9tN&EV*W30Q2ZXgs?hPK>wV z$1|6O&R70AwWX>p&{Y?}lixTY{yb0b0qEUDkjsfk0-@q|J1Mn&wrm8-; zdNcnCj5d<~=1njZI;#Y3(Rqg(=MW0Dh9ar%3` zA2!njrwEJ~e3&}tsD?EuCe4-3I*HbK!47NXr6fp0--e3X@ZKF4Ng6S*pgTU=cZPyM zT;mtzsAly!-9Z<)@!y!<#$)00Z~F(r!i0UO{%Bt^glh1^oq#j|0000q6O%4Y?1!Ejb}yH#+xL(=3Qk}KK34jU#0KjNV8E)cw-{>h00000NkvXXu0mjfe*Sv6 literal 0 HcmV?d00001 diff --git a/parts/head_top/ice_crown.png b/parts/head_top/ice_crown.png new file mode 100644 index 0000000000000000000000000000000000000000..c18ec02ded7fdad8eb5b56c50023170e8379a4b9 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^qCm{Y!3HD`u!WxkQmvjYjv*CuwoYo~I-tPeqQC!6 z?at)2oe|!$ZyW?xzs_W=^ej_;%Bz~|Q@!}bqFE~?j1ue4ZN66aBt)TG`=mwQ zEV*?0P_eijU{jmJ0O?>Tlgep;&r+%S7*z9=N*$C3gyD3iA^ID$-{{=WH d5y{u_R$o|sX6bnGiR}OY002ovPDHLkV1gVGUitt4 literal 0 HcmV?d00001 diff --git a/parts/head_top/mushroom_cap.png b/parts/head_top/mushroom_cap.png new file mode 100644 index 0000000000000000000000000000000000000000..138236602ab307a6e80e9ffc70c042eeef9a469c GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^5E*8BXSeA1+f>8z3lVlnFkm9rhs^CV9FzhE&X%I*FUFL4n8lV8w^Z zPf~V=110CHdYQe5E^I51n~@vWu8_dkySwiKcLe*|wK7afo2{E-kBhEp?I?Y|YhUb) zvpO@rYq%XydZBn!?COs*92Rfwnp_t@U07rAt1Ql@ZOQU~!DR_Q_g~nv?j`S})sNS% z-u5wQonLVClLGE#O6PQac-0;poBX?MtIdyHS&PNbL`FT?y}fl7&~*%+u6{1-oD!M< D!+BJX literal 0 HcmV?d00001 diff --git a/parts/head_top/water_spout.png b/parts/head_top/water_spout.png new file mode 100644 index 0000000000000000000000000000000000000000..fd53e0c2bca3e5f1e1fc8ee1661d6551f23905b1 GIT binary patch literal 241 zcmVY>HmYyBG^%;tp)dv6htpXC!L)vQI2{TrKfEE6X?FOshH*t{XIsQg($X}GqQn^j?Kb5m=qZ*H4&QG8Ov rrx)G3-t3s0%{tY1!D>_e>oW!RmaT9wZ=NAnS2>OI-OK=w-cH%V+YQVL9PVtb zDPJ9!%RPME)>b^a7Pa{QuKxDV`}~tPF`9Ygr{=8gX6Y;0002*NklYP)?Vj|44nW`f~1``%s=&0N3 z)df}{Q4SOaVxB7ld3^W{CBfSxpJvsIRZkk_4X?wUL;#mM3HVufrFgIm&^|8OZx0PDju)Bpeg07*qoM6N<$f`;dDPXGV_ literal 0 HcmV?d00001 diff --git a/parts/tail/tail_curly.png b/parts/tail/tail_curly.png new file mode 100644 index 0000000000000000000000000000000000000000..c129a772a9ffb275a57ad28358c9b1ec690ed6aa GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`-JULvAr*6agDwg&D6;rGKK%7* z-klW_1Abp>iCyi`eb7@TkFhSrS>=HA#EkUW)-#y-H>tBth`PYMS*gIOPUmD$cF*eH zDguA+*(i2#R=o^$(O3T3EYNAe^S(#^&8-_E3tn@u-ONi!U4QCj!i4(8WoudZ*$-#F u*c4*Brr}+9KuDtPqaD|unN5nXKE$qKq`s~xMb`r8ECx?kKbLh*2~7ZAP)mOR literal 0 HcmV?d00001 diff --git a/parts/tail/tail_dragon.png b/parts/tail/tail_dragon.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5a5590ef41e16a5d10517cf20eed80f7c3fe44 GIT binary patch literal 263 zcmV+i0r>ujP)06z3E-T#TT0hKFc4EZf$JoAz%vQpA>IS%&^Hkz zmxU*QEP`2ZlmLE(DMx4qmkInJ7SH$81UF-X0M2)IxKqsvSQ>Ehav2LWn7IZSwvqwD z3|i!Zn~MsXRRs0GqDl~jR)@py7R_a#PL*I(!LJeNabVO<;J7{1Ezv-jqoWs8_-ux* zM21;jQ}n?xOaN1W85k_wgAF;i;geAKVZSl+r~126I}qZ~Z2e<*^amvXd0ge*!Ic02 N002ovPDHLkV1n|eY4!jB literal 0 HcmV?d00001 diff --git a/parts/tail/tail_feather_fan.png b/parts/tail/tail_feather_fan.png new file mode 100644 index 0000000000000000000000000000000000000000..5bff4bcc69ae2a59cfadfdba4e74c89488bdd219 GIT binary patch literal 310 zcmV-60m=S}P)j0;PK=*`N|R?)k^GWihWs@7>O{%%x}d5z>+Q?ci>d0MBkIc!YN#e46p!1 zAopcFl`5^1h(FMzqD#s#7!5FRajsB#8pWut)ohjUsMXad9kL!`M-O+fyKu+ySvmXQ z#3uUL7JA_nz$WKKwWaQi_CDup*!&!2TuD>?-I7_P6&FCei)<>%d;28Dz#ddfjw0pn2F1JszW^=~6q^ezoA7z2&}pATQ)gp$rcv&xrq~|$8MSM6s80K{cRD&uPe1!!1Y7g}^#|R!Zx~`UH$u8v3;+NC07*qo IM6N<$f@i#k?EnA( literal 0 HcmV?d00001 diff --git a/parts/tail/tail_fish_fin.png b/parts/tail/tail_fish_fin.png new file mode 100644 index 0000000000000000000000000000000000000000..427d4715b2160d9c38e26f880ff607e1b8199409 GIT binary patch literal 224 zcmV<603ZK}P)P000vR1^@s6Wpbtk0001|Nkl3`ECO?;ZQ(Q?-xQep9~f aDER>SWUUq`KpMvY0000P5G6)`Ea2Q?(0!(~(Y zSvE2v%s@V8i-}NwP6me2m;0ju?J!Kc&l83@LdKr}eR?TLNrRR~K1=!#RYm#dRZ{>h zMY*ER@T!7%qXr%LW3P;N$ds^r) z7vJ-a6ygSpNQQFL{+WK3!o5>3tE1wfP5D$;xK*%Ywnfrd0hDiqhU(ve9Jy3M3z>mb zZg>M?`s)^esA*lI){Pag<%k^sX<>GgOKegX!akJ*M|czf0w~_F-8VV`SOsWVZa~os XF3n?=)jd{o00000NkvXXu0mjf2_bK8 literal 0 HcmV?d00001 diff --git a/parts/tail/tail_ice_shard.png b/parts/tail/tail_ice_shard.png new file mode 100644 index 0000000000000000000000000000000000000000..e81b5103368d9af39f1a692aa1aaea4a237e0872 GIT binary patch literal 288 zcmV+*0pI?KP)s3b%$kWZJ}W2oe|zzyJ~QE01d4sZ)QVL>kk>2%uxy!2Tfs4Ustl;*(X zGK7~-X&Bh+6durlWxx_@COk!+9z;O;>+ z;D_G~=KrWDhIUU4nD)eZ&rm*%q_1vi;na~%r>>F>h+~yxVK~)Y>xOyznG7gZMe2YJ miH>h9@xNhKGOHgyg^6}12WqLlI2eZj0000I;P000*V1^@s6{^R5m0002bNklJYXCFfPgMEBKb?E4}3KC;WBk1N~5?jJAM(c8@MJk^J#Ngf;u%}KN)`!zQ~l2yme9l O0000(978JRY@Hm)bwGi~In<`Y zw({7Kq&v^Tws4F5UbtQ{i{qcu>Qzs8-Irfit!`w#t(K~P-B?w8#|eFAgJ$t|Mu`U| zEf1GJy4KJwqIaaM(zUg!?Y)c0V#a-1TRIf}Ugz6iUUed9$Kn05FBMq={>LPhtXVbv oPH|0tf7~9+^t%~Fn~NIRYrO+gRJUoK0lJ65)78&qol`;+0A<-pn*aa+ literal 0 HcmV?d00001 diff --git a/parts/tail/tail_rattle.png b/parts/tail/tail_rattle.png new file mode 100644 index 0000000000000000000000000000000000000000..df602e73d673cc50df41fcd07f139092aca74ae4 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^{6H+k!3HE-=Cy!09iA?ZAr*7xPU7ZkP~dU4ethux z$@G@SWQ){k@k~?fm>l93@kU=eaVYDm-Bc}s0^j9R3Ps+Yc)*lkFri~lk7#(&4P`BU z<%>pHH~ZA8s#o32m@IC0@a`GgL>KQ)zU`U6O|Esy@93DQ>*gQp+LmtK s?h;bqzj67~#Xz}!|Modj;`a}r_`4Jc~u_?O67!1NC zSNvx!ttt5dAOR^lM?kL4X}z$VV|NOqCkvpIWXmrhj(ae5wymnmHjqEii`C}g#3PsvZm`C%TQK?l(S-FaO+%3aHDM*`w=%G$6BH<9_q-6US00000NkvXXu0mjf&N_lm literal 0 HcmV?d00001 diff --git a/parts/tail/tail_stinger.png b/parts/tail/tail_stinger.png new file mode 100644 index 0000000000000000000000000000000000000000..25810f2f177f00211dea352910c799e852451b72 GIT binary patch literal 269 zcmV+o0rLKdP)QfHH zBlOQ4LwQoGVF!#Ue0tyxGy{f{Q8*DDXt{^3T_w zFUX{ss%55_+$J|W?Z4pPqCcu^oKs}kjhO}6jfzx!o-WkSZ`WAiSe-QWwT9N lN^3+H))m}yyDF=fug}6{Q>PH!?ErKRgQu&X%Q~loCII)aK`;OS literal 0 HcmV?d00001 diff --git a/parts/tail/tail_wispy.png b/parts/tail/tail_wispy.png new file mode 100644 index 0000000000000000000000000000000000000000..57b496ea0a7bc0e890c5cff1b27819c551843ff0 GIT binary patch literal 256 zcmV+b0ssDqP) # one base with markings and a few variations + python preview.py part # a variation part on every compatible base + python preview.py slot # every part of a slot on a default base + python preview.py raw # parts of a slot at 8x: raw keys + markers, and recolored + python preview.py show body/base_fox tail/tail_leaf # chosen parts at 10x, raw + recolored + python preview.py zoom body=base_fox tail=tail_leaf + body=base_blob head_top=leaf_sprout # tight crops, 5x + python preview.py all # raw + slot sheets for every slot, plus bases + +Options: --markings (default none), --scale N, --type (type ramp) +Grid sheets are drawn in three test palettes (dark / light / near-gray), one row block each. +Cells with a red frame triggered "canvas expanded". Set PARTS_DIR to preview another library. +""" +import os +import random +import sys +from pathlib import Path + +from PIL import Image, ImageDraw + +import generate_sprite as gs +import sprite_parts as cfg + +OUT = Path(os.environ.get("PREVIEW_OUT", "preview")) # set PREVIEW_OUT to keep sheets apart +PARTS = os.environ.get("PARTS_DIR", "parts") +PALETTES = [("dark", ["#0D47A1", "#FFC107"]), ("light", ["#FFEB3B", "#5D4037"]), ("gray", ["#757575", "#E53935"])] +BG = (226, 226, 220, 255) +LABEL_H = 9 + +BY_NAME = {slot: {p["name"]: p for p in parts} for slot, parts in cfg.SLOTS.items()} +DEFAULT_BODY = "base_fox" + + +def P(slot, name): + return BY_NAME[slot][name] + + +def exists(slot, name): + return (Path(PARTS) / slot / f"{name}.png").exists() + + +def allowed(slot, name, chosen): + """Same rules as generate_sprite.candidates, against an explicit selection.""" + p = P(slot, name) + body = P("body", chosen["body"]) + tags, excluded, denied = {f"plan:{body['plans'][0]}"}, set(), set() + for s, n in chosen.items(): + if s != slot and not s.startswith("_") and n != "none": + tags |= set(P(s, n)["tags"]) + excluded |= set(P(s, n)["excludes"]) + denied |= set(P(s, n)["deny"]) + if body["plans"][0] not in p["plans"] or name in denied: + return False + if p["requires"] and not set(p["requires"]) & tags: + return False + return not (set(p["tags"]) & excluded or set(p["excludes"]) & tags) + + +def top_type(slot, name): + p = P(slot, name) + return max(cfg.TYPES, key=lambda t: gs.part_weight(p, t)) + + +def typed(chosen, ptype): + """Type ramp to show: explicit, else the first type-palette part's best type, else the body's.""" + if ptype: + return ptype + for s, n in chosen.items(): + if s != "body" and not s.startswith("_") and n != "none" and P(s, n)["palette"] == "type": + return top_type(s, n) + return top_type("body", chosen["body"]) + + +def compose(chosen, markings="none"): + chosen = dict(chosen) + m = chosen.pop("_markings", markings) + picks = {s: P(s, n) for s, n in chosen.items() if n and n != "none"} + return gs.compose(PARTS, picks, m, random.Random(1)) + + +def colored(spr, colors, ptype): + main, accent = gs.pick_main_accent([(gs.hex_to_rgb(c), 1.0) for c in colors], ptype) + return gs.recolor(spr, gs.build_palette(main, accent, ptype)) + + +def sheet(cells, path, ptype=None, markings="none", scale=4, cols=6): + """cells: list of (label, chosen-dict). Renders each in all three palettes at 1x cells.""" + if not cells: + print(f"nothing to render for {path}") + return + cols = min(cols, len(cells)) + rows_per = (len(cells) + cols - 1) // cols + cw, ch = gs.CANVAS, gs.CANVAS + LABEL_H + block_h = rows_per * ch + 12 + img = Image.new("RGBA", (cols * cw, block_h * len(PALETTES)), BG) + d = ImageDraw.Draw(img) + for bi, (pname, colors) in enumerate(PALETTES): + y0 = bi * block_h + d.text((2, y0 + 1), f"{pname}: {', '.join(colors)}", fill=(60, 60, 60, 255)) + for i, (label, chosen) in enumerate(cells): + cx, cy = (i % cols) * cw, y0 + 12 + (i // cols) * ch + sprite = compose(chosen, markings) + expanded = max(sprite.width + 2, sprite.height + gs.GROUND_MARGIN + 1) > gs.CANVAS + spr = colored(gs.place_on_canvas(sprite), colors, typed(chosen, ptype)) + if spr.size[0] > cw: + spr = spr.crop(((spr.width - cw) // 2, spr.height - gs.CANVAS, (spr.width + cw) // 2, spr.height)) + img.alpha_composite(spr, (cx, cy)) + if expanded: + d.rectangle((cx, cy, cx + cw - 1, cy + gs.CANVAS - 1), outline=(220, 0, 0, 255)) + label = f"{label} {sprite.width}x{sprite.height}!" + d.text((cx + 1, cy + gs.CANVAS - 1), label[:19], fill=(40, 40, 40, 255)) + OUT.mkdir(parents=True, exist_ok=True) + img.resize((img.width * scale, img.height * scale), Image.NEAREST).save(path) + print(path) + + +def zoom_sheet(groups, path, scale=5, markings="none", ptype=None): + """Tight-cropped composites, one row per combo, one column per test palette.""" + rows = [] + for chosen in groups: + spr = compose(chosen, markings) + rows.append([colored(spr, colors, typed(chosen, ptype)) for _, colors in PALETTES]) + cw = max(i.width for r in rows for i in r) + 4 + ch = max(i.height for r in rows for i in r) + 4 + img = Image.new("RGBA", (cw * len(PALETTES), ch * len(rows)), BG) + for yi, r in enumerate(rows): + for xi, im in enumerate(r): + img.alpha_composite(im, (xi * cw + (cw - im.width) // 2, yi * ch + ch - 2 - im.height)) + OUT.mkdir(parents=True, exist_ok=True) + img.resize((img.width * scale, img.height * scale), Image.NEAREST).save(path) + print(path) + + +def bodies(): + return [b for b in BY_NAME["body"] if exists("body", b)] + + +def hosts_for(slot, name): + """Every drawn base that can legally carry the part.""" + cells = [] + for b in bodies(): + chosen = {"body": b, slot: name} + if slot == "body" or allowed(slot, name, chosen): + cells.append((b, chosen)) + return cells + + +def raw_sheet(items, path, scale=8, cols=6): + """items: [(slot, name)]. Each part drawn raw (with markers) and recolored in the dark palette.""" + imgs = [] + for slot, n in items: + if not exists(slot, n): + continue + raw = Image.open(Path(PARTS) / slot / f"{n}.png").convert("RGBA") + rec = colored(gs.load_part(PARTS, slot, n)[0], PALETTES[0][1], top_type(slot, n)) + imgs.append((n, raw, rec)) + if not imgs: + return + pad = 3 + cw = max(i.width for _, i, _ in imgs) * 2 + pad * 3 + ch = max(i.height for _, i, _ in imgs) + pad * 2 + cols = min(cols, len(imgs)) + rows = (len(imgs) + cols - 1) // cols + out = Image.new("RGBA", (cols * cw * scale, rows * (ch * scale + 12)), BG) + d = ImageDraw.Draw(out) + for k, (n, raw, rec) in enumerate(imgs): + x, y = (k % cols) * cw * scale, (k // cols) * (ch * scale + 12) + for j, im in enumerate((raw, rec)): + ox, oy = x + (pad + j * (raw.width + pad)) * scale, y + pad * scale + for yy in range(im.height): # checker shows the part canvas + for xx in range(im.width): + if (xx + yy) % 2: + d.rectangle((ox + xx * scale, oy + yy * scale, ox + (xx + 1) * scale - 1, + oy + (yy + 1) * scale - 1), fill=(205, 205, 200, 255)) + out.alpha_composite(im.resize((im.width * scale, im.height * scale), Image.NEAREST), (ox, oy)) + d.text((x + 2, y + ch * scale), f"{n} {raw.width}x{raw.height}", fill=(30, 30, 30, 255)) + OUT.mkdir(parents=True, exist_ok=True) + out.save(path) + print(path) + + +def variation(b): + """A light, legal variation for a base: first allowed head_top, back and tail in a fixed list.""" + chosen = {"body": b} + prefs = {"head_top": ["leaf_sprout", "crest_flame", "gem_forehead", "horn_single"], + "back": ["wings_feather_small", "wings_bug", "spikes_back", "dorsal_fin"], + "tail": ["tail_flame_tip", "tail_leaf", "tail_fish_fin", "tail_stinger", "tail_wispy", "tail_rattle"]} + for slot, names in prefs.items(): + for n in names: + if exists(slot, n) and allowed(slot, n, {**chosen, slot: n}): + chosen[slot] = n + break + return chosen + + +def parse_opts(args): + opts, rest, it = {"markings": "none", "scale": None, "type": None}, [], iter(args) + for a in it: + if a in ("--markings", "--scale", "--type"): + opts[a[2:]] = next(it) + else: + rest.append(a) + return opts, rest + + +def main(argv): + opts, args = parse_opts(argv) + scale = int(opts["scale"]) if opts["scale"] else None + kw = {"markings": opts["markings"], "scale": scale or 4, "ptype": opts["type"]} + if not args: + print(__doc__) + return + cmd = args[0] + if cmd == "bases": + cells = [] + for b in bodies(): + cells += [(b, {"body": b}), (f"{b} +", variation(b))] + sheet(cells, OUT / "bases.png", **kw) + elif cmd == "body": + b = args[1] + cells = [("bare", {"body": b}), ("varied", variation(b))] + cells += [(m, {"body": b, "_markings": m}) for m in cfg.MARKINGS if m != "none"] + sheet(cells, OUT / f"body_{b}.png", **kw) + elif cmd == "part": + slot, name = args[1], args[2] + sheet(hosts_for(slot, name), OUT / f"part_{slot}_{name}.png", + **{**kw, "ptype": kw["ptype"] or (top_type(slot, name) if slot != "body" else None)}) + elif cmd == "slot": + slot = args[1] + cells = [] + for n in BY_NAME[slot]: + if n == "none" or not exists(slot, n): + continue + hosts = hosts_for(slot, n) + pick = next((c for _, c in hosts if c["body"] == DEFAULT_BODY), hosts[0][1] if hosts else None) + if pick: + cells.append((n, pick)) + sheet(cells, OUT / f"slot_{slot}.png", **kw) + elif cmd == "raw": + slot = args[1] + raw_sheet([(slot, n) for n in BY_NAME[slot] if n != "none"], OUT / f"raw_{slot}.png", + cols=3 if slot in ("aura", "body", "back") else 6) + elif cmd == "show": + raw_sheet([tuple(a.split("/", 1)) for a in args[1:]], OUT / "show.png", scale=scale or 10, cols=3) + elif cmd == "zoom": + groups, cur = [], {} + for a in args[1:]: + if a == "+": + groups.append(cur) + cur = {} + else: + k, v = a.split("=", 1) + cur[k] = v + groups.append(cur) + zoom_sheet(groups, OUT / "zoom.png", scale=scale or 5, markings=opts["markings"], ptype=opts["type"]) + elif cmd == "all": + main(["bases"]) + for slot in cfg.SLOTS: + main(["raw", slot]) + main(["slot", slot]) + else: + print(__doc__) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b01714d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy>=2.0 +Pillow>=10.0 +requests>=2.31 diff --git a/sample.csv b/sample.csv new file mode 100644 index 0000000..d2537c0 --- /dev/null +++ b/sample.csv @@ -0,0 +1,46 @@ +name,type +Maple Street Bakery,normal +Corner Copy Shop,normal +Sunny Side Diner,normal +Blaze BBQ Pit,fire +Ember Forge Welding,fire +Hot Spot Pizza,fire +Clearwave Pools,water +Harbor Fish Market,water +Rainy Day Plumbing,water +Greenleaf Landscaping,grass +Fern Gully Florist,grass +Root & Sprout Farm,grass +Volt Auto Electric,electric +Bright Spark Repairs,electric +Zap Solar Co,electric +Frosty Treats Creamery,ice +Polar Air HVAC,ice +Glacier Cold Storage,ice +Iron Fist Dojo,fighting +Rumble Boxing Gym,fighting +Titan Strength Club,fighting +Nightshade Pest Control,poison +Venom Tattoo Parlor,poison +Toxic Waste Removal,poison +Bedrock Excavation,ground +Dusty Trail Concrete,ground +Mudflap Towing,ground +Skyline Drone Tours,flying +Feather Travel Agency,flying +Tailwind Kites,flying +Third Eye Tarot,psychic +Mindful Yoga Studio,psychic +Dreamscape Therapy,psychic +Busy Bee Honey,bug +Beetle Bros Moving,bug +Hive Mind Coworking,bug +Granite Countertops Plus,rock +Boulder Climbing Gym,rock +Pebble Jewelry,rock +Hollow Oaks Funeral Home,ghost +Midnight Escape Rooms,ghost +Phantom Haunt Tours,ghost +Wyvern Security,dragon +Drake Motorsports,dragon +Scale & Claw Reptiles,dragon diff --git a/sketch.py b/sketch.py new file mode 100755 index 0000000..e4ab784 --- /dev/null +++ b/sketch.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +Authoring helper for part drafts. Build a part from primitive forms, shade it with +the shared light model, add hand details, and save the text grid to parts_src/. +The saved .txt grid is the source of truth afterwards; hand-edit it freely. + + from sketch import Canvas, stamp, save + c = Canvas(40, 34) + c.capsule(26, 26, 26, 31, 2.4, r="F") # far leg (F = main ramp, in shadow) + c.ellipse(24, 24, 11, 8, r="M") # body (M = main ramp) + c.ellipse(15, 13, 12, 11, r="K") # big head in front (K = main ramp, front layer) + rows = c.shade(soft=["KM"], dark=0.84, light=(0.06, 0.50), side=(0.14, 0.80), min_light=4) + rows = stamp(rows, ["pp", "pp"], 10, 12) # hand details (face) on top + save("body/base_x", rows, marks=[("P", 22, 33), ("^", 15, 5), ("B", 28, 17), ("T", 33, 24), ("A", 24, 25)]) + +Region letters (see REGIONS): one letter per form layer. Same letter = one merged form. + main ramp: M N K (normal) F (far side, in shadow) U (extra lit) + accent ramp: C D (normal) G (far side) V (extra lit) + type ramp: Y Q (normal) J (far side) X (extra lit) +Literal palette chars (1 2 3 a b c x y z o w p) may also be used as a layer: painted flat. + +Shading: every pixel of a form gets its position along the light chord (march toward the +light at the top-left and away from it until leaving the form). The far end of the chord +is core shadow, so the shadow is a crescent that thickens where the form is deep (no +outline-following band). The highlight is an off-outline blob on the lit side. Each form +is shaded with its full silhouette, so overlapping front forms do not distort it. +Where a front form overlaps a form behind it, a 1px line is drawn on the front form, +except for pairs listed in `soft`. +""" +import math +from pathlib import Path + +REGIONS = { + "M": ("main", 0), "N": ("main", 0), "K": ("main", 0), "F": ("main", -1), "U": ("main", 1), + "C": ("accent", 0), "D": ("accent", 0), "G": ("accent", -1), "V": ("accent", 1), + "Y": ("type", 0), "Q": ("type", 0), "J": ("type", -1), "X": ("type", 1), +} +RAMPS = {"main": "123", "accent": "abc", "type": "xyz"} +LIGHT_DIR = (-0.55, -0.83) # toward the light, screen coords (y down) +DIRS4 = ((1, 0), (-1, 0), (0, 1), (0, -1)) + + +def _march(cset, x, y, dx, dy): + n, fx, fy = 0, x + 0.5, y + 0.5 + while True: + fx += dx * 0.5 + fy += dy * 0.5 + if (int(math.floor(fx)), int(math.floor(fy))) not in cset: + return n * 0.5 + n += 1 + + +def shade_form(pixels, ramp, bias=0, dark=0.76, light=(0.10, 0.42), side=(0.22, 0.72), min_light=5.0): + """{(x,y): tone char} for one form.""" + lx, ly = LIGHT_DIR + m = math.hypot(lx, ly) + lx, ly = lx / m, ly / m + px_, py_ = -ly, lx + out = {} + for x, y in pixels: + up, dn = _march(pixels, x, y, lx, ly), _march(pixels, x, y, -lx, -ly) + a, b = _march(pixels, x, y, px_, py_), _march(pixels, x, y, -px_, -py_) + chord, t = up + dn + 1, (up + 0.5) / (up + dn + 1) + q = (a + 0.5) / (a + b + 1) + d_th = dark - 0.30 * (bias < 0) + 0.10 * (bias > 0) + if t >= d_th: + tone = 2 + elif (bias >= 0 and chord >= min_light and light[0] <= t <= light[1] + 0.12 * (bias > 0) + and side[0] <= q <= side[1]): + tone = 0 + else: + tone = 1 + out[(x, y)] = RAMPS[ramp][tone] + return out + + +def add_outline(rows, ch="o"): + """1px outline (4-neighbour) around every opaque pixel.""" + h, w = len(rows), len(rows[0]) + g = [list(r) for r in rows] + add = [] + for y in range(h): + for x in range(w): + if g[y][x] == "." and any(0 <= x + dx < w and 0 <= y + dy < h and g[y + dy][x + dx] != "." + for dx, dy in DIRS4): + add.append((x, y)) + for x, y in add: + g[y][x] = ch + return ["".join(r) for r in g] + + +def stamp(rows, st, x0, y0): + """Paint text rows onto rows at x0,y0. '_' is skipped (see-through); '.' erases.""" + g = [list(r) for r in rows] + for j, s in enumerate(st): + for i, ch in enumerate(s): + if ch != "_" and 0 <= y0 + j < len(g) and 0 <= x0 + i < len(g[0]): + g[y0 + j][x0 + i] = ch + return ["".join(r) for r in g] + + +def replace(rows, pairs, region=None): + """Swap characters, e.g. replace(rows, {'o': '.'}, region=(x0, y0, x1, y1)) (inclusive).""" + g = [list(r) for r in rows] + for y, r in enumerate(g): + for x, ch in enumerate(r): + if region and not (region[0] <= x <= region[2] and region[1] <= y <= region[3]): + continue + if ch in pairs: + g[y][x] = pairs[ch] + return ["".join(r) for r in g] + + +def flip(rows): + return [r[::-1] for r in rows] + + +def save(key, rows, marks=()): + """Write parts_src/.txt, placing marker chars at (x, y).""" + g = [list(r) for r in rows] + widths = {len(r) for r in g} + if len(widths) != 1: + raise ValueError(f"{key}: ragged rows {sorted(widths)}") + for ch, x, y in marks: + g[y][x] = ch + path = Path("parts_src") / f"{key}.txt" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join("".join(r) for r in g) + "\n") + return ["".join(r) for r in g] + + +def load(key): + return [r for r in (Path("parts_src") / f"{key}.txt").read_text().splitlines() if r and not r.startswith("#")] + + +class Canvas: + """Collects forms as ordered layers; shade() turns them into palette rows.""" + + def __init__(self, w, h): + self.w, self.h = w, h + self.layers = [] # [(char, set of pixels)], back to front + + def _add(self, px, r): + px = {(x, y) for x, y in px if 0 <= x < self.w and 0 <= y < self.h} + if self.layers and self.layers[-1][0] == r: + self.layers[-1][1].update(px) + return + for ch, s in self.layers: # same letter again = same form, keep its depth + if ch == r and r in REGIONS: + s.update(px) + return + self.layers.append((r, px)) + + def ellipse(self, cx, cy, rx, ry, r="M", angle=0.0, clip=None): + """angle in degrees, positive rotates clockwise on screen.""" + ca, sa = math.cos(math.radians(angle)), math.sin(math.radians(angle)) + rr = max(rx, ry) + px = set() + for y in range(int(cy - rr) - 1, int(cy + rr) + 2): + for x in range(int(cx - rr) - 1, int(cx + rr) + 2): + ux, uy = x + 0.5 - cx, y + 0.5 - cy + lx, ly = ux * ca + uy * sa, -ux * sa + uy * ca + if (lx / rx) ** 2 + (ly / ry) ** 2 <= 1.0 and (clip is None or clip(x, y)): + px.add((x, y)) + self._add(px, r) + + def capsule(self, x0, y0, x1, y1, r0, r="M", r1=None): + """Tapered limb from (x0,y0) radius r0 to (x1,y1) radius r1 (default r0).""" + r1 = r0 if r1 is None else r1 + vx, vy = x1 - x0, y1 - y0 + ln2 = vx * vx + vy * vy or 1e-9 + px = set() + big = max(r0, r1) + for y in range(int(min(y0, y1) - big) - 1, int(max(y0, y1) + big) + 2): + for x in range(int(min(x0, x1) - big) - 1, int(max(x0, x1) + big) + 2): + cx, cy = x + 0.5, y + 0.5 + t = max(0.0, min(1.0, ((cx - x0) * vx + (cy - y0) * vy) / ln2)) + rr = r0 + (r1 - r0) * t + if math.hypot(cx - (x0 + vx * t), cy - (y0 + vy * t)) <= rr: + px.add((x, y)) + self._add(px, r) + + def poly(self, pts, r="M"): + px = set() + ys = [p[1] for p in pts] + for y in range(int(min(ys)) - 1, int(max(ys)) + 2): + yc = y + 0.5 + xs = [] + for (ax, ay), (bx, by) in zip(pts, pts[1:] + pts[:1]): + if (ay <= yc < by) or (by <= yc < ay): + xs.append(ax + (yc - ay) * (bx - ax) / (by - ay)) + xs.sort() + for a, b in zip(xs[::2], xs[1::2]): + for x in range(int(math.floor(a)), int(math.ceil(b)) + 1): + if a <= x + 0.5 <= b: + px.add((x, y)) + self._add(px, r) + + def mask(self, rows, x0=0, y0=0): + """Add forms from a hand-drawn letter mask (each letter becomes/extends its layer).""" + by = {} + for j, row in enumerate(rows): + for i, ch in enumerate(row): + if ch not in "._ ": + by.setdefault(ch, set()).add((x0 + i, y0 + j)) + for ch, px in by.items(): + self._add(px, ch) + + def erase(self, pts): + for _, s in self.layers: + s.difference_update(pts) + + def shade(self, soft=(), outline=True, seps=True, **kw): + out = [["."] * self.w for _ in range(self.h)] + owner = [[-1] * self.w for _ in range(self.h)] + softset = {frozenset(s) for s in soft} + for k, (ch, px) in enumerate(self.layers): + if ch in REGIONS: + ramp, bias = REGIONS[ch] + tones = shade_form(px, ramp, bias, **kw) + else: + tones = {p: ch for p in px} + line = set() + if seps and ch in REGIONS: + for x, y in px: + for dx, dy in DIRS4: + nx, ny = x + dx, y + dy + if (nx, ny) in px or not (0 <= nx < self.w and 0 <= ny < self.h): + continue + o = owner[ny][nx] + if o >= 0 and out[ny][nx] != "." and self.layers[o][0] in REGIONS \ + and frozenset((ch, self.layers[o][0])) not in softset and self.layers[o][0] != ch: + line.add((x, y)) + break + for (x, y), t in tones.items(): + out[y][x] = "o" if (x, y) in line else t + owner[y][x] = k + rows = ["".join(r) for r in out] + return add_outline(rows) if outline else rows + + +if __name__ == "__main__": + print(__doc__) diff --git a/sprite_parts.py b/sprite_parts.py new file mode 100644 index 0000000..4b64964 --- /dev/null +++ b/sprite_parts.py @@ -0,0 +1,423 @@ +""" +Part library and per-type weights for generate_sprite.py. + +How picking works +----------------- +1. A body plan is picked using PLAN_WEIGHTS[type]. +2. Each slot in PICK_ORDER (generate_sprite.py) is filled with one part. A part is a + candidate only if: + - the chosen body plan is in its `plans` + - if it has `requires`, at least one of those tags was added by an earlier pick + - none of its `tags` were excluded by an earlier pick, and none of its + `excludes` match an earlier pick's tags +3. Candidate weight = types.get(type, other). Higher = more likely. 0 = never. + "none" entries are real choices, so their weight controls how often a slot is empty. + +Bodies are complete base creatures (body, head and face drawn together); the other slots +vary them. Tags ending in "_head" (head family, gates head_top parts) and tags starting +with "_" (used only so a base can exclude one specific part shape) are internal plumbing. +All other tags show up as the sprite's "attributes" (beaked, finned, fluffy, tailed...). + +Editing this file changes which parts existing businesses get, since every pick +draws from the same seeded random sequence. +""" + +TYPES = [ + "normal", "fire", "water", "grass", "electric", "ice", "fighting", "poison", + "ground", "flying", "psychic", "bug", "rock", "ghost", "dragon", +] + +# Signature color per type. Used for the "type" palette (flames, leaves, sparks...) +# and as a fallback when a logo has no usable second color. +TYPE_COLORS = { + "normal": "#A8A878", "fire": "#F08030", "water": "#6890F0", "grass": "#78C850", + "electric": "#F8D030", "ice": "#98D8D8", "fighting": "#C03028", "poison": "#A040A0", + "ground": "#E0C068", "flying": "#A890F0", "psychic": "#F85888", "bug": "#A8B820", + "rock": "#B8A038", "ghost": "#705898", "dragon": "#7038F8", +} + +PLANS = ["quadruped", "biped", "serpentine", "avian", "aquatic", "blob", "insectoid"] +ALL = PLANS +LEGGED = ["quadruped", "biped"] + +PLAN_WEIGHTS = { + "normal": {"quadruped": 5, "biped": 4, "avian": 2, "blob": 1, "serpentine": 0.5}, + "fire": {"quadruped": 5, "biped": 3, "serpentine": 1.5, "avian": 1.5, "blob": 1}, + "water": {"aquatic": 5, "serpentine": 2.5, "quadruped": 2, "biped": 1.5, "blob": 1}, + "grass": {"quadruped": 4, "biped": 3, "blob": 2, "serpentine": 1, "insectoid": 1}, + "electric": {"quadruped": 4, "biped": 3, "blob": 2, "avian": 1, "insectoid": 1}, + "ice": {"quadruped": 3, "biped": 2, "aquatic": 2, "avian": 1.5, "blob": 1.5, "serpentine": 1}, + "fighting": {"biped": 7, "quadruped": 2, "insectoid": 0.5}, + "poison": {"blob": 4, "serpentine": 3, "quadruped": 2, "biped": 2, "insectoid": 2}, + "ground": {"quadruped": 5, "biped": 3, "serpentine": 2, "insectoid": 0.5}, + "flying": {"avian": 7, "quadruped": 1, "serpentine": 1, "insectoid": 1}, + "psychic": {"biped": 4, "quadruped": 3, "blob": 2, "avian": 1.5}, + "bug": {"insectoid": 7, "serpentine": 1, "avian": 0.5}, + "rock": {"biped": 3, "quadruped": 3, "serpentine": 2, "blob": 2}, + "ghost": {"blob": 6, "serpentine": 2, "biped": 1, "avian": 0.5}, + "dragon": {"serpentine": 4, "quadruped": 3, "biped": 3, "avian": 0.5}, +} + +# Procedural markings: painted onto the body's main-color pixels in the accent color. +# No sprites needed. "_other" is the weight for types not listed. The belly patch is +# centred on the body's aura anchor (its visual centre), so it lands on the tummy +# rather than across a big baked-in head. +MARKINGS = { + "none": {"_other": 4}, + "spots": {"grass": 3, "poison": 3, "bug": 2, "water": 2, "electric": 2, "normal": 1.5, "_other": 0.5}, + "speckles": {"ground": 3, "rock": 3, "ice": 1.5, "normal": 1, "_other": 0.3}, + "belly": {"normal": 3, "water": 2, "fire": 2, "dragon": 2, "ice": 2, "fighting": 2, "ground": 2, "_other": 1}, +} + +# Combination rules for the variation slots. +# - At most MAX_FEATURES of head_top/back/tail are filled (picked in that order), so a creature +# gets one or two signature features instead of a pile of unrelated parts. +# - Each feature part has a visual motif. Once a motif is on the creature, more parts of the +# same motif are boosted (x MOTIF_SAME) and parts of an unrelated motif are damped +# (x MOTIF_CLASH), unless the pair is listed in MOTIF_PAIRS. Parts without a motif (plain fur +# ears and tails) go with anything. +# Slots skipped during generation (their art stays in the library). Remove "aura" to bring +# the floating aura effects back. +DISABLED_SLOTS = {"aura"} +FEATURE_SLOTS = ("head_top", "back", "tail") +MAX_FEATURES = 2 +MOTIF_SAME, MOTIF_CLASH = 2.5, 0.08 +MOTIFS = { + # feathers + "crest_feather": "feather", "wings_feather": "feather", "wings_feather_small": "feather", + "tail_feather_fan": "feather", + # plants + "leaf_sprout": "plant", "flower_bloom": "plant", "mushroom_cap": "plant", "plant_bulb": "plant", + "leaf_cape": "plant", "flower_back": "plant", "mushroom_back": "plant", "tail_leaf": "plant", + # fire + "crest_flame": "fire", "flame_mane": "fire", "tail_flame_tip": "fire", + # spooky + "wisp_flame": "spooky", "wings_bat": "spooky", "ghost_tatters": "spooky", "tail_wispy": "spooky", + # crystal / ice + "ice_crown": "crystal", "crystal_spikes": "crystal", "gem_forehead": "crystal", "crystal_cluster": "crystal", + "icicle_spikes": "crystal", "tail_ice_shard": "crystal", "tail_crystal": "crystal", + # water + "ears_fin": "water", "water_spout": "water", "dorsal_fin": "water", "tail_fish_fin": "water", + # electric + "ears_bolt": "electric", "coil_spikes": "electric", "tail_lightning": "electric", + # armour / rock + "horn_single": "armor", "horns_ram": "armor", "shell_round": "armor", "shell_spiked": "armor", + "spikes_back": "armor", "rock_plates": "armor", "tail_club": "armor", "tail_spiked": "armor", + # dragon + "horns_dragon": "dragon", "wings_dragon": "dragon", "sail_fin": "dragon", "tail_dragon": "dragon", + # bug + "antennae": "bug", "wings_bug": "bug", "tail_stinger": "bug", + # sky + "cloud_tuft": "sky", "cloud_puffs": "sky", +} +MOTIF_PAIRS = {frozenset(p) for p in [ + ("dragon", "armor"), ("dragon", "fire"), ("crystal", "armor"), ("crystal", "water"), ("plant", "bug"), + ("feather", "sky"), ("fire", "armor"), ("water", "armor"), ("spooky", "crystal"), ("electric", "armor"), + ("sky", "electric"), ("sky", "crystal"), ("feather", "fire"), ("dragon", "water"), +]} + + +def part(name, plans=ALL, types=None, other=0.25, tags=(), requires=(), excludes=(), deny=(), + palette="main", size=None, z=None, desc=""): + return { + "name": name, "plans": list(plans), "types": dict(types or {}), "other": other, + "tags": list(tags), "requires": list(requires), "excludes": list(excludes), "deny": list(deny), + "palette": palette, "size": size, "z": z, "desc": desc, + } + + +def none(types=None, other=1.0, plans=ALL, requires=()): + return part("none", plans, types, other, requires=requires, desc="(empty slot, no sprite)") + + +# Head-family tags carried by each base body. head_top parts use them in `requires`. +MAMMAL = ["mammal_head"] +ANY_FACE = ["mammal_head", "reptile_head", "integrated_head"] +NOT_BEAK = ["mammal_head", "reptile_head", "rock_head", "insect_head", "integrated_head", "ghost_head"] +INTEGRATED = ["integrated_head"] + + +def base(name, plan, types, head, size, desc, tags=(), excludes=(), deny=()): + """A complete designed creature (body + head + face in one sprite). + + head: head-family tag(s) gating head_top parts. excludes: tags of variation parts that + clash with features already drawn into the base (e.g. "eared" when it has ears). + deny: specific variation part names that don't sit well on this base.""" + heads = [head] if isinstance(head, str) else list(head) + return part(name, [plan], types, 0.3, tags=heads + list(tags), excludes=excludes, deny=deny, + size=size, desc=desc) + + +# --------------------------------------------------------------------------- body (base creatures) +BODY = [ + # quadruped + base("base_fox", "quadruped", {"fire": 3, "electric": 3, "psychic": 3, "normal": 2, "ice": 2}, "mammal_head", + (42, 36), "Chibi fox/cat critter with cheek tufts, big pointed ears and a fluffy bib", excludes=["eared"], + deny=["ears_fin", "horns_ram", "horns_dragon", "crystal_spikes", "tail_fish_fin"]), + base("base_cub", "quadruped", {"ground": 4, "rock": 3, "fighting": 3, "normal": 2, "grass": 2}, "mammal_head", + (40, 34), "Chunky bear-cub on all fours, round ears, sturdy paws", excludes=["eared", "_side_horns"], + deny=["ears_fin", "horns_dragon", "crystal_spikes", "mane_fluffy", "tail_fish_fin"]), + base("base_pup", "quadruped", {"normal": 4, "fire": 3, "fighting": 2, "water": 1.5}, "mammal_head", + (42, 34), "Bouncy puppy with floppy ears and a happy open mouth", excludes=["eared", "_side_horns"], + deny=["ears_fin", "horns_dragon", "tail_fish_fin"]), + base("base_salamander", "quadruped", {"fire": 3, "water": 3, "poison": 3, "dragon": 2, "grass": 1}, "reptile_head", + (47, 27), "Low chibi salamander/newt with a wide smile and frilly cheeks", excludes=["_head_fins"], + deny=["horns_ram", "tail_fluffy", "mane_fluffy"]), + base("base_drake", "quadruped", {"dragon": 5, "fire": 3, "rock": 2, "ground": 1}, "reptile_head", + (42, 38), "Cool baby dragon on four legs, nub horns, confident grin", excludes=["horned", "_headband"], + deny=["ears_fin", "crest_feather", "crystal_spikes", "tail_fluffy"]), + base("base_fawn", "quadruped", {"grass": 4, "psychic": 3, "ice": 3, "normal": 1}, "mammal_head", + (42, 35), "Dainty chibi fawn in 3/4 view: big leaf ears, gentle eyes, spotted back", + excludes=["eared", "_side_horns"], + deny=["ears_fin", "crystal_spikes", "mane_fluffy", "tail_fish_fin", "horns_dragon", "wings_bat", + "wings_dragon", "mushroom_back"]), + # biped + base("base_imp", "biped", {"electric": 4, "normal": 3, "psychic": 2, "ghost": 1}, "mammal_head", + (40, 40), "Upright mouse-imp with big round ears and a cheeky fanged grin", excludes=["eared"], + deny=["ears_fin", "crystal_spikes", "shell_round", "shell_spiked", "rock_plates", "tail_fish_fin"]), + base("base_brawler", "biped", {"fighting": 5, "rock": 3, "ground": 3, "normal": 1}, "mammal_head", + (42, 43), "Stout little fighter with big fists and a determined face", excludes=["eared"], + deny=["ears_fin", "spiky_hair", "crystal_spikes", "flame_mane", "tail_stub", "tail_fish_fin", "tail_feather_fan"]), + base("base_bunny", "biped", {"normal": 4, "grass": 3, "ice": 2, "psychic": 2}, "mammal_head", + (36, 45), "Upright chibi bunny with long ears and a fluffy chest", excludes=["eared"], + deny=["ears_fin", "horn_single", "horns_dragon", "antennae", "crest_flame", "crystal_spikes", "spiky_hair", "water_spout", "wings_dragon", "flame_mane", "tail_fish_fin", "tail_feather_fan"]), + base("base_dino", "biped", {"dragon": 4, "fire": 3, "ground": 2, "water": 1}, "reptile_head", + (36, 44), "Little upright dino with a big head, tiny arms and a toothy grin", excludes=["_headband"], + deny=["flame_mane"]), + base("base_pixie", "biped", {"psychic": 4, "ghost": 3, "grass": 2, "poison": 2}, "mammal_head", + (42, 39), "Leaf-elf pixie with sweeping pointed ears, round torso, stubby legs and a mischievous grin", + excludes=["eared"], + deny=["ears_fin", "wings_dragon", "shell_spiked", "rock_plates", "tail_feather_fan"]), + # avian + base("base_chick", "avian", {"normal": 4, "ice": 3, "electric": 2, "water": 2, "flying": 2}, "beak_head", + (36, 35), "Egg-round chick with a feather tuft, wing nubs and a tiny beak", + deny=["crest_feather", "wings_bat"]), + base("base_owl", "avian", {"psychic": 4, "flying": 3, "ghost": 3, "normal": 1}, "beak_head", + (36, 36), "Round owlet with huge eyes and ear tufts", excludes=["eared"], + deny=["crest_feather", "crest_flame", "crystal_spikes", "ice_crown", "wisp_flame", "wings_feather", "wings_bat"]), + base("base_hawk", "avian", {"flying": 5, "fire": 3, "fighting": 3, "dragon": 1}, "beak_head", + (38, 37), "Cool young hawk, puffed chest, sharp brow and hooked beak", + deny=["crest_feather", "wisp_flame", "wings_feather", "wings_bat"]), + base("base_penguin", "avian", {"ice": 5, "water": 4, "normal": 1}, "beak_head", + (36, 38), "Chubby penguin chick with flipper wings and a bib", + deny=["wings_feather", "wings_feather_small", "wings_bat", "tail_wispy", "ghost_tatters"]), + # aquatic + base("base_guppy", "aquatic", {"water": 5, "poison": 2, "ice": 2, "dragon": 1}, "integrated_head", + (46, 33), "Round bubbly fish with big eyes and frilly fins", + deny=["ears_round", "ears_long", "ears_floppy", "shell_round"]), + base("base_otter", "aquatic", {"ice": 4, "water": 4, "normal": 2}, "mammal_head", + (49, 30), "Chubby seal/otter pup lying on its flippers", excludes=["eared"], + deny=["shell_round", "tail_fish_fin"]), + base("base_axolotl", "aquatic", {"water": 4, "poison": 3, "dragon": 2, "grass": 1}, "reptile_head", + (51, 30), "Smiling axolotl with feathery gill fronds", excludes=["_head_fins"], + deny=["horns_ram", "shell_round"]), + # serpentine + base("base_noodle", "serpentine", {"poison": 4, "ground": 3, "normal": 2, "grass": 2}, "reptile_head", + (44, 40), "Cute coiled snake with a big round head and a sly smile", + deny=["dorsal_fin"]), + base("base_eel", "serpentine", {"water": 4, "ice": 3, "electric": 2, "dragon": 2}, "reptile_head", + (45, 46), "Rising sea-serpent with fin frills and big eyes", excludes=["finned"], + deny=["horns_ram", "wings_bat", "wings_dragon", "spikes_back", "leaf_cape", "crystal_cluster", "rock_plates", "cloud_puffs", "ghost_tatters", "icicle_spikes"]), + base("base_wyrm", "serpentine", {"dragon": 4, "ghost": 3, "psychic": 1, "ice": 1}, "reptile_head", + (43, 44), "Cool coiled young wyrm with whisker barbels and a proud look", excludes=["horned", "feathered"], + deny=["ears_fin", "crystal_spikes", "spiky_hair", "dorsal_fin"]), + # blob + base("base_blob", "blob", {"normal": 4, "grass": 3, "ice": 3, "electric": 2}, "integrated_head", + (40, 33), "Squishy mochi blob with a curl on top and a big smile", + deny=["horn_single", "horns_ram", "ears_bolt", "ears_fin", "flower_back"]), + base("base_slime", "blob", {"poison": 5, "water": 3, "ghost": 1}, "integrated_head", + (40, 34), "Drippy goo blob with a cheeky tongue-out face", + deny=["ears_floppy", "mushroom_back"]), + base("base_wisp", "blob", {"ghost": 5, "psychic": 3, "fire": 1, "flying": 1}, ["integrated_head", "ghost_head"], + (36, 40), "Floating ghost wisp with a curly tail-end and a spooky-cute grin", excludes=["tailed"], + deny=["ears_floppy", "flower_back"]), + base("base_pebble", "blob", {"rock": 5, "ground": 3, "fighting": 1}, "integrated_head", + (38, 32), "Round rock lump creature with a sturdy little face and stubby feet", + deny=["ears_floppy", "mushroom_back"]), + # insectoid + base("base_beetle", "insectoid", {"bug": 4, "rock": 2, "ground": 2, "poison": 1}, "insect_head", + (42, 30), "Round shiny beetle bug with a horn and stubby legs", excludes=["horned"], + deny=["crystal_spikes", "shell_spiked"]), + base("base_grub", "insectoid", {"bug": 4, "grass": 3, "poison": 3}, "insect_head", + (44, 27), "Plump caterpillar with a big happy face and tufted segments", + deny=["shell_spiked", "coil_spikes"]), + base("base_bee", "insectoid", {"bug": 4, "flying": 2, "fighting": 2, "electric": 1}, "insect_head", + (38, 35), "Fuzzy round bee/bug with tiny wings and antennae", excludes=["winged", "antennae"], + deny=["horns_ram", "horns_dragon", "shell_spiked", "tail_leaf", "tail_stinger", "coil_spikes"]), +] + +# --------------------------------------------------------------------------- head_top +HEAD_TOP = [ + none({"normal": 4, "poison": 2.5, "fighting": 2.5, "water": 2.5, "ghost": 2.5}, 2), + part("ears_pointed", types={"electric": 4, "normal": 3, "fire": 3, "psychic": 3, "grass": 1.5}, other=0.5, + requires=MAMMAL + INTEGRATED, tags=["eared"], desc="Pair of tall pointed ears"), + part("ears_round", types={"normal": 4, "electric": 2, "ground": 2, "ice": 2, "water": 1.5}, other=0.5, + requires=MAMMAL + INTEGRATED, tags=["eared"], desc="Pair of round mouse/bear ears"), + part("ears_long", types={"normal": 3, "psychic": 3, "grass": 2, "flying": 1.5, "ice": 1}, other=0.3, + requires=MAMMAL + INTEGRATED, tags=["eared"], size=(18, 20), z=25, desc="Long rabbit ears (drawn behind the creature)"), + part("ears_floppy", types={"normal": 4, "water": 2, "ground": 1.5, "grass": 1}, other=0.3, + requires=MAMMAL + INTEGRATED, tags=["eared"], size=(26, 12), z=25, desc="Droopy hanging ears (drawn behind the creature)"), + part("ears_bolt", types={"electric": 7}, other=0.05, requires=MAMMAL + INTEGRATED, tags=["eared"], + desc="Zigzag lightning-bolt ears, type-colored tips"), + part("ears_fin", types={"water": 5, "ice": 2, "dragon": 1.5}, other=0.1, requires=ANY_FACE, + tags=["finned", "eared", "_head_fins"], palette="accent", desc="Fin-shaped ear frills"), + part("horn_single", types={"rock": 3, "ground": 3, "bug": 3, "dragon": 2, "fighting": 2, "poison": 2}, other=0.3, + requires=NOT_BEAK, tags=["horned"], palette="accent", size=(10, 14), desc="One forehead horn (rhino/beetle)"), + part("horns_ram", types={"rock": 4, "ground": 4, "fighting": 3, "normal": 1.5}, other=0.2, + requires=NOT_BEAK, tags=["horned", "_side_horns"], palette="accent", size=(26, 14), desc="Curled ram horns"), + part("horns_dragon", types={"dragon": 7, "fire": 2, "ghost": 1}, other=0.1, + requires=NOT_BEAK, tags=["horned"], palette="accent", size=(22, 16), desc="Swept-back dragon horns"), + part("antennae", types={"bug": 7, "electric": 2, "psychic": 1.5, "poison": 1}, other=0.1, + requires=["insect_head", "mammal_head", "integrated_head"], tags=["antennae"], size=(16, 16), + desc="Two thin antennae with tips"), + part("crest_feather", types={"flying": 6, "fire": 2, "fighting": 1.5, "psychic": 1}, other=0.2, + requires=["beak_head", "reptile_head"], tags=["feathered"], palette="accent", desc="Swept feather crest"), + part("crest_flame", types={"fire": 8}, other=0.05, palette="type", desc="Flickering flame on top of head"), + part("leaf_sprout", types={"grass": 8}, other=0.05, palette="type", size=(14, 14), desc="Leaf/sprout growing from head"), + part("flower_bloom", types={"grass": 5, "poison": 2, "psychic": 1.5}, other=0.1, palette="accent", + tags=["floral"], desc="Small flower on head"), + part("mushroom_cap", types={"poison": 4, "grass": 3, "bug": 2, "ghost": 1}, other=0.05, palette="accent", + size=(20, 12), desc="Mushroom cap worn like a hat"), + part("ice_crown", types={"ice": 8}, other=0.05, palette="type", size=(18, 12), desc="Ring of small icicles"), + part("crystal_spikes", types={"rock": 5, "ice": 3, "psychic": 2}, other=0.05, palette="accent", + tags=["crystal"], desc="Cluster of crystal shards"), + part("gem_forehead", types={"psychic": 6, "dragon": 2, "rock": 1.5, "ghost": 1}, other=0.05, palette="accent", + tags=["gem"], size=(8, 8), desc="Gem set in forehead"), + part("spiky_hair", types={"fighting": 3, "electric": 3, "poison": 2, "normal": 1, "fire": 1}, other=0.2, + requires=ANY_FACE, tags=["spiky"], size=(20, 12), desc="Spiky tuft of fur/hair"), + part("headband", types={"fighting": 6, "normal": 0.5}, other=0.02, + requires=["mammal_head", "reptile_head", "rock_head", "ghost_head"], tags=["_headband"], palette="accent", + size=(24, 10), + desc="Tied headband with trailing ends"), + part("wisp_flame", types={"ghost": 6, "fire": 1.5, "psychic": 1}, other=0.02, palette="type", + desc="Ghostly floating flame wisp"), + part("water_spout", types={"water": 5, "ice": 1}, other=0.02, palette="type", desc="Water droplet/fountain crest"), + part("cloud_tuft", types={"flying": 3, "electric": 2, "ice": 1.5}, other=0.05, palette="type", + tags=["fluffy"], size=(20, 12), desc="Puffy cloud tuft"), +] + +# (Faces are baked into the base creatures; there are no head, snout or eyes slots.) + +# --------------------------------------------------------------------------- back +BACK = [ + none({"normal": 5, "fighting": 4, "water": 3, "electric": 3, "psychic": 3, "poison": 3, "ground": 3, + "fire": 3, "ghost": 3, "ice": 2.5, "rock": 2, "grass": 2, "dragon": 1.5, "bug": 1, "flying": 0.5}, 2), + part("wings_feather", ["avian", "biped", "quadruped"], + {"flying": 8, "fire": 2, "normal": 1.5, "psychic": 1.5, "ice": 1}, 0.3, + tags=["winged", "feathered"], palette="accent", size=(44, 30), desc="Full feathered wings, spread"), + part("wings_feather_small", ["avian", "biped", "quadruped", "blob"], + {"flying": 4, "normal": 2, "ice": 2, "psychic": 1, "water": 1}, 0.3, + tags=["winged", "feathered"], palette="accent", size=(30, 14), desc="Tiny stubby wings"), + part("wings_bat", ["biped", "quadruped", "avian", "blob", "serpentine"], + {"ghost": 4, "poison": 4, "dragon": 2, "flying": 2, "psychic": 1}, 0.1, + tags=["winged"], palette="accent", size=(44, 26), desc="Membrane bat wings"), + part("wings_bug", ["insectoid", "biped", "blob"], + {"bug": 6, "grass": 2, "poison": 2, "psychic": 1.5, "electric": 1}, 0.1, + tags=["winged"], palette="accent", size=(40, 28), desc="Translucent veined insect wings"), + part("wings_dragon", ["quadruped", "biped", "serpentine"], {"dragon": 6, "fire": 3, "flying": 2, "rock": 1}, 0.1, + tags=["winged"], palette="accent", size=(48, 32), desc="Large clawed dragon wings"), + part("shell_round", ["quadruped", "biped", "aquatic"], {"water": 5, "ground": 3, "rock": 2, "grass": 1}, 0.2, + tags=["shelled"], palette="accent", size=(40, 26), desc="Domed turtle shell"), + part("shell_spiked", ["quadruped", "biped", "insectoid"], + {"rock": 4, "ground": 3, "poison": 2, "water": 2, "fighting": 1}, 0.1, + tags=["shelled", "spiky"], palette="accent", size=(40, 28), desc="Armored shell with spikes"), + part("spikes_back", ["quadruped", "biped", "serpentine", "aquatic", "blob"], + {"ground": 4, "poison": 3, "rock": 3, "dragon": 2, "ice": 2, "fire": 1.5}, 0.2, + tags=["spiky"], palette="accent", size=(36, 14), desc="Row of spikes along the spine"), + part("sail_fin", ["quadruped", "biped", "serpentine"], {"fire": 4, "dragon": 3, "water": 2, "electric": 1.5}, 0.1, + tags=["finned"], palette="accent", size=(34, 20), desc="Tall dimetrodon-style back sail"), + part("dorsal_fin", ["aquatic", "serpentine", "quadruped"], {"water": 7, "ice": 2, "dragon": 1.5}, 0.05, + tags=["finned"], palette="accent", size=(20, 16), desc="Shark-style dorsal fin"), + part("plant_bulb", ["quadruped", "biped", "blob"], {"grass": 6, "poison": 2}, 0.02, + tags=["leafy"], palette="type", size=(30, 26), desc="Large seed pod/bulb with leaves"), + part("leaf_cape", ["quadruped", "biped", "serpentine", "insectoid"], {"grass": 6, "bug": 2, "flying": 1}, 0.02, + tags=["leafy"], palette="type", size=(38, 20), desc="Overlapping leaves along the back"), + part("flower_back", ["quadruped", "biped", "blob"], {"grass": 5, "poison": 2, "psychic": 1.5}, 0.02, + tags=["floral"], palette="accent", size=(32, 26), desc="Big blooming flower"), + part("mushroom_back", ["quadruped", "insectoid", "blob"], {"grass": 3, "poison": 3, "bug": 3, "ghost": 1}, 0.02, + palette="accent", size=(30, 22), desc="Cluster of mushrooms growing on back"), + part("flame_mane", LEGGED, {"fire": 8, "dragon": 1}, 0.02, + tags=["fiery"], palette="type", size=(36, 28), desc="Mane/collar of fire"), + part("crystal_cluster", ["quadruped", "biped", "blob", "serpentine"], {"rock": 5, "ice": 3, "psychic": 3}, 0.02, + tags=["crystal"], palette="accent", size=(30, 24), desc="Crystals jutting from back"), + part("rock_plates", ["quadruped", "biped", "serpentine"], {"rock": 6, "ground": 3, "fighting": 1}, 0.02, + tags=["armored"], palette="accent", size=(38, 20), desc="Boulder armor plates"), + part("icicle_spikes", ["quadruped", "biped", "aquatic", "avian", "blob"], {"ice": 8, "water": 1}, 0.02, + tags=["spiky"], palette="type", size=(29, 24), desc="Tight fan of icicles rising from the back"), + part("mane_fluffy", LEGGED, {"normal": 4, "fighting": 3, "electric": 2, "fire": 1.5, "ice": 1.5}, 0.2, + tags=["fluffy"], size=(34, 26), desc="Big fluffy fur collar/mane"), + part("coil_spikes", ["quadruped", "biped", "blob", "insectoid"], {"electric": 7, "normal": 0.3}, 0.02, + tags=["electric"], palette="type", size=(26, 24), desc="Chunky coil/battery prongs with a spark"), + part("cloud_puffs", ["blob", "avian", "quadruped", "serpentine"], + {"flying": 4, "ice": 2, "psychic": 2, "electric": 1.5, "normal": 1}, 0.05, + tags=["fluffy"], palette="type", size=(30, 20), desc="Fluffy cloud hugging the back"), + part("ghost_tatters", ["biped", "blob", "serpentine", "avian"], {"ghost": 6, "psychic": 1, "poison": 1}, 0.02, + palette="accent", size=(26, 24), desc="Short ghostly shroud draped from the shoulders"), +] + +# --------------------------------------------------------------------------- tail +TAIL = [ + none({"fighting": 3, "bug": 3, "ghost": 3, "psychic": 2, "normal": 2}, 1.5, plans=ALL), + part("tail_stub", ["quadruped", "biped", "avian"], {"normal": 4, "ground": 2, "ice": 2, "rock": 1.5, "fighting": 1.5}, 0.5, + tags=["tailed"], size=(10, 10), desc="Short round stub tail"), + part("tail_fluffy", LEGGED, {"normal": 4, "ice": 3, "electric": 2, "psychic": 2, "fire": 1.5}, 0.4, + tags=["tailed", "fluffy"], size=(26, 20), desc="Big fluffy fox/squirrel tail"), + part("tail_long_thin", LEGGED, {"psychic": 3, "normal": 2, "poison": 2, "electric": 2, "ghost": 1}, 0.4, + tags=["tailed"], size=(30, 12), desc="Long thin tail with tuft"), + part("tail_curly", LEGGED, {"normal": 3, "psychic": 3, "grass": 1.5, "poison": 1}, 0.3, + tags=["tailed"], size=(16, 16), desc="Spiral curly tail"), + part("tail_flame_tip", ["quadruped", "biped", "serpentine"], {"fire": 8}, 0.05, + tags=["tailed", "fiery"], palette="type", size=(28, 20), desc="Tail ending in a flame"), + part("tail_lightning", LEGGED, {"electric": 8}, 0.05, + tags=["tailed"], palette="type", size=(26, 22), desc="Zigzag lightning-bolt tail"), + part("tail_leaf", ["quadruped", "biped", "insectoid", "serpentine"], {"grass": 7, "bug": 1.5}, 0.05, + tags=["tailed", "leafy"], palette="type", size=(24, 16), desc="Tail ending in a big leaf"), + part("tail_fish_fin", ["aquatic", "serpentine", "quadruped", "biped"], {"water": 7, "ice": 2, "dragon": 1.5}, 0.05, + tags=["tailed", "finned"], palette="accent", size=(22, 22), desc="Forked fish/mermaid tail fin"), + part("tail_club", LEGGED, {"rock": 5, "ground": 4, "fighting": 1.5}, 0.05, + tags=["tailed"], size=(28, 16), desc="Heavy club-ended tail"), + part("tail_spiked", ["quadruped", "biped", "serpentine"], + {"ground": 3, "rock": 3, "dragon": 3, "poison": 2, "ice": 1.5}, 0.1, + tags=["tailed", "spiky"], size=(28, 14), desc="Tail with spikes"), + part("tail_stinger", ["insectoid", "serpentine", "quadruped", "biped"], {"bug": 5, "poison": 5}, 0.05, + tags=["tailed", "stinger"], palette="accent", size=(22, 18), desc="Curved stinger tail"), + part("tail_feather_fan", ["avian", "biped"], {"flying": 7, "fire": 2, "psychic": 1.5, "normal": 1}, 0.1, + tags=["tailed", "feathered"], palette="accent", size=(22, 22), desc="Fanned tail feathers"), + part("tail_wispy", ["blob", "serpentine", "biped", "avian"], {"ghost": 7, "psychic": 1.5, "fire": 1}, 0.05, + tags=["tailed"], size=(24, 16), desc="Fading smoky wisp tail"), + part("tail_ice_shard", ["quadruped", "biped", "aquatic", "serpentine"], {"ice": 8}, 0.05, + tags=["tailed", "crystal"], palette="type", size=(24, 16), desc="Tail ending in an ice shard"), + part("tail_dragon", ["quadruped", "biped", "serpentine"], {"dragon": 6, "fire": 2, "ground": 1.5, "water": 1}, 0.1, + tags=["tailed"], size=(30, 16), desc="Thick tapered tail with small fin"), + part("tail_rattle", ["serpentine"], {"poison": 4, "ground": 4, "normal": 1}, 0.2, + tags=["tailed"], palette="accent", size=(14, 12), desc="Rattlesnake rattle tip"), + part("tail_crystal", ["quadruped", "biped", "serpentine"], {"psychic": 4, "rock": 3, "ice": 2}, 0.05, + tags=["tailed", "crystal"], palette="accent", size=(24, 16), desc="Tail tipped with a crystal"), +] + +# --------------------------------------------------------------------------- aura +_AURA_SIZE = (84, 84) +AURA = [ + none(other=8), + part("aura_embers", types={"fire": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Floating embers"), + part("aura_sparks", types={"electric": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Crackling sparks"), + part("aura_bubbles", types={"water": 3, "poison": 0.5}, other=0, palette="type", size=_AURA_SIZE, desc="Rising bubbles"), + part("aura_leaves", types={"grass": 3, "flying": 0.5, "bug": 0.5}, other=0, palette="type", size=_AURA_SIZE, + desc="Drifting leaves"), + part("aura_snowflakes", types={"ice": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Snowflakes"), + part("aura_wisps", types={"ghost": 3, "psychic": 0.5}, other=0, palette="type", size=_AURA_SIZE, desc="Ghost wisps"), + part("aura_rings", types={"psychic": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Psychic rings/ripples"), + part("aura_dust", types={"ground": 3, "rock": 1.5, "fighting": 1}, other=0, palette="type", size=_AURA_SIZE, + desc="Dust cloud at the feet"), + part("aura_toxic", types={"poison": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Toxic bubbles and drips"), + part("aura_sparkle", types={"dragon": 2, "normal": 1, "psychic": 1, "flying": 0.5}, other=0, palette="type", + size=_AURA_SIZE, desc="Twinkling sparkles"), + part("aura_gusts", types={"flying": 3, "dragon": 0.5}, other=0, palette="type", size=_AURA_SIZE, desc="Wind swirls"), + part("aura_pebbles", types={"rock": 3, "ground": 1, "psychic": 0.5}, other=0, palette="type", size=_AURA_SIZE, + desc="Floating pebbles"), + part("aura_power", types={"fighting": 3}, other=0, palette="type", size=_AURA_SIZE, desc="Power-up energy lines"), + part("aura_spores", types={"bug": 2, "grass": 1.5, "poison": 1.5}, other=0, palette="type", size=_AURA_SIZE, + desc="Floating spores"), +] + +SLOTS = {"body": BODY, "back": BACK, "tail": TAIL, "head_top": HEAD_TOP, "aura": AURA} diff --git a/validate_parts.py b/validate_parts.py new file mode 100755 index 0000000..41ff20f --- /dev/null +++ b/validate_parts.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Validate the part library in parts/. + +Fails on: + - a missing file for any part listed in sprite_parts.py + - any color outside the palette keys, pupil, eye white and marker colors + - any partial alpha + - a missing (or duplicated) required marker for the slot / body type + - any legal composite that triggers "canvas expanded": checked exhaustively per body + via bounding boxes, plus a random sample of real composites + + python validate_parts.py [--sample N] +""" +import random +import sys +from itertools import product +from pathlib import Path + +import numpy as np +from PIL import Image + +import generate_sprite as gs +import sprite_parts as cfg + +PARTS = Path("parts") +ALLOWED = {tuple(v) for v in gs.KEYS.values()} | {gs.PUPIL, gs.EYE_WHITE} | {tuple(v) for v in gs.MARKERS.values()} +MARKER_BY_COLOR = {v: k for k, v in gs.MARKERS.items()} + + +def required_markers(slot, p): + if slot == "body": + return {"pivot", "tail", "back", "aura", "head_top"} + return {"pivot"} + + +def check_file(slot, p): + errs = [] + path = PARTS / slot / f"{p['name']}.png" + if not path.exists(): + return [f"missing file {path}"] + arr = np.array(Image.open(path).convert("RGBA")) + a = arr[:, :, 3] + partial = np.count_nonzero((a > 0) & (a < 255)) + if partial: + errs.append(f"{path}: {partial} partial-alpha pixel(s)") + opaque = arr[a > 0][:, :3] + colors = {tuple(int(v) for v in c) for c in np.unique(opaque, axis=0)} if len(opaque) else set() + bad = colors - ALLOWED + if bad: + errs.append(f"{path}: illegal colors {sorted(gs.rgb_to_hex(c) for c in bad)[:8]}") + found = {} + for c in colors & set(MARKER_BY_COLOR): + n = int(np.count_nonzero(np.all(arr[:, :, :3] == c, axis=-1) & (a > 0))) + found[MARKER_BY_COLOR[c]] = n + req = required_markers(slot, p) + missing = req - set(found) + if missing: + errs.append(f"{path}: missing markers {sorted(missing)}") + extra = set(found) - req + if extra: + errs.append(f"{path}: unexpected markers {sorted(extra)}") + dup = [k for k, n in found.items() if n > 1] + if dup: + errs.append(f"{path}: marker used more than once {sorted(dup)}") + if slot == "aura" and (arr.shape[1], arr.shape[0]) != (84, 84): + errs.append(f"{path}: aura must be 84x84, is {arr.shape[1]}x{arr.shape[0]}") + return errs + + +# ------------------------------------------------------------------ size checks + +def bbox_of(slot, name): + """(anchors, opaque bbox x0,y0,x1,y1 exclusive) of a loaded part.""" + loaded = gs.load_part(str(PARTS), slot, name) + if loaded is None: + return None + img, anch = loaded + bb = img.getbbox() + return anch, bb + + +def exhaustive_extents(): + """For each body, the worst legal composite size, computed from bounding boxes. + + Every variation slot is independent given the body, so the union bbox worst case is + found by taking each side's extreme from a different slot (or one part covering both).""" + problems = [] + worst = {} + for body in cfg.BODY: + plan = body["plans"][0] + b = bbox_of("body", body["name"]) + if b is None: + continue + banch, bbb = b + bpos = (-banch["pivot"][0], -banch["pivot"][1]) + tags = {f"plan:{plan}"} | set(body["tags"]) + excl = set(body["excludes"]) + fx0, fy0, fx1, fy1 = bpos[0] + bbb[0], bpos[1] + bbb[1], bpos[0] + bbb[2], bpos[1] + bbb[3] + opt = {} + for slot in gs.PICK_ORDER[1:]: + lst = [] + for p in cfg.SLOTS[slot]: + if p["name"] == "none" or plan not in p["plans"] or p["name"] in body["deny"]: + continue + if p["requires"] and not set(p["requires"]) & tags: + continue + if set(p["tags"]) & excl or set(p["excludes"]) & tags: + continue + pb = bbox_of(slot, p["name"]) + a = banch.get(slot) + if pb is None or a is None or pb[1] is None: + continue + panch, pbb = pb + piv = panch.get("pivot", (0, 0)) + pos = (bpos[0] + a[0] - piv[0], bpos[1] + a[1] - piv[1]) + lst.append((pos[0] + pbb[0], pos[1] + pbb[1], pos[0] + pbb[2], pos[1] + pbb[3], p["name"])) + if lst: + opt[slot] = lst + + def worst_span(lo_i, hi_i, f_lo, f_hi): + best = (f_hi - f_lo, "") + for s1 in opt: + for p in opt[s1]: # single part both sides + span = max(f_hi, p[hi_i]) - min(f_lo, p[lo_i]) + if span > best[0]: + best = (span, f"{s1}={p[4]}") + for s1, s2 in product(opt, opt): + if s1 == s2: + continue + lo = min(opt[s1], key=lambda p: p[lo_i]) + hi = max(opt[s2], key=lambda p: p[hi_i]) + span = max(f_hi, hi[hi_i]) - min(f_lo, lo[lo_i]) + if span > best[0]: + best = (span, f"{s1}={lo[4]} + {s2}={hi[4]}") + return best + + w, wwhy = worst_span(0, 2, fx0, fx1) + h, hwhy = worst_span(1, 3, fy0, fy1) + worst[body["name"]] = (w, h, wwhy, hwhy) + if w + 2 > gs.CANVAS: + problems.append(f"{body['name']}: worst width {w} > {gs.CANVAS - 2} ({wwhy})") + if h + gs.GROUND_MARGIN + 1 > gs.CANVAS: + problems.append(f"{body['name']}: worst height {h} > {gs.CANVAS - gs.GROUND_MARGIN - 1} ({hwhy})") + return problems, worst + + +def sample_composites(n): + problems = [] + rng = random.Random(12345) + for i in range(n): + ptype = rng.choice(cfg.TYPES) + prng = random.Random(i) + plan, chosen, markings, tags = gs.pick_parts(ptype, prng) + if any(not (PARTS / s / f"{p['name']}.png").exists() for s, p in chosen.items() if p["name"] != "none"): + continue + spr = gs.compose(str(PARTS), chosen, markings, prng) + if max(spr.width + 2, spr.height + gs.GROUND_MARGIN + 1) > gs.CANVAS: + problems.append(f"sample {ptype} seed {i}: {spr.width}x{spr.height} " + f"({', '.join(f'{s}={p['name']}' for s, p in chosen.items())})") + return problems + + +def main(argv): + n = 2000 + if "--sample" in argv: + n = int(argv[argv.index("--sample") + 1]) + errs = [] + for slot, parts in cfg.SLOTS.items(): + for p in parts: + if p["name"] != "none": + errs += check_file(slot, p) + size_probs, worst = exhaustive_extents() + errs += size_probs + errs += sample_composites(n) + for e in errs: + print("FAIL", e) + if "--verbose" in argv: + for b, (w, hh, ww, hw) in sorted(worst.items()): + print(f"{b:16s} worst {w}x{hh} w:{ww} h:{hw}") + total = sum(1 for parts in cfg.SLOTS.values() for p in parts if p["name"] != "none") + present = sum(1 for slot, parts in cfg.SLOTS.items() for p in parts + if p["name"] != "none" and (PARTS / slot / f"{p['name']}.png").exists()) + print(f"{present}/{total} parts present, {len(errs)} problem(s)") + return 1 if errs else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))