package inf101.v18.rogue101.game; import inf101.v18.gfx.Screen; import inf101.v18.gfx.gfxmode.ITurtle; import inf101.v18.gfx.gfxmode.TurtlePainter; import inf101.v18.gfx.textmode.Printer; import inf101.v18.grid.GridDirection; import inf101.v18.grid.IGrid; import inf101.v18.grid.Location; import inf101.v18.rogue101.Main; import inf101.v18.rogue101.enemies.Boss; import inf101.v18.rogue101.enemies.Girl; import inf101.v18.rogue101.examples.Carrot; import inf101.v18.rogue101.examples.Rabbit; import inf101.v18.rogue101.items.buff.Binoculars; import inf101.v18.rogue101.items.buff.BuffItem; import inf101.v18.rogue101.items.buff.Shield; import inf101.v18.rogue101.items.consumable.HealthPotion; import inf101.v18.rogue101.items.consumable.Manga; import inf101.v18.rogue101.items.container.Chest; import inf101.v18.rogue101.items.container.Static; import inf101.v18.rogue101.items.weapon.BasicBow; import inf101.v18.rogue101.items.weapon.BasicSword; import inf101.v18.rogue101.items.weapon.FireStaff; import inf101.v18.rogue101.items.weapon.MagicWeapon; import inf101.v18.rogue101.items.weapon.MeleeWeapon; import inf101.v18.rogue101.items.weapon.RangedWeapon; import inf101.v18.rogue101.items.weapon.Weapon; import inf101.v18.rogue101.map.AGameMap; import inf101.v18.rogue101.map.GameMap; import inf101.v18.rogue101.map.MapReader; import inf101.v18.rogue101.map.MapView; import inf101.v18.rogue101.object.Actor; import inf101.v18.rogue101.object.Dust; import inf101.v18.rogue101.object.FakeWall; import inf101.v18.rogue101.object.Item; import inf101.v18.rogue101.object.NonPlayerCharacter; import inf101.v18.rogue101.object.Player; import inf101.v18.rogue101.object.PlayerCharacter; import inf101.v18.rogue101.object.StairsDown; import inf101.v18.rogue101.object.StairsUp; import inf101.v18.rogue101.object.Wall; import inf101.v18.rogue101.state.AttackType; import inf101.v18.rogue101.state.Sound; import inf101.v18.util.NPCHelper; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.function.Supplier; public class RogueGame implements Game { /** * All the IActors that have things left to do this turn */ private List actors = Collections.synchronizedList(new ArrayList<>()); /** * For fancy solution to factory problem */ private final Map> itemFactories = new HashMap<>(); /** * Useful random generator */ private final Random random = new Random(); /** * Saves the last three messages */ private final List lastMessages = new ArrayList<>(); /** * The game map. {@link GameMap} gives us a few more details than * {@link MapView} (write access to item lists); the game needs this but * individual items don't. */ private GameMap map; private final List maps = new ArrayList<>(); private int currentLVL = 0; private Actor currentActor; private Location currentLocation; private int movePoints = 0; private final ITurtle painter; private final Printer printer; private int numberOfPlayers = 0; private boolean won = false; private List visible = null; public RogueGame(Screen screen, ITurtle painter, Printer printer) { this.painter = painter; this.printer = printer; addFactory(); // NOTE: in a more realistic situation, we will have multiple levels (one map // per level), and (at least for a Roguelike game) the levels should be // generated // // inputGrid will be filled with single-character strings indicating what (if // anything) // should be placed at that map square loadMap("level1.txt", 1); loadMap("level2.txt", 2); loadMap("level3.txt", 3); loadMap("level4.txt", 4); loadMap("level5.txt", 5); map = maps.get(0); map.add(map.getLocation(3, 3), new Player()); //We must be sure there is never more than one player // Prints some helpful information. String[] info = {"Controls:", "WASD or arrow keys for movement", "E to pick up an item", "Q to drop an item", "1-0 to choose an item (10=0)", "N to change name", "ENTER to confirm", "R to use a ranged attack", "F to use a magic attack", "C to use a consumable"}; for (int i = 0; i < info.length; i++) { this.printer.printAt(Main.COLUMN_RIGHT_SIDE_START, 1 + i, info[i]); } } public RogueGame(String mapString) { printer = new Printer(1280, 720); painter = new TurtlePainter(1280, 720); addFactory(); IGrid inputGrid = MapReader.readString(mapString); this.map = new AGameMap(inputGrid.getArea()); for (Location loc : inputGrid.locations()) { Item item = createItem(inputGrid.get(loc)); if (item != null) { map.add(loc, item); } } } @Override public void addItem(Item item) { map.add(currentLocation, item); } @Override public void addItem(String sym) { Item item = createItem(sym); if (item != null) { map.add(currentLocation, item); } } /** * Goes up one floor if possible. */ public void goUp() { if (currentLVL + 1 < maps.size()) { map = maps.get(++currentLVL); if (!map.has(currentLocation, currentActor)) { map.add(currentLocation, currentActor); } actors = new ArrayList<>(); displayMessage("You went up the stairs"); } } /** * Goes down one floor if possible. */ public void goDown() { if (currentLVL > 0) { map = maps.get(--currentLVL); if (!map.has(currentLocation, currentActor)) { map.add(currentLocation, currentActor); } actors = new ArrayList<>(); displayMessage("You went down the stairs"); } } /** * Calculates the attack of an IActor based on equipped items. * * @return The attack */ private int getAttack(AttackType type) { int attack = currentActor.getAttack() + random.nextInt(20) + 1; Weapon weapon = NPCHelper.getWeapon(type, currentActor); if (weapon != null) { attack += weapon.getWeaponAttack(); } return attack; } /** * Gets the defence of the current target. * * @param target The target to evaluate * @return The defence of the target */ private int getDefence(Item target) { int defence = target.getDefense() + 10; Actor actor = (Actor) target; BuffItem item = (BuffItem) actor.getItem(BuffItem.class); if (item != null) { defence += item.getBuffDefene(); } return defence; } /** * Gets the damage done against the current target. * * @param target The target to evaluate. * @return The damage done to the target. */ private int getDamage(Item target, AttackType type) { int damage = currentActor.getDamage(); Weapon weapon = NPCHelper.getWeapon(type, currentActor); if (weapon != null) { damage += weapon.getWeaponDamage(); } BuffItem buff = (BuffItem) currentActor.getItem(BuffItem.class); if (buff != null) { damage += buff.getBuffDamage(); } BuffItem item = (BuffItem) ((Actor) target).getItem(BuffItem.class); if (item != null) { damage -= item.getBuffDamageReduction(); } return damage; } @Override public Location attack(GridDirection dir, Item target) { Location loc = currentLocation.go(dir); if (!map.has(loc, target)) { throw new IllegalMoveException("Target isn't there!"); } Weapon weapon = (Weapon) currentActor.getItem(MeleeWeapon.class); if (weapon != null) { weapon.getSound().play(); } else { Sound.MELEE_NO_WEAPON.play(); } if (getAttack(AttackType.MELEE) >= getDefence(target)) { int actualDamage = target.handleDamage(this, target, getDamage(target, AttackType.MELEE)); if (currentActor != null) { formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); } } else { formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); } map.clean(loc); if (target.isDestroyed() && currentLocation != null) { return move(dir); } else { movePoints--; return currentLocation; } } @Override public Location rangedAttack(GridDirection dir, Item target, AttackType type) { Location loc = currentLocation; Weapon weapon = null; switch (type) { case MAGIC: weapon = (Weapon) currentActor.getItem(MagicWeapon.class); break; case RANGED: weapon = (Weapon) currentActor.getItem(RangedWeapon.class); } if (weapon != null) { weapon.getSound().play(); } else { Sound.RANGED_NO_WEAPON.play(); } if (getAttack(type) >= getDefence(target)) { int damage = getDamage(target, type) / loc.gridDistanceTo(map.getLocation(target)); int actualDamage = target.handleDamage(this, target, damage); formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); } else { formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); } map.clean(map.getLocation(target)); if (target.isDestroyed() && map.has(currentLocation.go(dir), target)) { return move(dir); } else { return currentLocation; } } /** * Begin a new game turn, or continue to the previous turn * * @return True if the game should wait for more user input */ public boolean doTurn() { do { if (actors.isEmpty()) { // System.err.println("new turn!"); // no one in the queue, we're starting a new turn! // first collect all the actors: beginTurn(); } /*if (random.nextInt(100) < 20) { ILocation loc = map.getLocation(random.nextInt(map.getWidth()), random.nextInt(map.getHeight())); if (!map.hasActors(loc) && !map.hasItems(loc) && !map.hasWall(loc)) { map.add(loc, new Carrot()); } }*/ //We don't want this in the actual game. // process actors one by one; for the IPlayer, we return and wait for keypresses // Possible for INonPlayer, we could also return early (returning // *false*), and then insert a little timer delay between each non-player move // (the timer // is already set up in Main) if (numberOfPlayers == 0 && !won) { kill(); } while (!actors.isEmpty()) { // get the next player or non-player in the queue currentActor = actors.remove(0); if (currentActor == null) { return false; //TODO: Find out why a girl suddenly becomes null (only after map change, not caught in beginTurn, happens randomly) } // skip if it's dead if (currentActor.isDestroyed()) { continue; } currentLocation = map.getLocation(currentActor); if (currentLocation == null) { displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor); } movePoints = 1; // everyone gets to do one thing visible = null; if (currentActor instanceof NonPlayerCharacter) { // computer-controlled players do their stuff right away ((NonPlayerCharacter) currentActor).doTurn(this); // remove any dead items from current location map.clean(currentLocation); return false; } else if (currentActor instanceof PlayerCharacter) { if (!currentActor.isDestroyed()) { // For the human player, we need to wait for input, so we just return. // Further keypresses will cause keyPressed() to be called, and once the human // makes a move, it'll lose its movement point and doTurn() will be called again // // NOTE: currentActor and currentLocation are set to the IPlayer (above), // so the game remembers who the player is whenever new keypresses occur. This // is also how e.g., getLocalItems() work – the game always keeps track of // whose turn it is. return true; } } else { displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor); } } } while (numberOfPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of // the loop) once we hit the player return true; } /** * Player is dead. It needs to be properly killed off. */ private void kill() { map.remove(currentLocation, currentActor); currentActor = null; currentLocation = null; actors = new ArrayList<>(); loadMap("gameover.txt"); Sound.GAME_OVER.play(); } public void win() { //Trigger when the boss dies map.remove(currentLocation, currentActor); currentActor = null; currentLocation = null; actors = new ArrayList<>(); loadMap("victory.txt"); map.draw(painter, printer); Sound.WIN.play(); won = true; } /** * Loads a map with the desired name * * @param mapName Name of map, including extension. */ private void loadMap(String mapName) { IGrid inputGrid = MapReader.readFile("maps/" + mapName); if (inputGrid == null) { System.err.println("Map not found – falling back to builtin map"); inputGrid = MapReader.readString(Main.BUILTIN_MAP); } GameMap map = new AGameMap(inputGrid.getArea()); for (Location loc : inputGrid.locations()) { Item item = createItem(inputGrid.get(loc)); if (item != null) { map.add(loc, item); } } this.map = map; } private void loadMap(String mapName, int lvl) { IGrid inputGrid = MapReader.readFile("maps/" + mapName); if (inputGrid == null) { System.err.println("Map not found – falling back to builtin map"); inputGrid = MapReader.readString(Main.BUILTIN_MAP); } GameMap map = new AGameMap(inputGrid.getArea()); for (Location loc : inputGrid.locations()) { Item item = createItem(inputGrid.get(loc)); if (item instanceof Chest) { ((Chest) item).fill(lvl); } else if (item instanceof Girl) { ((Girl) item).giveWeapon(lvl); } if (item != null) { map.add(loc, item); } } maps.add(map); } /** * Go through the map and collect all the actors. */ private void beginTurn() { numberOfPlayers = 0; // this extra fancy iteration over each map location runs *in parallel* on // multicore systems! // that makes some things more tricky, hence the "synchronized" block and // "Collections.synchronizedList()" in the initialization of "actors". // NOTE: If you want to modify this yourself, it might be a good idea to replace // "parallelStream()" by "stream()", because weird things can happen when many // things happen // at the same time! (or do INF214 or DAT103 to learn about locks and threading) map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map List list = map.getAllModifiable(loc); // all items at loc Iterator li = list.iterator(); // manual iterator lets us remove() items while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside Item item = li.next(); if (item.getCurrentHealth() < 0) { // normally, we expect these things to be removed when they are destroyed, so // this shouldn't happen synchronized (this) { formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n", item.getName(), item.getSymbol(), loc); } li.remove(); map.remove(loc, item); // need to do this too, to update item map } else if (item instanceof PlayerCharacter) { actors.add(0, (Actor) item); // we let the human player go first synchronized (this) { numberOfPlayers++; } } else if (item instanceof Actor) { actors.add((Actor) item); // add other actors to the end of the list } else if (item instanceof Carrot) { ((Carrot) item).doTurn(); } } }); } @Override public boolean canGo(GridDirection dir) { return map.canGo(currentLocation, dir); } private void addFactory() { itemFactories.put("#", Wall::new); itemFactories.put("@", Player::new); itemFactories.put("C", Carrot::new); itemFactories.put("R", Rabbit::new); itemFactories.put("M", Manga::new); itemFactories.put("G", Girl::new); itemFactories.put(".", Dust::new); itemFactories.put("S", BasicSword::new); itemFactories.put("c", Chest::new); itemFactories.put("B", Boss::new); itemFactories.put("s", FireStaff::new); itemFactories.put("b", BasicBow::new); itemFactories.put("H", HealthPotion::new); itemFactories.put("=", FakeWall::new); itemFactories.put("<", StairsUp::new); itemFactories.put(">", StairsDown::new); itemFactories.put("m", Binoculars::new); itemFactories.put("f", Shield::new); } @Override public Item createItem(String symbol) { if (" ".equals(symbol)) { return null; }// alternative/advanced method Supplier factory = itemFactories.get(symbol); if (factory != null) { return factory.get(); } else { System.err.println("createItem: Don't know how to create a '" + symbol + "'"); return null; } } @Override public void displayDebug(String s) { printer.clearLine(Main.LINE_DEBUG); printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED); System.err.println(s); } @Override public void displayMessage(String s) { if (lastMessages.size() >= 3) { lastMessages.remove(2); } lastMessages.add(0, s); printer.clearLine(Main.LINE_MSG1); printer.clearLine(Main.LINE_MSG2); printer.clearLine(Main.LINE_MSG3); int maxLen = 80; //The maximum length of a message to not overflow. boolean secondLineWritten = false; boolean thirdLineWritten = false; //Makes long messages overflow to the next line. if (lastMessages.size() > 0) { String message = lastMessages.get(0); if (message.length() > 2 * maxLen) { printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen, 2 * maxLen)); printer.printAt(1, Main.LINE_MSG3, message.substring(2 * maxLen)); secondLineWritten = thirdLineWritten = true; } else if (message.length() > maxLen) { printer.printAt(1, Main.LINE_MSG1, message.substring(0, maxLen)); printer.printAt(1, Main.LINE_MSG2, message.substring(maxLen)); secondLineWritten = true; } else { printer.printAt(1, Main.LINE_MSG1, message); } } if (lastMessages.size() > 1 && !secondLineWritten) { String message = lastMessages.get(1); if (message.length() > maxLen) { printer.printAt(1, Main.LINE_MSG2, message.substring(0, maxLen)); printer.printAt(1, Main.LINE_MSG3, message.substring(maxLen)); thirdLineWritten = true; } else { printer.printAt(1, Main.LINE_MSG2, message); } } if (lastMessages.size() > 2 && !thirdLineWritten) { printer.printAt(1, Main.LINE_MSG3, lastMessages.get(2)); } //System.out.println("Message: «" + s + "»"); } @Override public void displayStatus(String s) { printer.clearLine(Main.LINE_STATUS); printer.printAt(1, Main.LINE_STATUS, s); //System.out.println("Status: «" + s + "»"); } public void draw() { if (numberOfPlayers == 0) { map.draw(painter, printer); } else { ((AGameMap) map).drawVisible(painter, printer); } } @Override public boolean drop(Item item) { if (item != null) { map.add(currentLocation, item); return true; } else { return false; } } @Override public boolean dropAt(Location loc, Item item) { if (item != null) { map.add(loc, item); return true; } else { return false; } } @Override public void formatDebug(String s, Object... args) { displayDebug(String.format(s, args)); } @Override public void formatMessage(String s, Object... args) { displayMessage(String.format(s, args)); } @Override public void formatStatus(String s, Object... args) { displayStatus(String.format(s, args)); } @Override public int getHeight() { return map.getHeight(); } @Override public List getLocalItems() { return map.getItems(currentLocation); } @Override public Location getLocation() { return currentLocation; } @Override public Location getLocation(GridDirection dir) { if (currentLocation.canGo(dir)) { return currentLocation.go(dir); } else { return null; } } /** * Return the game map. {@link GameMap} gives us a few more details than * {@link MapView} (write access to item lists); the game needs this but * individual items don't. */ @Override public MapView getMap() { return map; } @Override public List getPossibleMoves() { List moves = new ArrayList<>(); for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) { if (canGo(dir)) { moves.add(dir); } } return moves; } @Override public List getVisible() { if (this.visible != null) { return this.visible; } List neighbours = this.map.getNeighbourhood(this.currentLocation, this.currentActor.getVision()); List invalid = new ArrayList<>(); for (Location neighbour : neighbours) { for (Location tile : this.currentLocation.gridLineTo(neighbour)) { if (this.map.hasWall(tile)) { invalid.add(neighbour); break; } } } neighbours.removeAll(invalid); this.visible = neighbours; return neighbours; } @Override public int getWidth() { return this.map.getWidth(); } public boolean keyPressed(KeyCode code) { // only an IPlayer/human can handle keypresses, and only if it's the human's // turn return !(currentActor instanceof PlayerCharacter) || !((PlayerCharacter) currentActor).keyPressed(this, code); } @Override public Location move(GridDirection dir) { if (movePoints < 1) { throw new IllegalMoveException("You're out of moves!"); } Location newLoc = map.go(currentLocation, dir); map.remove(currentLocation, currentActor); map.add(newLoc, currentActor); currentLocation = newLoc; movePoints--; return currentLocation; } @Override public Item pickUp(Item item) { if (item != null && map.has(currentLocation, item) && !(item instanceof Static)) { if (item instanceof Actor) { if (item.getCurrentHealth() / item.getMaxHealth() < 3) { map.remove(currentLocation, item); return item; } else { return null; } } else if (currentActor.getAttack() > item.getDefense()) { map.remove(currentLocation, item); return item; } else { return null; } } else { return null; } } @Override public ITurtle getPainter() { return painter; } @Override public Printer getPrinter() { return printer; } @Override public int[] getFreeTextAreaBounds() { int[] area = new int[4]; area[0] = getWidth() + 1; area[1] = 1; area[2] = printer.getLineWidth(); area[3] = printer.getPageHeight() - 5; return area; } @Override public void clearFreeTextArea() { printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5); } @Override public void clearFreeGraphicsArea() { painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0, painter.getWidth() - getWidth() * printer.getCharWidth(), (printer.getPageHeight() - 5) * printer.getCharHeight()); } @Override public double[] getFreeGraphicsAreaBounds() { double[] area = new double[4]; area[0] = getWidth() * printer.getCharWidth(); area[1] = 0; area[2] = painter.getWidth(); area[3] = getHeight() * printer.getCharHeight(); return area; } @Override public Actor getActor() { return currentActor; } public Location setCurrent(Actor actor) { currentLocation = map.getLocation(actor); if (currentLocation != null) { currentActor = actor; movePoints = 1; } return currentLocation; } public Actor setCurrent(Location loc) { List list = map.getActors(loc); if (!list.isEmpty()) { currentActor = list.get(0); currentLocation = loc; movePoints = 1; } return currentActor; } public Actor setCurrent(int x, int y) { return setCurrent(map.getLocation(x, y)); } @Override public Random getRandom() { return random; } @Override public List locationDirection(Location start, Location target) { int targetX = target.getX(), targetY = target.getY(); int startX = start.getX(), startY = start.getY(); List dirs = new ArrayList<>(); boolean yDifferenceIsLarger = Math.abs(targetX - startX) < Math.abs(targetY - startY); if (targetX > startX && targetY > startY) { if (yDifferenceIsLarger) { dirs.add(GridDirection.SOUTH); dirs.add(GridDirection.EAST); } else { dirs.add(GridDirection.EAST); dirs.add(GridDirection.SOUTH); } } else if (targetX > startX && targetY < startY) { if (yDifferenceIsLarger) { dirs.add(GridDirection.NORTH); dirs.add(GridDirection.EAST); } else { dirs.add(GridDirection.EAST); dirs.add(GridDirection.NORTH); } } else if (targetX < startX && targetY > startY) { if (yDifferenceIsLarger) { dirs.add(GridDirection.SOUTH); dirs.add(GridDirection.WEST); } else { dirs.add(GridDirection.WEST); dirs.add(GridDirection.SOUTH); } } else if (targetX < startX && targetY < startY) { if (yDifferenceIsLarger) { dirs.add(GridDirection.NORTH); dirs.add(GridDirection.WEST); } else { dirs.add(GridDirection.WEST); dirs.add(GridDirection.NORTH); } } else if (targetX > startX) { dirs.add(GridDirection.EAST); } else if (targetX < startX) { dirs.add(GridDirection.WEST); } else if (targetY > startY) { dirs.add(GridDirection.SOUTH); } else if (targetY < startY) { dirs.add(GridDirection.NORTH); } return dirs; } }