/* * 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.weapons; import java.io.Serializable; import java.text.DecimalFormat; import net.puppygames.applet.Game; import org.lwjgl.util.ReadableColor; import worm.Entity; import worm.Res; import worm.Statistics; import worm.Worm; import worm.entities.Building; import worm.features.BulletFeature; import worm.features.ResearchFeature; import worm.screens.GameScreen; import com.shavenpuppy.jglib.openal.ALBuffer; import com.shavenpuppy.jglib.resources.Feature; import com.shavenpuppy.jglib.resources.Range; import com.shavenpuppy.jglib.resources.ResourceArray; import com.shavenpuppy.jglib.sound.SoundEffect; import com.shavenpuppy.jglib.sprites.Sprite; import com.shavenpuppy.jglib.util.Util; /** * Weaponry * @author Cas */ public class WeaponFeature extends Feature implements Statistics { private static DecimalFormat RATE_FORMAT = new DecimalFormat("##.#"); /** Minimum fire rate */ private int minFireRate; /** Minimum reload speed */ private int minReloadSpeed; protected int maxCoolingTowers; protected int maxBatteries; protected int maxReactors; protected int maxAutoloaders; /** Firing rate (either in ticks, when expressed as a range for units/gidrahs, or in rounds / minute (3600 ticks)) */ private Range fireRate; /** Sound when fired */ private String sound; /** Magazine */ private int magazine; /** Ammo per battery */ private int ammoPerBattery; /** Fire rate adjustment per cooling tower, rounds / minute */ private int fireRatePerCoolingTower; /** Reload per reactor */ private int reloadPerReloader; /** Reload speed */ private int reloadSpeed; /** Heavy weapon ? */ private boolean heavyWeapon; /** Bullets */ protected BulletFeature bullet; /** Constant reload */ private boolean constantReload; /** Danger */ private int danger; /** Multiple sounds */ private ResourceArray sounds; private transient ALBuffer soundResource; public class WeaponInstance implements Serializable { private static final long serialVersionUID = 1L; protected final Entity entity; /** Ammo remaining */ private int ammo; /** Extra ammo */ private int extraAmmo; /** Extra cooling */ private boolean extraCooling; /** Faster reloading */ private int faster; /** Reloading? */ private boolean reloading; /** Fire down? */ private boolean latched; private int tick; private int targetX, targetY; private Sprite reloadSprite; private transient SoundEffect soundInstance; public WeaponInstance(Entity entity) { this.entity = entity; tick = Util.random(0, (int) fireRate.getValue()); extraAmmo = Worm.getGameState().isResearched(ResearchFeature.LITHIUM) ? ammoPerBattery / 2 : 0; extraCooling = Worm.getGameState().isResearched(ResearchFeature.SODIUM); faster = Worm.getGameState().isResearched(ResearchFeature.PRECISION) ? reloadPerReloader / 2 : 0; ammo = getMaxAmmo(); } public void addBatteries(int n) { if (n > 0 && ((Building) entity).getBatteries() < maxBatteries && ammo != 0 && !reloading) { ammo += ammoPerBattery * n; } } public int getMaxAmmo() { if (entity.usesAmmo()) { return magazine + ammoPerBattery * Math.min(maxBatteries, ((Building) entity).getBatteries()) + extraAmmo; } else { return magazine; } } public WeaponFeature getFeature() { return WeaponFeature.this; } public void reload(Sprite reloadSprite) { if (ammo == getMaxAmmo() || reloading) { return; } ammo = 0; reloading = true; tick = getReloadTime(); this.reloadSprite = reloadSprite; reloadSprite.setAnimation(null); reloadSprite.setColors(ReadableColor.WHITE); if (heavyWeapon) { reloadSprite.setImage(Res.getReloadLarge(1.0f)); } else { reloadSprite.setImage(Res.getReload(1.0f)); } } /** * @return true if the weapon is ready to fire */ public boolean isReady() { return tick == 0 && ( !reloading && ammo > 0 || magazine == 0 || !entity.usesAmmo() ); } /** * Gidrah weapons: is this weapon automatically fired at a nearby target when ready? * @return true by default */ public boolean isAutomatic() { return true; } public boolean isReloading() { return reloading; } public int getAmmo() { return ammo; } public boolean isLatched() { return latched; } /** * Fire the weapon! The weapon may first delay and will then start shooting. * @param newTargetX * @param newTargetY * @return true if a round was expended */ public boolean fire(int newTargetX, int newTargetY) { this.targetX = newTargetX; this.targetY = newTargetY; latched = true; // Reloading etc? if (!isReady()) { return false; } // Allowed to fire? if (entity.usesAmmo()) { boolean bezerk = Worm.getGameState().isBezerk(); if (constantReload) { tick = 1; } else { int towers = bezerk ? maxCoolingTowers : Math.min(maxCoolingTowers, ((Building) entity).getCoolingTowers()); int extraRounds = fireRatePerCoolingTower * towers + (extraCooling ? Math.max(1, fireRatePerCoolingTower / 2) : 0); tick = (int) Math.max(minFireRate, (int) 3600.0f / (fireRate.getMin() + extraRounds)); } if (!bezerk && magazine > 0) { ammo --; } } else { tick = (int) fireRate.getValue(); } doFire(newTargetX, newTargetY); // Shooty noise if (soundResource != null) { soundInstance = Game.allocateSound(soundResource, Worm.calcGain(entity.getMapX() + entity.getOffsetX(), entity.getMapY() + entity.getOffsetY()), 1.0f, this); } else if (sounds != null) { ALBuffer s = (ALBuffer) sounds.getResource(Util.random(0, sounds.getNumResources() - 1)); soundInstance = Game.allocateSound(s, Worm.calcGain(entity.getMapX() + entity.getOffsetX(), entity.getMapY() + entity.getOffsetY()), 1.0f, this); } return true; } /** * @return Returns the targetX. */ protected int getTargetX() { return targetX; } /** * @return Returns the targetY. */ protected int getTargetY() { return targetY; } /** * @return Returns the entity. */ protected Entity getEntity() { return entity; } protected void doFire(float newTargetX, float newTargetY) { bullet.spawn(GameScreen.getInstance(), entity, (int) (entity.getMapX() + entity.getOffsetX()), (int) (entity.getMapY() + entity.getOffsetY()), (int) newTargetX, (int) newTargetY, entity.getExtraDamage()); } public void remove() { } public float getBlastRange() { return 0.0f; } public void instantReload() { ammo = getMaxAmmo(); tick = 0; reloading = false; if (reloadSprite != null) { reloadSprite.setAnimation(null); if (heavyWeapon) { reloadSprite.setAppearance(Res.getAmmoLarge(ammo, ammo)); } else { reloadSprite.setAppearance(Res.getAmmo(ammo, ammo)); } } if (entity.usesAmmo()) { entity.onReloaded(); } } public void tick() { if (constantReload) { if (!latched) { if (getAmmo() < getMaxAmmo()) { if (tick == 0) { tick = getReloadTime(); } else { tick --; if (tick == 0) { ammo ++; } } } } else { latched = false; if (tick > 0) { tick --; } } } else if (tick > 0) { tick --; if (reloading) { if (tick == 0) { instantReload(); } else { reloadSprite.setAnimation(null); if (heavyWeapon) { reloadSprite.setImage(Res.getReloadLarge((float) tick / getReloadTime())); } else { reloadSprite.setImage(Res.getReload((float) tick / getReloadTime())); } } } } } private int getReloadTime() { if (reloadSpeed < minReloadSpeed) { return Math.max(1, reloadSpeed - Math.min(maxAutoloaders, ((Building) entity).getAutoLoaders()) * reloadPerReloader - faster); } else { return Math.max(minReloadSpeed, reloadSpeed - Math.min(maxAutoloaders, ((Building) entity).getAutoLoaders()) * reloadPerReloader - faster); } } public boolean canReload() { // Don't allow reloading if only one shot's been fired return ammo < getMaxAmmo() - 1 && !reloading && magazine > 0 && !constantReload; } } /** * C'tor * @param name */ public WeaponFeature(String name) { super(name); setAutoCreated(); } /** * @return Returns the soundResource. */ public final ALBuffer getSound() { return soundResource; } /** * @return Returns the bullet feature to use. */ public BulletFeature getBulletFeature() { return bullet; } /** * Spawn a weapon to be used by the entity * @param entity * @return the weapon instance */ public WeaponInstance spawn(Entity entity) { return new WeaponInstance(entity); } public boolean isLaser() { return false; } public boolean isHeavyWeapon() { return heavyWeapon; } public boolean isDisruptor() { return false; } public int getMagazine() { return magazine; } protected final int getAmmoPerBattery() { return ammoPerBattery; } public int getDanger() { return danger; } protected final String getFireRateDescription() { StringBuilder sb = new StringBuilder(10); float rate = fireRate.getMin(); sb.append(RATE_FORMAT.format(rate)); if (Worm.getGameState().isResearched(ResearchFeature.COOLINGTOWER)) { float rate2 = fireRate.getMin() + fireRatePerCoolingTower * maxCoolingTowers; sb.append("-"); sb.append(RATE_FORMAT.format(rate2)); } sb.append("/min"); return sb.toString(); } /** * @return the reloadPerReactor */ protected final int getReloadPerReactor() { return reloadPerReloader; } /** * @return the reloadSpeed */ protected final int getReloadSpeed() { return reloadSpeed; } public void getResearchStats(StringBuilder stats_1_text, StringBuilder stats_2_text) { stats_1_text.append("\n{font:tinyfont.glfont color:text}"+Game.getMessage("ultraworm.weaponstats.damage")+": {color:text-bold}"); stats_1_text.append(getDamageStats()); stats_2_text.append("\n{color:text}"+Game.getMessage("ultraworm.weaponstats.firerate")+": {color:text-bold}"); stats_2_text.append(getFireRateDescription()); stats_2_text.append("\n{color:text}"+Game.getMessage("ultraworm.weaponstats.ammo")+": {color:text-bold}"); stats_2_text.append(getMagazine()); if (Worm.getGameState().isResearched(ResearchFeature.BATTERY)) { stats_2_text.append("+"); stats_2_text.append(getAmmoPerBattery()); stats_2_text.append("/"); stats_2_text.append(Game.getMessage("ultraworm.weaponstats.battery")); } stats_2_text.append("\n{color:text}"+Game.getMessage("ultraworm.weaponstats.reload")+": {color:text-bold}"); stats_2_text.append(getReloadSpeed() / 60); if (Worm.getGameState().isResearched(ResearchFeature.AUTOLOADER)) { stats_2_text.append("-"); stats_2_text.append(getReloadPerReactor() / 60); stats_2_text.append("s/"+Game.getMessage("ultraworm.weaponstats.reloader")); } else { stats_2_text.append("s"); } } protected String getDamageStats() { StringBuilder sb = new StringBuilder(); sb.append(bullet.getDamage()); return sb.toString(); } protected String getArmourPiercingStats() { StringBuilder sb = new StringBuilder(); if (bullet != null && bullet.getArmourPiercing() > 0) { sb.append("\n "+Game.getMessage("ultraworm.weaponstats.armour_piercing")+": "); sb.append(bullet.getArmourPiercing()); } return sb.toString(); } protected String getStunStats() { StringBuilder sb = new StringBuilder(); if (bullet != null && bullet.getStun() > 0) { sb.append("\n "+Game.getMessage("ultraworm.weaponstats.stun")+": "); DecimalFormat df = new DecimalFormat("#.#"); sb.append(df.format(bullet.getStun() * .016666666667)); sb.append(" "+Game.getMessage("ultraworm.weaponstats.time_unit")); } return sb.toString(); } @Override public void appendFullStats(StringBuilder dest) { dest.append("\n "+Game.getMessage("ultraworm.weaponstats.firerate_lowercase")+": "); dest.append(getFireRateDescription().toUpperCase()); dest.append("\n "+Game.getMessage("ultraworm.weaponstats.damage_lowercase")+": "); dest.append(getDamageStats()); dest.append(getArmourPiercingStats()); dest.append(getStunStats()); dest.append("\n "+Game.getMessage("ultraworm.weaponstats.ammo_lowercase")+": "); dest.append(getMagazine()); dest.append("\n "+Game.getMessage("ultraworm.weaponstats.reload_time")+": "); dest.append(getReloadSpeed() / 60); dest.append(" "+Game.getMessage("ultraworm.weaponstats.time_units")); } @Override public void appendBasicStats(StringBuilder dest) { } @Override public void appendTitle(StringBuilder dest) { } /** * @return true if this weapon makes the wielder immune to disruptors */ public boolean confersImmunityFromDisruptors() { return false; } }