Merge branch 'master' into planetdraw
This commit is contained in:
@@ -36,6 +36,8 @@ public class Vars implements Loadable{
|
||||
public static boolean loadLocales = true;
|
||||
/** Whether the logger is loaded. */
|
||||
public static boolean loadedLogger = false, loadedFileLogger = false;
|
||||
/** Whether to show the cliff button in the editor*/
|
||||
public static boolean addCliffButton = false;
|
||||
/** Maximum extra padding around deployment schematics. */
|
||||
public static final int maxLoadoutSchematicPad = 5;
|
||||
/** Maximum schematic size.*/
|
||||
@@ -86,6 +88,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 / 25f;
|
||||
/** 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 */
|
||||
|
||||
@@ -40,11 +40,11 @@ public class BaseAI{
|
||||
}
|
||||
|
||||
public void update(){
|
||||
if(timer.get(timerSpawn, 60) && data.hasCore()){
|
||||
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 2.5f) && data.hasCore()){
|
||||
CoreBlock block = (CoreBlock)data.core().block;
|
||||
|
||||
//create AI core unit
|
||||
if(!state.isEditor() && !Groups.unit.contains(u -> u.team() == data.team && u.type() == block.unitType)){
|
||||
if(!state.isEditor() && !Groups.unit.contains(u -> u.team() == data.team && u.type == block.unitType)){
|
||||
Unit unit = block.unitType.create(data.team);
|
||||
unit.set(data.core());
|
||||
unit.add();
|
||||
|
||||
@@ -48,7 +48,7 @@ public class WaveSpawner{
|
||||
|
||||
/** @return true if the player is near a ground spawn point. */
|
||||
public boolean playerNear(){
|
||||
return !player.dead() && spawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius && player.team() != state.rules.waveTeam);
|
||||
return state.rules.waves && !player.dead() && spawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius && player.team() != state.rules.waveTeam);
|
||||
}
|
||||
|
||||
public void spawnEnemies(){
|
||||
@@ -175,7 +175,7 @@ public class WaveSpawner{
|
||||
}
|
||||
|
||||
private void spawnEffect(Unit unit){
|
||||
Call.spawnEffect(unit.x, unit.y, unit.type());
|
||||
Call.spawnEffect(unit.x, unit.y, unit.type);
|
||||
Time.run(30f, unit::add);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ public class BuilderAI extends AIController{
|
||||
float dist = Math.min(cons.dst(unit) - buildingRange, 0);
|
||||
|
||||
//make sure you can reach the request in time
|
||||
if(dist / unit.type().speed < cons.buildCost * 0.9f){
|
||||
if(dist / unit.type.speed < cons.buildCost * 0.9f){
|
||||
following = b;
|
||||
found = true;
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public class BuilderAI extends AIController{
|
||||
|
||||
@Override
|
||||
public AIController fallback(){
|
||||
return unit.type().flying ? new FlyingAI() : new GroundAI();
|
||||
return unit.type.flying ? new FlyingAI() : new GroundAI();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FlyingAI extends AIController{
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(target != null && unit.hasWeapons() && command() == UnitCommand.attack){
|
||||
if(unit.type().weapons.first().rotate){
|
||||
if(unit.type.weapons.first().rotate){
|
||||
moveTo(target, unit.range() * 0.8f);
|
||||
unit.lookAt(target);
|
||||
}else{
|
||||
@@ -57,7 +57,7 @@ public class FlyingAI extends AIController{
|
||||
vec.setAngle(Mathf.slerpDelta(unit.vel().angle(), vec.angle(), 0.6f));
|
||||
}
|
||||
|
||||
vec.setLength(unit.type().speed);
|
||||
vec.setLength(unit.type.speed);
|
||||
|
||||
unit.moveAt(vec);
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ public class FormationAI extends AIController implements FormationMember{
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
UnitType type = unit.type();
|
||||
UnitType type = unit.type;
|
||||
|
||||
if(leader.dead){
|
||||
unit.resetController();
|
||||
return;
|
||||
}
|
||||
|
||||
if(unit.type().canBoost && unit.canPassOn()){
|
||||
if(unit.type.canBoost && unit.canPassOn()){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, 0.08f);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class FormationAI extends AIController implements FormationMember{
|
||||
|
||||
unit.aim(leader.aimX(), leader.aimY());
|
||||
|
||||
if(unit.type().rotateShooting){
|
||||
if(unit.type.rotateShooting){
|
||||
unit.lookAt(leader.aimX(), leader.aimY());
|
||||
}else if(unit.moving()){
|
||||
unit.lookAt(unit.vel.angle());
|
||||
@@ -65,7 +65,7 @@ public class FormationAI extends AIController implements FormationMember{
|
||||
|
||||
CoreBuild core = unit.team.core();
|
||||
|
||||
if(core != null && com.mineTile().drop() != null && unit.within(core, unit.type().range) && !unit.acceptsItem(com.mineTile().drop())){
|
||||
if(core != null && com.mineTile().drop() != null && unit.within(core, unit.type.range) && !unit.acceptsItem(com.mineTile().drop())){
|
||||
if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){
|
||||
Call.transferItemTo(unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ public class GroundAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
if(unit.type().canBoost && !unit.onSolid()){
|
||||
if(unit.type.canBoost && !unit.onSolid()){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, 0.08f);
|
||||
}
|
||||
|
||||
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type().rotateShooting){
|
||||
if(unit.type().hasWeapons()){
|
||||
unit.lookAt(Predict.intercept(unit, target, unit.type().weapons.first().bullet.speed));
|
||||
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type.rotateShooting){
|
||||
if(unit.type.hasWeapons()){
|
||||
unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
|
||||
}
|
||||
}else if(unit.moving()){
|
||||
unit.lookAt(unit.vel().angle());
|
||||
|
||||
@@ -98,7 +98,7 @@ public class LogicAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
if(unit.type().canBoost && !unit.type().flying){
|
||||
if(unit.type.canBoost && !unit.type.flying){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid()), 0.08f);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public class LogicAI extends AIController{
|
||||
|
||||
@Override
|
||||
protected boolean shouldShoot(){
|
||||
return shoot && !(unit.type().canBoost && boost);
|
||||
return shoot && !(unit.type.canBoost && boost);
|
||||
}
|
||||
|
||||
//always aim for the main target
|
||||
|
||||
@@ -19,7 +19,7 @@ public class MinerAI extends AIController{
|
||||
|
||||
if(!(unit instanceof Minerc miner) || core == null) return;
|
||||
|
||||
if(miner.mineTile() != null && !miner.mineTile().within(unit, unit.type().range)){
|
||||
if(miner.mineTile() != null && !miner.mineTile().within(unit, unit.type.range)){
|
||||
miner.mineTile(null);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class MinerAI extends AIController{
|
||||
}
|
||||
|
||||
//if inventory is full, drop it off.
|
||||
if(unit.stack.amount >= unit.type().itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
|
||||
if(unit.stack.amount >= unit.type.itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
|
||||
mining = false;
|
||||
}else{
|
||||
if(retarget() && targetItem != null){
|
||||
@@ -44,9 +44,9 @@ public class MinerAI extends AIController{
|
||||
}
|
||||
|
||||
if(ore != null){
|
||||
moveTo(ore, unit.type().range / 2f);
|
||||
moveTo(ore, unit.type.range / 2f);
|
||||
|
||||
if(unit.within(ore, unit.type().range)){
|
||||
if(unit.within(ore, unit.type.range)){
|
||||
miner.mineTile(ore);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class MinerAI extends AIController{
|
||||
return;
|
||||
}
|
||||
|
||||
if(unit.within(core, unit.type().range)){
|
||||
if(unit.within(core, unit.type.range)){
|
||||
if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){
|
||||
Call.transferItemTo(unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ public class MinerAI extends AIController{
|
||||
mining = true;
|
||||
}
|
||||
|
||||
circle(core, unit.type().range / 1.8f);
|
||||
circle(core, unit.type.range / 1.8f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public class RepairAI extends AIController{
|
||||
if(target instanceof Building){
|
||||
boolean shoot = false;
|
||||
|
||||
if(target.within(unit, unit.type().range)){
|
||||
if(target.within(unit, unit.type.range)){
|
||||
unit.aim(target);
|
||||
shoot = true;
|
||||
}
|
||||
@@ -23,8 +23,8 @@ public class RepairAI extends AIController{
|
||||
}
|
||||
|
||||
if(target != null){
|
||||
if(!target.within(unit, unit.type().range * 0.65f) && target instanceof Building){
|
||||
moveTo(target, unit.type().range * 0.65f);
|
||||
if(!target.within(unit, unit.type.range * 0.65f) && target instanceof Building){
|
||||
moveTo(target, unit.type.range * 0.65f);
|
||||
}
|
||||
|
||||
unit.lookAt(target);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SuicideAI extends GroundAI{
|
||||
}
|
||||
|
||||
if(retarget()){
|
||||
target = target(unit.x, unit.y, unit.range(), unit.type().targetAir, unit.type().targetGround);
|
||||
target = target(unit.x, unit.y, unit.range(), unit.type.targetAir, unit.type.targetGround);
|
||||
}
|
||||
|
||||
Building core = unit.closestEnemyCore();
|
||||
@@ -30,11 +30,11 @@ public class SuicideAI extends GroundAI{
|
||||
|
||||
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.hasWeapons()){
|
||||
rotate = true;
|
||||
shoot = unit.within(target, unit.type().weapons.first().bullet.range() +
|
||||
shoot = unit.within(target, unit.type.weapons.first().bullet.range() +
|
||||
(target instanceof Building ? ((Building)target).block.size * Vars.tilesize / 2f : ((Hitboxc)target).hitSize() / 2f));
|
||||
|
||||
if(unit.type().hasWeapons()){
|
||||
unit.aimLook(Predict.intercept(unit, target, unit.type().weapons.first().bullet.speed));
|
||||
if(unit.type.hasWeapons()){
|
||||
unit.aimLook(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
|
||||
}
|
||||
|
||||
//do not move toward walls or transport blocks
|
||||
@@ -65,7 +65,7 @@ public class SuicideAI extends GroundAI{
|
||||
if(!blocked){
|
||||
moveToTarget = true;
|
||||
//move towards target directly
|
||||
unit.moveAt(vec.set(target).sub(unit).limit(unit.type().speed));
|
||||
unit.moveAt(vec.set(target).sub(unit).limit(unit.type.speed));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
PhysicRef ref = entity.physref();
|
||||
|
||||
ref.body.layer =
|
||||
entity.type().allowLegStep ? layerLegs :
|
||||
entity.type.allowLegStep ? layerLegs :
|
||||
entity.isGrounded() ? layerGround : layerFlying;
|
||||
ref.x = entity.x();
|
||||
ref.y = entity.y();
|
||||
|
||||
@@ -56,7 +56,7 @@ public class Fx{
|
||||
|
||||
mixcol(Pal.accent, 1f);
|
||||
alpha(e.fout());
|
||||
rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type().icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f);
|
||||
rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type.icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f);
|
||||
alpha(1f);
|
||||
Lines.stroke(e.fslope() * 1f);
|
||||
Lines.square(select.x, select.y, e.fout() * select.hitSize * 2f, 45);
|
||||
@@ -66,7 +66,7 @@ public class Fx{
|
||||
}),
|
||||
|
||||
unitDespawn = new Effect(100f, e -> {
|
||||
if(!(e.data instanceof Unit) || e.<Unit>data().type() == null) return;
|
||||
if(!(e.data instanceof Unit) || e.<Unit>data().type == null) return;
|
||||
|
||||
Unit select = e.data();
|
||||
float scl = e.fout(Interp.pow2Out);
|
||||
@@ -74,7 +74,7 @@ public class Fx{
|
||||
Draw.scl *= scl;
|
||||
|
||||
mixcol(Pal.accent, 1f);
|
||||
rect(select.type().icon(Cicon.full), select.x, select.y, select.rotation - 90f);
|
||||
rect(select.type.icon(Cicon.full), select.x, select.y, select.rotation - 90f);
|
||||
reset();
|
||||
|
||||
Draw.scl = p;
|
||||
|
||||
@@ -18,7 +18,7 @@ public class SectorPresets implements ContentList{
|
||||
groundZero = new SectorPreset("groundZero", serpulo, 15){{
|
||||
alwaysUnlocked = true;
|
||||
captureWave = 10;
|
||||
difficulty = 0;
|
||||
difficulty = 1;
|
||||
}};
|
||||
|
||||
saltFlats = new SectorPreset("saltFlats", serpulo, 101){{
|
||||
@@ -26,23 +26,23 @@ public class SectorPresets implements ContentList{
|
||||
}};
|
||||
|
||||
frozenForest = new SectorPreset("frozenForest", serpulo, 86){{
|
||||
captureWave = 40;
|
||||
difficulty = 1;
|
||||
captureWave = 20;
|
||||
difficulty = 2;
|
||||
}};
|
||||
|
||||
craters = new SectorPreset("craters", serpulo, 18){{
|
||||
captureWave = 40;
|
||||
captureWave = 20;
|
||||
difficulty = 2;
|
||||
}};
|
||||
|
||||
ruinousShores = new SectorPreset("ruinousShores", serpulo, 19){{
|
||||
captureWave = 40;
|
||||
captureWave = 30;
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
stainedMountains = new SectorPreset("stainedMountains", serpulo, 20){{
|
||||
captureWave = 30;
|
||||
difficulty = 2;
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
fungalPass = new SectorPreset("fungalPass", serpulo, 21){{
|
||||
@@ -54,7 +54,7 @@ public class SectorPresets implements ContentList{
|
||||
}};
|
||||
|
||||
tarFields = new SectorPreset("tarFields", serpulo, 23){{
|
||||
captureWave = 40;
|
||||
captureWave = 50;
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
|
||||
@@ -872,7 +872,6 @@ public class UnitTypes implements ContentList{
|
||||
drag = 0.01f;
|
||||
flying = true;
|
||||
health = 75;
|
||||
faceTarget = false;
|
||||
engineOffset = 5.5f;
|
||||
range = 140f;
|
||||
|
||||
@@ -1449,13 +1448,13 @@ public class UnitTypes implements ContentList{
|
||||
trailMult = 0.8f;
|
||||
hitEffect = Fx.massiveExplosion;
|
||||
knockback = 1.5f;
|
||||
lifetime = 140f;
|
||||
lifetime = 100f;
|
||||
height = 15.5f;
|
||||
width = 15f;
|
||||
collidesTiles = false;
|
||||
ammoMultiplier = 4f;
|
||||
splashDamageRadius = 60f;
|
||||
splashDamage = 85f;
|
||||
splashDamage = 80f;
|
||||
backColor = Pal.missileYellowBack;
|
||||
frontColor = Pal.missileYellow;
|
||||
trailEffect = Fx.artilleryTrail;
|
||||
|
||||
@@ -279,19 +279,29 @@ 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()){
|
||||
|
||||
//no spawn set -> delete the sector save
|
||||
if(sector.info.spawnPosition == 0){
|
||||
//delete old save
|
||||
sector.save = null;
|
||||
slot.delete();
|
||||
//play again
|
||||
playSector(origin, sector);
|
||||
return;
|
||||
}
|
||||
|
||||
//reset wave so things are more fair
|
||||
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());
|
||||
Schematics.placeLoadout(universe.getLastLoadout(), spawn.x, spawn.y);
|
||||
Tile spawn = world.tile(sector.info.spawnPosition);
|
||||
Schematics.placeLaunchLoadout(spawn.x, spawn.y);
|
||||
|
||||
//set up camera/player locations
|
||||
player.set(spawn.x * tilesize, spawn.y * tilesize);
|
||||
@@ -313,7 +323,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
|
||||
|
||||
@@ -14,9 +14,6 @@ import mindustry.maps.*;
|
||||
import mindustry.type.*;
|
||||
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.*;
|
||||
|
||||
@@ -41,32 +38,7 @@ public class Logic implements ApplicationListener{
|
||||
//skip null entities or un-rebuildables, for obvious reasons; also skip client since they can't modify these requests
|
||||
if(tile.build == null || !tile.block().rebuildable || net.client()) return;
|
||||
|
||||
if(block instanceof ConstructBlock){
|
||||
|
||||
ConstructBuild entity = tile.bc();
|
||||
|
||||
//update block to reflect the fact that something was being constructed
|
||||
if(entity.cblock != null && entity.cblock.synthetic()){
|
||||
block = entity.cblock;
|
||||
}else{
|
||||
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TeamData data = state.teams.get(tile.team());
|
||||
|
||||
//remove existing blocks that have been placed here.
|
||||
//painful O(n) iteration + copy
|
||||
for(int i = 0; i < data.blocks.size; i++){
|
||||
BlockPlan b = data.blocks.get(i);
|
||||
if(b.x == tile.x && b.y == tile.y){
|
||||
data.blocks.removeIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
data.blocks.addFirst(new BlockPlan(tile.x, tile.y, (short)tile.build.rotation, block.id, tile.build.config()));
|
||||
tile.build.addPlan(true);
|
||||
});
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, event -> {
|
||||
@@ -88,13 +60,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,50 +74,33 @@ 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();
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(WorldLoadEvent.class, e -> {
|
||||
//enable infinite ammo for wave team by default
|
||||
state.rules.waveTeam.rules().infiniteAmmo = true;
|
||||
if(state.isCampaign()){
|
||||
//enable building AI
|
||||
state.rules.waveTeam.rules().ai = true;
|
||||
state.rules.waveTeam.rules().infiniteResources = true;
|
||||
}
|
||||
|
||||
//save settings
|
||||
Core.settings.manualSave();
|
||||
@@ -200,11 +152,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;
|
||||
}
|
||||
|
||||
@@ -232,9 +179,12 @@ public class Logic implements ApplicationListener{
|
||||
}
|
||||
|
||||
//if there's a "win" wave and no enemies are present, win automatically
|
||||
if(state.rules.waves && state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()){
|
||||
if(state.rules.waves && (state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()) ||
|
||||
(state.rules.attackMode && state.rules.waveTeam.cores().isEmpty())){
|
||||
//the sector has been conquered - waves get disabled
|
||||
state.rules.waves = false;
|
||||
//disable attack mode
|
||||
state.rules.attackMode = false;
|
||||
|
||||
//fire capture event
|
||||
Events.fire(new SectorCaptureEvent(state.rules.sector));
|
||||
@@ -322,7 +272,7 @@ public class Logic implements ApplicationListener{
|
||||
|
||||
if(state.isGame()){
|
||||
if(!net.client()){
|
||||
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.type().isCounted);
|
||||
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.type.isCounted);
|
||||
}
|
||||
|
||||
if(!state.isPaused()){
|
||||
|
||||
@@ -575,7 +575,7 @@ public class NetServer implements ApplicationListener{
|
||||
shooting = false;
|
||||
}
|
||||
|
||||
if(!player.dead() && (player.unit().type().flying || !player.unit().type().canBoost)){
|
||||
if(!player.dead() && (player.unit().type.flying || !player.unit().type.canBoost)){
|
||||
boosting = false;
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ public class NetServer implements ApplicationListener{
|
||||
Unit unit = player.unit();
|
||||
|
||||
long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime);
|
||||
float maxSpeed = ((player.unit().type().canBoost && player.unit().isFlying()) ? player.unit().type().boostMultiplier : 1f) * player.unit().type().speed;
|
||||
float maxSpeed = ((player.unit().type.canBoost && player.unit().isFlying()) ? player.unit().type.boostMultiplier : 1f) * player.unit().type.speed;
|
||||
if(unit.isGrounded()){
|
||||
maxSpeed *= unit.floorSpeedMultiplier();
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -4,6 +4,7 @@ import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.editor.DrawOperation.*;
|
||||
@@ -180,6 +181,52 @@ public class MapEditor{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addCliffs(){
|
||||
for(Tile tile : world.tiles){
|
||||
if(!tile.block().isStatic() || tile.block() == Blocks.cliff) continue;
|
||||
|
||||
int rotation = 0;
|
||||
for(int i = 0; i < 8; i++){
|
||||
Tile other = world.tiles.get(tile.x + Geometry.d8[i].x, tile.y + Geometry.d8[i].y);
|
||||
if(other != null && !other.block().isStatic()){
|
||||
rotation |= (1 << i);
|
||||
}
|
||||
}
|
||||
|
||||
if(rotation != 0){
|
||||
tile.setBlock(Blocks.cliff);
|
||||
}
|
||||
|
||||
tile.data = (byte)rotation;
|
||||
}
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
if(tile.block() != Blocks.cliff && tile.block().isStatic()){
|
||||
tile.setBlock(Blocks.air);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addFloorCliffs(){
|
||||
for(Tile tile : world.tiles){
|
||||
if(!tile.floor().hasSurface() || tile.block() == Blocks.cliff) continue;
|
||||
|
||||
int rotation = 0;
|
||||
for(int i = 0; i < 8; i++){
|
||||
Tile other = world.tiles.get(tile.x + Geometry.d8[i].x, tile.y + Geometry.d8[i].y);
|
||||
if(other != null && !other.floor().hasSurface()){
|
||||
rotation |= (1 << i);
|
||||
}
|
||||
}
|
||||
|
||||
if(rotation != 0){
|
||||
tile.setBlock(Blocks.cliff);
|
||||
}
|
||||
|
||||
tile.data = (byte)rotation;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawCircle(int x, int y, Cons<Tile> drawer){
|
||||
for(int rx = -brushSize; rx <= brushSize; rx++){
|
||||
for(int ry = -brushSize; ry <= brushSize; ry++){
|
||||
|
||||
@@ -385,7 +385,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
}
|
||||
|
||||
public void build(){
|
||||
float size = 60f;
|
||||
float size = 58f;
|
||||
|
||||
clearChildren();
|
||||
table(cont -> {
|
||||
@@ -559,10 +559,19 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
mid.row();
|
||||
|
||||
mid.table(t -> {
|
||||
t.button("@editor.center", Icon.move, Styles.cleart, () -> view.center()).growX().margin(9f);
|
||||
}).growX().top();
|
||||
if(!mobile){
|
||||
mid.table(t -> {
|
||||
t.button("@editor.center", Icon.move, Styles.cleart, view::center).growX().margin(9f);
|
||||
}).growX().top();
|
||||
}
|
||||
|
||||
if(addCliffButton){
|
||||
mid.row();
|
||||
|
||||
mid.table(t -> {
|
||||
t.button("Cliffs", Icon.terrain, Styles.cleart, editor::addCliffs).growX().margin(9f);
|
||||
}).growX().top();
|
||||
}
|
||||
}).margin(0).left().growY();
|
||||
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ public class Units{
|
||||
|
||||
nearby(x, y, width, height, unit -> {
|
||||
if(boolResult) return;
|
||||
if((unit.isGrounded() && !unit.type().hovering) == ground){
|
||||
if((unit.isGrounded() && !unit.type.hovering) == ground){
|
||||
unit.hitbox(hitrect);
|
||||
|
||||
if(hitrect.overlaps(x, y, width, height)){
|
||||
|
||||
@@ -104,6 +104,8 @@ public abstract class BulletType extends Content{
|
||||
public float incendChance = 1f;
|
||||
public float homingPower = 0f;
|
||||
public float homingRange = 50f;
|
||||
/** Use a negative value to disable homing delay. */
|
||||
public float homingDelay = -1f;
|
||||
|
||||
public Color lightningColor = Pal.surge;
|
||||
public int lightning;
|
||||
@@ -260,7 +262,7 @@ public abstract class BulletType extends Content{
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(homingPower > 0.0001f){
|
||||
if(homingPower > 0.0001f && b.time >= homingDelay){
|
||||
Teamc target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> (e.isGrounded() && collidesGround) || (e.isFlying() && collidesAir), t -> collidesGround);
|
||||
if(target != null){
|
||||
b.vel.setAngle(Mathf.slerpDelta(b.rotation(), b.angleTo(target), homingPower));
|
||||
|
||||
@@ -39,6 +39,11 @@ public class LaserBulletType extends BulletType{
|
||||
this(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float estimateDPS(){
|
||||
return super.estimateDPS() * 2f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
super.init();
|
||||
|
||||
@@ -22,6 +22,8 @@ public class LiquidBulletType extends BulletType{
|
||||
if(liquid != null){
|
||||
this.liquid = liquid;
|
||||
this.status = liquid.effect;
|
||||
lightColor = liquid.lightColor;
|
||||
lightOpacity = liquid.lightColor.a;
|
||||
}
|
||||
|
||||
ammoMultiplier = 1f;
|
||||
|
||||
@@ -21,12 +21,14 @@ import mindustry.ctype.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.ConstructBlock.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.blocks.power.*;
|
||||
@@ -191,6 +193,36 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
//endregion
|
||||
//region utility methods
|
||||
|
||||
public void addPlan(boolean checkPrevious){
|
||||
if(!block.rebuildable) return;
|
||||
|
||||
if(self() instanceof ConstructBuild entity){
|
||||
//update block to reflect the fact that something was being constructed
|
||||
if(entity.cblock != null && entity.cblock.synthetic()){
|
||||
block = entity.cblock;
|
||||
}else{
|
||||
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TeamData data = state.teams.get(team);
|
||||
|
||||
if(checkPrevious){
|
||||
//remove existing blocks that have been placed here.
|
||||
//painful O(n) iteration + copy
|
||||
for(int i = 0; i < data.blocks.size; i++){
|
||||
BlockPlan b = data.blocks.get(i);
|
||||
if(b.x == tile.x && b.y == tile.y){
|
||||
data.blocks.removeIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.blocks.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, block.id, config()));
|
||||
}
|
||||
|
||||
/** Configure with the current, local player. */
|
||||
public void configure(Object value){
|
||||
//save last used config
|
||||
@@ -1263,7 +1295,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
case type -> block;
|
||||
case firstItem -> items == null ? null : items.first();
|
||||
case config -> block.configurations.containsKey(Item.class) || block.configurations.containsKey(Liquid.class) ? config() : null;
|
||||
case payloadType -> getPayload() instanceof UnitPayload p1 ? p1.unit.type() : getPayload() instanceof BuildPayload p2 ? p2.block() : null;
|
||||
case payloadType -> getPayload() instanceof UnitPayload p1 ? p1.unit.type : getPayload() instanceof BuildPayload p2 ? p2.block() : null;
|
||||
default -> noSensed;
|
||||
};
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ abstract class CommanderComp implements Entityc, Posc{
|
||||
units.clear();
|
||||
|
||||
Units.nearby(team, x, y, 150f, u -> {
|
||||
if(u.isAI() && include.get(u) && u != self() && u.type().flying == type.flying && u.hitSize <= hitSize * 1.1f){
|
||||
if(u.isAI() && include.get(u) && u != self() && u.type.flying == type.flying && u.hitSize <= hitSize * 1.1f){
|
||||
units.add(u);
|
||||
}
|
||||
});
|
||||
@@ -82,7 +82,7 @@ abstract class CommanderComp implements Entityc, Posc{
|
||||
FormationAI ai;
|
||||
unit.controller(ai = new FormationAI(self(), formation));
|
||||
spacing = Math.max(spacing, ai.formationSize());
|
||||
minFormationSpeed = Math.min(minFormationSpeed, unit.type().speed);
|
||||
minFormationSpeed = Math.min(minFormationSpeed, unit.type.speed);
|
||||
}
|
||||
this.formation = formation;
|
||||
|
||||
@@ -106,7 +106,7 @@ abstract class CommanderComp implements Entityc, Posc{
|
||||
//reset controlled units
|
||||
for(Unit unit : controlling){
|
||||
if(unit.controller().isBeingControlled(self())){
|
||||
unit.controller(unit.type().createController());
|
||||
unit.controller(unit.type.createController());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
admin = typing = false;
|
||||
textFadeTime = 0f;
|
||||
if(!dead()){
|
||||
unit.controller(unit.type().createController());
|
||||
unit.controller(unit.type.createController());
|
||||
unit = Nulls.unit;
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
|
||||
@Replace
|
||||
public float clipSize(){
|
||||
return unit.isNull() ? 20 : unit.type().hitSize * 2f;
|
||||
return unit.isNull() ? 20 : unit.type.hitSize * 2f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,7 +123,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
deathTimer = 0;
|
||||
|
||||
//update some basic state to sync things
|
||||
if(unit.type().canBoost){
|
||||
if(unit.type.canBoost){
|
||||
Tile tile = unit.tileOn();
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, (tile != null && tile.solid()) || boosting ? 1f : 0f, 0.08f);
|
||||
}
|
||||
@@ -177,7 +177,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
|
||||
if(this.unit != Nulls.unit){
|
||||
//un-control the old unit
|
||||
this.unit.controller(this.unit.type().createController());
|
||||
this.unit.controller(this.unit.type.createController());
|
||||
}
|
||||
this.unit = unit;
|
||||
if(unit != Nulls.unit){
|
||||
|
||||
@@ -74,7 +74,7 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc{
|
||||
unit.apply(liquid.effect, 60 * 2);
|
||||
|
||||
if(unit.vel.len() > 0.1){
|
||||
Fx.ripple.at(unit.x, unit.y, unit.type().rippleScale, liquid.color);
|
||||
Fx.ripple.at(unit.x, unit.y, unit.type.rippleScale, liquid.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
@Import int id;
|
||||
|
||||
private UnitController controller;
|
||||
private UnitType type;
|
||||
UnitType type;
|
||||
boolean spawnedByCore;
|
||||
double flag;
|
||||
|
||||
@@ -110,7 +110,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
case firstItem -> stack().amount == 0 ? null : item();
|
||||
case payloadType -> self() instanceof Payloadc pay ?
|
||||
(pay.payloads().isEmpty() ? null :
|
||||
pay.payloads().peek() instanceof UnitPayload p1 ? p1.unit.type() :
|
||||
pay.payloads().peek() instanceof UnitPayload p1 ? p1.unit.type :
|
||||
pay.payloads().peek() instanceof BuildPayload p2 ? p2.block() : null) : null;
|
||||
default -> noSensed;
|
||||
};
|
||||
@@ -163,22 +163,12 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
|
||||
@Override
|
||||
public void set(UnitType def, UnitController controller){
|
||||
type(type);
|
||||
if(this.type != def){
|
||||
setType(def);
|
||||
}
|
||||
controller(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void type(UnitType type){
|
||||
if(this.type == type) return;
|
||||
|
||||
setStats(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnitType type(){
|
||||
return type;
|
||||
}
|
||||
|
||||
/** @return pathfinder path type for calculating costs */
|
||||
public int pathType(){
|
||||
return Pathfinder.costGround;
|
||||
@@ -208,7 +198,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
return Units.getCap(team);
|
||||
}
|
||||
|
||||
public void setStats(UnitType type){
|
||||
public void setType(UnitType type){
|
||||
this.type = type;
|
||||
this.maxHealth = type.health;
|
||||
this.drag = type.drag;
|
||||
@@ -226,7 +216,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
@Override
|
||||
public void afterSync(){
|
||||
//set up type info after reading
|
||||
setStats(this.type);
|
||||
setType(this.type);
|
||||
controller.unit(self());
|
||||
}
|
||||
|
||||
@@ -286,7 +276,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
drag = type.drag * (isGrounded() ? (floorOn().dragMultiplier) : 1f);
|
||||
|
||||
//apply knockback based on spawns
|
||||
if(team != state.rules.waveTeam){
|
||||
if(team != state.rules.waveTeam && state.rules.waves){
|
||||
float relativeSize = state.rules.dropZoneRadius + hitSize/2f + 1f;
|
||||
for(Tile spawn : spawner.getSpawns()){
|
||||
if(within(spawn.worldx(), spawn.worldy(), relativeSize)){
|
||||
|
||||
@@ -95,7 +95,7 @@ public class AIController implements UnitController{
|
||||
|
||||
if(tile == targetTile || (costType == Pathfinder.costWater && !targetTile.floor().isLiquid)) return;
|
||||
|
||||
unit.moveAt(vec.trns(unit.angleTo(targetTile), unit.type().speed));
|
||||
unit.moveAt(vec.trns(unit.angleTo(targetTile), unit.type.speed));
|
||||
}
|
||||
|
||||
protected void updateWeapons(){
|
||||
@@ -105,7 +105,7 @@ public class AIController implements UnitController{
|
||||
boolean ret = retarget();
|
||||
|
||||
if(ret){
|
||||
target = findTarget(unit.x, unit.y, unit.range(), unit.type().targetAir, unit.type().targetGround);
|
||||
target = findTarget(unit.x, unit.y, unit.range(), unit.type.targetAir, unit.type.targetGround);
|
||||
}
|
||||
|
||||
if(invalid(target)){
|
||||
@@ -119,7 +119,7 @@ public class AIController implements UnitController{
|
||||
float mountX = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y),
|
||||
mountY = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y);
|
||||
|
||||
if(unit.type().singleTarget){
|
||||
if(unit.type.singleTarget){
|
||||
targets[i] = target;
|
||||
}else{
|
||||
if(ret){
|
||||
@@ -176,7 +176,7 @@ public class AIController implements UnitController{
|
||||
}
|
||||
|
||||
protected void circle(Position target, float circleLength){
|
||||
circle(target, circleLength, unit.type().speed);
|
||||
circle(target, circleLength, unit.type.speed);
|
||||
}
|
||||
|
||||
protected void circle(Position target, float circleLength, float speed){
|
||||
|
||||
@@ -81,7 +81,7 @@ public class DefaultWaves{
|
||||
effect = StatusEffects.overdrive;
|
||||
}},
|
||||
|
||||
new SpawnGroup(mace){{
|
||||
new SpawnGroup(pulsar){{
|
||||
begin = 120;
|
||||
spacing = 2;
|
||||
unitScaling = 3;
|
||||
@@ -122,7 +122,7 @@ public class DefaultWaves{
|
||||
shieldScaling = 30;
|
||||
}},
|
||||
|
||||
new SpawnGroup(dagger){{
|
||||
new SpawnGroup(nova){{
|
||||
begin = 35;
|
||||
spacing = 3;
|
||||
unitAmount = 4;
|
||||
@@ -233,7 +233,7 @@ public class DefaultWaves{
|
||||
shieldScaling = 20f;
|
||||
}},
|
||||
|
||||
new SpawnGroup(atrax){{
|
||||
new SpawnGroup(toxopid){{
|
||||
begin = 210;
|
||||
unitAmount = 1;
|
||||
unitScaling = 1;
|
||||
@@ -258,7 +258,7 @@ public class DefaultWaves{
|
||||
{nova, pulsar, quasar, vela, corvus},
|
||||
{crawler, atrax, spiroct, arkyid, toxopid},
|
||||
//{risso, minke, bryde, sei, omura}, //questionable choices
|
||||
//{mono, poly, mega, quad, oct}, //do not attack
|
||||
{poly, poly, mega, quad, quad},
|
||||
{flare, horizon, zenith, antumbra, eclipse}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,13 @@ public class EventType{
|
||||
preDraw,
|
||||
postDraw,
|
||||
uiDrawBegin,
|
||||
uiDrawEnd
|
||||
uiDrawEnd,
|
||||
//before/after bloom used, skybox or planets drawn
|
||||
universeDrawBegin,
|
||||
//skybox drawn and bloom is enabled - use Vars.renderer.planets
|
||||
universeDraw,
|
||||
//planets drawn and bloom disabled
|
||||
universeDrawEnd
|
||||
}
|
||||
|
||||
public static class WinEvent{}
|
||||
@@ -73,6 +79,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;
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ public class Rules{
|
||||
public boolean ai;
|
||||
/** TODO Tier of blocks/designs that the AI uses for building. [0, 1]*/
|
||||
public float aiTier = 0f;
|
||||
/** Whether, when AI is enabled, ships should be spawned from the core. */
|
||||
public boolean aiCoreSpawn = true;
|
||||
/** If true, blocks don't require power or resources. */
|
||||
public boolean cheat;
|
||||
/** If true, resources are not consumed when building. */
|
||||
|
||||
@@ -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,28 @@ 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;
|
||||
/** Whether attack mode is enabled here. */
|
||||
public boolean attack = false;
|
||||
/** 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 +99,66 @@ public class SectorInfo{
|
||||
return export.get(item, ExportStat::new).mean;
|
||||
}
|
||||
|
||||
/** Write contents of meta into main storage. */
|
||||
public void write(){
|
||||
//enable attack mode when there's a core.
|
||||
if(state.rules.waveTeam.core() != null){
|
||||
attack = true;
|
||||
winWave = 0;
|
||||
}
|
||||
|
||||
//if there are infinite waves and no win wave, add a win wave.
|
||||
if(waves && winWave <= 0 && !attack){
|
||||
winWave = 30;
|
||||
}
|
||||
|
||||
state.wave = wave;
|
||||
state.rules.waves = waves;
|
||||
state.rules.waveSpacing = waveSpacing;
|
||||
state.rules.winWave = winWave;
|
||||
state.rules.attackMode = attack;
|
||||
|
||||
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.storageCapacity)));
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
attack = state.rules.attackMode;
|
||||
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 +169,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
|
||||
|
||||
@@ -9,6 +9,8 @@ import mindustry.gen.*;
|
||||
import mindustry.io.legacy.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/**
|
||||
@@ -138,4 +140,20 @@ public class SpawnGroup implements Serializable{
|
||||
", items=" + items +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o){
|
||||
if(this == o) return true;
|
||||
if(o == null || getClass() != o.getClass()) return false;
|
||||
SpawnGroup group = (SpawnGroup)o;
|
||||
return end == group.end && begin == group.begin && spacing == group.spacing && max == group.max
|
||||
&& Float.compare(group.unitScaling, unitScaling) == 0 && Float.compare(group.shields, shields) == 0
|
||||
&& Float.compare(group.shieldScaling, shieldScaling) == 0 && unitAmount == group.unitAmount &&
|
||||
type == group.type && effect == group.effect && Structs.eq(items, group.items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode(){
|
||||
return Arrays.hashCode(new Object[]{type, end, begin, spacing, max, unitScaling, shields, shieldScaling, unitAmount, effect, items});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public class Teams{
|
||||
}
|
||||
|
||||
private void count(Unit unit){
|
||||
unit.team.data().updateCount(unit.type(), 1);
|
||||
unit.team.data().updateCount(unit.type, 1);
|
||||
|
||||
if(unit instanceof Payloadc){
|
||||
((Payloadc)unit).payloads().each(p -> {
|
||||
@@ -178,15 +178,15 @@ public class Teams{
|
||||
data.units.add(unit);
|
||||
data.presentFlag = true;
|
||||
|
||||
if(data.unitsByType == null || data.unitsByType.length <= unit.type().id){
|
||||
if(data.unitsByType == null || data.unitsByType.length <= unit.type.id){
|
||||
data.unitsByType = new Seq[content.units().size];
|
||||
}
|
||||
|
||||
if(data.unitsByType[unit.type().id] == null){
|
||||
data.unitsByType[unit.type().id] = new Seq<>();
|
||||
if(data.unitsByType[unit.type.id] == null){
|
||||
data.unitsByType[unit.type.id] = new Seq<>();
|
||||
}
|
||||
|
||||
data.unitsByType[unit.type().id].add(unit);
|
||||
data.unitsByType[unit.type.id].add(unit);
|
||||
|
||||
count(unit);
|
||||
}
|
||||
|
||||
@@ -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,81 @@ 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.winWave > 1 && 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){
|
||||
//invasion chance depends on # of nearby bases
|
||||
if(Mathf.chance(baseInvasionChance * sector.near().count(Sector::hasEnemyBase))){
|
||||
int waveMax = Math.max(sector.info.winWave, sector.isBeingPlayed() ? state.wave : 0) + Mathf.random(2, 5) * 5;
|
||||
|
||||
//assign invasion-related things
|
||||
if(sector.isBeingPlayed()){
|
||||
state.rules.winWave = waveMax;
|
||||
state.rules.waves = true;
|
||||
}else{
|
||||
sector.info.winWave = waveMax;
|
||||
sector.info.waves = true;
|
||||
sector.saveInfo();
|
||||
}
|
||||
|
||||
Events.fire(new SectorInvasionEvent(sector));
|
||||
}
|
||||
}
|
||||
|
||||
//reset time spent to 0
|
||||
sector.setTimeSpent(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +227,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class BlockRenderer implements Disposable{
|
||||
private FrameBuffer dark = new FrameBuffer();
|
||||
private Seq<Building> outArray2 = new Seq<>();
|
||||
private Seq<Tile> shadowEvents = new Seq<>();
|
||||
private IntSet processedEntities = new IntSet(), processedLinks = new IntSet();
|
||||
private IntSet procEntities = new IntSet(), procLinks = new IntSet(), procLights = new IntSet();
|
||||
private boolean displayStatus = false;
|
||||
|
||||
public BlockRenderer(){
|
||||
@@ -191,8 +191,9 @@ public class BlockRenderer implements Disposable{
|
||||
|
||||
tileview.clear();
|
||||
lightview.clear();
|
||||
processedEntities.clear();
|
||||
processedLinks.clear();
|
||||
procEntities.clear();
|
||||
procLinks.clear();
|
||||
procLights.clear();
|
||||
|
||||
int minx = Math.max(avgx - rangex - expandr, 0);
|
||||
int miny = Math.max(avgy - rangey - expandr, 0);
|
||||
@@ -209,25 +210,25 @@ public class BlockRenderer implements Disposable{
|
||||
tile = tile.build.tile;
|
||||
}
|
||||
|
||||
if(block != Blocks.air && block.cacheLayer == CacheLayer.normal && (tile.build == null || !processedEntities.contains(tile.build.id))){
|
||||
if(block != Blocks.air && block.cacheLayer == CacheLayer.normal && (tile.build == null || !procEntities.contains(tile.build.id))){
|
||||
if(block.expanded || !expanded){
|
||||
if(tile.build == null || processedLinks.add(tile.build.id)){
|
||||
if(tile.build == null || procLinks.add(tile.build.id)){
|
||||
tileview.add(tile);
|
||||
if(tile.build != null){
|
||||
processedEntities.add(tile.build.id);
|
||||
processedLinks.add(tile.build.id);
|
||||
procEntities.add(tile.build.id);
|
||||
procLinks.add(tile.build.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//lights are drawn even in the expanded range
|
||||
if(tile.build != null || tile.block().emitLight){
|
||||
if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){
|
||||
lightview.add(tile);
|
||||
}
|
||||
|
||||
if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){
|
||||
for(Building other : tile.build.getPowerConnections(outArray2)){
|
||||
if(other.block instanceof PowerNode && processedLinks.add(other.id)){ //TODO need a generic way to render connections!
|
||||
if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections!
|
||||
tileview.add(other.tile);
|
||||
}
|
||||
}
|
||||
@@ -235,7 +236,7 @@ public class BlockRenderer implements Disposable{
|
||||
}
|
||||
|
||||
//special case for floors
|
||||
if(block == Blocks.air && tile.floor().emitLight){
|
||||
if((block == Blocks.air && tile.floor().emitLight) && procLights.add(tile.pos())){
|
||||
lightview.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ public class LightRenderer{
|
||||
|
||||
Draw.vert(ledge.texture, vertices, 0, vertices.length);
|
||||
|
||||
|
||||
Vec2 v3 = Tmp.v2.trnsExact(rot, stroke);
|
||||
|
||||
u = ledge.u;
|
||||
|
||||
@@ -96,7 +96,7 @@ public class MinimapRenderer implements Disposable{
|
||||
|
||||
Draw.mixcol(unit.team().color, 1f);
|
||||
float scale = Scl.scl(1f) / 2f * scaling * 32f;
|
||||
Draw.rect(unit.type().icon(Cicon.full), x + rx, y + ry, scale, scale, unit.rotation() - 90);
|
||||
Draw.rect(unit.type.icon(Cicon.full), x + rx, y + ry, scale, scale, unit.rotation() - 90);
|
||||
Draw.reset();
|
||||
|
||||
//only disable player names in multiplayer
|
||||
|
||||
@@ -85,7 +85,7 @@ public class OverlayRenderer{
|
||||
//special selection for block "units"
|
||||
Fill.square(select.x, select.y, ((BlockUnitc)select).tile().block.size * tilesize/2f);
|
||||
}else{
|
||||
Draw.rect(select.type().icon(Cicon.full), select.x(), select.y(), select.rotation() - 90);
|
||||
Draw.rect(select.type.icon(Cicon.full), select.x(), select.y(), select.rotation() - 90);
|
||||
}
|
||||
|
||||
Lines.stroke(unitFade);
|
||||
@@ -121,10 +121,12 @@ public class OverlayRenderer{
|
||||
Lines.stroke(2f);
|
||||
Draw.color(Color.gray, Color.lightGray, Mathf.absin(Time.time(), 8f, 1f));
|
||||
|
||||
for(Tile tile : spawner.getSpawns()){
|
||||
if(tile.within(player.x, player.y, state.rules.dropZoneRadius + spawnerMargin)){
|
||||
Draw.alpha(Mathf.clamp(1f - (player.dst(tile) - state.rules.dropZoneRadius) / spawnerMargin));
|
||||
Lines.dashCircle(tile.worldx(), tile.worldy(), state.rules.dropZoneRadius);
|
||||
if(state.rules.waves){
|
||||
for(Tile tile : spawner.getSpawns()){
|
||||
if(tile.within(player.x, player.y, state.rules.dropZoneRadius + spawnerMargin)){
|
||||
Draw.alpha(Mathf.clamp(1f - (player.dst(tile) - state.rules.dropZoneRadius) / spawnerMargin));
|
||||
Lines.dashCircle(tile.worldx(), tile.worldy(), state.rules.dropZoneRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,16 @@ public class PlanetGrid{
|
||||
{5, 3, 10, 1, 4}, {2, 5, 4, 0, 11}, {3, 7, 6, 1, 8}, {7, 2, 9, 0, 6}
|
||||
};
|
||||
|
||||
public final int size;
|
||||
public final Ptile[] tiles;
|
||||
public final Corner[] corners;
|
||||
public final Edge[] edges;
|
||||
public int size;
|
||||
public Ptile[] tiles;
|
||||
public Corner[] corners;
|
||||
public Edge[] edges;
|
||||
|
||||
PlanetGrid(int size){
|
||||
//this is protected so if you want to make strange grids you should know what you're doing.
|
||||
protected PlanetGrid(int size){
|
||||
this.size = size;
|
||||
|
||||
tiles = new Ptile[Buildingount(size)];
|
||||
tiles = new Ptile[tileCount(size)];
|
||||
for(int i = 0; i < tiles.length; i++){
|
||||
tiles[i] = new Ptile(i, i < 12 ? 5 : 6);
|
||||
}
|
||||
@@ -67,7 +68,7 @@ public class PlanetGrid{
|
||||
return result;
|
||||
}
|
||||
|
||||
static PlanetGrid initialGrid(){
|
||||
public static PlanetGrid initialGrid(){
|
||||
PlanetGrid grid = new PlanetGrid(0);
|
||||
|
||||
for(Ptile t : grid.tiles){
|
||||
@@ -111,7 +112,7 @@ public class PlanetGrid{
|
||||
return grid;
|
||||
}
|
||||
|
||||
static PlanetGrid subdividedGrid(PlanetGrid prev){
|
||||
public static PlanetGrid subdividedGrid(PlanetGrid prev){
|
||||
PlanetGrid grid = new PlanetGrid(prev.size + 1);
|
||||
|
||||
int prevTiles = prev.tiles.length;
|
||||
@@ -207,7 +208,7 @@ public class PlanetGrid{
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int Buildingount(int size){
|
||||
static int tileCount(int size){
|
||||
return 10 * Mathf.pow(3, size) + 2;
|
||||
}
|
||||
|
||||
@@ -220,12 +221,12 @@ public class PlanetGrid{
|
||||
}
|
||||
|
||||
public static class Ptile{
|
||||
public final int id;
|
||||
public final int edgeCount;
|
||||
public int id;
|
||||
public int edgeCount;
|
||||
|
||||
public final Ptile[] tiles;
|
||||
public final Corner[] corners;
|
||||
public final Edge[] edges;
|
||||
public Ptile[] tiles;
|
||||
public Corner[] corners;
|
||||
public Edge[] edges;
|
||||
|
||||
public Vec3 v = new Vec3();
|
||||
|
||||
@@ -240,11 +241,11 @@ public class PlanetGrid{
|
||||
}
|
||||
|
||||
public static class Corner{
|
||||
public final int id;
|
||||
public final Ptile[] tiles = new Ptile[3];
|
||||
public final Corner[] corners = new Corner[3];
|
||||
public final Edge[] edges = new Edge[3];
|
||||
public final Vec3 v = new Vec3();
|
||||
public int id;
|
||||
public Ptile[] tiles = new Ptile[3];
|
||||
public Corner[] corners = new Corner[3];
|
||||
public Edge[] edges = new Edge[3];
|
||||
public Vec3 v = new Vec3();
|
||||
|
||||
public Corner(int id){
|
||||
this.id = id;
|
||||
@@ -252,9 +253,9 @@ public class PlanetGrid{
|
||||
}
|
||||
|
||||
public static class Edge{
|
||||
public final int id;
|
||||
public final Ptile[] tiles = new Ptile[2];
|
||||
public final Corner[] corners = new Corner[2];
|
||||
public int id;
|
||||
public Ptile[] tiles = new Ptile[2];
|
||||
public Corner[] corners = new Corner[2];
|
||||
|
||||
public Edge(int id){
|
||||
this.id = id;
|
||||
|
||||
@@ -10,6 +10,7 @@ import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.graphics.g3d.PlanetGrid.*;
|
||||
import mindustry.type.*;
|
||||
@@ -38,19 +39,19 @@ public class PlanetRenderer implements Disposable{
|
||||
public float zoom = 1f;
|
||||
|
||||
private final Mesh[] outlines = new Mesh[10];
|
||||
private final PlaneBatch3D projector = new PlaneBatch3D();
|
||||
private final Mat3D mat = new Mat3D();
|
||||
private final FrameBuffer buffer = new FrameBuffer(2, 2, true);
|
||||
private PlanetInterfaceRenderer irenderer;
|
||||
public final PlaneBatch3D projector = new PlaneBatch3D();
|
||||
public final Mat3D mat = new Mat3D();
|
||||
public final FrameBuffer buffer = new FrameBuffer(2, 2, true);
|
||||
public PlanetInterfaceRenderer irenderer;
|
||||
|
||||
private final Bloom bloom = new Bloom(Core.graphics.getWidth()/4, Core.graphics.getHeight()/4, true, false){{
|
||||
public final Bloom bloom = new Bloom(Core.graphics.getWidth()/4, Core.graphics.getHeight()/4, true, false){{
|
||||
setThreshold(0.8f);
|
||||
blurPasses = 6;
|
||||
}};
|
||||
private final Mesh atmosphere = MeshBuilder.buildHex(Color.white, 2, false, 1.5f);
|
||||
public final Mesh atmosphere = MeshBuilder.buildHex(Color.white, 2, false, 1.5f);
|
||||
|
||||
//seed: 8kmfuix03fw
|
||||
private final CubemapMesh skybox = new CubemapMesh(new Cubemap("cubemaps/stars/"));
|
||||
public final CubemapMesh skybox = new CubemapMesh(new Cubemap("cubemaps/stars/"));
|
||||
|
||||
public PlanetRenderer(){
|
||||
camPos.set(0, 0f, camLength);
|
||||
@@ -82,14 +83,20 @@ public class PlanetRenderer implements Disposable{
|
||||
projector.proj(cam.combined);
|
||||
batch.proj(cam.combined);
|
||||
|
||||
Events.fire(Trigger.universeDrawBegin);
|
||||
|
||||
beginBloom();
|
||||
|
||||
skybox.render(cam.combined);
|
||||
|
||||
Events.fire(Trigger.universeDraw);
|
||||
|
||||
renderPlanet(solarSystem);
|
||||
|
||||
endBloom();
|
||||
|
||||
Events.fire(Trigger.universeDrawEnd);
|
||||
|
||||
Gl.enable(Gl.blend);
|
||||
|
||||
irenderer.renderProjections();
|
||||
@@ -100,18 +107,19 @@ public class PlanetRenderer implements Disposable{
|
||||
cam.update();
|
||||
}
|
||||
|
||||
private void beginBloom(){
|
||||
public void beginBloom(){
|
||||
bloom.resize(Core.graphics.getWidth() / 4, Core.graphics.getHeight() / 4);
|
||||
bloom.capture();
|
||||
}
|
||||
|
||||
private void endBloom(){
|
||||
public void endBloom(){
|
||||
bloom.render();
|
||||
}
|
||||
|
||||
private void renderPlanet(Planet planet){
|
||||
if(!planet.visible()) return;
|
||||
|
||||
public void renderPlanet(Planet planet){
|
||||
if(!planet.visible()) return;
|
||||
|
||||
//render planet at offsetted position in the world
|
||||
planet.draw(cam.combined, planet.getTransform(mat));
|
||||
|
||||
@@ -139,7 +147,7 @@ public class PlanetRenderer implements Disposable{
|
||||
}
|
||||
}
|
||||
|
||||
private void renderOrbit(Planet planet){
|
||||
public void renderOrbit(Planet planet){
|
||||
if(planet.parent == null || !planet.visible()) return;
|
||||
|
||||
Vec3 center = planet.parent.position;
|
||||
@@ -149,7 +157,7 @@ public class PlanetRenderer implements Disposable{
|
||||
batch.flush(Gl.lineLoop);
|
||||
}
|
||||
|
||||
private void renderSectors(Planet planet){
|
||||
public void renderSectors(Planet planet){
|
||||
//apply transformed position
|
||||
batch.proj().mul(planet.getTransform(mat));
|
||||
|
||||
@@ -270,7 +278,7 @@ public class PlanetRenderer implements Disposable{
|
||||
}
|
||||
}
|
||||
|
||||
private Mesh outline(int size){
|
||||
public Mesh outline(int size){
|
||||
if(outlines[size] == null){
|
||||
outlines[size] = MeshBuilder.buildHex(new HexMesher(){
|
||||
@Override
|
||||
|
||||
@@ -599,11 +599,11 @@ public class DesktopInput extends InputHandler{
|
||||
}
|
||||
|
||||
protected void updateMovement(Unit unit){
|
||||
boolean omni = unit.type().omniMovement;
|
||||
boolean omni = unit.type.omniMovement;
|
||||
boolean ground = unit.isGrounded();
|
||||
|
||||
float strafePenalty = ground ? 1f : Mathf.lerp(1f, unit.type().strafePenalty, Angles.angleDist(unit.vel().angle(), unit.rotation()) / 180f);
|
||||
float baseSpeed = unit.type().speed;
|
||||
float strafePenalty = ground ? 1f : Mathf.lerp(1f, unit.type.strafePenalty, Angles.angleDist(unit.vel().angle(), unit.rotation()) / 180f);
|
||||
float baseSpeed = unit.type.speed;
|
||||
|
||||
//limit speed to minimum formation speed to preserve formation
|
||||
if(unit.isCommanding()){
|
||||
@@ -611,7 +611,7 @@ public class DesktopInput extends InputHandler{
|
||||
baseSpeed = unit.minFormationSpeed * 0.95f;
|
||||
}
|
||||
|
||||
float speed = baseSpeed * Mathf.lerp(1f, unit.isCommanding() ? 1f : unit.type().canBoost ? unit.type().boostMultiplier : 1f, unit.elevation) * strafePenalty;
|
||||
float speed = baseSpeed * Mathf.lerp(1f, unit.isCommanding() ? 1f : unit.type.canBoost ? unit.type.boostMultiplier : 1f, unit.elevation) * strafePenalty;
|
||||
float xa = Core.input.axis(Binding.move_x);
|
||||
float ya = Core.input.axis(Binding.move_y);
|
||||
boolean boosted = (unit instanceof Mechc && unit.isFlying());
|
||||
@@ -622,7 +622,7 @@ public class DesktopInput extends InputHandler{
|
||||
}
|
||||
|
||||
float mouseAngle = Angles.mouseAngle(unit.x, unit.y);
|
||||
boolean aimCursor = omni && player.shooting && unit.type().hasWeapons() && unit.type().faceTarget && !boosted && unit.type().rotateShooting;
|
||||
boolean aimCursor = omni && player.shooting && unit.type.hasWeapons() && unit.type.faceTarget && !boosted && unit.type.rotateShooting;
|
||||
|
||||
if(aimCursor){
|
||||
unit.lookAt(mouseAngle);
|
||||
@@ -637,11 +637,11 @@ public class DesktopInput extends InputHandler{
|
||||
}else{
|
||||
unit.moveAt(Tmp.v2.trns(unit.rotation, movement.len()));
|
||||
if(!movement.isZero() && ground){
|
||||
unit.vel.rotateTo(movement.angle(), unit.type().rotateSpeed);
|
||||
unit.vel.rotateTo(movement.angle(), unit.type.rotateSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
unit.aim(unit.type().faceTarget ? Core.input.mouseWorld() : Tmp.v1.trns(unit.rotation, Core.input.mouseWorld().dst(unit)).add(unit.x, unit.y));
|
||||
unit.aim(unit.type.faceTarget ? Core.input.mouseWorld() : Tmp.v1.trns(unit.rotation, Core.input.mouseWorld().dst(unit)).add(unit.x, unit.y));
|
||||
unit.controlWeapons(true, player.shooting && !boosted);
|
||||
|
||||
player.boosting = Core.input.keyDown(Binding.boost) && !movement.isZero();
|
||||
|
||||
@@ -158,7 +158,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
Payloadc pay = (Payloadc)unit;
|
||||
|
||||
if(target.isAI() && target.isGrounded() && pay.canPickup(target)
|
||||
&& target.within(unit, unit.type().hitSize * 2f + target.type().hitSize * 2f)){
|
||||
&& target.within(unit, unit.type.hitSize * 2f + target.type.hitSize * 2f)){
|
||||
Call.pickedUnitPayload(unit, target);
|
||||
}
|
||||
}
|
||||
@@ -365,7 +365,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
|
||||
if(commander.isCommanding()){
|
||||
commander.clearCommand();
|
||||
}else if(player.unit().type().commandLimit > 0){
|
||||
}else if(player.unit().type.commandLimit > 0){
|
||||
|
||||
//TODO try out some other formations
|
||||
commander.commandNearby(new CircleFormation());
|
||||
@@ -398,17 +398,17 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
if(player.shooting && !wasShooting && player.unit().hasWeapons() && state.rules.unitAmmo && player.unit().ammo <= 0){
|
||||
player.unit().type().weapons.first().noAmmoSound.at(player.unit());
|
||||
player.unit().type.weapons.first().noAmmoSound.at(player.unit());
|
||||
}
|
||||
|
||||
wasShooting = player.shooting;
|
||||
|
||||
if(!player.dead()){
|
||||
controlledType = player.unit().type();
|
||||
controlledType = player.unit().type;
|
||||
}
|
||||
|
||||
if(controlledType != null && player.dead()){
|
||||
Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type() == controlledType && !u.dead);
|
||||
Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && !u.dead);
|
||||
|
||||
if(unit != null){
|
||||
Call.unitControl(player, unit);
|
||||
@@ -418,7 +418,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
|
||||
public void checkUnit(){
|
||||
if(controlledType != null){
|
||||
Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type() == controlledType && !u.dead);
|
||||
Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && !u.dead);
|
||||
if(unit == null && controlledType == UnitTypes.block){
|
||||
unit = world.buildWorld(player.x, player.y) instanceof ControlBlock ? ((ControlBlock)world.buildWorld(player.x, player.y)).unit() : null;
|
||||
}
|
||||
@@ -437,7 +437,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
Unit unit = player.unit();
|
||||
if(!(unit instanceof Payloadc pay)) return;
|
||||
|
||||
Unit target = Units.closest(player.team(), pay.x(), pay.y(), unit.type().hitSize * 2.5f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f));
|
||||
Unit target = Units.closest(player.team(), pay.x(), pay.y(), unit.type.hitSize * 2.5f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f));
|
||||
if(target != null){
|
||||
Call.requestUnitPayload(player, target);
|
||||
}else{
|
||||
|
||||
@@ -85,7 +85,7 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
if(tile != null && player.team().isEnemy(tile.team)){
|
||||
player.miner().mineTile(null);
|
||||
target = tile;
|
||||
}else if(tile != null && player.unit().type().canHeal && tile.team == player.team() && tile.damaged()){
|
||||
}else if(tile != null && player.unit().type.canHeal && tile.team == player.team() && tile.damaged()){
|
||||
player.miner().mineTile(null);
|
||||
target = tile;
|
||||
}
|
||||
@@ -834,10 +834,10 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
protected void updateMovement(Unit unit){
|
||||
Rect rect = Tmp.r3;
|
||||
|
||||
UnitType type = unit.type();
|
||||
UnitType type = unit.type;
|
||||
if(type == null) return;
|
||||
|
||||
boolean omni = unit.type().omniMovement;
|
||||
boolean omni = unit.type.omniMovement;
|
||||
boolean legs = unit.isGrounded();
|
||||
boolean allowHealing = type.canHeal;
|
||||
boolean validHealTarget = allowHealing && target instanceof Building && ((Building)target).isValid() && target.team() == unit.team &&
|
||||
@@ -855,7 +855,7 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
float attractDst = 15f;
|
||||
float strafePenalty = legs ? 1f : Mathf.lerp(1f, type.strafePenalty, Angles.angleDist(unit.vel.angle(), unit.rotation) / 180f);
|
||||
|
||||
float baseSpeed = unit.type().speed;
|
||||
float baseSpeed = unit.type.speed;
|
||||
|
||||
//limit speed to minimum formation speed to preserve formation
|
||||
if(unit.isCommanding()){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -395,7 +395,7 @@ public class LExecutor{
|
||||
if(unit instanceof Payloadc pay){
|
||||
//units
|
||||
if(exec.bool(p1)){
|
||||
Unit result = Units.closest(unit.team, unit.x, unit.y, unit.type().hitSize * 2f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f));
|
||||
Unit result = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u.isAI() && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize * 1.2f));
|
||||
|
||||
if(result != null){
|
||||
Call.pickedUnitPayload(unit, result);
|
||||
|
||||
@@ -11,6 +11,7 @@ import mindustry.entities.abilities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.defense.*;
|
||||
import mindustry.world.blocks.defense.turrets.*;
|
||||
@@ -25,8 +26,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 +80,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 +114,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 +139,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();
|
||||
@@ -230,13 +253,16 @@ public class SectorDamage{
|
||||
if(unit.isPlayer()) continue;
|
||||
|
||||
if(unit.team == state.rules.defaultTeam){
|
||||
sumHealth += unit.health + unit.shield;
|
||||
sumDps += unit.type().dpsEstimate;
|
||||
//scale health based on armor - yes, this is inaccurate, but better than nothing
|
||||
float healthMult = 1f + Mathf.clamp(unit.armor / 20f);
|
||||
|
||||
sumHealth += unit.health*healthMult + unit.shield;
|
||||
sumDps += unit.type.dpsEstimate;
|
||||
if(unit.abilities.find(a -> a instanceof HealFieldAbility) instanceof HealFieldAbility h){
|
||||
sumRps += h.amount / h.reload * 60f;
|
||||
}
|
||||
}else{
|
||||
curEnemyDps += unit.type().dpsEstimate;
|
||||
curEnemyDps += unit.type.dpsEstimate;
|
||||
curEnemyHealth += unit.health;
|
||||
}
|
||||
}
|
||||
@@ -255,10 +281,12 @@ public class SectorDamage{
|
||||
}
|
||||
|
||||
for(SpawnGroup group : state.rules.spawns){
|
||||
float healthMult = 1f + Mathf.clamp(group.type.armor / 20f);
|
||||
StatusEffect effect = (group.effect == null ? StatusEffects.none : group.effect);
|
||||
int spawned = group.getSpawned(wave);
|
||||
if(spawned <= 0) continue;
|
||||
sumWaveHealth += spawned * (group.getShield(wave) + group.type.health);
|
||||
sumWaveDps += spawned * group.type.dpsEstimate;
|
||||
sumWaveHealth += spawned * (group.getShield(wave) + group.type.health * effect.healthMultiplier * healthMult);
|
||||
sumWaveDps += spawned * group.type.dpsEstimate * effect.damageMultiplier;
|
||||
}
|
||||
waveDps.add(new Vec2(wave, sumWaveDps));
|
||||
waveHealth.add(new Vec2(wave, sumWaveHealth));
|
||||
@@ -273,7 +301,8 @@ public class SectorDamage{
|
||||
info.waveDpsBase = reg.intercept;
|
||||
info.waveDpsSlope = reg.slope;
|
||||
|
||||
info.sumHealth = sumHealth;
|
||||
//enemy units like to aim for a lot of non-essential things, so increase resulting health slightly
|
||||
info.sumHealth = sumHealth * 1.2f;
|
||||
info.sumDps = sumDps;
|
||||
info.sumRps = sumRps;
|
||||
|
||||
@@ -306,7 +335,7 @@ public class SectorDamage{
|
||||
int radius = 3;
|
||||
|
||||
//only penetrate a certain % by health, not by distance
|
||||
float totalHealth = path.sumf(t -> {
|
||||
float totalHealth = damage >= 1f ? 1f : path.sumf(t -> {
|
||||
float s = 0;
|
||||
for(int dx = -radius; dx <= radius; dx++){
|
||||
for(int dy = -radius; dy <= radius; dy++){
|
||||
@@ -323,7 +352,7 @@ public class SectorDamage{
|
||||
float healthCount = 0;
|
||||
|
||||
out:
|
||||
for(int i = 0; i < path.size && healthCount < targetHealth; i++){
|
||||
for(int i = 0; i < path.size && (healthCount < targetHealth || damage >= 1f); i++){
|
||||
Tile t = path.get(i);
|
||||
|
||||
for(int dx = -radius; dx <= radius; dx++){
|
||||
@@ -343,7 +372,7 @@ public class SectorDamage{
|
||||
|
||||
removal.add(other.build);
|
||||
|
||||
if(healthCount >= targetHealth){
|
||||
if(healthCount >= targetHealth && damage < 0.999f){
|
||||
break out;
|
||||
}
|
||||
}
|
||||
@@ -354,6 +383,7 @@ public class SectorDamage{
|
||||
|
||||
for(Building r : removal){
|
||||
if(r.tile.build == r){
|
||||
r.addPlan(false);
|
||||
r.tile.remove();
|
||||
}
|
||||
}
|
||||
@@ -361,7 +391,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();
|
||||
}
|
||||
@@ -402,6 +432,7 @@ public class SectorDamage{
|
||||
Effect.rubble(other.build.x, other.build.y, other.block().size);
|
||||
}
|
||||
|
||||
other.build.addPlan(false);
|
||||
other.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,12 +412,12 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{
|
||||
if(sector.hasEnemyBase()){
|
||||
basegen.generate(tiles, enemies.map(r -> tiles.getn(r.x, r.y)), tiles.get(spawn.x, spawn.y), state.rules.waveTeam, sector, difficulty);
|
||||
|
||||
state.rules.attackMode = true;
|
||||
state.rules.attackMode = sector.info.attack = true;
|
||||
}else{
|
||||
state.rules.winWave = 15 * (int)Math.max(difficulty * 10, 1);
|
||||
state.rules.winWave = sector.info.winWave = 15 * (int)Math.max(difficulty * 10, 1);
|
||||
}
|
||||
|
||||
state.rules.waves = true;
|
||||
state.rules.waves = sector.info.waves = true;
|
||||
|
||||
//TODO better waves
|
||||
state.rules.spawns = DefaultWaves.generate(difficulty);
|
||||
|
||||
@@ -260,8 +260,8 @@ public class ContentParser{
|
||||
//TODO test this!
|
||||
read(() -> {
|
||||
//add reconstructor type
|
||||
if(value.hasChild("requirements")){
|
||||
JsonValue rec = value.remove("requirements");
|
||||
if(value.has("requirements")){
|
||||
JsonValue rec = value.remove("requirements");
|
||||
|
||||
//intermediate class for parsing
|
||||
class UnitReq{
|
||||
@@ -286,6 +286,17 @@ public class ContentParser{
|
||||
|
||||
}
|
||||
|
||||
//read extra default waves
|
||||
if(value.has("waves")){
|
||||
JsonValue waves = value.remove("waves");
|
||||
SpawnGroup[] groups = parser.readValue(SpawnGroup[].class, waves);
|
||||
for(SpawnGroup group : groups){
|
||||
group.type = unit;
|
||||
}
|
||||
|
||||
Vars.defaultWaves.get().addAll(groups);
|
||||
}
|
||||
|
||||
readFields(unit, value, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ public class AmmoTypes implements ContentList{
|
||||
|
||||
if(build.block.consumes.hasPower() && build.block.consumes.getPower().buffered){
|
||||
float amount = closest.build.power.status * build.block.consumes.getPower().capacity;
|
||||
float powerPerAmmo = totalPower / unit.type().ammoCapacity;
|
||||
float ammoRequired = unit.type().ammoCapacity - unit.ammo;
|
||||
float powerPerAmmo = totalPower / unit.type.ammoCapacity;
|
||||
float ammoRequired = unit.type.ammoCapacity - unit.ammo;
|
||||
float powerRequired = ammoRequired * powerPerAmmo;
|
||||
float powerTaken = Math.min(amount, powerRequired);
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ public class ItemStack implements Comparable<ItemStack>{
|
||||
return item.compareTo(itemStack.item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o){
|
||||
if(this == o) return true;
|
||||
if(!(o instanceof ItemStack stack)) return false;
|
||||
return amount == stack.amount && item == stack.item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "ItemStack{" +
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public class SectorPreset extends UnlockableContent{
|
||||
super(name);
|
||||
this.generator = new FileMapGenerator(name);
|
||||
this.planet = planet;
|
||||
sector %= planet.sectors.size;
|
||||
this.sector = planet.sectors.get(sector);
|
||||
|
||||
planet.preset(sector, this);
|
||||
|
||||
@@ -56,7 +56,7 @@ public class StatusEffect extends MappableContent{
|
||||
}
|
||||
|
||||
if(effect != Fx.none && Mathf.chanceDelta(effectChance)){
|
||||
Tmp.v1.rnd(unit.type().hitSize /2f);
|
||||
Tmp.v1.rnd(unit.type.hitSize /2f);
|
||||
effect.at(unit.x + Tmp.v1.x, unit.y + Tmp.v1.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class UnitType extends UnlockableContent{
|
||||
public Unit create(Team team){
|
||||
Unit unit = constructor.get();
|
||||
unit.team = team;
|
||||
unit.type(this);
|
||||
unit.setType(this);
|
||||
unit.ammo = ammoCapacity; //fill up on ammo upon creation
|
||||
unit.elevation = flying ? 1f : 0;
|
||||
unit.heal();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,76 @@ 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){
|
||||
if(sector.save != null && sector.info.resources.any()){
|
||||
stable.add("@sectors.resources").row();
|
||||
stable.table(t -> {
|
||||
|
||||
if(sector.save != null && sector.save.meta.secinfo != null && sector.save.meta.secinfo.resources.any()){
|
||||
t.left();
|
||||
int idx = 0;
|
||||
int max = 5;
|
||||
for(UnlockableContent c : sector.save.meta.secinfo.resources){
|
||||
t.image(c.icon(Cicon.small)).padRight(3);
|
||||
if(++idx % max == 0) t.row();
|
||||
}
|
||||
}else{
|
||||
t.add("@unknown").color(Color.lightGray);
|
||||
t.left();
|
||||
int idx = 0;
|
||||
int max = 5;
|
||||
for(UnlockableContent c : sector.info.resources){
|
||||
t.image(c.icon(Cicon.small)).padRight(3);
|
||||
if(++idx % max == 0) t.row();
|
||||
}
|
||||
|
||||
|
||||
}).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).row();
|
||||
}
|
||||
}
|
||||
|
||||
//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){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -339,9 +339,6 @@ public class SettingsMenuDialog extends SettingsDialog{
|
||||
graphics.checkPref("smoothcamera", true);
|
||||
graphics.checkPref("position", false);
|
||||
graphics.checkPref("fps", false);
|
||||
if(!mobile){
|
||||
graphics.checkPref("blockselectkeys", true);
|
||||
}
|
||||
graphics.checkPref("playerindicators", true);
|
||||
graphics.checkPref("indicators", true);
|
||||
graphics.checkPref("animatedwater", true);
|
||||
|
||||
@@ -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 -> {
|
||||
@@ -706,7 +712,7 @@ public class HudFragment extends Fragment{
|
||||
t.add(new SideBar(() -> player.unit().healthf(), () -> true, true)).width(bw).growY().padRight(pad);
|
||||
t.image(() -> player.icon()).scaling(Scaling.bounded).grow().maxWidth(54f);
|
||||
t.add(new SideBar(() -> player.dead() ? 0f : player.displayAmmo() ? player.unit().ammof() : player.unit().healthf(), () -> !player.displayAmmo(), false)).width(bw).growY().padLeft(pad).update(b -> {
|
||||
b.color.set(player.displayAmmo() ? player.dead() || player.unit() instanceof BlockUnitc ? Pal.ammo : player.unit().type().ammoType.color : Pal.health);
|
||||
b.color.set(player.displayAmmo() ? player.dead() || player.unit() instanceof BlockUnitc ? Pal.ammo : player.unit().type.ammoType.color : Pal.health);
|
||||
});
|
||||
|
||||
t.getChildren().get(1).toFront();
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ public class PlacementFragment extends Fragment{
|
||||
|
||||
topTable.table(header -> {
|
||||
String keyCombo = "";
|
||||
if(!mobile && Core.settings.getBool("blockselectkeys")){
|
||||
if(!mobile){
|
||||
Seq<Block> blocks = getByCategory(currentCategory);
|
||||
for(int i = 0; i < blocks.size; i++){
|
||||
if(blocks.get(i) == displayBlock && (i + 1) / 10 - 1 < blockSelect.length){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -115,15 +115,15 @@ public class LaunchPad extends Block{
|
||||
public void display(Table table){
|
||||
super.display(table);
|
||||
|
||||
if(!state.isCampaign()) return;
|
||||
|
||||
table.row();
|
||||
table.label(() -> {
|
||||
Sector dest = state.secinfo.getRealDestination();
|
||||
|
||||
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 +213,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 +223,7 @@ public class LaunchPad extends Block{
|
||||
Events.fire(new LaunchItemEvent(stack));
|
||||
}
|
||||
|
||||
destsec.setExtraItems(dest);
|
||||
destsec.addItems(dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class TractorBeamTurret extends Block{
|
||||
}
|
||||
|
||||
//look at target
|
||||
if(target != null && target.within(this, range) && target.team() != team && target.type().flying && efficiency() > 0.01f){
|
||||
if(target != null && target.within(this, range) && target.team() != team && target.type.flying && efficiency() > 0.01f){
|
||||
any = true;
|
||||
float dest = angleTo(target);
|
||||
rotation = Angles.moveToward(rotation, dest, rotateSpeed * edelta());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ▼
|
||||
|
||||
@@ -43,7 +43,7 @@ public class UnitPayload implements Payload{
|
||||
|
||||
@Override
|
||||
public boolean dump(){
|
||||
if(!Units.canCreate(unit.team, unit.type())){
|
||||
if(!Units.canCreate(unit.team, unit.type)){
|
||||
deactiveTime = 1f;
|
||||
return false;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public class UnitPayload implements Payload{
|
||||
@Override
|
||||
public void draw(){
|
||||
Drawf.shadow(unit.x, unit.y, 20);
|
||||
Draw.rect(unit.type().icon(Cicon.full), unit.x, unit.y, unit.rotation - 90);
|
||||
Draw.rect(unit.type.icon(Cicon.full), unit.x, unit.y, unit.rotation - 90);
|
||||
|
||||
//draw warning
|
||||
if(deactiveTime > 0){
|
||||
|
||||
@@ -196,6 +196,21 @@ public class CoreBlock extends StorageBlock{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyed(){
|
||||
super.onDestroyed();
|
||||
|
||||
//add a spawn to the map for future reference - waves should be disabled, so it shouldn't matter
|
||||
if(state.isCampaign() && team == state.rules.waveTeam){
|
||||
//do not recache
|
||||
tile.setOverlayQuiet(Blocks.spawn);
|
||||
|
||||
if(!spawner.getSpawns().contains(tile)){
|
||||
spawner.getSpawns().add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLight(){
|
||||
Drawf.light(team, x, y, 30f * size, Pal.accent, 0.5f + Mathf.absin(20f, 0.1f));
|
||||
@@ -318,7 +333,7 @@ public class CoreBlock extends StorageBlock{
|
||||
|
||||
@Override
|
||||
public void itemTaken(Item item){
|
||||
if(state.isCampaign()){
|
||||
if(state.isCampaign() && team == state.rules.defaultTeam){
|
||||
//update item taken amount
|
||||
state.secinfo.handleCoreItem(item, -1);
|
||||
}
|
||||
@@ -327,6 +342,9 @@ public class CoreBlock extends StorageBlock{
|
||||
@Override
|
||||
public void handleItem(Building source, Item item){
|
||||
if(net.server() || !net.active()){
|
||||
if(team == state.rules.defaultTeam){
|
||||
state.secinfo.handleCoreItem(item, 1);
|
||||
}
|
||||
|
||||
if(items.get(item) >= getMaximumAccepted(item)){
|
||||
//create item incineration effect at random intervals
|
||||
|
||||
@@ -26,7 +26,7 @@ public class StorageBlock extends Block{
|
||||
}
|
||||
|
||||
public static void incinerateEffect(Building self, Building source){
|
||||
if(Mathf.chance(0.1)){
|
||||
if(Mathf.chance(0.3)){
|
||||
Tile edge = Edges.getFacingEdge(source, self);
|
||||
Tile edge2 = Edges.getFacingEdge(self, source);
|
||||
if(edge != null && edge2 != null){
|
||||
@@ -46,7 +46,9 @@ public class StorageBlock extends Block{
|
||||
@Override
|
||||
public void handleItem(Building source, Item item){
|
||||
if(linkedCore != null){
|
||||
incinerateEffect(this, source);
|
||||
if(linkedCore.items.get(item) >= ((CoreBuild)linkedCore).storageCapacity){
|
||||
incinerateEffect(this, source);
|
||||
}
|
||||
((CoreBuild)linkedCore).noEffect = true;
|
||||
linkedCore.handleItem(source, item);
|
||||
}else{
|
||||
@@ -70,7 +72,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ public class Reconstructor extends UnitBlock{
|
||||
return this.payload == null
|
||||
&& relativeTo(source) != rotation
|
||||
&& payload instanceof UnitPayload
|
||||
&& hasUpgrade(((UnitPayload)payload).unit.type());
|
||||
&& hasUpgrade(((UnitPayload)payload).unit.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,9 +114,9 @@ public class Reconstructor extends UnitBlock{
|
||||
if(constructing() && hasArrived()){
|
||||
Draw.draw(Layer.blockOver, () -> {
|
||||
Draw.alpha(1f - progress/ constructTime);
|
||||
Draw.rect(payload.unit.type().icon(Cicon.full), x, y, rotdeg() - 90);
|
||||
Draw.rect(payload.unit.type.icon(Cicon.full), x, y, rotdeg() - 90);
|
||||
Draw.reset();
|
||||
Drawf.construct(this, upgrade(payload.unit.type()), rotdeg() - 90f, progress / constructTime, speedScl, time);
|
||||
Drawf.construct(this, upgrade(payload.unit.type), rotdeg() - 90f, progress / constructTime, speedScl, time);
|
||||
});
|
||||
}else{
|
||||
Draw.z(Layer.blockOver);
|
||||
@@ -135,7 +135,7 @@ public class Reconstructor extends UnitBlock{
|
||||
|
||||
if(payload != null){
|
||||
//check if offloading
|
||||
if(!hasUpgrade(payload.unit.type())){
|
||||
if(!hasUpgrade(payload.unit.type)){
|
||||
moveOutPayload();
|
||||
}else{ //update progress
|
||||
if(moveInPayload()){
|
||||
@@ -146,7 +146,7 @@ public class Reconstructor extends UnitBlock{
|
||||
|
||||
//upgrade the unit
|
||||
if(progress >= constructTime){
|
||||
payload.unit = upgrade(payload.unit.type()).create(payload.unit.team());
|
||||
payload.unit = upgrade(payload.unit.type).create(payload.unit.team());
|
||||
progress = 0;
|
||||
Effect.shake(2f, 3f, this);
|
||||
Fx.producesmoke.at(this);
|
||||
@@ -168,12 +168,12 @@ public class Reconstructor extends UnitBlock{
|
||||
public UnitType unit(){
|
||||
if(payload == null) return null;
|
||||
|
||||
UnitType t = upgrade(payload.unit.type());
|
||||
UnitType t = upgrade(payload.unit.type);
|
||||
return t != null && t.unlockedNow() ? t : null;
|
||||
}
|
||||
|
||||
public boolean constructing(){
|
||||
return payload != null && hasUpgrade(payload.unit.type());
|
||||
return payload != null && hasUpgrade(payload.unit.type);
|
||||
}
|
||||
|
||||
public boolean hasUpgrade(UnitType type){
|
||||
|
||||
@@ -65,10 +65,10 @@ public class ResupplyPoint extends Block{
|
||||
public static boolean resupply(Team team, float x, float y, float range, float ammoAmount, Color ammoColor, Boolf<Unit> valid){
|
||||
if(!state.rules.unitAmmo) return false;
|
||||
|
||||
Unit unit = Units.closest(team, x, y, range, u -> u.type().ammoType instanceof ItemAmmoType && u.ammo <= u.type().ammoCapacity - ammoAmount && valid.get(u));
|
||||
Unit unit = Units.closest(team, x, y, range, u -> u.type.ammoType instanceof ItemAmmoType && u.ammo <= u.type.ammoCapacity - ammoAmount && valid.get(u));
|
||||
if(unit != null){
|
||||
Fx.itemTransfer.at(x, y, ammoAmount / 2f, ammoColor, unit);
|
||||
unit.ammo = Math.min(unit.ammo + ammoAmount, unit.type().ammoCapacity);
|
||||
unit.ammo = Math.min(unit.ammo + ammoAmount, unit.type.ammoCapacity);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.blocks.*;
|
||||
@@ -122,6 +123,12 @@ public class UnitFactory extends UnitBlock{
|
||||
return currentPlan == -1 ? 0 : progress / plans.get(currentPlan).time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object senseObject(LAccess sensor){
|
||||
if(sensor == LAccess.config) return currentPlan == -1 ? null : plans.get(currentPlan).unit;
|
||||
return super.senseObject(sensor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildConfiguration(Table table){
|
||||
Seq<UnitType> units = Seq.with(plans).map(u -> u.unit).filter(u -> u.unlockedNow());
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user