/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.shared.physics.entities; import aphelion.shared.gameconfig.GCBoolean; import aphelion.shared.gameconfig.GCBooleanList; import aphelion.shared.gameconfig.GCColour; import aphelion.shared.gameconfig.GCImage; import aphelion.shared.gameconfig.GCInteger; import aphelion.shared.gameconfig.GCIntegerFixed; import aphelion.shared.gameconfig.GCIntegerList; import aphelion.shared.gameconfig.GCString; import aphelion.shared.gameconfig.GCStringList; import aphelion.shared.gameconfig.enums.GCFunction2D; import aphelion.shared.net.protobuf.GameOperation; import aphelion.shared.physics.*; import aphelion.shared.physics.config.WeaponConfig; import aphelion.shared.physics.events.ProjectileExplosion; import aphelion.shared.physics.events.pub.ProjectileExplosionPublic; import aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON; import aphelion.shared.physics.valueobjects.PhysicsPoint; import aphelion.shared.physics.valueobjects.PhysicsPositionVector; import aphelion.shared.physics.valueobjects.PhysicsShipPosition; import aphelion.shared.resource.ResourceDB; import aphelion.shared.swissarmyknife.AttachmentData; import aphelion.shared.swissarmyknife.AttachmentManager; import aphelion.shared.swissarmyknife.LinkedListEntry; import aphelion.shared.swissarmyknife.LoopFilter; import aphelion.shared.swissarmyknife.SwissArmyKnife; import static aphelion.shared.swissarmyknife.SwissArmyKnife.max; import static aphelion.shared.swissarmyknife.SwissArmyKnife.min; import java.util.Collection; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; /** * * @author Joris */ public final class Projectile extends MapEntity implements ProjectilePublic { public static final AttachmentManager attachmentManager = new AttachmentManager(); private final AttachmentData attachments = attachmentManager.getNewDataContainer(); public final ProjectileKey key; public final LinkedListEntry<Projectile> projectileListLink_state = new LinkedListEntry<>(this); public final LinkedListEntry<Projectile> forceEmitterListLink_state = new LinkedListEntry<>(this); public final LinkedListEntry<Projectile> projectileListLink_actor = new LinkedListEntry<>(this); // circular headless linked list final public LinkedListEntry<Projectile> coupled = new LinkedListEntry<>(this); /** Has initFire or initFromSync been called?. */ private boolean initialized; public Actor owner; public WeaponConfig config; public final int configIndex; // the config index // IF AN ATTRIBUTE IS ADDED, DO NOT FORGET TO UPDATE resetTo() // (except config, etc) public long expiresAt_tick; public int bouncesLeft; // -1 for infinite public int activateBouncesLeft; // Settings that are read when the projectile is spawned, instead of on usage. // This mainly affects settings that are randomized based on tick. // This is because of performance. // All of these attributes will have to go into the WeaponSync message public boolean collideTile; public boolean collideShip; public boolean damageSelf; public boolean damageTeam; public int bounceFriction; public int bounceOtherAxisFriction; public int proxDist; public int proxExplodeDelay; public GCFunction2D forceFunction; public int forceDistanceShip; public int forceVelocityShip; public int forceDistanceProjectile; public int forceVelocityProjectile; /** If set, we hit a tile during during performDeadReckoning() and an event should be fired soon. */ private final PhysicsPoint hitTile = new PhysicsPoint(); public Actor proxActivatedBy; public long proxLastSeenDist; public long proxLastSeenDist_tick; public long proxActivatedAt_tick; public Projectile( ProjectileKey key, State state, MapEntity[] crossStateList, Actor owner, long createdAt_tick, WeaponConfig config, int projectile_index) { super(state, crossStateList, createdAt_tick, (state.isLast ? 0 : state.econfig.TRAILING_STATE_DELAY) + state.econfig.MINIMUM_HISTORY_TICKS); this.key = key; this.owner = owner; // note may be null for now, will be resolved by resetTo this.config = config; // note may be null for now, will be resolved by resetTo this.configIndex = projectile_index; this.radius = GCIntegerFixed.ZERO; } @Override public void getSync(GameOperation.WeaponSync.Projectile.Builder p) { p.setIndex(configIndex); p.setX(pos.pos.x); p.setY(pos.pos.y); p.setXVel(pos.vel.x); p.setYVel(pos.vel.y); p.setExpiresAt(this.expiresAt_tick); p.setBouncesLeft(bouncesLeft); p.setActivateBouncesLeft(activateBouncesLeft); p.setCollideTile(collideTile); p.setCollideShip(collideShip); p.setDamageSelf(damageSelf); p.setDamageTeam(damageTeam); p.setBounceFriction(bounceFriction); p.setBounceOtherAxisFriction(bounceOtherAxisFriction); p.setProxDist(proxDist); p.setProxExplodeDelay(proxExplodeDelay); p.setProxActivatedBy(proxActivatedBy == null || proxActivatedBy.isNonExistent(state.tick_now) ? 0 : proxActivatedBy.pid); p.setProxLastSeenDist(proxLastSeenDist); p.setProxLastSeenDistTick(proxLastSeenDist_tick); p.setProxActivatedAtTick(proxActivatedAt_tick); p.setForceFunction(forceFunction.id); p.setForceDistanceShip(forceDistanceShip); p.setForceVelocityShip(forceVelocityShip); p.setForceDistanceProjectile(forceDistanceProjectile); p.setForceVelocityProjectile(forceVelocityProjectile); } public void initFromSync(GameOperation.WeaponSync.Projectile s, long tick_now) { initialized = true; assert configIndex == s.getIndex(); pos.pos.x = s.getX(); pos.pos.y = s.getY(); pos.vel.x = s.getXVel(); pos.vel.y = s.getYVel(); expiresAt_tick = s.getExpiresAt(); bouncesLeft = s.getBouncesLeft(); activateBouncesLeft = s.getActivateBouncesLeft(); collideTile = s.getCollideTile(); collideShip = s.getCollideShip(); damageSelf = s.getDamageSelf(); damageTeam = s.getDamageTeam(); bounceFriction = s.getBounceFriction(); bounceOtherAxisFriction = s.getBounceOtherAxisFriction(); proxDist = s.getProxDist(); proxExplodeDelay = s.getProxExplodeDelay(); proxActivatedBy = s.getProxActivatedBy() == 0 ? null : state.actors.get(new ActorKey(s.getProxActivatedBy())); proxLastSeenDist = s.getProxLastSeenDist(); proxLastSeenDist_tick = s.getProxLastSeenDistTick(); proxActivatedAt_tick = s.getProxActivatedAtTick(); forceFunction = GCFunction2D.byId(s.getForceFunction()); forceDistanceShip = s.getForceDistanceShip(); forceVelocityShip = s.getForceVelocityShip(); forceDistanceProjectile = s.getForceDistanceProjectile(); forceVelocityProjectile = s.getForceVelocityProjectile(); } public void register() { Projectile oldValue = state.projectiles.put(this.key, this); assert oldValue == null; state.projectilesList.append(this.projectileListLink_state); this.owner.projectiles.append(this.projectileListLink_actor); if (this.isForceEmitter()) { state.forceEmitterList.append(this.forceEmitterListLink_state); } } @Override public void hardRemove(long tick) { super.hardRemove(tick); projectileListLink_actor.remove(); projectileListLink_state.remove(); forceEmitterListLink_state.remove(); state.projectiles.remove(this.key); coupled.remove(); // only modify the entry for _this_ projectile in the state list! // this method may be called while looping over this state list } public void initFire(long tick, PhysicsShipPosition actorPos) { initialized = true; pos.pos.set(actorPos.x, actorPos.y); int rot = 0; if (cfg(config.projectile_angleRelative, tick)) { rot = actorPos.rot_snapped; } rot += cfg(config.projectile_angle, tick); // relative to rot 0 int offsetX = cfg(config.projectile_offsetX, tick); int offsetY = config.projectile_offsetY.isIndexSet(configIndex)? cfg(config.projectile_offsetY, tick) : owner.radius.get(); PhysicsPoint offset = new PhysicsPoint(0, 0); PhysicsMath.rotationToPoint( offset, rot + EnvironmentConf.ROTATION_1_4TH, offsetX); PhysicsMath.rotationToPoint( offset, rot, offsetY); Collision collision = state.collision; collision.reset(); collision.setPreviousPosition(pos.pos); if (cfg(config.projectile_hitTile, tick)) { collision.setMap(state.env.getMap()); } collision.setVelocity(offset); collision.setRadius(radius.get()); collision.setBouncesLeft(0); // 0 bounces, the resulting position will be set at the collide position collision.tickMap(tick); collision.getNewPosition(pos.pos); // make sure the projectile does not spawn inside of a tile (unless the ship is inside a tile too) if (cfg(config.projectile_speedRelative, tick)) { pos.vel.set(actorPos.x_vel, actorPos.y_vel); } PhysicsMath.rotationToPoint( pos.vel, rot, cfg(config.projectile_speed, tick)); pos.enforceOverflowLimit(); collideTile = cfg(config.projectile_hitTile, tick); collideShip = cfg(config.projectile_hitShip, tick); damageSelf = cfg(config.projectile_damageSelf, tick); damageTeam = cfg(config.projectile_damageTeam, tick); expiresAt_tick = tick + cfg(config.projectile_expirationTicks, tick); bouncesLeft = cfg(config.projectile_bounces, tick); activateBouncesLeft = cfg(config.projectile_activateBounces, tick); bounceFriction = cfg(config.projectile_bounceFriction, tick); bounceOtherAxisFriction = cfg(config.projectile_bounceOtherAxisFriction, tick); proxDist = cfg(config.projectile_proxDistance, tick); proxExplodeDelay = config.projectile_proxExplodeTicks.isSet() ? cfg(config.projectile_proxExplodeTicks, tick) : -1; forceFunction = config.projectile_forceFunction.get(this.configIndex, configSeed(tick)); forceDistanceShip = cfg(config.projectile_forceDistanceShip, tick); forceVelocityShip = cfg(config.projectile_forceVelocityShip, tick); forceDistanceProjectile = cfg(config.projectile_forceDistanceProjectile, tick); forceVelocityProjectile = cfg(config.projectile_forceVelocityProjectile, tick); } public void attemptCorrectionForLateProjectile(long operation_tick, boolean operation_late) { // dead reckon current position so that it is no longer late // the position at the tick of this operation should not be dead reckoned, therefor +1 // First update the projectile up to the expiry... long expiry_tick = max(operation_tick, this.getExpiry()); long untilExpiry_ticks = min(expiry_tick - operation_tick, state.tick_now - operation_tick); long afterExpiry_ticks = max(state.tick_now - expiry_tick, 0); this.markDirtyWithinForceRange(operation_tick); // (only does something if we really are a force emitter. // further ticks are marked dirty by performDeadReckoning) this.performDeadReckoning(state.env.getMap(), operation_tick + 1, untilExpiry_ticks, true, true); if (expiry_tick <= state.tick_now) { // (performDeadReckoning and emitForce check for isNonExistent(tick)) this.explodeWithoutHit(expiry_tick, EXPLODE_REASON.EXPIRATION); this.softRemove(expiry_tick); // The expiry itself has already been simulated this.performDeadReckoning(state.env.getMap(), expiry_tick + 1, afterExpiry_ticks, true); } assert this.posHistory.getHighestTick() == state.tick_now; } @Override public void performDeadReckoning(PhysicsMap map, long tick_now, long reckon_ticks, boolean applyForceEmitters) { this.performDeadReckoning(map, tick_now, reckon_ticks, applyForceEmitters, false); } @SuppressWarnings("unchecked") private void performDeadReckoning(PhysicsMap map, long tick_now, long reckon_ticks, boolean applyForceEmitters, boolean markDirtyPositionPathWithinForceRange) { Collision collision = state.collision; collision.reset(); collision.setMap(this.collideTile ? map : null); collision.setRadius(this.radius.get()); for (long t = 0; t < reckon_ticks; ++t) { long tick = tick_now + t; dirtyPositionPathTracker.resolved(tick); assert !dirtyPositionPathTracker.isDirty(tick) : "performDeadReckoning: Skipped a tick!"; if (this.isNonExistent(tick) || hitTile.set) { updatedPosition(tick); continue; } collision.setPreviousPosition(pos.pos); collision.setVelocity(pos.vel); collision.setBouncesLeft(bouncesLeft); collision.tickMap(tick); collision.getNewPosition(pos.pos); collision.getVelocity(pos.vel); updatedPosition(tick); if (bouncesLeft >= 0) { bouncesLeft -= collision.getBounces(); if (bouncesLeft < 0) bouncesLeft = 0; } if (activateBouncesLeft > 0) { activateBouncesLeft -= collision.getBounces(); if (activateBouncesLeft < 0) activateBouncesLeft = 0; } // (applyForceEmitters is only set when correcting for late operations) if (applyForceEmitters && !this.isForceEmitter()) // Force emitters never affect other force emitters { // (or, at least for now: it causes too many inconsistencies) for (Projectile emitter : state.forceEmitterList) { // does nothing if out of range emitter.emitForce(tick, this); } } updatedPosition(tick); if (markDirtyPositionPathWithinForceRange) { this.markDirtyWithinForceRange(tick); } // hit a tile? if (collision.hasExhaustedBounces()) { collision.getHitTile(hitTile); hitTile.set = true; // The event is not really executed until actors have ticked (see tickProjectileAfterActor) // This is so that hitting an actor is prioritized over hitting a tile. } } } public void hitByActor(long tick, Actor actor, PhysicsPoint location) { // note: we may execute the event multiple times on the same state // This is valid. (it happens when a timewarp occurs) // The event should discard the previous consistency information. if (location.set) { this.pos.pos.set(location); updatedPosition(tick); } // Do not execute the hit tile event if it was planned. hitTile.unset(); ProjectileExplosion.Key eventKey = new ProjectileExplosion.Key(this.key); ProjectileExplosion event = (ProjectileExplosion) state.env.findEvent(eventKey); if (event == null) { event = new ProjectileExplosion(state.env, eventKey); } state.env.registerEvent(event); event.execute(tick, this.state, this, ProjectileExplosionPublic.EXPLODE_REASON.HIT_SHIP, actor, null); } public void tickProjectileAfterActor(long tick) { if (this.isNonExistent(tick)) { return; } if (this.hitTile.set) { // note: we may execute the event multiple times on the same state // This is valid. (it happens when a timewarp occurs) // The event should discard the previous consistency information. ProjectileExplosion.Key eventKey = new ProjectileExplosion.Key(this.key); ProjectileExplosion event = (ProjectileExplosion) state.env.findEvent(eventKey); if (event == null) { event = new ProjectileExplosion(state.env, eventKey); } if (event.hasOccurred(this.state.id)) { System.out.println("already occured??"); } state.env.registerEvent(event); event.execute(tick, this.state, this, ProjectileExplosionPublic.EXPLODE_REASON.HIT_TILE, null, hitTile); assert this.isRemoved(tick); // Otherwise this event fires over and over and over return; } // TODO: Use state.entityGrid if it is much faster? // Proximity bombs // (unlike continuum prox bombs still take part in regular collision unless // disabled by config) if (proxDist > 0 && (this.proxActivatedBy == null || this.proxActivatedBy.isNonExistent(tick))) { long proxDistSq = proxDist * (long) proxDist; for (Actor actor : state.actorsList) { if (this.collidesWithFilter.loopFilter(actor, tick)) { continue; } // easy case long distSq = this.pos.pos.distanceSquared(actor.pos.pos); if (distSq > proxDistSq) { continue; } long dist = this.pos.pos.distance(actor.pos.pos, distSq); if (dist > proxDist) { continue; } this.proxActivatedBy = actor; this.proxActivatedAt_tick = tick; this.proxLastSeenDist = dist; this.proxLastSeenDist_tick = tick; break; } } if (this.proxActivatedBy != null) { long dist = this.pos.pos.distance(this.proxActivatedBy.pos.pos); if (tick <= this.proxLastSeenDist_tick) { // Reexecuting moves, reset the last seen distance } else { if (dist > this.proxLastSeenDist) { // moving away! detonate explodeWithoutHit(tick, EXPLODE_REASON.PROX_DIST); assert this.isNonExistent(tick); // Otherwise this event fires over and over and over return; } } this.proxLastSeenDist = dist; this.proxLastSeenDist_tick = tick; if (proxExplodeDelay >= 0 && tick - this.proxActivatedAt_tick >= proxExplodeDelay) { explodeWithoutHit(tick, EXPLODE_REASON.PROX_DELAY); assert this.isNonExistent(tick); // Otherwise this event fires over and over and over return; } } } public void explodeWithoutHit(long tick, ProjectileExplosionPublic.EXPLODE_REASON reason) { assert !this.isNonExistent(tick); ProjectileExplosion.Key eventKey = new ProjectileExplosion.Key(this.key); ProjectileExplosion event = (ProjectileExplosion) state.env.findEvent(eventKey); if (event == null) { event = new ProjectileExplosion(state.env, eventKey); } state.env.registerEvent(event); event.execute(tick, this.state, this, reason, null, null); } public @Nullable Projectile findInOtherState(State otherState) { if (this.state.isForeign(otherState)) { return otherState.projectiles.get(this.key); } else { return (Projectile) this.crossStateList[otherState.id]; } } @Override public void resetTo(MapEntity other_) { super.resetTo(other_); Projectile other = (Projectile) other_; assert this.key.equals(other.key); this.initialized = other.initialized; if (this.owner == null) { if (other.owner != null) { this.owner = (Actor) other.owner.findInOtherState(state); this.config = this.owner.config.getWeaponConfig(other.config.weaponKey); } } else { assert this.owner.pid == other.owner.pid : "Projectiles should never change owners in a timewarp"; } this.coupled.previous = null; this.coupled.next = null; if (other.coupled.previous != null && other.coupled.next != null) { Projectile otherPrev = other.coupled.previous.data; Projectile otherNext = other.coupled.next.data; Projectile prev = otherPrev.findInOtherState(this.state); Projectile next = otherNext.findInOtherState(this.state); // Note that the coupled projectile might not exist yet! // it will be created at a different moment in the timewarp // therefor prev or next (or both) might be null. // State.resetTo has an assertion check to make sure the code below is proper if (prev != null) { this.coupled.previous = prev.coupled; prev.coupled.next = this.coupled; } if (next != null) { this.coupled.next = next.coupled; next.coupled.previous = this.coupled; } } else { assert other.coupled.previous == null; assert other.coupled.next == null; } this.expiresAt_tick = other.expiresAt_tick; this.bouncesLeft = other.bouncesLeft; this.activateBouncesLeft = other.activateBouncesLeft; if (other.proxActivatedBy == null) { this.proxActivatedBy = null; } else { this.proxActivatedBy = (Actor) other.proxActivatedBy.findInOtherState(state); } this.proxLastSeenDist = other.proxLastSeenDist; this.proxLastSeenDist_tick = other.proxLastSeenDist_tick; this.proxActivatedAt_tick = other.proxActivatedAt_tick; // do not reset references to events } public void resetToEmpty(long tick) { Projectile dummy = new Projectile(this.key, this.state, crossStateList, this.owner, tick, this.config, this.configIndex); crossStateList[this.state.id] = null; // skip assertion in resetTo this.resetTo(dummy); crossStateList[this.state.id] = (MapEntity) this; } public int getSplashDamage(Actor actor, long tick, int damage, int range, long rangeSq) { final PhysicsPositionVector myPos = new PhysicsPositionVector(); final PhysicsPositionVector actorPos = new PhysicsPositionVector(); if (!this.getHistoricPosition(myPos, tick, false)) { return 0; } if (!actor.getHistoricPosition(actorPos, tick, false)) { return 0; } long distSq = myPos.pos.distanceSquared(actorPos.pos); if (distSq >= rangeSq) { return 0; // out of range } long ldist = myPos.pos.distance(actorPos.pos, distSq); assert ldist >= 0; int dist = ldist > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) ldist; assert range >= dist; // if dist == splash do 0 damage return (int) ((long) (range - dist) * damage / range); } public void doSplashDamage(Actor except, long tick, Collection<Integer> killed) { int splash = cfg(config.projectile_damageSplash, tick) * 1024; if (splash == 0) { return; } int damage = cfg(config.projectile_damage, tick) * 1024; long splashSq = splash * (long) splash; boolean damageSelfKill = cfg(config.projectile_damageSelfKill, tick); boolean damageTeamKill = cfg(config.projectile_damageTeamKill, tick); for (Actor actor : state.actorsList) { if (actor.isNonExistent(tick) || actor.isDead(tick)) { continue; } if (actor == except) { continue; } if (actor == this.owner && !damageSelf) { continue; } // todo team int effectiveDamage = getSplashDamage(actor, tick, damage, splash, splashSq); actor.energy.addRelativeValue( Actor.ENERGY_SETTER.OTHER.id, tick, -effectiveDamage); if (actor.energy.get(tick) <= 0) { if (!damageSelfKill && actor == this.owner) { } else { actor.died(tick); killed.add(actor.pid); } } } } public void doSplashEmp(Actor except, long tick) { int p = this.configIndex; int splash = cfg(config.projectile_empSplash, tick) * 1024; if (splash == 0) { return; } int damage = cfg(config.projectile_empTicks, tick); long splashSq = splash * (long) splash; boolean empSelf = cfg(config.projectile_empSelf, tick); boolean empTeam = cfg(config.projectile_empTeam, tick); for (Actor actor : state.actorsList) { if (actor.isNonExistent(tick) || actor.isDead(tick)) { continue; } if (actor == except) { continue; } if (actor == this.owner && !empSelf) { continue; } // todo team actor.applyEmp(tick, getSplashDamage(actor, tick, damage, splash, splashSq)); } } @Override public int getStateId() { return state.id; } @Override public int getOwner() { return owner.pid; } @Override public long getExpiry() { return expiresAt_tick; } @Override public AttachmentData getAttachments() { return attachments; } public List<MapEntity> getCollidesWith() { return (List<MapEntity>) (Object) state.actorsList; } // second arg = tick public final LoopFilter<MapEntity, Long> collidesWithFilter = new LoopFilter<MapEntity, Long>() { @Override public boolean loopFilter(MapEntity en, Long arg) { if (en instanceof Actor) { Actor actor = (Actor) en; if (actor.isNonExistent(arg) || actor.isDead(arg)) { return true; } // never hit your own weapons... if (owner != null && owner.pid == actor.pid) { return true; } // todo freqs } return false; } }; @Override public int getBouncesLeft() { return this.bouncesLeft; } @Override public int getActivateBouncesLeft() { return this.activateBouncesLeft; } @Override public boolean isActive() { if (this.activateBouncesLeft > 0) { return false; } return true; } public boolean isForceEmitter() { if (!initialized) { throw new IllegalStateException(); } final boolean doShip = this.forceDistanceShip > 0 && this.forceVelocityShip != 0; final boolean doProjectile = this.forceDistanceProjectile > 0 && this.forceVelocityProjectile != 0; return doShip || doProjectile; } /** * Is the force of this entity applicable to the specified entity?. * This method checks if both of the entities currently exist, not dead, not on the same team, et cetera. * It does not check if the entity is within range. * @param tick The tick at which you would like to apply the force * @param en The entity that the force should be applied to * @return true if the force of this emitter is able to abe applied to "en" */ private boolean forceAppliesTo(long tick, MapEntity en) { final boolean doShip = this.forceDistanceShip > 0 && this.forceVelocityShip != 0; final boolean doProjectile = this.forceDistanceProjectile > 0 && this.forceVelocityProjectile != 0; if (en == this || en == this.owner) { return false; } if (this.isNonExistent(tick) || en.isNonExistent(tick)) { return false; } if (doShip && en instanceof Actor) { Actor actor = (Actor) en; if (actor.isDead(tick)) { return false; } if (!damageSelf && en == this.owner) { return false; } return true; } else if (doProjectile && en instanceof Projectile) { Projectile proj = (Projectile) en; if (!damageSelf && proj.owner == this.owner) { return false; } // Force emitters never affect other force emitters // (or, at least for now: it causes too many inconsistencies) if (proj.isForceEmitter()) { return false; } return true; } return false; } /** * Mark all entities we might apply a force on and are (somewhat) within range as having a dirty position path. * @param tick The tick to mark as dirty and the tick used for checks. */ public void markDirtyWithinForceRange(long tick) { final boolean doShip = this.forceDistanceShip > 0 && this.forceVelocityShip != 0; final boolean doProjectile = this.forceDistanceProjectile > 0 && this.forceVelocityProjectile != 0; if (!doShip && !doProjectile) { return; } if (this.isNonExistent(tick)) { return; } final PhysicsPoint forcePoint = new PhysicsPoint(); this.posHistory.get(forcePoint, tick); state.entityGrid.enableQueue(); try { Iterator<MapEntity> it = state.entityGrid.iterator( forcePoint, SwissArmyKnife.max(this.forceDistanceShip, this.forceDistanceProjectile)); while (it.hasNext()) { MapEntity en = it.next(); if (this.forceAppliesTo(tick, en)) { en.markDirtyPositionPath(tick); } } } finally { state.entityGrid.disableQueue(); } } /** Emit the configured force on all entities within range. * The velocity of these entities will be increased by the applied force (which might be negative). * @param tick */ public void emitForce(long tick) { final boolean doShip = this.forceDistanceShip > 0 && this.forceVelocityShip != 0; final boolean doProjectile = this.forceDistanceProjectile > 0 && this.forceVelocityProjectile != 0; if (!doShip && !doProjectile) { return; } if (this.isNonExistent(tick)) { return; } final PhysicsPoint forcePoint = new PhysicsPoint(); this.posHistory.get(forcePoint, tick); state.entityGrid.enableQueue(); try { Iterator<MapEntity> it = state.entityGrid.iterator( forcePoint, SwissArmyKnife.max(this.forceDistanceShip, this.forceDistanceProjectile)); while (it.hasNext()) { this.emitForce(tick, it.next()); } } finally { state.entityGrid.disableQueue(); } } /** Emit the configured force on the specified entity (if it is in range) * The velocity of the entity will be increased by the applied force (which might be negative). * @param tick * @param en */ public void emitForce(long tick, MapEntity en) { final PhysicsPoint forcePoint = new PhysicsPoint(); final PhysicsPositionVector otherPosition = new PhysicsPositionVector(); final PhysicsPoint velocity = new PhysicsPoint(); if (!this.forceAppliesTo(tick, en)) { return; } int forceDistance = 0; int forceVelocity = 0; if (en instanceof Actor) { forceDistance = forceDistanceShip; forceVelocity = forceVelocityShip; } else if (en instanceof Projectile) { forceDistance = forceDistanceProjectile; forceVelocity = forceVelocityProjectile; } else { assert false; } this.posHistory.get(forcePoint, tick); if (en.getHistoricPosition(otherPosition, tick, false) && PhysicsMath.force(velocity, otherPosition.pos, forcePoint, forceDistance, forceVelocity, forceFunction)) { en.markDirtyPositionPath(tick); en.pos.vel.add(velocity); en.pos.vel.enforceOverflowLimit(); en.updatedPosition(tick); } } @Override public GCInteger getWeaponConfigInteger(String name) { return config.selection.getInteger(name); } @Override public GCString getWeaponConfigString(String name) { return config.selection.getString(name); } @Override public GCBoolean getWeaponConfigBoolean(String name) { return config.selection.getBoolean(name); } @Override public GCIntegerList getWeaponConfigIntegerList(String name) { return config.selection.getIntegerList(name); } @Override public GCStringList getWeaponConfigStringList(String name) { return config.selection.getStringList(name); } @Override public GCBooleanList getWeaponConfigBooleanList(String name) { return config.selection.getBooleanList(name); } @Override public GCImage getWeaponConfigImage(String name, ResourceDB db) { return config.selection.getImage(name, db); } @Override public GCColour getWeaponConfigColour(String name) { return config.selection.getColour(name); } @Override public String getWeaponKey() { return config.weaponKey; } @Override public int getProjectileIndex() { return this.configIndex; } @Override public Iterator<ProjectilePublic> getCoupledProjectiles() { return (Iterator<ProjectilePublic>) (Object) coupled.iteratorReadOnly(); } public int configSeed(long tick) { assert owner != null; return owner.seed_low ^ ((int) tick); } // some short hands to save typing public int cfg(GCIntegerList configValue, long tick) { return configValue.get(this.configIndex, configSeed(tick)); } public boolean cfg(GCBooleanList configValue, long tick) { return configValue.get(this.configIndex, configSeed(tick)); } public String cfg(GCStringList configValue, long tick) { return configValue.get(this.configIndex, configSeed(tick)); } }