package org.erikaredmark.monkeyshines.editor.importlogic;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import org.erikaredmark.monkeyshines.AnimationSpeed;
import org.erikaredmark.monkeyshines.AnimationType;
import org.erikaredmark.monkeyshines.ImmutablePoint2D;
import org.erikaredmark.monkeyshines.ImmutableRectangle;
import org.erikaredmark.monkeyshines.Sprite.ForcedDirection;
import org.erikaredmark.monkeyshines.Sprite.SpriteType;
import org.erikaredmark.monkeyshines.Sprite.TwoWayFacing;
import org.erikaredmark.monkeyshines.editor.importlogic.WorldTranslationException.TranslationFailure;
import org.erikaredmark.monkeyshines.resource.WorldResource;
import static org.erikaredmark.monkeyshines.editor.importlogic.TranslationUtil.*;
/**
*
* This class mimics 1:1 the C structure in the original game that was saved in the old level file
* format. For ease of use, translation utilities can read this structure whole-cloth from a binary
* stream.
* <p/>
* Instances of this class are immutable and are built by the translator using the internal builder.
* They can ONLY be constructed from a valid binary stream.
* <p/>
* Use accessor methods to get translated values for the port. Directly access the final public data
* to get raw values.
*
* @author Erika Redmark
*
*/
final class MSSpriteData {
public final ImmutablePoint2D location;
public final ImmutablePoint2D minimum;
public final ImmutablePoint2D maximum;
public final ImmutablePoint2D speed;
public final int id;
public final int flags;
// Precomputed non-raw data
private final int portId;
private final boolean alwaysFacingRight;
private final boolean alwaysFacingLeft;
// Bit twiddling for flags
private static final int FLAG_INCREASING_FRAMES = 1;
// Second bit is skipped. It has a 1 for cycling frames. No point since a 0 for increasing frames means the same thing
private static final int FLAG_SLOW_ANIMATION = 1 << 2;
private static final int FLAG_TWO_WAY_FACING_VERTICAL = 1 << 4;
private static final int FLAG_TWO_WAY_FACING_HORIZONTAL = 1 << 3;
// Sixth bit is probably unused.
// Seventh bit if for door, but ID 0 is always bonus, ID 1 is always exit. This flag is not relevant
// and probably wasn't relevant in the original game either.
private static final int FLAG_ENERGY_DRAINER = 1 << 7;
private MSSpriteData(final ImmutablePoint2D location,
final ImmutablePoint2D minimum,
final ImmutablePoint2D maximum,
final ImmutablePoint2D speed,
final int id,
final int flags,
final WorldResource rsrc) {
this.location = location;
this.minimum = minimum;
this.maximum = maximum;
this.speed = speed;
this.id = id;
this.flags = flags;
int idCounter = 0;
int actualId = 0;
boolean facingRight = false;
// TODO interpret flags to allow for always left facing sprites.
for (int i = 0; i < rsrc.getSpritesCount(); ++i) {
if (idCounter == id) {
actualId = i;
break;
}
BufferedImage img = rsrc.getSpritesheetFor(i);
idCounter += img.getHeight() / 40;
// need to check if we skipped the id, indicating a right way facing sprite.
if (idCounter == id + 1) {
actualId = i;
facingRight = true;
break;
}
}
this.portId = actualId;
this.alwaysFacingRight = facingRight;
// If the sprite is not always facing right, there is a CHANCE it may
// always be facing left based on the flags.
if (!(facingRight) ) {
if ( (flags & FLAG_TWO_WAY_FACING_HORIZONTAL) == 0 ) {
this.alwaysFacingLeft = true;
} else {
this.alwaysFacingLeft = false;
}
} else {
this.alwaysFacingLeft = false;
}
}
/**
*
* Reads a single MSSpriteData object from the given stream. The stream must currently be positioned at the beginning of the
* definition of the proper object (up to translators to handle that). At the conclusion of this method, the stream will have
* been advanced 20 bytes, the size of this data as stored in the binary form.
* <p/>
* This method REQUIRES a world resource because the original game
* considered the left/right facing of a sprite two unique ids, the port does not. Only by counting
* each sprite in the resource pack, in order, as 1 or 2 can the actual sprite id be determined. If
* it ends up skipping the id, that means the sprite is facing right only.
* this information is precomputed for each sprite.
*
* @param is
* input stream
*
* @return
* an instance of this object
*
* @throws IOException
* if something unexpected happens reading the stream
*
* @throws WorldTranslationException
* if the stream is either malformed or too small
*
*/
static MSSpriteData fromStream(InputStream is, WorldResource rsrc) throws IOException, WorldTranslationException {
// Just to ease on the typing for this method.
final TranslationFailure FAIL = TranslationFailure.WRONG_LEVEL_SIZE;
int locationY = readMacShort(is, FAIL, "Could not read sprite location Y");
int locationX = readMacShort(is, FAIL, "Could not read sprite location X");
int minimumY = readMacShort(is, FAIL, "Could not read sprite minimum Y");
int minimumX = readMacShort(is, FAIL, "Could not read sprite minimum X");
int maximumY = readMacShort(is, FAIL, "Could not read sprite maximum Y");
int maximumX = readMacShort(is, FAIL, "Could not read sprite maximum X");
int speedY = readMacShort(is, FAIL, "Could not read sprite speed Y");
int speedX = readMacShort(is, FAIL, "Could not read sprite speed X");
int spriteId = readMacShort(is, FAIL, "Could not read sprite id");
int flags = readMacShort(is, FAIL, "Could not read sprite flags");
// byte[] flagsRaw = new byte[2];
// read(is, flagsRaw, FAIL, "Could not read sprite flags");
//
// ByteBuffer flags = ByteBuffer.allocate(2);
// flags.order(ByteOrder.LITTLE_ENDIAN);
// flags.put(flagsRaw);
return new MSSpriteData(ImmutablePoint2D.of(locationX, locationY),
ImmutablePoint2D.of(minimumX, minimumY),
ImmutablePoint2D.of(maximumX, maximumY),
ImmutablePoint2D.of(speedX, speedY),
spriteId,
flags,
rsrc);
}
/**
*
* See {@code fromStream(InputStream, WorldResource)}. Reads an array of instances of this class. The amount
* is determined by the size parameter. The stream will be read by 20 * size bytes at the conclusion of this
* method
*
*/
static MSSpriteData[] arrayFromStream(InputStream is, int size, WorldResource rsrc) throws IOException, WorldTranslationException {
MSSpriteData[] data = new MSSpriteData[size];
for (int i = 0; i < size; ++i) {
data[i] = fromStream(is, rsrc);
}
return data;
}
// Some information here is not even the same as it is in the original game level editor.
// basic notes: -80s are because original game included UI banner as playable field, port doesn't
// -40s are because bounds in port are widths, not actual bounds, so the sprite size is removed.
/**
* Returns the location of the sprite in terms of the port, not the original.
*/
ImmutablePoint2D getPortLocation() {
return ImmutablePoint2D.of(location.x(), location.y() - 80);
}
/**
* Returns bounding box for the port
*/
ImmutableRectangle getPortBoundingBox() {
// Maximum x and y are Right and bottom (in the level editor) - 40. Based the info here
// https://github.com/ErikaRedmark/monkey-shines-java-port/wiki/Porting-Old-Worlds
// the -40s cancel so it is just max-min.
return ImmutableRectangle.of(minimum.x(),
minimum.y() - 80,
maximum.x() - minimum.x(),
maximum.y() - minimum.y() );
}
/**
* Returns the forced direction, if any, of the sprite
*/
ForcedDirection getPortDirection() {
// Always facing left and always facing right can apply to always bottom/top identically given how
// the sprite graphics are computed.
if (alwaysFacingLeft) return ForcedDirection.LEFT_DOWN;
else if (alwaysFacingRight) return ForcedDirection.RIGHT_UP;
// Else if not returned yet
return ForcedDirection.NONE;
}
/**
* Returns whether the sprite has two sets vertical, horizontal, or is just one set.
* @return
*/
TwoWayFacing getTwoFacing() {
if ((flags & FLAG_TWO_WAY_FACING_HORIZONTAL) != 0) {
return TwoWayFacing.HORIZONTAL;
} else if ((flags & FLAG_TWO_WAY_FACING_VERTICAL) != 1) {
return TwoWayFacing.VERTICAL;
} else {
return TwoWayFacing.SINGLE;
}
}
/**
* Returns the velocity of the sprite
*/
ImmutablePoint2D getPortVelocity() {
// Port inverts speeds.
return ImmutablePoint2D.of(-speed.x(), -speed.y() );
}
/**
* Returns the id of the sprite, for the PORT, not the raw id.
*/
int getSpriteId() {
return portId;
}
/**
* Returns if the sprite should always face right.
*/
boolean getSpriteFacingRight() {
return alwaysFacingRight;
}
boolean getSpriteFacingLeft() {
return alwaysFacingLeft;
}
/**
* Returns the animation type of the sprite.
*/
AnimationType getSpriteAnimationType() {
return (flags & FLAG_INCREASING_FRAMES) != 0
? AnimationType.INCREASING_FRAMES
: AnimationType.CYCLING_FRAMES;
}
/**
* Returns the animation speed of the sprite
*/
AnimationSpeed getSpriteAnimationSpeed() {
return (flags & FLAG_SLOW_ANIMATION) != 0
? AnimationSpeed.SLOW
: AnimationSpeed.NORMAL;
}
/**
* Returns the sprite type, Normal, Energy Drainer, Exit Door, or Bonus Door. Harmless sprites
* did not exist in the original game.
*/
SpriteType getSpriteType() {
// original game ALWAYs had 0 be the bonus door, and 1 be the exit door. The editor manual
// even advised not to change this. So, we can easily determine the bonus and exit doors
// We check doors first so that a level always has doors.
if (id == 0) {
return SpriteType.BONUS_DOOR;
} else if (id == 1) {
return SpriteType.EXIT_DOOR;
} else {
return (flags & FLAG_ENERGY_DRAINER) != 0
? SpriteType.HEALTH_DRAIN
: SpriteType.NORMAL;
}
}
}