1303 lines
39 KiB
Java
1303 lines
39 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.ArcAnnotate.*;
|
|
import arc.util.io.*;
|
|
import mindustry.annotations.Annotations.*;
|
|
import mindustry.audio.*;
|
|
import mindustry.content.*;
|
|
import mindustry.ctype.*;
|
|
import mindustry.entities.*;
|
|
import mindustry.game.EventType.*;
|
|
import mindustry.game.*;
|
|
import mindustry.gen.*;
|
|
import mindustry.graphics.*;
|
|
import mindustry.logic.*;
|
|
import mindustry.type.*;
|
|
import mindustry.ui.*;
|
|
import mindustry.world.*;
|
|
import mindustry.world.blocks.environment.*;
|
|
import mindustry.world.blocks.payloads.*;
|
|
import mindustry.world.blocks.power.*;
|
|
import mindustry.world.consumers.*;
|
|
import mindustry.world.meta.*;
|
|
import mindustry.world.modules.*;
|
|
|
|
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, Senseable, Controllable{
|
|
//region vars and initialization
|
|
static final float timeToSleep = 60f * 1, timeToUncontrol = 60f * 6;
|
|
static final ObjectSet<Building> tmpTiles = new ObjectSet<>();
|
|
static final Seq<Building> tempTileEnts = new Seq<>();
|
|
static final Seq<Tile> tempTiles = new Seq<>();
|
|
static int sleepingEntities = 0;
|
|
|
|
@Import float x, y, health;
|
|
@Import Team team;
|
|
|
|
transient Tile tile;
|
|
transient Block block;
|
|
transient Seq<Building> proximity = new Seq<>(8);
|
|
transient boolean updateFlow;
|
|
transient byte dump;
|
|
transient int rotation;
|
|
transient boolean enabled = true;
|
|
transient float enabledControlTime;
|
|
|
|
PowerModule power;
|
|
ItemModule items;
|
|
LiquidModule liquids;
|
|
ConsumeModule cons;
|
|
|
|
private transient float timeScale = 1f, timeScaleDuration;
|
|
|
|
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);
|
|
}
|
|
this.rotation = rotation;
|
|
this.tile = tile;
|
|
|
|
set(tile.drawx(), tile.drawy());
|
|
|
|
if(shouldAdd){
|
|
add();
|
|
}
|
|
|
|
created();
|
|
|
|
return base();
|
|
}
|
|
|
|
/** Sets up all the necessary variables, but does not add this entity anywhere. */
|
|
public Building create(Block block, Team team){
|
|
this.tile = emptyTile;
|
|
this.block = block;
|
|
this.team = team;
|
|
|
|
if(block.activeSound != Sounds.none){
|
|
sound = new SoundLoop(block.activeSound, block.activeSoundVolume);
|
|
}
|
|
|
|
health = block.health;
|
|
maxHealth(block.health);
|
|
timer(new Interval(block.timers));
|
|
|
|
cons = new ConsumeModule(base());
|
|
if(block.hasItems) items = new ItemModule();
|
|
if(block.hasLiquids) liquids = new LiquidModule();
|
|
if(block.hasPower){
|
|
power = new PowerModule();
|
|
power.graph.add(base());
|
|
}
|
|
|
|
initialized = true;
|
|
|
|
return base();
|
|
}
|
|
|
|
@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){
|
|
write.f(health);
|
|
write.b(rotation | 0b10000000);
|
|
write.b(team.id);
|
|
write.b(0); //extra padding for later use
|
|
if(items != null) items.write(write);
|
|
if(power != null) power.write(write);
|
|
if(liquids != null) liquids.write(write);
|
|
if(cons != null) cons.write(write);
|
|
}
|
|
|
|
public final void readBase(Reads read){
|
|
health = read.f();
|
|
byte rot = read.b();
|
|
team = Team.get(read.b());
|
|
|
|
rotation = rot & 0b01111111;
|
|
boolean legacy = true;
|
|
if((rot & 0b10000000) != 0){
|
|
read.b(); //padding
|
|
legacy = false;
|
|
}
|
|
|
|
if(items != null) items.read(read, legacy);
|
|
if(power != null) power.read(read, legacy);
|
|
if(liquids != null) liquids.read(read, legacy);
|
|
if(cons != null) cons.read(read, legacy);
|
|
}
|
|
|
|
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
|
|
|
|
/** Configure with the current, local player. */
|
|
public void configure(Object value){
|
|
//save last used config
|
|
block.lastConfig = value;
|
|
Call.tileConfig(player, base(), value);
|
|
}
|
|
|
|
/** Configure from a server. */
|
|
public void configureAny(Object value){
|
|
Call.tileConfig(null, base(), value);
|
|
}
|
|
|
|
/** Deselect this tile from configuration. */
|
|
public void deselect(){
|
|
if(!headless && control.input.frag.config.getSelectedTile() == base()){
|
|
control.input.frag.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 void applyBoost(float intensity, float duration){
|
|
timeScale = Math.max(timeScale, intensity);
|
|
timeScaleDuration = Math.max(timeScaleDuration, duration);
|
|
}
|
|
|
|
public Building nearby(int dx, int dy){
|
|
return world.build(tile.x + dx, tile.y + dy);
|
|
}
|
|
|
|
public Building nearby(int rotation){
|
|
if(rotation == 0) return world.build(tile.x + 1, tile.y);
|
|
if(rotation == 1) return world.build(tile.x, tile.y + 1);
|
|
if(rotation == 2) return world.build(tile.x - 1, tile.y);
|
|
if(rotation == 3) return world.build(tile.x, tile.y - 1);
|
|
return null;
|
|
}
|
|
|
|
public byte relativeTo(Tile tile){
|
|
return relativeTo(tile.x, tile.y);
|
|
}
|
|
|
|
public byte relativeTo(Building tile){
|
|
return relativeTo(tile.tile());
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public int pos(){
|
|
return tile.pos();
|
|
}
|
|
|
|
public float rotdeg(){
|
|
return rotation * 90;
|
|
}
|
|
|
|
public Floor floor(){
|
|
return tile.floor();
|
|
}
|
|
|
|
public boolean interactable(Team team){
|
|
return state.teams.canInteract(team, team());
|
|
}
|
|
|
|
public float timeScale(){
|
|
return timeScale;
|
|
}
|
|
|
|
public boolean consValid(){
|
|
return cons.valid();
|
|
}
|
|
|
|
public void consume(){
|
|
cons.trigger();
|
|
}
|
|
|
|
/** Scaled delta. */
|
|
public float delta(){
|
|
return Time.delta * timeScale;
|
|
}
|
|
|
|
/** Efficiency * delta. */
|
|
public float edelta(){
|
|
return efficiency() * delta();
|
|
}
|
|
|
|
/** Base efficiency. If this entity has non-buffered power, returns the power %, otherwise returns 1. */
|
|
public float efficiency(){
|
|
//disabled -> 0 efficiency
|
|
if(!enabled) return 0;
|
|
return power != null && (block.consumes.has(ConsumeType.power) && !block.consumes.getPower().buffered) ? power.status : 1f;
|
|
}
|
|
|
|
/** 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
|
|
|
|
/** 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(){}
|
|
|
|
public boolean shouldConsume(){
|
|
return enabled;
|
|
}
|
|
|
|
public boolean productionValid(){
|
|
return true;
|
|
}
|
|
|
|
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(base(), 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, Teamc source){
|
|
noSleep();
|
|
items.add(item, amount);
|
|
}
|
|
|
|
/** Returns offset for stack placement. */
|
|
public void getStackOffset(Item item, Vec2 trns){
|
|
|
|
}
|
|
|
|
public void onProximityUpdate(){
|
|
noSleep();
|
|
}
|
|
|
|
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(@NonNull Payload todump){
|
|
int trns = block.size/2 + 1;
|
|
Tile next = tile.getNearby(Geometry.d4(rotation).x * trns, Geometry.d4(rotation).y * trns);
|
|
|
|
if(next != null && next.build != null && next.build.team == team && next.build.acceptPayload(base(), todump)){
|
|
next.build.handlePayload(base(), 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(@NonNull Payload todump){
|
|
if(proximity.size == 0) return false;
|
|
|
|
int dump = this.dump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
|
|
if(other.team == team && other.acceptPayload(base(), todump)){
|
|
other.handlePayload(base(), 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.consumes.itemFilters.get(item.id) && items.get(item) < getMaximumAccepted(item);
|
|
}
|
|
|
|
public boolean acceptLiquid(Building source, Liquid liquid, float amount){
|
|
return block.hasLiquids && liquids.get(liquid) + amount < block.liquidCapacity && block.consumes.liquidfilters.get(liquid.id);
|
|
}
|
|
|
|
public void handleLiquid(Building source, Liquid liquid, float amount){
|
|
liquids.add(liquid, amount);
|
|
}
|
|
|
|
public void dumpLiquid(Liquid liquid){
|
|
int dump = this.dump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
other = other.getLiquidDestination(base(), liquid);
|
|
|
|
if(other != null && other.team == team && 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 / 2f, 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) - 0.001f, amount);
|
|
|
|
if(next.acceptLiquid(base(), liquid, flow)){
|
|
next.handleLiquid(base(), liquid, flow);
|
|
liquids.remove(liquid, flow);
|
|
}
|
|
}
|
|
|
|
public float moveLiquidForward(float leakResistance, Liquid liquid){
|
|
Tile next = tile.getNearby(rotation);
|
|
|
|
if(next == null) return 0;
|
|
|
|
if(next.build != null){
|
|
return moveLiquid(next.build, liquid);
|
|
}else if(leakResistance != 100f && !next.block().solid && !next.block().hasLiquids){
|
|
float leakAmount = liquids.get(liquid) / leakResistance;
|
|
Puddles.deposit(next, tile, liquid, leakAmount);
|
|
liquids.remove(liquid, leakAmount);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public float moveLiquid(Building next, Liquid liquid){
|
|
if(next == null) return 0;
|
|
|
|
next = next.getLiquidDestination(base(), 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) * (1f)) * (block.liquidCapacity), liquids.get(liquid));
|
|
flow = Math.min(flow, next.block.liquidCapacity - next.liquids.get(liquid) - 0.001f);
|
|
|
|
if(flow > 0f && ofract <= fract && next.acceptLiquid(base(), liquid, flow)){
|
|
next.handleLiquid(base(), liquid, flow);
|
|
liquids.remove(liquid, flow);
|
|
return flow;
|
|
}else if(next.liquids.currentAmount() / next.block.liquidCapacity > 0.1f && fract > 0.1f){
|
|
//TODO these are incorrect effect positions
|
|
float fx = (x + next.x) / 2f, fy = (y + next.y) / 2f;
|
|
|
|
Liquid other = next.liquids.current();
|
|
if((other.flammability > 0.3f && liquid.temperature > 0.7f) || (liquid.flammability > 0.3f && other.temperature > 0.7f)){
|
|
damage(1 * Time.delta);
|
|
next.damage(1 * Time.delta);
|
|
if(Mathf.chance(0.1 * Time.delta)){
|
|
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.chance(0.2f * Time.delta)){
|
|
Fx.steam.at(fx, fy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public Building getLiquidDestination(Building from, Liquid liquid){
|
|
return base();
|
|
}
|
|
|
|
public @Nullable Payload getPayload(){
|
|
return null;
|
|
}
|
|
|
|
/** Tries to take the payload. Returns null if no payload is present. */
|
|
public @Nullable Payload takePayload(){
|
|
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){
|
|
int dump = this.dump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
if(other.team == team && other.acceptItem(base(), item) && canDump(other, item)){
|
|
other.handleItem(base(), item);
|
|
return;
|
|
}
|
|
}
|
|
|
|
handleItem(base(), 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.dump;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
incrementDump(proximity.size);
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
if(other.team == team && other.acceptItem(base(), item) && canDump(other, item)){
|
|
other.handleItem(base(), item);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Try dumping any item near the */
|
|
public boolean dump(){
|
|
return dump(null);
|
|
}
|
|
|
|
/**
|
|
* Try dumping a specific item near the
|
|
* @param todump Item to dump. Can be null to dump anything.
|
|
*/
|
|
public boolean dump(Item todump){
|
|
if(!block.hasItems || items.total() == 0 || (todump != null && !items.has(todump))) return false;
|
|
|
|
int dump = this.dump;
|
|
|
|
if(proximity.size == 0) return false;
|
|
|
|
for(int i = 0; i < proximity.size; i++){
|
|
Building other = proximity.get((i + dump) % proximity.size);
|
|
|
|
if(todump == null){
|
|
|
|
for(int ii = 0; ii < content.items().size; ii++){
|
|
Item item = content.item(ii);
|
|
|
|
if(other.team == team && items.has(item) && other.acceptItem(base(), item) && canDump(other, item)){
|
|
other.handleItem(base(), item);
|
|
items.remove(item, 1);
|
|
incrementDump(proximity.size);
|
|
return true;
|
|
}
|
|
}
|
|
}else{
|
|
if(other.team == team && other.acceptItem(base(), todump) && canDump(other, todump)){
|
|
other.handleItem(base(), todump);
|
|
items.remove(todump, 1);
|
|
incrementDump(proximity.size);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
incrementDump(proximity.size);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void incrementDump(int prox){
|
|
dump = (byte)((dump + 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(base(), item)){
|
|
other.handleItem(base(), item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void onProximityRemoved(){
|
|
if(power != null){
|
|
powerGraphRemoved();
|
|
}
|
|
}
|
|
|
|
public void onProximityAdded(){
|
|
if(block.hasPower) updatePowerGraph();
|
|
}
|
|
|
|
public void updatePowerGraph(){
|
|
|
|
for(Building other : getPowerConnections(tempTileEnts)){
|
|
if(other.power != null){
|
|
other.power.graph.addGraph(power.graph);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void powerGraphRemoved(){
|
|
if(power == null){
|
|
return;
|
|
}
|
|
|
|
power.graph.remove(base());
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
public Seq<Building> getPowerConnections(Seq<Building> out){
|
|
out.clear();
|
|
if(power == null) return out;
|
|
|
|
for(Building other : proximity){
|
|
if(other != null && other.power != null
|
|
&& !(block.consumesPower && other.block.consumesPower && !block.outputsPower && !other.block.outputsPower)
|
|
&& !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) 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 whether this block should play its idle sound.*/
|
|
public boolean shouldIdleSound(){
|
|
return shouldConsume();
|
|
}
|
|
|
|
public void drawStatus(){
|
|
if(block.enableDrawStatus && block.consumes.any()){
|
|
float brcx = tile.drawx() + (block.size * tilesize / 2f) - (tilesize / 2f);
|
|
float brcy = tile.drawy() - (block.size * tilesize / 2f) + (tilesize / 2f);
|
|
|
|
Draw.z(Layer.power + 1);
|
|
Draw.color(Pal.gray);
|
|
Fill.square(brcx, brcy, 2.5f, 45);
|
|
Draw.color(cons.status().color);
|
|
Fill.square(brcx, brcy, 1.5f, 45);
|
|
Draw.color();
|
|
}
|
|
}
|
|
|
|
public void drawCracks(){
|
|
if(!damaged() || block.size > Block.maxCrackSize) return;
|
|
int id = pos();
|
|
TextureRegion region = Block.cracks[block.size - 1][Mathf.clamp((int)((1f - healthf()) * Block.crackRegions), 0, Block.crackRegions-1)];
|
|
Draw.colorl(0.2f, 0.1f + (1f - healthf())* 0.6f);
|
|
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(){
|
|
}
|
|
|
|
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(){
|
|
Draw.rect(block.region, x, y, block.rotate ? rotdeg() : 0);
|
|
|
|
drawTeamTop();
|
|
}
|
|
|
|
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(){
|
|
if(block.hasLiquids && block.drawLiquidLight && liquids.current().lightColor.a > 0.001f){
|
|
drawLiquidLight(liquids.current(), liquids.smoothAmount());
|
|
}
|
|
}
|
|
|
|
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(team, x, y, block.size * 30f * fract, color, opacity);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/** Called after the block is placed by this client. */
|
|
@CallSuper
|
|
public void playerPlaced(){
|
|
if(block.saveConfig && block.lastConfig != null){
|
|
configure(block.lastConfig);
|
|
}
|
|
}
|
|
|
|
/** Called after the block is placed by anyone. */
|
|
@CallSuper
|
|
public void placed(){
|
|
if(net.client()) return;
|
|
|
|
if((block.consumesPower && !block.outputsPower) || (!block.consumesPower && block.outputsPower)){
|
|
int range = 10;
|
|
tempTiles.clear();
|
|
Geometry.circle(tileX(), tileY(), range, (x, y) -> {
|
|
Building other = world.build(x, y);
|
|
if(other != null && other.block instanceof PowerNode && ((PowerNode)other.block).linkValid(other, base()) && !PowerNode.insulated(other, base())
|
|
&& !other.proximity().contains(this.<Building>base()) &&
|
|
!(block.outputsPower && proximity.contains(p -> p.power != null && p.power.graph == other.power.graph))){
|
|
tempTiles.add(other.tile());
|
|
}
|
|
});
|
|
tempTiles.sort(Structs.comparingFloat(t -> t.dst2(tile)));
|
|
if(!tempTiles.isEmpty()){
|
|
Tile toLink = tempTiles.first();
|
|
if(!toLink.build.power.links.contains(pos())){
|
|
toLink.build.configureAny(pos());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 Player player, @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(block.configurations.containsKey(type)){
|
|
block.configurations.get(type).get(this, value);
|
|
}
|
|
}
|
|
|
|
/** Called when the block is tapped by the local player. */
|
|
public void tapped(){
|
|
|
|
}
|
|
|
|
/** Called when the block is destroyed. */
|
|
public void onDestroyed(){
|
|
float explosiveness = block.baseExplosiveness;
|
|
float flammability = 0f;
|
|
float power = 0f;
|
|
|
|
if(block.hasItems){
|
|
for(Item item : content.items()){
|
|
int amount = items.get(item);
|
|
explosiveness += item.explosiveness * amount;
|
|
flammability += item.flammability * amount;
|
|
}
|
|
}
|
|
|
|
if(block.hasLiquids){
|
|
flammability += liquids.sum((liquid, amount) -> liquid.explosiveness * amount / 2f);
|
|
explosiveness += liquids.sum((liquid, amount) -> liquid.flammability * amount / 2f);
|
|
}
|
|
|
|
if(block.consumes.hasPower() && block.consumes.getPower().buffered){
|
|
power += this.power.status * block.consumes.getPower().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.tile(tileX() + Mathf.range(block.size / 2), tileY() + Mathf.range(block.size / 2));
|
|
if(other != null){
|
|
Puddles.deposit(other, liquid, splash);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, Pal.darkFlame, state.rules.damageExplosions);
|
|
|
|
if(!floor().solid && !floor().isLiquid){
|
|
Effect.rubble(x, y, block.size);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the flammability of the Used for fire calculations.
|
|
* Takes flammability of floor liquid into account.
|
|
*/
|
|
public float getFlammability(){
|
|
if(!block.hasItems){
|
|
if(floor().isLiquid && !block.solid){
|
|
return floor().liquidDrop.flammability;
|
|
}
|
|
return 0;
|
|
}else{
|
|
float result = items.sum((item, amount) -> item.flammability * amount);
|
|
|
|
if(block.hasLiquids){
|
|
result += liquids.sum((liquid, amount) -> liquid.flammability * amount / 3f);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public String getDisplayName(){
|
|
return block.localizedName;
|
|
}
|
|
|
|
public TextureRegion getDisplayIcon(){
|
|
return block.icon(Cicon.medium);
|
|
}
|
|
|
|
@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) && Core.settings.getBool("flow");
|
|
|
|
if(displayFlow){
|
|
String ps = " " + StatUnit.perSecond.localized();
|
|
|
|
if(items != null){
|
|
table.row();
|
|
table.left();
|
|
table.table(l -> {
|
|
Bits current = new Bits();
|
|
|
|
Runnable rebuild = () -> {
|
|
l.clearChildren();
|
|
l.left();
|
|
for(Item item : content.items()){
|
|
if(items.hasFlowItem(item)){
|
|
l.image(item.icon(Cicon.small)).padRight(3f);
|
|
l.label(() -> items.getFlowRate(item) < 0 ? "..." : Strings.fixed(items.getFlowRate(item), 1) + ps).color(Color.lightGray);
|
|
l.row();
|
|
}
|
|
}
|
|
};
|
|
|
|
rebuild.run();
|
|
l.update(() -> {
|
|
for(Item item : content.items()){
|
|
if(items.hasFlowItem(item) && !current.get(item.id)){
|
|
current.set(item.id);
|
|
rebuild.run();
|
|
}
|
|
}
|
|
});
|
|
}).left();
|
|
}
|
|
|
|
if(liquids != null){
|
|
table.row();
|
|
table.table(l -> {
|
|
l.left();
|
|
l.image(() -> liquids.current().icon(Cicon.small)).padRight(3f);
|
|
l.label(() -> liquids.getFlowRate() < 0 ? "..." : Strings.fixed(liquids.getFlowRate(), 2) + ps).color(Color.lightGray);
|
|
}).left();
|
|
}
|
|
}
|
|
|
|
table.marginBottom(-5);
|
|
}
|
|
}
|
|
|
|
public void displayConsumption(Table table){
|
|
table.left();
|
|
for(Consume cons : block.consumes.all()){
|
|
if(cons.isOptional() && cons.isBoost()) continue;
|
|
cons.build(base(), table);
|
|
}
|
|
}
|
|
|
|
public void displayBars(Table table){
|
|
for(Func<Building, Bar> bar : block.bars.list()){
|
|
table.add(bar.get(base())).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 or not a hand cursor should be shown over this block. */
|
|
public Cursor getCursor(){
|
|
return block.configurable ? SystemCursor.hand : SystemCursor.arrow;
|
|
}
|
|
|
|
/**
|
|
* Called when another tile is tapped while this block is selected.
|
|
* @return whether or not this block should be deselected.
|
|
*/
|
|
public boolean onConfigureTileTapped(Building other){
|
|
return base() != other;
|
|
}
|
|
|
|
/** 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 collide(Bullet other){
|
|
return true;
|
|
}
|
|
|
|
/** Handle a bullet collision.
|
|
* @return whether the bullet should be removed. */
|
|
public boolean collision(Bullet other){
|
|
damage(other.damage() * other.type().tileDamageMultiplier);
|
|
|
|
return true;
|
|
}
|
|
|
|
public boolean canPickup(){
|
|
return true;
|
|
}
|
|
|
|
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(base(), true);
|
|
other.onProximityUpdate();
|
|
}
|
|
}
|
|
|
|
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.tile.interactable(team))) continue;
|
|
|
|
//add this tile to proximity of nearby tiles
|
|
if(!other.proximity.contains(base(), true)){
|
|
other.proximity.add(base());
|
|
}
|
|
|
|
tmpTiles.add(other);
|
|
}
|
|
|
|
//using a set to prevent duplicates
|
|
for(Building tile : tmpTiles){
|
|
proximity.add(tile);
|
|
}
|
|
|
|
for(Building other : tmpTiles){
|
|
other.onProximityUpdate();
|
|
}
|
|
|
|
onProximityAdded();
|
|
onProximityUpdate();
|
|
|
|
for(Building other : tmpTiles){
|
|
other.onProximityUpdate();
|
|
}
|
|
}
|
|
|
|
public void updateTile(){
|
|
|
|
}
|
|
|
|
//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 == base() && !dead();
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public void kill(){
|
|
Call.tileDestroyed(base());
|
|
}
|
|
|
|
@Replace
|
|
@Override
|
|
public void damage(float damage){
|
|
if(dead()) return;
|
|
|
|
if(Mathf.zero(state.rules.blockHealthMultiplier)){
|
|
damage = health + 1;
|
|
}else{
|
|
damage /= state.rules.blockHealthMultiplier;
|
|
}
|
|
|
|
Call.tileDamage(base(), health - handleDamage(damage));
|
|
|
|
if(health <= 0){
|
|
Call.tileDestroyed(base());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public double sense(LAccess sensor){
|
|
if(sensor == LAccess.x) return x;
|
|
if(sensor == LAccess.y) return y;
|
|
if(sensor == LAccess.team) return team.id;
|
|
if(sensor == LAccess.health) return health;
|
|
if(sensor == LAccess.efficiency) return efficiency();
|
|
if(sensor == LAccess.rotation) return rotation;
|
|
if(sensor == LAccess.totalItems && items != null) return items.total();
|
|
if(sensor == LAccess.totalLiquids && liquids != null) return liquids.total();
|
|
if(sensor == LAccess.totalPower && power != null && block.consumes.hasPower()) return power.status * (block.consumes.getPower().buffered ? block.consumes.getPower().capacity : 1f);
|
|
if(sensor == LAccess.itemCapacity) return block.itemCapacity;
|
|
if(sensor == LAccess.liquidCapacity) return block.liquidCapacity;
|
|
if(sensor == LAccess.powerCapacity) return block.consumes.hasPower() ? block.consumes.getPower().capacity : 0f;
|
|
if(sensor == LAccess.powerNetIn && power != null) return power.graph.getPowerProduced();
|
|
if(sensor == LAccess.powerNetOut && power != null) return power.graph.getPowerNeeded();
|
|
if(sensor == LAccess.powerNetStored && power != null) return power.graph.getLastPowerStored();
|
|
if(sensor == LAccess.powerNetCapacity && power != null) return power.graph.getBatteryCapacity();
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public double sense(Content content){
|
|
if(content instanceof Item && items != null) return items.get((Item)content);
|
|
if(content instanceof Liquid && liquids != null) return liquids.get((Liquid)content);
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void control(LAccess type, double p1, double p2, double p3, double p4){
|
|
if(type == LAccess.enabled){
|
|
enabled = !Mathf.zero((float)p1);
|
|
enabledControlTime = timeToUncontrol;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void remove(){
|
|
if(sound != null){
|
|
sound.stop();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void killed(){
|
|
Events.fire(new BlockDestroyEvent(tile));
|
|
block.breakSound.at(tile);
|
|
onDestroyed();
|
|
tile.remove();
|
|
remove();
|
|
}
|
|
|
|
@Final
|
|
@Override
|
|
public void update(){
|
|
timeScaleDuration -= Time.delta;
|
|
if(timeScaleDuration <= 0f || !block.canOverdrive){
|
|
timeScale = 1f;
|
|
}
|
|
|
|
if(block.autoResetEnabled){
|
|
enabledControlTime -= Time.delta;
|
|
|
|
if(enabledControlTime <= 0){
|
|
enabled = true;
|
|
}
|
|
}
|
|
|
|
if(sound != null){
|
|
sound.update(x, y, shouldActiveSound());
|
|
}
|
|
|
|
if(block.idleSound != Sounds.none && shouldIdleSound()){
|
|
loops.play(block.idleSound, base(), block.idleSoundVolume);
|
|
}
|
|
|
|
if(enabled || !block.noUpdateDisabled){
|
|
updateTile();
|
|
}
|
|
|
|
if(items != null){
|
|
items.update(updateFlow);
|
|
}
|
|
|
|
if(liquids != null){
|
|
liquids.update(updateFlow);
|
|
}
|
|
|
|
if(cons != null){
|
|
cons.update();
|
|
}
|
|
|
|
if(power != null){
|
|
power.graph.update();
|
|
}
|
|
|
|
updateFlow = false;
|
|
}
|
|
|
|
@Override
|
|
public void hitbox(Rect out){
|
|
out.setCentered(x, y, block.size * tilesize, block.size * tilesize);
|
|
}
|
|
|
|
//endregion
|
|
}
|