diff --git a/src/Command/SeedCharactersCommand.php b/src/Command/SeedCharactersCommand.php new file mode 100644 index 0000000..2221f74 --- /dev/null +++ b/src/Command/SeedCharactersCommand.php @@ -0,0 +1,177 @@ +addArgument('names', InputArgument::IS_ARRAY, 'Character names to add (case-insensitive); prompts with a list if omitted') + ->addOption('all', null, InputOption::VALUE_NONE, 'Add every available character without prompting') + ->addOption('purge', null, InputOption::VALUE_NONE, 'Remove the seeded characters instead of adding them'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $catalog = []; + foreach ($this->sheets() as $row) { + $catalog[$row['name']] = $row; + } + $available = array_keys($catalog); + + if ($input->getOption('purge')) { + $removed = 0; + foreach ($available as $name) { + if ($c = $this->characters->findOneBy(['name' => $name])) { + $this->em->remove($c); + ++$removed; + } + } + $this->em->flush(); + $io->success(sprintf('Removed %d seeded character(s).', $removed)); + + return Command::SUCCESS; + } + + $selected = $this->resolveSelection($input, $io, $available); + if (null === $selected) { + return Command::FAILURE; + } + + $added = $skipped = 0; + foreach ($selected as $name) { + if ($this->characters->findOneBy(['name' => $name])) { + ++$skipped; + continue; + } + $this->em->persist($this->hydrate($catalog[$name])); + ++$added; + } + $this->em->flush(); + + $io->success(sprintf('%d character(s) added, %d already present.', $added, $skipped)); + + return Command::SUCCESS; + } + + /** + * @param list $available + * + * @return list|null the chosen names, or null on an unknown-name error + */ + private function resolveSelection(InputInterface $input, SymfonyStyle $io, array $available): ?array + { + /** @var list $requested */ + $requested = $input->getArgument('names'); + + if ($requested) { + $selected = []; + foreach ($requested as $name) { + $match = null; + foreach ($available as $known) { + if (0 === strcasecmp($known, $name)) { + $match = $known; + break; + } + } + if (null === $match) { + $io->error(sprintf('Unknown character "%s". Available: %s', $name, implode(', ', $available))); + + return null; + } + $selected[] = $match; + } + + return array_values(array_unique($selected)); + } + + if ($input->getOption('all') || !$input->isInteractive()) { + return $available; + } + + /** @var list $choice */ + $choice = $io->choice('Which character(s) do you want to add?', [...$available, self::ADD_ALL], null, true); + + return \in_array(self::ADD_ALL, $choice, true) ? $available : $choice; + } + + /** + * @param array $row + */ + private function hydrate(array $row): Character + { + $c = new Character(); + foreach ($row as $prop => $value) { + $c->{'set'.ucfirst($prop)}($value); + } + + return $c; + } + + /** + * @return list> + */ + private function sheets(): array + { + return [ + [ + 'name' => 'Darragh Oakwanderer', 'playerName' => 'PH', + 'race' => 'Halfling, stout', 'characterClass' => 'Ranger', 'level' => 2, + 'background' => 'Urchin', 'alignment' => 'CN', + 'armorClass' => 15, 'initiative' => 3, 'speed' => '25 ft.', 'hitDice' => '2d10', + 'proficiencyBonus' => 2, + 'strength' => 12, 'dexterity' => 17, 'constitution' => 14, + 'intelligence' => 12, 'wisdom' => 13, 'charisma' => 8, + 'savingThrowProficiencies' => ['Strength', 'Dexterity'], + 'skillProficiencies' => ['Sleight of Hand', 'Stealth'], + 'attacks' => [ + ['name' => 'Dart', 'bonus' => '+5', 'damage' => '1d4+5', 'range' => '20/60 ft. (×10 carried)'], + ['name' => 'Dagger', 'bonus' => '+5', 'damage' => '1d4+3(5)', 'range' => '20/60 ft. (×3 carried)'], + ['name' => 'Spear', 'bonus' => '+3', 'damage' => '1d8+6(1+3)', 'range' => '20/60 ft. (×1 carried)'], + ], + 'otherProficienciesLanguages' => "Languages: Halfling, Common\n\nTools: Disguise kit, Thieves' tools", + 'equipment' => "Small knife\nMap of the city\nPet bees\nToken of parents\nCommon clothes\nStudded leather armor\nSpear\n10 darts\n3 daggers\nExplorer's pack", + 'featuresTraits' => "Stout Resilience\nLucky\nBrave\nNimble\n\nCity Secrets\n\nFavored Foe\nDeft Explorer\nFighting Style: Throwing\nSpellcasting", + 'spellcasting' => "Spellcasting ability: Wisdom (spell save DC 11, spell attack bonus +3)\n\nLevel 1 (2 slots): Hunter's Mark, Cure Wounds", + 'personalityTraits' => 'I eat like a pig and have bad manners', + 'ideals' => "I help the people who help me, that's what keeps us alive", + 'bonds' => "My town is my home and I'll fight to defend it", + 'flaws' => "It's not stealing if I need it more than someone else", + ], + ]; + } +}