/* * 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.entities; import java.util.ArrayList; import net.puppygames.applet.Game; import net.puppygames.applet.Screen; import net.puppygames.applet.effects.Emitter; import net.puppygames.applet.effects.LabelEffect; import org.lwjgl.util.Point; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.Rectangle; import worm.Barracks; import worm.Entity; import worm.GameConfiguration; import worm.GameStateInterface; import worm.Layers; import worm.Res; import worm.Worm; import worm.effects.ElectronZapEffect; import worm.features.LayersFeature; import worm.features.ResearchFeature; import worm.features.UnitFeature; import worm.screens.GameScreen; import worm.weapons.WeaponFeature.WeaponInstance; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.resources.MappedColor; /** * $Id: Unit.java,v 1.18 2010/10/09 01:11:37 foo Exp $ * Player units * @author $Author: foo $ * @version $Revision: 1.18 $ */ public class Unit extends Entity implements PlayerWeaponInstallation { private static final long serialVersionUID = 1L; private static final ArrayList<Entity> COLLISIONS = new ArrayList<Entity>(); private static final int SPAWN_DURATION = 20; private static final int RETARGET_TIME = 300; private static final int SHOOT_INTERVAL = 180; private static final float MAX_WIDTH = 0.25f; private static final float MAX_WOBBLE = 8.0f; private static final float WOBBLE_FACTOR = 0.25f; private static final float WIDTH_FACTOR = 0.025f; private static final int BEAM_X_OFFSET = 0; private static final int BEAM_Y_OFFSET = 4; private static final Rectangle TEMP_BOUNDS = new Rectangle(); /** The gidrah feature */ private final UnitFeature feature; /** The owner barracks */ private final Barracks barracks; /** Hitpoints */ private int hitPoints; /** Gidrah wounds (0=healthy, increases as damage is taken) */ private int wounds; /** Flash tick */ private int flashTick; /** Hit by an exploding bullet? */ private ArrayList<Bullet> explodingBullets; /** Current appearance */ private LayersFeature appearance; /** Emitters */ private transient Emitter[] emitter; /** Weapon instance */ private WeaponInstance weaponInstance; /** Movement handler */ private final Movement movement; /** Target gidrah */ private Entity target; /** Retargeting */ private int targetTick; /** Shooting */ private int shootTick; /** Repair interval */ private final int repairInterval; /** Ignore list */ private final ArrayList<Entity> ignore = new ArrayList<Entity>(); /** Phase */ private int phase; private int tick; private static final int PHASE_SPAWN = 0; private static final int PHASE_ALIVE = 1; private static final int PHASE_DYING = 2; /** Zap effect for repair drones */ private transient ElectronZapEffect zapEffect; /** * C'tor */ public Unit(Barracks barracks, UnitFeature feature, float mapX, float mapY) { this.barracks = barracks; this.feature = feature; setLocation(mapX, mapY); movement = new UnitMovement(this); hitPoints = feature.getHitPoints(); repairInterval = Worm.getGameState().isResearched(ResearchFeature.DROIDBUFF) ? feature.getBuffedRepairInterval() : feature.getRepairInterval(); } public int getWidth() { return feature.getBounds().getWidth(); } public int getHeight() { return feature.getBounds().getHeight(); } @Override public boolean isShootable() { return false; } @Override public boolean isSolid() { return canCollide(); } @Override public boolean canCollide() { return isActive() && phase == PHASE_SPAWN || phase == PHASE_ALIVE; } @Override public final Rectangle getBounds(Rectangle bounds) { if (bounds == null) { bounds = new Rectangle(); } Rectangle featureBounds = feature.getBounds(); bounds.setBounds((int) getMapX() + featureBounds.getX(), (int) getMapY() + featureBounds.getY(), featureBounds.getWidth(), featureBounds.getHeight()); return bounds; } @Override public float getOffsetX() { return feature.getBounds().getX() + feature.getBounds().getWidth() / 2; } @Override public float getOffsetY() { return feature.getBounds().getY() + feature.getBounds().getHeight() / 2; } @Override public boolean isRound() { return false; } @Override public float getRadius() { return 0.0f; } @Override protected void createSprites(Screen screen) { appearance = feature.getAppearance(); // A bit hacky, this - would be better to call normalAppearance()... feature.getAppearance().createSprites(screen, this); } @Override public void onCollision(Entity entity) { entity.onCollisionWithUnit(this); } @Override public void onCollisionWithBullet(Bullet bullet) { if (bullet.isExploding() || bullet.isPassThrough()) { if (explodingBullets != null && explodingBullets.contains(bullet)) { return; } if (explodingBullets == null) { explodingBullets = new ArrayList<Bullet>(8); } explodingBullets.add(bullet); } if (bullet.getSource() == this) { return; } if (!bullet.isDangerousToBuildings()) { return; } int damage = bullet.getDamage(); damage(damage); bullet.onHit(true, this); if (isActive()) { Emitter e = Res.getGidrahPainEmitter().spawn(GameScreen.getInstance()); // TODO: unit pain emitter e.setLocation(bullet.getMapX(), bullet.getMapY()); e.setOffset(GameScreen.getSpriteOffset()); } } @Override public void onCollisionWithGidrah(Gidrah gidrah) { kill(); } @Override public void explosionDamage(int damageAmount, boolean friendly) { damage(damageAmount); } @Override public int crush() { kill(); return 0; } /** * Damage and maybe kill the unit * @param amount The amount of damage to inflict */ protected void damage(int amount) { wounds += amount; flashTick = 3; setFlash(true); if (wounds >= getHitPoints()) { kill(); } else { Emitter e = Res.getGidrahPainEmitter().spawn(GameScreen.getInstance()); e.setOffset(GameScreen.getSpriteOffset()); e.setLocation(getMapX(), getMapY()); } } private int getHitPoints() { return hitPoints; } /** * Kill the unit */ private void kill() { if (!isActive()) { return; } if (feature.getDeathAppearance() != null) { setAppearance(feature.getDeathAppearance()); phase = PHASE_DYING; } else { // Just remove us for now remove(); } } @Override public final void addToGameState(GameStateInterface gsi) { gsi.addToUnits(this); } @Override public final void removeFromGameState(GameStateInterface gsi) { gsi.removeFromUnits(this); } @Override protected final void doSpawn() { emitter = feature.getAppearance().createEmitters(GameScreen.getInstance(), getMapX(), getMapY()); if (feature.getWeapon() != null) { weaponInstance = feature.getWeapon().spawn(this); } tick(); update(); } @Override protected final void doRespawn() { emitter = feature.getAppearance().createEmitters(GameScreen.getInstance(), getMapX(), getMapY()); } @Override protected final void doRemove() { setEmitters(null); // Clean up brain and remove our occupation status movement.remove(); // Inform barracks barracks.onUnitRemoved(this); if (zapEffect != null) { zapEffect.finish(); zapEffect = null; } } private void setEmitters(Emitter[] newEmitter) { if (emitter != null) { for (Emitter element : emitter) { if (element != null) { element.remove(); } } } emitter = newEmitter; } public void setAppearance(LayersFeature newAppearance) { this.appearance = newAppearance; boolean mirrored = isMirrored(); newAppearance.createSprites(GameScreen.getInstance(), this); setEmitters(newAppearance.createEmitters(GameScreen.getInstance(), getMapX(), getMapY())); setMirrored(mirrored); } @Override protected LayersFeature getCurrentAppearance() { return appearance; } @Override public int getExtraDamage() { return Worm.getGameState().isResearched(ResearchFeature.BIOLOGY) ? 1 : 0; } @Override protected void calculateScreenPosition() { super.calculateScreenPosition(); // Add feature offsets too (so angry gidrahs are better lookin') Point offset = feature.getOffset(); if (offset != null) { setScreenX(getScreenX() + offset.getX()); setScreenY(getScreenY() + offset.getY()); } } @Override protected final void doUpdate() { if (emitter != null && appearance != null) { appearance.updateEmitters(emitter, getMapX(), getMapY()); } } @Override protected final void doTick() { switch (phase) { case PHASE_SPAWN: doSpawnTick(); break; case PHASE_ALIVE: doAliveTick(); break; case PHASE_DYING: doDyingTick(); break; default: assert false; break; } } private void doDyingTick() { // Wait for sprite 0 to flag an event 1 if (getEvent() == 1) { remove(); } } private void doSpawnTick() { tick ++; if (tick > SPAWN_DURATION) { phase = PHASE_ALIVE; tick = 0; setAlpha(255); } else { setAlpha((int) LinearInterpolator.instance.interpolate(0.0f, 255.0f, (float) tick / SPAWN_DURATION)); } } @Override public void onBulletDeflected(Entity target) { ignore.remove(target); ignore.add(target); } private void doAliveTick() { if (flashTick > 0) { flashTick --; if (flashTick == 0) { setFlash(false); } } // Retarget every couple of seconds Entity oldTarget = target; if (targetTick > 0) { targetTick --; if (targetTick == 0) { target = null; } } // If we've got no target, find one: if (target == null || !target.isActive()) { findTarget(); } else if (feature.isRepairDrone() && !((Building) target).isDamaged()) { findTarget(); } else if (!feature.isRepairDrone() && !target.isAttackableByUnits()) { findTarget(); } if (oldTarget != target && zapEffect != null) { if (zapEffect != null) { zapEffect.finish(); zapEffect = null; } } if (target == null) { // Don't move or shoot return; } // Shoot weapon if it's in range of the target AND we're allowed to if (weaponInstance != null) { weaponInstance.tick(); if (shootTick > 0) { shootTick --; } } float range = getDistanceTo(target); if (weaponInstance != null && weaponInstance.isReady() && range < feature.getRange()) { weaponInstance.fire((int) (target.getX()), (int) (target.getY())); } else if (range >= feature.getRange()) { // Move. Maybe find a new target. movement.tick(); if (shootTick == 0 && weaponInstance != null && weaponInstance.isReady()) { // Take a potshot at anything in range. Only check every few frames TEMP_BOUNDS.setBounds((int) (getX() - feature.getRange()), (int) (getY() - feature.getRange()), (int) (feature.getRange() * 2.0f), (int) (feature.getRange() * 2.0f)); Entity.getCollisions(TEMP_BOUNDS, COLLISIONS); int n = COLLISIONS.size(); float distance = Float.MAX_VALUE; Entity closest = null; for (int i = n; --i >= 0; ) { Entity e = COLLISIONS.get(i); if (e.isActive() && e.isAttackableByUnits()) { float dist = getDistanceTo(e); if (dist < distance && dist <= feature.getRange()) { if (!ignore.contains(e)) { distance = dist; closest = e; } } } } if (closest != null) { weaponInstance.fire((int) (closest.getX()), (int) (closest.getY())); } else { shootTick = SHOOT_INTERVAL; } } } else if (range < feature.getRange() && feature.isRepairDrone()) { // In range of the building we are supposed to be repairing Building building = (Building) target; int repairCost = (int) (GameConfiguration.getInstance().getRepairCost() * building.getFeature().getInitialValue()); if (building.isAlive() && building.isDamaged() && Worm.getGameState().getMoney() >= repairCost) { if (zapEffect == null) { zapEffect = new ElectronZapEffect ( false, Res.getRepairZapSound(), new MappedColor("repairzap.background"), new MappedColor("repairzap.foreground"), 128, feature.getBeamStartEmitter(), feature.getBeamEndEmitter(), getX() + BEAM_X_OFFSET, getY() + BEAM_Y_OFFSET, MAX_WIDTH, MAX_WOBBLE, WOBBLE_FACTOR, WIDTH_FACTOR ); zapEffect.setTarget(building.getX(), building.getY()); zapEffect.spawn(GameScreen.getInstance()); Game.allocateSound(Res.getCapacitorStartBuffer(), Worm.calcGain(getX(), getY()) * 0.25f, 1.0f); } shootTick ++; if (shootTick > getRepairInterval()) { building.repair(); shootTick = 0; LabelEffect effect = new LabelEffect ( net.puppygames.applet.Res.getTinyFont(), "-$"+String.valueOf(repairCost), ReadableColor.WHITE, new MappedColor("repairzap.background"), 50, 20 ); effect.spawn(GameScreen.getInstance()); effect.setLayer(Layers.HUD); effect.setLocation(building.getX(), building.getY()); effect.setVelocity(0.0f, 0.5f); effect.setAcceleration(0.0f, -0.01f); effect.setDelay(0); Worm.getGameState().addMoney(-repairCost); } } else { if (zapEffect != null) { zapEffect.finish(); zapEffect = null; } } } else { if (zapEffect != null) { zapEffect.finish(); zapEffect = null; } } } private int getRepairInterval() { return repairInterval; } /** * Determines if our move takes us into another gidrah or solid building or map tile */ private boolean isValidMove() { if (!Worm.getGameState().getMap().isClearPX(getBounds(TEMP_BOUNDS))) { return false; } ArrayList<Entity> entities = Worm.getGameState().getEntities(); int n = entities.size(); for (int i = 0; i < n; i ++) { Entity test = entities.get(i); if (test == this) { continue; } if (test.isSolid() && test.isTouching(this)) { return false; } } return true; } /** * @return the target */ public Entity getTarget() { return target; } /** * Find a new target. */ private void findTarget() { if (targetTick > 0) { return; } targetTick = RETARGET_TIME; Entity newTarget = feature.getBrain().findTarget(this); if (newTarget != target) { movement.reset(); target = newTarget; } } @Override public ArrayList<Entity> getIgnore() { return ignore; } /** * @return the feature */ public UnitFeature getFeature() { return feature; } @Override public boolean isFiringAtAerialTargets() { return feature.isAerialTargets() && target.isFlying(); } @Override public String toString() { return "Unit["+System.identityHashCode(this)+","+feature+","+getTileX()+","+getTileY()+"]"; } public static void resetTotalThinkTime() { UnitMovement.resetTotalThinkTime(); } }