Sector refactoring, invasions and cleanup

This commit is contained in:
Anuken 2020-10-16 11:02:24 -04:00
parent 5ee4101ba4
commit 2f54edf34f
27 changed files with 319 additions and 376 deletions

View file

@ -285,6 +285,7 @@ selectschematic = [accent][[{0}][] to select+copy
pausebuilding = [accent][[{0}][] to pause building
resumebuilding = [scarlet][[{0}][] to resume building
wave = [accent]Wave {0}
wave.cap = [accent]Wave {0}/{1}
wave.waiting = [lightgray]Wave in {0}
wave.waveInProgress = [lightgray]Wave in progress
waiting = [lightgray]Waiting...
@ -521,6 +522,7 @@ sectors.resume = Resume
sectors.launch = Launch
sectors.select = Select
sectors.nonelaunch = [lightgray]none (sun)
sectors.rename = Rename Sector
planet.serpulo.name = Serpulo
#TODO better name

View file

@ -86,6 +86,10 @@ public class Vars implements Loadable{
public static final float logicItemTransferRange = 45f;
/** duration of time between turns in ticks */
public static final float turnDuration = 2 * Time.toMinutes;
/** chance of an invasion per turn, 1 = 100% */
public static final float baseInvasionChance = 1f / 15f;
/** how many turns have to pass before invasions start */
public static final int invasionGracePeriod = 20;
/** min armor fraction damage; e.g. 0.05 = at least 5% damage */
public static final float minArmorDamage = 0.1f;
/** launch animation duration */

View file

@ -279,6 +279,7 @@ public class Control implements ApplicationListener, Loadable{
slot.load();
slot.setAutosave(true);
state.rules.sector = sector;
state.secinfo = state.rules.sector.info;
//if there is no base, simulate a new game and place the right loadout at the spawn position
if(state.rules.defaultTeam.cores().isEmpty()){
@ -286,11 +287,9 @@ public class Control implements ApplicationListener, Loadable{
state.wave = 1;
//kill all units, since they should be dead anwyay
for(Unit unit : Groups.unit){
unit.remove();
}
Groups.unit.clear();
Tile spawn = world.tile(sector.getSpawnPosition());
Tile spawn = world.tile(sector.info.spawnPosition);
Schematics.placeLoadout(universe.getLastLoadout(), spawn.x, spawn.y);
//set up camera/player locations
@ -313,7 +312,6 @@ public class Control implements ApplicationListener, Loadable{
}else{
net.reset();
logic.reset();
sector.setSecondsPassed(0);
world.loadSector(sector);
state.rules.sector = sector;
//assign origin when launching

View file

@ -16,7 +16,6 @@ import mindustry.type.Weather.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.ConstructBlock.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import java.util.*;
@ -88,13 +87,10 @@ public class Logic implements ApplicationListener{
//when loading a 'damaged' sector, propagate the damage
Events.on(SaveLoadEvent.class, e -> {
if(state.isCampaign()){
CoreBuild core = state.rules.defaultTeam.core();
state.secinfo.write();
//how much wave time has passed
int wavesPassed = state.rules.sector.getWavesPassed();
//reset passed waves
state.rules.sector.setWavesPassed(0);
int wavesPassed = state.secinfo.wavesPassed;
//wave has passed, remove all enemies, they are assumed to be dead
if(wavesPassed > 0){
@ -105,44 +101,22 @@ public class Logic implements ApplicationListener{
});
}
//simulate passing of waves
if(wavesPassed > 0){
//simulate wave counter moving forward
state.wave += wavesPassed;
state.wavetime = state.rules.waveSpacing;
SectorDamage.applyCalculatedDamage();
}
//reset damage display
state.rules.sector.setDamage(0f);
//reset values
state.secinfo.damage = 0f;
state.secinfo.wavesPassed = 0;
state.secinfo.hasCore = true;
state.secinfo.secondsPassed = 0;
//simulate damage if applicable
if(wavesPassed > 0){
SectorDamage.applyCalculatedDamage(wavesPassed);
}
//waves depend on attack status.
state.rules.waves = state.rules.sector.isUnderAttack() || !state.rules.sector.hasBase();
//add resources based on turns passed
if(state.rules.sector.save != null && core != null){
//update correct storage capacity
state.rules.sector.save.meta.secinfo.storageCapacity = core.storageCapacity;
//add new items received
state.rules.sector.calculateReceivedItems().each((item, amount) -> core.items.add(item, amount));
//clear received items
state.rules.sector.setExtraItems(new ItemSeq());
//validation
for(Item item : content.items()){
//ensure positive items
if(core.items.get(item) < 0) core.items.set(item, 0);
//cap the items
if(core.items.get(item) > core.storageCapacity) core.items.set(item, core.storageCapacity);
}
}
state.rules.sector.setSecondsPassed(0);
state.rules.sector.saveInfo();
}
});
@ -200,11 +174,6 @@ public class Logic implements ApplicationListener{
}
public void skipWave(){
if(state.isCampaign()){
//warp time spent forward because the wave was just skipped.
state.secinfo.internalTimeSpent += state.wavetime;
}
state.wavetime = 0;
}

View file

@ -253,7 +253,7 @@ public class World{
setSectorRules(sector);
if(state.rules.defaultTeam.core() != null){
sector.setSpawnPosition(state.rules.defaultTeam.core().pos());
sector.info.spawnPosition = state.rules.defaultTeam.core().pos();
}
}
@ -267,8 +267,6 @@ public class World{
ObjectIntMap<Block> floorc = new ObjectIntMap<>();
ObjectSet<UnlockableContent> content = new ObjectSet<>();
float waterFloors = 0, totalFloors = 0;
for(Tile tile : world.tiles){
if(world.getDarkness(tile.x, tile.y) >= 3){
continue;
@ -280,10 +278,6 @@ public class World{
if(liquid != null) content.add(liquid);
if(!tile.block().isStatic()){
totalFloors ++;
if(liquid == Liquids.water){
waterFloors += tile.floor().isDeep() ? 1f : 0.7f;
}
floorc.increment(tile.floor());
if(tile.overlay() != Blocks.air){
floorc.increment(tile.overlay());
@ -326,9 +320,9 @@ public class World{
state.rules.weather.add(new WeatherEntry(Weathers.sporestorm));
}
state.secinfo.resources = content.asArray();
state.secinfo.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
sector.info.resources = content.asArray();
sector.info.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
sector.saveInfo();
}
public Context filterContext(Map map){

View file

@ -73,6 +73,15 @@ public class EventType{
}
}
/** Called when a sector is destroyed by waves when you're not there. */
public static class SectorInvasionEvent{
public final Sector sector;
public SectorInvasionEvent(Sector sector){
this.sector = sector;
}
}
public static class LaunchItemEvent{
public final ItemStack stack;

View file

@ -26,7 +26,7 @@ public class SectorInfo{
/** Export statistics. */
public ObjectMap<Item, ExportStat> export = new ObjectMap<>();
/** Items stored in all cores. */
public ItemSeq coreItems = new ItemSeq();
public ItemSeq items = new ItemSeq();
/** The best available core type. */
public Block bestCoreType = Blocks.air;
/** Max storage capacity. */
@ -39,13 +39,26 @@ public class SectorInfo{
public @Nullable Sector destination;
/** Resources known to occur at this sector. */
public Seq<UnlockableContent> resources = new Seq<>();
/** Whether waves are enabled here. */
public boolean waves = true;
/** Wave # from state */
public int wave = 1, winWave = -1;
/** Time between waves. */
public float waveSpacing = 60 * 60 * 2;
/** 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;
/** Display name. */
public @Nullable String name;
/** Special variables for simulation. */
public float sumHealth, sumRps, sumDps, waveHealthBase, waveHealthSlope, waveDpsBase, waveDpsSlope;
/** Time spent at this sector. Do not use unless you know what you're doing. */
public transient float internalTimeSpent;
/** Counter refresh state. */
private transient Interval time = new Interval();
/** Core item storage to prevent spoofing. */
@ -84,27 +97,55 @@ public class SectorInfo{
return export.get(item, ExportStat::new).mean;
}
/** Write contents of meta into main storage. */
public void write(){
state.wave = wave;
state.rules.waves = waves;
state.rules.waveSpacing = waveSpacing;
state.rules.winWave = winWave;
CoreBuild entity = state.rules.defaultTeam.core();
if(entity != null){
entity.items.clear();
entity.items.add(items);
//ensure capacity.
entity.items.each((i, a) -> entity.items.set(i, Math.min(a, entity.block.itemCapacity)));
}
//TODO write items.
}
/** Prepare data for writing to a save. */
public void prepare(){
//update core items
coreItems.clear();
items.clear();
CoreBuild entity = state.rules.defaultTeam.core();
if(entity != null){
ItemModule items = entity.items;
for(int i = 0; i < items.length(); i++){
coreItems.set(content.item(i), items.get(i));
this.items.set(content.item(i), items.get(i));
}
spawnPosition = entity.pos();
}
waveSpacing = state.rules.waveSpacing;
wave = state.wave;
winWave = state.rules.winWave;
waves = state.rules.waves;
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;
//update sector's internal time spent counter
state.rules.sector.setTimeSpent(internalTimeSpent);
state.rules.sector.setUnderAttack(state.rules.waves);
if(state.rules.sector != null){
state.rules.sector.info = this;
state.rules.sector.saveInfo();
}
SectorDamage.writeParameters(this);
}
@ -115,14 +156,6 @@ public class SectorInfo{
//updating in multiplayer as a client doesn't make sense
if(net.client()) return;
internalTimeSpent += Time.delta;
//autorun turns
if(internalTimeSpent >= turnDuration){
internalTimeSpent = 0;
universe.runTurn();
}
CoreBuild ent = state.rules.defaultTeam.core();
//refresh throughput

View file

@ -40,7 +40,7 @@ public class Stats{
//weigh used fractions
float frac = 0f;
Seq<Item> obtainable = zone.save == null ? new Seq<>() : zone.save.meta.secinfo.resources.select(i -> i instanceof Item).as();
Seq<Item> obtainable = zone.save == null ? new Seq<>() : zone.info.resources.select(i -> i instanceof Item).as();
for(Item item : obtainable){
frac += Mathf.clamp((float)itemsDelivered.get(item, 0) / capacity) / (float)obtainable.size;
}

View file

@ -18,6 +18,7 @@ public class Universe{
private int netSeconds;
private float secondCounter;
private int turn;
private float turnCounter;
private Schematic lastLoadout;
private ItemSeq lastLaunchResources = new ItemSeq();
@ -54,17 +55,19 @@ public class Universe{
}
}
/** @return sectors attacked on the current planet, minus the ones that are being played on right now. */
public Seq<Sector> getAttacked(Planet planet){
return planet.sectors.select(s -> s.isUnderAttack() && s.hasBase() && !s.isBeingPlayed() && s.getWavesPassed() > 0);
}
/** Update planet rotations, global time and relevant state. */
public void update(){
//only update time when not in multiplayer
if(!net.client()){
secondCounter += Time.delta / 60f;
turnCounter += Time.delta;
//auto-run turns
if(turnCounter >= turnDuration){
turnCounter = 0;
runTurn();
}
if(secondCounter >= 1){
seconds += (int)secondCounter;
@ -133,59 +136,84 @@ public class Universe{
//update relevant sectors
for(Planet planet : content.planets()){
for(Sector sector : planet.sectors){
if(sector.hasSave()){
int spent = (int)(sector.getTimeSpent() / 60);
int actuallyPassed = Math.max(newSecondsPassed - spent, 0);
if(sector.hasSave() && sector.hasBase()){
//increment seconds passed for this sector by the time that just passed with this turn
if(!sector.isBeingPlayed()){
int secPassed = sector.getSecondsPassed() + actuallyPassed;
//increment time
sector.info.secondsPassed += turnDuration/60f;
sector.setSecondsPassed(secPassed);
boolean attacked = sector.isUnderAttack();
int wavesPassed = (int)(secPassed*60f / sector.save.meta.rules.waveSpacing);
float damage = attacked ? SectorDamage.getDamage(sector.save.meta.secinfo, sector.save.meta.rules.waveSpacing, sector.save.meta.wave, wavesPassed) : 0f;
int wavesPassed = (int)(sector.info.secondsPassed*60f / sector.info.waveSpacing);
boolean attacked = sector.info.waves;
if(attacked){
sector.setWavesPassed(wavesPassed);
sector.info.wavesPassed = wavesPassed;
}
sector.setDamage(damage);
float damage = attacked ? SectorDamage.getDamage(sector.info) : 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));
//if so, just delete the save for now. it's lost.
//TODO don't delete it later maybe
sector.setExtraItems(new ItemSeq());
sector.setDamage(1.01f);
}else if(attacked && wavesPassed > 0 && sector.save.meta.wave + wavesPassed >= sector.save.meta.rules.winWave && !sector.hasEnemyBase()){
//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.wave + wavesPassed >= sector.info.winWave && !sector.hasEnemyBase()){
//autocapture the sector
sector.setUnderAttack(false);
sector.info.waves = false;
//fire the event
Events.fire(new SectorCaptureEvent(sector));
}
float scl = sector.getProductionScale();
//export to another sector
if(sector.info.destination != null){
Sector to = sector.info.destination;
if(to.hasBase()){
ItemSeq items = new ItemSeq();
//calculated exported items to this sector
sector.info.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed * scl)));
to.addItems(items);
}
}
//add production, making sure that it's capped
sector.info.production.each((item, stat) -> sector.info.items.add(item, Math.min((int)(stat.mean * seconds * scl), sector.info.storageCapacity - sector.info.items.get(item))));
sector.saveInfo();
}
//export to another sector
if(sector.save != null && sector.save.meta != null && sector.save.meta.secinfo != null && sector.save.meta.secinfo.destination != null){
Sector to = sector.save.meta.secinfo.destination;
if(to.save != null){
float scl = Math.max(1f - sector.getDamage(), 0);
ItemSeq items = new ItemSeq();
//calculated exported items to this sector
sector.save.meta.secinfo.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed * scl)));
to.addItems(items);
//queue random invasions
if(!sector.isAttacked() && turn > invasionGracePeriod){
//TODO use factors like difficulty for better invasion chance
if(sector.near().contains(Sector::hasEnemyBase) && Mathf.chance(baseInvasionChance)){
int waveMax = Math.max(sector.info.winWave, sector.isBeingPlayed() ? state.wave : 0) + Mathf.random(2, 4) * 5;
float waveSpace = Math.max(sector.info.waveSpacing - Mathf.random(1, 4) * 5 * 60, 40 * 60);
//assign invasion-related things
if(sector.isBeingPlayed()){
state.rules.winWave = waveMax;
state.rules.waves = true;
state.rules.waveSpacing = waveSpace;
}else{
sector.info.winWave = waveMax;
sector.info.waves = true;
sector.info.waveSpacing = waveSpace;
sector.saveInfo();
}
Events.fire(new SectorInvasionEvent(sector));
}
}
//reset time spent to 0
sector.setTimeSpent(0f);
}
}
}
@ -202,7 +230,7 @@ public class Universe{
for(Planet planet : content.planets()){
for(Sector sector : planet.sectors){
if(sector.hasSave()){
count.add(sector.calculateItems());
count.add(sector.items());
}
}
}

View file

@ -14,12 +14,10 @@ public class SaveMeta{
public Map map;
public int wave;
public Rules rules;
public SectorInfo secinfo;
public StringMap tags;
public String[] mods;
public boolean hasProduction;
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, SectorInfo secinfo, StringMap tags){
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, StringMap tags){
this.version = version;
this.build = build;
this.timestamp = timestamp;
@ -29,8 +27,5 @@ public class SaveMeta{
this.rules = rules;
this.tags = tags;
this.mods = JsonIO.read(String[].class, tags.get("mods", "[]"));
this.secinfo = secinfo;
secinfo.production.each((e, amount) -> hasProduction |= amount.mean > 0.001f);
}
}

View file

@ -40,7 +40,6 @@ public abstract class SaveVersion extends SaveFileReader{
map.get("mapname"),
map.getInt("wave"),
JsonIO.read(Rules.class, map.get("rules", "{}")),
JsonIO.read(SectorInfo.class, map.get("secinfo", "{}")),
map
);
}
@ -74,6 +73,7 @@ public abstract class SaveVersion extends SaveFileReader{
//prepare campaign data for writing
if(state.isCampaign()){
state.secinfo.prepare();
state.rules.sector.saveInfo();
}
//flush tech node progress
@ -89,7 +89,6 @@ public abstract class SaveVersion extends SaveFileReader{
"wave", state.wave,
"wavetime", state.wavetime,
"stats", JsonIO.write(state.stats),
"secinfo", state.isCampaign() ? JsonIO.write(state.secinfo) : "{}",
"rules", JsonIO.write(state.rules),
"mods", JsonIO.write(mods.getModStrings().toArray(String.class)),
"width", world.width(),
@ -107,14 +106,13 @@ public abstract class SaveVersion extends SaveFileReader{
state.wave = map.getInt("wave");
state.wavetime = map.getFloat("wavetime", state.rules.waveSpacing);
state.stats = JsonIO.read(Stats.class, map.get("stats", "{}"));
state.secinfo = JsonIO.read(SectorInfo.class, map.get("secinfo", "{}"));
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
if(state.rules.spawns.isEmpty()) state.rules.spawns = defaultWaves.get();
lastReadBuild = map.getInt("build", -1);
//load time spent on sector into state
//load in sector info
if(state.rules.sector != null){
state.secinfo.internalTimeSpent = state.rules.sector.getStoredTimeSpent();
state.secinfo = state.rules.sector.info;
}
if(!headless){

View file

@ -25,8 +25,11 @@ public class SectorDamage{
private static final int maxWavesSimulated = 50;
/** @return calculated capture progress of the enemy */
public static float getDamage(SectorInfo info, float waveSpace, int wave, int wavesPassed){
public static float getDamage(SectorInfo info){
float health = info.sumHealth;
int wavesPassed = info.wavesPassed;
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.
@ -76,9 +79,9 @@ public class SectorDamage{
}
/** Applies wave damage based on sector parameters. */
public static void applyCalculatedDamage(int wavesPassed){
public static void applyCalculatedDamage(){
//calculate base damage fraction
float damage = getDamage(state.secinfo, state.rules.waveSpacing, state.wave, wavesPassed);
float damage = getDamage(state.secinfo);
//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.5f);
@ -110,6 +113,21 @@ public class SectorDamage{
}
}
if(state.secinfo.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);
}
@ -120,6 +138,10 @@ public class SectorDamage{
Seq<Tile> 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;
Tile start = spawns.first();
@ -361,7 +383,7 @@ public class SectorDamage{
}
//kill every core if damage is maximum
if(damage >= 1){
if(fraction >= 1){
for(Building c : state.rules.defaultTeam.cores().copy()){
c.tile.remove();
}

View file

@ -177,7 +177,7 @@ public class Planet extends UnlockableContent{
public void updateBaseCoverage(){
for(Sector sector : sectors){
float sum = 1f;
for(Sector other : sector.inRange(2)){
for(Sector other : sector.near()){
if(other.generateEnemyBase){
sum += 1f;
}
@ -204,6 +204,10 @@ public class Planet extends UnlockableContent{
@Override
public void init(){
for(Sector sector : sectors){
sector.loadInfo();
}
if(generator != null){
Noise.setSeed(id + 1);

View file

@ -7,6 +7,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.Saves.*;
import mindustry.game.*;
import mindustry.graphics.g3d.PlanetGrid.*;
import mindustry.world.modules.*;
@ -25,6 +26,7 @@ public class Sector{
public @Nullable SaveSlot save;
public @Nullable SectorPreset preset;
public SectorInfo info = new SectorInfo();
/** Number 0-1 indicating the difficulty based on nearby bases. */
public float baseCoverage;
@ -38,60 +40,50 @@ public class Sector{
this.id = tile.id;
}
public Seq<Sector> inRange(int range){
//TODO cleanup/remove
if(true){
tmpSeq1.clear();
neighbors(tmpSeq1::add);
return tmpSeq1;
}
public Seq<Sector> near(){
tmpSeq1.clear();
tmpSeq2.clear();
tmpSet.clear();
near(tmpSeq1::add);
tmpSeq1.add(this);
tmpSet.add(this);
for(int i = 0; i < range; i++){
while(!tmpSeq1.isEmpty()){
Sector sec = tmpSeq1.pop();
tmpSet.add(sec);
sec.neighbors(other -> {
if(tmpSet.add(other)){
tmpSeq2.add(other);
}
});
}
tmpSeq1.clear();
tmpSeq1.addAll(tmpSeq2);
}
tmpSeq3.clear().addAll(tmpSeq2);
return tmpSeq3;
return tmpSeq1;
}
public void neighbors(Cons<Sector> cons){
public void near(Cons<Sector> cons){
for(Ptile tile : tile.tiles){
cons.get(planet.getSector(tile));
}
}
/** @return whether this sector can be landed on at all.
* Only sectors adjacent to non-wave sectors can be landed on.
* TODO also preset sectors*/
* Only sectors adjacent to non-wave sectors can be landed on. */
public boolean unlocked(){
return hasBase() || (preset != null && preset.alwaysUnlocked);
}
public void saveInfo(){
Core.settings.putJson(planet.name + "-s-" + id + "-info", info);
}
public void loadInfo(){
info = Core.settings.getJson(planet.name + "-s-" + id + "-info", SectorInfo.class, SectorInfo::new);
}
public float getProductionScale(){
return Math.max(1f - info.damage, 0);
}
public boolean isAttacked(){
if(isBeingPlayed()) return state.rules.waves;
return save != null && info.waves && info.hasCore;
}
/** @return whether the player has a base here. */
public boolean hasBase(){
return save != null && !save.meta.tags.getBool("nocores") && getDamage() < 1f;
return save != null && info.hasCore;
}
/** @return whether the enemy has a generated base here. */
public boolean hasEnemyBase(){
return generateEnemyBase && (save == null || save.meta.rules.waves);
return generateEnemyBase && (save == null || info.waves);
}
public boolean isBeingPlayed(){
@ -99,26 +91,18 @@ public class Sector{
return Vars.state.isGame() && Vars.state.rules.sector == this && !Vars.state.gameOver;
}
public String name(){
if(preset != null) return preset.localizedName;
return info.name == null ? id + "" : info.name;
}
public void setName(String name){
info.name = name;
saveInfo();
}
public boolean isCaptured(){
return save != null && !save.meta.rules.waves;
}
/** @return whether waves are present - if true, any bases here will be attacked.
* only applicable to sectors with active player bases. */
public boolean isUnderAttack(){
return hasBase() && Core.settings.getBool(key("under-attack"), true);
}
public void setUnderAttack(boolean underAttack){
Core.settings.put(key("under-attack"), underAttack);
}
public void setWavesPassed(int waves){
put("waves-passed", waves);
}
public int getWavesPassed(){
return Core.settings.getInt(key("waves-passed"), 0);
return save != null && !info.waves;
}
public boolean hasSave(){
@ -143,15 +127,6 @@ public class Sector{
return res % 2 == 0 ? res : res + 1;
}
//TODO this should be stored in a more efficient structure, and be updated each turn
public ItemSeq getExtraItems(){
return Core.settings.getJson(key("extra-items"), ItemSeq.class, ItemSeq::new);
}
public void setExtraItems(ItemSeq stacks){
Core.settings.putJson(key("extra-items"), stacks);
}
public void addItem(Item item, int amount){
removeItem(item, -amount);
}
@ -169,151 +144,27 @@ public class Sector{
int cap = state.rules.defaultTeam.core().storageCapacity;
items.each((item, amount) -> storage.add(item, Math.min(cap - storage.get(item), amount)));
}
}else{
ItemSeq recv = getExtraItems();
if(save != null){
//"shave off" extra items
ItemSeq count = new ItemSeq();
//add items already present
count.add(save.meta.secinfo.coreItems);
count.add(calculateReceivedItems());
int capacity = save.meta.secinfo.storageCapacity;
//when over capacity, add that to the extra items
count.each((i, a) -> {
if(a > capacity){
recv.remove(i, (a - capacity));
}
});
}
recv.add(items);
setExtraItems(recv);
}else if(hasBase()){
items.each((item, amount) -> info.items.add(item, Math.min(info.storageCapacity - info.items.get(item), amount)));
saveInfo();
}
}
public ItemSeq calculateItems(){
/** @return items currently in this sector, taking into account playing state. */
public ItemSeq items(){
ItemSeq count = new ItemSeq();
//for sectors being played on, add items directly
if(isBeingPlayed()){
count.add(state.rules.defaultTeam.items());
}else if(save != null){
}else{
//add items already present
count.add(save.meta.secinfo.coreItems);
count.add(calculateReceivedItems());
int capacity = save.meta.secinfo.storageCapacity;
//validation
count.each((item, amount) -> {
//ensure positive items
if(amount < 0) count.set(item, 0);
//cap the items
if(amount > capacity) count.set(item, capacity);
});
count.add(info.items);
}
return count;
}
public ItemSeq calculateReceivedItems(){
ItemSeq count = new ItemSeq();
if(save != null){
long seconds = getSecondsPassed();
float scl = Math.max(1f - getDamage(), 0);
//add produced items
save.meta.secinfo.production.each((item, stat) -> count.add(item, (int)(stat.mean * seconds * scl)));
//add received items
count.add(getExtraItems());
}
return count;
}
//TODO these methods should maybe move somewhere else and/or be contained in a data object
public void setSpawnPosition(int position){
put("spawn-position", position);
}
/** Only valid after this sector has been landed on once. */
//TODO move to sector data?
public int getSpawnPosition(){
return Core.settings.getInt(key("spawn-position"), Point2.pack(world.width() / 2, world.height() / 2));
}
/** @return sector damage from enemy, 0 to 1 */
public float getDamage(){
//dead sector
if(save != null & save.meta.tags.getBool("nocores")) return 1.01f;
return Core.settings.getFloat(key("damage"), 0f);
}
public void setDamage(float damage){
put("damage", damage);
}
/** @return time spent in this sector this turn in ticks. */
public float getTimeSpent(){
//return currently counting time spent if being played on
if(isBeingPlayed()) return state.secinfo.internalTimeSpent;
//else return the stored value
return getStoredTimeSpent();
}
public void setTimeSpent(float time){
put("time-spent", time);
//update counting time
if(isBeingPlayed()){
state.secinfo.internalTimeSpent = time;
}
}
public String displayTimeRemaining(){
float amount = Vars.turnDuration - getTimeSpent();
int seconds = (int)(amount / 60);
int sf = seconds % 60;
return (seconds / 60) + ":" + (sf < 10 ? "0" : "") + sf;
}
/** @return the stored amount of time spent in this sector this turn in ticks.
* Do not use unless you know what you're doing. */
public float getStoredTimeSpent(){
return Core.settings.getFloat(key("time-spent"));
}
public void setSecondsPassed(int number){
put("secondsi-passed", number);
}
/** @return how much time has passed in this sector without the player resuming here.
* Used for resource production calculations. */
public int getSecondsPassed(){
return Core.settings.getInt(key("secondsi-passed"));
}
//TODO this is terrible
private String key(String key){
return planet.name + "-s-" + id + "-" + key;
}
//TODO this is terrible
private void put(String key, Object value){
Core.settings.put(key(key), value);
}
public String toString(){
return planet.name + "#" + id;
}

View file

@ -10,7 +10,7 @@ import arc.func.*;
public class IntFormat{
private final StringBuilder builder = new StringBuilder();
private final String text;
private int lastValue = Integer.MIN_VALUE;
private int lastValue = Integer.MIN_VALUE, lastValue2 = Integer.MIN_VALUE;
private Func<Integer, String> converter = String::valueOf;
public IntFormat(String text){
@ -30,4 +30,14 @@ public class IntFormat{
lastValue = value;
return builder;
}
public CharSequence get(int value1, int value2){
if(lastValue != value1 || lastValue2 != value2){
builder.setLength(0);
builder.append(Core.bundle.format(text, value1, value2));
}
lastValue = value1;
lastValue2 = value2;
return builder;
}
}

View file

@ -23,6 +23,7 @@ import static mindustry.gen.Tex.*;
@StyleDefaults
public class Styles{
//TODO all these names are inconsistent and not descriptive
public static Drawable black, black9, black8, black6, black3, black5, none, flatDown, flatOver;
public static ButtonStyle defaultb, waveb;
public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, infot, clearPartialt, clearTogglet, clearToggleMenut, togglet, transt, fullTogglet, logict;

View file

@ -34,13 +34,6 @@ public class PausedDialog extends BaseDialog{
});
if(!mobile){
//TODO localize + move to other wave menu
cont.label(() -> state.getSector() == null || state.rules.winWave <= 0 || state.getSector().isCaptured() ? "" :
(state.rules.winWave > 0 && !state.getSector().isCaptured() ?
(state.wave >= state.rules.winWave ? "\n[lightgray]Defeat remaining enemies to capture" : "\n[lightgray]Reach wave[accent] " + state.rules.winWave + "[] to capture") : ""))
.visible(() -> state.getSector() != null).colspan(2);
cont.row();
float dw = 220f;
cont.defaults().width(dw).height(55).pad(5f);

View file

@ -217,9 +217,9 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
public void renderProjections(){
if(hovered != null){
planets.drawPlane(hovered, () -> {
Draw.color(hovered.isUnderAttack() ? Pal.remove : Color.white, Pal.accent, Mathf.absin(5f, 1f));
Draw.color(hovered.isAttacked() ? Pal.remove : Color.white, Pal.accent, Mathf.absin(5f, 1f));
TextureRegion icon = hovered.locked() && !canSelect(hovered) ? Icon.lock.getRegion() : hovered.isUnderAttack() ? Icon.warning.getRegion() : null;
TextureRegion icon = hovered.locked() && !canSelect(hovered) ? Icon.lock.getRegion() : hovered.isAttacked() ? Icon.warning.getRegion() : null;
if(icon != null){
Draw.rect(icon, 0, 0);
@ -352,69 +352,80 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
stable.clear();
stable.background(Styles.black6);
stable.add("[accent]" + (sector.preset == null ? sector.id : sector.preset.localizedName)).row();
stable.table(title -> {
title.add("[accent]" + sector.name());
if(sector.preset == null){
title.button(Icon.pencilSmall, Styles.clearPartiali, () -> {
ui.showTextInput("@sectors.rename", "@name", 20, sector.name(), v -> {
sector.setName(v);
updateSelected();
});
}).size(40f).padLeft(4);
}
}).row();
stable.image().color(Pal.accent).fillX().height(3f).pad(3f).row();
stable.add(sector.save != null ? sector.save.getPlayTime() : "@sectors.unexplored").row();
if(sector.isUnderAttack() || sector.hasEnemyBase()){
if(sector.isAttacked() || sector.hasEnemyBase()){
stable.add("[accent]Difficulty: " + (int)(sector.baseCoverage * 10)).row();
}
if(sector.isUnderAttack()){
if(sector.isAttacked()){
//TODO localize when finalized
//these mechanics are likely to change and as such are not added to the bundle
stable.add("[scarlet]Under attack!");
stable.row();
stable.add("[accent]" + (int)(sector.getDamage() * 100) + "% damaged");
stable.add("[accent]" + (int)(sector.info.damage * 100) + "% damaged");
stable.row();
}
if(sector.save != null){
stable.add("@sectors.resources").row();
stable.table(t -> {
if(sector.save != null && sector.save.meta.secinfo != null && sector.save.meta.secinfo.resources.any()){
if(sector.info.resources.any()){
t.left();
int idx = 0;
int max = 5;
for(UnlockableContent c : sector.save.meta.secinfo.resources){
for(UnlockableContent c : sector.info.resources){
t.image(c.icon(Cicon.small)).padRight(3);
if(++idx % max == 0) t.row();
}
}else{
t.add("@unknown").color(Color.lightGray);
}
}).fillX().row();
}
//production
if(sector.hasBase() && sector.save.meta.hasProduction){
stable.add("@sectors.production").row();
stable.table(t -> {
t.left();
if(sector.hasBase()){
Table t = new Table().left();
float scl = Math.max(1f - sector.getDamage(), 0);
float scl = sector.getProductionScale();
sector.save.meta.secinfo.production.each((item, stat) -> {
int total = (int)(stat.mean * 60 * scl);
if(total > 1){
t.image(item.icon(Cicon.small)).padRight(3);
t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray);
t.row();
}
});
}).row();
sector.info.production.each((item, stat) -> {
int total = (int)(stat.mean * 60 * scl);
if(total > 1){
t.image(item.icon(Cicon.small)).padRight(3);
t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray);
t.row();
}
});
if(t.getChildren().any()){
stable.add("@sectors.production").row();
stable.add(t);
}
}
//stored resources
if(sector.hasBase() && sector.save.meta.secinfo.coreItems.total > 0){
if(sector.hasBase() && sector.info.items.total > 0){
stable.add("@sectors.stored").row();
stable.table(t -> {
t.left();
t.table(res -> {
ItemSeq items = sector.calculateItems();
ItemSeq items = sector.items();
int i = 0;
for(ItemStack stack : items){

View file

@ -60,7 +60,7 @@ public class ResearchDialog extends BaseDialog{
for(Planet planet : content.planets()){
for(Sector sector : planet.sectors){
if(sector.hasSave()){
ItemSeq cached = sector.calculateItems();
ItemSeq cached = sector.items();
add(cached);
cache.put(sector, cached);
}

View file

@ -71,12 +71,17 @@ public class HudFragment extends Fragment{
//TODO details and stuff
Events.on(SectorCaptureEvent.class, e ->{
//TODO localize
showToast("Sector [accent]" + (e.sector.isBeingPlayed() ? "" : e.sector.id + " ") + "[]captured!");
showToast("Sector [accent]" + (e.sector.isBeingPlayed() ? "" : e.sector.name() + " ") + "[white]captured!");
});
//TODO localize
Events.on(SectorLoseEvent.class, e -> {
showToast(Icon.warning, "Sector " + e.sector.id + " [scarlet]lost!");
showToast(Icon.warning, "Sector [accent]" + e.sector.name() + "[white] lost!");
});
//TODO localize
Events.on(SectorInvasionEvent.class, e -> {
showToast(Icon.warning, "Sector [accent]" + e.sector.name() + "[white] under attack!");
});
Events.on(ResetEvent.class, e -> {
@ -589,6 +594,7 @@ public class HudFragment extends Fragment{
StringBuilder ibuild = new StringBuilder();
IntFormat wavef = new IntFormat("wave");
IntFormat wavefc = new IntFormat("wave.cap");
IntFormat enemyf = new IntFormat("wave.enemy");
IntFormat enemiesf = new IntFormat("wave.enemies");
IntFormat waitingf = new IntFormat("wave.waiting", i -> {
@ -714,7 +720,11 @@ public class HudFragment extends Fragment{
table.labelWrap(() -> {
builder.setLength(0);
builder.append(wavef.get(state.wave));
if(state.rules.winWave > 1 && state.rules.winWave >= state.wave && state.isCampaign()){
builder.append(wavefc.get(state.wave, state.rules.winWave));
}else{
builder.append(wavef.get(state.wave));
}
builder.append("\n");
if(state.enemies > 0){
@ -727,7 +737,7 @@ public class HudFragment extends Fragment{
}
if(state.rules.waveTimer){
builder.append((logic.isWaitingWave() ? Core.bundle.get("wave.waveInProgress") : ( waitingf.get((int)(state.wavetime/60)))));
builder.append((logic.isWaitingWave() ? Core.bundle.get("wave.waveInProgress") : (waitingf.get((int)(state.wavetime/60)))));
}else if(state.enemies == 0){
builder.append(Core.bundle.get("waiting"));
}

View file

@ -267,6 +267,10 @@ public class Tile implements Position, QuadTreeObject, Displayable{
Geometry.circle(x, y, world.width(), world.height(), radius, cons);
}
public void circle(int radius, Cons<Tile> cons){
circle(radius, (x, y) -> cons.get(world.rawTile(x, y)));
}
public void recache(){
if(!headless && !world.isGenerating()){
renderer.blocks.floor.recacheTile(this);
@ -332,6 +336,11 @@ public class Tile implements Position, QuadTreeObject, Displayable{
recache();
}
/** Sets the overlay without a recache. */
public void setOverlayQuiet(Block block){
this.overlay = (Floor)block;
}
public void clearOverlay(){
setOverlayID((short)0);
}

View file

@ -121,9 +121,7 @@ public class LaunchPad extends Block{
return Core.bundle.format("launch.destination",
dest == null ? Core.bundle.get("sectors.nonelaunch") :
dest.preset == null ?
"[accent]Sector " + dest.id :
"[accent]" + dest.preset.localizedName);
"[accent]" + dest.name());
}).pad(4);
}
@ -213,7 +211,7 @@ public class LaunchPad extends Block{
//actually launch the items upon removal
if(team() == state.rules.defaultTeam){
if(destsec != null && (destsec != state.rules.sector || net.client())){
ItemSeq dest = destsec.getExtraItems();
ItemSeq dest = new ItemSeq();
for(ItemStack stack : stacks){
dest.add(stack);
@ -223,7 +221,7 @@ public class LaunchPad extends Block{
Events.fire(new LaunchItemEvent(stack));
}
destsec.setExtraItems(dest);
destsec.addItems(dest);
}
}
}

View file

@ -156,7 +156,7 @@ public class Conveyor extends Block implements Autotiler{
lastInserted = build.lastInserted;
mid = build.mid;
minitem = build.minitem;
items.addAll(build.items);
items.add(build.items);
}
}

View file

@ -203,7 +203,7 @@ public class StackConveyor extends Block implements Autotiler{
if(front() instanceof StackConveyorBuild e && e.team == team){
// sleep if its occupied
if(e.link == -1){
e.items.addAll(items);
e.items.add(items);
e.lastItem = lastItem;
e.link = tile.pos();
// to | from

View file

@ -70,7 +70,7 @@ public class StorageBlock extends Block{
public void overwrote(Seq<Building> previous){
for(Building other : previous){
if(other.items != null){
items.addAll(other.items);
items.add(other.items);
}
}

View file

@ -243,6 +243,16 @@ public class ItemModule extends BlockModule{
}
}
public void add(ItemSeq stacks){
stacks.each(this::add);
}
public void add(ItemModule items){
for(int i = 0; i < items.items.length; i++){
add(i, items.items[i]);
}
}
public void add(Item item, int amount){
add(item.id, amount);
}
@ -261,12 +271,6 @@ public class ItemModule extends BlockModule{
}
}
public void addAll(ItemModule items){
for(int i = 0; i < items.items.length; i++){
add(i, items.items[i]);
}
}
public void remove(Item item, int amount){
amount = Math.min(amount, items[item.id]);

View file

@ -153,9 +153,9 @@ public class Generators{
ImagePacker.generate("cracks", () -> {
RidgedPerlin r = new RidgedPerlin(1, 3);
for(int size = 1; size <= Block.maxCrackSize; size++){
for(int size = 1; size <= BlockRenderer.maxCrackSize; size++){
int dim = size * 32;
int steps = Block.crackRegions;
int steps = BlockRenderer.crackRegions;
for(int i = 0; i < steps; i++){
float fract = i / (float)steps;