Kristian Knarvik a869cb1049 Kind of finished
It is possible to win and lose the game.
The boss is quite hard to beat, but it should be possible if RNGsus is with you.
Girls have an increasing chance of having a weapon based on map lvl.
Player has been nerfed.
Shields have been nerfed.
Girls' hp has been nerfed.
2018-03-21 00:10:42 +01:00

810 lines
25 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package inf101.v18.rogue101.game;
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;
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.ILocation;
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.Manga;
import inf101.v18.rogue101.items.*;
import inf101.v18.rogue101.examples.Rabbit;
import inf101.v18.rogue101.map.GameMap;
import inf101.v18.rogue101.map.IGameMap;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.map.MapReader;
import inf101.v18.rogue101.objects.*;
import inf101.v18.rogue101.shared.NPC;
import inf101.v18.rogue101.states.Attack;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
public class Game implements IGame {
/**
* All the IActors that have things left to do this turn
*/
private List<IActor> actors = Collections.synchronizedList(new ArrayList<>());
/**
* For fancy solution to factory problem
*/
private Map<String, Supplier<IItem>> itemFactories = new HashMap<>();
/**
* Useful random generator
*/
private Random random = new Random();
/**
* Saves the last three messages
*/
private List<String> lastMessages = new ArrayList<>();
/**
* The game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
private IGameMap map;
private List<IGameMap> maps = new ArrayList<>();
private int currentLVL = 0;
private IActor currentActor;
private ILocation currentLocation;
private int movePoints = 0;
private final ITurtle painter;
private final Printer printer;
private int numPlayers = 0;
boolean won = false;
public Game(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 potion"};
for (int i = 0; i < info.length; i++) {
this.printer.printAt(Main.COLUMN_RIGHTSIDE_START, 1 + i, info[i]);
}
}
public Game(String mapString) {
printer = new Printer(1280, 720);
painter = new TurtlePainter(1280, 720);
addFactory();
IGrid<String> inputGrid = MapReader.readString(mapString);
this.map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
}
@Override
public void addItem(IItem item) {
map.add(currentLocation, item);
}
@Override
public void addItem(String sym) {
IItem item = createItem(sym);
if (item != null)
map.add(currentLocation, item);
}
/**
* Calculates the attack of an IActor based on equipped items.
*
* @return The attack
*/
private int getAttack() {
int damage = currentActor.getAttack() + random.nextInt(20) + 1;
IBuffItem buff = (IBuffItem)currentActor.getItem(IBuffItem.class);
if (buff != null) {
damage += buff.getBuffDamage();
}
return damage;
}
/**
* Gets the defence of the current target.
*
* @param target The target to evaluate
* @return The defence of the target
*/
private int getDefence(IItem target) {
int defence = target.getDefence() + 10;
IActor actor = (IActor) target;
IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class);
if (item != null) {
defence += item.getBuffDefence();
}
return defence;
}
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");
}
}
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");
}
}
/**
* Gets the damage done against the current target.
*
* @param target The target to evaluate.
* @return The damage done to the target.
*/
private int getDamage(IItem target) {
int damage = currentActor.getDamage();
IWeapon weapon = (IWeapon)currentActor.getItem(IWeapon.class);
if (weapon != null) {
damage += weapon.getWeaponDamage();
}
IActor actor = (IActor) target;
IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class);
if (item != null) {
damage -= item.getBuffDamageReduction();
}
return damage;
}
@Override
public ILocation attack(GridDirection dir, IItem target) {
ILocation loc = currentLocation.go(dir);
if (!map.has(loc, target)) {
throw new IllegalMoveException("Target isn't there!");
}
IWeapon weapon = (IWeapon) currentActor.getItem(IMeleeWeapon.class);
if (weapon != null) {
NPC.playSound(weapon.getSound());
} else {
NPC.playSound("audio/Realistic_Punch-Mark_DiAngelo-1609462330.wav");
}
if (getAttack() >= getDefence(target)) {
int actualDamage = target.handleDamage(this, target, getDamage(target));
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 ILocation rangedAttack(GridDirection dir, IItem target, Attack type) {
ILocation loc = currentLocation;
IWeapon weapon = null;
switch (type) {
case MAGIC:
weapon = (IWeapon) currentActor.getItem(IMagicWeapon.class);
break;
case RANGED:
weapon = (IWeapon) currentActor.getItem(IRangedWeapon.class);
}
if (weapon != null) {
NPC.playSound(weapon.getSound());
} else {
NPC.playSound("audio/Snow Ball Throw And Splat-SoundBible.com-992042947.wav");
}
if (getAttack() >= getDefence(target)) {
int damage = getDamage(target) / 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 (numPlayers == 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)
}
if (currentActor.isDestroyed()) // skip if it's dead
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
if (currentActor instanceof INonPlayer) {
// computer-controlled players do their stuff right away
((INonPlayer) currentActor).doTurn(this);
// remove any dead items from current location
map.clean(currentLocation);
return false;
} else if (currentActor instanceof IPlayer) {
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 (numPlayers > 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");
NPC.playSound("audio/Dying-SoundBible.com-1255481835.wav");
}
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);
NPC.playSound("audio/1_person_cheering-Jett_Rifkin-1851518140.wav");
won = true;
}
/**
* Loads a map with the desired name
*
* @param mapName Name of map, including extension.
*/
private void loadMap(String mapName) {
IGrid<String> 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);
}
IGameMap map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem item = createItem(inputGrid.get(loc));
if (item != null) {
map.add(loc, item);
}
}
this.map = map;
}
private void loadMap(String mapName, int lvl) {
IGrid<String> 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);
}
IGameMap map = new GameMap(inputGrid.getArea());
for (ILocation loc : inputGrid.locations()) {
IItem 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() {
numPlayers = 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<IItem> list = map.getAllModifiable(loc); // all items at loc
Iterator<IItem> li = list.iterator(); // manual iterator lets us remove() items
while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside
IItem 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 IPlayer) {
actors.add(0, (IActor) item); // we let the human player go first
synchronized (this) {
numPlayers++;
}
} else if (item instanceof IActor) {
actors.add((IActor) 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", Sword::new);
itemFactories.put("c", Chest::new);
itemFactories.put("B", Boss::new);
itemFactories.put("s", Staff::new);
itemFactories.put("b", Bow::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 IItem createItem(String sym) {
switch (sym) {
case " ":
return null;
default:
// alternative/advanced method
Supplier<IItem> factory = itemFactories.get(sym);
if (factory != null) {
return factory.get();
} else {
System.err.println("createItem: Don't know how to create a '" + sym + "'");
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;
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 (numPlayers == 0) {
map.draw(painter, printer);
} else {
((GameMap) map).drawVisible(painter, printer);
}
}
@Override
public boolean drop(IItem item) {
if (item != null) {
map.add(currentLocation, item);
return true;
} else
return false;
}
@Override
public boolean dropAt(ILocation loc, IItem 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<IItem> getLocalItems() {
return map.getItems(currentLocation);
}
@Override
public ILocation getLocation() {
return currentLocation;
}
@Override
public ILocation getLocation(GridDirection dir) {
if (currentLocation.canGo(dir))
return currentLocation.go(dir);
else
return null;
}
/**
* Return the game map. {@link IGameMap} gives us a few more details than
* {@link IMapView} (write access to item lists); the game needs this but
* individual items don't.
*/
@Override
public IMapView getMap() {
return map;
}
@Override
public List<GridDirection> getPossibleMoves() {
List<GridDirection> moves = new ArrayList<>();
for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) {
if (canGo(dir)) {
moves.add(dir);
}
}
return moves;
}
@Override
public List<ILocation> getVisible() {
List<ILocation> neighbours = map.getNeighbourhood(currentLocation, currentActor.getVision());
List<ILocation> invalid = new ArrayList<>();
for (ILocation neighbour : neighbours) {
for (ILocation tile : currentLocation.gridLineTo(neighbour)) {
if (map.hasWall(tile)) {
invalid.add(neighbour);
break;
}
}
}
neighbours.removeAll(invalid);
return neighbours;
}
@Override
public int getWidth() {
return 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 IPlayer) || !((IPlayer) currentActor).keyPressed(this, code);
}
@Override
public ILocation move(GridDirection dir) {
if (movePoints < 1)
throw new IllegalMoveException("You're out of moves!");
ILocation newLoc = map.go(currentLocation, dir);
map.remove(currentLocation, currentActor);
map.add(newLoc, currentActor);
currentLocation = newLoc;
movePoints--;
return currentLocation;
}
@Override
public IItem pickUp(IItem item) {
if (item != null && map.has(currentLocation, item) && !(item instanceof IStatic)) {
if (item instanceof IActor) {
if (item.getCurrentHealth() / item.getMaxHealth() < 3) {
map.remove(currentLocation, item);
return item;
} else {
return null;
}
} else if (currentActor.getAttack() > item.getDefence()) {
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 IActor getActor() {
return currentActor;
}
public ILocation setCurrent(IActor actor) {
currentLocation = map.getLocation(actor);
if (currentLocation != null) {
currentActor = actor;
movePoints = 1;
}
return currentLocation;
}
public IActor setCurrent(ILocation loc) {
List<IActor> list = map.getActors(loc);
if (!list.isEmpty()) {
currentActor = list.get(0);
currentLocation = loc;
movePoints = 1;
}
return currentActor;
}
public IActor setCurrent(int x, int y) {
return setCurrent(map.getLocation(x, y));
}
@Override
public Random getRandom() {
return random;
}
@Override
public List<GridDirection> locationDirection(ILocation start, ILocation target) {
int targetX = target.getX(), targetY = target.getY();
int startX = start.getX(), startY = start.getY();
List<GridDirection> dirs = new ArrayList<>();
if (targetX > startX && targetY > startY) {
if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
dirs.add(GridDirection.SOUTH);
dirs.add(GridDirection.EAST);
} else {
dirs.add(GridDirection.EAST);
dirs.add(GridDirection.SOUTH);
}
} else if (targetX > startX && targetY < startY) {
if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
dirs.add(GridDirection.NORTH);
dirs.add(GridDirection.EAST);
} else {
dirs.add(GridDirection.EAST);
dirs.add(GridDirection.NORTH);
}
} else if (targetX < startX && targetY > startY) {
if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
dirs.add(GridDirection.SOUTH);
dirs.add(GridDirection.WEST);
} else {
dirs.add(GridDirection.WEST);
dirs.add(GridDirection.SOUTH);
}
} else if (targetX < startX && targetY < startY) {
if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) {
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;
}
}