diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index aed6127661..bbdcc53e2b 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -822,13 +822,12 @@ sectors.redirect = Redirect Launch Pads sectors.rename = Rename Sector sectors.enemybase = [scarlet]Enemy Base sectors.vulnerable = [scarlet]Vulnerable -sectors.underattack = [scarlet]Under attack! [accent]{0}% damaged -sectors.underattack.nodamage = [scarlet]Uncaptured -sectors.survives = [accent]Survives {0} waves +sectors.underattack = [scarlet]Under attack! sectors.go = Go sector.abandon = Abandon sector.abandon.confirm = This sector's core(s) will self-destruct.\nContinue? sector.curcapture = Sector Captured +sector.lockdown = [red]:warning:[accent] Sector currently under attack\n[lightgray]production, research, export and import disabled sector.curlost = Sector Lost sector.missingresources = [scarlet]Insufficient Core Resources sector.attacked = Sector [accent]{0}[white] under attack! diff --git a/core/src/mindustry/content/Planets.java b/core/src/mindustry/content/Planets.java index b81f2decb3..c4d642957b 100644 --- a/core/src/mindustry/content/Planets.java +++ b/core/src/mindustry/content/Planets.java @@ -133,7 +133,6 @@ public class Planets{ sectorSeed = 2; allowWaves = true; allowLegacyLaunchPads = true; - allowWaveSimulation = true; allowSectorInvasion = true; allowLaunchSchematics = true; enemyCoreSpawnReplace = true; @@ -157,7 +156,7 @@ public class Planets{ landCloudColor = Pal.spore.cpy().a(0.5f); }}; - verilus = makeAsteroid("verlius", sun, Blocks.stoneWall, Blocks.iceWall, -1, 0.5f, 12, 2f, gen -> { + verilus = makeAsteroid("verilus", sun, Blocks.stoneWall, Blocks.iceWall, -1, 0.5f, 12, 2f, gen -> { gen.berylChance = 0f; gen.iceChance = 0.6f; gen.carbonChance = 0.1f; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index ed955e4857..fc26751e20 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -77,37 +77,9 @@ public class Logic implements ApplicationListener{ SectorInfo info = state.rules.sector.info; info.write(); - //only simulate waves if the planet allows it - if(state.rules.sector.planet.allowWaveSimulation){ - //how much wave time has passed - int wavesPassed = info.wavesPassed; - - //wave has passed, remove all enemies, they are assumed to be dead - if(wavesPassed > 0){ - Groups.unit.each(u -> { - if(u.team == state.rules.waveTeam){ - u.remove(); - } - }); - } - - //simulate passing of waves - if(wavesPassed > 0){ - //simulate wave counter moving forward - state.wave += wavesPassed; - state.wavetime = state.rules.waveSpacing * state.getPlanet().campaignRules.difficulty.waveTimeMultiplier; - - SectorDamage.applyCalculatedDamage(); - } - } - state.getSector().planet.applyRules(state.rules); - //reset values - info.damage = 0f; - info.wavesPassed = 0; info.hasCore = true; - info.secondsPassed = 0; state.rules.sector.saveInfo(); } diff --git a/core/src/mindustry/game/SectorInfo.java b/core/src/mindustry/game/SectorInfo.java index 23f5e1bca5..ee733923d1 100644 --- a/core/src/mindustry/game/SectorInfo.java +++ b/core/src/mindustry/game/SectorInfo.java @@ -6,7 +6,6 @@ import arc.struct.*; import arc.util.*; import mindustry.content.*; import mindustry.ctype.*; -import mindustry.maps.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -63,18 +62,10 @@ public class SectorInfo{ public int attempts; /** Wave # from state */ public int wave = 1, winWave = -1; - /** Waves this sector can survive if under attack. Based on wave in info. <0 means uncalculated. */ - public int wavesSurvived = -1; /** Time between waves. */ public float waveSpacing = 2 * Time.toMinutes; - /** Damage dealt to sector. */ - public float damage; - /** How many waves have passed while the player was away. */ - public int wavesPassed; /** Packed core spawn position. */ public int spawnPosition; - /** How long the player has been playing elsewhere. */ - public float secondsPassed; /** How many minutes this sector has been captured. */ public float minutesCaptured; /** Light coverage in terms of radius. */ @@ -90,11 +81,6 @@ public class SectorInfo{ /** Whether this sector was indicated to the player or not. */ public boolean shown = false; - /** Special variables for simulation. */ - public float sumHealth, sumRps, sumDps, bossHealth, bossDps, curEnemyHealth, curEnemyDps; - /** Wave where first boss shows up. */ - public int bossWave = -1; - public ObjectFloatMap importCooldownTimers = new ObjectFloatMap<>(); public @Nullable transient float[] importRateCache; @@ -233,9 +219,6 @@ public class SectorInfo{ hasCore = entity != null; bestCoreType = !hasCore ? Blocks.air : state.rules.defaultTeam.cores().max(e -> e.block.size).block; storageCapacity = entity != null ? entity.storageCapacity : 0; - secondsPassed = 0; - wavesPassed = 0; - damage = 0; hasSpawns = spawner.countSpawns() > 0; lastPresetName = sector.preset == null ? null : sector.preset.name; lastWidth = world.width(); @@ -264,10 +247,6 @@ public class SectorInfo{ sector.saveInfo(); - if(sector.planet.allowWaveSimulation){ - SectorDamage.writeParameters(sector); - } - if(sector.planet.generator != null){ sector.planet.generator.beforeSaveWrite(sector); } diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 2fc005ca68..0946134ca8 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -9,7 +9,6 @@ import mindustry.game.EventType.*; import mindustry.game.Schematic.*; import mindustry.game.SectorInfo.*; import mindustry.gen.*; -import mindustry.maps.*; import mindustry.type.*; import mindustry.world.blocks.storage.*; @@ -158,13 +157,8 @@ public class Universe{ //update relevant sectors for(Planet planet : content.planets()){ - //planets with different wave simulation status are not updated - if(current != null && current.allowWaveSimulation != planet.allowWaveSimulation){ - 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())){ + //do not update other planets + if(current != null && current != planet && !current.updateGroup.contains(planet) && !planet.updateGroup.contains(current)){ continue; } @@ -178,7 +172,7 @@ public class Universe{ //second pass: update export & import statistics for(Sector sector : planet.sectors){ - if(sector.hasBase() && !sector.isBeingPlayed()){ + if(sector.hasBase() && !sector.isBeingPlayed() && !sector.isAttacked()){ //export to another sector if(sector.info.destination != null){ @@ -186,7 +180,7 @@ public class Universe{ if(to.hasBase() && to.planet == planet){ ItemSeq items = new ItemSeq(); //calculated exported items to this sector - sector.info.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed * sector.getProductionScale()))); + sector.info.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed))); to.addItems(items); to.info.lastImported.add(items); } @@ -209,95 +203,58 @@ public class Universe{ sector.info.minutesCaptured += turnDuration / 60 / 60; } - //increment seconds passed for this sector by the time that just passed with this turn - if(!sector.isBeingPlayed()){ + //attacked sectors are frozen in time; don't update those + if(!sector.isAttacked()){ - //TODO: if a planet has sectors under attack and simulation is OFF, just don't simulate it + //increment seconds passed for this sector by the time that just passed with this turn + if(!sector.isBeingPlayed()){ - //increment time if attacked - if(sector.isAttacked()){ - sector.info.secondsPassed += turnDuration/60f; - } + //add production, making sure that it's capped + sector.info.production.each((item, stat) -> sector.info.items.add(item, Math.min((int)(stat.mean * newSecondsPassed), sector.info.storageCapacity - sector.info.items.get(item)))); - int wavesPassed = (int)(sector.info.secondsPassed*60f / sector.info.waveSpacing); - boolean attacked = sector.info.waves && sector.planet.allowWaveSimulation; - - if(attacked){ - sector.info.wavesPassed = wavesPassed; - } - - float damage = attacked ? SectorDamage.getDamage(sector) : 0f; - - //damage never goes down until the player visits the sector, so use max - sector.info.damage = Math.max(sector.info.damage, damage); - - //check if the sector has been attacked too many times... - if(attacked && damage >= 0.999f){ - //fire event for losing the sector - Events.fire(new SectorLoseEvent(sector)); - - //sector is dead. - sector.info.items.clear(); - sector.info.damage = 1f; - sector.info.hasCore = false; - sector.info.production.clear(); - }else if(attacked && wavesPassed > 0 && sector.info.winWave > 1 && sector.info.wave + wavesPassed >= sector.info.winWave && !sector.hasEnemyBase()){ - //autocapture the sector - sector.info.waves = false; - boolean was = sector.info.wasCaptured; - sector.info.wasCaptured = true; - - //fire the event - Events.fire(new SectorCaptureEvent(sector, !was)); - } - - float scl = sector.getProductionScale(); - - //add production, making sure that it's capped - sector.info.production.each((item, stat) -> sector.info.items.add(item, Math.min((int)(stat.mean * newSecondsPassed * scl), sector.info.storageCapacity - sector.info.items.get(item)))); - - if(planet.campaignRules.legacyLaunchPads){ - sector.info.export.each((item, stat) -> { - if(sector.info.items.get(item) <= 0 && sector.info.production.get(item, ExportStat::new).mean < 0 && stat.mean > 0){ - //cap export by import when production is negative. - //TODO remove - stat.mean = Math.min(sector.info.lastImported.get(item) / (float)newSecondsPassed, stat.mean); - } - }); - } - - //prevent negative values with unloaders - sector.info.items.checkNegative(); - - sector.saveInfo(); - } - - //queue random invasions - if(!sector.isAttacked() && sector.planet.campaignRules.sectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){ - int count = sector.near().count(s -> s.hasEnemyBase() && !s.hasBase() && (s.preset == null || !s.preset.requireUnlock)); - - //invasion chance depends on # of nearby bases - if(count > 0 && Mathf.chance(baseInvasionChance * (0.8f + (count - 1) * 0.3f))){ - int waveMax = Math.max(sector.info.winWave, sector.isBeingPlayed() ? state.wave : sector.info.wave + sector.info.wavesPassed) + Mathf.random(2, 4) * 5; - - //assign invasion-related things - if(sector.isBeingPlayed()){ - state.rules.winWave = waveMax; - state.rules.waves = true; - state.rules.attackMode = false; - planet.campaignRules.apply(planet, state.rules); //enabling waves may force changes in campaign rules - //update rules in multiplayer - if(net.server()){ - Call.setRules(state.rules); - } - }else{ - sector.info.winWave = waveMax; - sector.info.waves = true; - sector.info.attack = false; - sector.saveInfo(); + if(planet.campaignRules.legacyLaunchPads){ + sector.info.export.each((item, stat) -> { + if(sector.info.items.get(item) <= 0 && sector.info.production.get(item, ExportStat::new).mean < 0 && stat.mean > 0){ + //cap export by import when production is negative. + //TODO remove + stat.mean = Math.min(sector.info.lastImported.get(item) / (float)newSecondsPassed, stat.mean); + } + }); } - Events.fire(new SectorInvasionEvent(sector)); + //prevent negative values with unloaders + sector.info.items.checkNegative(); + + sector.saveInfo(); + } + + //queue random invasions + if(sector.planet.campaignRules.sectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){ + int count = sector.near().count(s -> s.hasEnemyBase() && !s.hasBase() && (s.preset == null || !s.preset.requireUnlock)); + + //invasion chance depends on # of nearby bases + if(count > 0 && Mathf.chance(baseInvasionChance * (0.8f + (count - 1) * 0.3f))){ + int waveMax = Math.max(sector.info.winWave, sector.isBeingPlayed() ? state.wave : sector.info.wave) + Mathf.random(2, 4) * 5; + + //assign invasion-related things + if(sector.isBeingPlayed()){ + state.rules.winWave = waveMax; + state.rules.waves = true; + state.rules.attackMode = false; + planet.campaignRules.apply(planet, state.rules); //enabling waves may force changes in campaign rules + //update rules in multiplayer + if(net.server()){ + Call.setRules(state.rules); + } + }else{ + sector.info.winWave = waveMax; + sector.info.waves = true; + sector.info.attack = false; + sector.saveInfo(); + } + + Events.fire(new SectorInvasionEvent(sector)); + } } } } diff --git a/core/src/mindustry/maps/SectorDamage.java b/core/src/mindustry/maps/SectorDamage.java index 64784b16de..befd456945 100644 --- a/core/src/mindustry/maps/SectorDamage.java +++ b/core/src/mindustry/maps/SectorDamage.java @@ -4,379 +4,17 @@ import arc.math.*; import arc.math.geom.*; import arc.struct.*; import mindustry.ai.*; -import mindustry.ai.types.*; import mindustry.content.*; -import mindustry.core.*; import mindustry.entities.*; -import mindustry.game.*; import mindustry.gen.*; -import mindustry.logic.*; -import mindustry.type.*; import mindustry.world.*; -import mindustry.world.blocks.defense.*; -import mindustry.world.blocks.defense.turrets.*; -import mindustry.world.blocks.defense.turrets.Turret.*; import mindustry.world.blocks.storage.*; import static mindustry.Vars.*; public class SectorDamage{ - public static final int maxRetWave = 110, maxWavesSimulated = 111; - - //direct damage is for testing only private static final boolean rubble = true; - /** @return calculated capture progress of the enemy */ - public static float getDamage(Sector sector){ - return getDamage(sector, sector.info.wavesPassed); - } - - /** @return calculated capture progress of the enemy */ - public static float getDamage(Sector sector, int wavesPassed){ - return getDamage(sector, wavesPassed, false); - } - - /** @return maximum waves survived, up to maxRetWave. */ - public static int getWavesSurvived(Sector sector){ - return (int)getDamage(sector, maxRetWave, true); - } - - /** @return calculated capture progress of the enemy if retWave is false, otherwise return the maximum waves survived as int. - * if it survives all the waves, returns maxRetWave. */ - public static float getDamage(Sector sector, int wavesPassed, boolean retWave){ - var info = sector.info; - float health = info.sumHealth; - int wave = info.wave; - float waveSpace = info.waveSpacing; - - //this approach is O(n), it simulates every wave passing. - //other approaches can assume all the waves come as one, but that's not as fair. - if(wavesPassed > 0){ - int waveBegin = wave; - int waveEnd = wave + wavesPassed; - - //do not simulate every single wave if there's too many - if(wavesPassed > maxWavesSimulated && !retWave){ - waveBegin = waveEnd - maxWavesSimulated; - } - - int groundSpawns = Math.max(spawner.countFlyerSpawns(), 1), airSpawns = Math.max(spawner.countGroundSpawns(), 1); - - for(int i = waveBegin; i <= waveEnd; i++){ - float enemyDps = 0f, enemyHealth = 0f; - - if(sector.save != null || sector.isBeingPlayed()){ - for(SpawnGroup group : (sector.isBeingPlayed() ? state.rules.spawns : sector.save.meta.rules.spawns)){ - //calculate the amount of spawn points used - //if there's a spawn position override, there is only one potential place they spawn - //assume that all overridden positions are valid, should always be true in properly designed campaign maps - int spawnCount = group.spawn != -1 ? 1 : group.type.flying ? airSpawns : groundSpawns; - - float healthMult = 1f + Mathf.clamp(group.type.armor / 20f); - StatusEffect effect = (group.effect == null ? StatusEffects.none : group.effect); - int spawned = group.getSpawned(i) * spawnCount; - if(spawned <= 0) continue; - enemyHealth += spawned * (group.getShield(i) + group.type.health * effect.healthMultiplier * healthMult); - enemyDps += spawned * group.type.dpsEstimate * effect.damageMultiplier; - } - } - - float efficiency = health / info.sumHealth; - float dps = info.sumDps * efficiency; - float rps = info.sumRps * efficiency; - - if(info.bossWave == i){ - enemyDps += info.bossDps; - enemyHealth += info.bossHealth; - } - - if(i == waveBegin){ - enemyDps += info.curEnemyDps; - enemyHealth += info.curEnemyHealth; - } - - //happens due to certain regressions - if(enemyHealth < 0 || enemyDps < 0) continue; - - //calculate time to destroy both sides - float timeDestroyEnemy = dps <= 0.0001f ? Float.POSITIVE_INFINITY : enemyHealth / dps; //if dps == 0, this is infinity - float timeDestroyBase = health / (enemyDps - rps); //if regen > enemyDps this is negative - - //regenerating faster than the base can be damaged - if(timeDestroyBase < 0) continue; - - //sector is lost, enemy took too long. - if(timeDestroyEnemy > timeDestroyBase){ - health = 0f; - //return current wave if simulating - if(retWave) return Math.max(i - waveBegin - 1, waveBegin); - break; - } - - //otherwise, the enemy shoots for timeDestroyEnemy seconds, so calculate damage taken - float damageTaken = timeDestroyEnemy * (enemyDps - rps); - - //damage the base. - health -= damageTaken; - - //regen health after wave. - health = Math.min(health + rps / 60f * waveSpace, info.sumHealth); - } - } - - //survived everything - if(retWave){ - return maxRetWave; - } - - return 1f - Mathf.clamp(health / info.sumHealth); - } - - /** Applies wave damage based on sector parameters. */ - public static void applyCalculatedDamage(){ - //calculate base damage fraction - float damage = getDamage(state.rules.sector); - - //scaled damage has a power component to make it seem a little more realistic (as systems fail, enemy capturing gets easier and easier) - float scaled = Mathf.pow(damage, 1.2f); - - Tile spawn = spawner.getFirstSpawn(); - - //damage only units near the spawn point - if(spawn != null){ - Seq allies = new Seq<>(); - float sumUnitHealth = 0f; - for(Unit ally : Groups.unit){ - if(ally.team == state.rules.defaultTeam && ally.within(spawn, state.rules.dropZoneRadius * 2.5f)){ - allies.add(ally); - sumUnitHealth += ally.health; - } - } - - allies.sort(u -> u.dst2(spawn)); - - //apply damage to units - float unitDamage = damage * sumUnitHealth; - - //damage units one by one, not uniformly - for(var u : allies){ - if(u.health < unitDamage){ - u.remove(); - unitDamage -= u.health; - }else{ - u.health -= unitDamage; - break; - } - } - } - - if(state.rules.sector.info.wavesPassed > 0){ - //simply remove each block in the spawner range if a wave passed - for(Tile spawner : spawner.getSpawns()){ - spawner.circle((int)(state.rules.dropZoneRadius / tilesize), tile -> { - if(tile.team() == state.rules.defaultTeam){ - if(rubble && tile.floor().hasSurface() && Mathf.chance(0.4)){ - Effect.rubble(tile.build.x, tile.build.y, tile.block().size); - } - - tile.remove(); - } - }); - } - } - - //finally apply scaled damage - apply(scaled); - } - - /** Calculates damage simulation parameters before a game is saved. */ - public static void writeParameters(Sector sector){ - var info = sector.info; - Building core = state.rules.defaultTeam.core(); - Seq spawns = new Seq<>(); - spawner.eachGroundSpawn((x, y) -> spawns.add(world.tile(x, y))); - - if(spawns.isEmpty() && state.rules.waveTeam.core() != null){ - spawns.add(state.rules.waveTeam.core().tile); - } - - if(core == null || spawns.isEmpty()) return; - - boolean airOnly = !state.rules.spawns.contains(g -> !g.type.flying); - - Tile start = spawns.first(); - Seq path = new Seq<>(); - - //TODO would be nice if this worked in a more generic way, with two different calculations and paths - if(airOnly){ - World.raycastEach(start.x, start.y, core.tileX(), core.tileY(), (x, y) -> { - path.add(world.rawTile(x, y)); - return false; - }); - }else{ - var field = pathfinder.getField(state.rules.waveTeam, Pathfinder.costGround, Pathfinder.fieldCore); - boolean found = false; - - if(field != null && field.weights != null){ - int[] weights = field.weights; - int count = 0; - Tile current = start; - while(count < weights.length){ - int minCost = Integer.MAX_VALUE; - int cx = current.x, cy = current.y; - for(Point2 p : Geometry.d4){ - int nx = cx + p.x, ny = cy + p.y, packed = world.packArray(nx, ny); - - Tile other = world.tile(nx, ny); - if(other != null && weights[packed] < minCost && weights[packed] != -1){ - minCost = weights[packed]; - current = other; - } - } - - path.add(current); - - if(current.build == core){ - found = true; - break; - } - - count ++; - } - } - - if(!found){ - path.clear(); - path.addAll(Astar.pathfind(start, core.tile, SectorDamage::cost, t -> !(t.block().isStatic() && t.solid()))); - } - } - - //create sparse tile array for fast range query - int sparseSkip = 5, sparseSkip2 = 3; - Seq sparse = new Seq<>(path.size / sparseSkip + 1); - Seq sparse2 = new Seq<>(path.size / sparseSkip2 + 1); - - for(int i = 0; i < path.size; i++){ - if(i % sparseSkip == 0){ - sparse.add(path.get(i)); - } - if(i % sparseSkip2 == 0){ - sparse2.add(path.get(i)); - } - } - - //regen is in health per second - //dps is per second - float sumHealth = 0f, sumRps = 0f, sumDps = 0f; - float totalPathBuild = 0; - - //first, calculate the total health of blocks in the path - - //radius around the path that gets counted - int radius = 6; - IntSet counted = new IntSet(); - - for(Tile t : sparse2){ - - //radius is square. - for(int dx = -radius; dx <= radius; dx++){ - for(int dy = -radius; dy <= radius; dy++){ - int wx = dx + t.x, wy = dy + t.y; - if(wx >= 0 && wy >= 0 && wx < world.width() && wy < world.height()){ - Tile tile = world.rawTile(wx, wy); - - if(tile.build != null && tile.team() == state.rules.defaultTeam && counted.add(tile.pos())){ - //health is divided by block size, because multiblocks are counted multiple times. - sumHealth += tile.build.health / (tile.block().size * tile.block().size); - totalPathBuild += 1f / (tile.block().size * tile.block().size); - } - } - } - } - } - - float avgHealth = totalPathBuild <= 1 ? sumHealth : sumHealth / totalPathBuild; - - //block dps + regen + extra health/shields - for(Building build : state.rules.defaultTeam.data().buildings){ - float e = build.potentialEfficiency; - if(e > 0.08f){ - if(build instanceof Ranged ranged && sparse.contains(t -> t.within(build, ranged.range() + 4*tilesize))){ - //TODO make sure power turret network supports the turrets? - if(build instanceof TurretBuild b && b.hasAmmo()){ - sumDps += b.estimateDps(); - } - - if(build.block instanceof MendProjector m){ - sumRps += m.healPercent / m.reload * avgHealth * 60f / 100f * e * build.timeScale(); - } - - //point defense turrets act as flat health right now - if(build.block instanceof PointDefenseTurret){ - sumHealth += 150f * build.timeScale() * build.potentialEfficiency; - } - - if(build.block instanceof ForceProjector f){ - sumHealth += f.shieldHealth * e * build.timeScale(); - sumRps += e; - } - } - } - } - - float curEnemyHealth = 0f, curEnemyDps = 0f; - - //unit regen + health + dps - for(Unit unit : Groups.unit){ - //skip player - if(unit.isPlayer()) continue; - - //scale health based on armor - yes, this is inaccurate, but better than nothing - float healthMult = 1f + Mathf.clamp(unit.armor / 20f); - - if(unit.team == state.rules.defaultTeam){ - sumHealth += unit.health*healthMult + unit.shield; - sumDps += unit.type.dpsEstimate; - sumRps += unit.type.weapons.sumf(w -> w.shotsPerSec() * (w.bullet.healPercent/100f * 20f + w.bullet.healAmount)); - if(unit.controller() instanceof CommandAI ai && ai.command == UnitCommand.rebuildCommand){ - sumRps += unit.type.buildSpeed * 20f; - } - }else{ - float bossMult = unit.isBoss() ? 3f : 1f; - curEnemyDps += unit.type.dpsEstimate * unit.damageMultiplier() * bossMult; - curEnemyHealth += unit.health * healthMult * unit.healthMultiplier() * bossMult + unit.shield; - } - } - - SpawnGroup bossGroup = state.rules.spawns.find(s -> s.effect == StatusEffects.boss); - - if(bossGroup != null){ - float bossMult = 1.2f; - //calculate first boss appearance - for(int wave = state.wave; wave < state.wave + 60; wave++){ - int spawned = bossGroup.getSpawned(wave - 1); - if(spawned > 0){ - //set up relevant stats - info.bossWave = wave; - info.bossDps = spawned * bossGroup.type.dpsEstimate * StatusEffects.boss.damageMultiplier * bossMult; - info.bossHealth = spawned * (bossGroup.getShield(wave) + bossGroup.type.health * StatusEffects.boss.healthMultiplier * (1f + Mathf.clamp(bossGroup.type.armor / 20f))) * bossMult; - break; - } - } - } - - info.sumHealth = sumHealth * 0.9f; - info.sumDps = sumDps; - info.sumRps = sumRps; - - float cmult = 1.6f; - - info.curEnemyDps = curEnemyDps*cmult; - info.curEnemyHealth = curEnemyHealth*cmult; - - info.wavesSurvived = getWavesSurvived(sector); - } - public static void apply(float fraction){ Tiles tiles = world.tiles; diff --git a/core/src/mindustry/type/Planet.java b/core/src/mindustry/type/Planet.java index 0a314342ce..b8dc3db005 100644 --- a/core/src/mindustry/type/Planet.java +++ b/core/src/mindustry/type/Planet.java @@ -115,8 +115,6 @@ public class Planet extends UnlockableContent{ public boolean allowLaunchSchematics = false; /** Whether to allow users to specify the resources they take to this map. */ public boolean allowLaunchLoadout = false; - /** Whether to allow sectors to simulate waves in the background. */ - public boolean allowWaveSimulation = false; /** Whether to simulate sector invasions from enemy bases. */ public boolean allowSectorInvasion = false; /** If true, legacy launch pads can be enabled. */ @@ -164,6 +162,8 @@ public class Planet extends UnlockableContent{ /** Loads the planet grid outline mesh. Clientside only. */ public Prov gridMeshLoader = () -> MeshBuilder.buildPlanetGrid(grid, outlineColor, outlineRad * radius); + /** Planets that are allowed to update at the same time as this one for background calculations. */ + public ObjectSet updateGroup = new ObjectSet<>(); /** Global difficulty/modifier settings for this planet's campaign. */ public CampaignRules campaignRules = new CampaignRules(); /** Defaults applied to the rules. */ @@ -251,7 +251,6 @@ public class Planet extends UnlockableContent{ public void applyDefaultRules(CampaignRules rules){ JsonIO.copy(campaignRuleDefaults, rules); - rules.sectorInvasion = allowSectorInvasion; } public @Nullable Sector getLastSector(){ diff --git a/core/src/mindustry/type/Sector.java b/core/src/mindustry/type/Sector.java index 503f39d754..ee7b3f88f8 100644 --- a/core/src/mindustry/type/Sector.java +++ b/core/src/mindustry/type/Sector.java @@ -121,10 +121,6 @@ public class Sector{ Core.settings.remove(planet.name + "-s-" + id + "-info"); } - public float getProductionScale(){ - return Math.max(1f - info.damage, 0); - } - public boolean isAttacked(){ if(isBeingPlayed()) return state.rules.waves || state.rules.attackMode; return save != null && (info.waves || info.attack) && info.hasCore; @@ -135,6 +131,10 @@ public class Sector{ return save != null && info.hasCore && !(Vars.state.isGame() && Vars.state.rules.sector == this && state.gameOver); } + public boolean isFrozen(){ + return isAttacked() && !isBeingPlayed(); + } + /** @return whether the enemy has a generated base here. */ public boolean hasEnemyBase(){ return ((generateEnemyBase && preset == null) || (preset != null && preset.captureWave == 0)) && (save == null || info.attack); diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index ce6798e820..4f73a47695 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -587,8 +587,6 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ } boolean selectable(Planet planet){ - //TODO what if any sector is selectable? - //TODO launch criteria - which planets can be launched to? Where should this be defined? Should planets even be selectable? if(mode == select) return planet == state.planet; if(mode == planetLaunch) return launchSector != null && (launchCandidates.contains(planet) || (planet == launchSector.planet && planet.allowSelfSectorLaunch)); return (planet.alwaysUnlocked && planet.isLandable()) || planet.sectors.contains(Sector::hasBase) || debugSelect; @@ -1006,18 +1004,18 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ state.uiAlpha = Mathf.lerpDelta(state.uiAlpha, Mathf.num(state.zoom < 1.9f), 0.1f); } - void displayItems(Table c, float scl, ObjectMap stats, String name){ - displayItems(c, scl, stats, name, t -> {}); + void displayItems(Table c, ObjectMap stats, String name){ + displayItems(c, stats, name, t -> {}); } - void displayItems(Table c, float scl, ObjectMap stats, String name, Cons builder){ + void displayItems(Table c, ObjectMap stats, String name, Cons
builder){ Table t = new Table().left(); int i = 0; for(var item : content.items()){ var stat = stats.get(item); if(stat == null) continue; - int total = (int)(stat.mean * 60 * scl); + int total = (int)(stat.mean * 60); if(total > 1){ t.image(item.uiIcon).padRight(3); t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray).padRight(3); @@ -1052,7 +1050,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ } if(sector.info.waves && sector.hasBase()){ - c.add(Core.bundle.get("sectors.wave") + " [accent]" + (sector.info.wave + sector.info.wavesPassed)).left().row(); + c.add(Core.bundle.get("sectors.wave") + " [accent]" + sector.info.wave).left().row(); } if(sector.isAttacked() || !sector.hasBase()){ @@ -1069,11 +1067,17 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ }).padLeft(10f).left().row(); } + boolean lockdown = sector.isAttacked() && !sector.isBeingPlayed(); + + if(lockdown){ + c.add(UI.formatIcons(bundle.get("sector.lockdown"))).wrap().fillX().padBottom(10f).row(); + } + //production - displayItems(c, sector.getProductionScale(), sector.info.production, "@sectors.production"); + displayItems(c, sector.info.production, "@sectors.production"); //export - displayItems(c, sector.getProductionScale(), sector.info.export, "@sectors.export", t -> { + displayItems(c, sector.info.export, "@sectors.export", t -> { if(sector.info.destination != null && sector.info.destination.hasBase()){ String ic = sector.info.destination.iconChar(); t.add(Iconc.rightOpen + " " + (ic == null || ic.isEmpty() ? "" : ic + " ") + sector.info.destination.name()).padLeft(10f).row(); @@ -1082,7 +1086,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ //import if(sector.hasBase()){ - displayItems(c, 1f, sector.info.imports, "@sectors.import", t -> { + displayItems(c, sector.info.imports, "@sectors.import", t -> { sector.info.eachImport(sector.planet, other -> { String ic = other.iconChar(); t.add(Iconc.rightOpen + " " + (ic == null || ic.isEmpty() ? "" : ic + " ") + other.name()).padLeft(10f).row(); @@ -1138,7 +1142,6 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ }); }else{ sector.info.items.clear(); - sector.info.damage = 1f; sector.info.hasCore = false; sector.info.production.clear(); sector.saveInfo(); @@ -1150,14 +1153,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ void addSurvivedInfo(Sector sector, Table table, boolean wrap){ if(!wrap){ - table.add(sector.planet.allowWaveSimulation ? Core.bundle.format("sectors.underattack", (int)(sector.info.damage * 100)) : "@sectors.underattack.nodamage").wrapLabel(wrap).row(); - } - - if(sector.planet.allowWaveSimulation && sector.info.wavesSurvived >= 0 && sector.info.wavesSurvived - sector.info.wavesPassed >= 0 && !sector.isBeingPlayed()){ - int toCapture = sector.info.attack || sector.info.winWave <= 1 ? -1 : sector.info.winWave - (sector.info.wave + sector.info.wavesPassed); - boolean plus = (sector.info.wavesSurvived - sector.info.wavesPassed) >= SectorDamage.maxRetWave - 1; - table.add(Core.bundle.format("sectors.survives", Math.min(sector.info.wavesSurvived - sector.info.wavesPassed, toCapture <= 0 ? 200 : toCapture) + - (plus ? "+" : "") + (toCapture < 0 ? "" : "/" + toCapture))).wrapLabel(wrap).row(); + table.add("@sectors.underattack").wrapLabel(wrap).row(); } } @@ -1349,27 +1345,6 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ return; } - Planet planet = sector.planet; - - //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; - } - } - boolean shouldHide = true; //save before launch. diff --git a/core/src/mindustry/ui/dialogs/ResearchDialog.java b/core/src/mindustry/ui/dialogs/ResearchDialog.java index 17d223090c..f1243d058f 100644 --- a/core/src/mindustry/ui/dialogs/ResearchDialog.java +++ b/core/src/mindustry/ui/dialogs/ResearchDialog.java @@ -231,7 +231,7 @@ public class ResearchDialog extends BaseDialog{ //add global counts of each sector for(Planet planet : rootPlanets){ for(Sector sector : planet.sectors){ - if(sector.hasBase()){ + if(sector.hasBase() && !sector.isFrozen()){ ItemSeq cached = sector.items(); cache.put(sector, cached); cached.each((item, amount) -> {