diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 1253de6a13..aa515bfc6f 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -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 diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 6fdc1418dd..d88b6584f7 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -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 */ diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index e4b4e3d44f..f6ae999945 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -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 diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 3829c98cb6..00787d9e43 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -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; } diff --git a/core/src/mindustry/core/World.java b/core/src/mindustry/core/World.java index 03a18c01ed..d6fcb40a97 100644 --- a/core/src/mindustry/core/World.java +++ b/core/src/mindustry/core/World.java @@ -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 floorc = new ObjectIntMap<>(); ObjectSet 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){ diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 272c26363f..0ffb2f7369 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -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; diff --git a/core/src/mindustry/game/SectorInfo.java b/core/src/mindustry/game/SectorInfo.java index 8e7f73a256..c6c5d3ee51 100644 --- a/core/src/mindustry/game/SectorInfo.java +++ b/core/src/mindustry/game/SectorInfo.java @@ -26,7 +26,7 @@ public class SectorInfo{ /** Export statistics. */ public ObjectMap 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 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 diff --git a/core/src/mindustry/game/Stats.java b/core/src/mindustry/game/Stats.java index 1d175607ba..b963223518 100644 --- a/core/src/mindustry/game/Stats.java +++ b/core/src/mindustry/game/Stats.java @@ -40,7 +40,7 @@ public class Stats{ //weigh used fractions float frac = 0f; - Seq obtainable = zone.save == null ? new Seq<>() : zone.save.meta.secinfo.resources.select(i -> i instanceof Item).as(); + Seq 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; } diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 8eb9a8d2af..37a22d4209 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -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 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()); } } } diff --git a/core/src/mindustry/io/SaveMeta.java b/core/src/mindustry/io/SaveMeta.java index f2e039f634..c6133889bc 100644 --- a/core/src/mindustry/io/SaveMeta.java +++ b/core/src/mindustry/io/SaveMeta.java @@ -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); } } diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index cacac7700a..e153b74587 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -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){ diff --git a/core/src/mindustry/maps/SectorDamage.java b/core/src/mindustry/maps/SectorDamage.java index fb7ee28e8f..1c942d1228 100644 --- a/core/src/mindustry/maps/SectorDamage.java +++ b/core/src/mindustry/maps/SectorDamage.java @@ -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 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(); } diff --git a/core/src/mindustry/type/Planet.java b/core/src/mindustry/type/Planet.java index 6153aa4721..911939b870 100644 --- a/core/src/mindustry/type/Planet.java +++ b/core/src/mindustry/type/Planet.java @@ -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); diff --git a/core/src/mindustry/type/Sector.java b/core/src/mindustry/type/Sector.java index db3587674b..37535bffca 100644 --- a/core/src/mindustry/type/Sector.java +++ b/core/src/mindustry/type/Sector.java @@ -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 inRange(int range){ - //TODO cleanup/remove - if(true){ - tmpSeq1.clear(); - neighbors(tmpSeq1::add); - - return tmpSeq1; - } - + public Seq 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 cons){ + public void near(Cons 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; } diff --git a/core/src/mindustry/ui/IntFormat.java b/core/src/mindustry/ui/IntFormat.java index 2ca0e9de1f..fab10cd67f 100644 --- a/core/src/mindustry/ui/IntFormat.java +++ b/core/src/mindustry/ui/IntFormat.java @@ -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 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; + } } diff --git a/core/src/mindustry/ui/Styles.java b/core/src/mindustry/ui/Styles.java index 13e97202e5..802a18004b 100644 --- a/core/src/mindustry/ui/Styles.java +++ b/core/src/mindustry/ui/Styles.java @@ -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; diff --git a/core/src/mindustry/ui/dialogs/PausedDialog.java b/core/src/mindustry/ui/dialogs/PausedDialog.java index f06c09ce30..5bd1e5abd3 100644 --- a/core/src/mindustry/ui/dialogs/PausedDialog.java +++ b/core/src/mindustry/ui/dialogs/PausedDialog.java @@ -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); diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 44ef8eb17a..9ad8c87831 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -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){ diff --git a/core/src/mindustry/ui/dialogs/ResearchDialog.java b/core/src/mindustry/ui/dialogs/ResearchDialog.java index bb6841dcbb..b79aacbce5 100644 --- a/core/src/mindustry/ui/dialogs/ResearchDialog.java +++ b/core/src/mindustry/ui/dialogs/ResearchDialog.java @@ -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); } diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index d37095be54..327e65cd13 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -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")); } diff --git a/core/src/mindustry/world/Tile.java b/core/src/mindustry/world/Tile.java index 7478f81db4..8fdfd6c009 100644 --- a/core/src/mindustry/world/Tile.java +++ b/core/src/mindustry/world/Tile.java @@ -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 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); } diff --git a/core/src/mindustry/world/blocks/campaign/LaunchPad.java b/core/src/mindustry/world/blocks/campaign/LaunchPad.java index 093fff4f70..dd1cc69007 100644 --- a/core/src/mindustry/world/blocks/campaign/LaunchPad.java +++ b/core/src/mindustry/world/blocks/campaign/LaunchPad.java @@ -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); } } } diff --git a/core/src/mindustry/world/blocks/distribution/Conveyor.java b/core/src/mindustry/world/blocks/distribution/Conveyor.java index 457e7e65e7..60ab45f159 100644 --- a/core/src/mindustry/world/blocks/distribution/Conveyor.java +++ b/core/src/mindustry/world/blocks/distribution/Conveyor.java @@ -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); } } diff --git a/core/src/mindustry/world/blocks/distribution/StackConveyor.java b/core/src/mindustry/world/blocks/distribution/StackConveyor.java index e7095a686c..3327c62c15 100644 --- a/core/src/mindustry/world/blocks/distribution/StackConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/StackConveyor.java @@ -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 ▼ diff --git a/core/src/mindustry/world/blocks/storage/StorageBlock.java b/core/src/mindustry/world/blocks/storage/StorageBlock.java index 50eb65a270..a8a5578d95 100644 --- a/core/src/mindustry/world/blocks/storage/StorageBlock.java +++ b/core/src/mindustry/world/blocks/storage/StorageBlock.java @@ -70,7 +70,7 @@ public class StorageBlock extends Block{ public void overwrote(Seq previous){ for(Building other : previous){ if(other.items != null){ - items.addAll(other.items); + items.add(other.items); } } diff --git a/core/src/mindustry/world/modules/ItemModule.java b/core/src/mindustry/world/modules/ItemModule.java index 16d49484f0..6cd96985cb 100644 --- a/core/src/mindustry/world/modules/ItemModule.java +++ b/core/src/mindustry/world/modules/ItemModule.java @@ -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]); diff --git a/tools/src/mindustry/tools/Generators.java b/tools/src/mindustry/tools/Generators.java index d8f67eee8c..645b2ea7f0 100644 --- a/tools/src/mindustry/tools/Generators.java +++ b/tools/src/mindustry/tools/Generators.java @@ -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;