2115 lines
68 KiB
Java
2115 lines
68 KiB
Java
package mindustry.entities.comp;
|
|
|
|
import arc.*;
|
|
import arc.Graphics.*;
|
|
import arc.Graphics.Cursor.*;
|
|
import arc.func.*;
|
|
import arc.graphics.*;
|
|
import arc.graphics.g2d.*;
|
|
import arc.math.*;
|
|
import arc.math.geom.*;
|
|
import arc.math.geom.QuadTree.*;
|
|
import arc.scene.ui.*;
|
|
import arc.scene.ui.layout.*;
|
|
import arc.struct.*;
|
|
import arc.util.*;
|
|
import arc.util.io.*;
|
|
import mindustry.*;
|
|
import mindustry.annotations.Annotations.*;
|
|
import mindustry.audio.*;
|
|
import mindustry.content.*;
|
|
import mindustry.core.*;
|
|
import mindustry.ctype.*;
|
|
import mindustry.entities.*;
|
|
import mindustry.game.EventType.*;
|
|
import mindustry.game.*;
|
|
import mindustry.game.Teams.*;
|
|
import mindustry.gen.*;
|
|
import mindustry.graphics.*;
|
|
import mindustry.logic.*;
|
|
import mindustry.type.*;
|
|
import mindustry.ui.*;
|
|
import mindustry.world.*;
|
|
import mindustry.world.blocks.ConstructBlock.*;
|
|
import mindustry.world.blocks.*;
|
|
import mindustry.world.blocks.environment.*;
|
|
import mindustry.world.blocks.heat.*;
|
|
import mindustry.world.blocks.heat.HeatConductor.*;
|
|
import mindustry.world.blocks.logic.LogicBlock.*;
|
|
import mindustry.world.blocks.payloads.*;
|
|
import mindustry.world.blocks.power.*;
|
|
import mindustry.world.consumers.*;
|
|
import mindustry.world.meta.*;
|
|
import mindustry.world.modules.*;
|
|
|
|
import java.util.*;
|
|
|
|
import static mindustry.Vars.*;
|
|
|
|
@EntityDef(value = {Buildingc.class}, isFinal = false, genio = false, serialize = false)
|
|
@Component(base = true)
|
|
abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, QuadTreeObject, Displayable, Sized, Senseable, Controllable, Settable{
|
|
//region vars and initialization
|
|
static final float timeToSleep = 60f * 1, recentDamageTime = 60f * 5f;
|
|
static final ObjectSet<Building> tmpTiles = new ObjectSet<>();
|
|
static final Seq<Building> tempBuilds = new Seq<>();
|
|
static final BuildTeamChangeEvent teamChangeEvent = new BuildTeamChangeEvent();
|
|
static final BuildDamageEvent bulletDamageEvent = new BuildDamageEvent();
|
|
static int sleepingEntities = 0;
|
|
|
|
@Import float x, y, health, maxHealth;
|
|
@Import Team team;
|
|
|
|
transient Tile tile;
|
|
transient Block block;
|
|
transient Seq<Building> proximity = new Seq<>(6);
|
|
transient int cdump;
|
|
transient int rotation;
|
|
transient float payloadRotation;
|
|
transient String lastAccessed;
|
|
transient boolean wasDamaged; //used only by the indexer
|
|
transient float visualLiquid;
|
|
|
|
/** TODO Each bit corresponds to a team ID. Only 64 are supported. Does not work on servers. */
|
|
transient long visibleFlags;
|
|
transient boolean wasVisible; //used only by the block renderer when fog is on (TODO replace with discovered check?)
|
|
|
|
transient boolean enabled = true;
|
|
transient @Nullable Building lastDisabler;
|
|
|
|
@Nullable PowerModule power;
|
|
@Nullable ItemModule items;
|
|
@Nullable LiquidModule liquids;
|
|
|
|
/** Base efficiency. Takes the minimum value of all consumers. */
|
|
transient float efficiency;
|
|
/** Same as efficiency, but for optional consumers only. */
|
|
transient float optionalEfficiency;
|
|
/** The efficiency this block *would* have if shouldConsume() returned true. */
|
|
transient float potentialEfficiency;
|
|
|
|
transient float healSuppressionTime = -1f;
|
|
transient float lastHealTime = -120f * 10f;
|
|
transient Color suppressColor = Pal.sapBullet;
|
|
|
|
private transient float lastDamageTime = -recentDamageTime;
|
|
private transient float timeScale = 1f, timeScaleDuration;
|
|
private transient float dumpAccum;
|
|
|
|
private transient @Nullable SoundLoop sound;
|
|
|
|
private transient boolean sleeping;
|
|
private transient float sleepTime;
|
|
private transient boolean initialized;
|
|
|
|
/** Sets this tile entity data to this and adds it if necessary. */
|
|
public Building init(Tile tile, Team team, boolean shouldAdd, int rotation){
|
|
if(!initialized){
|
|
create(tile.block(), team);
|
|
}else{
|
|
if(block.hasPower){
|
|
power.init = false;
|
|
//reinit power graph
|
|
new PowerGraph().add(self());
|
|
}
|
|
}
|
|
proximity.clear();
|
|
this.rotation = rotation;
|
|
this.tile = tile;
|
|
|
|
set(tile.drawx(), tile.drawy());
|
|
|
|
if(shouldAdd){
|
|
add();
|
|
}
|
|
|
|
created();
|
|
|
|
return self();
|
|
}
|
|
|
|
/** Sets up all the necessary variables, but does not add this entity anywhere. */
|
|
public Building create(Block block, Team team){
|
|
this.block = block;
|
|
this.team = team;
|
|
|
|
if(block.loopSound != Sounds.none){
|
|
sound = new SoundLoop(block.loopSound, block.loopSoundVolume);
|
|
}
|
|
|
|
health = block.health;
|
|
maxHealth(block.health);
|
|
timer(new Interval(block.timers));
|
|
|
|
if(block.hasItems) items = new ItemModule();
|
|
if(block.hasLiquids) liquids = new LiquidModule();
|
|
if(block.hasPower){
|
|
power = new PowerModule();
|
|
power.graph.add(self());
|
|
}
|
|
|
|
initialized = true;
|
|
|
|
return self();
|
|
}
|
|
|
|
@Override
|
|
public void add(){
|
|
if(power != null){
|
|
power.graph.checkAdd();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Replace
|
|
public int tileX(){
|
|
return tile.x;
|
|
}
|
|
|
|
@Override
|
|
@Replace
|
|
public int tileY(){
|
|
return tile.y;
|
|
}
|
|
|
|
//endregion
|
|
//region io
|
|
|
|
public final void writeBase(Writes write){
|
|
boolean writeVisibility = state.rules.fog && visibleFlags != 0;
|
|
|
|
write.f(health);
|
|
write.b(rotation | 0b10000000);
|
|
write.b(team.id);
|
|
write.b(writeVisibility ? 4 : 3); //version
|
|
write.b(enabled ? 1 : 0);
|
|
//write presence of items/power/liquids/cons, so removing/adding them does not corrupt future saves.
|
|
write.b(moduleBitmask());
|
|
if(items != null) items.write(write);
|
|
if(power != null) power.write(write);
|
|
if(liquids != null) liquids.write(write);
|
|
|
|
//efficiency is written as two bytes to save space
|
|
write.b((byte)(Mathf.clamp(efficiency) * 255f));
|
|
write.b((byte)(Mathf.clamp(optionalEfficiency) * 255f));
|
|
|
|
//only write visibility when necessary, saving 8 bytes - implies new version
|
|
if(writeVisibility){
|
|
write.l(visibleFlags);
|
|
}
|
|
}
|
|
|
|
public final void readBase(Reads read){
|
|
//cap health by block health in case of nerfs
|
|
health = Math.min(read.f(), block.health);
|
|
byte rot = read.b();
|
|
team = Team.get(read.b());
|
|
|
|
rotation = rot & 0b01111111;
|
|
|
|
int moduleBits = moduleBitmask();
|
|
boolean legacy = true;
|
|
byte version = 0;
|
|
|
|
//new version
|
|
if((rot & 0b10000000) != 0){
|
|
version = read.b(); //version of entity save
|
|
if(version >= 1){
|
|
byte on = read.b();
|
|
this.enabled = on == 1;
|
|
}
|
|
|
|
//get which modules should actually be read; this was added in version 2
|
|
if(version >= 2){
|
|
moduleBits = read.b();
|
|
}
|
|
legacy = false;
|
|
}
|
|
|
|
if((moduleBits & 1) != 0) (items == null ? new ItemModule() : items).read(read, legacy);
|
|
if((moduleBits & 2) != 0) (power == null ? new PowerModule() : power).read(read, legacy);
|
|
if((moduleBits & 4) != 0) (liquids == null ? new LiquidModule() : liquids).read(read, legacy);
|
|
|
|
//unnecessary consume module read in version 2 and below
|
|
if(version <= 2) read.bool();
|
|
|
|
//version 3 has efficiency numbers instead of bools
|
|
if(version >= 3){
|
|
efficiency = potentialEfficiency = read.ub() / 255f;
|
|
optionalEfficiency = read.ub() / 255f;
|
|
}
|
|
|
|
//version 4 (and only 4 at the moment) has visibility flags
|
|
if(version == 4){
|
|
visibleFlags = read.l();
|
|
}
|
|
}
|
|
|
|
public int moduleBitmask(){
|
|
return (items != null ? 1 : 0) | (power != null ? 2 : 0) | (liquids != null ? 4 : 0) | 8;
|
|
}
|
|
|
|
public void writeAll(Writes write){
|
|
writeBase(write);
|
|
write(write);
|
|
}
|
|
|
|
public void readAll(Reads read, byte revision){
|
|
readBase(read);
|
|
read(read, revision);
|
|
}
|
|
|
|
@CallSuper
|
|
public void write(Writes write){
|
|
//overriden by subclasses!
|
|
}
|
|
|
|
@CallSuper
|
|
public void read(Reads read, byte revision){
|
|
//overriden by subclasses!
|
|
}
|
|
|
|
//endregion
|
|
//region utility methods
|
|
|
|
public boolean isDiscovered(Team viewer){
|
|
if(state.rules.limitMapArea && world.getDarkness(tile.x, tile.y) >= 3){
|
|
return false;
|
|
}
|
|
|
|
if(viewer == null || !state.rules.staticFog || !state.rules.fog){
|
|
return true;
|
|
}
|
|
if(block.size <= 2){
|
|
return fogControl.isDiscovered(viewer, tile.x, tile.y);
|
|
}else{
|
|
int s = block.size / 2;
|
|
return fogControl.isDiscovered(viewer, tile.x, tile.y) ||
|
|
fogControl.isDiscovered(viewer, tile.x - s, tile.y - s) ||
|
|
fogControl.isDiscovered(viewer, tile.x - s, tile.y + s) ||
|
|
fogControl.isDiscovered(viewer, tile.x + s, tile.y + s) ||
|
|
fogControl.isDiscovered(viewer, tile.x + s, tile.y - s);
|
|
}
|
|
}
|
|
|
|
public void addPlan(boolean checkPrevious){
|
|
addPlan(checkPrevious, false);
|
|
}
|
|
|
|
public void addPlan(boolean checkPrevious, boolean ignoreConditions){
|
|
if(!ignoreConditions && (!block.rebuildable || (team == state.rules.defaultTeam && state.isCampaign() && !block.isVisible()))) return;
|
|
|
|
Object overrideConfig = null;
|
|
Block toAdd = this.block;
|
|
|
|
if(self() instanceof ConstructBuild entity){
|
|
//update block to reflect the fact that something was being constructed
|
|
if(entity.current != null && entity.current.synthetic() && entity.wasConstructing){
|
|
toAdd = entity.current;
|
|
overrideConfig = entity.lastConfig;
|
|
}else{
|
|
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
|
|
return;
|
|
}
|
|
}
|
|
|
|
TeamData data = team.data();
|
|
|
|
if(checkPrevious){
|
|
//remove existing blocks that have been placed here.
|
|
//painful O(n) iteration + copy
|
|
for(int i = 0; i < data.plans.size; i++){
|
|
BlockPlan b = data.plans.get(i);
|
|
if(b.x == tile.x && b.y == tile.y){
|
|
data.plans.removeIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
data.plans.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, toAdd.id, overrideConfig == null ? config() : overrideConfig));
|
|
}
|
|
|
|
public @Nullable Tile findClosestEdge(Position to, Boolf<Tile> solid){
|
|
Tile best = null;
|
|
float mindst = 0f;
|
|
for(var point : Edges.getEdges(block.size)){
|
|
Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y);
|
|
if(other != null && !solid.get(other) && (best == null || to.dst2(other) < mindst)){
|
|
best = other;
|
|
mindst = other.dst2(other);
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/** Configure with the current, local player. */
|
|
public void configure(Object value){
|
|
//save last used config
|
|
block.lastConfig = value;
|
|
Call.tileConfig(player, self(), value);
|
|
}
|
|
|
|
/** Configure from a server. */
|
|
public void configureAny(Object value){
|
|
Call.tileConfig(null, self(), value);
|
|
}
|
|
|
|
/** Deselect this tile from configuration. */
|
|
public void deselect(){
|
|
if(!headless && control.input.config.getSelected() == self()){
|
|
control.input.config.hideConfig();
|
|
}
|
|
}
|
|
|
|
/** Called clientside when the client taps a block to config.
|
|
* @return whether the configuration UI should be shown. */
|
|
public boolean configTapped(){
|
|
return true;
|
|
}
|
|
|
|
public float calculateHeat(float[] sideHeat){
|
|
return calculateHeat(sideHeat, null);
|
|
}
|
|
|
|
public float calculateHeat(float[] sideHeat, @Nullable IntSet cameFrom){
|
|
Arrays.fill(sideHeat, 0f);
|
|
if(cameFrom != null) cameFrom.clear();
|
|
|
|
float heat = 0f;
|
|
|
|
for(var build : proximity){
|
|
if(build != null && build.team == team && build instanceof HeatBlock heater){
|
|
|
|
|
|
boolean split = build.block instanceof HeatConductor cond && cond.splitHeat;
|
|
// non-routers must face us, routers must face away - next to a redirector, they're forced to face away due to cycles anyway
|
|
if(!build.block.rotate || (!split && (relativeTo(build) + 2) % 4 == build.rotation) || (split && relativeTo(build) != build.rotation)){ //TODO hacky
|
|
|
|
//if there's a cycle, ignore its heat
|
|
if(!(build instanceof HeatConductorBuild hc && hc.cameFrom.contains(id()))){
|
|
//x/y coordinate difference across point of contact
|
|
int diff = Math.min(Math.abs(build.tileX() - tileX()), Math.abs(build.tileY() - tileY()));
|
|
//number of points that this block had contact with
|
|
int contactPoints = Math.min(Math.max(build.block.size, block.size) - diff, Math.min(build.block.size, block.size));
|
|
|
|
//heat is distributed across building size
|
|
float add = heater.heat() / build.block.size * contactPoints;
|
|
if(split){
|
|
//heat routers split heat across 3 surfaces
|
|
add /= 3f;
|
|
}
|
|
|
|
sideHeat[Mathf.mod(relativeTo(build), 4)] += add;
|
|
heat += add;
|
|
}
|
|
|
|
//register traversed cycles
|
|
if(cameFrom != null){
|
|
cameFrom.add(build.id);
|
|
if(build instanceof HeatConductorBuild hc){
|
|
cameFrom.addAll(hc.cameFrom);
|
|
}
|
|
}
|
|
|
|
//massive hack but I don't really care anymore
|
|
if(heater instanceof HeatConductorBuild cond){
|
|
cond.updateHeat();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return heat;
|
|
}
|
|
|
|
public void applyBoost(float intensity, float duration){
|
|
//do not refresh time scale when getting a weaker intensity
|
|
if(intensity >= this.timeScale - 0.001f){
|
|
timeScaleDuration = Math.max(timeScaleDuration, duration);
|
|
}
|
|
timeScale = Math.max(timeScale, intensity);
|
|
}
|
|
|
|
public void applySlowdown(float intensity, float duration){
|
|
//do not refresh time scale when getting a weaker intensity
|
|
if(intensity <= this.timeScale - 0.001f){
|
|
timeScaleDuration = Math.max(timeScaleDuration, duration);
|
|
}
|
|
timeScale = Math.min(timeScale, intensity);
|
|
}
|
|
|
|
public void applyHealSuppression(float amount){
|
|
applyHealSuppression(amount, Pal.sapBullet);
|
|
}
|
|
public void applyHealSuppression(float amount, Color suppressColor){
|
|
healSuppressionTime = Math.max(healSuppressionTime, Time.time + amount);
|
|
this.suppressColor = suppressColor;
|
|
}
|
|
|
|
public boolean isHealSuppressed(){
|
|
return block.suppressable && Time.time <= healSuppressionTime;
|
|
}
|
|
|
|
public void recentlyHealed(){
|
|
lastHealTime = Time.time;
|
|
}
|
|
|
|
public boolean wasRecentlyHealed(float duration){
|
|
return lastHealTime + duration >= Time.time;
|
|
}
|
|
|
|
public boolean wasRecentlyDamaged(){
|
|
return lastDamageTime + recentDamageTime >= Time.time;
|
|
}
|
|
|
|
public Building nearby(int dx, int dy){
|
|
return world.build(tile.x + dx, tile.y + dy);
|
|
}
|
|
|
|
public Building nearby(int rotation){
|
|
return switch(rotation){
|
|
case 0 -> world.build(tile.x + 1, tile.y);
|
|
case 1 -> world.build(tile.x, tile.y + 1);
|
|
case 2 -> world.build(tile.x - 1, tile.y);
|
|
case 3 -> world.build(tile.x, tile.y - 1);
|
|
default -> null;
|
|
};
|
|
}
|
|
|
|
public byte relativeTo(Tile tile){
|
|
return relativeTo(tile.x, tile.y);
|
|
}
|
|
|
|
public byte relativeTo(Building build){
|
|
if(Math.abs(x - build.x) > Math.abs(y - build.y)){
|
|
if(x <= build.x - 1) return 0;
|
|
if(x >= build.x + 1) return 2;
|
|
}else{
|
|
if(y <= build.y - 1) return 1;
|
|
if(y >= build.y + 1) return 3;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public byte relativeToEdge(Tile other){
|
|
return relativeTo(Edges.getFacingEdge(other, tile));
|
|
}
|
|
|
|
public byte relativeTo(int cx, int cy){
|
|
return tile.absoluteRelativeTo(cx, cy);
|
|
}
|
|
|
|
/** Multiblock front. */
|
|
public @Nullable Building front(){
|
|
int trns = block.size/2 + 1;
|
|
return nearby(Geometry.d4(rotation).x * trns, Geometry.d4(rotation).y * trns);
|
|
}
|
|
|
|
/** Multiblock back. */
|
|
public @Nullable Building back(){
|
|
int trns = block.size/2 + 1;
|
|
return nearby(Geometry.d4(rotation + 2).x * trns, Geometry.d4(rotation + 2).y * trns);
|
|
}
|
|
|
|
/** Multiblock left. */
|
|
public @Nullable Building left(){
|
|
int trns = block.size/2 + 1;
|
|
return nearby(Geometry.d4(rotation + 1).x * trns, Geometry.d4(rotation + 1).y * trns);
|
|
}
|
|
|
|
/** Multiblock right. */
|
|
public @Nullable Building right(){
|
|
int trns = block.size/2 + 1;
|
|
return nearby(Geometry.d4(rotation + 3).x * trns, Geometry.d4(rotation + 3).y * trns);
|
|
}
|
|
|
|
/** Any class that overrides this method and changes the value must call Vars.fogControl.forceUpdate(team). */
|
|
public float fogRadius(){
|
|
return block.fogRadius;
|
|
}
|
|
|
|
public int pos(){
|
|
return tile.pos();
|
|
}
|
|
|
|
public float rotdeg(){
|
|
return rotation * 90;
|
|
}
|
|
|
|
/** @return preferred rotation of main texture region to be drawn */
|
|
public float drawrot(){
|
|
return block.rotate && block.rotateDraw ? rotation * 90 : 0f;
|
|
}
|
|
|
|
public Floor floor(){
|
|
return tile.floor();
|
|
}
|
|
|
|
public boolean interactable(Team team){
|
|
return state.teams.canInteract(team, team());
|
|
}
|
|
|
|
public float timeScale(){
|
|
return timeScale;
|
|
}
|
|
|
|
/**
|
|
* @return the building's 'warmup', a smooth value from 0 to 1.
|
|
* usually used for crafters and things that need to spin up before reaching full efficiency.
|
|
* many blocks will just return 0.
|
|
* */
|
|
public float warmup(){
|
|
return 0f;
|
|
}
|
|
|
|
/** @return total time this block has been producing something; non-crafter blocks usually return Time.time. */
|
|
public float totalProgress(){
|
|
return Time.time;
|
|
}
|
|
|
|
public float progress(){
|
|
return 0f;
|
|
}
|
|
|
|
/** @return whether this block is allowed to update based on team/environment */
|
|
public boolean allowUpdate(){
|
|
return team != Team.derelict && block.supportsEnv(state.rules.env) &&
|
|
//check if outside map limit
|
|
(!state.rules.limitMapArea || !state.rules.disableOutsideArea || Rect.contains(state.rules.limitX, state.rules.limitY, state.rules.limitWidth, state.rules.limitHeight, tile.x, tile.y));
|
|
}
|
|
|
|
public BlockStatus status(){
|
|
if(!enabled){
|
|
return BlockStatus.logicDisable;
|
|
}
|
|
|
|
if(!shouldConsume()){
|
|
return BlockStatus.noOutput;
|
|
}
|
|
|
|
if(efficiency <= 0 || !productionValid()){
|
|
return BlockStatus.noInput;
|
|
}
|
|
|
|
return ((state.tick / 30f) % 1f) < efficiency ? BlockStatus.active : BlockStatus.noInput;
|
|
}
|
|
|
|
/** Call when nothing is happening to the entity. This increments the internal sleep timer. */
|
|
public void sleep(){
|
|
sleepTime += Time.delta;
|
|
if(!sleeping && sleepTime >= timeToSleep){
|
|
remove();
|
|
sleeping = true;
|
|
sleepingEntities++;
|
|
}
|
|
}
|
|
|
|
/** Call when this entity is updating. This wakes it up. */
|
|
public void noSleep(){
|
|
sleepTime = 0f;
|
|
if(sleeping){
|
|
add();
|
|
sleeping = false;
|
|
sleepingEntities--;
|
|
}
|
|
}
|
|
|
|
/** Returns the version of this Building IO code.*/
|
|
public byte version(){
|
|
return 0;
|
|
}
|
|
|
|
//endregion
|
|
//region handler methods
|
|
|
|
/** @return whether the player can select (but not actually control) this building. */
|
|
public boolean canControlSelect(Unit player){
|
|
return false;
|
|
}
|
|
|
|
/** Called when a player control-selects this building - not called for ControlBlock subclasses. */
|
|
public void onControlSelect(Unit player){
|
|
|
|
}
|
|
|
|
/** Called when this building receives a position command. Requires a commandable block. */
|
|
public void onCommand(Vec2 target){
|
|
|
|
}
|
|
|
|
/** @return the position that this block points to for commands, or null. */
|
|
public @Nullable Vec2 getCommandPosition(){
|
|
return null;
|
|
}
|
|
|
|
public void handleUnitPayload(Unit unit, Cons<Payload> grabber){
|
|
Fx.spawn.at(unit);
|
|
|
|
if(unit.isPlayer()){
|
|
unit.getPlayer().clearUnit();
|
|
}
|
|
|
|
unit.remove();
|
|
|
|
//needs new ID as it is now a payload
|
|
if(net.client()){
|
|
unit.id = EntityGroup.nextId();
|
|
}else{
|
|
//server-side, this needs to be delayed until next frame because otherwise the packets sent out right after this event would have the wrong unit ID, leading to ghosts
|
|
Core.app.post(() -> unit.id = EntityGroup.nextId());
|
|
}
|
|
|
|
grabber.get(new UnitPayload(unit));
|
|
Fx.unitDrop.at(unit);
|
|
}
|
|
|
|
public boolean canWithdraw(){
|
|
return true;
|
|
}
|
|
|
|
public boolean canUnload(){
|
|
return block.unloadable;
|
|
}
|
|
|
|
public boolean canResupply(){
|
|
return block.allowResupply;
|
|
}
|
|
|
|
public boolean payloadCheck(int conveyorRotation){
|
|
return block.rotate && (rotation + 2) % 4 == conveyorRotation;
|
|
}
|
|
|
|
/** Called when an unloader takes an item. */
|
|
public void itemTaken(Item item){
|
|
|
|
}
|
|
|
|
/** Called when this block is dropped as a payload. */
|
|
public void dropped(){
|
|
|
|
}
|
|
|
|
/** This is for logic blocks. */
|
|
public void handleString(Object value){
|
|
|
|
}
|
|
|
|
public void created(){}
|
|
|
|
/** @return whether this block is currently "active" and should be consuming requirements. */
|
|
public boolean shouldConsume(){
|
|
return enabled;
|
|
}
|
|
|
|
public boolean productionValid(){
|
|
return true;
|
|
}
|
|
|
|
/** @return whether this building is currently "burning" a trigger consumer (an item) - if true, valid() on those will return true. */
|
|
public boolean consumeTriggerValid(){
|
|
return false;
|
|
}
|
|
|
|
public float getPowerProduction(){
|
|
return 0f;
|
|
}
|
|
|
|
/** Returns the amount of items this block can accept. */
|
|
public int acceptStack(Item item, int amount, Teamc source){
|
|
if(acceptItem(self(), item) && block.hasItems && (source == null || source.team() == team)){
|
|
return Math.min(getMaximumAccepted(item) - items.get(item), amount);
|
|
}else{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public int getMaximumAccepted(Item item){
|
|
return block.itemCapacity;
|
|
}
|
|
|
|
/** Remove a stack from this inventory, and return the amount removed. */
|
|
public int removeStack(Item item, int amount){
|
|
if(items == null) return 0;
|
|
amount = Math.min(amount, items.get(item));
|
|
noSleep();
|
|
items.remove(item, amount);
|
|
return amount;
|
|
}
|
|
|
|
/** Handle a stack input. */
|
|
public void handleStack(Item item, int amount, @Nullable Teamc source){
|
|
noSleep();
|
|
items.add(item, amount);
|
|
}
|
|
|
|
/** Returns offset for stack placement. */
|
|
public void getStackOffset(Item item, Vec2 trns){
|
|
|
|
}
|
|
|
|
public boolean acceptPayload(Building source, Payload payload){
|
|
return false;
|
|
}
|
|
|
|
public void handlePayload(Building source, Payload payload){
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Tries moving a payload forwards.
|
|
* @param todump payload to dump.
|
|
* @return whether the payload was moved successfully
|
|
*/
|
|
public boolean movePayload(Payload todump){
|
|
int trns = block.size/2 + 1;
|
|
Tile next = tile.nearby(Geometry.d4(rotation).x * trns, Geometry.d4(rotation).y * trns);
|
|
|
|
if(next != null && next.build != null && next.build.team == team && next.build.acceptPayload(self(), todump)){
|
|
next.build.handlePayload(self(), todump);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tries dumping a payload to any adjacent block.
|
|
* @param todump payload to dump.
|
|
* @return whether the payload was moved successfully
|
|
*/
|
|
public boolean dumpPayload(Payload todump){
|
|
if(proximity.size == 0) return false;
|
|
|
|
int dump = this.cdump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
|
|
if(other.acceptPayload(self(), todump)){
|
|
other.handlePayload(self(), todump);
|
|
incrementDump(proximity.size);
|
|
return true;
|
|
}
|
|
|
|
incrementDump(proximity.size);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void handleItem(Building source, Item item){
|
|
items.add(item, 1);
|
|
}
|
|
|
|
public boolean acceptItem(Building source, Item item){
|
|
return block.consumesItem(item) && items.get(item) < getMaximumAccepted(item);
|
|
}
|
|
|
|
public boolean acceptLiquid(Building source, Liquid liquid){
|
|
return block.hasLiquids && block.consumesLiquid(liquid);
|
|
}
|
|
|
|
public void handleLiquid(Building source, Liquid liquid, float amount){
|
|
liquids.add(liquid, amount);
|
|
}
|
|
|
|
//TODO entire liquid system is awful
|
|
public void dumpLiquid(Liquid liquid){
|
|
dumpLiquid(liquid, 2f);
|
|
}
|
|
|
|
public void dumpLiquid(Liquid liquid, float scaling){
|
|
dumpLiquid(liquid, scaling, -1);
|
|
}
|
|
|
|
/** @param outputDir output liquid direction relative to rotation, or -1 to use any direction. */
|
|
public void dumpLiquid(Liquid liquid, float scaling, int outputDir){
|
|
int dump = this.cdump;
|
|
|
|
if(liquids.get(liquid) <= 0.0001f) return;
|
|
|
|
if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) liquid.unlock();
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
if(outputDir != -1 && (outputDir + rotation) % 4 != relativeTo(other)) continue;
|
|
|
|
other = other.getLiquidDestination(self(), liquid);
|
|
|
|
if(other != null && other.block.hasLiquids && canDumpLiquid(other, liquid) && other.liquids != null){
|
|
float ofract = other.liquids.get(liquid) / other.block.liquidCapacity;
|
|
float fract = liquids.get(liquid) / block.liquidCapacity;
|
|
|
|
if(ofract < fract) transferLiquid(other, (fract - ofract) * block.liquidCapacity / scaling, liquid);
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean canDumpLiquid(Building to, Liquid liquid){
|
|
return true;
|
|
}
|
|
|
|
public void transferLiquid(Building next, float amount, Liquid liquid){
|
|
float flow = Math.min(next.block.liquidCapacity - next.liquids.get(liquid), amount);
|
|
|
|
if(next.acceptLiquid(self(), liquid)){
|
|
next.handleLiquid(self(), liquid, flow);
|
|
liquids.remove(liquid, flow);
|
|
}
|
|
}
|
|
|
|
public float moveLiquidForward(boolean leaks, Liquid liquid){
|
|
Tile next = tile.nearby(rotation);
|
|
|
|
if(next == null) return 0;
|
|
|
|
if(next.build != null){
|
|
return moveLiquid(next.build, liquid);
|
|
}else if(leaks && !next.block().solid && !next.block().hasLiquids){
|
|
float leakAmount = liquids.get(liquid) / 1.5f;
|
|
Puddles.deposit(next, tile, liquid, leakAmount, true, true);
|
|
liquids.remove(liquid, leakAmount);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public float moveLiquid(Building next, Liquid liquid){
|
|
if(next == null) return 0;
|
|
|
|
next = next.getLiquidDestination(self(), liquid);
|
|
|
|
if(next.team == team && next.block.hasLiquids && liquids.get(liquid) > 0f){
|
|
float ofract = next.liquids.get(liquid) / next.block.liquidCapacity;
|
|
float fract = liquids.get(liquid) / block.liquidCapacity * block.liquidPressure;
|
|
float flow = Math.min(Mathf.clamp((fract - ofract)) * (block.liquidCapacity), liquids.get(liquid));
|
|
flow = Math.min(flow, next.block.liquidCapacity - next.liquids.get(liquid));
|
|
|
|
if(flow > 0f && ofract <= fract && next.acceptLiquid(self(), liquid)){
|
|
next.handleLiquid(self(), liquid, flow);
|
|
liquids.remove(liquid, flow);
|
|
return flow;
|
|
//handle reactions between different liquid types ▼
|
|
}else if(!next.block.consumesLiquid(liquid) && next.liquids.currentAmount() / next.block.liquidCapacity > 0.1f && fract > 0.1f){
|
|
//TODO !IMPORTANT! uses current(), which is 1) wrong for multi-liquid blocks and 2) causes unwanted reactions, e.g. hydrogen + slag in pump
|
|
//TODO these are incorrect effect positions
|
|
float fx = (x + next.x) / 2f, fy = (y + next.y) / 2f;
|
|
|
|
Liquid other = next.liquids.current();
|
|
if(other.blockReactive && liquid.blockReactive){
|
|
//TODO liquid reaction handler for extensibility
|
|
if((other.flammability > 0.3f && liquid.temperature > 0.7f) || (liquid.flammability > 0.3f && other.temperature > 0.7f)){
|
|
damageContinuous(1);
|
|
next.damageContinuous(1);
|
|
if(Mathf.chanceDelta(0.1)){
|
|
Fx.fire.at(fx, fy);
|
|
}
|
|
}else if((liquid.temperature > 0.7f && other.temperature < 0.55f) || (other.temperature > 0.7f && liquid.temperature < 0.55f)){
|
|
liquids.remove(liquid, Math.min(liquids.get(liquid), 0.7f * Time.delta));
|
|
if(Mathf.chanceDelta(0.2f)){
|
|
Fx.steam.at(fx, fy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public Building getLiquidDestination(Building from, Liquid liquid){
|
|
return self();
|
|
}
|
|
|
|
public @Nullable Payload getPayload(){
|
|
return null;
|
|
}
|
|
|
|
/** Tries to take the payload. Returns null if no payload is present. */
|
|
public @Nullable Payload takePayload(){
|
|
return null;
|
|
}
|
|
|
|
public @Nullable PayloadSeq getPayloads(){
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Tries to put this item into a nearby container, if there are no available
|
|
* containers, it gets added to the block's inventory.
|
|
*/
|
|
public void offload(Item item){
|
|
produced(item, 1);
|
|
int dump = this.cdump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
if(other.acceptItem(self(), item) && canDump(other, item)){
|
|
other.handleItem(self(), item);
|
|
return;
|
|
}
|
|
}
|
|
|
|
handleItem(self(), item);
|
|
}
|
|
|
|
/**
|
|
* Tries to put this item into a nearby container. Returns success. Unlike #offload(), this method does not change the block inventory.
|
|
*/
|
|
public boolean put(Item item){
|
|
int dump = this.cdump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
if(other.acceptItem(self(), item) && canDump(other, item)){
|
|
other.handleItem(self(), item);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void produced(Item item){
|
|
produced(item, 1);
|
|
}
|
|
|
|
public void produced(Item item, int amount){
|
|
if(Vars.state.rules.sector != null && team == state.rules.defaultTeam){
|
|
Vars.state.rules.sector.info.handleProduction(item, amount);
|
|
|
|
if(!net.client()) item.unlock();
|
|
}
|
|
}
|
|
|
|
/** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */
|
|
public boolean dumpAccumulate(){
|
|
return dumpAccumulate(null);
|
|
}
|
|
|
|
/** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */
|
|
public boolean dumpAccumulate(Item item){
|
|
boolean res = false;
|
|
dumpAccum += delta();
|
|
while(dumpAccum >= 1f){
|
|
res |= dump(item);
|
|
dumpAccum -=1f;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/** Try dumping any item near the building. */
|
|
public boolean dump(){
|
|
return dump(null);
|
|
}
|
|
|
|
/**
|
|
* Try dumping a specific item near the building.
|
|
* @param todump Item to dump. Can be null to dump anything.
|
|
*/
|
|
public boolean dump(Item todump){
|
|
if(!block.hasItems || items.total() == 0 || proximity.size == 0 || (todump != null && !items.has(todump))) return false;
|
|
|
|
int dump = this.cdump;
|
|
var allItems = content.items();
|
|
int itemSize = allItems.size;
|
|
Object[] itemArray = allItems.items;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
|
|
if(todump == null){
|
|
|
|
for(int ii = 0; ii < itemSize; ii++){
|
|
if(!items.has(ii)) continue;
|
|
Item item = (Item)itemArray[ii];
|
|
|
|
if(other.acceptItem(self(), item) && canDump(other, item)){
|
|
other.handleItem(self(), item);
|
|
items.remove(item, 1);
|
|
incrementDump(proximity.size);
|
|
return true;
|
|
}
|
|
}
|
|
}else{
|
|
if(other.acceptItem(self(), todump) && canDump(other, todump)){
|
|
other.handleItem(self(), todump);
|
|
items.remove(todump, 1);
|
|
incrementDump(proximity.size);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
incrementDump(proximity.size);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void incrementDump(int prox){
|
|
cdump = ((cdump + 1) % prox);
|
|
}
|
|
|
|
/** Used for dumping items. */
|
|
public boolean canDump(Building to, Item item){
|
|
return true;
|
|
}
|
|
|
|
/** Try offloading an item to a nearby container in its facing direction. Returns true if success. */
|
|
public boolean moveForward(Item item){
|
|
Building other = front();
|
|
if(other != null && other.team == team && other.acceptItem(self(), item)){
|
|
other.handleItem(self(), item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Called shortly before this building is removed. */
|
|
public void onProximityRemoved(){
|
|
if(power != null){
|
|
powerGraphRemoved();
|
|
}
|
|
}
|
|
|
|
/** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */
|
|
public void onProximityAdded(){
|
|
if(power != null){
|
|
updatePowerGraph();
|
|
}
|
|
}
|
|
|
|
/** Called when anything adjacent to this building is placed/removed, including itself. */
|
|
public void onProximityUpdate(){
|
|
noSleep();
|
|
}
|
|
|
|
public void updatePowerGraph(){
|
|
for(Building other : getPowerConnections(tempBuilds)){
|
|
if(other.power != null){
|
|
other.power.graph.addGraph(power.graph);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void powerGraphRemoved(){
|
|
if(power == null) return;
|
|
|
|
power.graph.remove(self());
|
|
for(int i = 0; i < power.links.size; i++){
|
|
Tile other = world.tile(power.links.get(i));
|
|
if(other != null && other.build != null && other.build.power != null){
|
|
other.build.power.links.removeValue(pos());
|
|
}
|
|
}
|
|
power.links.clear();
|
|
}
|
|
|
|
public boolean conductsTo(Building other){
|
|
return !block.insulated;
|
|
}
|
|
|
|
public Seq<Building> getPowerConnections(Seq<Building> out){
|
|
out.clear();
|
|
if(power == null) return out;
|
|
|
|
for(Building other : proximity){
|
|
if(other != null && other.power != null
|
|
&& other.team == team
|
|
&& !(block.consumesPower && other.block.consumesPower && !block.outputsPower && !other.block.outputsPower && !block.conductivePower && !other.block.conductivePower)
|
|
&& conductsTo(other) && other.conductsTo(self()) && !power.links.contains(other.pos())){
|
|
out.add(other);
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < power.links.size; i++){
|
|
Tile link = world.tile(power.links.get(i));
|
|
if(link != null && link.build != null && link.build.power != null && link.build.team == team) out.add(link.build);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
public float getProgressIncrease(float baseTime){
|
|
return 1f / baseTime * edelta();
|
|
}
|
|
|
|
public float getDisplayEfficiency(){
|
|
return getProgressIncrease(1f) / edelta();
|
|
}
|
|
|
|
/** @return whether this block should play its active sound.*/
|
|
public boolean shouldActiveSound(){
|
|
return false;
|
|
}
|
|
|
|
/** @return volume cale of active sound. */
|
|
public float activeSoundVolume(){
|
|
return 1f;
|
|
}
|
|
|
|
/** @return whether this block should play its idle sound.*/
|
|
public boolean shouldAmbientSound(){
|
|
return shouldConsume();
|
|
}
|
|
|
|
public void drawStatus(){
|
|
if(block.enableDrawStatus && block.consumers.length > 0){
|
|
float multiplier = block.size > 1 ? 1 : 0.64f;
|
|
float brcx = x + (block.size * tilesize / 2f) - (tilesize * multiplier / 2f);
|
|
float brcy = y - (block.size * tilesize / 2f) + (tilesize * multiplier / 2f);
|
|
|
|
Draw.z(Layer.power + 1);
|
|
Draw.color(Pal.gray);
|
|
Fill.square(brcx, brcy, 2.5f * multiplier, 45);
|
|
Draw.color(status().color);
|
|
Fill.square(brcx, brcy, 1.5f * multiplier, 45);
|
|
Draw.color();
|
|
}
|
|
}
|
|
|
|
public void drawCracks(){
|
|
if(!block.drawCracks || !damaged() || block.size > BlockRenderer.maxCrackSize) return;
|
|
int id = pos();
|
|
TextureRegion region = renderer.blocks.cracks[block.size - 1][Mathf.clamp((int)((1f - healthf()) * BlockRenderer.crackRegions), 0, BlockRenderer.crackRegions-1)];
|
|
Draw.colorl(0.2f, 0.1f + (1f - healthf())* 0.6f);
|
|
//TODO could be random, flipped, pseudorandom, etc
|
|
Draw.rect(region, x, y, (id%4)*90);
|
|
Draw.color();
|
|
}
|
|
|
|
/** Draw the block overlay that is shown when a cursor is over the block. */
|
|
public void drawSelect(){
|
|
block.drawOverlay(x, y, rotation);
|
|
}
|
|
|
|
public void drawDisabled(){
|
|
Draw.color(Color.scarlet);
|
|
Draw.alpha(0.8f);
|
|
|
|
float size = 6f;
|
|
Draw.rect(Icon.cancel.getRegion(), x, y, size, size);
|
|
|
|
Draw.reset();
|
|
}
|
|
|
|
public void draw(){
|
|
if(block.variants == 0 || block.variantRegions == null){
|
|
Draw.rect(block.region, x, y, drawrot());
|
|
}else{
|
|
Draw.rect(block.variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, block.variantRegions.length - 1))], x, y, drawrot());
|
|
}
|
|
|
|
drawTeamTop();
|
|
}
|
|
|
|
public void payloadDraw(){
|
|
draw();
|
|
}
|
|
|
|
public void drawTeamTop(){
|
|
if(block.teamRegion.found()){
|
|
if(block.teamRegions[team.id] == block.teamRegion) Draw.color(team.color);
|
|
Draw.rect(block.teamRegions[team.id], x, y);
|
|
Draw.color();
|
|
}
|
|
}
|
|
|
|
public void drawLight(){
|
|
Liquid liq = block.hasLiquids && block.lightLiquid == null ? liquids.current() : block.lightLiquid;
|
|
if(block.hasLiquids && block.drawLiquidLight && liq.lightColor.a > 0.001f){
|
|
//yes, I am updating in draw()... but this is purely visual anyway, better have it here than in update() where it wastes time
|
|
visualLiquid = Mathf.lerpDelta(visualLiquid, liquids.get(liq)>= 0.01f ? 1f : 0f, 0.06f);
|
|
drawLiquidLight(liq, visualLiquid);
|
|
}
|
|
}
|
|
|
|
public void drawLiquidLight(Liquid liquid, float amount){
|
|
if(amount > 0.01f){
|
|
Color color = liquid.lightColor;
|
|
float fract = 1f;
|
|
float opacity = color.a * fract;
|
|
if(opacity > 0.001f){
|
|
Drawf.light(x, y, block.size * 30f * fract, color, opacity * amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void drawTeam(){
|
|
Draw.color(team.color);
|
|
Draw.rect("block-border", x - block.size * tilesize / 2f + 4, y - block.size * tilesize / 2f + 4);
|
|
Draw.color();
|
|
}
|
|
|
|
/** @return whether a building has regen/healing suppressed; if so, spawns particles on it. */
|
|
public boolean checkSuppression(){
|
|
if(isHealSuppressed()){
|
|
if(Mathf.chanceDelta(0.03)){
|
|
Fx.regenSuppressParticle.at(x + Mathf.range(block.size * tilesize/2f - 1f), y + Mathf.range(block.size * tilesize/2f - 1f), suppressColor);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Called after the block is placed by this client. */
|
|
@CallSuper
|
|
public void playerPlaced(Object config){
|
|
|
|
}
|
|
|
|
/** Called after the block is placed by anyone. */
|
|
@CallSuper
|
|
public void placed(){
|
|
if(net.client()) return;
|
|
|
|
if((block.consumesPower || block.outputsPower) && block.hasPower && block.connectedPower){
|
|
PowerNode.getNodeLinks(tile, block, team, other -> {
|
|
if(!other.power.links.contains(pos())){
|
|
other.configureAny(pos());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** @return whether this building is in a payload */
|
|
public boolean isPayload(){
|
|
return tile == emptyTile;
|
|
}
|
|
|
|
/**
|
|
* Called when a block is placed over some other blocks. This seq will always have at least one item.
|
|
* Should load some previous state, if necessary. */
|
|
public void overwrote(Seq<Building> previous){
|
|
|
|
}
|
|
|
|
public void onRemoved(){
|
|
}
|
|
|
|
/** Called every frame a unit is on this */
|
|
public void unitOn(Unit unit){
|
|
}
|
|
|
|
/** Called when a unit that spawned at this tile is removed. */
|
|
public void unitRemoved(Unit unit){
|
|
}
|
|
|
|
/** Called when arbitrary configuration is applied to a tile. */
|
|
public void configured(@Nullable Unit builder, @Nullable Object value){
|
|
//null is of type void.class; anonymous classes use their superclass.
|
|
Class<?> type = value == null ? void.class : value.getClass().isAnonymousClass() ? value.getClass().getSuperclass() : value.getClass();
|
|
|
|
if(value instanceof Item) type = Item.class;
|
|
if(value instanceof Block) type = Block.class;
|
|
if(value instanceof Liquid) type = Liquid.class;
|
|
if(value instanceof UnitType) type = UnitType.class;
|
|
|
|
if(builder != null && builder.isPlayer()){
|
|
lastAccessed = builder.getPlayer().coloredName();
|
|
}
|
|
|
|
if(block.configurations.containsKey(type)){
|
|
block.configurations.get(type).get(this, value);
|
|
}else if(value instanceof Building build){
|
|
//copy config of another building
|
|
var conf = build.config();
|
|
if(conf != null && !(conf instanceof Building)){
|
|
configured(builder, conf);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called when the block is tapped by the local player. */
|
|
public void tapped(){
|
|
|
|
}
|
|
|
|
/** Called *after* the tile has been removed. */
|
|
public void afterDestroyed(){
|
|
if(block.destroyBullet != null){
|
|
//I really do not like that the bullet will not destroy derelict
|
|
//but I can't do anything about it without using a random team
|
|
//which may or may not cause issues with servers and js
|
|
block.destroyBullet.create(this, block.destroyBulletSameTeam ? team : Team.derelict, x, y, Mathf.randomSeed(id(), 360f));
|
|
}
|
|
}
|
|
|
|
/** @return the cap for item amount calculations, used when this block explodes. */
|
|
public int explosionItemCap(){
|
|
return block.itemCapacity;
|
|
}
|
|
|
|
/** Called when the block is destroyed. The tile is still intact at this stage. */
|
|
public void onDestroyed(){
|
|
float explosiveness = block.baseExplosiveness;
|
|
float flammability = 0f;
|
|
float power = 0f;
|
|
|
|
if(block.hasItems){
|
|
for(Item item : content.items()){
|
|
int amount = Math.min(items.get(item), explosionItemCap());
|
|
explosiveness += item.explosiveness * amount;
|
|
flammability += item.flammability * amount;
|
|
power += item.charge * Mathf.pow(amount, 1.1f) * 150f;
|
|
}
|
|
}
|
|
|
|
if(block.hasLiquids){
|
|
flammability += liquids.sum((liquid, amount) -> liquid.flammability * amount / 2f);
|
|
explosiveness += liquids.sum((liquid, amount) -> liquid.explosiveness * amount / 2f);
|
|
}
|
|
|
|
if(block.consPower != null && block.consPower.buffered){
|
|
power += this.power.status * block.consPower.capacity;
|
|
}
|
|
|
|
if(block.hasLiquids && state.rules.damageExplosions){
|
|
|
|
liquids.each((liquid, amount) -> {
|
|
float splash = Mathf.clamp(amount / 4f, 0f, 10f);
|
|
|
|
for(int i = 0; i < Mathf.clamp(amount / 5, 0, 30); i++){
|
|
Time.run(i / 2f, () -> {
|
|
Tile other = world.tileWorld(x + Mathf.range(block.size * tilesize / 2), y + Mathf.range(block.size * tilesize / 2));
|
|
if(other != null){
|
|
Puddles.deposit(other, liquid, splash);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//cap explosiveness so fluid tanks/vaults don't instakill units
|
|
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect);
|
|
|
|
if(block.createRubble && !floor().solid && !floor().isLiquid){
|
|
Effect.rubble(x, y, block.size);
|
|
}
|
|
}
|
|
|
|
public String getDisplayName(){
|
|
//derelict team icon currently doesn't display
|
|
return team == Team.derelict ?
|
|
block.localizedName + "\n" + Core.bundle.get("block.derelict") :
|
|
block.localizedName + (team == player.team() || team.emoji.isEmpty() ? "" : " " + team.emoji);
|
|
}
|
|
|
|
public TextureRegion getDisplayIcon(){
|
|
return block.uiIcon;
|
|
}
|
|
|
|
/** @return the item module to use for flow rate calculations */
|
|
public ItemModule flowItems(){
|
|
return items;
|
|
}
|
|
@Override
|
|
public void display(Table table){
|
|
//display the block stuff
|
|
//TODO duplicated code?
|
|
table.table(t -> {
|
|
t.left();
|
|
t.add(new Image(block.getDisplayIcon(tile))).size(8 * 4);
|
|
t.labelWrap(block.getDisplayName(tile)).left().width(190f).padLeft(5);
|
|
}).growX().left();
|
|
|
|
table.row();
|
|
|
|
//only display everything else if the team is the same
|
|
if(team == player.team()){
|
|
table.table(bars -> {
|
|
bars.defaults().growX().height(18f).pad(4);
|
|
|
|
displayBars(bars);
|
|
}).growX();
|
|
table.row();
|
|
table.table(this::displayConsumption).growX();
|
|
|
|
boolean displayFlow = (block.category == Category.distribution || block.category == Category.liquid) && block.displayFlow;
|
|
|
|
if(displayFlow){
|
|
String ps = " " + StatUnit.perSecond.localized();
|
|
|
|
var flowItems = flowItems();
|
|
|
|
if(flowItems != null){
|
|
table.row();
|
|
table.left();
|
|
table.table(l -> {
|
|
Bits current = new Bits();
|
|
|
|
Runnable rebuild = () -> {
|
|
l.clearChildren();
|
|
l.left();
|
|
for(Item item : content.items()){
|
|
if(flowItems.hasFlowItem(item)){
|
|
l.image(item.uiIcon).scaling(Scaling.fit).padRight(3f);
|
|
l.label(() -> flowItems.getFlowRate(item) < 0 ? "..." : Strings.fixed(flowItems.getFlowRate(item), 1) + ps).color(Color.lightGray);
|
|
l.row();
|
|
}
|
|
}
|
|
};
|
|
|
|
rebuild.run();
|
|
l.update(() -> {
|
|
for(Item item : content.items()){
|
|
if(flowItems.hasFlowItem(item) && !current.get(item.id)){
|
|
current.set(item.id);
|
|
rebuild.run();
|
|
}
|
|
}
|
|
});
|
|
}).left();
|
|
}
|
|
|
|
if(liquids != null){
|
|
table.row();
|
|
table.left();
|
|
table.table(l -> {
|
|
Bits current = new Bits();
|
|
|
|
Runnable rebuild = () -> {
|
|
l.clearChildren();
|
|
l.left();
|
|
for(var liquid : content.liquids()){
|
|
if(liquids.hasFlowLiquid(liquid)){
|
|
l.image(liquid.uiIcon).scaling(Scaling.fit).size(32f).padRight(3f);
|
|
l.label(() -> liquids.getFlowRate(liquid) < 0 ? "..." : Strings.fixed(liquids.getFlowRate(liquid), 1) + ps).color(Color.lightGray);
|
|
l.row();
|
|
}
|
|
}
|
|
};
|
|
|
|
rebuild.run();
|
|
l.update(() -> {
|
|
for(var liquid : content.liquids()){
|
|
if(liquids.hasFlowLiquid(liquid) && !current.get(liquid.id)){
|
|
current.set(liquid.id);
|
|
rebuild.run();
|
|
}
|
|
}
|
|
});
|
|
}).left();
|
|
}
|
|
}
|
|
|
|
if(net.active() && lastAccessed != null){
|
|
table.row();
|
|
table.add(Core.bundle.format("lastaccessed", lastAccessed)).growX().wrap().left();
|
|
}
|
|
|
|
table.marginBottom(-5);
|
|
}
|
|
}
|
|
|
|
public void displayConsumption(Table table){
|
|
table.left();
|
|
for(Consume cons : block.consumers){
|
|
if(cons.optional && cons.booster) continue;
|
|
cons.build(self(), table);
|
|
}
|
|
}
|
|
|
|
public void displayBars(Table table){
|
|
for(Func<Building, Bar> bar : block.listBars()){
|
|
var result = bar.get(self());
|
|
if(result == null) continue;
|
|
table.add(result).growX();
|
|
table.row();
|
|
}
|
|
}
|
|
|
|
/** Called when this block is tapped to build a UI on the table.
|
|
* configurable must be true for this to be called.*/
|
|
public void buildConfiguration(Table table){
|
|
}
|
|
|
|
/** Update table alignment after configuring.*/
|
|
public void updateTableAlign(Table table){
|
|
Vec2 pos = Core.input.mouseScreen(x, y - block.size * tilesize / 2f - 1);
|
|
table.setPosition(pos.x, pos.y, Align.top);
|
|
}
|
|
|
|
/** Returns whether a hand cursor should be shown over this block. */
|
|
public Cursor getCursor(){
|
|
return block.configurable && interactable(player.team()) ? SystemCursor.hand : SystemCursor.arrow;
|
|
}
|
|
|
|
/**
|
|
* Called when another tile is tapped while this building is selected.
|
|
* @return whether this block should be deselected.
|
|
*/
|
|
public boolean onConfigureBuildTapped(Building other){
|
|
if(block.clearOnDoubleTap){
|
|
if(self() == other){
|
|
deselect();
|
|
configure(null);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return self() != other;
|
|
}
|
|
|
|
/**
|
|
* Called when a position is tapped while this building is selected.
|
|
*
|
|
* @return whether the tap event is consumed - if true, the player will not start shooting or interact with things under the cursor.
|
|
* */
|
|
public boolean onConfigureTapped(float x, float y){
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called when this block's config menu is closed.
|
|
*/
|
|
public void onConfigureClosed(){}
|
|
|
|
/** Returns whether this config menu should show when the specified player taps it. */
|
|
public boolean shouldShowConfigure(Player player){
|
|
return true;
|
|
}
|
|
|
|
/** Whether this configuration should be hidden now. Called every frame the config is open. */
|
|
public boolean shouldHideConfigure(Player player){
|
|
return false;
|
|
}
|
|
|
|
public void drawConfigure(){
|
|
Draw.color(Pal.accent);
|
|
Lines.stroke(1f);
|
|
Lines.square(x, y, block.size * tilesize / 2f + 1f);
|
|
Draw.reset();
|
|
}
|
|
|
|
public boolean checkSolid(){
|
|
return false;
|
|
}
|
|
|
|
public float handleDamage(float amount){
|
|
return amount;
|
|
}
|
|
|
|
public boolean absorbLasers(){
|
|
return block.absorbLasers;
|
|
}
|
|
|
|
public boolean isInsulated(){
|
|
return block.insulated;
|
|
}
|
|
|
|
public boolean collide(Bullet other){
|
|
return true;
|
|
}
|
|
|
|
/** Handle a bullet collision.
|
|
* @return whether the bullet should be removed. */
|
|
public boolean collision(Bullet other){
|
|
boolean wasDead = health <= 0;
|
|
|
|
float damage = other.damage() * other.type().buildingDamageMultiplier;
|
|
if(!other.type.pierceArmor){
|
|
damage = Damage.applyArmor(damage, block.armor);
|
|
}
|
|
|
|
damage(other.team, damage);
|
|
Events.fire(bulletDamageEvent.set(self(), other));
|
|
|
|
if(health <= 0 && !wasDead){
|
|
Events.fire(new BuildingBulletDestroyEvent(self(), other));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Used to handle damage from splash damage for certain types of blocks. */
|
|
public void damage(@Nullable Team source, float damage){
|
|
damage(damage);
|
|
}
|
|
|
|
/** Handles splash damage with a bullet source. */
|
|
public void damage(Bullet bullet, Team source, float damage){
|
|
damage(source, damage);
|
|
Events.fire(bulletDamageEvent.set(self(), bullet));
|
|
}
|
|
|
|
/** Changes this building's team in a safe manner. */
|
|
public void changeTeam(Team next){
|
|
if(this.team == next) return;
|
|
|
|
Team last = this.team;
|
|
boolean was = isValid();
|
|
|
|
if(was) indexer.removeIndex(tile);
|
|
|
|
this.team = next;
|
|
|
|
if(was){
|
|
indexer.addIndex(tile);
|
|
Events.fire(teamChangeEvent.set(last, self()));
|
|
}
|
|
}
|
|
|
|
public boolean canPickup(){
|
|
return true;
|
|
}
|
|
|
|
/** Called right before this building is picked up. */
|
|
public void pickedUp(){
|
|
|
|
}
|
|
|
|
/** Called right after this building is picked up. */
|
|
public void afterPickedUp(){
|
|
if(power != null){
|
|
//TODO can lead to ghost graphs?
|
|
power.graph = new PowerGraph();
|
|
power.links.clear();
|
|
if(block.consPower != null && !block.consPower.buffered){
|
|
power.status = 0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void removeFromProximity(){
|
|
onProximityRemoved();
|
|
tmpTiles.clear();
|
|
|
|
Point2[] nearby = Edges.getEdges(block.size);
|
|
for(Point2 point : nearby){
|
|
Building other = world.build(tile.x + point.x, tile.y + point.y);
|
|
//remove this tile from all nearby tile's proximities
|
|
if(other != null){
|
|
tmpTiles.add(other);
|
|
}
|
|
}
|
|
|
|
for(Building other : tmpTiles){
|
|
other.proximity.remove(self(), true);
|
|
other.onProximityUpdate();
|
|
}
|
|
proximity.clear();
|
|
}
|
|
|
|
public void updateProximity(){
|
|
tmpTiles.clear();
|
|
proximity.clear();
|
|
|
|
Point2[] nearby = Edges.getEdges(block.size);
|
|
for(Point2 point : nearby){
|
|
Building other = world.build(tile.x + point.x, tile.y + point.y);
|
|
|
|
if(other == null || other.team != team) continue;
|
|
|
|
other.proximity.addUnique(self());
|
|
|
|
tmpTiles.add(other);
|
|
}
|
|
|
|
//using a set to prevent duplicates
|
|
for(Building tile : tmpTiles){
|
|
proximity.add(tile);
|
|
}
|
|
|
|
onProximityAdded();
|
|
onProximityUpdate();
|
|
|
|
for(Building other : tmpTiles){
|
|
other.onProximityUpdate();
|
|
}
|
|
}
|
|
|
|
public void consume(){
|
|
for(Consume cons : block.consumers){
|
|
cons.trigger(self());
|
|
}
|
|
}
|
|
|
|
public boolean canConsume(){
|
|
return potentialEfficiency > 0;
|
|
}
|
|
|
|
/** Scaled delta. */
|
|
public float delta(){
|
|
return Time.delta * timeScale;
|
|
}
|
|
|
|
/** Efficiency * delta. */
|
|
public float edelta(){
|
|
return efficiency * delta();
|
|
}
|
|
|
|
/** Called after efficiency is updated but before consumers are updated. Use to apply your own multiplier. */
|
|
public void updateEfficiencyMultiplier(){
|
|
float scale = efficiencyScale();
|
|
efficiency *= scale;
|
|
optionalEfficiency *= scale;
|
|
}
|
|
|
|
/** Calculate your own efficiency multiplier. By default, this is applied in updateEfficiencyMultiplier. */
|
|
public float efficiencyScale(){
|
|
return 1f;
|
|
}
|
|
|
|
public void updateConsumption(){
|
|
//everything is valid when cheating
|
|
if(!block.hasConsumers || cheating()){
|
|
potentialEfficiency = enabled && productionValid() ? 1f : 0f;
|
|
efficiency = optionalEfficiency = shouldConsume() ? potentialEfficiency : 0f;
|
|
updateEfficiencyMultiplier();
|
|
return;
|
|
}
|
|
|
|
//disabled -> nothing works
|
|
if(!enabled){
|
|
potentialEfficiency = efficiency = optionalEfficiency = 0f;
|
|
return;
|
|
}
|
|
|
|
boolean update = shouldConsume() && productionValid();
|
|
|
|
float minEfficiency = 1f;
|
|
|
|
//assume efficiency is 1 for the calculations below
|
|
efficiency = optionalEfficiency = 1f;
|
|
|
|
//first pass: get the minimum efficiency of any consumer
|
|
for(var cons : block.nonOptionalConsumers){
|
|
minEfficiency = Math.min(minEfficiency, cons.efficiency(self()));
|
|
}
|
|
|
|
//same for optionals
|
|
for(var cons : block.optionalConsumers){
|
|
optionalEfficiency = Math.min(optionalEfficiency, cons.efficiency(self()));
|
|
}
|
|
|
|
//efficiency is now this minimum value
|
|
efficiency = minEfficiency;
|
|
optionalEfficiency = Math.min(optionalEfficiency, minEfficiency);
|
|
|
|
//assign "potential"
|
|
potentialEfficiency = efficiency;
|
|
|
|
//no updating means zero efficiency
|
|
if(!update){
|
|
efficiency = optionalEfficiency = 0f;
|
|
}
|
|
|
|
updateEfficiencyMultiplier();
|
|
|
|
//second pass: update every consumer based on efficiency
|
|
if(update && efficiency > 0){
|
|
for(var cons : block.updateConsumers){
|
|
cons.update(self());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void updatePayload(@Nullable Unit unitHolder, @Nullable Building buildingHolder){
|
|
update();
|
|
}
|
|
|
|
public void updateTile(){
|
|
|
|
}
|
|
|
|
/** @return ambient sound volume scale. */
|
|
public float ambientVolume(){
|
|
return efficiency;
|
|
}
|
|
|
|
//endregion
|
|
//region overrides
|
|
|
|
/** Tile configuration. Defaults to null. Used for block rebuilding. */
|
|
@Nullable
|
|
public Object config(){
|
|
return null;
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public boolean isValid(){
|
|
return tile.build == self() && !dead();
|
|
}
|
|
|
|
@MethodPriority(100)
|
|
@Override
|
|
public void heal(){
|
|
healthChanged();
|
|
}
|
|
|
|
@MethodPriority(100)
|
|
@Override
|
|
public void heal(float amount){
|
|
healthChanged();
|
|
}
|
|
|
|
@Override
|
|
public float hitSize(){
|
|
return tile.block().size * tilesize;
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public void kill(){
|
|
Call.buildDestroyed(self());
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public void damage(float damage){
|
|
if(dead()) return;
|
|
|
|
float dm = state.rules.blockHealth(team);
|
|
lastDamageTime = Time.time;
|
|
|
|
if(Mathf.zero(dm)){
|
|
damage = health + 1;
|
|
}else{
|
|
damage /= dm;
|
|
}
|
|
|
|
//TODO handle this better on the client.
|
|
if(!net.client()){
|
|
health -= handleDamage(damage);
|
|
}
|
|
|
|
healthChanged();
|
|
|
|
if(health <= 0){
|
|
Call.buildDestroyed(self());
|
|
}
|
|
}
|
|
|
|
public void healthChanged(){
|
|
//server-side, health updates are batched.
|
|
if(net.server()){
|
|
netServer.buildHealthUpdate(self());
|
|
}
|
|
|
|
indexer.notifyHealthChanged(self());
|
|
}
|
|
|
|
@Override
|
|
public double sense(LAccess sensor){
|
|
return switch(sensor){
|
|
case x -> World.conv(x);
|
|
case y -> World.conv(y);
|
|
case color -> Color.toDoubleBits(team.color.r, team.color.g, team.color.b, 1f);
|
|
case dead -> !isValid() ? 1 : 0;
|
|
case team -> team.id;
|
|
case health -> health;
|
|
case maxHealth -> maxHealth;
|
|
case efficiency -> efficiency;
|
|
case timescale -> timeScale;
|
|
case range -> this instanceof Ranged r ? r.range() / tilesize : 0;
|
|
case rotation -> rotation;
|
|
case totalItems -> items == null ? 0 : items.total();
|
|
//totalLiquids is inherently bad design, but unfortunately it is useful for conduits/tanks
|
|
case totalLiquids -> liquids == null ? 0 : liquids.currentAmount();
|
|
case totalPower -> power == null || block.consPower == null ? 0 : power.status * (block.consPower.buffered ? block.consPower.capacity : 1f);
|
|
case itemCapacity -> block.hasItems ? block.itemCapacity : 0;
|
|
case liquidCapacity -> block.hasLiquids ? block.liquidCapacity : 0;
|
|
case powerCapacity -> block.consPower != null ? block.consPower.capacity : 0f;
|
|
case powerNetIn -> power == null ? 0 : power.graph.getLastScaledPowerIn() * 60;
|
|
case powerNetOut -> power == null ? 0 : power.graph.getLastScaledPowerOut() * 60;
|
|
case powerNetStored -> power == null ? 0 : power.graph.getLastPowerStored();
|
|
case powerNetCapacity -> power == null ? 0 : power.graph.getLastCapacity();
|
|
case enabled -> enabled ? 1 : 0;
|
|
case controlled -> this instanceof ControlBlock c && c.isControlled() ? GlobalVars.ctrlPlayer : 0;
|
|
case payloadCount -> getPayload() != null ? 1 : 0;
|
|
case size -> block.size;
|
|
case cameraX, cameraY, cameraWidth, cameraHeight -> this instanceof ControlBlock c ? c.unit().sense(sensor) : 0;
|
|
default -> Float.NaN; //gets converted to null in logic
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public Object senseObject(LAccess sensor){
|
|
return switch(sensor){
|
|
case type -> block;
|
|
case firstItem -> items == null ? null : items.first();
|
|
case config -> block.configSenseable() ? config() : null;
|
|
case payloadType -> getPayload() instanceof UnitPayload p1 ? p1.unit.type : getPayload() instanceof BuildPayload p2 ? p2.block() : null;
|
|
default -> noSensed;
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public double sense(Content content){
|
|
if(content instanceof Item i && items != null) return items.get(i);
|
|
if(content instanceof Liquid l && liquids != null) return liquids.get(l);
|
|
return Float.NaN; //invalid sense
|
|
}
|
|
|
|
@Override
|
|
public void control(LAccess type, double p1, double p2, double p3, double p4){
|
|
if(type == LAccess.enabled){
|
|
enabled = !Mathf.zero((float)p1);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void control(LAccess type, Object p1, double p2, double p3, double p4){
|
|
//don't execute configure instructions that copy logic building configures; this can cause extreme lag
|
|
if(type == LAccess.config && block.logicConfigurable && !(p1 instanceof LogicBuild)){
|
|
//change config only if it's new
|
|
configured(null, p1);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setProp(LAccess prop, double value){
|
|
switch(prop){
|
|
case health -> {
|
|
health = (float)Mathf.clamp(value, 0, maxHealth);
|
|
healthChanged();
|
|
if(health <= 0f && !dead()){
|
|
Call.buildDestroyed(self());
|
|
}
|
|
}
|
|
case team -> {
|
|
Team team = Team.get((int)value);
|
|
if(this.team != team){
|
|
changeTeam(team);
|
|
}
|
|
}
|
|
case totalPower -> {
|
|
if(power != null && block.consPower != null && block.consPower.buffered){
|
|
power.status = Mathf.clamp((float)(value / block.consPower.capacity));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setProp(LAccess prop, Object value){
|
|
switch(prop){
|
|
case team -> {
|
|
if(value instanceof Team team && this.team != team){
|
|
changeTeam(team);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setProp(UnlockableContent content, double value){
|
|
if(content instanceof Item item && items != null){
|
|
int amount = (int)value;
|
|
if(items.get(item) != amount){
|
|
if(items.get(item) < amount){
|
|
handleStack(item, acceptStack(item, amount - items.get(item), null), null);
|
|
}else if(amount >= 0){
|
|
removeStack(item, items.get(item) - amount);
|
|
}
|
|
}
|
|
}else if(content instanceof Liquid liquid && liquids != null){
|
|
float amount = Mathf.clamp((float)value, 0f, block.liquidCapacity);
|
|
//decreasing amount is always allowed
|
|
if(amount < liquids.get(liquid) || (acceptLiquid(self(), liquid) && (liquids.current() == liquid || liquids.currentAmount() <= 0.1f || block.consumesLiquid(liquid)))){
|
|
liquids.set(liquid, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public boolean inFogTo(Team viewer){
|
|
if(team == viewer || !state.rules.fog) return false;
|
|
|
|
int size = block.size, of = block.sizeOffset, tx = tile.x, ty = tile.y;
|
|
|
|
if(!isDiscovered(viewer)) return true;
|
|
|
|
for(int x = 0; x < size; x++){
|
|
for(int y = 0; y < size; y++){
|
|
if(fogControl.isVisibleTile(viewer, tx + x + of, ty + y + of)){
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void remove(){
|
|
if(sound != null){
|
|
sound.stop();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void killed(){
|
|
Events.fire(new BlockDestroyEvent(tile));
|
|
block.destroySound.at(tile);
|
|
onDestroyed();
|
|
if(tile != emptyTile){
|
|
tile.remove();
|
|
}
|
|
remove();
|
|
afterDestroyed();
|
|
}
|
|
|
|
@Final
|
|
@Replace
|
|
@Override
|
|
public void update(){
|
|
//TODO should just avoid updating buildings instead
|
|
if(state.isEditor()) return;
|
|
|
|
//TODO refactor to timestamp-based system?
|
|
if((timeScaleDuration -= Time.delta) <= 0f || !block.canOverdrive){
|
|
timeScale = 1f;
|
|
}
|
|
|
|
if(!allowUpdate()){
|
|
enabled = false;
|
|
}
|
|
|
|
if(!headless && !wasVisible && state.rules.fog && !inFogTo(player.team())){
|
|
visibleFlags |= (1L << player.team().id);
|
|
wasVisible = true;
|
|
renderer.blocks.updateShadow(self());
|
|
renderer.minimap.update(tile);
|
|
}
|
|
|
|
//TODO separate system for sound? AudioSource, etc
|
|
if(!headless){
|
|
if(sound != null){
|
|
sound.update(x, y, shouldActiveSound(), activeSoundVolume());
|
|
}
|
|
|
|
if(block.ambientSound != Sounds.none && shouldAmbientSound()){
|
|
control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
|
|
}
|
|
}
|
|
|
|
updateConsumption();
|
|
|
|
//TODO just handle per-block instead
|
|
if(enabled || !block.noUpdateDisabled){
|
|
updateTile();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void hitbox(Rect out){
|
|
out.setCentered(x, y, block.size * tilesize, block.size * tilesize);
|
|
}
|
|
|
|
@Override
|
|
@Replace
|
|
public String toString(){
|
|
return "Building#" + id() + "[" + tileX() + "," + tileY() + "]:" + block;
|
|
}
|
|
|
|
//endregion
|
|
}
|