diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 0fca202a3b..810dfc8e12 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -6379,9 +6379,8 @@ public class Blocks{ itemCapacity = 100; - liquidCapacity = 2000; - addLiquidBar(Liquids.water); - consumeLiquid(Liquids.water, liquidCapacity).trigger(true).update(false); + liquidCapacity = 4000f; + consumeLiquidAmount = 2000f; }}; interplanetaryAccelerator = new Accelerator("interplanetary-accelerator"){{ diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index 58f7fa65e3..dd49e90f88 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -215,6 +215,10 @@ public class Control implements ApplicationListener, Loadable{ } if(state.isCampaign()){ + if(state.rules.sector.info.importRateCache != null){ + state.rules.sector.info.refreshImportRates(state.rules.sector.planet); + } + //don't run when hosting, that doesn't really work. if(state.rules.sector.planet.prebuildBase){ toBePlaced.clear(); diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index ed9271e519..328d838c4c 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -429,6 +429,8 @@ public class Logic implements ApplicationListener{ } if(!state.isPaused()){ + Events.fire(Trigger.beforeGameUpdate); + float delta = Core.graphics.getDeltaTime(); state.tick += Float.isNaN(delta) || Float.isInfinite(delta) ? 0f : delta * 60f; state.updateId ++; @@ -488,6 +490,8 @@ public class Logic implements ApplicationListener{ Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity)); Groups.update(); + + Events.fire(Trigger.afterGameUpdate); } if(runStateCheck){ diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 39333fdf96..ec3bac3628 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -38,6 +38,8 @@ public class EventType{ teamCoreDamage, socketConfigChanged, update, + beforeGameUpdate, + afterGameUpdate, unitCommandChange, unitCommandPosition, unitCommandAttack, diff --git a/core/src/mindustry/game/SectorInfo.java b/core/src/mindustry/game/SectorInfo.java index 85084e8ea1..2ccb9cd3f2 100644 --- a/core/src/mindustry/game/SectorInfo.java +++ b/core/src/mindustry/game/SectorInfo.java @@ -86,6 +86,9 @@ public class SectorInfo{ /** Wave where first boss shows up. */ public int bossWave = -1; + public ObjectFloatMap importCooldownTimers = new ObjectFloatMap<>(); + public @Nullable transient float[] importRateCache; + /** Counter refresh state. */ private transient Interval time = new Interval(); /** Core item storage input/output deltas. */ @@ -105,12 +108,6 @@ public class SectorInfo{ productionDeltas[item.id] += amount; } - /** @return the real location items go when launched on this sector */ - public Sector getRealDestination(){ - //on multiplayer the destination is, by default, the first captured sector (basically random) - return !net.client() || destination != null ? destination : state.rules.sector.planet.sectors.find(Sector::hasBase); - } - /** Updates export statistics. */ public void handleItemExport(ItemStack stack){ handleItemExport(stack.item, stack.amount); @@ -125,6 +122,29 @@ public class SectorInfo{ return export.get(item, ExportStat::new).mean; } + public void refreshImportRates(Planet planet){ + if(importRateCache == null || importRateCache.length != content.items().size){ + importRateCache = new float[content.items().size]; + }else{ + Arrays.fill(importRateCache, 0f); + } + eachImport(planet, sector -> sector.info.export.each((item, stat) -> { + importRateCache[item.id] += stat.mean; + })); + } + + public float[] getImportRates(Planet planet){ + if(importRateCache == null){ + refreshImportRates(planet); + } + return importRateCache; + } + + /** @return the import rate of an item as item/second. */ + public float getImportRate(Planet planet, Item item){ + return getImportRates(planet)[item.id]; + } + /** Write contents of meta into main storage. */ public void write(){ //enable attack mode when there's a core. @@ -291,7 +311,7 @@ public class SectorInfo{ /** Iterates through every sector this one imports from. */ public void eachImport(Planet planet, Cons cons){ for(Sector sector : planet.sectors){ - Sector dest = sector.info.getRealDestination(); + Sector dest = sector.info.destination; if(sector.hasBase() && sector.info != this && dest != null && dest.info == this && sector.info.anyExports()){ cons.get(sector); } diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index b159ca4991..94945eae55 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -163,9 +163,17 @@ public class Universe{ continue; } + //don't simulate the planet if there is an in-progress mission on that planet + if(!planet.allowWaveSimulation && planet.sectors.contains(s -> s.hasBase() && !s.isBeingPlayed() && s.isAttacked())){ + continue; + } + //third pass: everything else for(Sector sector : planet.sectors){ if(sector.hasBase()){ + if(sector.info.importRateCache != null){ + sector.info.refreshImportRates(planet); + } //if it is being attacked, capture time is 0; otherwise, increment the timer if(sector.isAttacked()){ diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index a7ec975735..55b564047f 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -504,11 +504,11 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(selected != null && selected != sec && selected.hasBase()){ //imports - if(sec.info.getRealDestination() == selected && sec.info.anyExports()){ + if(sec.info.destination == selected && sec.info.anyExports()){ planets.drawArc(planet, sec.tile.v, selected.tile.v, Color.gray.write(Tmp.c2).a(state.uiAlpha), Pal.accent.write(Tmp.c3).a(state.uiAlpha), 0.4f, 90f, 25); } //exports - if(selected.info.getRealDestination() == sec && selected.info.anyExports()){ + if(selected.info.destination == sec && selected.info.anyExports()){ planets.drawArc(planet, selected.tile.v, sec.tile.v, Pal.place.write(Tmp.c2).a(state.uiAlpha), Pal.accent.write(Tmp.c3).a(state.uiAlpha), 0.4f, 90f, 25); } } @@ -1219,24 +1219,24 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ return; } - //make sure there are no under-attack sectors (other than this one) - for(Planet planet : content.planets()){ - if(!planet.allowWaveSimulation && !debugSelect && planet.allowWaveSimulation == sector.planet.allowWaveSimulation){ - //if there are two or more attacked sectors... something went wrong, don't show the dialog to prevent softlock - Sector attacked = planet.sectors.find(s -> s.isAttacked() && s != sector); - if(attacked != null && planet.sectors.count(s -> s.isAttacked()) < 2){ - BaseDialog dialog = new BaseDialog("@sector.noswitch.title"); - dialog.cont.add(bundle.format("sector.noswitch", attacked.name(), attacked.planet.localizedName)).width(400f).labelAlign(Align.center).center().wrap(); - dialog.addCloseButton(); - dialog.buttons.button("@sector.view", Icon.eyeSmall, () -> { - dialog.hide(); - lookAt(attacked); - selectSector(attacked); - }); - dialog.show(); + Planet planet = sector.planet; - return; - } + //make sure there are no under-attack sectors (other than this one) + if(!planet.allowWaveSimulation && !debugSelect){ + //if there are two or more attacked sectors... something went wrong, don't show the dialog to prevent softlock + Sector attacked = planet.sectors.find(s -> s.isAttacked() && s != sector); + if(attacked != null && planet.sectors.count(s -> s.isAttacked()) < 2){ + BaseDialog dialog = new BaseDialog("@sector.noswitch.title"); + dialog.cont.add(bundle.format("sector.noswitch", attacked.name(), attacked.planet.localizedName)).width(400f).labelAlign(Align.center).center().wrap(); + dialog.addCloseButton(); + dialog.buttons.button("@sector.view", Icon.eyeSmall, () -> { + dialog.hide(); + lookAt(attacked); + selectSector(attacked); + }); + dialog.show(); + + return; } } diff --git a/core/src/mindustry/world/blocks/campaign/LandingPad.java b/core/src/mindustry/world/blocks/campaign/LandingPad.java index bb95caee6b..676c204916 100644 --- a/core/src/mindustry/world/blocks/campaign/LandingPad.java +++ b/core/src/mindustry/world/blocks/campaign/LandingPad.java @@ -1,18 +1,46 @@ package mindustry.world.blocks.campaign; +import arc.*; +import arc.math.*; +import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.game.EventType.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.io.*; import mindustry.type.*; +import mindustry.ui.*; import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.consumers.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; public class LandingPad extends Block{ + static ObjectMap> waiting = new ObjectMap<>(); + static long lastUpdateId = -1; + + static{ + Events.on(ResetEvent.class, e -> { + waiting.clear(); + lastUpdateId = -1; + }); + } + + public float cooldownTime = 12f; + public float consumeLiquidAmount = 100f; + public Liquid consumeLiquid = Liquids.water; public LandingPad(String name){ super(name); hasItems = true; + hasLiquids = true; solid = true; update = true; configurable = true; @@ -22,19 +50,127 @@ public class LandingPad extends Block{ configClear((LandingPadBuild build) -> build.config = null); } + @Override + public void init(){ + consume(new ConsumeLiquid(consumeLiquid, consumeLiquidAmount){ + + @Override + public void build(Building build, Table table){ + table.add(new ReqImage(liquid.uiIcon, () -> build.liquids.get(liquid) >= amount)).size(iconMed).top().left(); + } + + @Override + public float efficiency(Building build){ + return build.liquids.get(consumeLiquid) >= amount ? 1f : 0f; + } + + @Override + public void display(Stats stats){ + stats.add(Stat.input, liquid, amount, false); + } + }).update(false); + + super.init(); + } + + @Override + public void setBars(){ + super.setBars(); + + addLiquidBar(consumeLiquid); + //TODO: does cooldown even need to exist? + addBar("heat", (LandingPadBuild entity) -> new Bar("bar.heat", Pal.lightOrange, () -> entity.cooldown)); + } + @Override public boolean outputsItems(){ return true; } + @Remote(called = Loc.server) + public static void landingPadLanded(Tile tile){ + if(tile == null || !(tile.build instanceof LandingPadBuild build)) return; + build.handleLanding(); + } + public class LandingPadBuild extends Building{ public @Nullable Item config; + //priority collisions are possible, but should be extremely rare + public int priority = Mathf.rand.nextInt(); + public float cooldown = 0f; + + + public void handleLanding(){ + if(!state.isCampaign() || config == null) return; + //TODO animation, etc + + cooldown = 1f; + items.set(config, itemCapacity); + liquids.remove(consumeLiquid, consumeLiquidAmount); + for(int i = 0; i < 10; i++){ + Fx.steam.at(this); + } + //TODO this is a temporary effect + Fx.shockwave.at(this); + + state.rules.sector.info.importCooldownTimers.put(config, 0f); + } + + public void updateTimers(){ + if(state.isCampaign() && lastUpdateId != state.updateId){ + lastUpdateId = state.updateId; + + float[] imports = state.rules.sector.info.getImportRates(state.getPlanet()); + + for(Item item : content.items()){ + float importedPerFrame = imports[item.id]/60f; + if(importedPerFrame > 0f){ + float framesBetweenArrival = itemCapacity / importedPerFrame; + + state.rules.sector.info.importCooldownTimers.increment(item, 0f, 1f / framesBetweenArrival * Time.delta); + } + } + + waiting.each((item, pads) -> { + if(pads.size > 0){ + pads.sort(p -> p.priority); + + var first = pads.first(); + var head = pads.peek(); + + Call.landingPadLanded(first.tile); + + + //swap priorities, moving this block to the end of the list (if there is only one block waiting, this does nothing) + var tmp = first.priority; + first.priority = head.priority; + head.priority = tmp; + + + pads.clear(); + } + }); + } + } @Override public void updateTile(){ + updateTimers(); + if(items.total() > 0){ dumpAccumulate(config == null || items.get(config) != items.total() ? null : config); } + + if(config != null && state.isCampaign()){ + + cooldown -= delta() / cooldownTime; + + if(cooldown <= 0f && efficiency > 0f && items.total() == 0 && state.rules.sector.info.importCooldownTimers.get(config, 0f) >= 1f){ + + //queue landing for next frame + waiting.get(config, Seq::new).add(this); + } + } } @Override @@ -45,6 +181,11 @@ public class LandingPad extends Block{ return true; } + @Override + public void buildConfiguration(Table table){ + ItemSelection.buildTable(LandingPad.this, table, content.items(), () -> config, this::configure, selectionRows, selectionColumns); + } + @Override public boolean acceptItem(Building source, Item item){ return false; @@ -59,12 +200,16 @@ public class LandingPad extends Block{ public void read(Reads read, byte revision){ super.read(read, revision); config = TypeIO.readItem(read); + priority = read.i(); + cooldown = read.f(); } @Override public void write(Writes write){ super.write(write); TypeIO.writeItem(write, config); + write.i(priority); + write.f(cooldown); } } } diff --git a/core/src/mindustry/world/blocks/campaign/LaunchPad.java b/core/src/mindustry/world/blocks/campaign/LaunchPad.java index 517df73e2f..1f63f5a17a 100644 --- a/core/src/mindustry/world/blocks/campaign/LaunchPad.java +++ b/core/src/mindustry/world/blocks/campaign/LaunchPad.java @@ -150,7 +150,7 @@ public class LaunchPad extends Block{ table.row(); table.label(() -> { - Sector dest = state.rules.sector == null ? null : state.rules.sector.info.getRealDestination(); + Sector dest = state.rules.sector == null ? null : state.rules.sector.info.destination; return Core.bundle.format("launch.destination", dest == null || !dest.hasBase() ? Core.bundle.get("sectors.nonelaunch") : @@ -161,7 +161,7 @@ public class LaunchPad extends Block{ @Override public void buildConfiguration(Table table){ //TODO: this UI should be on landing pads - if(!state.isCampaign() || net.client() || true){ + if(!state.isCampaign() || net.client()){ deselect(); return; } @@ -262,27 +262,23 @@ public class LaunchPad extends Block{ @Override public void remove(){ - if(!state.isCampaign()) return; + if(!state.isCampaign() || net.client()) return; - Sector destsec = state.rules.sector.info.getRealDestination(); + Sector destsec = state.rules.sector.info.destination; //actually launch the items upon removal - if(team() == state.rules.defaultTeam){ - if(destsec != null && (destsec != state.rules.sector || net.client())){ - ItemSeq dest = new ItemSeq(); + if(team() == state.rules.defaultTeam && destsec != null && destsec != state.rules.sector){ + ItemSeq dest = new ItemSeq(); - for(ItemStack stack : stacks){ - dest.add(stack); + for(ItemStack stack : stacks){ + dest.add(stack); - //update export - state.rules.sector.info.handleItemExport(stack); - Events.fire(new LaunchItemEvent(stack)); - } - - if(!net.client()){ - destsec.addItems(dest); - } + //update export statistics + state.rules.sector.info.handleItemExport(stack); + Events.fire(new LaunchItemEvent(stack)); } + + destsec.addItems(dest); } } } diff --git a/core/src/mindustry/world/blocks/sandbox/ItemSource.java b/core/src/mindustry/world/blocks/sandbox/ItemSource.java index 0a7461a7ec..28d42f86ec 100644 --- a/core/src/mindustry/world/blocks/sandbox/ItemSource.java +++ b/core/src/mindustry/world/blocks/sandbox/ItemSource.java @@ -88,6 +88,7 @@ public class ItemSource extends Block{ while(counter >= limit){ items.set(outputItem, 1); dump(outputItem); + produced(outputItem); items.set(outputItem, 0); counter -= limit; } diff --git a/core/src/mindustry/world/consumers/ConsumeLiquid.java b/core/src/mindustry/world/consumers/ConsumeLiquid.java index b1258f67d4..8d756cdbdc 100644 --- a/core/src/mindustry/world/consumers/ConsumeLiquid.java +++ b/core/src/mindustry/world/consumers/ConsumeLiquid.java @@ -12,7 +12,6 @@ import static mindustry.Vars.*; //TODO replace with ConsumeLiquids? public class ConsumeLiquid extends ConsumeLiquidBase{ public final Liquid liquid; - public boolean trigger; public ConsumeLiquid(Liquid liquid, float amount){ super(amount); @@ -23,10 +22,6 @@ public class ConsumeLiquid extends ConsumeLiquidBase{ this(null, 0f); } - public ConsumeLiquid trigger(boolean trigger){ - this.trigger = trigger; - return this; - } @Override public void apply(Block block){ @@ -44,13 +39,6 @@ public class ConsumeLiquid extends ConsumeLiquidBase{ build.liquids.remove(liquid, amount * build.edelta() * multiplier.get(build)); } - @Override - public void trigger(Building build){ - if(trigger){ - build.liquids.remove(liquid, amount); - } - } - @Override public float efficiency(Building build){ float ed = build.edelta() * build.efficiencyScale(); diff --git a/gradle.properties b/gradle.properties index c5d42ecc57..a6c6ea3647 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=4940b68158 +archash=99a42db331