Power PR merged / Various bugfixes

Closes #288
This commit is contained in:
Anuken 2019-01-03 18:22:13 -05:00
commit b8e6e5df61
58 changed files with 1158 additions and 549 deletions

View File

@ -217,8 +217,9 @@ project(":tests"){
dependencies{
testImplementation project(":core")
testImplementation('org.junit.jupiter:junit-jupiter-api:5.1.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.1.0')
testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.1"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1"
compile arcModule("backends:backend-headless")
}

View File

@ -46,7 +46,6 @@ public class CraftingBlocks extends BlockList implements ContentList{
craftEffect = BlockFx.smeltsmoke;
result = Items.silicon;
craftTime = 40f;
powerCapacity = 20f;
size = 2;
hasLiquids = false;
flameColor = Color.valueOf("ffef99");
@ -61,7 +60,6 @@ public class CraftingBlocks extends BlockList implements ContentList{
craftTime = 60f;
output = Items.plastanium;
itemCapacity = 30;
powerCapacity = 40f;
size = 2;
health = 320;
hasPower = hasLiquids = true;
@ -77,7 +75,6 @@ public class CraftingBlocks extends BlockList implements ContentList{
craftEffect = BlockFx.smeltsmoke;
result = Items.phasefabric;
craftTime = 120f;
powerCapacity = 50f;
size = 2;
consumes.items(new ItemStack(Items.thorium, 4), new ItemStack(Items.sand, 10));
@ -88,7 +85,6 @@ public class CraftingBlocks extends BlockList implements ContentList{
craftEffect = BlockFx.smeltsmoke;
result = Items.surgealloy;
craftTime = 75f;
powerCapacity = 60f;
size = 2;
useFlux = true;

View File

@ -39,28 +39,26 @@ public class DebugBlocks extends BlockList implements ContentList{
public void load(){
powerVoid = new PowerBlock("powervoid"){
{
powerCapacity = Float.MAX_VALUE;
consumes.power(Float.MAX_VALUE);
}
@Override
public void init(){
super.init();
stats.remove(BlockStat.powerCapacity);
stats.remove(BlockStat.powerUse);
}
};
powerInfinite = new PowerNode("powerinfinite"){
{
powerCapacity = 10000f;
maxNodes = 100;
outputsPower = true;
consumesPower = false;
}
@Override
public void update(Tile tile){
super.update(tile);
tile.entity.power.amount = powerCapacity;
public float getPowerProduction(Tile tile){
return 10000f;
}
};

View File

@ -71,19 +71,18 @@ public class DefenseBlocks extends BlockList implements ContentList{
}};
mendProjector = new MendProjector("mend-projector"){{
consumes.power(0.2f);
consumes.power(0.2f, 1.0f);
size = 2;
consumes.item(Items.phasefabric).optional(true);
}};
overdriveProjector = new OverdriveProjector("overdrive-projector"){{
consumes.power(0.35f);
consumes.power(0.35f, 1.0f);
size = 2;
consumes.item(Items.phasefabric).optional(true);
}};
forceProjector = new ForceProjector("force-projector"){{
consumes.power(0.2f);
size = 3;
consumes.item(Items.phasefabric).optional(true);
}};

View File

@ -35,7 +35,7 @@ public class DistributionBlocks extends BlockList implements ContentList{
phaseConveyor = new ItemBridge("phase-conveyor"){{
range = 12;
hasPower = true;
consumes.power(0.03f);
consumes.power(0.03f, 1.0f);
}};
sorter = new Sorter("sorter");

View File

@ -20,7 +20,6 @@ public class LiquidBlocks extends BlockList implements ContentList{
pumpAmount = 0.2f;
consumes.power(0.015f);
liquidCapacity = 30f;
powerCapacity = 20f;
hasPower = true;
size = 2;
tier = 1;
@ -31,7 +30,6 @@ public class LiquidBlocks extends BlockList implements ContentList{
consumes.power(0.03f);
liquidCapacity = 40f;
hasPower = true;
powerCapacity = 20f;
size = 2;
tier = 2;
}};
@ -66,7 +64,7 @@ public class LiquidBlocks extends BlockList implements ContentList{
phaseConduit = new LiquidBridge("phase-conduit"){{
range = 12;
hasPower = true;
consumes.power(0.03f);
consumes.power(0.03f, 1.0f);
}};
}
}

View File

@ -4,6 +4,7 @@ import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.*;
public class PowerBlocks extends BlockList implements ContentList{
@ -13,48 +14,43 @@ public class PowerBlocks extends BlockList implements ContentList{
@Override
public void load(){
combustionGenerator = new BurnerGenerator("combustion-generator"){{
powerOutput = 0.09f;
powerCapacity = 40f;
powerProduction = 0.09f;
itemDuration = 40f;
}};
thermalGenerator = new LiquidHeatGenerator("thermal-generator"){{
maxLiquidGenerate = 2f;
powerCapacity = 40f;
powerPerLiquid = 0.35f;
powerProduction = 2f;
generateEffect = BlockFx.redgeneratespark;
size = 2;
}};
turbineGenerator = new TurbineGenerator("turbine-generator"){{
powerOutput = 0.28f;
powerCapacity = 40f;
powerProduction = 0.28f;
itemDuration = 30f;
powerPerLiquid = 0.7f;
consumes.liquid(Liquids.water, 0.05f);
size = 2;
}};
rtgGenerator = new DecayGenerator("rtg-generator"){{
powerCapacity = 40f;
size = 2;
powerOutput = 0.3f;
powerProduction = 0.3f;
itemDuration = 220f;
}};
solarPanel = new SolarGenerator("solar-panel"){{
generation = 0.0045f;
powerProduction = 0.0045f;
}};
largeSolarPanel = new SolarGenerator("solar-panel-large"){{
size = 3;
generation = 0.055f;
powerProduction = 0.055f;
}};
thoriumReactor = new NuclearReactor("thorium-reactor"){{
size = 3;
health = 700;
powerMultiplier = 1.1f;
powerProduction = 1.1f;
}};
fusionReactor = new FusionReactor("fusion-reactor"){{
@ -63,12 +59,12 @@ public class PowerBlocks extends BlockList implements ContentList{
}};
battery = new Battery("battery"){{
powerCapacity = 320f;
consumes.powerBuffered(320f, 1f);
}};
batteryLarge = new Battery("battery-large"){{
size = 3;
powerCapacity = 2000f;
consumes.powerBuffered(2000f, 1f);
}};
powerNode = new PowerNode("power-node"){{

View File

@ -92,8 +92,8 @@ public class TurretBlocks extends BlockList implements ContentList{
recoil = 2f;
reload = 100f;
cooldown = 0.03f;
powerUsed = 20f;
powerCapacity = 60f;
powerUsed = 1 / 3f;
consumes.powerBuffered(60f);
shootShake = 2f;
shootEffect = ShootFx.lancerLaserShoot;
smokeEffect = ShootFx.lancerLaserShootSmoke;
@ -111,8 +111,8 @@ public class TurretBlocks extends BlockList implements ContentList{
shootShake = 1f;
shootCone = 40f;
rotatespeed = 8f;
powerUsed = 10f;
powerCapacity = 30f;
powerUsed = 1f / 3f;
consumes.powerBuffered(30f);
range = 150f;
shootEffect = ShootFx.lightningShoot;
heatColor = Color.RED;
@ -249,8 +249,8 @@ public class TurretBlocks extends BlockList implements ContentList{
recoil = 4f;
size = 4;
shootShake = 2f;
powerUsed = 60f;
powerCapacity = 120f;
powerUsed = 0.5f;
consumes.powerBuffered(120f);
range = 160f;
reload = 200f;
firingMoveFract = 0.1f;

View File

@ -9,52 +9,53 @@ public class UpgradeBlocks extends BlockList{
@Override
public void load(){
alphaPad = new MechPad("alpha-mech-pad"){{
mech = Mechs.alpha;
size = 2;
powerCapacity = 50f;
consumes.powerBuffered(50f);
}};
deltaPad = new MechPad("delta-mech-pad"){{
mech = Mechs.delta;
size = 2;
powerCapacity = 70f;
consumes.powerBuffered(70f);
}};
tauPad = new MechPad("tau-mech-pad"){{
mech = Mechs.tau;
size = 2;
powerCapacity = 100f;
consumes.powerBuffered(100f);
}};
omegaPad = new MechPad("omega-mech-pad"){{
mech = Mechs.omega;
size = 3;
powerCapacity = 120f;
consumes.powerBuffered(120f);
}};
dartPad = new MechPad("dart-ship-pad"){{
mech = Mechs.dart;
size = 2;
powerCapacity = 50f;
consumes.powerBuffered(50f);
}};
javelinPad = new MechPad("javelin-ship-pad"){{
mech = Mechs.javelin;
size = 2;
powerCapacity = 80f;
consumes.powerBuffered(80f);
}};
tridentPad = new MechPad("trident-ship-pad"){{
mech = Mechs.trident;
size = 2;
powerCapacity = 100f;
consumes.powerBuffered(100f);
}};
glaivePad = new MechPad("glaive-ship-pad"){{
mech = Mechs.glaive;
size = 3;
powerCapacity = 120f;
consumes.powerBuffered(120f);
}};
}
}

View File

@ -22,7 +22,7 @@ public class OperationStack{
stack.add(action);
if(stack.size > maxSize){
stack.removeAt(0);
stack.remove(0);
}
}

View File

@ -310,7 +310,7 @@ public interface BuilderTrait extends Entity{
return;
}
Draw.color(Palette.accent);
Lines.stroke(1f, Palette.accent);
float focusLen = 3.8f + Mathf.absin(Time.time(), 1.1f, 0.6f);
float px = unit.x + Angles.trnsx(unit.rotation, focusLen);
float py = unit.y + Angles.trnsy(unit.rotation, focusLen);

View File

@ -67,6 +67,7 @@ public class DesktopInput extends InputHandler{
@Override
public void drawOutlined(){
Lines.stroke(1f);
int cursorX = tileX(Core.input.mouseX());
int cursorY = tileY(Core.input.mouseY());

View File

@ -656,7 +656,7 @@ public class MobileInput extends InputHandler implements GestureListener{
PlaceRequest request = removals.get(i);
if(request.scale <= 0.0001f){
removals.removeAt(i);
removals.remove(i);
i--;
}
}

View File

@ -180,7 +180,7 @@ public class FortressGenerator{
Block block = tile.block();
if(block instanceof PowerTurret){
tile.entity.power.amount = block.powerCapacity;
tile.entity.power.satisfaction = 1.0f;
}else if(block instanceof ItemTurret){
ItemTurret turret = (ItemTurret)block;
AmmoType[] type = turret.getAmmoTypes();

View File

@ -32,7 +32,7 @@ public class LoadingFragment extends Fragment{
public void setButton(Runnable listener){
button.visible(true);
button.getListeners().removeAt(button.getListeners().size - 1);
button.getListeners().remove(button.getListeners().size - 1);
button.clicked(listener);
}

View File

@ -26,12 +26,11 @@ public abstract class BaseBlock extends MappableContent{
public boolean outputsLiquid = false;
public boolean singleLiquid = true;
public boolean consumesPower = true;
public boolean outputsPower;
public boolean outputsPower = false;
public int itemCapacity;
public float liquidCapacity = 10f;
public float liquidFlowFactor = 4.9f;
public float powerCapacity = 10f;
public Consumers consumes = new Consumers();
public Producers produces = new Producers();
@ -40,6 +39,10 @@ public abstract class BaseBlock extends MappableContent{
return true;
}
public float getPowerProduction(Tile tile){
return 0f;
}
/**Returns the amount of items this block can accept.*/
public int acceptStack(Item item, int amount, Tile tile, Unit source){
if(acceptItem(item, tile, tile) && hasItems && (source == null || source.getTeam() == tile.getTeam())){
@ -98,19 +101,6 @@ public abstract class BaseBlock extends MappableContent{
tile.entity.liquids.add(liquid, amount);
}
public boolean acceptPower(Tile tile, Tile source, float amount){
return true;
}
/**Returns how much power is accepted.*/
public float addPower(Tile tile, float amount){
float canAccept = Math.min(powerCapacity - tile.entity.power.amount, amount);
tile.entity.power.amount += canAccept;
return canAccept;
}
public void tryDumpLiquid(Tile tile, Liquid liquid){
Array<Tile> proximity = tile.entity.proximity();
int dump = tile.getDump();

View File

@ -28,6 +28,7 @@ import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentType;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.consumers.ConsumePower;
import io.anuke.mindustry.world.meta.*;
import static io.anuke.mindustry.Vars.*;
@ -179,6 +180,14 @@ public class Block extends BaseBlock {
return out;
}
protected float getProgressIncrease(TileEntity entity, float baseTime){
float progressIncrease = 1f / baseTime * entity.delta();
if(hasPower){
progressIncrease *= entity.power.satisfaction; // Reduced increase in case of low power
}
return progressIncrease;
}
public boolean isLayer(Tile tile){
return true;
}
@ -321,7 +330,7 @@ public class Block extends BaseBlock {
consumes.forEach(cons -> cons.display(stats));
if(hasPower) stats.add(BlockStat.powerCapacity, powerCapacity, StatUnit.powerUnits);
// Note: Power stats are added by the consumers.
if(hasLiquids) stats.add(BlockStat.liquidCapacity, liquidCapacity, StatUnit.liquidUnits);
if(hasItems) stats.add(BlockStat.itemCapacity, itemCapacity, StatUnit.items);
}
@ -379,8 +388,8 @@ public class Block extends BaseBlock {
explosiveness += tile.entity.liquids.sum((liquid, amount) -> liquid.flammability * amount / 2f);
}
if(hasPower){
power += tile.entity.power.amount;
if(consumes.has(ConsumePower.class) && consumes.get(ConsumePower.class).isBuffered){
power += tile.entity.power.satisfaction * consumes.get(ConsumePower.class).powerCapacity;
}
if(hasLiquids){
@ -529,4 +538,4 @@ public class Block extends BaseBlock {
"entity.graph", tile.entity.power != null && tile.entity.power.graph != null ? tile.entity.power.graph.getID() : null
);
}
}
}

View File

@ -59,22 +59,6 @@ public class BlockPart extends Block{
block.handleLiquid(tile.getLinked(), source, liquid, amount);
}
@Override
public float addPower(Tile tile, float amount){
Block block = linked(tile);
if(block.hasPower){
return block.addPower(tile.getLinked(), amount);
}else{
return amount;
}
}
@Override
public boolean acceptPower(Tile tile, Tile from, float amount){
Block block = linked(tile);
return block.hasPower && block.acceptPower(tile.getLinked(), from, amount);
}
private Block linked(Tile tile){
return tile.getLinked().block();
}

View File

@ -21,6 +21,7 @@ import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter;
import io.anuke.mindustry.world.consumers.ConsumePower;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
@ -40,9 +41,12 @@ public class ForceProjector extends Block {
protected float cooldownNormal = 1.75f;
protected float cooldownLiquid = 1.5f;
protected float cooldownBrokenBase = 0.35f;
protected float basePowerDraw = 0.2f;
protected float powerDamage = 0.1f;
protected final ConsumeForceProjectorPower consumePower;
protected TextureRegion topRegion;
public ForceProjector(String name) {
super(name);
update = true;
@ -50,10 +54,11 @@ public class ForceProjector extends Block {
hasPower = true;
canOverdrive = false;
hasLiquids = true;
powerCapacity = 60f;
hasItems = true;
itemCapacity = 10;
consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.1f)).optional(true).update(false);
consumePower = new ConsumeForceProjectorPower(60f, 60f);
consumes.add(consumePower);
}
@Override
@ -66,6 +71,7 @@ public class ForceProjector extends Block {
public void setStats(){
super.setStats();
stats.add(BlockStat.powerUse, basePowerDraw * 60f, StatUnit.powerSecond);
stats.add(BlockStat.powerDamage, powerDamage, StatUnit.powerUnits);
}
@ -91,15 +97,27 @@ public class ForceProjector extends Block {
Effects.effect(BlockFx.reactorsmoke, tile.drawx() + Mathf.range(tilesize/2f), tile.drawy() + Mathf.range(tilesize/2f));
}
if(!entity.cons.valid() && !cheat){
// Use Cases:
// - There is enough power in the buffer, and there are no shots fired => Draw base power and keep shield up
// - There is enough power in the buffer, but not enough power to cope for shots being fired => Draw all power and break shield
// - There is enough power in the buffer and enough power to cope for shots being fired => Draw base power + additional power based on shots absorbed
// - There is not enough base power in the buffer => Draw all power and break shield
// - The generator is in the AI base and uses cheat mode => Only draw power from shots being absorbed
float relativePowerDraw = 0.0f;
if(!cheat){
relativePowerDraw = basePowerDraw / consumePower.powerCapacity;
}
if(entity.power.satisfaction < relativePowerDraw){
entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.15f);
entity.power.satisfaction = .0f;
if(entity.warmup <= 0.09f){
entity.broken = true;
}
}else{
entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.1f);
float powerUse = Math.min(powerDamage * entity.delta() * (1f + entity.buildup / breakage), powerCapacity);
entity.power.amount -= powerUse;
entity.power.satisfaction -= Math.min(entity.power.satisfaction, relativePowerDraw);
}
if(entity.buildup > 0){
@ -134,12 +152,12 @@ public class ForceProjector extends Block {
if(trait.canBeAbsorbed() && trait.getTeam() != tile.getTeam() && isInsideHexagon(trait.getX(), trait.getY(), realRadius * 2f, tile.drawx(), tile.drawy())){
trait.absorb();
Effects.effect(BulletFx.absorb, trait);
float hit = trait.getShieldDamage()*powerDamage;
float relativeDamagePowerDraw = trait.getShieldDamage() * powerDamage / consumePower.powerCapacity;
entity.hit = 1f;
entity.power.amount -= Math.min(hit, entity.power.amount);
if(entity.power.amount <= 0.0001f){
entity.buildup += trait.getShieldDamage() * entity.warmup*2f;
entity.power.satisfaction -= Math.min(relativeDamagePowerDraw, entity.power.satisfaction);
if(entity.power.satisfaction <= 0.0001f){
entity.buildup += trait.getShieldDamage() * entity.warmup * 2f;
}
entity.buildup += trait.getShieldDamage() * entity.warmup;
}
@ -246,4 +264,14 @@ public class ForceProjector extends Block {
return shieldGroup;
}
}
public class ConsumeForceProjectorPower extends ConsumePower{
public ConsumeForceProjectorPower(float powerCapacity, float ticksToFill){
super(powerCapacity / ticksToFill, 0.0f, powerCapacity, true);
}
@Override
public boolean valid(Block block, TileEntity entity){
return entity.power.satisfaction >= basePowerDraw / powerCapacity && super.valid(block, entity);
}
}
}

View File

@ -84,7 +84,7 @@ public class MendProjector extends Block{
other = other.target();
if(other.getTeamID() == tile.getTeamID() && !healed.contains(other.pos()) && other.entity != null && other.entity.health < other.entity.maxHealth()){
other.entity.healBy(other.entity.maxHealth() * (healPercent + entity.phaseHeat*phaseBoost)/100f);
other.entity.healBy(other.entity.maxHealth() * (healPercent + entity.phaseHeat*phaseBoost)/100f * entity.power.satisfaction);
Effects.effect(BlockFx.healBlockFull, Tmp.c1.set(color).lerp(phase, entity.phaseHeat), other.drawx(), other.drawy(), other.block().size);
healed.add(other.pos());
}

View File

@ -68,7 +68,7 @@ public class OverdriveProjector extends Block{
if(entity.charge >= reload){
float realRange = range + entity.phaseHeat * phaseRangeBoost;
float realBoost = speedBoost + entity.phaseHeat*speedBoostPhase;
float realBoost = (speedBoost + entity.phaseHeat*speedBoostPhase) * entity.power.satisfaction;
Effects.effect(BlockFx.overdriveWave, Tmp.c1.set(color).lerp(phase, entity.phaseHeat), tile.drawx(), tile.drawy(), realRange);
entity.charge = 0f;

View File

@ -6,6 +6,7 @@ import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
public abstract class PowerTurret extends CooledTurret{
/** The percentage of power which will be used per shot. */
protected float powerUsed = 0.5f;
protected AmmoType shootType;
@ -23,13 +24,15 @@ public abstract class PowerTurret extends CooledTurret{
@Override
public boolean hasAmmo(Tile tile){
return tile.entity.power.amount >= powerUsed;
// Allow shooting as long as the turret is at least at 50% power
return tile.entity.power.satisfaction >= powerUsed;
}
@Override
public AmmoType useAmmo(Tile tile){
if(tile.isEnemyCheat()) return shootType;
tile.entity.power.amount -= powerUsed;
// Make sure that power can not go negative in case of threading issues or similar
tile.entity.power.satisfaction -= Math.min(powerUsed, tile.entity.power.satisfaction);
return shootType;
}

View File

@ -31,7 +31,11 @@ public class LiquidBridge extends ItemBridge{
tryDumpLiquid(tile, entity.liquids.current());
}else{
if(entity.cons.valid()){
entity.uptime = Mathf.lerpDelta(entity.uptime, 1f, 0.04f);
float alpha = 0.04f;
if(hasPower){
alpha *= entity.power.satisfaction; // Exceed boot time unless power is at max.
}
entity.uptime = Mathf.lerpDelta(entity.uptime, 1f, alpha);
}else{
entity.uptime = Mathf.lerpDelta(entity.uptime, 0f, 0.02f);
}

View File

@ -28,6 +28,7 @@ import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumePower;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
@ -48,6 +49,7 @@ public class MassDriver extends Block{
protected Effect smokeEffect = ShootFx.shootBigSmoke2;
protected Effect recieveEffect = BlockFx.mineBig;
protected float shake = 3f;
protected final static float powerPercentageUsed = 1.0f;
protected TextureRegion turretRegion;
public MassDriver(String name){
@ -58,6 +60,8 @@ public class MassDriver extends Block{
hasItems = true;
layer = Layer.turret;
hasPower = true;
consumes.powerBuffered(30f);
consumes.require(ConsumePower.class);
}
@Remote(targets = Loc.both, called = Loc.server, forward = true)
@ -77,7 +81,8 @@ public class MassDriver extends Block{
MassDriverEntity other = target.entity();
entity.reload = 1f;
entity.power.amount = 0f;
entity.power.satisfaction -= Math.min(entity.power.satisfaction, powerPercentageUsed);
DriverBulletData data = Pools.obtain(DriverBulletData.class, DriverBulletData::new);
data.from = entity;
@ -125,7 +130,7 @@ public class MassDriver extends Block{
public void setStats(){
super.setStats();
stats.add(BlockStat.powerShot, powerCapacity, StatUnit.powerUnits);
stats.add(BlockStat.powerShot, consumes.get(ConsumePower.class).powerCapacity * powerPercentageUsed, StatUnit.powerUnits);
}
@Override
@ -164,8 +169,8 @@ public class MassDriver extends Block{
entity.rotation = Mathf.slerpDelta(entity.rotation, tile.angleTo(waiter), rotateSpeed);
}else if(tile.entity.items.total() >= minDistribute &&
linkValid(tile) && //only fire when at least at half-capacity and power
tile.entity.power.amount >= powerCapacity * 0.8f &&
linkValid(tile) && //only fire when at 100% power capacity
tile.entity.power.satisfaction >= powerPercentageUsed &&
link.block().itemCapacity - link.entity.items.total() >= minDistribute && entity.reload <= 0.0001f){
MassDriverEntity other = link.entity();

View File

@ -6,7 +6,7 @@ import io.anuke.mindustry.type.Liquid;
public class BurnerGenerator extends ItemLiquidGenerator{
public BurnerGenerator(String name){
super(name);
super(InputType.LiquidsAndItems, name);
}
@Override

View File

@ -1,11 +1,12 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.Liquid;
public class DecayGenerator extends ItemGenerator{
public class DecayGenerator extends ItemLiquidGenerator{
public DecayGenerator(String name){
super(name);
super(InputType.ItemsOnly, name);
hasItems = true;
hasLiquids = false;
}

View File

@ -9,13 +9,13 @@ import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.production.GenericCrafter.GenericCrafterEntity;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class FusionReactor extends PowerGenerator{
protected int plasmas = 4;
protected float maxPowerProduced = 2f;
protected float warmupSpeed = 0.001f;
protected Color plasma1 = Color.valueOf("ffd06b"), plasma2 = Color.valueOf("ff361b");
@ -25,33 +25,27 @@ public class FusionReactor extends PowerGenerator{
super(name);
hasPower = true;
hasLiquids = true;
powerCapacity = 100f;
powerProduction = 2.0f;
liquidCapacity = 30f;
hasItems = true;
}
@Override
public void setStats(){
super.setStats();
stats.add(BlockStat.basePowerGeneration, maxPowerProduced * 60f, StatUnit.powerSecond);
}
@Override
public void update(Tile tile){
FusionReactorEntity entity = tile.entity();
float increaseOrDecrease = 1.0f;
if(entity.cons.valid()){
entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, warmupSpeed);
}else{
entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.01f);
increaseOrDecrease = -1.0f;
}
float powerAdded = Math.min(powerCapacity - entity.power.amount, maxPowerProduced * Mathf.pow(entity.warmup, 4f) * Time.delta());
entity.power.amount += powerAdded;
entity.totalProgress += entity.warmup * Time.delta();
float efficiencyAdded = Mathf.pow(entity.warmup, 4f) * Time.delta();
entity.productionEfficiency = Mathf.clamp(entity.productionEfficiency + efficiencyAdded * increaseOrDecrease);
tile.entity.power.graph.update();
super.update(tile);
}
@Override
@ -95,7 +89,7 @@ public class FusionReactor extends PowerGenerator{
Draw.rect(name + "-top", tile.drawx(), tile.drawy());
Draw.color(ind1, ind2, entity.warmup + Mathf.absin(entity.totalProgress, 3f, entity.warmup * 0.5f));
Draw.color(ind1, ind2, entity.warmup + Mathf.absin(entity.productionEfficiency, 3f, entity.warmup * 0.5f));
Draw.rect(name + "-light", tile.drawx(), tile.drawy());
Draw.color();
@ -122,7 +116,19 @@ public class FusionReactor extends PowerGenerator{
//TODO catastrophic failure
}
public static class FusionReactorEntity extends GenericCrafterEntity{
public static class FusionReactorEntity extends GeneratorEntity{
public float warmup;
@Override
public void write(DataOutput stream) throws IOException{
super.write(stream);
stream.writeFloat(warmup);
}
@Override
public void read(DataInput stream) throws IOException{
super.read(stream);
warmup = stream.readFloat();
}
}
}

View File

@ -1,127 +0,0 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumeItemFilter;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
import io.anuke.arc.entities.Effects;
import io.anuke.arc.entities.Effects.Effect;
import io.anuke.arc.util.Time;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Mathf;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.tilesize;
public abstract class ItemGenerator extends PowerGenerator{
protected float minItemEfficiency = 0.2f;
protected float powerOutput;
protected float itemDuration = 70f;
protected Effect generateEffect = BlockFx.generatespark, explodeEffect =
BlockFx.generatespark;
protected Color heatColor = Color.valueOf("ff9b59");
protected TextureRegion topRegion;
public ItemGenerator(String name){
super(name);
itemCapacity = 20;
hasItems = true;
consumes.add(new ConsumeItemFilter(item -> getItemEfficiency(item) >= minItemEfficiency)).update(false).optional(true);
}
@Override
public void load(){
super.load();
topRegion = Core.atlas.find(name + "-top");
}
@Override
public void setStats(){
super.setStats();
stats.add(BlockStat.basePowerGeneration, powerOutput * 60f * 0.5f, StatUnit.powerSecond);
}
@Override
public void draw(Tile tile){
super.draw(tile);
GeneratorEntity entity = tile.entity();
if(entity.generateTime > 0){
Draw.color(heatColor);
float alpha = (entity.items.total() > 0 ? 1f : Mathf.clamp(entity.generateTime));
alpha = alpha * 0.7f + Mathf.absin(Time.time(), 12f, 0.3f) * alpha;
Draw.alpha(alpha);
Draw.rect(topRegion, tile.drawx(), tile.drawy());
Draw.reset();
}
}
@Override
public boolean acceptItem(Item item, Tile tile, Tile source){
return getItemEfficiency(item) >= minItemEfficiency && tile.entity.items.total() < itemCapacity;
}
@Override
public void update(Tile tile){
ItemGeneratorEntity entity = tile.entity();
float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency;
if(entity.generateTime <= 0f && entity.items.total() > 0){
Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f));
Item item = entity.items.take();
entity.efficiency = getItemEfficiency(item);
entity.explosiveness = item.explosiveness;
entity.generateTime = 1f;
}
entity.power.graph.update();
if(entity.generateTime > 0f){
entity.generateTime -= 1f / itemDuration * entity.delta();
entity.power.amount += maxPower;
entity.generateTime = Mathf.clamp(entity.generateTime);
if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){
//this block is run last so that in the event of a block destruction, no code relies on the block type
entity.damage(Mathf.random(8f));
Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f));
}
}
}
protected abstract float getItemEfficiency(Item item);
@Override
public TileEntity newEntity(){
return new ItemGeneratorEntity();
}
public static class ItemGeneratorEntity extends GeneratorEntity{
public float efficiency;
public float explosiveness;
@Override
public void write(DataOutput stream) throws IOException{
stream.writeFloat(efficiency);
}
@Override
public void read(DataInput stream) throws IOException{
efficiency = stream.readFloat();
}
}
}

View File

@ -1,107 +1,186 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.arc.Core;
import io.anuke.arc.entities.Effects;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumeItemFilter;
import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter;
import io.anuke.arc.entities.Effects;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Mathf;
import static io.anuke.mindustry.Vars.content;
import static io.anuke.mindustry.Vars.tilesize;
public abstract class ItemLiquidGenerator extends ItemGenerator{
/**
* Power generation block which can use items, liquids or both as input sources for power production.
* Liquids will take priority over items.
*/
public class ItemLiquidGenerator extends PowerGenerator{
protected float minItemEfficiency = 0.2f;
/** The time in number of ticks during which a single item will produce power. */
protected float itemDuration = 70f;
protected float minLiquidEfficiency = 0.2f;
protected float powerPerLiquid = 0.13f;
/**Maximum liquid used per frame.*/
/** Maximum liquid used per frame. */
protected float maxLiquidGenerate = 0.4f;
public ItemLiquidGenerator(String name){
super(name);
hasLiquids = true;
liquidCapacity = 10f;
protected Effects.Effect generateEffect = BlockFx.generatespark;
protected Effects.Effect explodeEffect = BlockFx.generatespark;
protected Color heatColor = Color.valueOf("ff9b59");
protected TextureRegion topRegion;
consumes.add(new ConsumeLiquidFilter(liquid -> getLiquidEfficiency(liquid) >= minLiquidEfficiency, 0.001f, true)).update(false).optional(true);
public enum InputType{
ItemsOnly,
LiquidsOnly,
LiquidsAndItems
}
public ItemLiquidGenerator(InputType inputType, String name){
super(name);
this.hasItems = inputType != InputType.LiquidsOnly;
this.hasLiquids = inputType != InputType.ItemsOnly;
if(hasItems){
itemCapacity = 20;
consumes.add(new ConsumeItemFilter(item -> getItemEfficiency(item) >= minItemEfficiency)).update(false).optional(true);
}
if(hasLiquids){
liquidCapacity = 10f;
consumes.add(new ConsumeLiquidFilter(liquid -> getLiquidEfficiency(liquid) >= minLiquidEfficiency, 0.001f, true)).update(false).optional(true);
}
}
@Override
public void init(){
super.init();
public void load(){
super.load();
if(hasItems){
topRegion = Core.atlas.find(name + "-top");
}
}
@Override
public void update(Tile tile){
ItemGeneratorEntity entity = tile.entity();
ItemLiquidGeneratorEntity entity = tile.entity();
entity.power.graph.update();
// Note: Do not use this delta when calculating the amount of power or the power efficiency, but use it for resource consumption if necessary.
// Power amount is delta'd by PowerGraph class already.
float calculationDelta = entity.delta();
if(!entity.cons.valid()){
entity.productionEfficiency = 0.0f;
return;
}
Liquid liquid = null;
for(Liquid other : content.liquids()){
if(entity.liquids.get(other) >= 0.001f && getLiquidEfficiency(other) >= minLiquidEfficiency){
if(hasLiquids && entity.liquids.get(other) >= 0.001f && getLiquidEfficiency(other) >= minLiquidEfficiency){
liquid = other;
break;
}
}
//liquid takes priority over solids
if(liquid != null && entity.liquids.get(liquid) >= 0.001f && entity.cons.valid()){
float powerPerLiquid = getLiquidEfficiency(liquid) * this.powerPerLiquid;
float used = Math.min(entity.liquids.get(liquid), maxLiquidGenerate * entity.delta());
used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid);
if(hasLiquids && liquid != null && entity.liquids.get(liquid) >= 0.001f){
float baseLiquidEfficiency = getLiquidEfficiency(liquid);
float maximumPossible = maxLiquidGenerate * calculationDelta;
float used = Math.min(entity.liquids.get(liquid) * calculationDelta, maximumPossible);
entity.liquids.remove(liquid, used);
entity.power.amount += used * powerPerLiquid;
// Note: 0.5 = 100%. PowerGraph will multiply this efficiency by two on its own.
entity.productionEfficiency = Mathf.clamp(baseLiquidEfficiency * used / maximumPossible);
if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){
Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f));
}
}else if(entity.cons.valid()){
float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency;
}else if(hasItems){
// No liquids accepted or none supplied, try using items if accepted
if(entity.generateTime <= 0f && entity.items.total() > 0){
Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f));
Item item = entity.items.take();
entity.efficiency = getItemEfficiency(item);
entity.productionEfficiency = getItemEfficiency(item);
entity.explosiveness = item.explosiveness;
entity.generateTime = 1f;
}
if(entity.generateTime > 0f){
entity.generateTime -= 1f / itemDuration * entity.delta();
entity.power.amount += maxPower;
entity.generateTime = Mathf.clamp(entity.generateTime);
entity.generateTime -= Math.min(1f / itemDuration * entity.delta(), entity.generateTime);
if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){
//this block is run last so that in the event of a block destruction, no code relies on the block type
entity.damage(Mathf.random(8f));
Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f));
}
}else{
entity.productionEfficiency = 0.0f;
}
}
super.update(tile);
}
@Override
public boolean acceptItem(Item item, Tile tile, Tile source){
return hasItems && getItemEfficiency(item) >= minItemEfficiency && tile.entity.items.total() < itemCapacity;
}
@Override
public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){
return hasLiquids && getLiquidEfficiency(liquid) >= minLiquidEfficiency && tile.entity.liquids.get(liquid) < liquidCapacity;
}
@Override
public void draw(Tile tile){
super.draw(tile);
TileEntity entity = tile.entity();
GeneratorEntity entity = tile.entity();
Draw.color(entity.liquids.current().color);
Draw.alpha(entity.liquids.currentAmount() / liquidCapacity);
drawLiquidCenter(tile);
Draw.color();
}
if(hasItems){
if(entity.generateTime > 0){
Draw.color(heatColor);
float alpha = (entity.items.total() > 0 ? 1f : Mathf.clamp(entity.generateTime));
alpha = alpha * 0.7f + Mathf.absin(Time.time(), 12f, 0.3f) * alpha;
Draw.alpha(alpha);
Draw.rect(topRegion, tile.drawx(), tile.drawy());
Draw.reset();
}
}
@Override
public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){
return getLiquidEfficiency(liquid) >= minLiquidEfficiency && tile.entity.liquids.get(liquid) < liquidCapacity;
if(hasLiquids){
Draw.color(entity.liquids.current().color);
Draw.alpha(entity.liquids.currentAmount() / liquidCapacity);
drawLiquidCenter(tile);
Draw.color();
}
}
public void drawLiquidCenter(Tile tile){
Draw.rect("blank", tile.drawx(), tile.drawy(), 2, 2);
}
protected abstract float getLiquidEfficiency(Liquid liquid);
protected float getItemEfficiency(Item item){
return 0.0f;
}
protected float getLiquidEfficiency(Liquid liquid){
return 0.0f;
}
@Override
public TileEntity newEntity(){
return new ItemLiquidGeneratorEntity();
}
public static class ItemLiquidGeneratorEntity extends GeneratorEntity{
public float explosiveness;
}
}

View File

@ -1,85 +0,0 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.ItemGenerator.ItemGeneratorEntity;
import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter;
import io.anuke.arc.entities.Effects;
import io.anuke.arc.entities.Effects.Effect;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Mathf;
public abstract class LiquidGenerator extends PowerGenerator{
protected float minEfficiency = 0.2f;
protected float powerPerLiquid;
/**Maximum liquid used per frame.*/
protected float maxLiquidGenerate;
protected Effect generateEffect = BlockFx.generatespark;
public LiquidGenerator(String name){
super(name);
liquidCapacity = 30f;
hasLiquids = true;
}
@Override
public void setStats(){
consumes.add(new ConsumeLiquidFilter(liquid -> getEfficiency(liquid) >= minEfficiency, maxLiquidGenerate)).update(false);
super.setStats();
}
@Override
public void draw(Tile tile){
super.draw(tile);
TileEntity entity = tile.entity();
Draw.color(entity.liquids.current().color);
Draw.alpha(entity.liquids.total() / liquidCapacity);
drawLiquidCenter(tile);
Draw.color();
}
public void drawLiquidCenter(Tile tile){
Draw.rect("blank", tile.drawx(), tile.drawy(), 2, 2);
}
@Override
public void update(Tile tile){
TileEntity entity = tile.entity();
if(entity.liquids.get(entity.liquids.current()) >= 0.001f){
float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid;
float used = Math.min(entity.liquids.currentAmount(), maxLiquidGenerate * entity.delta());
used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid);
entity.liquids.remove(entity.liquids.current(), used);
entity.power.amount += used * powerPerLiquid;
if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){
Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f));
}
}
tile.entity.power.graph.update();
}
@Override
public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){
return getEfficiency(liquid) >= minEfficiency && super.acceptLiquid(tile, source, liquid, amount);
}
@Override
public TileEntity newEntity(){
return new ItemGeneratorEntity();
}
/**
* Returns an efficiency value for the specified liquid.
* Greater efficiency means more power generation.
* If a liquid's efficiency is below {@link #minEfficiency}, it is not accepted.
*/
protected abstract float getEfficiency(Liquid liquid);
}

View File

@ -1,24 +1,27 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
public class LiquidHeatGenerator extends LiquidGenerator{
public class LiquidHeatGenerator extends ItemLiquidGenerator{
public LiquidHeatGenerator(String name){
super(name);
super(InputType.LiquidsOnly, name);
}
@Override
public void setStats(){
super.setStats();
stats.add(BlockStat.basePowerGeneration, maxLiquidGenerate * powerPerLiquid * 60f * 0.5f, StatUnit.powerSecond);
stats.remove(BlockStat.basePowerGeneration);
// Right now, Lava is the only thing that can be used.
stats.add(BlockStat.basePowerGeneration, powerProduction * getLiquidEfficiency(Liquids.lava) / maxLiquidGenerate * 60f, StatUnit.powerSecond);
}
@Override
protected float getEfficiency(Liquid liquid){
protected float getLiquidEfficiency(Liquid liquid){
return liquid.temperature - 0.5f;
}
}

View File

@ -33,7 +33,6 @@ public class NuclearReactor extends PowerGenerator{
protected Color coolColor = new Color(1, 1, 1, 0f);
protected Color hotColor = Color.valueOf("ff9575a3");
protected int fuelUseTime = 120; //time to consume 1 fuel
protected float powerMultiplier = 0.45f; //power per frame, depends on full capacity
protected float heating = 0.013f; //heating per frame
protected float coolantPower = 0.015f; //how much heat decreases per coolant unit
protected float smokeThreshold = 0.3f; //threshold at which block starts smoking
@ -48,7 +47,6 @@ public class NuclearReactor extends PowerGenerator{
super(name);
itemCapacity = 30;
liquidCapacity = 50;
powerCapacity = 80f;
hasItems = true;
hasLiquids = true;
@ -67,7 +65,10 @@ public class NuclearReactor extends PowerGenerator{
public void setStats(){
super.setStats();
stats.add(BlockStat.inputLiquid, new LiquidFilterValue(liquid -> liquid.temperature <= 0.5f));
stats.add(BlockStat.basePowerGeneration, powerMultiplier * 60f * 0.5f, StatUnit.powerSecond);
stats.remove(BlockStat.basePowerGeneration);
// Display the power which will be produced at 50% efficiency
stats.add(BlockStat.basePowerGeneration, powerProduction * 60f * 0.5f, StatUnit.powerSecond);
}
@Override
@ -76,11 +77,11 @@ public class NuclearReactor extends PowerGenerator{
int fuel = entity.items.get(consumes.item());
float fullness = (float) fuel / itemCapacity;
entity.productionEfficiency = fullness / 2.0f; // Currently, efficiency of 0.5 = 100%
if(fuel > 0){
entity.heat += fullness * heating * Math.min(entity.delta(), 4f);
entity.power.amount += powerMultiplier * fullness * entity.delta();
entity.power.amount = Mathf.clamp(entity.power.amount, 0f, powerCapacity);
if(entity.timer.get(timerFuel, fuelUseTime)){
entity.items.remove(consumes.item(), 1);
}
@ -115,7 +116,7 @@ public class NuclearReactor extends PowerGenerator{
if(entity.heat >= 0.999f){
entity.kill();
}else{
tile.entity.power.graph.update();
super.update(tile);
}
}

View File

@ -1,10 +1,20 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.arc.collection.EnumSet;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class PowerGenerator extends PowerDistributor{
/** The amount of power produced per tick in case of an efficiency of 1.0, which currently represents 200%. */
protected float powerProduction;
public BlockStat generationType = BlockStat.basePowerGeneration;
public PowerGenerator(String name){
super(name);
@ -12,6 +22,20 @@ public class PowerGenerator extends PowerDistributor{
flags = EnumSet.of(BlockFlag.producer);
}
@Override
public void setStats(){
super.setStats();
// Divide power production by two since that is what is produced at an efficiency of 0.5, which currently represents 100%
stats.add(generationType, powerProduction * 60.0f / 2.0f, StatUnit.powerSecond);
}
@Override
public float getPowerProduction(Tile tile){
// While 0.5 efficiency currently reflects 100%, we do not need to multiply by any factor since powerProduction states the
// power which would be produced at 1.0 efficiency
return powerProduction * tile.<GeneratorEntity>entity().productionEfficiency;
}
@Override
public boolean outputsItems(){
return false;
@ -24,5 +48,17 @@ public class PowerGenerator extends PowerDistributor{
public static class GeneratorEntity extends TileEntity{
public float generateTime;
/** The efficiency of the producer. Currently, an efficiency of 0.5 means 100% */
public float productionEfficiency = 0.0f;
@Override
public void write(DataOutput stream) throws IOException{
stream.writeFloat(productionEfficiency);
}
@Override
public void read(DataInput stream) throws IOException{
productionEfficiency = stream.readFloat();
}
}
}

View File

@ -5,7 +5,11 @@ import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.IntSet;
import io.anuke.arc.collection.ObjectSet;
import io.anuke.arc.collection.Queue;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.Consume;
import io.anuke.mindustry.world.consumers.ConsumePower;
import io.anuke.mindustry.world.consumers.Consumers;
public class PowerGraph{
private final static Queue<Tile> queue = new Queue<>();
@ -15,6 +19,7 @@ public class PowerGraph{
private final ObjectSet<Tile> producers = new ObjectSet<>();
private final ObjectSet<Tile> consumers = new ObjectSet<>();
private final ObjectSet<Tile> batteries = new ObjectSet<>();
private final ObjectSet<Tile> all = new ObjectSet<>();
private long lastFrameUpdated;
@ -29,71 +34,127 @@ public class PowerGraph{
return graphID;
}
public float getPowerProduced(){
float powerProduced = 0f;
for(Tile producer : producers){
powerProduced += producer.block().getPowerProduction(producer) * producer.entity.delta();
}
return powerProduced;
}
public float getPowerNeeded(){
float powerNeeded = 0f;
for(Tile consumer : consumers){
Consumers consumes = consumer.block().consumes;
if(consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumes.get(ConsumePower.class);
if(otherConsumersAreValid(consumer, consumePower)){
powerNeeded += consumePower.requestedPower(consumer.block(), consumer.entity) * consumer.entity.delta();
}
}
}
return powerNeeded;
}
public float getBatteryStored(){
float totalAccumulator = 0f;
for(Tile battery : batteries){
Consumers consumes = battery.block().consumes;
if(consumes.has(ConsumePower.class)){
totalAccumulator += battery.entity.power.satisfaction * consumes.get(ConsumePower.class).powerCapacity;
}
}
return totalAccumulator;
}
public float getBatteryCapacity(){
float totalCapacity = 0f;
for(Tile battery : batteries){
Consumers consumes = battery.block().consumes;
if(consumes.has(ConsumePower.class)){
totalCapacity += consumes.get(ConsumePower.class).requestedPower(battery.block(), battery.entity) * battery.entity.delta();
}
}
return totalCapacity;
}
public float useBatteries(float needed){
float stored = getBatteryStored();
if(Mathf.isEqual(stored, 0f)){ return 0f; }
float used = Math.min(stored, needed);
float consumedPowerPercentage = Math.min(1.0f, needed / stored);
for(Tile battery : batteries){
Consumers consumes = battery.block().consumes;
if(consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumes.get(ConsumePower.class);
if(consumePower.powerCapacity > 0f){
battery.entity.power.satisfaction = Math.max(0.0f, battery.entity.power.satisfaction - consumedPowerPercentage);
}
}
}
return used;
}
public float chargeBatteries(float excess){
float capacity = getBatteryCapacity();
if(Mathf.isEqual(capacity, 0f)){ return 0f; }
for(Tile battery : batteries){
Consumers consumes = battery.block().consumes;
if(consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumes.get(ConsumePower.class);
if(consumePower.powerCapacity > 0f){
float additionalPowerPercentage = Math.min(1.0f, excess / consumePower.powerCapacity);
battery.entity.power.satisfaction = Math.min(1.0f, battery.entity.power.satisfaction + additionalPowerPercentage);
}
}
}
return Math.min(excess, capacity);
}
public void distributePower(float needed, float produced){
if(Mathf.isEqual(needed, 0f)){ return; }
float coverage = Math.min(1, produced / needed);
for(Tile consumer : consumers){
Consumers consumes = consumer.block().consumes;
if(consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumes.get(ConsumePower.class);
if(!otherConsumersAreValid(consumer, consumePower)){
consumer.entity.power.satisfaction = 0.0f; // Only supply power if the consumer would get valid that way
}else{
if(consumePower.isBuffered){
// Add an equal percentage of power to all buffers, based on the global power coverage in this graph
float maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage * consumer.entity.delta();
consumer.entity.power.satisfaction = Mathf.clamp(consumer.entity.power.satisfaction + maximumRate / consumePower.powerCapacity);
}else{
consumer.entity.power.satisfaction = coverage;
}
}
}
}
}
public void update(){
if(Core.graphics.getFrameId() == lastFrameUpdated || consumers.size == 0 || producers.size == 0){
if(Core.graphics.getFrameId() == lastFrameUpdated || consumers.size == 0 && producers.size == 0 && batteries.size == 0){
return;
}
lastFrameUpdated = Core.graphics.getFrameId();
boolean charge = false;
float powerNeeded = getPowerNeeded();
float powerProduced = getPowerProduced();
float totalInput = 0f;
float bufferInput = 0f;
for(Tile producer : producers){
if(producer.block().consumesPower){
bufferInput += producer.entity.power.amount;
}else{
totalInput += producer.entity.power.amount;
if(!Mathf.isEqual(powerNeeded, powerProduced)){
if(powerNeeded > powerProduced){
powerProduced += useBatteries(powerNeeded - powerProduced);
}else if(powerProduced > powerNeeded){
powerProduced -= chargeBatteries(powerProduced - powerNeeded);
}
}
float maxOutput = 0f;
float bufferOutput = 0f;
for(Tile consumer : consumers){
if(consumer.block().outputsPower){
bufferOutput += consumer.block().powerCapacity - consumer.entity.power.amount;
}else{
maxOutput += consumer.block().powerCapacity - consumer.entity.power.amount;
}
}
if(maxOutput < totalInput){
charge = true;
}
if(totalInput + bufferInput <= 0.0001f || maxOutput + bufferOutput <= 0.0001f){
return;
}
float bufferUsed;
if(charge){
bufferUsed = Math.min((totalInput - maxOutput) / bufferOutput, 1f);
}else{
bufferUsed = Math.min((maxOutput - totalInput) / bufferInput, 1f);
}
float inputUsed = charge ? Math.min((maxOutput + bufferOutput) / totalInput, 1f) : 1f;
for(Tile producer : producers){
if(producer.block().consumesPower){
if(!charge){
producer.entity.power.amount -= producer.entity.power.amount * bufferUsed;
}
continue;
}
producer.entity.power.amount -= producer.entity.power.amount * inputUsed;
}
float outputSatisfied = charge ? 1f : Math.min((totalInput + bufferInput) / maxOutput, 1f);
for(Tile consumer : consumers){
if(consumer.block().outputsPower){
if(charge){
consumer.entity.power.amount += (consumer.block().powerCapacity - consumer.entity.power.amount) * bufferUsed;
}
continue;
}
consumer.entity.power.amount += (consumer.block().powerCapacity - consumer.entity.power.amount) * outputSatisfied;
}
distributePower(powerNeeded, powerProduced);
}
public void add(PowerGraph graph){
@ -106,22 +167,29 @@ public class PowerGraph{
tile.entity.power.graph = this;
all.add(tile);
if(tile.block().outputsPower){
if(tile.block().outputsPower && tile.block().consumesPower){
batteries.add(tile);
}else if(tile.block().outputsPower){
producers.add(tile);
}
if(tile.block().consumesPower){
}else if(tile.block().consumesPower){
consumers.add(tile);
}
}
public void clear(){
for(Tile other : all){
if(other.entity != null && other.entity.power != null) other.entity.power.graph = null;
if(other.entity != null && other.entity.power != null){
if(other.block().consumes.has(ConsumePower.class) && !other.block().consumes.get(ConsumePower.class).isBuffered){
// Reset satisfaction to zero in case of direct consumer. There is no reason to clear power from buffered consumers.
other.entity.power.satisfaction = 0.0f;
}
other.entity.power.graph = null;
}
}
all.clear();
producers.clear();
consumers.clear();
batteries.clear();
}
public void reflow(Tile tile){
@ -146,7 +214,7 @@ public class PowerGraph{
closedSet.clear();
for(Tile other : tile.block().getPowerConnections(tile, outArray1)){
if(other.entity.power == null || other.entity.power.graph != null) continue;
if(other.entity.power == null || other.entity.power.graph != null){ continue; }
PowerGraph graph = new PowerGraph();
queue.clear();
queue.addLast(other);
@ -161,17 +229,29 @@ public class PowerGraph{
}
}
}
// Update the graph once so direct consumers without any connected producer lose their power
graph.update();
}
}
private boolean otherConsumersAreValid(Tile tile, Consume consumePower){
for(Consume cons : tile.block().consumes.all()){
if(cons != consumePower && !cons.isOptional() && !cons.valid(tile.block(), tile.entity())){
return false;
}
}
return true;
}
@Override
public String toString(){
return "PowerGraph{" +
"producers=" + producers +
", consumers=" + consumers +
", all=" + all +
", lastFrameUpdated=" + lastFrameUpdated +
", graphID=" + graphID +
'}';
"producers=" + producers +
", consumers=" + consumers +
", batteries=" + batteries +
", all=" + all +
", lastFrameUpdated=" + lastFrameUpdated +
", graphID=" + graphID +
'}';
}
}

View File

@ -39,7 +39,6 @@ public class PowerNode extends PowerBlock{
super(name);
expanded = true;
layer = Layer.power;
powerCapacity = 5f;
configurable = true;
consumesPower = false;
outputsPower = false;

View File

@ -1,34 +1,30 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.meta.StatUnit;
import io.anuke.arc.util.Time;
import io.anuke.arc.collection.EnumSet;
public class SolarGenerator extends PowerGenerator{
/**
* power generated per frame
*/
protected float generation = 0.005f;
public SolarGenerator(String name){
super(name);
// Remove the BlockFlag.producer flag to make this a lower priority target than other generators.
flags = EnumSet.of();
}
@Override
public void setStats(){
super.setStats();
stats.add(BlockStat.basePowerGeneration, generation * 60f, StatUnit.powerSecond);
// Solar Generators don't really have an efficiency (yet), so for them 100% = 1.0f
stats.remove(generationType);
stats.add(generationType, powerProduction * 60.0f, StatUnit.powerSecond);
}
@Override
public void update(Tile tile){
addPower(tile, generation * Time.delta());
tile.entity.power.graph.update();
public TileEntity newEntity(){
return new PowerGenerator.GeneratorEntity(){{
productionEfficiency = 1.0f;
}};
}
}

View File

@ -190,6 +190,9 @@ public class Drill extends Block{
if(entity.consumed(ConsumeLiquid.class) && !liquidRequired){
speed = liquidBoostIntensity;
}
if(hasPower){
speed *= entity.power.satisfaction; // Drill slower when not at full power
}
entity.warmup = Mathf.lerpDelta(entity.warmup, speed, warmupSpeed);
entity.progress += entity.delta()

View File

@ -65,7 +65,7 @@ public class Fracker extends SolidPump{
if(entity.cons.valid() && entity.accumulator < itemUseTime){
super.update(tile);
entity.accumulator += entity.delta();
entity.accumulator += entity.delta() * entity.power.satisfaction;
}else{
tryDumpLiquid(tile, result);
}

View File

@ -74,7 +74,7 @@ public class GenericCrafter extends Block{
if(entity.cons.valid() && tile.entity.items.get(output) < itemCapacity){
entity.progress += 1f / craftTime * entity.delta();
entity.progress += getProgressIncrease(entity, craftTime);
entity.totalProgress += entity.delta();
entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f);

View File

@ -25,7 +25,8 @@ public class Incinerator extends Block{
update = true;
solid = true;
consumes.power(0.05f);
// Incinerator has no speed which could be adjusted, so it will only operate fully powered for now
consumes.power(0.05f, 1.0f);
}
@Override

View File

@ -49,6 +49,9 @@ public class LiquidMixer extends LiquidBlock{
if(tile.entity.cons.valid()){
float use = Math.min(consumes.get(ConsumeLiquid.class).used() * entity.delta(), liquidCapacity - entity.liquids.get(outputLiquid));
if(hasPower){
use *= entity.power.satisfaction; // Produce less liquid if power is not maxed
}
entity.accumulator += use;
entity.liquids.add(outputLiquid, use);
for(int i = 0; i < (int) (entity.accumulator / liquidPerItem); i++){

View File

@ -65,7 +65,7 @@ public class PowerCrafter extends Block{
GenericCrafterEntity entity = tile.entity();
if(entity.cons.valid()){
entity.progress += 1f / craftTime * entity.delta();
entity.progress += getProgressIncrease(entity, craftTime);
entity.totalProgress += entity.delta();
}

View File

@ -111,7 +111,7 @@ public class PowerSmelter extends PowerBlock{
}
}
entity.craftTime += entity.delta();
entity.craftTime += entity.delta() * entity.power.satisfaction;
if(entity.items.get(result) >= itemCapacity //output full
|| entity.heat <= minHeat //not burning

View File

@ -96,6 +96,9 @@ public class Pump extends LiquidBlock{
if(tile.entity.cons.valid() && liquidDrop != null){
float maxPump = Math.min(liquidCapacity - tile.entity.liquids.total(), tiles * pumpAmount * tile.entity.delta());
if(hasPower){
maxPump *= tile.entity.power.satisfaction; // Produce slower if not at full power
}
tile.entity.liquids.add(liquidDrop, maxPump);
}

View File

@ -88,7 +88,7 @@ public class Separator extends Block{
entity.totalProgress += entity.warmup * entity.delta();
if(entity.cons.valid()){
entity.progress += 1f / filterTime*entity.delta();
entity.progress += getProgressIncrease(entity, filterTime);
entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f);
}else{
entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.02f);

View File

@ -77,7 +77,7 @@ public class SolidPump extends Pump{
}
if(tile.entity.cons.valid() && typeLiquid(tile) < liquidCapacity - 0.001f){
float maxPump = Math.min(liquidCapacity - typeLiquid(tile), pumpAmount * entity.delta() * fraction);
float maxPump = Math.min(liquidCapacity - typeLiquid(tile), pumpAmount * entity.delta() * fraction * entity.power.satisfaction);
tile.entity.liquids.add(result, maxPump);
entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f);
if(Mathf.chance(entity.delta() * updateEffectChance))

View File

@ -24,7 +24,6 @@ import io.anuke.mindustry.graphics.Shaders;
import io.anuke.mindustry.type.Mech;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumePowerExact;
import io.anuke.mindustry.world.meta.BlockStat;
import java.io.DataInput;
@ -37,6 +36,7 @@ import static io.anuke.mindustry.Vars.tilesize;
public class MechPad extends Block{
protected Mech mech;
protected float buildTime = 60 * 5;
protected float requiredSatisfaction = 1f;
protected TextureRegion openRegion;
@ -49,16 +49,9 @@ public class MechPad extends Block{
@Override
public void init(){
consumes.add(new ConsumePowerExact(powerCapacity * 0.8f));
super.init();
}
@Override
public void setStats(){
super.setStats();
stats.remove(BlockStat.powerUse);
}
@Override
public boolean shouldConsume(Tile tile){
return false;
@ -66,10 +59,14 @@ public class MechPad extends Block{
@Remote(targets = Loc.both, called = Loc.server)
public static void onMechFactoryTap(Player player, Tile tile){
if(player == null || !checkValidTap(tile, player)) return;
if(player == null || !checkValidTap(tile, player) || !(tile.block() instanceof MechPad)) return;
MechFactoryEntity entity = tile.entity();
entity.power.amount = 0f;
MechPad pad = (MechPad)tile.block();
if(entity.power.satisfaction < pad.requiredSatisfaction) return;
entity.power.satisfaction -= Math.min(entity.power.satisfaction, pad.requiredSatisfaction);
player.beginRespawning(entity);
}
@ -102,7 +99,7 @@ public class MechPad extends Block{
protected static boolean checkValidTap(Tile tile, Player player){
MechFactoryEntity entity = tile.entity();
return Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f &&
return Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f &&
Math.abs(player.y - tile.drawy()) <= tile.block().size * tilesize / 2f && entity.cons.valid() && entity.player == null;
}

View File

@ -34,7 +34,8 @@ import static io.anuke.mindustry.Vars.world;
public class Reconstructor extends Block{
protected float departTime = 30f;
protected float arriveTime = 40f;
protected float powerPerTeleport = 5f;
/** Stores the percentage of buffered power to be used upon teleporting. */
protected float powerPerTeleport = 0.5f;
protected Effect arriveEffect = Fx.spawn;
protected TextureRegion openRegion;
@ -44,13 +45,14 @@ public class Reconstructor extends Block{
solidifes = true;
hasPower = true;
configurable = true;
consumes.powerBuffered(30f);
}
protected static boolean checkValidTap(Tile tile, ReconstructorEntity entity, Player player){
return validLink(tile, entity.link) &&
Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f &&
Math.abs(player.y - tile.drawy()) <= tile.block().size * tilesize / 2f &&
entity.current == null && entity.power.amount >= ((Reconstructor) tile.block()).powerPerTeleport;
entity.current == null && entity.power.satisfaction >= ((Reconstructor) tile.block()).powerPerTeleport;
}
protected static boolean validLink(Tile tile, int position){
@ -75,13 +77,13 @@ public class Reconstructor extends Block{
public static void reconstructPlayer(Player player, Tile tile){
ReconstructorEntity entity = tile.entity();
if(!checkValidTap(tile, entity, player) || entity.power.amount < ((Reconstructor) tile.block()).powerPerTeleport)
if(!checkValidTap(tile, entity, player) || entity.power.satisfaction < ((Reconstructor) tile.block()).powerPerTeleport)
return;
entity.departing = true;
entity.current = player;
entity.solid = false;
entity.power.amount -= ((Reconstructor) tile.block()).powerPerTeleport;
entity.power.satisfaction -= Math.min(entity.power.satisfaction, ((Reconstructor) tile.block()).powerPerTeleport);
entity.updateTime = 1f;
entity.set(tile.drawx(), tile.drawy());
player.rotation = 90f;
@ -242,13 +244,13 @@ public class Reconstructor extends Block{
entity.updateTime -= Time.delta() / departTime;
if(entity.updateTime <= 0f){
//no power? death.
if(other.power.amount < powerPerTeleport){
if(other.power.satisfaction < powerPerTeleport){
entity.current.setDead(true);
//entity.current.setRespawning(false);
entity.current = null;
return;
}
other.power.amount -= powerPerTeleport;
other.power.satisfaction -= Math.min(other.power.satisfaction, powerPerTeleport);
other.current = entity.current;
other.departing = false;
other.current.set(other.x, other.y);
@ -272,8 +274,8 @@ public class Reconstructor extends Block{
if(validLink(tile, entity.link)){
Tile other = world.tile(entity.link);
if(other.entity.power.amount >= powerPerTeleport && Units.anyEntities(tile, 4f, unit -> unit.getTeam() == entity.getTeam() && unit instanceof Player) &&
entity.power.amount >= powerPerTeleport){
if(other.entity.power.satisfaction >= powerPerTeleport && Units.anyEntities(tile, 4f, unit -> unit.getTeam() == entity.getTeam() && unit instanceof Player) &&
entity.power.satisfaction >= powerPerTeleport){
entity.solid = false;
stayOpen = true;
}

View File

@ -18,7 +18,10 @@ import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.graphics.Shapes;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.consumers.ConsumePower;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.StatUnit;
public class RepairPoint extends Block{
private static Rectangle rect = new Rectangle();
@ -27,6 +30,8 @@ public class RepairPoint extends Block{
protected float repairRadius = 50f;
protected float repairSpeed = 0.3f;
protected float powerPerEvent = 0.06f;
protected ConsumePower consumePower;
protected TextureRegion topRegion;
@ -38,8 +43,7 @@ public class RepairPoint extends Block{
layer = Layer.turret;
layer2 = Layer.laser;
hasPower = true;
powerCapacity = 20f;
consumes.power(0.06f);
consumePower = consumes.powerBuffered(20f);
}
@Override
@ -49,6 +53,12 @@ public class RepairPoint extends Block{
topRegion = Core.atlas.find(name + "-turret");
}
@Override
public void setStats(){
super.setStats();
stats.add(BlockStat.powerUse, powerPerEvent * 60f, StatUnit.powerSecond);
}
@Override
public void drawSelect(Tile tile){
Draw.color(Palette.accent);
@ -84,16 +94,22 @@ public class RepairPoint extends Block{
public void update(Tile tile){
RepairPointEntity entity = tile.entity();
boolean targetIsBeingRepaired = false;
if(entity.target != null && (entity.target.isDead() || entity.target.dst(tile) > repairRadius ||
entity.target.health >= entity.target.maxHealth())){
entity.target = null;
}else if(entity.target != null){
entity.target.health += repairSpeed * Time.delta() * entity.strength;
entity.target.clampHealth();
entity.rotation = Mathf.slerpDelta(entity.rotation, entity.angleTo(entity.target), 0.5f);
float relativeConsumption = powerPerEvent / consumePower.powerCapacity;
if(entity.power.satisfaction > 0.0f){
entity.target.health += repairSpeed * Time.delta() * entity.strength * Mathf.clamp(entity.power.satisfaction / relativeConsumption);
entity.target.clampHealth();
entity.rotation = Mathf.slerpDelta(entity.rotation, entity.angleTo(entity.target), 0.5f);
entity.power.satisfaction -= Math.min(entity.power.satisfaction, relativeConsumption);
targetIsBeingRepaired = true;
}
}
if(entity.target != null && entity.cons.valid()){
if(entity.target != null && targetIsBeingRepaired){
entity.strength = Mathf.lerpDelta(entity.strength, 1f, 0.08f * Time.delta());
}else{
entity.strength = Mathf.lerpDelta(entity.strength, 0f, 0.07f * Time.delta());

View File

@ -149,7 +149,7 @@ public class UnitFactory extends Block{
if(hasRequirements(entity.items, entity.buildTime / produceTime) && entity.cons.valid()){
entity.buildTime += entity.delta();
entity.buildTime += entity.delta() * entity.power.satisfaction;
entity.speedScl = Mathf.lerpDelta(entity.speedScl, 1f, 0.05f);
}else{
entity.speedScl = Mathf.lerpDelta(entity.speedScl, 0f, 0.05f);

View File

@ -1,22 +1,53 @@
package io.anuke.mindustry.world.consumers;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.meta.BlockStat;
import io.anuke.mindustry.world.meta.BlockStats;
import io.anuke.mindustry.world.meta.StatUnit;
import io.anuke.arc.scene.ui.layout.Table;
/** Consumer class for blocks which consume power while being connected to a power graph. */
public class ConsumePower extends Consume{
protected final float use;
/** The maximum amount of power which can be processed per tick. This might influence efficiency or load a buffer. */
protected final float powerPerTick;
/** The minimum power satisfaction (fraction of powerPerTick) which must be achieved before the module may work. */
public final float minimumSatisfaction;
/** The maximum power capacity in power units. */
public final float powerCapacity;
/** True if the module can store power. */
public final boolean isBuffered;
public ConsumePower(float use){
this.use = use;
protected ConsumePower(float powerPerTick, float minimumSatisfaction, float powerCapacity, boolean isBuffered){
this.powerPerTick = powerPerTick;
this.minimumSatisfaction = minimumSatisfaction;
this.powerCapacity = powerCapacity;
this.isBuffered = isBuffered;
}
/**
* Makes the owner consume powerPerTick each tick and disables it unless minimumSatisfaction (1.0 = 100%) of that power is being supplied.
* @param powerPerTick The maximum amount of power which is required per tick for 100% efficiency.
* @param minimumSatisfaction The percentage of powerPerTick which must be available for the module to work.
*/
public static ConsumePower consumePowerDirect(float powerPerTick, float minimumSatisfaction){
return new ConsumePower(powerPerTick, minimumSatisfaction, 0.0f, false);
}
/**
* Adds a power buffer to the owner which takes ticksToFill number of ticks to be filled.
* Note that this object does not remove power from the buffer.
* @param powerCapacity The maximum capacity in power units.
* @param ticksToFill The number of ticks it shall take to fill the buffer.
*/
public static ConsumePower consumePowerBuffered(float powerCapacity, float ticksToFill){
return new ConsumePower(powerCapacity / ticksToFill, 0.0f, powerCapacity, true);
}
@Override
public void buildTooltip(Table table){
// No tooltip for power
}
@Override
@ -26,21 +57,41 @@ public class ConsumePower extends Consume{
@Override
public void update(Block block, TileEntity entity){
if(entity.power == null) return;
entity.power.amount -= Math.min(use(block, entity), entity.power.amount);
// Nothing to do since PowerGraph directly updates entity.power.satisfaction
}
@Override
public boolean valid(Block block, TileEntity entity){
return entity.power != null && entity.power.amount >= use(block, entity);
if(isBuffered){
return true;
}else{
return entity.power.satisfaction >= minimumSatisfaction;
}
}
@Override
public void display(BlockStats stats){
stats.add(BlockStat.powerUse, use * 60f, StatUnit.powerSecond);
if(isBuffered){
stats.add(BlockStat.powerCapacity, powerCapacity, StatUnit.powerSecond);
}else{
stats.add(BlockStat.powerUse, powerPerTick * 60f, StatUnit.powerSecond);
}
}
protected float use(Block block, TileEntity entity){
return Math.min(use * entity.delta(), block.powerCapacity);
/**
* Retrieves the amount of power which is requested for the given block and entity.
* @param block The block which needs power.
* @param entity The entity which contains the power module.
* @return The amount of power which is requested per tick.
*/
public float requestedPower(Block block, TileEntity entity){
if(isBuffered){
// Stop requesting power once the buffer is full.
return Mathf.isEqual(entity.power.satisfaction, 1.0f) ? 0.0f : powerPerTick;
}else{
return powerPerTick;
}
}
}

View File

@ -1,16 +0,0 @@
package io.anuke.mindustry.world.consumers;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.Block;
public class ConsumePowerExact extends ConsumePower{
public ConsumePowerExact(float use){
super(use);
}
@Override
protected float use(Block block, TileEntity entity){
return this.use;
}
}

View File

@ -30,18 +30,52 @@ public class Consumers{
}
}
public ConsumePower power(float amount){
ConsumePower p = new ConsumePower(amount);
add(p);
return p;
}
public ConsumeLiquid liquid(Liquid liquid, float amount){
ConsumeLiquid c = new ConsumeLiquid(liquid, amount);
add(c);
return c;
}
/**
* Creates a consumer which directly uses power without buffering it. The module will work while at least 50% of power is supplied.
* @param powerPerTick The amount of power which is required each tick for 100% efficiency.
* @return the created consumer object.
*/
public ConsumePower power(float powerPerTick){
return power(powerPerTick, 0.5f);
}
/**
* Creates a consumer which directly uses power without buffering it. The module will work while the available power is greater than or equal to the minimumSatisfaction percentage (0..1).
* @param powerPerTick The amount of power which is required each tick for 100% efficiency.
* @return the created consumer object.
*/
public ConsumePower power(float powerPerTick, float minimumSatisfaction){
ConsumePower c = ConsumePower.consumePowerDirect(powerPerTick, minimumSatisfaction);
add(c);
return c;
}
/**
* Creates a consumer which stores power and uses it only in case of certain events (e.g. a turret firing).
* It will take 180 ticks (three second) to fill the buffer, given enough power supplied.
* @param powerCapacity The maximum capacity in power units.
*/
public ConsumePower powerBuffered(float powerCapacity){
return powerBuffered(powerCapacity, 1f);
}
/**
* Creates a consumer which stores power and uses it only in case of certain events (e.g. a turret firing).
* @param powerCapacity The maximum capacity in power units.
* @param ticksToFill The number of ticks it shall take to fill the buffer.
*/
public ConsumePower powerBuffered(float powerCapacity, float ticksToFill){
ConsumePower c = ConsumePower.consumePowerBuffered(powerCapacity, ticksToFill);
add(c);
return c;
}
public ConsumeItem item(Item item){
return item(item, 1);
}
@ -75,7 +109,7 @@ public class Consumers{
}
public Consume add(Consume consume){
map.put(consume.getClass(), consume);
map.put((consume instanceof ConsumePower ? ConsumePower.class : consume.getClass()), consume);
return consume;
}

View File

@ -8,14 +8,18 @@ import java.io.DataOutput;
import java.io.IOException;
public class PowerModule extends BlockModule{
public float amount;
/** In case of unbuffered consumers, this is the percentage (1.0f = 100%) of the demanded power which can be supplied.
* Blocks will work at a reduced efficiency if this is not equal to 1.0f.
* In case of buffered consumers, this is the percentage of power stored in relation to the maximum capacity.
*/
public float satisfaction = 0.0f;
/** Specifies power which is required additionally, e.g. while a force projector is being shot at. */
public float extraUse = 0f;
public PowerGraph graph = new PowerGraph();
public IntArray links = new IntArray();
@Override
public void write(DataOutput stream) throws IOException{
stream.writeFloat(amount);
stream.writeShort(links.size);
for(int i = 0; i < links.size; i++){
stream.writeInt(links.get(i));
@ -24,15 +28,6 @@ public class PowerModule extends BlockModule{
@Override
public void read(DataInput stream) throws IOException{
amount = stream.readFloat();
if(Float.isNaN(amount)){
amount = 0f;
}
// Workaround: If power went negative for some reason, at least fix it when reloading the map
if(amount < 0f){
amount = 0f;
}
short amount = stream.readShort();
for(int i = 0; i < amount; i++){
links.add(stream.readInt());

View File

@ -0,0 +1,55 @@
package power;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.UnitTypes;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.PowerGenerator;
import io.anuke.mindustry.world.blocks.power.PowerGraph;
import io.anuke.mindustry.world.blocks.units.UnitFactory;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** Tests for direct power consumers. */
public class DirectConsumerTests extends PowerTestFixture{
@Test
void noPowerRequestedWithNoItems(){
testUnitFactory(0, 0, 0.08f, 0.08f, 0.0f);
}
@Test
void noPowerRequestedWithInsufficientItems(){
testUnitFactory(30, 0, 0.08f, 0.08f, 0.0f);
testUnitFactory(0, 30, 0.08f, 0.08f, 0.0f);
}
@Test
void powerRequestedWithSufficientItems(){
testUnitFactory(30, 30, 0.08f, 0.08f, 1.0f);
}
void testUnitFactory(int siliconAmount, int leadAmount, float producedPower, float requestedPower, float expectedSatisfaction){
Tile consumerTile = createFakeTile(0, 0, new UnitFactory("fakefactory"){{
type = UnitTypes.spirit;
produceTime = 60;
consumes.power(requestedPower);
consumes.items(new ItemStack(Items.silicon, 30), new ItemStack(Items.lead, 30));
}});
consumerTile.entity.items.add(Items.silicon, siliconAmount);
consumerTile.entity.items.add(Items.lead, leadAmount);
Tile producerTile = createFakeTile(2, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 0.5f; // 100%
PowerGraph graph = new PowerGraph();
graph.add(producerTile);
graph.add(consumerTile);
consumerTile.entity.update();
graph.update();
assertEquals(expectedSatisfaction, consumerTile.entity.power.satisfaction);
}
}

View File

@ -0,0 +1,175 @@
package power;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.ItemLiquidGenerator;
import org.junit.jupiter.api.*;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* This class tests generators which can process items, liquids or both.
* All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well.
* Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame.
* Both of these constraints are handled by FakeThreadHandler within PowerTestFixture.
* Any expected power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not!
*/
public class ItemLiquidGeneratorTests extends PowerTestFixture{
private ItemLiquidGenerator generator;
private Tile tile;
private ItemLiquidGenerator.ItemLiquidGeneratorEntity entity;
private final float fakeItemDuration = 60f; // 60 ticks
private final float maximumLiquidUsage = 0.5f;
public void createGenerator(ItemLiquidGenerator.InputType inputType){
generator = new ItemLiquidGenerator(inputType, "fakegen"){
{
powerProduction = 0.1f;
itemDuration = 60f;
itemDuration = fakeItemDuration;
maxLiquidGenerate = maximumLiquidUsage;
}
@Override
public float getItemEfficiency(Item item){
return item.flammability;
}
@Override
public float getLiquidEfficiency(Liquid liquid){
return liquid.flammability;
}
};
tile = createFakeTile(0, 0, generator);
entity = tile.entity();
}
/** Tests the consumption and efficiency when being supplied with liquids. */
@TestFactory
DynamicTest[] generatorWorksProperlyWithLiquidInput(){
// Execute all tests for the case where only liquids are accepted and for the case where liquids and items are accepted (but supply only liquids)
ItemLiquidGenerator.InputType[] inputTypesToBeTested = new ItemLiquidGenerator.InputType[]{
ItemLiquidGenerator.InputType.LiquidsOnly,
ItemLiquidGenerator.InputType.LiquidsAndItems
};
ArrayList<DynamicTest> tests = new ArrayList<>();
for(ItemLiquidGenerator.InputType inputType : inputTypesToBeTested){
tests.add(dynamicTest("01", () -> simulateLiquidConsumption(inputType, Liquids.oil, 0.0f, "No liquids provided")));
tests.add(dynamicTest("02", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage / 4.0f, "Low oil provided")));
tests.add(dynamicTest("03", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage * 1.0f, "Sufficient oil provided")));
tests.add(dynamicTest("04", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage * 2.0f, "Excess oil provided")));
// Note: The generator will decline any other liquid since it's not flammable
}
DynamicTest[] testArray = new DynamicTest[tests.size()];
testArray = tests.toArray(testArray);
return testArray;
}
void simulateLiquidConsumption(ItemLiquidGenerator.InputType inputType, Liquid liquid, float availableLiquidAmount, String parameterDescription){
final float baseEfficiency = liquid.flammability;
final float expectedEfficiency = Math.min(1.0f, availableLiquidAmount / maximumLiquidUsage) * baseEfficiency;
final float expectedConsumptionPerTick = Math.min(maximumLiquidUsage, availableLiquidAmount);
final float expectedRemainingLiquidAmount = Math.max(0.0f, availableLiquidAmount - expectedConsumptionPerTick * Time.delta());
createGenerator(inputType);
assertTrue(generator.acceptLiquid(tile, null, liquid, availableLiquidAmount), inputType + " | " + parameterDescription + ": Liquids which will be declined by the generator don't need to be tested - The code won't be called for those cases.");
entity.liquids.add(liquid, availableLiquidAmount);
entity.cons.update(tile.entity);
assertTrue(entity.cons.valid());
// Perform an update on the generator once - This should use up any resource up to the maximum liquid usage
generator.update(tile);
assertEquals(expectedRemainingLiquidAmount, entity.liquids.get(liquid), inputType + " | " + parameterDescription + ": Remaining liquid amount mismatch.");
assertEquals(expectedEfficiency, entity.productionEfficiency, inputType + " | " + parameterDescription + ": Efficiency mismatch.");
}
/** Tests the consumption and efficiency when being supplied with items. */
@TestFactory
DynamicTest[] generatorWorksProperlyWithItemInput(){
// Execute all tests for the case where only items are accepted and for the case where liquids and items are accepted (but supply only items)
ItemLiquidGenerator.InputType[] inputTypesToBeTested = new ItemLiquidGenerator.InputType[]{
ItemLiquidGenerator.InputType.ItemsOnly,
ItemLiquidGenerator.InputType.LiquidsAndItems
};
ArrayList<DynamicTest> tests = new ArrayList<>();
for(ItemLiquidGenerator.InputType inputType : inputTypesToBeTested){
tests.add(dynamicTest("01", () -> simulateItemConsumption(inputType, Items.coal, 0, "No items provided")));
tests.add(dynamicTest("02", () -> simulateItemConsumption(inputType, Items.coal, 1, "Sufficient coal provided")));
tests.add(dynamicTest("03", () -> simulateItemConsumption(inputType, Items.coal, 10, "Excess coal provided")));
tests.add(dynamicTest("04", () -> simulateItemConsumption(inputType, Items.blastCompound, 1, "Blast compound provided")));
//dynamicTest("03", () -> simulateItemConsumption(inputType, Items.plastanium, 1, "Plastanium provided")), // Not accepted by generator due to low flammability
tests.add(dynamicTest("05", () -> simulateItemConsumption(inputType, Items.biomatter, 1, "Biomatter provided")));
tests.add(dynamicTest("06", () -> simulateItemConsumption(inputType, Items.pyratite, 1, "Pyratite provided")));
}
DynamicTest[] testArray = new DynamicTest[tests.size()];
testArray = tests.toArray(testArray);
return testArray;
}
void simulateItemConsumption(ItemLiquidGenerator.InputType inputType, Item item, int amount, String parameterDescription){
final float expectedEfficiency = Math.min(1.0f, amount > 0 ? item.flammability : 0f);
final float expectedRemainingItemAmount = Math.max(0, amount - 1);
createGenerator(inputType);
assertTrue(generator.acceptItem(item, tile, null), inputType + " | " + parameterDescription + ": Items which will be declined by the generator don't need to be tested - The code won't be called for those cases.");
if(amount > 0){
entity.items.add(item, amount);
}
entity.cons.update(tile.entity);
assertTrue(entity.cons.valid());
// Perform an update on the generator once - This should use up one or zero items - dependent on if the item is accepted and available or not.
generator.update(tile);
assertEquals(expectedRemainingItemAmount, entity.items.get(item), inputType + " | " + parameterDescription + ": Remaining item amount mismatch.");
assertEquals(expectedEfficiency, entity.productionEfficiency, inputType + " | " + parameterDescription + ": Efficiency mismatch.");
}
/** Makes sure the efficiency stays equal during the item duration. */
@Test
void efficiencyRemainsConstantWithinItemDuration_ItemsOnly(){
testItemDuration(ItemLiquidGenerator.InputType.ItemsOnly);
}
/** Makes sure the efficiency stays equal during the item duration. */
@Test
void efficiencyRemainsConstantWithinItemDuration_ItemsAndLiquids(){
testItemDuration(ItemLiquidGenerator.InputType.LiquidsAndItems);
}
void testItemDuration(ItemLiquidGenerator.InputType inputType){
createGenerator(inputType);
// Burn a single coal and test for the duration
entity.items.add(Items.coal, 1);
entity.cons.update(tile.entity);
generator.update(tile);
float expectedEfficiency = entity.productionEfficiency;
float currentDuration = 0.0f;
while((currentDuration += Time.delta()) <= fakeItemDuration){
generator.update(tile);
assertEquals(expectedEfficiency, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration));
}
generator.update(tile);
assertEquals(0.0f, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration));
}
}

View File

@ -0,0 +1,105 @@
package power;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.core.ContentLoader;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.PowerBlock;
import io.anuke.mindustry.world.blocks.power.Battery;
import io.anuke.mindustry.world.blocks.power.PowerGenerator;
import io.anuke.mindustry.world.modules.ConsumeModule;
import io.anuke.mindustry.world.modules.ItemModule;
import io.anuke.mindustry.world.modules.LiquidModule;
import io.anuke.mindustry.world.modules.PowerModule;
import org.junit.jupiter.api.BeforeAll;
import java.lang.reflect.Field;
/** This class provides objects commonly used by power related unit tests.
* For now, this is a helper with static methods, but this might change.
*
* Note: All tests which subclass this will run with a fixed delta of 0.5!
* */
public class PowerTestFixture{
public static final float smallRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR;
public static final float mediumRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR * 10;
public static final float highRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR * 100;
@BeforeAll
static void initializeDependencies(){
Vars.content = new ContentLoader();
Vars.content.load();
Time.setDeltaProvider(() -> 0.5f);
}
protected static PowerGenerator createFakeProducerBlock(float producedPower){
// Multiply produced power by 2 since production efficiency is defined to be 0.5 = 100%
return new PowerGenerator("fakegen"){{
powerProduction = producedPower * 2.0f;
}};
}
protected static Battery createFakeBattery(float capacity, float ticksToFill){
return new Battery("fakebattery"){{
consumes.powerBuffered(capacity, ticksToFill);
}};
}
protected static Block createFakeDirectConsumer(float powerPerTick, float minimumSatisfaction){
return new PowerBlock("fakedirectconsumer"){{
consumes.power(powerPerTick, minimumSatisfaction);
}};
}
protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){
return new PowerBlock("fakebufferedconsumer"){{
consumes.powerBuffered(capacity, ticksToFill);
}};
}
/**
* Creates a fake tile on the given location using the given block.
* @param x The X coordinate.
* @param y The y coordinate.
* @param block The block on the tile.
* @return The created tile or null in case of exceptions.
*/
protected static Tile createFakeTile(int x, int y, Block block){
try{
Tile tile = new Tile(x, y);
// Using the Tile(int, int, byte, byte) constructor would require us to register any fake block or tile we create
// Since this part shall not be part of the test and would require more work anyway, we manually set the block and floor
// through reflections and then simulate part of what the changed() method does.
Field field = Tile.class.getDeclaredField("wall");
field.setAccessible(true);
field.set(tile, block);
field = Tile.class.getDeclaredField("floor");
field.setAccessible(true);
field.set(tile, Blocks.sand);
// Simulate the "changed" method. Calling it through reflections would require half the game to be initialized.
tile.entity = block.newEntity().init(tile, false);
tile.entity.cons = new ConsumeModule();
if(block.hasItems) tile.entity.items = new ItemModule();
if(block.hasLiquids) tile.entity.liquids = new LiquidModule();
if(block.hasPower){
tile.entity.power = new PowerModule();
tile.entity.power.graph.add(tile);
}
// Assign incredibly high health so the block does not get destroyed on e.g. burning Blast Compound
block.health = 100000;
tile.entity.health = 100000.0f;
return tile;
}catch(Exception ex){
return null;
}
}
}

View File

@ -0,0 +1,183 @@
package power;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.PowerGenerator;
import io.anuke.mindustry.world.blocks.power.PowerGraph;
import io.anuke.mindustry.world.consumers.ConsumePower;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Tests code related to the power system in general, but not specific blocks.
* All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well.
* Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame.
* Both of these constraints are handled by FakeThreadHandler within PowerTestFixture.
* Any power amount (produced, consumed, buffered) should be affected by Time.delta() but satisfaction should not!
*/
public class PowerTests extends PowerTestFixture{
@BeforeEach
void initTest(){
}
@Nested
class PowerGraphTests{
/** Tests the satisfaction of a single consumer after a single update of the power graph which contains a single producer.
*
* Assumption: When the consumer requests zero power, satisfaction does not change. Default is 0.0f.
*/
@TestFactory
DynamicTest[] directConsumerSatisfactionIsAsExpected(){
return new DynamicTest[]{
// Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975
// That's why we inject the description into the test method for now.
// Additional Note: If you don't see any labels in front of the values supplied as function parameters, use a better IDE like IntelliJ IDEA.
dynamicTest("01", () -> simulateDirectConsumption(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")),
dynamicTest("02", () -> simulateDirectConsumption(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")),
dynamicTest("03", () -> simulateDirectConsumption(1.0f, 0.0f, 0.0f, "1.0 produced, 0.0 consumed (no power requested)")),
dynamicTest("04", () -> simulateDirectConsumption(1.0f, 1.0f, 1.0f, "1.0 produced, 1.0 consumed (stable consumption)")),
dynamicTest("05", () -> simulateDirectConsumption(0.5f, 1.0f, 0.5f, "0.5 produced, 1.0 consumed (power shortage)")),
dynamicTest("06", () -> simulateDirectConsumption(1.0f, 0.5f, 1.0f, "1.0 produced, 0.5 consumed (power excess)")),
dynamicTest("07", () -> simulateDirectConsumption(0.09f, 0.09f - Mathf.FLOAT_ROUNDING_ERROR / 10.0f, 1.0f, "floating point inaccuracy (stable consumption)"))
};
}
void simulateDirectConsumption(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 0.5f; // Currently, 0.5f = 100%
Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower, 0.6f));
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(directConsumerTile);
assertEquals(producedPower * Time.delta(), powerGraph.getPowerProduced(), Mathf.FLOAT_ROUNDING_ERROR);
assertEquals(requiredPower * Time.delta(), powerGraph.getPowerNeeded(), Mathf.FLOAT_ROUNDING_ERROR);
// Update and check for the expected power satisfaction of the consumer
powerGraph.update();
assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
}
/** Tests the satisfaction of a single buffered consumer after a single update of the power graph which contains a single producer. */
@TestFactory
DynamicTest[] bufferedConsumerSatisfactionIsAsExpected(){
return new DynamicTest[]{
// Note: powerPerTick may not be 0 in any of the test cases. This would equal a "ticksToFill" of infinite.
// Note: Due to a fixed delta of 0.5, only half of what is defined here will in fact be produced/consumed. Keep this in mind when defining expectedSatisfaction!
dynamicTest("01", () -> simulateBufferedConsumption(0.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power anywhere")),
dynamicTest("02", () -> simulateBufferedConsumption(0.0f, 1.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power provided")),
dynamicTest("03", () -> simulateBufferedConsumption(1.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power requested")),
dynamicTest("04", () -> simulateBufferedConsumption(1.0f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Stable Power, One tick to fill")),
dynamicTest("05", () -> simulateBufferedConsumption(2.0f, 1.0f, 2.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One delta to fill")),
dynamicTest("06", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Stable Power, multiple ticks to fill")),
dynamicTest("07", () -> simulateBufferedConsumption(1.2f, 0.5f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Power excess, one delta to fill")),
dynamicTest("08", () -> simulateBufferedConsumption(1.0f, 0.5f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power excess, multiple ticks to fill")),
dynamicTest("09", () -> simulateBufferedConsumption(1.0f, 1.0f, 2.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one delta to fill")),
dynamicTest("10", () -> simulateBufferedConsumption(0.5f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Power shortage, multiple ticks to fill")),
dynamicTest("11", () -> simulateBufferedConsumption(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")),
dynamicTest("12", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")),
dynamicTest("13", () -> simulateBufferedConsumption(0.2f, 1.0f, 0.5f, 0.5f, 0.6f, "Half buffer, power shortage")),
dynamicTest("14", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.5f, 0.9f, 1.0f, "Buffer does not get exceeded")),
dynamicTest("15", () -> simulateBufferedConsumption(2.0f, 1.0f, 1.0f, 0.5f, 1.0f, "Half buffer, filled with excess"))
};
}
void simulateBufferedConsumption(float producedPower, float maxBuffer, float powerConsumedPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 0.5f; // Currently, 0.5 = 100%
Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerConsumedPerTick : 1.0f));
bufferedConsumerTile.entity.power.satisfaction = initialSatisfaction;
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(bufferedConsumerTile);
assertEquals(producedPower * Time.delta(), powerGraph.getPowerProduced(), Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Produced power did not match");
float expectedPowerUsage;
if(initialSatisfaction == 1.0f){
expectedPowerUsage = 0f;
}else{
expectedPowerUsage = Math.min(maxBuffer, powerConsumedPerTick * Time.delta());
}
assertEquals(expectedPowerUsage, powerGraph.getPowerNeeded(), Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Consumed power did not match");
// Update and check for the expected power satisfaction of the consumer
powerGraph.update();
assertEquals(expectedSatisfaction, bufferedConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of buffered consumer did not match");
}
/** Tests the satisfaction of a single direct consumer after a single update of the power graph which contains a single producer and a single battery.
* The used battery is created with a maximum capacity of 100 and receives ten power per tick.
*/
@TestFactory
DynamicTest[] batteryCapacityIsAsExpected(){
return new DynamicTest[]{
// Note: expectedBatteryCapacity is currently adjusted to a delta of 0.5! (FakeThreadHandler sets it to that)
dynamicTest("01", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 0.0f, 5.0f, 0.0f, "Empty battery, no consumer")),
dynamicTest("02", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 94.999f, 99.999f, 0.0f, "Battery almost full after update, no consumer")),
dynamicTest("03", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 100.0f, 100.0f, 0.0f, "Full battery, no consumer")),
dynamicTest("04", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, "No producer, no consumer, empty battery")),
dynamicTest("05", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 100.0f, 100.0f, 0.0f, "No producer, no consumer, full battery")),
dynamicTest("06", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 0.0f, 0.0f, 0.0f, "No producer, empty battery")),
dynamicTest("07", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 100.0f, 95.0f, 1.0f, "No producer, full battery")),
dynamicTest("08", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 2.5f, 0.0f, 0.5f, "No producer, low battery")),
dynamicTest("09", () -> simulateDirectConsumptionWithBattery(5.0f, 10.0f, 5.0f, 0.0f, 1.0f, "Producer + Battery = Consumed")),
};
}
void simulateDirectConsumptionWithBattery(float producedPower, float requestedPower, float initialBatteryCapacity, float expectedBatteryCapacity, float expectedSatisfaction, String parameterDescription){
PowerGraph powerGraph = new PowerGraph();
if(producedPower > 0.0f){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 0.5f;
powerGraph.add(producerTile);
}
Tile directConsumerTile = null;
if(requestedPower > 0.0f){
directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requestedPower, 0.6f));
powerGraph.add(directConsumerTile);
}
float maxCapacity = 100f;
Tile batteryTile = createFakeTile(0, 2, createFakeBattery(maxCapacity, 10 ));
batteryTile.entity.power.satisfaction = initialBatteryCapacity / maxCapacity;
powerGraph.add(batteryTile);
powerGraph.update();
assertEquals(expectedBatteryCapacity / maxCapacity, batteryTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery satisfaction did not match");
if(directConsumerTile != null){
assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
}
}
/** Makes sure a direct consumer stops working after power production is set to zero. */
@Test
void directConsumptionStopsWithNoPower(){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(10.0f));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 1.0f;
Tile consumerTile = createFakeTile(0, 1, createFakeDirectConsumer(5.0f, 0.6f));
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(consumerTile);
powerGraph.update();
assertEquals(1.0f, consumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR);
powerGraph.remove(producerTile);
powerGraph.add(consumerTile);
powerGraph.update();
assertEquals(0.0f, consumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR);
if(consumerTile.block().consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumerTile.block().consumes.get(ConsumePower.class);
assertFalse(consumePower.valid(consumerTile.block(), consumerTile.entity()));
}
}
}
}