/* * 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 worm.features; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.puppygames.applet.Anchor; import net.puppygames.applet.Game; import net.puppygames.applet.Screen; import net.puppygames.applet.effects.Effect; import net.puppygames.applet.effects.FadeEffect; import org.lwjgl.util.Color; import org.lwjgl.util.Point; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableRectangle; import org.lwjgl.util.Rectangle; import org.w3c.dom.Element; import worm.animation.SimpleThingWithLayers; import com.shavenpuppy.jglib.Resources; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.openal.ALBuffer; import com.shavenpuppy.jglib.opengl.GLFont; import com.shavenpuppy.jglib.opengl.GLStyledText; import com.shavenpuppy.jglib.opengl.GLStyledText.DefaultStyledText; import com.shavenpuppy.jglib.opengl.GLStyledText.StyledText; import com.shavenpuppy.jglib.opengl.GLStyledText.StyledTextFactory; import com.shavenpuppy.jglib.resources.Background; import com.shavenpuppy.jglib.resources.Data; import com.shavenpuppy.jglib.resources.Feature; import com.shavenpuppy.jglib.resources.MappedColor; import com.shavenpuppy.jglib.sound.SoundEffect; import com.shavenpuppy.jglib.sprites.Sprite; import com.shavenpuppy.jglib.util.FPMath; import com.shavenpuppy.jglib.util.XMLUtil; /** * An Actor in a Setting. Uses a CharacterFeature and has coordinates for itself and its speechbubble. */ public class ActorFeature extends Feature { private static final long serialVersionUID = 1L; private static final Color TEMP = new Color(); private static final Color INVISIBLE = new Color(0,0,0,0); private static final int LEADING = -1; /** Where to place the character RELATIVE TO BOUNDS!! */ private Point position; /** The character */ private String character; /** Speech bubble bounds */ private Rectangle bounds; /** Whether to mirror the character */ private boolean mirrored; /** Whether to fade in the character */ private boolean fade; /** How to expand */ @Data private String expand = "up"; @Data private String leading; private int textLeading; /** Anchors */ private ArrayList<Anchor> anchors; /* * Transient */ private transient CharacterFeature characterFeature; /** * Instances */ private class ActorInstance extends Effect implements Actor { private static final int TEXT_DELAY = 30; private static final int FADE_IN_DURATION = 16; private final ArrayList<String> text = new ArrayList<String>(1); private int paragraph; private int tick, pos, nextCharDelay, voiceTick; private Background.Instance bg; private GLStyledText textArea; private boolean done; private SimpleThingWithLayers layersSprite; private Sprite foreground; private AnimatedTextFactory factory; private final Setting setting; private Rectangle instanceBounds; private int fadeAfter; private int delayTick; private int fadeInTick; private int nextParagraphDelayTick; private char last = 0; private SoundEffect currentSound; private class AnimatedTextFactory implements StyledTextFactory { int numVisibleGlyphs; private ReadableColor topColor, bottomColor; private GLFont font; private StringBuilder parsed; public AnimatedTextFactory() { topColor = characterFeature.getColor(); bottomColor = topColor; font = net.puppygames.applet.Res.getTinyFont(); } public void setNumVisibleGlyphs(int numVisibleGlyphs) { if (this.numVisibleGlyphs != numVisibleGlyphs) { this.numVisibleGlyphs = numVisibleGlyphs; textArea.setChanged(true); } } int length() { return parsed.length(); } char charAt(int n) { return parsed.charAt(n); } @Override public void parse(String text, List<StyledText> dest) { dest.clear(); // start parsing. Better get a { first! StringBuilder sb = new StringBuilder(text.length()); parsed = new StringBuilder(text.length()); int n = text.length(); int style = -1; for (int i = 0; i < n; ) { char c = text.charAt(i); if (c == '{') { if (sb.length() > 0) { DefaultStyledText dst = new DefaultStyledText(sb.toString(), font, topColor, bottomColor); dest.add(dst); sb = new StringBuilder(text.length() - i); } i += parse(text, i + 1); if (style == 2) { topColor = bottomColor = INVISIBLE; } } else { if (parsed.length() >= numVisibleGlyphs) { if (style != 2) { if (sb.length() > 0) { DefaultStyledText dst = new DefaultStyledText(sb.toString(), font, topColor, bottomColor); dest.add(dst); sb = new StringBuilder(text.length() - i); } style = 2; // Make these glyphs invisible topColor = bottomColor = INVISIBLE; } } else { if ((Character.isUpperCase(c) || Character.isDigit(c) || c == '+' || c == '$') && style != 0) { if (sb.length() > 0) { DefaultStyledText dst = new DefaultStyledText(sb.toString(), font, topColor, bottomColor); dest.add(dst); sb = new StringBuilder(text.length() - i); } style = 0; topColor = characterFeature.getBoldColor(); bottomColor = topColor; } else if (Character.isLowerCase(c) && style != 1) { if (sb.length() > 0) { DefaultStyledText dst = new DefaultStyledText(sb.toString(), font, topColor, bottomColor); dest.add(dst); sb = new StringBuilder(text.length() - i); } style = 1; topColor = characterFeature.getColor(); bottomColor = topColor; } } sb.append(Character.toUpperCase(c)); if (!Character.isWhitespace(c)) { parsed.append(c); } i ++; } } if (sb.length() > 0) { DefaultStyledText dst = new DefaultStyledText(sb.toString(), font, topColor, bottomColor); dest.add(dst); } } private int parse(String text, int pos) { // Find index of } int idx = text.indexOf('}', pos); // Get substring String format = text.substring(pos, idx); // Split into tokens separated by spaces StringTokenizer st = new StringTokenizer(format, " "); // Parse each token, a key:value pair while (st.hasMoreTokens()) { String token = st.nextToken(); int colon = token.indexOf(':'); if (colon == -1) { if (Game.DEBUG) { System.out.println("Bad token : "+token); } } else { String key = token.substring(0, colon).toLowerCase(); String value = token.substring(colon + 1); if (key.equals("top")) { topColor = new MappedColor(value); } else if (key.equals("bottom")) { bottomColor = new MappedColor(value); } else if (key.equals("color")) { topColor = new MappedColor(value); bottomColor = topColor; } else if (key.equals("font")) { font = (GLFont) Resources.get(value); } else { if (Game.DEBUG) { System.out.println("Bad key : "+key); } } } } return idx - pos + 2; } } /** * C'tor * @param text The text that this actor will read out */ public ActorInstance(Setting setting) { this.setting = setting; instanceBounds = new Rectangle(bounds); if (leading == null) { textLeading = LEADING; } else { textLeading = Integer.parseInt(leading); } } @Override public void setFadeAfter(int time) { this.fadeAfter = time; } @Override public void addText(String text) { // chaz hack! just to make things a bit more readable in levels-.xml... '\n' already works btw this.text.add(replace(text, "\n ", "\n")); if (this.text.size() == 1) { initTextAndBubble(); } } private String replace(String src, String a, String b) { return Pattern.compile(a, Pattern.LITERAL).matcher(src).replaceAll(Matcher.quoteReplacement(b)); } @Override public CharacterFeature getCharacter() { return characterFeature; } @Override public void onResized() { // Apply anchors unless screen is "centred" if (anchors != null) { for (Anchor anchor : anchors) { anchor.apply(this); } } else if (position != null) { boolean centreX = setting.isCentred() || setting.isCentredX(); boolean centreY = setting.isCentred() || setting.isCentredY(); if (centreX || centreY) { int newX = centreX ? (Game.getWidth() - Game.getScale()) / 2 + bounds.getX() : bounds.getX(); int newY = centreY ? (Game.getHeight() - Game.getScale()) / 2 + bounds.getY() : bounds.getY(); setBounds(newX, newY, bounds.getWidth(), bounds.getHeight()); } } if (layersSprite != null) { for (int i = 0; i < layersSprite.getSprites().length; i ++) { layersSprite.getSprite(i).setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); } } if (foreground != null) { foreground.setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); } int textHeight = textArea.getTextHeight(); if ("down".equals(expand)) { textArea.setVerticalAlignment(GLStyledText.TOP); textArea.setLocation(instanceBounds.getX(), (instanceBounds.getY() + instanceBounds.getHeight() - Math.max(instanceBounds.getHeight(), textHeight))); textArea.setHeight(Math.max(instanceBounds.getHeight(), textHeight)); if (bg!=null) { bg.setBounds(new Rectangle(instanceBounds.getX(), instanceBounds.getY() + instanceBounds.getHeight() - textArea.getHeight(), instanceBounds.getWidth(), textArea.getHeight())); } } else { textArea.setLocation(instanceBounds.getX(),instanceBounds.getY()); textArea.setVerticalAlignment(GLStyledText.BOTTOM); textArea.setHeight(Math.max(instanceBounds.getHeight(), textHeight)); if (bg!=null) { bg.setBounds(new Rectangle(instanceBounds.getX(), instanceBounds.getY(), instanceBounds.getWidth(), textArea.getHeight())); } } } @Override public ReadableRectangle getBounds() { return instanceBounds; } @Override public void setBounds(int x, int y, int w, int h) { instanceBounds.setBounds(x, y, w, h); } @Override protected void render() { if (delayTick > 0 || isPaused()) { return; } int alpha = (int) LinearInterpolator.instance.interpolate(0.0f, 255.0f, (float) fadeInTick / FADE_IN_DURATION); if (bg != null) { bg.setAlpha(alpha); bg.render(this); } textArea.render(this); } @Override public int getDefaultLayer() { return characterFeature.getBubbleLayer(); } @Override protected void doSpawnEffect() { last = 0; if (!fade) { initCharacter(); } } private void initTextAndBubble() { if (characterFeature.getBubble() != null) { bg = characterFeature.getBubble().spawn(); } textArea = new GLStyledText(); factory = new AnimatedTextFactory(); textArea.setFactory(factory); textArea.setLeading(textLeading); textArea.setText(text.get(paragraph)); textArea.setWidth(instanceBounds.getWidth()); onResized(); factory.setNumVisibleGlyphs(0); delayTick = TEXT_DELAY; } private void setAppearance(LayersFeature layers) { // Remove existing sprites if (layersSprite != null) { layersSprite.remove(); layersSprite = null; } layersSprite = new SimpleThingWithLayers(getScreen()); layers.createSprites(getScreen(), layersSprite); if (layersSprite.getSprites() != null && mirrored) { for (int i = 0; i < layersSprite.getSprites().length; i ++) { layersSprite.getSprite(i).setMirrored(true); } } if (foreground != null && mirrored) { foreground.setMirrored(true); } } @Override public void begin() { if (isPaused()) { setPaused(false); //start(); init(); if (characterFeature.getTalkLayers() != null) { setAppearance(characterFeature.getTalkLayers()); } } else { // Next paragraph paragraph ++; pos = 0; tick = 0; voiceTick = 0; // Delay nextParagraphDelayTick = 180; } } @Override public void end() { if (isPaused()) { begin(); } if (paragraph == text.size() - 1) { finish(); } } @Override public boolean advance() { // Bollocks to fixing this :) return true; } @Override public void finish() { if (!done) { tick = 0; delayTick = 0; fadeInTick = FADE_IN_DURATION; factory.setNumVisibleGlyphs(pos = factory.length()); if (foreground != null && characterFeature.isAnimated()) { foreground.setAppearance(characterFeature.getDefaultAppearance()); } if (characterFeature.getIdleLayers() != null) { setAppearance(characterFeature.getIdleLayers()); } if (fadeAfter != 0) { new FadeEffect(fadeAfter, FADE_IN_DURATION) { @Override protected void onTicked() { textArea.setAlpha(getAlpha()); } }.spawn(getScreen()); } } } private void initCharacter() { if (characterFeature.getDefaultAppearance() != null) { foreground = getScreen().allocateSprite(ActorFeature.this); foreground.setLayer(characterFeature.getMouthLayer()); foreground.setAppearance(characterFeature.getDefaultAppearance()); } if (characterFeature.getIdleLayers() != null) { setAppearance(characterFeature.getIdleLayers()); } if (layersSprite != null) { for (int i = 0; i < layersSprite.getSprites().length; i ++) { layersSprite.getSprite(i).setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); if (fade) { layersSprite.getSprite(i).setAlpha(0); } } } if (foreground != null) { foreground.setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); if (fade) { foreground.setAlpha(0); } } } @Override protected void doTick() { if (nextCharDelay == 0) { nextCharDelay = characterFeature.getTextSpeed(); } if (nextParagraphDelayTick > 0) { nextParagraphDelayTick --; if (nextParagraphDelayTick == 0) { initTextAndBubble(); if (characterFeature.getTalkLayers() != null) { setAppearance(characterFeature.getTalkLayers()); } } return; } if (delayTick > 0) { delayTick --; if (delayTick == 0 && fade) { initCharacter(); } return; } if (fadeInTick < FADE_IN_DURATION) { fadeInTick ++; } if (pos < factory.length()) { tick ++; if (tick > nextCharDelay) { tick = 0; char c = factory.charAt(pos ++); if (pos == factory.length()) { finish(); } factory.setNumVisibleGlyphs(pos); c = Character.toLowerCase(c); if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y') { if (foreground != null && characterFeature.isAnimated()) { foreground.setAppearance(characterFeature.getVowelAppearance()); } nextCharDelay = characterFeature.getTextSpeed(); } else if (c == '.' || c == ',' || c == '!' || c == '?' || c == '&') { if (foreground != null && characterFeature.isAnimated()) { foreground.setAppearance(characterFeature.getDefaultAppearance()); } nextCharDelay = characterFeature.getTextSpeed(); if (characterFeature.isSpeech()) { nextCharDelay = characterFeature.getTextSpeed() * 3; } } else { if (foreground != null && characterFeature.isAnimated()) { foreground.setAppearance(characterFeature.getConsonantAppearance()); } nextCharDelay = characterFeature.getTextSpeed(); } } if (pos < factory.length()) { if (currentSound == null || !currentSound.isOwnedBy(this) || !currentSound.isActive() && currentSound.isOwnedBy(this)) { char c = factory.charAt(pos); voiceTick = characterFeature.getSpeechSpeed(); ALBuffer buf = characterFeature.getSound(c); if (buf != null) { currentSound = Game.allocateSound(buf, 1.0f, 1.0f - (characterFeature.isSpeech() ? 0.05f * (float) Math.random() : 0.0f), this); } } } } } @Override public boolean isFinished() { return pos == factory.length(); } @Override public boolean isEffectActive() { return !done; } @Override protected void doRemove() { done = true; if (layersSprite != null) { layersSprite.remove(); layersSprite = null; } if (foreground != null) { foreground.deallocate(); foreground = null; } } @Override protected void doUpdate() { int alpha = (int) LinearInterpolator.instance.interpolate(0.0f, 255.0f, (float) fadeInTick / FADE_IN_DURATION); if (layersSprite != null) { for (int i = 0; i < layersSprite.getSprites().length; i ++) { layersSprite.getSprite(i).setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); if (fade) { layersSprite.getSprite(i).setAlpha(alpha); } } } if (foreground != null) { foreground.setLocation(instanceBounds.getX() - bounds.getX() + position.getX(), instanceBounds.getY() - bounds.getY() + position.getY()); if (fade) { foreground.setAlpha(alpha); } } // chaz hack! - child offsets... if (layersSprite != null) { boolean searchForChildOffsets = false; for (int i = 0; i < layersSprite.getSprites().length; i ++) { // check anims for childOffset if (layersSprite.getSprite(i).isDoChildOffset()) { searchForChildOffsets=true; } } if (searchForChildOffsets) { float xOffset = 0; float yOffset = 0; float yOffsetTotal = 0; float xOffsetTotal = 0; for (int i = 0; i < layersSprite.getSprites().length; i ++) { boolean doOffset = false; // check for offset if (layersSprite.getSprite(i).getChildXOffset() != 0) { xOffset = layersSprite.getSprite(i).getChildXOffset(); // offsets after first sprite in array arent scaled? if (i==0) { xOffset *= FPMath.floatValue(layersSprite.getSprite(0).getXScale()); } xOffsetTotal += xOffset; doOffset = true; } if (layersSprite.getSprite(i).getChildYOffset() != 0) { yOffset = layersSprite.getSprite(i).getChildYOffset(); // offsets after first sprite in array arent scaled? if (i==0) { yOffset *= FPMath.floatValue(layersSprite.getSprite(0).getYScale()); } yOffsetTotal += yOffset; doOffset = true; } // if we've found an offset apply this to any sprites after where we found the offset if (doOffset) { if (mirrored) { xOffsetTotal = -xOffsetTotal; } for (int j = i+1; j < layersSprite.getSprites().length; j ++) { if (layersSprite.getSprite(j).isDoChildOffset()) { layersSprite.getSprite(j).setLocation(instanceBounds.getX() - bounds.getX() + position.getX() + xOffsetTotal, instanceBounds.getY() - bounds.getY() + position.getY() + yOffsetTotal); } } } } } } // chaz hack! - attach mouth to head - use first sprite in sprite if (foreground != null && layersSprite != null && !characterFeature.getSuppressChildOffsetMouth()) { boolean doOffset = false; float xOffset = 0, yOffset = 0; // check for offset if (layersSprite.getSprite(0).getChildXOffset() != 0) { xOffset = layersSprite.getSprite(0).getChildXOffset() * FPMath.floatValue(layersSprite.getSprite(0).getXScale()); doOffset = true; } if (layersSprite.getSprite(0).getChildYOffset() != 0) { yOffset = layersSprite.getSprite(0).getChildYOffset() * FPMath.floatValue(layersSprite.getSprite(0).getYScale()); doOffset = true; } if (doOffset) { if (mirrored) { xOffset = -xOffset; } foreground.setLocation(instanceBounds.getX() - bounds.getX() + position.getX() + xOffset, instanceBounds.getY() - bounds.getY() + position.getY() + yOffset); } } } } /** * C'tor */ public ActorFeature() { setAutoCreated(); } /** * Spawn an instance of this actor, reading out the specified text. * @param screen * @param text * @return the Actor */ public Actor spawn(Screen screen, Setting setting) { ActorInstance actor = new ActorInstance(setting); actor.spawn(screen); actor.setPaused(true); return actor; } /** * @return the character name */ public String getCharacter() { return character; } @Override public void load(Element element, Loader loader) throws Exception { super.load(element, loader); // Anchors List<Element> anchorElements = XMLUtil.getChildren(element, "anchor"); if (anchorElements.size() > 0) { anchors = new ArrayList<Anchor>(anchorElements.size()); for (Element anchorChild : anchorElements) { Anchor anchor = (Anchor) loader.load(anchorChild); anchors.add(anchor); } } } }