/* * Aphelion * Copyright (c) 2014 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.*; import aphelion.shared.net.protobuf.GameOperation; import aphelion.shared.physics.*; import aphelion.shared.physics.config.ActorConfig; import aphelion.shared.physics.config.WeaponConfig; import aphelion.shared.physics.valueobjects.PhysicsMoveable; import aphelion.shared.physics.valueobjects.PhysicsMovement; import aphelion.shared.physics.valueobjects.PhysicsPoint; import aphelion.shared.physics.valueobjects.PhysicsPointHistory; import aphelion.shared.physics.valueobjects.PhysicsPointHistoryDetailed; import aphelion.shared.physics.valueobjects.PhysicsRotation; import aphelion.shared.physics.valueobjects.PhysicsShipPosition; import aphelion.shared.physics.valueobjects.PhysicsWarp; import aphelion.shared.physics.valueobjects.*; import aphelion.shared.physics.valueobjects.PhysicsPointHistorySmooth.SMOOTHING_ALGORITHM; import aphelion.shared.swissarmyknife.*; import java.util.Iterator; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; /** * * @author Joris */ public class Actor extends MapEntity { private static final Logger log = Logger.getLogger("aphelion.shared.physics"); public ActorPublicImpl publicWrapper; public final LinkedListHead<Projectile> projectiles = new LinkedListHead<>(); // fired by the actor public final int pid; public final ActorKey key; public long seed; public int seed_high; public int seed_low; public String ship; public final PhysicsRotation rot = new PhysicsRotation(); public final PhysicsPointHistoryDetailed posHistoryDetailed; public final PhysicsPointHistory rotHistory; // x = rotation, y = rotation snapped public final RollingHistory<PhysicsMoveable> moveHistory; private long mostRecentMove_tick; public final PhysicsPointHistorySmooth smoothHistory; public final PhysicsExpiration switchedWeaponReload = new PhysicsExpiration(); public WeaponConfig lastWeaponFire; // RollingHistorySerialInteger is a very specific optimalization for // a value that is updated very often! (energy). This way we can prevent // making a timewarp anytime something involving with energy is a bit late. // Use get(-1) for any energy requirement checking (this is so that a different // execution order of energy modifications does not cause a timewarp) // use setXXX(-0) to substract this requirement public final RollingHistorySerialInteger energy; // * 1024 public static enum ENERGY_SETTER { OTHER(0), // for stuff that is only resetable using a timewarp RECHARGE(1), BOOST_COST(2); public final int id; // array index private ENERGY_SETTER(int id) { this.id = id; } } public Long empUntil_tick; public final RollingHistorySerialInteger dead; public Long spawnAt_tick; // IF AN ATTRIBUTE IS ADDED, DO NOT FORGET TO UPDATE resetTo() // (except config, publicWrapper, etc) public final ActorConfig config; public Actor(State state, MapEntity[] crossStateList, int pid, long createdAt_tick) { // Move history is used so that reordered move operations (which occurs a lot) // do not cause continues time warps. // We have to store enough history for moves so that any delayed move can recalculate your position // Suppose there are 4 trailing states. State 0 (the most recent) would need to store 3 trailing states // worth of move history. 4 * TRAILING_STATE_DELAY is the maximum age of any operation executed on state 0. // 3 * TRAILING_STATE_DELAY would the max on state 1, et cetera. super(state, crossStateList, createdAt_tick, (state.econfig.TRAILING_STATES - state.id - 1) * state.econfig.TRAILING_STATE_DELAY + state.econfig.MINIMUM_HISTORY_TICKS); posHistoryDetailed = (PhysicsPointHistoryDetailed) this.posHistory; this.switchedWeaponReload.setExpiration(createdAt_tick); this.pid = pid; key = new ActorKey(this.pid); moveHistory = new RollingHistory<>(createdAt_tick, HISTORY_LENGTH); this.mostRecentMove_tick = this.createdAt_tick; rotHistory = new PhysicsPointHistory(createdAt_tick, HISTORY_LENGTH); smoothHistory = new PhysicsPointHistorySmooth(createdAt_tick, posHistory, velHistory); // Never smooth on the last state (unless this is the only state that exists) // so that any inconsistencies will always be resolved if (state.isLast && state.id > 0) { smoothHistory.setAlgorithm(SMOOTHING_ALGORITHM.NONE); } else { smoothHistory.setAlgorithm(state.econfig.POSITION_SMOOTHING ? SMOOTHING_ALGORITHM.LINEAR : SMOOTHING_ALGORITHM.NONE); } energy = new RollingHistorySerialInteger(createdAt_tick, HISTORY_LENGTH, ENERGY_SETTER.values().length); energy.setMinimum(createdAt_tick, 0); dead = new RollingHistorySerialInteger(createdAt_tick, HISTORY_LENGTH, 1); dead.setMinimum(createdAt_tick, 0); dead.setMaximum(createdAt_tick, 1); config = new ActorConfig(this); this.radius = config.radius; config.smoothingAlgorithm.addWeakChangeListenerAndFire(smoothingChangeListener); config.smoothingLookAheadTicks.addWeakChangeListenerAndFire(smoothingChangeListener); config.smoothingStepRatio.addWeakChangeListenerAndFire(smoothingChangeListener); config.smoothingDistanceLimit.addWeakChangeListenerAndFire(smoothingChangeListener); publicWrapper = new ActorPublicImpl(this, state); } private final WrappedValueAbstract.ChangeListener smoothingChangeListener = new WrappedValueAbstract.ChangeListener() { @Override public void gameConfigValueChanged(WrappedValueAbstract val) { if (val == config.smoothingAlgorithm) { SMOOTHING_ALGORITHM algo = SMOOTHING_ALGORITHM.NONE; String algoStr = config.smoothingAlgorithm.get(); try { if (!algoStr.isEmpty()) { algo = SMOOTHING_ALGORITHM.valueOf(algoStr); } } catch (IllegalArgumentException ex) { log.log(Level.WARNING, "Given smoothing-algorithm algorithm {0} is not a known algorithm", config.smoothingAlgorithm.get()); } smoothHistory.setAlgorithm(algo); } if (val == config.smoothingLookAheadTicks) { smoothHistory.setLookAheadTicks(config.smoothingLookAheadTicks.get()); } if (val == config.smoothingStepRatio) { smoothHistory.setStepRatio(config.smoothingStepRatio.get()); } if (val == config.smoothingDistanceLimit) { smoothHistory.setSmoothLimitDistance(config.smoothingDistanceLimit.get() * 1024); } } }; @Override public void hardRemove(long tick) { super.hardRemove(tick); state.actors.remove(this.key); state.actorsList.remove(this); } public void getSync(GameOperation.ActorSync.Builder s) { s.setPid(pid); s.setTick(state.tick_now); s.setX(pos.pos.x); s.setY(pos.pos.y); s.setXVel(pos.vel.x); s.setYVel(pos.vel.y); s.setRotation(rot.points); // position is synced using a warp packet s.setSwitchedWeaponReloadTick(this.switchedWeaponReload.getExpiration()); for (WeaponConfig c : config.weapons.values()) { s.addWeaponReloadKey(c.weaponKey); s.addWeaponReloadTick(c.weaponReload.getExpiration()); } if (lastWeaponFire == null) { s.clearLastWeaponFire(); } else { s.setLastWeaponFire(lastWeaponFire.weaponKey); } s.setEnergy(energy.get(state.tick_now)); if (empUntil_tick == null) { s.clearEmpUntilTick(); } else { s.setEmpUntilTick(this.empUntil_tick); } s.setDead(dead.get(state.tick_now) == 1); if (this.spawnAt_tick == null) { s.clearSpawnAtTick(); } else { s.setSpawnAtTick(this.spawnAt_tick); } } private boolean tmp_initFromSync_dirty; private <T> T initFromSync_set(T oldVal, T newVal) { if (!tmp_initFromSync_dirty) { boolean changed = !Objects.equals(oldVal, newVal); tmp_initFromSync_dirty = changed; } return newVal; } public boolean initFromSync(GameOperation.ActorSync s) { tmp_initFromSync_dirty = false; long operation_tick = s.getTick(); assert operation_tick <= state.tick_now; // make sure the energy used for matching does not get modified by a side effect int myEnergy = this.energy.get(operation_tick); this.switchedWeaponReload.setExpiration(initFromSync_set(this.switchedWeaponReload.getExpiration(), s.getSwitchedWeaponReloadTick())); PhysicsWarp warp = new PhysicsWarp(s.getX(), s.getY(), s.getXVel(), s.getYVel(), s.getRotation()); PhysicsShipPosition currentPos = new PhysicsShipPosition(); this.getHistoricPosition(currentPos, operation_tick, false); if (!warp.equalsShipPosition(currentPos)) { // incorrect position tmp_initFromSync_dirty = true; this.moveHistory.setHistory(operation_tick, warp); this.applyMoveable(warp, operation_tick); // sets the current position this.updatedPosition(operation_tick); // 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 this.performDeadReckoning(state.env.getMap(), operation_tick + 1, state.tick_now - operation_tick, true); } for (int i = 0; i < s.getWeaponReloadKeyCount(); ++i) { WeaponConfig c = config.getWeaponConfig(s.getWeaponReloadKey(i)); try { c.weaponReload.setExpiration(initFromSync_set(c.weaponReload.getExpiration(), s.getWeaponReloadTick(i))); } catch (IndexOutOfBoundsException ex) { log.log(Level.SEVERE, "", ex); } } if (s.hasLastWeaponFire()) { this.lastWeaponFire = initFromSync_set(this.lastWeaponFire, config.getWeaponConfig(s.getLastWeaponFire())); } else { this.lastWeaponFire = initFromSync_set(this.lastWeaponFire, null); } if (myEnergy != s.getEnergy()) { this.energy.setAbsoluteOverrideValue(ENERGY_SETTER.OTHER.id, operation_tick, s.getEnergy()); tmp_initFromSync_dirty = true; } if (s.hasEmpUntilTick()) { this.empUntil_tick = initFromSync_set(this.empUntil_tick, s.getEmpUntilTick()); } else { this.empUntil_tick = initFromSync_set(this.empUntil_tick, null); } this.dead.setAbsoluteValue(0, operation_tick, s.getDead() ? 1 : 0); if (s.hasSpawnAtTick()) { this.spawnAt_tick = initFromSync_set(this.spawnAt_tick, s.getSpawnAtTick()); } else { this.spawnAt_tick = initFromSync_set(this.spawnAt_tick, null); } return tmp_initFromSync_dirty; } public boolean isDead(long tick) { return dead.get(tick) == 1; } public void died(long tick) { dead.setAbsoluteValue(0, tick, 1); spawnAt_tick = tick + config.respawnDelay.get(); if (spawnAt_tick <= tick) { // can not be respawned in this tick (or in the past) spawnAt_tick = tick + 1; } } @Override public void performDeadReckoning(PhysicsMap map, long tick_now, long reckon_ticks, boolean applyForceEmitters) { Collision collision = state.collision; collision.reset(); collision.setMap(map); collision.setRadius(this.radius.get()); collision.setCollideGrid(state.entityGrid); collision.setCollideFilter(collideFilter); collision.setBounceFriction(config.bounceFriction.get()); collision.setOtherAxisFriction(config.bounceOtherAxisFriction.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!"; final boolean dead = this.isDead(tick); int prevEnergy = this.energy.get(tick - 1); PhysicsMoveable prevMove = this.getHistoricMovement(tick - 1, false); boolean prevBoost = prevMove instanceof PhysicsMovement && ((PhysicsMovement) prevMove).isValidBoost() && prevEnergy >= config.boostEnergy.get(); this.pos.vel.limitLength( prevBoost && config.boostSpeed.isSet() ? config.boostSpeed.get() : config.speed.get()); this.pos.vel.enforceOverflowLimit(); collision.setPreviousPosition(pos.pos); collision.setPosHistoryDetails(this.posHistoryDetailed); collision.setVelocity(pos.vel); collision.tickMap(tick); collision.getNewPosition(pos.pos); collision.getVelocity(pos.vel); if (!dead) { if (this.useSmoothForCollision(tick)) { final PhysicsPoint point = new PhysicsPoint(); this.getHistoricSmoothPosition(point, tick - 1, false); collision.setPreviousPosition(point); collision.setPosHistoryDetails(null); // this would not work properly when smoothed this.getHistoricSmoothPosition(point, tick, false); collision.setNewPosition(point); } else { // the prevPos, newPos and posHistoryDetails are still set correctly on Collision, // in order to properly execute tickEntityCollision } collision.tickEntityCollision(tick); updatedPosition(tick); Iterator<Collision.HitData> it = collision.getHitEntities(); while (it.hasNext()) { Collision.HitData hit = it.next(); if (hit.entity instanceof Projectile) { Projectile proj = (Projectile)hit.entity; proj.hitByActor(tick, this, hit.location); if (this.isDead(tick)) { break; // just died, no more hits } } else { log.log(Level.WARNING, "Hit something unexpected"); } } // Any speed increase applied during this tick is not used until the next tick applyMoveable(moveHistory.get(tick), tick); } updatedPosition(tick); // (applyForceEmitters is only set when correcting for late operations) if (applyForceEmitters) { for (Projectile other : state.forceEmitterList) { // does nothing if out of range other.emitForce(tick, this); } } updatedPosition(tick); } } private final LoopFilter<MapEntity, Long> collideFilter = new LoopFilter<MapEntity, Long>() { @Override public boolean loopFilter(MapEntity en, Long tick) { if (!(en instanceof Projectile)) { return true; // skip } Projectile proj = (Projectile) en; return !proj.collideShip || proj.owner == Actor.this || proj.activateBouncesLeft > 0; } }; public void setShip(String ship) { if (Objects.equals(ship, this.ship)) { return; } this.ship = ship; config.selection.selection.setShip(ship); config.selection.resolveAllValues(); for (WeaponConfig weapon : config.weapons.values()) { weapon.selection.selection.setShip(ship); weapon.selection.resolveAllValues(); } } public void applyMoveable(PhysicsMoveable move_, long tick) { // reset the boost cost for this tick this.energy.setRelativeValue( ENERGY_SETTER.BOOST_COST.id, tick, 0 ); if (move_ == null) { return; } if (tick > mostRecentMove_tick) { mostRecentMove_tick = tick; } if (move_ instanceof PhysicsWarp) { PhysicsWarp warp = (PhysicsWarp) move_; if (warp.has_x) { pos.pos.x = warp.x; } if (warp.has_y) { pos.pos.y = warp.y; } if (warp.has_x_vel) { pos.vel.x = warp.x_vel; } if (warp.has_y_vel) { pos.vel.y = warp.y_vel; } if (warp.has_rotation) { rot.points = warp.rotation; rot.snap(config.rotationPoints.get()); } } else if (move_ instanceof PhysicsMovement) { PhysicsMovement move = (PhysicsMovement) move_; if (move.left || move.right) { // rotationSpeed is never larger than ROTATION_POINTS if (move.left) { this.rot.points -= config.rotationSpeed.get(); } if (move.right) { this.rot.points += config.rotationSpeed.get(); } this.rot.points %= EnvironmentConf.ROTATION_POINTS; if (this.rot.points < 0) { this.rot.points += EnvironmentConf.ROTATION_POINTS; } this.rot.snapped = PhysicsMath.snapRotation(this.rot.points, config.rotationPoints.get()); } if (move.up || move.down) { int thrust; // Base the boost requirement on the previous tick (tick - 1), // because the energy for this tick may not have been defined fully yet. // Using "tick - 0" could result in inconsistencies with different states // if this movement is not executed in the same order in relation to other // energy modifiers. if (move.boost && this.energy.get(tick - 1) >= config.boostEnergy.get()) { thrust = config.boostThrust.isSet() ? config.boostThrust.get() : config.thrust.get(); this.energy.setRelativeValue( ENERGY_SETTER.BOOST_COST.id, tick, -config.boostEnergy.get() ); //System.out.println(tick + " " + state.id + " " + this.energy.get(tick)); } else { thrust = config.thrust.get(); } if (move.up) { PhysicsMath.rotationToPoint(this.pos.vel, this.rot.points, thrust); } if (move.down) { PhysicsMath.rotationToPoint(this.pos.vel, this.rot.points, -thrust); } // vel may overflow here if thrust is extremely high, but that is ok // just make sure other stuff does not overflow as a result this.pos.vel.enforceOverflowLimit(); } } else { assert false; } } @Override public void updatedPosition(long tick) { try { super.updatedPosition(tick); } catch(IllegalStateException ex) { log.log(Level.SEVERE, "Error setting the history for the actor {0}", this.pid); throw ex; } rotHistory.setHistory(tick, rot.points, rot.snapped); } public @Nullable Actor findInOtherState(State otherState) { if (this.state.isForeign(otherState)) { return otherState.actors.get(this.key); } else { return (Actor) this.crossStateList[otherState.id]; } } @Override public void resetTo(MapEntity other_) { super.resetTo(other_); Actor other = (Actor) other_; assert pid == other.pid; seed = other.seed; seed_high = other.seed_high; seed_low = other.seed_low; rot.set(other.rot); setShip(other.ship); rotHistory.set(other.rotHistory); moveHistory.set(other.moveHistory); mostRecentMove_tick = other.mostRecentMove_tick; if (pid != publicWrapper.pid) { publicWrapper = new ActorPublicImpl(this, this.state); } switchedWeaponReload.set(other.switchedWeaponReload); this.lastWeaponFire = null; for (WeaponConfig otherConfig : other.config.weapons.values()) { WeaponConfig myConfig = config.getWeaponConfig(otherConfig.weaponKey); // never returns null myConfig.weaponReload.set(otherConfig.weaponReload); if (otherConfig == other.lastWeaponFire) { this.lastWeaponFire = myConfig; } } energy.set(other.energy); empUntil_tick = other.empUntil_tick; dead.set(other.dead); spawnAt_tick = other.spawnAt_tick; } public void resetToEmpty(long tick) { // use an empty actor to reset everything to default values Actor dummy = new Actor(this.state, crossStateList, pid, tick); crossStateList[this.state.id] = null; // skip assertion in resetTo this.resetTo(dummy); crossStateList[this.state.id] = (MapEntity) this; } public boolean canFireWeapon(WEAPON_SLOT weapon, long tick) { return canFireWeapon(weapon, tick, false); } /** Check if the actor is able to fire thew weapon at the given tick. * * @param weapon * @param tick * @param weakCheck If set, do not check for conditions that are likely to be * (temporarily) incorrect when dealing with networking. * For example when energy of an enemy is decreased by hitting him, * his weapon might not be able to fire. A timewarp then decides the * enemy was not hit after all, and the weapon is spawned. Therefor * energy is not included if weakCheck=true. * * @return true if the given weapon is able to fire. */ public boolean canFireWeapon(WEAPON_SLOT weapon, long tick, boolean weakCheck) { ActorConfig.WeaponSlotConfig slot = this.config.weaponSlots[weapon.id]; if (isDead(tick)) { return false; } if (!slot.isValidWeapon()) { return false; } if (slot.config.projectile_expirationTicks.get(0, this.configSeed(tick)) <= 0) { return false; } // History is not tracked for fire delay; this is fine, use a timewarp for late weapons. // (this is very rare because the input code does not send a weapon fire packet // to the server if the player does not have the proper amount of energy) if (slot.config.weaponReload.isActiveAt(tick)) { // reload per weapon return false; } if (slot.config != this.lastWeaponFire) { if (this.switchedWeaponReload.isActiveAt(tick)) { // weapon switch delay return false; } } if (weakCheck) { return true; } if (this.energy.get(tick - 1) < this.config.weaponSlots[weapon.id].config.fireEnergy.get() * 1024) { return false; } if (slot.config.fireProjectileLimit.isSet()) { for (int i = 0; i < slot.config.fireProjectileLimit.getValuesLength(); ++i) { int count = findProjectileCount(tick, slot.config.fireProjectileLimitGroup.get(i, this.configSeed(tick)), null); if (count >= slot.config.fireProjectileLimit.get(i, this.configSeed(tick))) { return false; // too many } } } return true; } public int getMaxEnergy() { int nrg = config.maxEnergy.get() * 1024; if (nrg < 1) { nrg = 1; } return nrg; } public void tickEnergy() { int effectiveRecharge = config.recharge.get(); if (isDead(state.tick_now)) { effectiveRecharge = 0; } if (this.empUntil_tick != null && state.tick_now <= this.empUntil_tick) { effectiveRecharge = 0; } energy.setRelativeValue(ENERGY_SETTER.RECHARGE.id, state.tick_now, effectiveRecharge); energy.setMinimum(state.tick_now, 0); energy.setMaximum(state.tick_now, this.getMaxEnergy()); } public Actor getOlderActor(long tick, boolean ignoreSoftDelete, boolean lookAtOtherStates) { return (Actor) getEntityAt(tick, ignoreSoftDelete, lookAtOtherStates); } public boolean getHistoricPosition(PhysicsShipPosition pos, long tick, boolean lookAtOlderStates) { Actor actor = getOlderActor(tick, false, lookAtOlderStates); if (actor == null) { pos.unset(); return false; // deleted or ticks go back way too far } pos.x = actor.posHistory.getX(tick); pos.y = actor.posHistory.getY(tick); pos.x_vel = actor.velHistory.getX(tick); pos.y_vel = actor.velHistory.getY(tick); pos.smooth_x = actor.smoothHistory.getX(tick); pos.smooth_y = actor.smoothHistory.getY(tick); pos.rot = actor.rotHistory.getX(tick); pos.rot_snapped = actor.rotHistory.getY(tick); pos.set = true; return true; } /** {@inheritDoc} */ @Override public boolean getHistoricSmoothPosition(PhysicsPoint pos, long tick, boolean lookAtOtherStates) { Actor actor = getOlderActor(tick, false, lookAtOtherStates); if (actor == null) { pos.unset(); return false; // deleted or too far in the past } actor.smoothHistory.getSmooth(pos, tick); return pos.set; } private boolean useSmoothForCollision(long tick) { if (state.isLast) { // The last state should consistent across all servers and clients // Smoothed position is not consistent because it does its magic by // relying on the order moves are received. return false; } // No need to use smoothed if we have up to date movement data // This implementation might cause jumpiness, for example, if: // 1. now = tick 120 // 2. A move has been received at tick 100 // Moments later: // 3. A move has been received at tick 90 // This should not occur often because move packets overlap each other. if (tick > mostRecentMove_tick) { return false; } return config.smoothingProjectileCollisions.get(); } public int getHistoricEnergy(long tick, boolean lookAtOlderStates) { Actor actor = getOlderActor(tick, false, lookAtOlderStates); if (actor == null) { return 0; // deleted or ticks go back way too far } return actor.energy.get(tick); } public PhysicsMoveable getHistoricMovement(long tick, boolean lookAtOlderStates) { Actor actor = getOlderActor(tick, false, lookAtOlderStates); if (actor == null) { return null; // deleted or ticks go back way too far } return actor.moveHistory.get(tick); } public int randomRotation(long tick) { int tick_low = (int) tick; // low bits int hash = SwissArmyKnife.jenkinMix(seed_high, seed_low, tick_low); return Math.abs(hash) % EnvironmentConf.ROTATION_POINTS; } public void findSpawnPoint(PhysicsPoint resultTile, long tick) { int tileRadius = radius.get() * 2 / PhysicsMap.TILE_PIXELS; state.findRandomPointOnMap( resultTile, tick, seed, config.spawnX.get(), config.spawnY.get(), config.spawnRadius.get(), tileRadius); } public void applyEmp(long tick, int empTime) { if (empTime > 0) { long until = tick + empTime; if (this.empUntil_tick == null || until > this.empUntil_tick) { this.empUntil_tick = until; } // remove recharge for (long t = tick; t <= this.empUntil_tick && t <= energy.getMostRecentTick(); ++t) { energy.setRelativeValue(ENERGY_SETTER.RECHARGE.id, t, 0); } } } public int findProjectileCount(long tick, int projectile_limit_group, Projectile[] oldest) { int count = 0; for (LinkedListEntry<Projectile> e = this.projectiles.first;e != null; e = e.next) { Projectile proj = e.data; int group = proj.cfg(proj.config.projectile_limitGroup, tick); if (group != projectile_limit_group) { continue; } if (proj.createdAt_tick > tick) { continue; } if (proj.isNonExistent(tick)) { continue; } ++count; if (oldest != null) { for (int i = 0; i < oldest.length; ++i) { if (oldest[i] == null) { oldest[i] = proj; } else if (proj.createdAt_tick < oldest[i].createdAt_tick) { oldest[i] = proj; } } } } return count; } public int configSeed(long tick) { return this.seed_low ^ ((int) tick); } }