/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.shavenpuppy.jglib.sprites; import java.io.ObjectStreamException; import java.io.Serializable; import java.util.Stack; import org.lwjgl.util.Color; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.vector.ReadableVector2f; import org.lwjgl.util.vector.Vector2f; import com.shavenpuppy.jglib.resources.ResourceArray; import com.shavenpuppy.jglib.util.FPMath; import com.shavenpuppy.jglib.util.Util; /** * A Sprite has associated with it an Animation and frame counter OR a single SpriteImage, and has a position * and offset. */ public class Sprite implements Serializable { private static final long serialVersionUID = 1L; /** Appearance to use when sprite is accidentally imageless */ private static SpriteImage missingImage; /** Index of sprite into the sprite engine pool */ int index; /** The sprite engine */ private final SpriteEngine engine; /** The sprite allocator (used for serialization purposes - make sure this is SMALL! */ private SpriteAllocator allocator; /** The sprite's current owner - used for debugging more than anything else */ private Serializable owner; /** Are we allocated? */ private boolean allocated; /** The Animation if any */ private Animation animation; /** Framelist, if any */ private ResourceArray frameList; /** Index into framelist */ private int frame; /** Loop counter */ private int loop; /** Current frame tick */ private int tick; /** Animation sequence */ private int sequence; /** The current "event" state */ private int event; /** Pause animation */ private boolean paused; /** Current child x and y offsets */ private float childXOffset, childYOffset; /** The current image */ private SpriteImage image; /** Current style */ private Style style; /** Layer */ private int layer; /** Sublayer */ private int subLayer; /** World coordinates */ private float x, y; /** Offset coordinates */ private float ox, oy; /** Y Sort offset */ private float ySortOffset; /** Flash mode: renders an additive mode version on top */ private boolean flash; /** Colours of the four corners (bottom left, bottom right, top right, top left) */ private final ReadableColor[] color = new ReadableColor[] { Color.WHITE, Color.WHITE, Color.WHITE, Color.WHITE }; /** Handy constant */ private static final int NO_SCALE = FPMath.fpValue(1); /** Sprite scale */ private int xscale = NO_SCALE, yscale = NO_SCALE; /** Sprite alpha 0..255 */ private int alpha = 255; /** Rotation angle */ private int angle = 0; /** Sprite visibility */ private boolean visible = true; /** Sprite active flag */ private boolean active = true; /** Mirrored */ private boolean mirrored; /** Flipped */ private boolean flipped; /** Offset by childXOffset/childYOffset*/ private boolean doChildOffset; /** Stack entries */ static class StackEntry implements Serializable { private static final long serialVersionUID = 1L; final Animation animation; final int sequence; StackEntry(Animation animation, int sequence) { this.animation = animation; this.sequence = sequence; } } /** Stack */ private Stack<StackEntry> stack; /** * Constructor for Sprite. */ Sprite(SpriteEngine engine) { super(); this.engine = engine; } /** * @return the Sprite Engine */ public SpriteEngine getEngine() { return engine; } public void setAllocator(SpriteAllocator allocator) { this.allocator = allocator; } public SpriteAllocator getAllocator() { return allocator; } /** * Copy a source sprite. Only information required for rendering is copied. * @param src */ void copy(Sprite src) { image = src.image; style = src.style; layer = src.layer; subLayer = src.subLayer; x = src.x; y = src.y; ox = src.ox; oy = src.oy; ySortOffset = src.ySortOffset; flash = src.flash; for (int i = 0; i < 4; i ++) { color[i] = src.color[i]; } xscale = src.xscale; yscale = src.yscale; alpha = src.alpha; angle = src.angle; visible = src.visible; active = src.active; mirrored = src.mirrored; flipped = src.flipped; doChildOffset = src.doChildOffset; } /** * Init this sprite back to construction values * @param newOwner The sprite's new owner */ public void init(Serializable newOwner) { if (allocated) { throw new IllegalStateException(this + "Already allocated: "+newOwner+" can't have it!"); } allocated = true; reset(); owner = newOwner; active = true; visible = true; image = null; flash = false; ox = 0; oy = 0; x = 0; y = 0; color[0] = ReadableColor.WHITE; color[1] = ReadableColor.WHITE; color[2] = ReadableColor.WHITE; color[3] = ReadableColor.WHITE; layer = 0; subLayer = 0; xscale = NO_SCALE; yscale = NO_SCALE; alpha = 255; angle = 0; mirrored = false; flipped = false; doChildOffset = false; style = null; } /** * @return the owner */ public Serializable getOwner() { return owner; } /** * @return the x coordinate */ public float getX() { return x; } /** * Sets the x coordinate. * @param x The x to set */ public void setX(float x) { this.x = x; } /** * Gets the y coordinate. * @return the y coordinate */ public float getY() { return y; } /** * Sets the y. * @param y The y to set */ public void setY(float y) { this.y = y; } /** * Convenience accessor */ public Vector2f getLocation(Vector2f ret) { if (ret == null) { ret = new Vector2f(x, y); } else { ret.set(x, y); } return ret; } /** * Convenience accessor */ public void setLocation(float x, float y) { this.x = x; this.y = y; } public void setLocation(ReadableVector2f newLocation) { this.x = newLocation.getX(); this.y = newLocation.getY(); } /** * Convenience accessor */ public Vector2f getOffset(Vector2f ret) { if (ret == null) { ret = new Vector2f(ox, oy); } else { ret.set(ox, oy); } return ret; } /** * Convenience accessor */ public void setOffset(float ox, float oy) { this.ox = ox; this.oy = oy; } /** * Convenience accessor */ public void setOffset(ReadableVector2f location) { this.ox = location.getX(); this.oy = location.getY(); } /** * Frame ticker. Call this every frame. */ public void tick() { // If the sprite is not active, don't do anything. // If there's no animation, we don't need to do anything. // If we're paused we don't need to do anything. if (!active || animation == null || isPaused()) { return; } animation.animate(this); } /** * Set flash mode on or off. This renders the sprite more brightly. * @param flash */ public void setFlash(boolean flash) { this.flash = flash; } /** * Is the sprite visible * @return true if the sprite is visible */ public boolean isVisible() { return visible; } /** * Sets whether the sprite is visible or not * @param visible Sprite visibility */ public void setVisible(boolean visible) { this.visible = visible; } /** * Gets the alpha transparency of the whole sprite. * @return int between 0 and 255 */ public int getAlpha() { return alpha; } /** * Sets the alpha transparency of the whole sprite. * @param alpha The alpha to set, which should be between 0 and 255 */ public void setAlpha(int alpha) { this.alpha = alpha; } /** * Gets the scale. Scale is 16:16 fixed point. * @return Returns a float */ public int getXScale() { return xscale; } /** * Gets the scale. Scale is 16:16 fixed point. * @return Returns a float */ public int getYScale() { return yscale; } /** * Sets the scale. Scale is specified in 8-bit fixed point. * @param scale The scale to set */ public void setScale(int scale) { this.xscale = scale; this.yscale = scale; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#setScale(int, int) */ public void setScale(int xscale, int yscale) { this.xscale = xscale; this.yscale = yscale; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Scaled#adjustScale(int, int) */ public void adjustScale(int xscale, int yscale) { this.xscale += xscale; this.yscale += yscale; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Transparent#adjustAlpha(int) */ public void adjustAlpha(int delta) { alpha = Math.max(0, Math.min(255, alpha + delta)); } /** * Determines whether the sprite is active. If it isn't active is is simply * not processed at all by the sprite renderer. * @return true if the sprite is active */ public boolean isActive() { return active; } /** * Sets whether the sprite is active. * @param active The active to set * @see isActive() */ public void setActive(boolean active) { this.active = active; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animation.Animated#hide() */ public void hide() { setVisible(false); } /** * Sets the value of one of the sprite's corner colors, used to * attenuate it when it is drawn. * @param index The color index (0..3: BL, BR, TR, TL) * @param r The new red value * @param g The new green value * @param b The new blue value * @param a The new alpha value */ public void setColor(int index, ReadableColor color) { this.color[index] = color; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#setColors(org.lwjgl.util.ReadableColor) */ public void setColors(ReadableColor src) { color[0] = src; color[1] = src; color[2] = src; color[3] = src; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#setAngle(int) */ public void setAngle(int angle) { this.angle = angle % 0xFFFF; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#getAngle() */ public int getAngle() { return angle; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#adjustAngle(int) */ public void adjustAngle(int delta) { angle = (angle + delta) % 0xFFFF; } /** * Returns the color of one of the sprite's corners. * @param index The color index (0..3: BL, BR, TR, TL) * @param color The color to store the values in, or null to create a new one * @return a color */ public ReadableColor getColor(int index) { return color[index]; } /** * @see com.shavenpuppy.jglib.sprites.Animated#setCurrentImage(com.shavenpuppy.jglib.sprites.SpriteImage) */ public void setImage(SpriteImage image) { this.image = image; if (image == null) { return; } assert image.isCreated() : "Image "+image+" not created!"; assert image.getStyle() != null : "Image "+image+" has no style!"; } /** * @see com.shavenpuppy.jglib.sprites.Animated#deactivate() */ public void deactivate() { setActive(false); } /** * @see com.shavenpuppy.jglib.sprites.Animated#getCurrentImage() */ public SpriteImage getImage() { return image;//image == null ? missingImage : image; } /** * Set the override style * @param style */ public void setStyle(Style style) { this.style = style; } /** * @return the override style */ public Style getStyle() { return style != null ? style : getImage() != null ? getImage().getStyle() : null; } /** * @see com.shavenpuppy.jglib.sprites.Animated#moveLocation(int, int) */ public void moveLocation(int dx, int dy) { x += dx; y += dy; } /** * @see com.shavenpuppy.jglib.sprites.Animated#moveOffset(int, int) */ public void moveOffset(int dx, int dy) { ox += dx; oy += dy; } /** * Returns the layer. * @return int */ public int getLayer() { return layer; } /** * Sets the layer. * @param layer The layer to set */ public void setLayer(int layer) { this.layer = layer; } /** * Deallocate this sprite and return it to the sprite engine */ public void deallocate() { if (!allocated) { assert false; return; } setStyle(null); allocated = false; engine.deallocate(this); owner = null; } /** This is intended only for use by the containing SpriteEngine as a way to do fast .clear operations. */ void forceDeallocate() { assert allocated; allocated = false; visible = false; } @Override public String toString() { return "Sprite[idx="+index+", owner="+owner+", image="+image+", active="+active+", visible="+visible+", position="+x+","+y+", "+getAnimation()+", "+getStyle()+"]"; } /** * @return true if the sprite is "flashing" */ public boolean isFlashing() { return flash; } /** * Sets the sprite's appearance. * @param appearance */ public void setAppearance(Appearance appearance) { if (appearance == null) { image = null; setAnimation(null); rewind(); } else{ appearance.toSprite(this); } } public boolean isAllocated() { return allocated; } /** * @return Returns the flipped. */ public boolean isFlipped() { return flipped; } /** * @param flipped The flipped to set. */ public void setFlipped(boolean flipped) { this.flipped = flipped; } /** * @return Returns the mirrored. */ public boolean isMirrored() { return mirrored; } /** * @param mirrored The mirrored to set. */ public void setMirrored(boolean mirrored) { this.mirrored = mirrored; } /** * @return Returns the doChildOffset. */ public boolean isDoChildOffset() { return doChildOffset; } /** * @param doChildOffset The doChildOffset to set. */ public void setDoChildOffset(boolean doChildOffset) { this.doChildOffset = doChildOffset; } /** * Rotates a sprite towards a particular direction (specified in relative * coordinates) * @param dirX * @param dirY */ public void rotateToTarget(float dirX, float dirY) { setAngle(FPMath.fpYaklyDegrees( Util.angleFromDirection(dirX, dirY))); } /** * Serialization support. We completely replace the serialized sprite with an * instance of SerializedSprite instead. */ private Object writeReplace() throws ObjectStreamException { SerializedSprite ss = new SerializedSprite(); ss.fromSprite(this); return ss; } /** * @param missingImage The missingImage to set. */ public static void setMissingImage(SpriteImage missingImage) { Sprite.missingImage = missingImage; } /** * @return Returns the missingImage. */ public static SpriteImage getMissingImage() { return missingImage; } float getOffsetY() { return oy; } /** * @param sortOffset the ySortOffset to set */ public void setYSortOffset(float sortOffset) { ySortOffset = sortOffset; } /** * @return the ySortOffset */ public float getYSortOffset() { return ySortOffset; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Layered#getSubLayer() */ public int getSubLayer() { return subLayer; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Layered#setSubLayer(int) */ public void setSubLayer(int newSubLayer) { this.subLayer = newSubLayer; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#pushSequence() */ public void pushSequence() { if (stack == null) { stack = new Stack<StackEntry>(); } if (stack.size() > 10) { System.err.println("Stack overflow "+this+"/"+getAnimation()); return; } // System.out.println(this+" pushed "+getAnimation()+" / "+(getSequence()+1)); stack.push(new StackEntry(getAnimation(), getSequence() + 1)); } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#popSequence() */ public void popSequence() { if (stack == null) { return; } if (stack.size() == 0) { System.err.println("Stack underflow "+this+"/"+getAnimation()); return; } StackEntry se = stack.pop(); setAnimationNoRewind(se.animation); setSequence(se.sequence); } /** * Accessor for {@link SerializedSprite} * @return the stack */ Stack<StackEntry> getStack() { return stack; } /** * Accessor for {@link SerializedSprite} * @param stack */ void setStack(Stack<StackEntry> stack) { this.stack = stack; } public void reset() { animation = null; frameList = null; sequence = 0; frame = 0; tick = 0; event = 0; paused = false; childXOffset = 0; childYOffset = 0; } /** * Gets the animation. * @return Returns a Animation */ public Animation getAnimation() { return animation; } /** * Sets the animation. * @param animation The animation to set */ public void setAnimation(Animation animation) { if (animation != null) { assert animation.isCreated(); } this.animation = animation; rewind(); } /** * Sets the animation, without rewinding * @param animation */ void setAnimationNoRewind(Animation animation) { this.animation = animation; tick = 0; } /** * Rewind the animation, if any. The first sequence of the animation, if any, is executed immediately. */ public void rewind() { sequence = 0; tick = 0; tick(); } /** * @see com.shavenpuppy.jglib.sprites.Animation.Animated#getCurrentSequence() */ public int getSequence() { return sequence; } /** * @see com.shavenpuppy.jglib.sprites.Animation.Animated#getCurrentTick() */ public int getTick() { return tick; } /** * @see com.shavenpuppy.jglib.sprites.Animation.Animated#eventReceived(int) */ public void eventReceived(int event) { } /** * @see com.shavenpuppy.jglib.sprites.Animated#setCurrentSequence(int) */ public void setSequence(int newSeq) { sequence = newSeq; } /** * @see com.shavenpuppy.jglib.sprites.Animated#setCurrentTick(int) */ public void setTick(int newTick) { tick = newTick; } /** * @return int */ public int getEvent() { return event; } /** * Sets the event. * @param event The event to set */ public void setEvent(int event) { this.event = event; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#isPaused() */ public final boolean isPaused() { return paused; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#setPaused(boolean) */ public final void setPaused(boolean paused) { this.paused = paused; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#addLoop(int) */ public final void addLoop(int d) { loop += d; } /* (non-Javadoc) * @see com.shavenpuppy.jglib.sprites.Animated#getLoop() */ public final int getLoop() { return loop; } public final void setLoop(int i) { loop = i; } public float getChildXOffset(){ return childXOffset; } public void setChildXOffset(float childXOffset) { this.childXOffset = childXOffset; } public float getChildYOffset(){ return childYOffset; } public void setChildYOffset(float childYOffset) { this.childYOffset = childYOffset; } public void setFrameList(ResourceArray frameList) { this.frameList = frameList; if (frameList != null) { updateFrame(); } } public ResourceArray getFrameList() { return frameList; } /** * @return the frame */ public int getFrame() { return frame; } /** * @param frame the frame to set */ public boolean setFrame(int frame) { this.frame = frame; if (frameList != null) { return updateFrame(); } else { return false; } } private boolean updateFrame() { if (frame < 0 || frame >= frameList.getNumResources()) { return false; } Appearance newAppearance = (Appearance) frameList.getResource(frame); if ((animation != null && newAppearance != animation) || animation == null) { return newAppearance.toSprite(this); } else { return false; } } }