package com.lmweav.schoolquest.scripting; import android.content.Context; import android.util.SparseArray; import com.lmweav.schoolquest.Game; import com.lmweav.schoolquest.GameActivity; import com.lmweav.schoolquest.R; import com.lmweav.schoolquest.characters.GameCharacter; import com.lmweav.schoolquest.characters.NPC; import com.lmweav.schoolquest.characters.NPCDataStructure; import com.lmweav.schoolquest.tiles.TileMap; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Set; import androidx.annotation.NonNull; import static com.lmweav.schoolquest.Game.GAME; import static com.lmweav.schoolquest.Constants.*; /* * School Quest: Script * * This class runs an in-game script by executing a list of game characters with a stack of * commands. * * Methods in this class parse the external script file and execute the created script object. * * @author Luke Weaver * @version 1.0.9 * @since 2019-10-16 */ public class Script { private static SparseArray<Script> scripts = new SparseArray<>(); private int playerX; private int playerY; private int playerDirection; private int oldSpeed; private int bgmId; private boolean loaded; private boolean started; private boolean finished; private boolean skip; private boolean skippable; private String playerTileSet; private HashMap<GameCharacter, LinkedList<Command>> commands = new HashMap<>(); private HashMap<GameCharacter, LinkedList<Command>> commandsCopy = new HashMap<>(); private HashMap<String, GameCharacter> actors = new HashMap<>(); private ArrayList<NPC> npcs = new ArrayList<>(); private TileMap scriptMap; private int[] endInfo = new int[] {-1, -1, -1, -1, -1, -1}; private Runnable endRunnable = null; /*--------------------------------------------------------------------------------------------- | Constructors ----------------------------------------------------------------------------------------------*/ private Script(Context context, int id, int bgm, boolean skippable) { InputStream inputStream = context.getResources().openRawResource(id); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line; boolean scriptStart = false; boolean scriptEnd = false; commands.put(null, new LinkedList<Command>()); try { while ((line = bufferedReader.readLine()) != null) { if (line.contains("\\\\\\\\")) { scriptStart = false; scriptEnd = true; } if (line.contains("////")) { scriptStart = true; scriptEnd = false; } else { String[] split = line.replaceAll("(^.*?\\[|]\\s*$)", ""). split("]\\["); if (scriptStart) { readScriptLine(split); } else if (scriptEnd) { readScriptEnd(split); } else { readScriptMeta(context, split); } } } for (GameCharacter gc: actors.values()) { if (gc instanceof NPC) { npcs.add((NPC) gc); } } } catch (IOException | NumberFormatException e) { e.printStackTrace(); } bgmId = bgm; this.skippable = skippable; } /*--------------------------------------------------------------------------------------------- | Getters and Setters ----------------------------------------------------------------------------------------------*/ public static Script getScript(int index) { return scripts.get(index); } public int getMapId() { return scriptMap.getId(); } public int getPlayerX() { return playerX; } public int getPlayerY() { return playerY; } public int getPlayerDirection() { return playerDirection; } public void setOldSpeed(int oldSpeed) { this.oldSpeed = oldSpeed; } public boolean isLoaded() { return loaded; } public void setLoaded(boolean loaded) { this.loaded = loaded; } public boolean isStarted() { return started; } public void setStarted(boolean started) { this.started = started; } public boolean isFinished() { return finished; } public void setFinished(boolean finished) { this.finished = finished; } public void setSkip(boolean skip) { this.skip = skip; } public boolean isSkippable() { return skippable; } public String getPlayerTileSet() { return playerTileSet; } public ArrayList<NPC> getNpcs() { return npcs; } public Runnable getEndRunnable() { return endRunnable; } public void setEndRunnable(Runnable runnable) { this.endRunnable = runnable; } public int getBGM() { return bgmId; } /*--------------------------------------------------------------------------------------------- | Methods ----------------------------------------------------------------------------------------------*/ public static void loadScripts(Context context) { scripts.put(TRACK_CLUB_CUTSCENE, new Script(context, R.raw._cutscene_track_club, R.raw._music_activity, true)); scripts.put(CHEMISTRY_CUTSCENE, new Script(context, R.raw._cutscene_chemistry, R.raw._music_activity, true)); scripts.put(CHEMISTRY_HEIST_CUTSCENE, new Script(context, R.raw._cutscene_chemistry_heist, R.raw._music_activity, false)); scripts.put(DT_HEIST_CUTSCENE, new Script(context, R.raw._cutscene_dt_heist, R.raw._music_activity, false)); scripts.put(TUTORING_CUTSCENE, new Script(context, R.raw._cutscene_tutoring, R.raw._music_activity, true)); scripts.put(TUTORING_HEIST_CUTSCENE, new Script(context, R.raw._cutscene_tutoring_heist, R.raw._music_activity, false)); } boolean isCommandFinished(@NonNull GameCharacter actor, int index) { LinkedList<Command> commands = commandsCopy.get(actor); return commands.isEmpty() || commands.peek().lineIndex > index; } private void readScriptLine(String[] line) { GameCharacter actor = null; if (!line[0].equals("ui")) { actor = actors.get(line[0]); } String[] commandData = line[1].split("\\|"); int speed; int steps; int lineIndex; if (commands.get(actor).isEmpty()) { lineIndex = 0; } else { lineIndex = commands.get(actor).getLast().lineIndex + 1; } Command command; switch (commandData[0]) { case "path": String[] startData = commandData[2].split(","); int oldDestX = Integer.parseInt(startData[0]); int oldDestY = Integer.parseInt(startData[1]); for (int i = 3; i < commandData.length; i++) { String[] pointData = commandData[i].split(","); int destinationX = Integer.parseInt(pointData[0]); int destinationY = Integer.parseInt(pointData[1]); speed = Integer.parseInt(commandData[1]); command = new PathCommand(scriptMap, actor, oldDestX, oldDestY, destinationX, destinationY, speed); command.lineIndex = lineIndex; commands.get(actor).add(command); oldDestX = destinationX; oldDestY = destinationY; } break; case "wait": GameCharacter target = actors.get(commandData[1]); int index = Integer.parseInt(commandData[2]); command = new WaitCommand(this, target, index); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "up": speed = Integer.parseInt(commandData[1]); steps = Integer.parseInt(commandData[2]); assert actor != null; command = new DirectionCommand(scriptMap, actor, OBJECT_DIRECTION_UP, speed, steps); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "down": speed = Integer.parseInt(commandData[1]); steps = Integer.parseInt(commandData[2]); assert actor != null; command = new DirectionCommand(scriptMap, actor, OBJECT_DIRECTION_DOWN, speed, steps); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "left": speed = Integer.parseInt(commandData[1]); steps = Integer.parseInt(commandData[2]); assert actor != null; command = new DirectionCommand(scriptMap, actor, OBJECT_DIRECTION_LEFT, speed, steps); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "right": speed = Integer.parseInt(commandData[1]); steps = Integer.parseInt(commandData[2]); assert actor != null; command = new DirectionCommand(scriptMap, actor, OBJECT_DIRECTION_RIGHT, speed, steps); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "emotion": command = new EmotionCommand(actor, commandData[1], this); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "text": String text = line[2]; if (line.length > 3) { Runnable runnable = null; switch (line[3]) { case "skip": runnable = new Runnable() { @Override public void run() { for (GameCharacter actor: commandsCopy.keySet()) { if (actor != null) { commandsCopy.remove(actor); } } } }; break; } command = new TextCommand(actor, text, runnable); } else { command = new TextCommand(actor, text); } command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "loading": boolean loading = Boolean.parseBoolean(commandData[1]); command = new LoadingCommand(loading); command.lineIndex = lineIndex; commands.get(actor).add(command); break; case "jingle": command = new JingleCommand(commandData[1]); command.lineIndex = lineIndex; commands.get(actor).add(command); break; } } private void readScriptEnd(String[] line) { String[] endData = line[0].split("\\|"); switch (endData[0]) { case "next": endInfo[0] = Integer.parseInt(endData[1]); break; case "time": endInfo[1] = Game.getTimeId(endData[1]); break; case "map": endInfo[2] = Game.getMapId(endData[1]); break; case "player": String[] charInfo = endData[1].split(","); endInfo[3] = Integer.parseInt(charInfo[0]); endInfo[4] = Integer.parseInt(charInfo[1]); switch (charInfo[2]) { case "up": endInfo[5] = OBJECT_DIRECTION_UP; break; case "down": endInfo[5] = OBJECT_DIRECTION_DOWN; break; case "left": endInfo[5] = OBJECT_DIRECTION_LEFT; break; case "right": endInfo[5] = OBJECT_DIRECTION_RIGHT; break; } break; } } private void readScriptMeta(Context context, String[] line) { String[] info = line[0].split("\\|"); switch (info[0]) { case "map": int mapId = Game.getMapId(info[1]); scriptMap = TileMap.getMap(mapId); break; case "actor": GameCharacter actor; String[] charInfo = line[1].split(","); int x = Integer.parseInt(charInfo[0]); int y = Integer.parseInt(charInfo[1]); int direction = 0; switch (charInfo[2]) { case "up": direction = OBJECT_DIRECTION_UP; break; case "down": direction = OBJECT_DIRECTION_DOWN; break; case "left": direction = OBJECT_DIRECTION_LEFT; break; case "right": direction = OBJECT_DIRECTION_RIGHT; break; } switch (info[1]) { case "player": actor = GAME.getPlayer(); playerX = x; playerY = y; playerDirection = direction; if (info.length > 2) { playerTileSet = info[2]; } else { playerTileSet = "default"; } break; default: NPCDataStructure data = NPC.getData(info[1]); actor = new NPC(context, x, y, data, NPC.getName(data.getNpcId()), "", direction, false, null); if (info.length > 2) { actor.changeTile(info[2]); } break; } actors.put(info[1], actor); commands.put(actor, new LinkedList<Command>()); } } public void copyCommands() { commandsCopy = new HashMap<>(); for (Map.Entry<GameCharacter, LinkedList<Command>> entry : commands.entrySet()) { commandsCopy.put(entry.getKey(), new LinkedList<>(entry.getValue())); } } boolean uiFinished() { return commandsCopy.get(null).isEmpty(); } public void execute() { started = true; Set<GameCharacter> actors = commands.keySet(); ArrayList<LinkedList> comm = new ArrayList<LinkedList>(commandsCopy.values()); if (skip) { for (GameCharacter actor : actors) { LinkedList<Command> commands = commandsCopy.get(actor); if (commands == null || commands.isEmpty()) { continue; } commands.peek().reset(); } comm.clear(); } else { for (GameCharacter actor: actors) { if (commandsCopy.get(actor).isEmpty()) { continue; } Command current = commandsCopy.get(actor).peek(); assert current != null; if (current.finished) { current.reset(); commandsCopy.get(actor).pop(); if (commandsCopy.get(actor).isEmpty()) { continue; } else { current = commandsCopy.get(actor).peek(); } } assert current != null; current.execute(); if (current.finished) { current.reset(); commandsCopy.get(actor).pop(); if (!commandsCopy.get(actor).isEmpty()) { current = commandsCopy.get(actor).peek(); assert current != null; current.execute(); } } } } if (allEmpty(comm)) { GameActivity.getInstance().runOnUiThread(new Runnable() { @Override public void run() { GameActivity.getInstance().scriptHUD(false); GameActivity.getInstance().runningShoes(oldSpeed == 4); } }); finished = true; GAME.getPlayer().changeTile("default"); if (endInfo[3] > -1) { GAME.getPlayer().setPoint(endInfo[3], endInfo[4]); GAME.getPlayer().rotate(endInfo[5]); } if (endInfo[1] > -1) { int oldTime = GAME.getTime(); final String before = Game.getTimeKey(oldTime).toUpperCase(); GAME.setTime(endInfo[1]); int newTime = GAME.getTime(); final String after = Game.getTimeKey(newTime).toUpperCase(); if (oldTime != newTime) { GameActivity.getInstance().setSlideLoadingTransition(before, after); } } if (endInfo[2] > -1) { GAME.loadMap(endInfo[2]); } else { GAME.reloadMap(); } GAME.changeBGM(GAME.getTileMap().getBGM()); } } private boolean allEmpty(ArrayList<LinkedList> lists) { for (LinkedList list: lists) { if (!list.isEmpty()) { return false; } } return true; } }