diff --git a/assets/characters/aggron-1.png b/assets/characters/aggron-1.png new file mode 100644 index 0000000..5900d16 Binary files /dev/null and b/assets/characters/aggron-1.png differ diff --git a/assets/characters/aggron-2.png b/assets/characters/aggron-2.png new file mode 100644 index 0000000..4d63274 Binary files /dev/null and b/assets/characters/aggron-2.png differ diff --git a/assets/characters/aggron-3.png b/assets/characters/aggron-3.png new file mode 100644 index 0000000..11bbdf2 Binary files /dev/null and b/assets/characters/aggron-3.png differ diff --git a/assets/characters/agumon-1.png b/assets/characters/agumon-1.png new file mode 100644 index 0000000..302d4f2 Binary files /dev/null and b/assets/characters/agumon-1.png differ diff --git a/assets/characters/agumon-2.png b/assets/characters/agumon-2.png new file mode 100644 index 0000000..0968a5f Binary files /dev/null and b/assets/characters/agumon-2.png differ diff --git a/assets/characters/agumon-3.png b/assets/characters/agumon-3.png new file mode 100644 index 0000000..86368a6 Binary files /dev/null and b/assets/characters/agumon-3.png differ diff --git a/assets/characters/alakazam-1.png b/assets/characters/alakazam-1.png new file mode 100644 index 0000000..d1b86b5 Binary files /dev/null and b/assets/characters/alakazam-1.png differ diff --git a/assets/characters/alakazam-2.png b/assets/characters/alakazam-2.png new file mode 100644 index 0000000..5e9db0a Binary files /dev/null and b/assets/characters/alakazam-2.png differ diff --git a/assets/characters/alakazam-3.png b/assets/characters/alakazam-3.png new file mode 100644 index 0000000..1ec94be Binary files /dev/null and b/assets/characters/alakazam-3.png differ diff --git a/assets/characters/blaziken-1.png b/assets/characters/blaziken-1.png new file mode 100644 index 0000000..085e7f2 Binary files /dev/null and b/assets/characters/blaziken-1.png differ diff --git a/assets/characters/blaziken-2.png b/assets/characters/blaziken-2.png new file mode 100644 index 0000000..09fd4f1 Binary files /dev/null and b/assets/characters/blaziken-2.png differ diff --git a/assets/characters/blaziken-3.png b/assets/characters/blaziken-3.png new file mode 100644 index 0000000..6aaa54d Binary files /dev/null and b/assets/characters/blaziken-3.png differ diff --git a/assets/characters/blueeyes-1.png b/assets/characters/blueeyes-1.png new file mode 100644 index 0000000..c983661 Binary files /dev/null and b/assets/characters/blueeyes-1.png differ diff --git a/assets/characters/blueeyes-2.png b/assets/characters/blueeyes-2.png new file mode 100644 index 0000000..7397d86 Binary files /dev/null and b/assets/characters/blueeyes-2.png differ diff --git a/assets/characters/blueeyes-3.png b/assets/characters/blueeyes-3.png new file mode 100644 index 0000000..efb73e4 Binary files /dev/null and b/assets/characters/blueeyes-3.png differ diff --git a/assets/characters/braviary-1.png b/assets/characters/braviary-1.png new file mode 100644 index 0000000..2d921dd Binary files /dev/null and b/assets/characters/braviary-1.png differ diff --git a/assets/characters/braviary-2.png b/assets/characters/braviary-2.png new file mode 100644 index 0000000..5fab350 Binary files /dev/null and b/assets/characters/braviary-2.png differ diff --git a/assets/characters/braviary-3.png b/assets/characters/braviary-3.png new file mode 100644 index 0000000..7a37b10 Binary files /dev/null and b/assets/characters/braviary-3.png differ diff --git a/assets/characters/chandelure-1.png b/assets/characters/chandelure-1.png new file mode 100644 index 0000000..e81b17e Binary files /dev/null and b/assets/characters/chandelure-1.png differ diff --git a/assets/characters/chandelure-2.png b/assets/characters/chandelure-2.png new file mode 100644 index 0000000..31b5506 Binary files /dev/null and b/assets/characters/chandelure-2.png differ diff --git a/assets/characters/chandelure-3.png b/assets/characters/chandelure-3.png new file mode 100644 index 0000000..cc32cbc Binary files /dev/null and b/assets/characters/chandelure-3.png differ diff --git a/assets/characters/charizard-1.png b/assets/characters/charizard-1.png new file mode 100644 index 0000000..2ef005f Binary files /dev/null and b/assets/characters/charizard-1.png differ diff --git a/assets/characters/charizard-2.png b/assets/characters/charizard-2.png new file mode 100644 index 0000000..13cbbdb Binary files /dev/null and b/assets/characters/charizard-2.png differ diff --git a/assets/characters/charizard-3.png b/assets/characters/charizard-3.png new file mode 100644 index 0000000..0c19483 Binary files /dev/null and b/assets/characters/charizard-3.png differ diff --git a/assets/characters/darkmagician-1.png b/assets/characters/darkmagician-1.png new file mode 100644 index 0000000..2140ead Binary files /dev/null and b/assets/characters/darkmagician-1.png differ diff --git a/assets/characters/darkmagician-2.png b/assets/characters/darkmagician-2.png new file mode 100644 index 0000000..81b5724 Binary files /dev/null and b/assets/characters/darkmagician-2.png differ diff --git a/assets/characters/darkmagician-3.png b/assets/characters/darkmagician-3.png new file mode 100644 index 0000000..b29e7c6 Binary files /dev/null and b/assets/characters/darkmagician-3.png differ diff --git a/assets/characters/ditto-1.png b/assets/characters/ditto-1.png new file mode 100644 index 0000000..82c5d52 Binary files /dev/null and b/assets/characters/ditto-1.png differ diff --git a/assets/characters/ditto-2.png b/assets/characters/ditto-2.png new file mode 100644 index 0000000..2ec5d8b Binary files /dev/null and b/assets/characters/ditto-2.png differ diff --git a/assets/characters/ditto-3.png b/assets/characters/ditto-3.png new file mode 100644 index 0000000..639b09a Binary files /dev/null and b/assets/characters/ditto-3.png differ diff --git a/assets/characters/dusknoir-1.png b/assets/characters/dusknoir-1.png new file mode 100644 index 0000000..314a2ae Binary files /dev/null and b/assets/characters/dusknoir-1.png differ diff --git a/assets/characters/dusknoir-2.png b/assets/characters/dusknoir-2.png new file mode 100644 index 0000000..2216887 Binary files /dev/null and b/assets/characters/dusknoir-2.png differ diff --git a/assets/characters/dusknoir-3.png b/assets/characters/dusknoir-3.png new file mode 100644 index 0000000..b888413 Binary files /dev/null and b/assets/characters/dusknoir-3.png differ diff --git a/assets/characters/eevee-1.png b/assets/characters/eevee-1.png new file mode 100644 index 0000000..4b1cabf Binary files /dev/null and b/assets/characters/eevee-1.png differ diff --git a/assets/characters/eevee-2.png b/assets/characters/eevee-2.png new file mode 100644 index 0000000..7d71f44 Binary files /dev/null and b/assets/characters/eevee-2.png differ diff --git a/assets/characters/eevee-3.png b/assets/characters/eevee-3.png new file mode 100644 index 0000000..15f2e7e Binary files /dev/null and b/assets/characters/eevee-3.png differ diff --git a/assets/characters/espeon-1.png b/assets/characters/espeon-1.png new file mode 100644 index 0000000..fe20756 Binary files /dev/null and b/assets/characters/espeon-1.png differ diff --git a/assets/characters/espeon-2.png b/assets/characters/espeon-2.png new file mode 100644 index 0000000..ceb5ef6 Binary files /dev/null and b/assets/characters/espeon-2.png differ diff --git a/assets/characters/espeon-3.png b/assets/characters/espeon-3.png new file mode 100644 index 0000000..59d62b5 Binary files /dev/null and b/assets/characters/espeon-3.png differ diff --git a/assets/characters/gardevoir-1.png b/assets/characters/gardevoir-1.png new file mode 100644 index 0000000..4426201 Binary files /dev/null and b/assets/characters/gardevoir-1.png differ diff --git a/assets/characters/gardevoir-2.png b/assets/characters/gardevoir-2.png new file mode 100644 index 0000000..02bb2a8 Binary files /dev/null and b/assets/characters/gardevoir-2.png differ diff --git a/assets/characters/gardevoir-3.png b/assets/characters/gardevoir-3.png new file mode 100644 index 0000000..829faab Binary files /dev/null and b/assets/characters/gardevoir-3.png differ diff --git a/assets/characters/gengar.png b/assets/characters/gengar.png new file mode 100644 index 0000000..57bd88e Binary files /dev/null and b/assets/characters/gengar.png differ diff --git a/assets/characters/goku.png b/assets/characters/goku.png new file mode 100644 index 0000000..beb8103 Binary files /dev/null and b/assets/characters/goku.png differ diff --git a/assets/characters/greninja.png b/assets/characters/greninja.png new file mode 100644 index 0000000..87cb711 Binary files /dev/null and b/assets/characters/greninja.png differ diff --git a/assets/characters/greymon-1.png b/assets/characters/greymon-1.png new file mode 100644 index 0000000..44b4457 Binary files /dev/null and b/assets/characters/greymon-1.png differ diff --git a/assets/characters/greymon-2.png b/assets/characters/greymon-2.png new file mode 100644 index 0000000..9d395c8 Binary files /dev/null and b/assets/characters/greymon-2.png differ diff --git a/assets/characters/greymon-3.png b/assets/characters/greymon-3.png new file mode 100644 index 0000000..8b70531 Binary files /dev/null and b/assets/characters/greymon-3.png differ diff --git a/assets/characters/index.json b/assets/characters/index.json new file mode 100644 index 0000000..122d39a --- /dev/null +++ b/assets/characters/index.json @@ -0,0 +1,1857 @@ +[ + { + "slug": "charizard-1", + "name": "Charizard", + "file": "charizard-1.png", + "element": [ + "Fire", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "dragon", + "fire", + "wings", + "flame", + "aggressive", + "flagship", + "attack" + ], + "source": "ytsearch1:charizard pokemon", + "credit": "Pokemon" + }, + { + "slug": "charizard-2", + "name": "Charizard", + "file": "charizard-2.png", + "element": [ + "Fire", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "dragon", + "fire", + "wings", + "flame", + "aggressive", + "flagship", + "attack" + ], + "source": "ytsearch1:charizard pokemon", + "credit": "Pokemon" + }, + { + "slug": "charizard-3", + "name": "Charizard", + "file": "charizard-3.png", + "element": [ + "Fire", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "dragon", + "fire", + "wings", + "flame", + "aggressive", + "flagship", + "attack" + ], + "source": "ytsearch1:charizard pokemon", + "credit": "Pokemon" + }, + { + "slug": "pikachu", + "name": "Pikachu", + "file": "pikachu.png", + "element": [ + "Electric" + ], + "tier": "Rare", + "keywords": [ + "electric", + "thunder", + "spark", + "fast", + "speed", + "shock", + "energy", + "probe" + ], + "source": "ytsearch1:pikachu thunderbolt anime", + "credit": "Pokemon" + }, + { + "slug": "mewtwo", + "name": "Mewtwo", + "file": "mewtwo.png", + "element": [ + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "psychic", + "mind", + "recon", + "scan", + "intel", + "osint", + "mystic", + "powerful" + ], + "source": "ytsearch1:mewtwo psychic anime scene", + "credit": "Pokemon" + }, + { + "slug": "gengar", + "name": "Gengar", + "file": "gengar.png", + "element": [ + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "ghost", + "shadow", + "crypto", + "cipher", + "dark", + "stealth", + "haunt" + ], + "source": "ytsearch1:gengar pokemon anime", + "credit": "Pokemon" + }, + { + "slug": "lucario", + "name": "Lucario", + "file": "lucario.png", + "element": [ + "Steel" + ], + "tier": "Epic", + "keywords": [ + "steel", + "armor", + "defense", + "harden", + "blue", + "audit", + "guardian" + ], + "source": "ytsearch1:lucario aura pokemon anime", + "credit": "Pokemon" + }, + { + "slug": "greninja", + "name": "Greninja", + "file": "greninja.png", + "element": [ + "Dark" + ], + "tier": "Rare", + "keywords": [ + "dark", + "exploit", + "pwn", + "stealth", + "ninja", + "attack", + "payload", + "water" + ], + "source": "ytsearch1:greninja pokemon anime", + "credit": "Pokemon" + }, + { + "slug": "goku", + "name": "Goku", + "file": "goku.png", + "element": [ + "Fire", + "Normal" + ], + "tier": "Epic", + "keywords": [ + "fire", + "fight", + "redteam", + "brute", + "power", + "aggressive", + "flagship" + ], + "source": "ytsearch1:goku super saiyan", + "credit": "Dragon Ball" + }, + { + "slug": "gardevoir-1", + "name": "Gardevoir", + "file": "gardevoir-1.png", + "element": [ + "Psychic", + "Fairy" + ], + "tier": "Epic", + "keywords": [ + "psychic", + "elegant", + "guardian", + "embrace", + "protector" + ], + "source": "Gardevoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "gardevoir-2", + "name": "Gardevoir", + "file": "gardevoir-2.png", + "element": [ + "Psychic", + "Fairy" + ], + "tier": "Epic", + "keywords": [ + "psychic", + "elegant", + "guardian", + "embrace", + "protector" + ], + "source": "Gardevoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "gardevoir-3", + "name": "Gardevoir", + "file": "gardevoir-3.png", + "element": [ + "Psychic", + "Fairy" + ], + "tier": "Epic", + "keywords": [ + "psychic", + "elegant", + "guardian", + "embrace", + "protector" + ], + "source": "Gardevoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "snorlax-1", + "name": "Snorlax", + "file": "snorlax-1.png", + "element": [ + "Normal" + ], + "tier": "Uncommon", + "keywords": [ + "heavy", + "sleepy", + "tank", + "bulk", + "immovable" + ], + "source": "Snorlax pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "snorlax-2", + "name": "Snorlax", + "file": "snorlax-2.png", + "element": [ + "Normal" + ], + "tier": "Uncommon", + "keywords": [ + "heavy", + "sleepy", + "tank", + "bulk", + "immovable" + ], + "source": "Snorlax pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "snorlax-3", + "name": "Snorlax", + "file": "snorlax-3.png", + "element": [ + "Normal" + ], + "tier": "Uncommon", + "keywords": [ + "heavy", + "sleepy", + "tank", + "bulk", + "immovable" + ], + "source": "Snorlax pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "eevee-1", + "name": "Eevee", + "file": "eevee-1.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "adaptable", + "versatile", + "cute", + "evolving" + ], + "source": "Eevee pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "eevee-2", + "name": "Eevee", + "file": "eevee-2.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "adaptable", + "versatile", + "cute", + "evolving" + ], + "source": "Eevee pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "eevee-3", + "name": "Eevee", + "file": "eevee-3.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "adaptable", + "versatile", + "cute", + "evolving" + ], + "source": "Eevee pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "ditto-1", + "name": "Ditto", + "file": "ditto-1.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "shapeshifter", + "clone", + "transform", + "copy", + "mimic" + ], + "source": "Ditto pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "ditto-2", + "name": "Ditto", + "file": "ditto-2.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "shapeshifter", + "clone", + "transform", + "copy", + "mimic" + ], + "source": "Ditto pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "ditto-3", + "name": "Ditto", + "file": "ditto-3.png", + "element": [ + "Normal" + ], + "tier": "Common", + "keywords": [ + "shapeshifter", + "clone", + "transform", + "copy", + "mimic" + ], + "source": "Ditto pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "regigigas-1", + "name": "Regigigas", + "file": "regigigas-1.png", + "element": [ + "Normal" + ], + "tier": "Legendary", + "keywords": [ + "titan", + "colossus", + "ancient", + "golem", + "legendary" + ], + "source": "Regigigas pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "regigigas-2", + "name": "Regigigas", + "file": "regigigas-2.png", + "element": [ + "Normal" + ], + "tier": "Legendary", + "keywords": [ + "titan", + "colossus", + "ancient", + "golem", + "legendary" + ], + "source": "Regigigas pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "regigigas-3", + "name": "Regigigas", + "file": "regigigas-3.png", + "element": [ + "Normal" + ], + "tier": "Legendary", + "keywords": [ + "titan", + "colossus", + "ancient", + "golem", + "legendary" + ], + "source": "Regigigas pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "agumon-1", + "name": "Agumon", + "file": "agumon-1.png", + "element": [ + "Normal", + "Fire" + ], + "tier": "Common", + "keywords": [ + "rookie", + "brave", + "dinosaur", + "partner", + "plucky" + ], + "source": "Agumon digimon render", + "credit": "Digimon" + }, + { + "slug": "agumon-2", + "name": "Agumon", + "file": "agumon-2.png", + "element": [ + "Normal", + "Fire" + ], + "tier": "Common", + "keywords": [ + "rookie", + "brave", + "dinosaur", + "partner", + "plucky" + ], + "source": "Agumon digimon render", + "credit": "Digimon" + }, + { + "slug": "agumon-3", + "name": "Agumon", + "file": "agumon-3.png", + "element": [ + "Normal", + "Fire" + ], + "tier": "Common", + "keywords": [ + "rookie", + "brave", + "dinosaur", + "partner", + "plucky" + ], + "source": "Agumon digimon render", + "credit": "Digimon" + }, + { + "slug": "yugi-1", + "name": "Yugi", + "file": "yugi-1.png", + "element": [ + "Normal" + ], + "tier": "Rare", + "keywords": [ + "duelist", + "strategist", + "heart", + "cards", + "hero" + ], + "source": "Yugi Muto Yu-Gi-Oh character art", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "yugi-2", + "name": "Yugi", + "file": "yugi-2.png", + "element": [ + "Normal" + ], + "tier": "Rare", + "keywords": [ + "duelist", + "strategist", + "heart", + "cards", + "hero" + ], + "source": "Yugi Muto Yu-Gi-Oh character art", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "yugi-3", + "name": "Yugi", + "file": "yugi-3.png", + "element": [ + "Normal" + ], + "tier": "Rare", + "keywords": [ + "duelist", + "strategist", + "heart", + "cards", + "hero" + ], + "source": "Yugi Muto Yu-Gi-Oh character art", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "mew-1", + "name": "Mew", + "file": "mew-1.png", + "element": [ + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "mythical", + "origin", + "playful", + "ancestor", + "rare" + ], + "source": "Mew pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "mew-2", + "name": "Mew", + "file": "mew-2.png", + "element": [ + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "mythical", + "origin", + "playful", + "ancestor", + "rare" + ], + "source": "Mew pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "mew-3", + "name": "Mew", + "file": "mew-3.png", + "element": [ + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "mythical", + "origin", + "playful", + "ancestor", + "rare" + ], + "source": "Mew pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "alakazam-1", + "name": "Alakazam", + "file": "alakazam-1.png", + "element": [ + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "intellect", + "genius", + "spoons", + "brain", + "calculate" + ], + "source": "Alakazam pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "alakazam-2", + "name": "Alakazam", + "file": "alakazam-2.png", + "element": [ + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "intellect", + "genius", + "spoons", + "brain", + "calculate" + ], + "source": "Alakazam pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "alakazam-3", + "name": "Alakazam", + "file": "alakazam-3.png", + "element": [ + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "intellect", + "genius", + "spoons", + "brain", + "calculate" + ], + "source": "Alakazam pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "espeon-1", + "name": "Espeon", + "file": "espeon-1.png", + "element": [ + "Psychic" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "elegant", + "sun", + "intuition", + "graceful" + ], + "source": "Espeon pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "espeon-2", + "name": "Espeon", + "file": "espeon-2.png", + "element": [ + "Psychic" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "elegant", + "sun", + "intuition", + "graceful" + ], + "source": "Espeon pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "espeon-3", + "name": "Espeon", + "file": "espeon-3.png", + "element": [ + "Psychic" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "elegant", + "sun", + "intuition", + "graceful" + ], + "source": "Espeon pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "darkmagician-1", + "name": "DarkMagician", + "file": "darkmagician-1.png", + "element": [ + "Psychic", + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "spellcaster", + "arcane", + "sorcerer", + "magic", + "conjure" + ], + "source": "Dark Magician Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "darkmagician-2", + "name": "DarkMagician", + "file": "darkmagician-2.png", + "element": [ + "Psychic", + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "spellcaster", + "arcane", + "sorcerer", + "magic", + "conjure" + ], + "source": "Dark Magician Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "darkmagician-3", + "name": "DarkMagician", + "file": "darkmagician-3.png", + "element": [ + "Psychic", + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "spellcaster", + "arcane", + "sorcerer", + "magic", + "conjure" + ], + "source": "Dark Magician Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "rayquaza-1", + "name": "Rayquaza", + "file": "rayquaza-1.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "sky", + "serpent", + "ozone", + "legendary", + "skybound" + ], + "source": "Rayquaza pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "rayquaza-2", + "name": "Rayquaza", + "file": "rayquaza-2.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "sky", + "serpent", + "ozone", + "legendary", + "skybound" + ], + "source": "Rayquaza pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "rayquaza-3", + "name": "Rayquaza", + "file": "rayquaza-3.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "sky", + "serpent", + "ozone", + "legendary", + "skybound" + ], + "source": "Rayquaza pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "pidgeot-1", + "name": "Pidgeot", + "file": "pidgeot-1.png", + "element": [ + "Flying", + "Normal" + ], + "tier": "Rare", + "keywords": [ + "swift", + "raptor", + "scout", + "recon", + "aerial" + ], + "source": "Pidgeot pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "pidgeot-2", + "name": "Pidgeot", + "file": "pidgeot-2.png", + "element": [ + "Flying", + "Normal" + ], + "tier": "Rare", + "keywords": [ + "swift", + "raptor", + "scout", + "recon", + "aerial" + ], + "source": "Pidgeot pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "pidgeot-3", + "name": "Pidgeot", + "file": "pidgeot-3.png", + "element": [ + "Flying", + "Normal" + ], + "tier": "Rare", + "keywords": [ + "swift", + "raptor", + "scout", + "recon", + "aerial" + ], + "source": "Pidgeot pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "lugia-1", + "name": "Lugia", + "file": "lugia-1.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "guardian", + "sea", + "storm", + "diving", + "legendary" + ], + "source": "Lugia pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "lugia-2", + "name": "Lugia", + "file": "lugia-2.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "guardian", + "sea", + "storm", + "diving", + "legendary" + ], + "source": "Lugia pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "lugia-3", + "name": "Lugia", + "file": "lugia-3.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Legendary", + "keywords": [ + "guardian", + "sea", + "storm", + "diving", + "legendary" + ], + "source": "Lugia pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "blueeyes-1", + "name": "BlueEyes", + "file": "blueeyes-1.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "destruction", + "white", + "legendary", + "burst", + "power" + ], + "source": "Blue-Eyes White Dragon Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "blueeyes-2", + "name": "BlueEyes", + "file": "blueeyes-2.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "destruction", + "white", + "legendary", + "burst", + "power" + ], + "source": "Blue-Eyes White Dragon Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "blueeyes-3", + "name": "BlueEyes", + "file": "blueeyes-3.png", + "element": [ + "Flying", + "Dragon" + ], + "tier": "Legendary", + "keywords": [ + "destruction", + "white", + "legendary", + "burst", + "power" + ], + "source": "Blue-Eyes White Dragon Yu-Gi-Oh artwork", + "credit": "Yu-Gi-Oh" + }, + { + "slug": "metagross-1", + "name": "Metagross", + "file": "metagross-1.png", + "element": [ + "Steel", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "supercomputer", + "armored", + "quad", + "calculating", + "fortress" + ], + "source": "Metagross pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "metagross-2", + "name": "Metagross", + "file": "metagross-2.png", + "element": [ + "Steel", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "supercomputer", + "armored", + "quad", + "calculating", + "fortress" + ], + "source": "Metagross pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "metagross-3", + "name": "Metagross", + "file": "metagross-3.png", + "element": [ + "Steel", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "supercomputer", + "armored", + "quad", + "calculating", + "fortress" + ], + "source": "Metagross pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "aggron-1", + "name": "Aggron", + "file": "aggron-1.png", + "element": [ + "Steel", + "Rock" + ], + "tier": "Rare", + "keywords": [ + "iron", + "fortress", + "defense", + "armored", + "tank" + ], + "source": "Aggron pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "aggron-2", + "name": "Aggron", + "file": "aggron-2.png", + "element": [ + "Steel", + "Rock" + ], + "tier": "Rare", + "keywords": [ + "iron", + "fortress", + "defense", + "armored", + "tank" + ], + "source": "Aggron pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "aggron-3", + "name": "Aggron", + "file": "aggron-3.png", + "element": [ + "Steel", + "Rock" + ], + "tier": "Rare", + "keywords": [ + "iron", + "fortress", + "defense", + "armored", + "tank" + ], + "source": "Aggron pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "metalgreymon-1", + "name": "MetalGreymon", + "file": "metalgreymon-1.png", + "element": [ + "Steel" + ], + "tier": "Epic", + "keywords": [ + "cyborg", + "missiles", + "armored", + "machine", + "upgraded" + ], + "source": "MetalGreymon digimon render", + "credit": "Digimon" + }, + { + "slug": "metalgreymon-2", + "name": "MetalGreymon", + "file": "metalgreymon-2.png", + "element": [ + "Steel" + ], + "tier": "Epic", + "keywords": [ + "cyborg", + "missiles", + "armored", + "machine", + "upgraded" + ], + "source": "MetalGreymon digimon render", + "credit": "Digimon" + }, + { + "slug": "metalgreymon-3", + "name": "MetalGreymon", + "file": "metalgreymon-3.png", + "element": [ + "Steel" + ], + "tier": "Epic", + "keywords": [ + "cyborg", + "missiles", + "armored", + "machine", + "upgraded" + ], + "source": "MetalGreymon digimon render", + "credit": "Digimon" + }, + { + "slug": "dusknoir-1", + "name": "Dusknoir", + "file": "dusknoir-1.png", + "element": [ + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "reaper", + "spectral", + "netherworld", + "grim", + "sentinel" + ], + "source": "Dusknoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "dusknoir-2", + "name": "Dusknoir", + "file": "dusknoir-2.png", + "element": [ + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "reaper", + "spectral", + "netherworld", + "grim", + "sentinel" + ], + "source": "Dusknoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "dusknoir-3", + "name": "Dusknoir", + "file": "dusknoir-3.png", + "element": [ + "Ghost" + ], + "tier": "Epic", + "keywords": [ + "reaper", + "spectral", + "netherworld", + "grim", + "sentinel" + ], + "source": "Dusknoir pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "chandelure-1", + "name": "Chandelure", + "file": "chandelure-1.png", + "element": [ + "Ghost", + "Fire" + ], + "tier": "Rare", + "keywords": [ + "spectral", + "flame", + "haunting", + "lantern", + "eerie" + ], + "source": "Chandelure pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "chandelure-2", + "name": "Chandelure", + "file": "chandelure-2.png", + "element": [ + "Ghost", + "Fire" + ], + "tier": "Rare", + "keywords": [ + "spectral", + "flame", + "haunting", + "lantern", + "eerie" + ], + "source": "Chandelure pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "chandelure-3", + "name": "Chandelure", + "file": "chandelure-3.png", + "element": [ + "Ghost", + "Fire" + ], + "tier": "Rare", + "keywords": [ + "spectral", + "flame", + "haunting", + "lantern", + "eerie" + ], + "source": "Chandelure pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "zapdos-1", + "name": "Zapdos", + "file": "zapdos-1.png", + "element": [ + "Electric", + "Flying" + ], + "tier": "Legendary", + "keywords": [ + "thunder", + "storm", + "legendary", + "charged", + "bolt" + ], + "source": "Zapdos pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "zapdos-2", + "name": "Zapdos", + "file": "zapdos-2.png", + "element": [ + "Electric", + "Flying" + ], + "tier": "Legendary", + "keywords": [ + "thunder", + "storm", + "legendary", + "charged", + "bolt" + ], + "source": "Zapdos pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "zapdos-3", + "name": "Zapdos", + "file": "zapdos-3.png", + "element": [ + "Electric", + "Flying" + ], + "tier": "Legendary", + "keywords": [ + "thunder", + "storm", + "legendary", + "charged", + "bolt" + ], + "source": "Zapdos pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "luxray-1", + "name": "Luxray", + "file": "luxray-1.png", + "element": [ + "Electric" + ], + "tier": "Rare", + "keywords": [ + "vision", + "predator", + "feline", + "hunt", + "piercing" + ], + "source": "Luxray pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "luxray-2", + "name": "Luxray", + "file": "luxray-2.png", + "element": [ + "Electric" + ], + "tier": "Rare", + "keywords": [ + "vision", + "predator", + "feline", + "hunt", + "piercing" + ], + "source": "Luxray pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "luxray-3", + "name": "Luxray", + "file": "luxray-3.png", + "element": [ + "Electric" + ], + "tier": "Rare", + "keywords": [ + "vision", + "predator", + "feline", + "hunt", + "piercing" + ], + "source": "Luxray pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "blaziken-1", + "name": "Blaziken", + "file": "blaziken-1.png", + "element": [ + "Fire" + ], + "tier": "Epic", + "keywords": [ + "blazing", + "martial", + "striker", + "kick", + "fierce" + ], + "source": "Blaziken pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "blaziken-2", + "name": "Blaziken", + "file": "blaziken-2.png", + "element": [ + "Fire" + ], + "tier": "Epic", + "keywords": [ + "blazing", + "martial", + "striker", + "kick", + "fierce" + ], + "source": "Blaziken pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "blaziken-3", + "name": "Blaziken", + "file": "blaziken-3.png", + "element": [ + "Fire" + ], + "tier": "Epic", + "keywords": [ + "blazing", + "martial", + "striker", + "kick", + "fierce" + ], + "source": "Blaziken pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "greymon-1", + "name": "Greymon", + "file": "greymon-1.png", + "element": [ + "Fire" + ], + "tier": "Rare", + "keywords": [ + "dinosaur", + "nova", + "blaze", + "horn", + "champion" + ], + "source": "Greymon digimon render", + "credit": "Digimon" + }, + { + "slug": "greymon-2", + "name": "Greymon", + "file": "greymon-2.png", + "element": [ + "Fire" + ], + "tier": "Rare", + "keywords": [ + "dinosaur", + "nova", + "blaze", + "horn", + "champion" + ], + "source": "Greymon digimon render", + "credit": "Digimon" + }, + { + "slug": "greymon-3", + "name": "Greymon", + "file": "greymon-3.png", + "element": [ + "Fire" + ], + "tier": "Rare", + "keywords": [ + "dinosaur", + "nova", + "blaze", + "horn", + "champion" + ], + "source": "Greymon digimon render", + "credit": "Digimon" + }, + { + "slug": "sigilyph-1", + "name": "Sigilyph", + "file": "sigilyph-1.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "psychic", + "ward", + "guardian", + "mystic", + "scanner" + ], + "source": "Sigilyph pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "sigilyph-2", + "name": "Sigilyph", + "file": "sigilyph-2.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "psychic", + "ward", + "guardian", + "mystic", + "scanner" + ], + "source": "Sigilyph pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "sigilyph-3", + "name": "Sigilyph", + "file": "sigilyph-3.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "psychic", + "ward", + "guardian", + "mystic", + "scanner" + ], + "source": "Sigilyph pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "xatu-1", + "name": "Xatu", + "file": "xatu-1.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "totem", + "watcher", + "mystic", + "sun" + ], + "source": "Xatu pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "xatu-2", + "name": "Xatu", + "file": "xatu-2.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "totem", + "watcher", + "mystic", + "sun" + ], + "source": "Xatu pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "xatu-3", + "name": "Xatu", + "file": "xatu-3.png", + "element": [ + "Psychic", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "foresight", + "totem", + "watcher", + "mystic", + "sun" + ], + "source": "Xatu pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "noctowl-1", + "name": "Noctowl", + "file": "noctowl-1.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "night", + "watcher", + "owl", + "analyst", + "wise" + ], + "source": "Noctowl pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "noctowl-2", + "name": "Noctowl", + "file": "noctowl-2.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "night", + "watcher", + "owl", + "analyst", + "wise" + ], + "source": "Noctowl pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "noctowl-3", + "name": "Noctowl", + "file": "noctowl-3.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "night", + "watcher", + "owl", + "analyst", + "wise" + ], + "source": "Noctowl pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "staraptor-1", + "name": "Staraptor", + "file": "staraptor-1.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "raptor", + "strike", + "swift", + "predator", + "aerial" + ], + "source": "Staraptor pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "staraptor-2", + "name": "Staraptor", + "file": "staraptor-2.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "raptor", + "strike", + "swift", + "predator", + "aerial" + ], + "source": "Staraptor pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "staraptor-3", + "name": "Staraptor", + "file": "staraptor-3.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Rare", + "keywords": [ + "raptor", + "strike", + "swift", + "predator", + "aerial" + ], + "source": "Staraptor pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "braviary-1", + "name": "Braviary", + "file": "braviary-1.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Epic", + "keywords": [ + "eagle", + "valor", + "bold", + "patriot", + "warrior" + ], + "source": "Braviary pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "braviary-2", + "name": "Braviary", + "file": "braviary-2.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Epic", + "keywords": [ + "eagle", + "valor", + "bold", + "patriot", + "warrior" + ], + "source": "Braviary pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "braviary-3", + "name": "Braviary", + "file": "braviary-3.png", + "element": [ + "Normal", + "Flying" + ], + "tier": "Epic", + "keywords": [ + "eagle", + "valor", + "bold", + "patriot", + "warrior" + ], + "source": "Braviary pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "togekiss-1", + "name": "Togekiss", + "file": "togekiss-1.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "blessing", + "peace", + "serene", + "messenger", + "grace" + ], + "source": "Togekiss pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "togekiss-2", + "name": "Togekiss", + "file": "togekiss-2.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "blessing", + "peace", + "serene", + "messenger", + "grace" + ], + "source": "Togekiss pokemon official artwork", + "credit": "Pokemon" + }, + { + "slug": "togekiss-3", + "name": "Togekiss", + "file": "togekiss-3.png", + "element": [ + "Flying", + "Psychic" + ], + "tier": "Epic", + "keywords": [ + "blessing", + "peace", + "serene", + "messenger", + "grace" + ], + "source": "Togekiss pokemon official artwork", + "credit": "Pokemon" + } +] \ No newline at end of file diff --git a/assets/characters/lucario.png b/assets/characters/lucario.png new file mode 100644 index 0000000..8d9f946 Binary files /dev/null and b/assets/characters/lucario.png differ diff --git a/assets/characters/lugia-1.png b/assets/characters/lugia-1.png new file mode 100644 index 0000000..1ade694 Binary files /dev/null and b/assets/characters/lugia-1.png differ diff --git a/assets/characters/lugia-2.png b/assets/characters/lugia-2.png new file mode 100644 index 0000000..eec0e35 Binary files /dev/null and b/assets/characters/lugia-2.png differ diff --git a/assets/characters/lugia-3.png b/assets/characters/lugia-3.png new file mode 100644 index 0000000..4097b9e Binary files /dev/null and b/assets/characters/lugia-3.png differ diff --git a/assets/characters/luxray-1.png b/assets/characters/luxray-1.png new file mode 100644 index 0000000..8062bfd Binary files /dev/null and b/assets/characters/luxray-1.png differ diff --git a/assets/characters/luxray-2.png b/assets/characters/luxray-2.png new file mode 100644 index 0000000..7512663 Binary files /dev/null and b/assets/characters/luxray-2.png differ diff --git a/assets/characters/luxray-3.png b/assets/characters/luxray-3.png new file mode 100644 index 0000000..ac63354 Binary files /dev/null and b/assets/characters/luxray-3.png differ diff --git a/assets/characters/metagross-1.png b/assets/characters/metagross-1.png new file mode 100644 index 0000000..8e84c2a Binary files /dev/null and b/assets/characters/metagross-1.png differ diff --git a/assets/characters/metagross-2.png b/assets/characters/metagross-2.png new file mode 100644 index 0000000..de04fd3 Binary files /dev/null and b/assets/characters/metagross-2.png differ diff --git a/assets/characters/metagross-3.png b/assets/characters/metagross-3.png new file mode 100644 index 0000000..ee9df34 Binary files /dev/null and b/assets/characters/metagross-3.png differ diff --git a/assets/characters/metalgreymon-1.png b/assets/characters/metalgreymon-1.png new file mode 100644 index 0000000..b81b27f Binary files /dev/null and b/assets/characters/metalgreymon-1.png differ diff --git a/assets/characters/metalgreymon-2.png b/assets/characters/metalgreymon-2.png new file mode 100644 index 0000000..1f733ed Binary files /dev/null and b/assets/characters/metalgreymon-2.png differ diff --git a/assets/characters/metalgreymon-3.png b/assets/characters/metalgreymon-3.png new file mode 100644 index 0000000..ca4256f Binary files /dev/null and b/assets/characters/metalgreymon-3.png differ diff --git a/assets/characters/mew-1.png b/assets/characters/mew-1.png new file mode 100644 index 0000000..ee996c1 Binary files /dev/null and b/assets/characters/mew-1.png differ diff --git a/assets/characters/mew-2.png b/assets/characters/mew-2.png new file mode 100644 index 0000000..18b8874 Binary files /dev/null and b/assets/characters/mew-2.png differ diff --git a/assets/characters/mew-3.png b/assets/characters/mew-3.png new file mode 100644 index 0000000..b78acb0 Binary files /dev/null and b/assets/characters/mew-3.png differ diff --git a/assets/characters/mewtwo.png b/assets/characters/mewtwo.png new file mode 100644 index 0000000..2405e70 Binary files /dev/null and b/assets/characters/mewtwo.png differ diff --git a/assets/characters/noctowl-1.png b/assets/characters/noctowl-1.png new file mode 100644 index 0000000..72a5426 Binary files /dev/null and b/assets/characters/noctowl-1.png differ diff --git a/assets/characters/noctowl-2.png b/assets/characters/noctowl-2.png new file mode 100644 index 0000000..11cd21e Binary files /dev/null and b/assets/characters/noctowl-2.png differ diff --git a/assets/characters/noctowl-3.png b/assets/characters/noctowl-3.png new file mode 100644 index 0000000..9cc0a48 Binary files /dev/null and b/assets/characters/noctowl-3.png differ diff --git a/assets/characters/pidgeot-1.png b/assets/characters/pidgeot-1.png new file mode 100644 index 0000000..601fac8 Binary files /dev/null and b/assets/characters/pidgeot-1.png differ diff --git a/assets/characters/pidgeot-2.png b/assets/characters/pidgeot-2.png new file mode 100644 index 0000000..9da3226 Binary files /dev/null and b/assets/characters/pidgeot-2.png differ diff --git a/assets/characters/pidgeot-3.png b/assets/characters/pidgeot-3.png new file mode 100644 index 0000000..647a5a0 Binary files /dev/null and b/assets/characters/pidgeot-3.png differ diff --git a/assets/characters/pikachu.png b/assets/characters/pikachu.png new file mode 100644 index 0000000..867d0a1 Binary files /dev/null and b/assets/characters/pikachu.png differ diff --git a/assets/characters/rayquaza-1.png b/assets/characters/rayquaza-1.png new file mode 100644 index 0000000..8e7df67 Binary files /dev/null and b/assets/characters/rayquaza-1.png differ diff --git a/assets/characters/rayquaza-2.png b/assets/characters/rayquaza-2.png new file mode 100644 index 0000000..e67ee09 Binary files /dev/null and b/assets/characters/rayquaza-2.png differ diff --git a/assets/characters/rayquaza-3.png b/assets/characters/rayquaza-3.png new file mode 100644 index 0000000..a60f8b1 Binary files /dev/null and b/assets/characters/rayquaza-3.png differ diff --git a/assets/characters/regigigas-1.png b/assets/characters/regigigas-1.png new file mode 100644 index 0000000..e24e4c7 Binary files /dev/null and b/assets/characters/regigigas-1.png differ diff --git a/assets/characters/regigigas-2.png b/assets/characters/regigigas-2.png new file mode 100644 index 0000000..08168b2 Binary files /dev/null and b/assets/characters/regigigas-2.png differ diff --git a/assets/characters/regigigas-3.png b/assets/characters/regigigas-3.png new file mode 100644 index 0000000..c497c7a Binary files /dev/null and b/assets/characters/regigigas-3.png differ diff --git a/assets/characters/sigilyph-1.png b/assets/characters/sigilyph-1.png new file mode 100644 index 0000000..7821e97 Binary files /dev/null and b/assets/characters/sigilyph-1.png differ diff --git a/assets/characters/sigilyph-2.png b/assets/characters/sigilyph-2.png new file mode 100644 index 0000000..20f1c1d Binary files /dev/null and b/assets/characters/sigilyph-2.png differ diff --git a/assets/characters/sigilyph-3.png b/assets/characters/sigilyph-3.png new file mode 100644 index 0000000..4e98c72 Binary files /dev/null and b/assets/characters/sigilyph-3.png differ diff --git a/assets/characters/snorlax-1.png b/assets/characters/snorlax-1.png new file mode 100644 index 0000000..ba083e2 Binary files /dev/null and b/assets/characters/snorlax-1.png differ diff --git a/assets/characters/snorlax-2.png b/assets/characters/snorlax-2.png new file mode 100644 index 0000000..54fae91 Binary files /dev/null and b/assets/characters/snorlax-2.png differ diff --git a/assets/characters/snorlax-3.png b/assets/characters/snorlax-3.png new file mode 100644 index 0000000..c4cdae0 Binary files /dev/null and b/assets/characters/snorlax-3.png differ diff --git a/assets/characters/staraptor-1.png b/assets/characters/staraptor-1.png new file mode 100644 index 0000000..fc726cf Binary files /dev/null and b/assets/characters/staraptor-1.png differ diff --git a/assets/characters/staraptor-2.png b/assets/characters/staraptor-2.png new file mode 100644 index 0000000..d00a3dc Binary files /dev/null and b/assets/characters/staraptor-2.png differ diff --git a/assets/characters/staraptor-3.png b/assets/characters/staraptor-3.png new file mode 100644 index 0000000..f8e827a Binary files /dev/null and b/assets/characters/staraptor-3.png differ diff --git a/assets/characters/togekiss-1.png b/assets/characters/togekiss-1.png new file mode 100644 index 0000000..1cf036c Binary files /dev/null and b/assets/characters/togekiss-1.png differ diff --git a/assets/characters/togekiss-2.png b/assets/characters/togekiss-2.png new file mode 100644 index 0000000..97ab031 Binary files /dev/null and b/assets/characters/togekiss-2.png differ diff --git a/assets/characters/togekiss-3.png b/assets/characters/togekiss-3.png new file mode 100644 index 0000000..b24a7ba Binary files /dev/null and b/assets/characters/togekiss-3.png differ diff --git a/assets/characters/xatu-1.png b/assets/characters/xatu-1.png new file mode 100644 index 0000000..284e57e Binary files /dev/null and b/assets/characters/xatu-1.png differ diff --git a/assets/characters/xatu-2.png b/assets/characters/xatu-2.png new file mode 100644 index 0000000..b03c9a7 Binary files /dev/null and b/assets/characters/xatu-2.png differ diff --git a/assets/characters/xatu-3.png b/assets/characters/xatu-3.png new file mode 100644 index 0000000..069cc9b Binary files /dev/null and b/assets/characters/xatu-3.png differ diff --git a/assets/characters/yugi-1.png b/assets/characters/yugi-1.png new file mode 100644 index 0000000..088675f Binary files /dev/null and b/assets/characters/yugi-1.png differ diff --git a/assets/characters/yugi-2.png b/assets/characters/yugi-2.png new file mode 100644 index 0000000..ed06b83 Binary files /dev/null and b/assets/characters/yugi-2.png differ diff --git a/assets/characters/yugi-3.png b/assets/characters/yugi-3.png new file mode 100644 index 0000000..9027de3 Binary files /dev/null and b/assets/characters/yugi-3.png differ diff --git a/assets/characters/zapdos-1.png b/assets/characters/zapdos-1.png new file mode 100644 index 0000000..86a1a14 Binary files /dev/null and b/assets/characters/zapdos-1.png differ diff --git a/assets/characters/zapdos-2.png b/assets/characters/zapdos-2.png new file mode 100644 index 0000000..c791755 Binary files /dev/null and b/assets/characters/zapdos-2.png differ diff --git a/assets/characters/zapdos-3.png b/assets/characters/zapdos-3.png new file mode 100644 index 0000000..317ca88 Binary files /dev/null and b/assets/characters/zapdos-3.png differ diff --git a/cmd_chat/chardex.py b/cmd_chat/chardex.py new file mode 100644 index 0000000..17e763c --- /dev/null +++ b/cmd_chat/chardex.py @@ -0,0 +1,188 @@ +"""hh-chardex — a library of harvested character portraits + a VM→character matcher. + +The cardex (`cardex.py`) judges a VM's value and assigns it an elemental *type* +and a *purpose*. This module gives each VM a **face**: it indexes a library of +real, auto-cropped character portraits (harvested by `scripts/charvest.py` from +YouTube / Instagram / image sources) and picks the one whose element + keywords +best fit the VM — deterministically, so a VM always wears the same character. + +Pure stdlib so `cardimg` can import it offline. The library lives under +`assets/characters/` (override with `$HH_CHARDEX_DIR`): + + assets/characters/ + index.json # [{slug,name,element,tier,keywords,source,credit,file}] + .png # the cropped square portrait + +Matching is a transparent additive score (type overlap dominates, then keyword +overlap with the VM's tags + purpose, then a rarity/tier affinity), with a +hash-of-label tie-break so picks are stable and well-distributed. No model, +no network. +""" + +from __future__ import annotations + +import hashlib +import json +import os +from dataclasses import dataclass, field +from pathlib import Path + + +def chardex_dir() -> Path: + env = os.environ.get("HH_CHARDEX_DIR") + if env: + return Path(env) + return Path(__file__).resolve().parent.parent / "assets" / "characters" + + +@dataclass +class Character: + slug: str + name: str + file: str # portrait filename within the library dir + element: list = field(default_factory=list) # card types this fits, e.g. ["Fire","Dragon"] + tier: str = "" # optional rarity affinity (Common..Legendary) + keywords: list = field(default_factory=list) # descriptive words for purpose-match + source: str = "" # acquisition URL + credit: str = "" # attribution / franchise + + def path(self, base: Path) -> Path: + return base / self.file + + +def load_library(base: Path | None = None) -> list[Character]: + base = base or chardex_dir() + idx = base / "index.json" + if not idx.exists(): + return [] + raw = json.loads(idx.read_text()) + out = [] + for e in raw: + out.append(Character( + slug=e["slug"], name=e.get("name", e["slug"]), + file=e.get("file", e["slug"] + ".png"), + element=e.get("element") or [], tier=e.get("tier", ""), + keywords=[k.lower() for k in (e.get("keywords") or [])], + source=e.get("source", ""), credit=e.get("credit", ""))) + return out + + +def save_library(chars: list[Character], base: Path | None = None) -> Path: + base = base or chardex_dir() + base.mkdir(parents=True, exist_ok=True) + idx = base / "index.json" + idx.write_text(json.dumps([{ + "slug": c.slug, "name": c.name, "file": c.file, "element": c.element, + "tier": c.tier, "keywords": c.keywords, "source": c.source, + "credit": c.credit} for c in chars], indent=2)) + return idx + + +_TIER_RANK = {"Common": 0, "Uncommon": 1, "Rare": 2, "Epic": 3, "Legendary": 4} +_STOP = {"a", "an", "the", "for", "and", "of", "to", "vm", "with", "via", "on", + "shared", "dev", "sandbox", "unknown", "from", "into", "kit", "lab", + # rarity / flavor-boilerplate words — rarity is handled by tier affinity, + # not keyword overlap, so these must not drive the character match. + "legendary", "epic", "rare", "uncommon", "common", "mythical", + "born", "base", "stat", "total", "type"} + + +def _purpose_words(purpose: str) -> set[str]: + toks = "".join(c if c.isalnum() else " " for c in (purpose or "").lower()).split() + return {t for t in toks if len(t) > 2 and t not in _STOP} + + +def _seed(s: str) -> int: + return int(hashlib.sha256(s.encode()).hexdigest(), 16) + + +def score(card, ch: Character) -> float: + """Additive, transparent fit score between a cardex Card and a Character.""" + s = 0.0 + types = set(getattr(card, "types", []) or []) + elem = set(ch.element) + matched = len(types & elem) + # elemental fit dominates, but with diminishing returns on extra matches so a + # strong single-type face can still compete (via jitter) with a dual-type one + # — otherwise every same-typed VM collapses onto the one dual-type character. + if matched: + s += 6.0 + 3.0 * (matched - 1) # 1→6.0, 2→9.0, 3→12.0 + # keyword overlap with the VM's tags + purpose prose (capped so purpose fit + # nudges but never overrides elemental fit) + vocab = {t.lower() for t in (getattr(card, "label", "").replace("-", " ").split())} + vocab |= _purpose_words(getattr(card, "flavor", "")) + # cardex Card has no raw tags, but flavor embeds purpose; also use name root + kw = set(ch.keywords) + s += min(1.5 * len(kw & vocab), 3.0) + # rarity / tier affinity — a gentle nudge so legendary characters lean toward + # legendary VMs, kept small enough that the per-VM jitter still spreads picks + # across same-type faces (see match()) instead of collapsing onto one. + if ch.tier and getattr(card, "rarity", ""): + dist = abs(_TIER_RANK.get(ch.tier, 2) - _TIER_RANK.get(card.rarity, 2)) + s += max(0.0, 1.5 - 0.75 * dist) + return s + + +# Jitter folded INTO the score so that among characters of the *same* element +# (equal 6.0-per-type base) the winner varies per VM instead of every same-type +# VM collapsing onto the single highest-tier character. Kept below JITTER_MAX < +# one type-match unit (6.0) so a better *elemental* fit always still wins. +JITTER_MAX = 4.0 + + +def match(card, library: list[Character] | None = None) -> Character | None: + """Pick the best-fitting character for a VM card; stable per VM label. + + Elemental fit dominates; within an element, a per-(VM,character) hash jitter + spreads picks across all equally-fitting faces so a common VM type doesn't + reuse one portrait. Deterministic — a given VM always wears the same face.""" + lib = library if library is not None else load_library() + if not lib: + return None + label = getattr(card, "label", "vm") + best, best_key = None, None + for ch in lib: + sc = score(card, ch) + jitter = JITTER_MAX * ((_seed(label + "\x00" + ch.slug) % 10000) / 10000.0) + key = sc + jitter + if best_key is None or key > best_key: + best, best_key = ch, key + return best + + +# ── CLI: inspect the library / preview matches ──────────────────────────────── +def main(argv=None) -> int: + import argparse + from .cardex import card_from_entry, load_entries, _registry_path + + p = argparse.ArgumentParser(prog="hh-chardex", + description="Inspect the character library and preview VM→character matches.") + p.add_argument("--dir", default=str(chardex_dir()), help="character library dir") + p.add_argument("--registry", default=str(_registry_path())) + p.add_argument("--matches", action="store_true", + help="show the matched character for every VM in the registry") + args = p.parse_args(argv) + + base = Path(args.dir) + lib = load_library(base) + if not lib: + print(f"empty character library at {base} — run scripts/charvest.py first") + return 1 + if not args.matches: + print(f"{len(lib)} characters in {base}:") + for c in lib: + print(f" {c.slug:<22} {c.name:<20} {'/'.join(c.element):<16} " + f"{c.tier:<10} {' '.join(c.keywords[:6])}") + return 0 + + entries = load_entries(Path(args.registry)) + for e in entries: + card = card_from_entry(e) + ch = match(card, lib) + print(f"{card.label:<22} {card.rarity:<10} {'/'.join(card.types):<16} " + f"→ {ch.name if ch else '(none)'}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/charvest.py b/scripts/charvest.py new file mode 100644 index 0000000..3414a06 --- /dev/null +++ b/scripts/charvest.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""charvest — harvest + auto-crop iconic character portraits for the hh card art. + +Pipeline (all local, free): + + source ──▶ frames ──▶ auto-crop subject ──▶ quality filter ──▶ library + (URL/video/ (cv2: anime+face (blur + near-dup + image/dir) (ffmpeg cascades, else rejection) + scene) edge-energy saliency) + +Acquisition reuses the `web-video-grab` skill (yt-dlp first, Playwright screen- +capture fallback) so YouTube / Instagram / X / TikTok all work; a local file or +a directory of images is taken as-is. Each kept portrait is squared, upscaled +and written into the chardex library (`assets/characters/`) with the metadata +you supply, so `cmd_chat/chardex.py` can match VMs to faces. + +Run with the anaconda interpreter (has cv2 + numpy): + + python3 scripts/charvest.py URL_OR_PATH --name "Charizard" \ + --element Fire Dragon --tier Legendary \ + --keywords dragon fire wings aggressive flagship --max 3 + +⚠️ Harvested characters are likely third-party IP — see + docs/character-art-licensing.md before any public/commercial use. +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.request +from pathlib import Path + +import cv2 +import numpy as np + +REPO = Path(__file__).resolve().parent.parent +LIB = Path(os.environ.get("HH_CHARDEX_DIR", REPO / "assets" / "characters")) +GRAB = Path.home() / ".claude" / "skills" / "web-video-grab" / "grab.sh" +CACHE = Path.home() / ".cache" / "hh-charvest" +ANIMEFACE_URL = ("https://raw.githubusercontent.com/nagadomi/" + "lbpcascade_animeface/master/lbpcascade_animeface.xml") +OUT_PX = 640 +ACQUIRE_SECONDS = 30 +IMG_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp"} +VID_EXT = {".mp4", ".mkv", ".webm", ".mov", ".avi", ".gif"} + + +# ── acquisition ─────────────────────────────────────────────────────────────── +def acquire(src: str, work: Path) -> tuple[str, Path]: + """Return ('image'|'video'|'dir', path). URLs are fetched to a clean mp4.""" + p = Path(src) + if p.exists(): + if p.is_dir(): + return "dir", p + ext = p.suffix.lower() + if ext in IMG_EXT: + return "image", p + if ext in VID_EXT: + return "video", p + raise SystemExit(f"unsupported local file type: {ext}") + # remote URL → grab just a short, low-res section fast via yt-dlp (no full + # download, no Playwright). Falls back to the web-video-grab skill if needed. + out = work / "grab.mp4" + print(f"[acquire] yt-dlp (≤{ACQUIRE_SECONDS}s clip) ← {src}") + r = subprocess.run( + ["yt-dlp", "--no-playlist", "--no-warnings", + "-f", "bv*[height<=720][ext=mp4]/bv*[height<=720]/b[height<=720]/best", + "--download-sections", f"*0:00-0:{ACQUIRE_SECONDS:02d}", + "-o", str(out), src], + timeout=150) + if r.returncode == 0 and out.exists(): + return "video", out + if GRAB.exists(): + print("[acquire] yt-dlp failed; web-video-grab fallback", file=sys.stderr) + r = subprocess.run(["bash", str(GRAB), "--out", str(out), src], timeout=180) + if r.returncode == 0 and out.exists(): + return "video", out + raise SystemExit(f"could not acquire media from {src}") + + +def extract_frames(video: Path, outdir: Path, scene: float, fps_cap: float, + max_frames: int) -> list[Path]: + """ffmpeg: prefer scene-change frames; fall back to a steady fps sample.""" + outdir.mkdir(parents=True, exist_ok=True) + vf = f"select='gt(scene,{scene})',scale=720:-2" + subprocess.run(["ffmpeg", "-y", "-i", str(video), "-vf", vf, "-vsync", "vfr", + "-frames:v", str(max_frames * 3), str(outdir / "s%04d.png")], + stderr=subprocess.DEVNULL) + frames = sorted(outdir.glob("s*.png")) + if len(frames) < 3: # sparse scene cuts → steady sample + subprocess.run(["ffmpeg", "-y", "-i", str(video), "-vf", + f"fps={fps_cap},scale=720:-2", "-frames:v", + str(max_frames * 3), str(outdir / "f%04d.png")], + stderr=subprocess.DEVNULL) + frames = sorted(outdir.glob("[sf]*.png")) + return frames + + +# ── subject detection + cropping ────────────────────────────────────────────── +def _animeface_cascade(): + CACHE.mkdir(parents=True, exist_ok=True) + dst = CACHE / "lbpcascade_animeface.xml" + if not dst.exists(): + try: + urllib.request.urlretrieve(ANIMEFACE_URL, dst) + except Exception as e: + print(f"[crop] anime cascade unavailable ({e}); faces only", file=sys.stderr) + return None + c = cv2.CascadeClassifier(str(dst)) + return None if c.empty() else c + + +_FRONTAL = cv2.CascadeClassifier( + cv2.data.haarcascades + "haarcascade_frontalface_default.xml") +_ANIME = _animeface_cascade() + + +def _face_bbox(gray: np.ndarray): + """Largest face (anime cascade first, then frontal); None if no face.""" + best = None + for cas, mn in ((_ANIME, 24), (_FRONTAL, 30)): + if cas is None: + continue + for (x, y, w, h) in cas.detectMultiScale(gray, 1.1, 4, minSize=(mn, mn)): + if best is None or w * h > best[2] * best[3]: + best = (x, y, w, h) + if best is not None: + break + return best + + +def _saliency_bbox(gray: np.ndarray): + """Edge-energy fallback for creatures/objects with no detectable face. + + Sobel magnitude → blur to an energy map → threshold at mean+std → bounding + box of the largest salient blob.""" + gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3) + gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3) + mag = cv2.magnitude(gx, gy) + mag = cv2.GaussianBlur(mag, (0, 0), sigmaX=gray.shape[1] / 40.0) + thr = mag.mean() + mag.std() + mask = (mag > thr).astype(np.uint8) * 255 + mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, + cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))) + cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not cnts: + return None + return cv2.boundingRect(max(cnts, key=cv2.contourArea)) + + +def _square_from_face(bbox, W, H): + x, y, w, h = bbox + cx = x + w / 2 + side = min(max(w * 2.4, h * 2.4), min(W, H)) # head+shoulders portrait + top = y - h * 0.9 # leave headroom above face + x0 = int(np.clip(cx - side / 2, 0, W - side)) + y0 = int(np.clip(top, 0, H - side)) + return x0, y0, int(side) + + +def _square_from_bbox(bbox, W, H, pad=0.18): + x, y, w, h = bbox + cx, cy = x + w / 2, y + h / 2 + if h > w * 1.25: + # tall standing figure (full-body art): frame head + upper body from the + # top instead of centering on the waist, so the face is always in shot. + side = float(np.clip(max(w * 1.5, h * 0.55), 0.42 * min(W, H), min(W, H))) + y0 = int(np.clip(y - side * 0.06, 0, H - side)) + else: + side = min(max(w, h) * (1 + pad), min(W, H)) + y0 = int(np.clip(cy - side / 2, 0, H - side)) + x0 = int(np.clip(cx - side / 2, 0, W - side)) + return x0, y0, int(side) + + +def autocrop(img: np.ndarray): + """Return a squared OUT_PX×OUT_PX portrait centered on the subject, or None.""" + H, W = img.shape[:2] + if min(H, W) < 120: + return None + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + face = _face_bbox(gray) + if face is not None: + x0, y0, side = _square_from_face(face, W, H) + else: + bb = _saliency_bbox(gray) + if bb is None: + return None + x0, y0, side = _square_from_bbox(bb, W, H) + if side < 100: + return None + crop = img[y0:y0 + side, x0:x0 + side] + crop = cv2.resize(crop, (OUT_PX, OUT_PX), interpolation=cv2.INTER_LANCZOS4) + # gentle unsharp for upscaled frames + blur = cv2.GaussianBlur(crop, (0, 0), 1.2) + return cv2.addWeighted(crop, 1.4, blur, -0.4, 0) + + +# ── quality filters ─────────────────────────────────────────────────────────── +def blur_score(img: np.ndarray) -> float: + return cv2.Laplacian(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cv2.CV_64F).var() + + +def ahash(img: np.ndarray) -> int: + g = cv2.resize(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), (8, 8)) + bits = (g > g.mean()).flatten() + h = 0 + for b in bits: + h = (h << 1) | int(b) + return h + + +def _hamming(a: int, b: int) -> int: + return bin(a ^ b).count("1") + + +# ── library write ───────────────────────────────────────────────────────────── +def _slug(name: str) -> str: + s = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") + return s or "char" + + +def append_library(name, element, tier, keywords, source, credit, + portraits: list[np.ndarray]) -> list[str]: + LIB.mkdir(parents=True, exist_ok=True) + idx_path = LIB / "index.json" + index = json.loads(idx_path.read_text()) if idx_path.exists() else [] + base = _slug(name) + written = [] + for i, port in enumerate(portraits): + slug = base if len(portraits) == 1 else f"{base}-{i+1}" + index = [e for e in index if e["slug"] != slug] # replace same slug + fn = f"{slug}.png" + cv2.imwrite(str(LIB / fn), port, [cv2.IMWRITE_PNG_COMPRESSION, 6]) + index.append({"slug": slug, "name": name, "file": fn, + "element": element, "tier": tier or "", + "keywords": keywords, "source": source, "credit": credit}) + written.append(slug) + idx_path.write_text(json.dumps(index, indent=2)) + return written + + +def harvest(args) -> int: + with tempfile.TemporaryDirectory(prefix="charvest-") as td: + work = Path(td) + kind, path = acquire(args.url, work) + if kind == "image": + frames = [path] + elif kind == "dir": + frames = sorted(p for p in path.iterdir() if p.suffix.lower() in IMG_EXT) + else: + frames = extract_frames(path, work / "frames", args.scene, + args.fps, args.max) + print(f"[crop] {len(frames)} candidate frame(s)") + + scored: list[tuple[float, int, np.ndarray]] = [] + seen: list[int] = [] + for fp in frames: + img = cv2.imread(str(fp)) + if img is None: + continue + port = autocrop(img) + if port is None: + continue + bs = blur_score(port) + if bs < args.min_sharpness: + continue + hh = ahash(port) + if any(_hamming(hh, s) <= 6 for s in seen): # near-duplicate + continue + seen.append(hh) + scored.append((bs, len(scored), port)) + + if not scored: + print("[crop] no usable portraits (no subject detected / too blurry)", + file=sys.stderr) + return 1 + scored.sort(key=lambda t: -t[0]) # sharpest first + picks = [p for _, _, p in scored[:args.max]] + slugs = append_library(args.name, args.element, args.tier, + [k.lower() for k in args.keywords], + args.url, args.credit, picks) + print(f"[done] {len(slugs)} portrait(s) → {LIB}: {', '.join(slugs)}") + return 0 + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(prog="charvest", description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("url", help="YouTube/Instagram/web URL, or a local video/image/dir") + p.add_argument("--name", required=True, help="character name (e.g. 'Charizard')") + p.add_argument("--element", nargs="*", default=[], + help="card type(s) it fits: Fire Dragon Psychic Steel …") + p.add_argument("--tier", default="", help="rarity affinity (Common..Legendary)") + p.add_argument("--keywords", nargs="*", default=[], + help="descriptive words used to match VM purpose") + p.add_argument("--credit", default="", help="franchise / attribution") + p.add_argument("--max", type=int, default=3, help="max portraits to keep") + p.add_argument("--scene", type=float, default=0.30, help="ffmpeg scene-cut threshold") + p.add_argument("--fps", type=float, default=0.5, help="fallback sample fps") + p.add_argument("--min-sharpness", type=float, default=40.0, + help="reject portraits below this Laplacian variance") + return harvest(p.parse_args(argv)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/harvest_roster.sh b/scripts/harvest_roster.sh new file mode 100644 index 0000000..9fd4822 --- /dev/null +++ b/scripts/harvest_roster.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Batch-harvest a genre-diverse character roster for the hh card-art library. +# Covers the card types the live VM registry actually uses (Normal/Psychic/ +# Flying/Steel/Ghost/Electric/Fire) with MULTIPLE distinct faces per type so +# cards stop sharing a portrait. Mixes Pokémon, Digimon and Yu-Gi-Oh. +set -u +cd "$(dirname "$0")/.." +PY=python3 + +# NAME | QUERY | ELEMENTS | TIER | KEYWORDS | CREDIT +ROSTER=$(cat <<'EOF' +Snorlax|Snorlax pokemon official artwork|Normal|Uncommon|heavy sleepy tank bulk immovable|Pokemon +Eevee|Eevee pokemon official artwork|Normal|Common|adaptable versatile cute evolving|Pokemon +Ditto|Ditto pokemon official artwork|Normal|Common|shapeshifter clone transform copy mimic|Pokemon +Regigigas|Regigigas pokemon official artwork|Normal|Legendary|titan colossus ancient golem legendary|Pokemon +Agumon|Agumon digimon render|Normal Fire|Common|rookie brave dinosaur partner plucky|Digimon +Yugi|Yugi Muto Yu-Gi-Oh character art|Normal|Rare|duelist strategist heart cards hero|Yu-Gi-Oh +Mew|Mew pokemon official artwork|Psychic|Legendary|mythical origin playful ancestor rare|Pokemon +Alakazam|Alakazam pokemon official artwork|Psychic|Epic|intellect genius spoons brain calculate|Pokemon +Espeon|Espeon pokemon official artwork|Psychic|Rare|foresight elegant sun intuition graceful|Pokemon +DarkMagician|Dark Magician Yu-Gi-Oh artwork|Psychic Ghost|Epic|spellcaster arcane sorcerer magic conjure|Yu-Gi-Oh +Rayquaza|Rayquaza pokemon official artwork|Flying Dragon|Legendary|sky serpent ozone legendary skybound|Pokemon +Pidgeot|Pidgeot pokemon official artwork|Flying Normal|Rare|swift raptor scout recon aerial|Pokemon +Lugia|Lugia pokemon official artwork|Flying Psychic|Legendary|guardian sea storm diving legendary|Pokemon +BlueEyes|Blue-Eyes White Dragon Yu-Gi-Oh artwork|Flying Dragon|Legendary|destruction white legendary burst power|Yu-Gi-Oh +Metagross|Metagross pokemon official artwork|Steel Psychic|Epic|supercomputer armored quad calculating fortress|Pokemon +Aggron|Aggron pokemon official artwork|Steel Rock|Rare|iron fortress defense armored tank|Pokemon +MetalGreymon|MetalGreymon digimon render|Steel|Epic|cyborg missiles armored machine upgraded|Digimon +Dusknoir|Dusknoir pokemon official artwork|Ghost|Epic|reaper spectral netherworld grim sentinel|Pokemon +Chandelure|Chandelure pokemon official artwork|Ghost Fire|Rare|spectral flame haunting lantern eerie|Pokemon +Zapdos|Zapdos pokemon official artwork|Electric Flying|Legendary|thunder storm legendary charged bolt|Pokemon +Luxray|Luxray pokemon official artwork|Electric|Rare|vision predator feline hunt piercing|Pokemon +Blaziken|Blaziken pokemon official artwork|Fire|Epic|blazing martial striker kick fierce|Pokemon +Greymon|Greymon digimon render|Fire|Rare|dinosaur nova blaze horn champion|Digimon +EOF +) + +n=0 +while IFS='|' read -r NAME QUERY ELEMENTS TIER KEYWORDS CREDIT; do + [ -z "$NAME" ] && continue + n=$((n+1)) + echo "== [$n] $NAME ($ELEMENTS / $TIER) ==" + $PY scripts/imgharvest.py "$QUERY" \ + --name "$NAME" --element $ELEMENTS --tier "$TIER" \ + --keywords $KEYWORDS --credit "$CREDIT" --max 3 --candidates 18 \ + || echo " ! $NAME failed" +done <<< "$ROSTER" +echo "ALL DONE ($n characters)" diff --git a/scripts/imgharvest.py b/scripts/imgharvest.py new file mode 100644 index 0000000..f2d71ba --- /dev/null +++ b/scripts/imgharvest.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""imgharvest — build the hh card-art library from web *image* search. + +Complements `charvest.py` (which pulls frames from a video URL). Instead of one +subject per video, this queries a keyless image engine (DuckDuckGo) for a +character, downloads many candidate stills, and runs each through the *same* +auto-crop + quality pipeline as charvest — then keeps the sharpest, most +distinct portraits. Static official art tends to sit on clean backgrounds, so +the saliency crop lands far better than on motion-blurred video frames, and one +query yields several usable, non-duplicate faces at once. + +Mix genres freely — Pokémon, Digimon, Yu-Gi-Oh — tagging each with the card +`--element`(s) so `cmd_chat/chardex.py` can match a VM to a fitting face. + +Run with the anaconda interpreter (needs cv2 + numpy, like charvest): + + python3 scripts/imgharvest.py "Gardevoir official art" \ + --name Gardevoir --element Psychic Fairy --tier Epic \ + --keywords psychic elegant guardian embrace --max 3 --credit Pokemon + +⚠️ Harvested characters are likely third-party IP — see + docs/character-art-licensing.md before any public/commercial use. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +import tempfile +import urllib.parse +import urllib.request +from pathlib import Path + +import cv2 + +# reuse the proven crop + quality + library-write pipeline +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import charvest as cv # noqa: E402 + +UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0 Safari/537.36") +IMG_EXT = cv.IMG_EXT + + +# ── keyless DuckDuckGo image search ─────────────────────────────────────────── +def _vqd(query: str) -> str | None: + req = urllib.request.Request( + "https://duckduckgo.com/?q=" + urllib.parse.quote(query) + "&iax=images&ia=images", + headers={"User-Agent": UA}) + html = urllib.request.urlopen(req, timeout=20).read().decode("utf-8", "ignore") + for pat in (r'vqd=([\d-]+)\&', r'vqd="([\d-]+)"', r"vqd='([\d-]+)'", + r'vqd=([\d-]+)'): + m = re.search(pat, html) + if m: + return m.group(1) + return None + + +def ddg_image_urls(query: str, limit: int) -> list[str]: + """Return direct image URLs for a query via DuckDuckGo's i.js endpoint.""" + vqd = _vqd(query) + if not vqd: + print(f"[search] no vqd token for {query!r}", file=sys.stderr) + return [] + api = ("https://duckduckgo.com/i.js?l=us-en&o=json&q=" + + urllib.parse.quote(query) + f"&vqd={vqd}&f=,,,,,&p=1") + req = urllib.request.Request(api, headers={ + "User-Agent": UA, "Referer": "https://duckduckgo.com/", + "Accept": "application/json, text/javascript, */*; q=0.01"}) + data = json.loads(urllib.request.urlopen(req, timeout=20).read().decode("utf-8", "ignore")) + urls = [r["image"] for r in data.get("results", []) if r.get("image")] + return urls[:limit] + + +def download(url: str, dst: Path) -> Path | None: + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + raw = urllib.request.urlopen(req, timeout=20).read() + except Exception: + return None + if len(raw) < 4096: # too small to be useful + return None + ext = Path(urllib.parse.urlparse(url).path).suffix.lower() + dst = dst.with_suffix(ext if ext in IMG_EXT else ".jpg") + dst.write_bytes(raw) + return dst + + +# ── harvest ─────────────────────────────────────────────────────────────────── +def harvest(args) -> int: + queries = args.query if isinstance(args.query, list) else [args.query] + with tempfile.TemporaryDirectory(prefix="imgharvest-") as td: + work = Path(td) + candidates: list[Path] = [] + for q in queries: + urls = ddg_image_urls(q, args.candidates) + print(f"[search] {len(urls)} image url(s) for {q!r}") + for i, u in enumerate(urls): + fp = download(u, work / f"{cv._slug(q)}-{i:02d}") + if fp: + candidates.append(fp) + print(f"[crop] {len(candidates)} downloaded candidate(s)") + + scored: list[tuple[float, cv.np.ndarray]] = [] + seen: list[int] = [] + for fp in candidates: + img = cv2.imread(str(fp)) + if img is None: + continue + port = cv.autocrop(img) + if port is None: + continue + bs = cv.blur_score(port) + if bs < args.min_sharpness: + continue + hh = cv.ahash(port) + if any(cv._hamming(hh, s) <= 6 for s in seen): # near-duplicate + continue + seen.append(hh) + scored.append((bs, port)) + + if not scored: + print("[crop] no usable portraits (no subject detected / too blurry)", + file=sys.stderr) + return 1 + scored.sort(key=lambda t: -t[0]) # sharpest first + picks = [p for _, p in scored[:args.max]] + slugs = cv.append_library(args.name, args.element, args.tier, + [k.lower() for k in args.keywords], + queries[0], args.credit, picks) + print(f"[done] {len(slugs)} portrait(s) → {cv.LIB}: {', '.join(slugs)}") + return 0 + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(prog="imgharvest", description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("query", nargs="+", help="image-search query/queries") + p.add_argument("--name", required=True, help="character name (e.g. 'Gardevoir')") + p.add_argument("--element", nargs="*", default=[], + help="card type(s) it fits: Normal Psychic Flying Steel …") + p.add_argument("--tier", default="", help="rarity affinity (Common..Legendary)") + p.add_argument("--keywords", nargs="*", default=[], + help="descriptive words used to match VM purpose") + p.add_argument("--credit", default="", help="franchise / attribution") + p.add_argument("--max", type=int, default=3, help="max portraits to keep") + p.add_argument("--candidates", type=int, default=20, + help="image results to download per query before filtering") + p.add_argument("--min-sharpness", type=float, default=45.0, + help="reject portraits below this Laplacian variance") + return harvest(p.parse_args(argv)) + + +if __name__ == "__main__": + raise SystemExit(main())