/* * ShootOFF - Software for Laser Dry Fire Training * Copyright (C) 2016 phrack * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.shootoff.plugins; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Random; import java.util.Stack; import com.shootoff.camera.Shot; import com.shootoff.targets.Hit; import com.shootoff.targets.Target; import com.shootoff.targets.TargetRegion; public class RandomShoot extends TrainingExerciseBase implements TrainingExercise { private List<Target> targets; private Target selectedTarget = null; private final List<String> subtargets = new ArrayList<>(); private final Stack<Integer> currentSubtargets = new Stack<>(); private Random rng = new Random(); public RandomShoot() {} public RandomShoot(List<Target> targets) { super(targets); this.targets = targets; if (fetchSubtargets(targets)) startRound(); } /** * This is used to make this plugin deterministic for testing. * * @param rng * an rng with a known seed */ protected RandomShoot(List<Target> targets, Random rng) { super(targets); this.targets = targets; this.rng = rng; if (fetchSubtargets(targets)) startRound(); } @Override public void init() {} @Override public void targetUpdate(Target target, TargetChange change) { switch(change) { case ADDED: // Didn't previously have a usable target, if we do now // start the exercise if (selectedTarget == null) { for (final TargetRegion region : target.getRegions()) { if (region.tagExists("subtarget")) { if (fetchSubtargets(Arrays.asList(target))) startRound(); break; } } } targets.add(target); break; case REMOVED: targets.remove(target); if (target.equals(selectedTarget)) { selectedTarget = null; // Clear state and state error if (fetchSubtargets(targets)) startRound(); } break; } } private void startRound() { pickSubtargets(); saySubtargets(); } /** * Returns the list of all known subtargets. This method exists to make this * exercise easier to test. * * @return a list of all known subtargets */ protected List<String> getSubtargets() { return subtargets; } /** * Returns the current subtarget stack. This method exists to make this * exercise easier to test. * * @return current subtarget stack */ protected Stack<Integer> getCurrentSubtargets() { return currentSubtargets; } /** * Finds the first target with subtargets and gets its regions. If there is * no target with substargets, this method uses TTS to tell the user * * @param targets * a list of all targets known to this exercise * @return <tt>true</tt> if we found subtargets, <tt>false</tt> otherwise */ private boolean fetchSubtargets(List<Target> targets) { subtargets.clear(); currentSubtargets.clear(); boolean foundTarget = false; for (final Target target : targets) { for (final TargetRegion region : target.getRegions()) { if (region.getAllTags().containsKey("subtarget")) { subtargets.add(region.getTag("subtarget")); foundTarget = true; } } if (foundTarget) { selectedTarget = target; break; } } if (foundTarget && subtargets.size() > 0) { return true; } else { TrainingExerciseBase.playSound(new File("sounds/voice/shootoff-subtargets-warning.wav")); return false; } } private void pickSubtargets() { currentSubtargets.clear(); final int count = rng.nextInt((subtargets.size() - 1) + 1) + 1; for (final int i : rng.ints(count, 0, subtargets.size()).toArray()) { currentSubtargets.push(Integer.valueOf(i)); } } private void saySubtargets() { final List<File> soundFiles = new ArrayList<>(); soundFiles.add(new File("sounds/voice/shootoff-shoot.wav")); final Stack<Integer> temp = new Stack<>(); temp.addAll(currentSubtargets); Collections.reverse(temp); final Iterator<Integer> it = temp.iterator(); while (it.hasNext()) { final Integer index = it.next(); if (!it.hasNext() && currentSubtargets.size() > 1) soundFiles.add(new File("sounds/voice/shootoff-and.wav")); final File targetNameSound = new File(String.format("sounds/voice/shootoff-%s.wav", subtargets.get(index))); if (targetNameSound.exists()) { soundFiles.add(targetNameSound); } else { // We don't have a voice actor sounds file for a target // subregion, fall back // to TTS saySubtargetsTTS(); return; } } super.playSounds(soundFiles); } private void saySubtargetsTTS() { final StringBuilder sentence = new StringBuilder("shoot subtarget "); sentence.append(subtargets.get(currentSubtargets.get(currentSubtargets.size() - 1))); for (int i = currentSubtargets.size() - 2; i >= 0; i--) { sentence.append(" then "); sentence.append(subtargets.get(currentSubtargets.get(i))); } TextToSpeech.say(sentence.toString()); } private void sayCurrentSubtarget() { final List<File> soundFiles = new ArrayList<>(); soundFiles.add(new File("sounds/voice/shootoff-shoot.wav")); final int subtargetIndex = currentSubtargets.peek(); if (subtargets.size() == 0 || subtargetIndex > subtargets.size()) { // Error condition, there are no subtargets left or the index indicates // a target that doesn't exist. Try restarting startRound(); return; } final File targetNameSound = new File( String.format("sounds/voice/shootoff-%s.wav", subtargets.get(subtargetIndex))); if (targetNameSound.exists()) { soundFiles.add(targetNameSound); } else { sayCurrentSubtargetTTS(); return; } super.playSounds(soundFiles); } private void sayCurrentSubtargetTTS() { final String sentence = "shoot " + subtargets.get(currentSubtargets.peek()); TextToSpeech.say(sentence); } @Override public ExerciseMetadata getInfo() { return new ExerciseMetadata("Random Shoot", "1.0", "phrack", "This exercise works with targets that have subtarget tags " + "assigned to some regions. Subtargets are selected at random " + "and the shooter is asked to shoot those subtargets in order. " + "If a subtarget is shot out of order or the shooter misses, the " + "name of the subtarget that should have been shot is repeated."); } @Override public void shotListener(Shot shot, Optional<Hit> hit) { if (currentSubtargets.isEmpty()) return; if (hit.isPresent()) { final String subtargetValue = hit.get().getHitRegion().getTag("subtarget"); if (subtargetValue != null && subtargetValue.equals(subtargets.get(currentSubtargets.peek()))) { currentSubtargets.pop(); } else { sayCurrentSubtarget(); } if (currentSubtargets.isEmpty()) startRound(); } else { sayCurrentSubtarget(); } } @Override public void reset(List<Target> targets) { this.targets = targets; if (fetchSubtargets(targets)) startRound(); } @Override public void destroy() { super.destroy(); } }