New system for Serpulo sector loss (WIP)

This commit is contained in:
Anuken
2025-09-07 14:14:24 -04:00
parent a20cfcf9ea
commit 8cb528264b
20 changed files with 176 additions and 98 deletions

View File

@@ -20,6 +20,7 @@ import mindustry.game.*;
import mindustry.game.EventType.*;
import mindustry.game.Objectives.*;
import mindustry.game.Saves.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.input.*;
import mindustry.io.*;
@@ -419,7 +420,7 @@ public class Control implements ApplicationListener, Loadable{
ui.planet.hide();
SaveSlot slot = sector.save;
sector.planet.setLastSector(sector);
if(slot != null && !clearSectors && (!(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor) || sector.info.hasCore)){
if(slot != null && !clearSectors && (!sector.planet.clearSectorOnLose || sector.info.hasCore)){
try{
boolean hadNoCore = !sector.info.hasCore;
@@ -433,7 +434,7 @@ public class Control implements ApplicationListener, Loadable{
//if there is no base, simulate a new game and place the right loadout at the spawn position
if(state.rules.defaultTeam.cores().isEmpty() || hadNoCore){
if(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor){
if(sector.planet.clearSectorOnLose){
playNewSector(origin, sector, reloader);
}else{
//no spawn set -> delete the sector save
@@ -446,61 +447,63 @@ public class Control implements ApplicationListener, Loadable{
return;
}
int spawnPos = sector.info.spawnPosition;
//set spawn for sector damage to use
Tile spawn = world.tile(sector.info.spawnPosition);
Tile spawn = world.tile(spawnPos);
spawn.setBlock(sector.planet.defaultCore, state.rules.defaultTeam);
//add extra damage.
//apply damage to simulate the sector being lost
SectorDamage.apply(1f);
//reset wave so things are more fair
state.wave = 1;
//set up default wave time
state.wavetime = state.rules.initialWaveSpacing <= 0f ? (state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier)) : state.rules.initialWaveSpacing;
state.wavetime *= sector.planet.campaignRules.difficulty.waveTimeMultiplier;
//reset captured state
sector.info.wasCaptured = false;
//save the plans and buildings from the previous save; they will be used to re-populate the sector
var previousPlans = state.rules.defaultTeam.data().plans.toArray(BlockPlan.class);
var previousBuildings = state.rules.defaultTeam.data().buildings.<Building>toArray(Building.class);
var previousDerelicts = Team.derelict.data().buildings.<Building>toArray(Building.class);
if(state.rules.sector.planet.allowWaves){
//re-enable waves
state.rules.waves = true;
//reset win wave??
state.rules.winWave = state.rules.attackMode ? -1 : sector.preset != null && sector.preset.captureWave > 0 ? sector.preset.captureWave : state.rules.winWave > state.wave ? state.rules.winWave : 30;
}
logic.reset();
//if there's still an enemy base left, fix it
if(state.rules.attackMode){
//replace all broken blocks
for(var plan : state.rules.waveTeam.data().plans){
Tile tile = world.tile(plan.x, plan.y);
if(tile != null){
tile.setBlock(plan.block, state.rules.waveTeam, plan.rotation);
if(plan.config != null && tile.build != null){
tile.build.configureAny(plan.config);
}
//now, load a fresh save; the old one was only used to grab previous building data
playNewSector(origin, sector, reloader, new WorldParams(){{
corePositionOverride = spawnPos;
}}, () -> {
var teamData = state.rules.defaultTeam.data();
//all the old derelicts have to be removed.
for(var generatedDerelict : Team.derelict.data().buildings.<Building>toArray(Building.class)){
generatedDerelict.tile.remove();
}
//retain old derelicts from the previous save.
for(var build : previousDerelicts){
Tile tile = world.tile(build.tileX(), build.tileY());
if(tile != null && tile.build == null && Build.validPlace(build.block, Team.derelict, build.tileX(), build.tileY(), build.rotation, false, false)){
tile.setBlock(build.block, Team.derelict, build.rotation, () -> build);
}
}
state.rules.waveTeam.data().plans.clear();
}
//kill all units, since they should be dead anyway
Groups.unit.clear();
Groups.fire.clear();
Groups.puddle.clear();
//copy over all buildings from the previous save, retaining config and health, and making them derelict. contents are not saved (derelict repair clears them anyway)
for(var build : previousBuildings){
Tile tile = world.tile(build.tileX(), build.tileY());
if(tile != null && tile.build == null && Build.validPlace(build.block, state.rules.defaultTeam, build.tileX(), build.tileY(), build.rotation, false, false)){
tile.setBlock(build.block, state.rules.defaultTeam, build.rotation, () -> build);
build.changeTeam(Team.derelict);
build.dropped(); //TODO: call pickedUp too? this may screw up power networks in a major way as they refer to potentially deleted entities
}
}
//reset to 0, so replaced cores don't count
state.rules.defaultTeam.data().unitCap = 0;
Schematics.placeLaunchLoadout(spawn.x, spawn.y);
for(var build : previousBuildings){
build.updateProximity();
}
//set up camera/player locations
player.set(spawn.x * tilesize, spawn.y * tilesize);
camera.position.set(player);
Events.fire(new SectorLaunchEvent(sector));
Events.fire(Trigger.newGame);
state.set(State.playing);
reloader.end();
//carry over all previous plans that don't already have the corresponding block at their position
for(var plan : previousPlans){
var build = world.build(plan.x, plan.y);
if(!(build != null && build.block == plan.block && build.tileX() == plan.x && build.tileY() == plan.y && build.team != state.rules.waveTeam)){
teamData.plans.add(plan);
}
}
});
}
}else{
state.set(State.playing);
@@ -522,12 +525,20 @@ public class Control implements ApplicationListener, Loadable{
}
public void playNewSector(@Nullable Sector origin, Sector sector, WorldReloader reloader){
playNewSector(origin, sector, reloader, new WorldParams(), null);
}
public void playNewSector(@Nullable Sector origin, Sector sector, WorldReloader reloader, WorldParams params, @Nullable Runnable beforePlay){
reloader.begin();
world.loadSector(sector);
world.loadSector(sector, params);
state.rules.sector = sector;
//assign origin when launching
sector.info.origin = origin;
sector.info.destination = origin;
if(beforePlay != null){
beforePlay.run();
}
logic.play();
control.saves.saveSector(sector);
Events.fire(new SectorLaunchEvent(sector));

View File

@@ -259,19 +259,19 @@ public class World{
}
public void loadSector(Sector sector){
loadSector(sector, 0, true);
loadSector(sector, new WorldParams());
}
public void loadSector(Sector sector, int seedOffset, boolean saveInfo){
setSectorRules(sector, saveInfo);
public void loadSector(Sector sector, WorldParams params){
setSectorRules(sector, params.saveInfo);
int size = sector.getSize();
loadGenerator(size, size, tiles -> {
if(sector.preset != null){
sector.preset.generator.generate(tiles);
sector.preset.generator.generate(tiles, params);
sector.preset.rules.get(state.rules); //apply extra rules
}else if(sector.planet.generator != null){
sector.planet.generator.generate(tiles, sector, seedOffset);
sector.planet.generator.generate(tiles, sector, params);
}else{
throw new RuntimeException("Sector " + sector.id + " on planet " + sector.planet.name + " has no generator or preset defined. Provide a planet generator or preset map.");
}
@@ -279,7 +279,7 @@ public class World{
state.rules.sector = sector;
});
if(saveInfo && state.rules.waves){
if(params.saveInfo && state.rules.waves){
sector.info.waves = state.rules.waves;
}
@@ -289,7 +289,7 @@ public class World{
}
//reset rules
setSectorRules(sector, saveInfo);
setSectorRules(sector, params.saveInfo);
if(state.rules.defaultTeam.core() != null){
sector.info.spawnPosition = state.rules.defaultTeam.core().pos();

View File

@@ -143,7 +143,7 @@ public class MapProcessorsDialog extends BaseDialog{
processors.clear();
//scan the entire world for processor (Groups.build can be empty, indexer is probably inaccurate)
//scan the entire world for processors (Groups.build can be empty, indexer is probably inaccurate)
Vars.world.tiles.eachTile(t -> {
if(t.isCenter() && t.block() == Blocks.worldProcessor){
processors.add(t.build);

View File

@@ -7,6 +7,7 @@ import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import static mindustry.Vars.*;
@@ -94,7 +95,10 @@ public class SectorGenerateDialog extends BaseDialog{
sectorobj.preset = null;
logic.reset(); //TODO: is this a good idea? all rules and map state are cleared, but it fixes inconsistent gen
world.loadSector(sectorobj, seed, false);
var params = new WorldParams();
params.seedOffset = seed;
params.saveInfo = false;
world.loadSector(sectorobj, params);
sectorobj.preset = preset;

View File

@@ -114,12 +114,35 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
if(plans.size > 1){
int total = 0;
int size = plans.size;
BuildPlan plan;
while((!within((plan = buildPlan()).tile(), finalPlaceDst) || shouldSkip(plan, core)) && total < size){
float bestDst = Float.MAX_VALUE;
boolean foundAny = false;
int bestIndex = -1;
while(total < size){
var plan = buildPlan();
float dst = plan.dst2(this);
boolean within = dst <= finalPlaceDst*finalPlaceDst;
//if it's a valid plan within range, break out of the loop
if(within && !shouldSkip(plan, core)){
foundAny = true;
break;
}else if(within && dst < bestDst){ //it's still bad, but at least it's within build radius
bestIndex = total;
bestDst = dst;
}
plans.removeFirst();
plans.addLast(plan);
total++;
}
//all the plans were useless, and the current one can't be reached. skip to the closest one, if applicable
if(!foundAny && bestIndex > 0 && !within(buildPlan(), finalPlaceDst)){
//this is slow, but should be rare in practice
for(int i = 0; i < bestIndex; i++){
plans.addLast(plans.removeFirst());
}
}
}
BuildPlan current = buildPlan();
@@ -236,10 +259,10 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
/** @return whether this plan should be skipped, in favor of the next one. */
boolean shouldSkip(BuildPlan plan, @Nullable Building core){
//plans that you have at least *started* are considered
if(state.rules.infiniteResources || team.rules().infiniteResources || plan.breaking || core == null || plan.isRotation(team) || (isBuilding() && !within(plans.last(), type.buildRange))) return false;
if(state.rules.infiniteResources || team.rules().infiniteResources || plan.breaking || core == null || plan.isRotation(team) || plan.isDerelictRepair()) return false;
return (plan.stuck && !core.items.has(plan.block.requirements)) || (Structs.contains(plan.block.requirements, i -> !core.items.has(i.item, Math.min(i.amount, 15)) && Mathf.round(i.amount * state.rules.buildCostMultiplier) > 0) && !plan.initialized);
return (plan.stuck && !core.items.has(plan.block.requirements)) ||
(Structs.contains(plan.block.requirements, i -> !core.items.has(i.item, Math.min(i.amount, 15)) && Mathf.round(i.amount * state.rules.buildCostMultiplier) > 0));
}
void removeBuild(int x, int y, boolean breaking){

View File

@@ -1333,6 +1333,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
/** Called when this block is derelict repaired. */
@CallSuper
public void onRepaired(){
placed();
}
public boolean isCommandable(){
return block.commandable;
}

View File

@@ -74,6 +74,12 @@ public class BuildPlan implements Position, QuadTreeObject{
return tile != null && tile.team() == team && tile.block() == block && tile.build != null && tile.build.rotation != rotation;
}
public boolean isDerelictRepair(){
if(breaking || !state.rules.derelictRepair) return false;
Tile tile = tile();
return tile != null && tile.team() == Team.derelict && tile.block() == block && tile.build != null;
}
public boolean samePos(BuildPlan other){
return x == other.x && y == other.y;
}

View File

@@ -41,8 +41,6 @@ public class SectorInfo{
public int storageCapacity = 0;
/** Whether a core is available here. */
public boolean hasCore = true;
/** Whether a world processor is on this map - implies that the map will get cleared. */
public boolean hasWorldProcessor;
/** Whether this sector was ever fully captured. */
public boolean wasCaptured = false;
/** Sector that was launched from. */
@@ -213,7 +211,6 @@ public class SectorInfo{
spawnPosition = entity.pos();
}
hasWorldProcessor = state.teams.present.contains(t -> t.getBuildings(Blocks.worldProcessor).any());
waveSpacing = state.rules.waveSpacing;
wave = state.wave;
winWave = state.rules.winWave;

View File

@@ -483,8 +483,8 @@ public class SectorDamage{
Tile other = tiles.getn(cx, cy);
float resultDamage = currDamage;
//damage the tile if it's not friendly
if(other.build != null && other.team() != state.rules.waveTeam){
//damage the tile if it's the player team (derelict blocks get ignored)
if(other.build != null && other.team() == state.rules.defaultTeam){
resultDamage -= other.build.health();
other.build.health -= currDamage;

View File

@@ -29,7 +29,7 @@ public abstract class BasicGenerator implements WorldGenerator{
public Schematic defaultLoadout = Loadouts.basicShard;
@Override
public void generate(Tiles tiles){
public void generate(Tiles tiles, WorldParams params){
this.tiles = tiles;
this.width = tiles.width;
this.height = tiles.height;

View File

@@ -13,14 +13,14 @@ public class BlankPlanetGenerator extends PlanetGenerator{
}
@Override
public void generate(Tiles tiles, Sector sec, int seed){
public void generate(Tiles tiles, Sector sec, WorldParams params){
this.tiles = tiles;
this.sector = sec;
this.rand.setSeed(sec.id + seed + baseSeed);
this.rand.setSeed(sec.id + params.seedOffset + baseSeed);
tiles.fill();
generate(tiles);
generate(tiles, params);
}
}

View File

@@ -9,6 +9,7 @@ import mindustry.maps.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import static mindustry.Vars.*;
@@ -38,7 +39,7 @@ public class FileMapGenerator implements WorldGenerator{
}
@Override
public void generate(Tiles tiles){
public void generate(Tiles tiles, WorldParams params){
if(map == null) throw new RuntimeException("Generator has null map, cannot be used.");
Sector sector = state.rules.sector;
@@ -72,6 +73,9 @@ public class FileMapGenerator implements WorldGenerator{
boolean anyCores = false;
//TODO: unsure if indexer even works at this stage
Block coreTypeToUse = state.rules.defaultTeam.cores().isEmpty() ? sector.planet.defaultCore : state.rules.defaultTeam.core().block;
for(Tile tile : tiles){
if(tile.overlay() == Blocks.spawn){
@@ -83,8 +87,26 @@ public class FileMapGenerator implements WorldGenerator{
});
}
if(tile.isCenter() && tile.block() instanceof CoreBlock && tile.team() == state.rules.defaultTeam && !anyCores){
if(state.rules.sector != null && state.rules.sector.allowLaunchLoadout()){
if(params.corePositionOverride != 0 && sector != null){
if(tile.pos() == params.corePositionOverride){
if(sector.allowLaunchLoadout()){
Schematics.placeLaunchLoadout(tile.x, tile.y);
}else{
//if there's an override and no loadout schematic is allowed, try to place a fitting core instead.
tile.setBlock(coreTypeToUse, state.rules.defaultTeam, 0);
}
anyCores = true;
if(preset.addStartingItems || !preset.planet.allowLaunchLoadout){
tile.build.items.clear();
tile.build.items.add(state.rules.loadout);
}
}else if(tile.build instanceof CoreBuild && tile.build.pos() != params.corePositionOverride){
//other cores placed must be cleared; they have been overridden
tile.remove();
}
}else if(tile.isCenter() && tile.block() instanceof CoreBlock && tile.team() == state.rules.defaultTeam && !anyCores){
if(sector != null && sector.allowLaunchLoadout()){
Schematics.placeLaunchLoadout(tile.x, tile.y);
}
anyCores = true;

View File

@@ -178,13 +178,13 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe
return res % 2 == 0 ? res : res + 1;
}
public void generate(Tiles tiles, Sector sec, int seed){
public void generate(Tiles tiles, Sector sec, WorldParams params){
this.tiles = tiles;
this.seed = seed + baseSeed;
this.seed = params.seedOffset + baseSeed;
this.sector = sec;
this.width = tiles.width;
this.height = tiles.height;
this.rand.setSeed(sec.id + seed + baseSeed);
this.rand.setSeed(sec.id + params.seedOffset + baseSeed);
TileGen gen = new TileGen();
for(int y = 0; y < height; y++){
@@ -197,6 +197,6 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe
}
}
generate(tiles);
generate(tiles, params);
}
}

View File

@@ -3,7 +3,7 @@ package mindustry.maps.generators;
import mindustry.world.*;
public interface WorldGenerator{
void generate(Tiles tiles);
void generate(Tiles tiles, WorldParams params);
/** Do not modify tiles here. This is only for specialized configuration. */
default void postGenerate(Tiles tiles){}

View File

@@ -15,7 +15,6 @@ import static mindustry.Vars.*;
public class IconSelectDialog extends Dialog{
private Intc consumer = i -> Log.info("you have mere seconds");
private boolean allowLocked;
public IconSelectDialog(){
this(true);

View File

@@ -1122,10 +1122,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
}
});
}else{
if(sector.save != null){
sector.save.delete();
}
sector.save = null;
sector.info.hasCore = false;
sector.saveInfo();
}
updateSelected();
rebuildList();

View File

@@ -92,26 +92,21 @@ public class Build{
//repair derelict tile
if(tile.team() == Team.derelict && team != Team.derelict && tile.block == result && tile.build != null && tile.block.allowDerelictRepair && state.rules.derelictRepair){
float healthf = tile.build.healthf();
var config = tile.build.config();
tile.setBlock(result, team, rotation);
tile.build.rotation = rotation;
tile.build.changeTeam(team);
tile.build.enabled = true;
tile.build.checkAllowUpdate();
tile.build.onRepaired();
if(unit != null && unit.getControllerName() != null) tile.build.lastAccessed = unit.getControllerName();
if(config != null){
tile.build.configured(unit, config);
}
//keep health
tile.build.health = result.health * healthf;
if(fogControl.isVisibleTile(team, tile.x, tile.y)){
result.placeEffect.at(tile.drawx(), tile.drawy(), result.size);
Fx.rotateBlock.at(tile.build.x, tile.build.y, tile.build.block.size);
//doesn't play a sound
}
Events.fire(new BlockBuildEndEvent(tile, unit, team, false, config));
Events.fire(new BlockBuildEndEvent(tile, unit, team, false, tile.build.config()));
return;
}
@@ -168,8 +163,8 @@ public class Build{
}
/** @return whether a tile can be placed at this location by this team. */
public static boolean validPlace(Block type, Team team, int x, int y, int rotation, boolean checkVisible, boolean ignoreCoreRadius){
return validPlaceIgnoreUnits(type, team, x, y, rotation, checkVisible, ignoreCoreRadius) && checkNoUnitOverlap(type, x, y);
public static boolean validPlace(Block type, Team team, int x, int y, int rotation, boolean checkVisible, boolean checkCoreRadius){
return validPlaceIgnoreUnits(type, team, x, y, rotation, checkVisible, checkCoreRadius) && checkNoUnitOverlap(type, x, y);
}
/** @return whether a tile can be placed at this location by this team. */

View File

@@ -0,0 +1,11 @@
package mindustry.world;
/** Parameters for loading or generating a world. */
public class WorldParams{
/** For sectors: World generator seed offset. */
public int seedOffset;
/** For sectors: Whether to save the info once the map is generated. A value of 'false' is used for editor generation. */
public boolean saveInfo = true;
/** Position in packed x/y format - not array format. Overrides the core position when generating with a FileMapGenerator. 0 to disable. */
public int corePositionOverride;
}

View File

@@ -283,6 +283,12 @@ public class Turret extends ReloadTurret{
}
}
//overridden so that the rotation isn't affected during repairs (standard placed() code isn't called)
@Override
public void onRepaired(){
super.placed();
}
@Override
public void remove(){
super.remove();
@@ -519,7 +525,7 @@ public class Turret extends ReloadTurret{
}
if(validateTarget()){
boolean canShoot = true;
boolean canShoot;
if(isControlled()){ //player behavior
targetPos.set(unit.aimX(), unit.aimY());