package org.erikaredmark.monkeyshines;
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import org.erikaredmark.monkeyshines.World.GoodieLocationPair;
import org.erikaredmark.monkeyshines.background.Background;
import org.erikaredmark.monkeyshines.background.SingleColorBackground;
import org.erikaredmark.monkeyshines.resource.WorldResource;
/**
* Improving the screen-by-screen architecture is NOT something I will be doing.
* This provides the classic Monkey Shines feel: Each screen is one part of a level. Going off to the side of a screen
* sends you to the next screen
* <p/>
* The initial screen is screen 1000. The Bonus screen can be determined by a constant (To allow more flexibility; the
* original used 10000.)
* <p/>
* This contains a lot of elements similar to World, but World is what contains the objects. These are only pointers.
* The large number of pointers allow the LevelScreen to draw itself and handle it's own collisions.
* <p/>
* Changes to a level screen are not persisted to a level. The level editor must use {@link LevelScreenEditor} objects
* that wrap level screens and store a list of changes to them to write out level data. This object, by itself, only
* represents the level as it is in one-instance of the game.
*
*/
public final class LevelScreen {
// Initialisation datas for a level screen
// Id IS repeated in the Map holding the screens. Kept here for ease of use with other algorithms
// that work only with screens and need an id.
private final int screenId;
private Background background;
private final TileMap map;
// Whilst this is generally final in gameplay, it is left non-final here so it may be modified by the level editor.
private ImmutablePoint2D bonzoStart;
private final List<Sprite> spritesOnScreen;
// state information for the screen. Bonzo can respawn where he came from at the velocity that
// he came into the screen
private BonzoSaveState bonzoCameFrom;
private ImmutablePoint2D bonzoLastOnGround;
// defaults to true on constructions; intended to allow level editor to stop sprite animations for easier
// selection and editing.
private boolean animateSprites;
// Graphics
// These are all pointers to what is stored in world.
private WorldResource rsrc;
/**
*
* Creates an empty level screen initialised to no tiles or sprites with a default background. The default
* background is either the first full background in the resource, the first pattern in the resource, or
* failing that, a solid colour of black.
*
* @param screenId
* the id of the screen, which must match with the id of the key that maps to this screen value in the world
* hash map. Id may be negative is so choosing
*
* @param rsrc
* a graphics resource to skin this level
*
* @param world
* a reference to the world that contains this level screen. Required for some world-level properties
* that affect all screens
*
* @return
* a new level screen
*
*/
public static final LevelScreen newScreen(int screenId, WorldResource rsrc) {
Background defaultBackground = rsrc.getBackgroundCount() > 0
? rsrc.getBackground(0)
: rsrc.getPatternCount() > 0
? rsrc.getPattern(0)
: new SingleColorBackground(Color.BLACK);
return new LevelScreen(screenId,
defaultBackground,
new TileMap(GameConstants.LEVEL_ROWS, GameConstants.LEVEL_COLS),
ImmutablePoint2D.of(0, 0),
new ArrayList<Sprite>(),
rsrc);
}
/**
*
* Intended for internal static factories and decoding system only
* BonzoStart is a resolved-to-tile-cordinate location, not pixel location
*
*/
public LevelScreen(final int screenId,
final Background background,
final TileMap map,
final ImmutablePoint2D bonzoStart,
final List<Sprite> spritesOnScreen,
final WorldResource rsrc) {
this.screenId = screenId;
this.background = background;
this.map = map;
this.bonzoStart = bonzoStart;
this.spritesOnScreen = spritesOnScreen;
this.rsrc = rsrc;
this.animateSprites = true;
}
/** Returns the screen id of this screen */
public int getId() { return this.screenId; }
/**
* Returns the background for this screen
* @return
*/
public Background getBackground() {
return this.background;
}
/**
*
* Only intended to be called from level editor: sets the background for the current screen.
*
* @param newBackground
* new background for this screen
*
*/
public void setBackground(Background newBackground) {
this.background = newBackground;
}
/**
*
* Gets the starting position of Bonzo in the level. The returned point is immutable: Use {@code Point2D#from(ImmutablePoint2D)}
* to get a mutable version
* <p/>
* This location is in <strong>Tile Coordinates</strong>, not pixel coordinates!
*
* @return
* the location bonzo starts on this level. Never {@code null}
*/
public ImmutablePoint2D getBonzoStartingLocation() {
return this.bonzoStart;
}
/**
*
* Same as {@code getBonzoStartingLocation} but returns values as pixel co-ordinates.
*
* @return
*/
public ImmutablePoint2D getBonzoStartingLocationPixels() {
return this.bonzoStart.multiply(GameConstants.TILE_SIZE_X, GameConstants.TILE_SIZE_Y);
}
/**
*
* Returns the location bonzo came from when entering this level, or the starting location if bonzo started
* on this screen.
*
* @return
* a new instance of a point that represents the location bonzo entered this screen. This initially is set to his
* starting location on the screen but is changed as he moves through the screens. Value never {@code null}
*
*/
public BonzoSaveState getBonzoCameFrom() {
return bonzoCameFrom;
}
/**
* Called when Bonzo enters the screen from another Screen. Sets the location he came from so if he dies on this screen,
* he can return to that position at the given velocity.
*
* @param bonzoCameFrom
* the location Bonzo entered the screen from
*/
public void setBonzoCameFrom(final BonzoSaveState bonzoCameFrom) {
this.bonzoCameFrom = bonzoCameFrom;
}
/**
* If, for some reason, the location Bonzo Came from becomes invalid, this resets it.
*/
public void resetBonzoCameFrom() {
this.bonzoCameFrom = BonzoSaveState.fromPoint(bonzoStart);
}
/**
*
* Resets screen. Hazards are returned to their locations, and sprites are reset to initial positions.
*
*/
public void resetScreen() {
resetSprites();
map.resetTiles();
}
/**
* Sprites move around the screen. This makes them return to where they spawn when the screen is first entered.
* Typically called alongside resetBonzo when Bonzo dies.
*/
private void resetSprites() {
for (Sprite nextSprite : spritesOnScreen) {
nextSprite.resetSprite();
}
}
// Careful! This is return by reference
public List<Sprite> getSpritesOnScreen() {
return spritesOnScreen;
}
/**
* Adds a sprite to the screen. Typically reserved for level editor.
*
* @param sprite
* the sprite to add. The sprite MUST have been skinned with a valid graphics resource first
*/
public void addSprite(Sprite sprite) {
this.spritesOnScreen.add(sprite);
}
/**
*
* Removes the given sprite off the screen. Typically reserved for level editor.
*
* @param sprite
* the sprite to remove
*
*/
public void removeSprite(Sprite sprite) {
this.spritesOnScreen.remove(sprite);
}
/**
*
* Replaces the sprite given by the first argument in the list with the one in the second argument.
* The order of the list is preserved.
* <p/>
* If {@code sprite} is not found in the sprite list, this method throws an exception.
*
* @param sprite
* old sprite
*
* @param newSprite
* new sprite to replace it with
*
* @throws IllegalArgumentException
* if the first sprite argument is not found in the list
*
*/
public void replaceSprite(Sprite sprite, Sprite newSprite) {
for (ListIterator<Sprite> it = this.spritesOnScreen.listIterator(); it.hasNext(); /* no op */) {
Sprite next = it.next();
if (sprite.equals(next) ) {
it.remove();
it.add(newSprite);
return;
}
}
// Natural termination of loop means not found. not found is not legal.
throw new IllegalArgumentException("Sprite not found in level screen to replace: " + sprite);
}
/**
* Returns a list of sprites within the given area. This is typically designed for the level editor.
* <p/>
* A rectangle, whose centre is 'point' and whose distance from the centre to a side is 'size', is drawn as a collision
* rectangle to find sprites in the area. Hence, the 'size' field is half the length and width of the final rectangle. Any
* sprites in which 'ANY' part of their 40x40 graphic touches within this rectangle will be returned.
*
* @param point
* centre of rectangle
*
* @param size
* distance from centre to a side of the rectangle
*
* @return
* a list of sprites in the area. This may return an empty list if there is none, but never {@code null}
*/
public List<Sprite> getSpritesWithin(ImmutablePoint2D point, int size) {
// Convert centre point into upper left point.
final ImmutableRectangle box = ImmutableRectangle.of(point.x() - (size / 2), point.y() - (size / 2), size, size);
final List<Sprite> returnList = new ArrayList<>();
for (Sprite s : spritesOnScreen) {
final ImmutableRectangle rect = s.getCurrentBounds();
if (box.intersect(rect) != null) returnList.add(s);
}
return returnList;
}
/**
*
* Sets bonzos starting position on this screen to be somewhere else. Should only be called by level editor.
*
* @param point
* bonzos new starting location
*
*/
public void setBonzoStartingLocation(ImmutablePoint2D point) {
this.bonzoStart = point;
}
/**
*
* Resets bonzos location last on the ground to 'null' The last on ground
* location is only used when bonzo dies on a screen where he never had a
* safe landing, and has to go back one or more screens to the last place he was
* safe on ground.
* <p/>
* Otherwise, this variable is set whenever bonzo executes a jump from some
* ground, or lands from a jump on ground. If bonzo dies from a jump or fall
* this variable is not set to that value
*
*/
public void resetBonzoOnGround() {
bonzoLastOnGround = null;
}
/**
*
* Returns the last location on this screen bonzo was on the ground, or
* {@code null} if he never was (which is possible for screens he just falls
* through). Resetting bonzo to the last place on GROUND should be done only
* if the screen he died on had no ground (as in, moving him to the last location
* he entered the screen from may kill him again).
*
* @return
* the last location he was on the ground safely in the screen, or
* {@code null} if he never landed on ground in the screen.
*
*/
public ImmutablePoint2D getBonzoLastOnGround() {
return bonzoLastOnGround;
}
/**
*
* Called during bonzo collison algorithms. Allows the screen
* to store his last safe ground landing.
* TODO there is a minute chance that this location may be set to a place a sprite will
* be if the screen is restarted. For now, keep it as this algorithm unless it is determined
* to impede level design and become a major issue.
*
* @param ground
* safe ground location for a respawn
*
*/
void setBonzoLastOnGround(ImmutablePoint2D ground) {
bonzoLastOnGround = ground;
}
/**
*
* Draw background, tiles, and sprites in one swoop.
*
* @param g2d
*
*/
public void paint(Graphics2D g2d) {
background.draw(g2d);
map.paint(g2d, rsrc);
for (Sprite s : spritesOnScreen) {
s.paint(g2d);
}
}
/**
*
* Runs one tick of time for the given game screen.
*
*/
public void update() {
map.update();
if (animateSprites) {
for (Sprite s : spritesOnScreen) {
s.update();
}
}
}
/**
*
* Paints the level screen to the graphics context with no sprites. This
* is intended as the first step for making a thumbnail of a level screen.
* This does not update the game at all.
* <p/>
* The tilemap is drawn over the background
*
*/
public void paintForThumbnail(Graphics2D g2d) {
background.draw(g2d);
map.paint(g2d, rsrc);
}
/**
*
* Returns the underlying tile map backing this level. Changes to the array will affect tiles in the world, so
* this is intended only for either editors, internal methods, or viewing the map.
*
*/
public TileMap getMap() { return this.map; }
/**
*
* Intended for level editor only; turns on and off sprite animations. If sprite animations are turned off,
* the sprites do not animate or move around the screen in any way.
*
* @param animation
* {@code true} to start animating, {@code false} to stop animating
*
*/
public void setSpriteAnimation(boolean animation) {
this.animateSprites = animation;
}
/**
*
* @return
* {@code true} if sprites are currently animating, {@code false} if otherwise
*
*/
public boolean getSpriteAnimation() {
return this.animateSprites;
}
/**
*
* Provides a deep-copy of all the elements of this screen, with the new id and a world reference
* so that the goodies that appear on this screen can have copies made for the next screen. This method
* ALSO has the side-effect of adding the new level to the given world (a requirement in order for the
* goodie information to transfer properly), so calling this method is good enough to actually add the
* new screen to the world. Returns an instance of the created screen
* <p/>
* WARNING: If the target screen, {@code newId}, already exists in the world it WILL be overwritten.
*
* @param levelScreen
* the level screen to copy
*
* @param newId
* the new id the copy will take on
*
* @param world
* a reference to the world so the Goodies entries can be updated
*
* @return
* a new instance of this level, identical in design to the target level but existing in a different
* location in the world.
*
*/
public static LevelScreen copyAndAddToWorld(LevelScreen levelScreen, int newId, World world) {
// Handle Tiles
TileMap newTiles = levelScreen.getMap().copy();
// Handle Sprites
List<Sprite> originalSprites = levelScreen.getSpritesOnScreen();
List<Sprite> newSprites = new ArrayList<>(originalSprites.size() );
for (Sprite s : originalSprites) {
newSprites.add(Sprite.copyOf(s) );
}
LevelScreen newScreen =
new LevelScreen(newId,
levelScreen.background,
newTiles,
levelScreen.getBonzoStartingLocation(),
newSprites,
levelScreen.rsrc);
world.addOrReplaceScreen(newScreen);
// Handle goodies
Collection<GoodieLocationPair> originalGoodiePairs = world.getGoodiesForLevel(levelScreen.getId() );
for (GoodieLocationPair pair : originalGoodiePairs) {
WorldCoordinate loc = pair.location;
world.addGoodie(newId, loc.getRow(), loc.getCol(), pair.goodie.getGoodieType() );
}
return newScreen;
}
}