diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index eb2dcff192..71185f740f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,7 +22,7 @@ jobs: - name: Run unit tests and build JAR run: ./gradlew desktop:dist - name: Upload desktop JAR for testing - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Desktop JAR (zipped) path: desktop/build/libs/Mindustry.jar diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 2864574457..c27355fb68 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -197,6 +197,7 @@ campaign.select = Select Starting Campaign campaign.none = [lightgray]Select a planet to start on.\nThis can be switched at any time. campaign.erekir = Newer, more polished content. Mostly linear campaign progression.\n\nMore difficult. Higher quality maps and overall experience. campaign.serpulo = Older content; the classic experience. More open-ended, more content.\n\nPotentially unbalanced maps and campaign mechanics. Less polished. +campaign.difficulty = Difficulty completed = [accent]Researched techtree = Tech Tree techtree.select = Tech Tree Selection @@ -800,6 +801,11 @@ threat.high = High threat.extreme = Extreme threat.eradication = Eradication +difficulty.easy = Easy +difficulty.normal = Normal +difficulty.hard = Hard +difficulty.eradication = Eradication + planets = Planets planet.serpulo.name = Serpulo @@ -1172,12 +1178,6 @@ setting.fpscap.text = {0} FPS setting.uiscale.name = UI Scaling setting.uiscale.description = Restart required to apply changes. setting.swapdiagonal.name = Always Diagonal Placement -setting.difficulty.training = Training -setting.difficulty.easy = Easy -setting.difficulty.normal = Normal -setting.difficulty.hard = Hard -setting.difficulty.insane = Insane -setting.difficulty.name = Difficulty: setting.screenshake.name = Screen Shake setting.bloomintensity.name = Bloom Intensity setting.bloomblur.name = Bloom Blur @@ -1397,6 +1397,8 @@ rules.title.teams = Teams rules.title.planet = Planet rules.lighting = Lighting rules.fog = Fog of War +rules.invasions = Enemy Sector Invasions +rules.showspawns = Show Enemy Spawns rules.fire = Fire rules.anyenv = rules.explosions = Block/Unit Explosion Damage diff --git a/core/src/mindustry/ai/WaveSpawner.java b/core/src/mindustry/ai/WaveSpawner.java index e23dbbff5e..58f770f20d 100644 --- a/core/src/mindustry/ai/WaveSpawner.java +++ b/core/src/mindustry/ai/WaveSpawner.java @@ -66,12 +66,19 @@ public class WaveSpawner{ if(group.type == null) continue; int spawned = group.getSpawned(state.wave - 1); + if(spawned == 0) continue; + + if(state.isCampaign()){ + spawned = Math.max(1, Mathf.round(spawned * state.getPlanet().campaignRules.difficulty.enemySpawnMultiplier)); + } + + int spawnedf = spawned; if(group.type.flying){ float spread = margin / 1.5f; eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> { - for(int i = 0; i < spawned; i++){ + for(int i = 0; i < spawnedf; i++){ Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread)); spawnEffect(unit); @@ -82,7 +89,7 @@ public class WaveSpawner{ eachGroundSpawn(group.spawn, (spawnX, spawnY, doShockwave) -> { - for(int i = 0; i < spawned; i++){ + for(int i = 0; i < spawnedf; i++){ Tmp.v1.rnd(spread); Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); @@ -153,7 +160,7 @@ public class WaveSpawner{ private void eachFlyerSpawn(int filterPos, Floatc2 cons){ boolean airUseSpawns = state.rules.airUseSpawns; - + for(Tile tile : spawns){ if(filterPos != -1 && filterPos != tile.pos()) continue; diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 873e1a9d45..f3a2d9332b 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -156,7 +156,7 @@ public class Blocks{ //payloads payloadConveyor, payloadRouter, reinforcedPayloadConveyor, reinforcedPayloadRouter, payloadMassDriver, largePayloadMassDriver, smallDeconstructor, deconstructor, constructor, largeConstructor, payloadLoader, payloadUnloader, - + //logic message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank, canvas, reinforcedMessage, @@ -1282,7 +1282,7 @@ public class Blocks{ itemCapacity = 0; consumePower(100f / 60f); }}; - + slagHeater = new HeatProducer("slag-heater"){{ requirements(Category.crafting, with(Items.tungsten, 50, Items.oxide, 20, Items.beryllium, 20)); @@ -3405,7 +3405,7 @@ public class Blocks{ lightningLength = 10; }} ); - + shoot = new ShootBarrel(){{ barrels = new float[]{ -4, -1.25f, 0, @@ -5326,7 +5326,7 @@ public class Blocks{ requirements(Category.units, with(Items.copper, 150, Items.lead, 130, Items.metaglass, 120)); plans = Seq.with( new UnitPlan(UnitTypes.risso, 60f * 45f, with(Items.silicon, 20, Items.metaglass, 35)), - new UnitPlan(UnitTypes.retusa, 60f * 50f, with(Items.silicon, 15, Items.metaglass, 25, Items.titanium, 20)) + new UnitPlan(UnitTypes.retusa, 60f * 35f, with(Items.silicon, 15, Items.titanium, 20)) ); size = 3; consumePower(1.2f); @@ -5930,7 +5930,7 @@ public class Blocks{ worldCell = new MemoryBlock("world-cell"){{ requirements(Category.logic, BuildVisibility.worldProcessorOnly, with()); - + targetable = false; privileged = true; memoryCapacity = 128; @@ -5939,7 +5939,7 @@ public class Blocks{ worldMessage = new MessageBlock("world-message"){{ requirements(Category.logic, BuildVisibility.worldProcessorOnly, with()); - + targetable = false; privileged = true; }}; diff --git a/core/src/mindustry/content/Planets.java b/core/src/mindustry/content/Planets.java index 7eec1f10b0..a37488943c 100644 --- a/core/src/mindustry/content/Planets.java +++ b/core/src/mindustry/content/Planets.java @@ -85,6 +85,8 @@ public class Planets{ r.coreDestroyClear = true; r.onlyDepositCore = true; }; + campaignRuleDefaults.fog = true; + campaignRuleDefaults.showSpawns = true; unlockedOnLand.add(Blocks.coreBastion); }}; diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index bc730c44c4..bdf8ab573c 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -16,9 +16,9 @@ import mindustry.content.*; import mindustry.content.TechTree.*; import mindustry.core.GameState.*; import mindustry.entities.*; +import mindustry.game.*; import mindustry.game.EventType.*; import mindustry.game.Objectives.*; -import mindustry.game.*; import mindustry.game.Saves.*; import mindustry.gen.*; import mindustry.input.*; @@ -30,7 +30,6 @@ import mindustry.net.*; import mindustry.type.*; import mindustry.ui.dialogs.*; import mindustry.world.*; -import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import java.io.*; @@ -441,6 +440,7 @@ public class Control implements ApplicationListener, Loadable{ state.wave = 1; //set up default wave time state.wavetime = state.rules.initialWaveSpacing <= 0f ? (state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier)) : state.rules.initialWaveSpacing; + state.wavetime *= sector.planet.campaignRules.difficulty.waveTimeMultiplier; //reset captured state sector.info.wasCaptured = false; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index d1c7c5ca60..bbf4d7f3ed 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -92,7 +92,7 @@ public class Logic implements ApplicationListener{ if(wavesPassed > 0){ //simulate wave counter moving forward state.wave += wavesPassed; - state.wavetime = state.rules.waveSpacing; + state.wavetime = state.rules.waveSpacing * state.getPlanet().campaignRules.difficulty.waveTimeMultiplier; SectorDamage.applyCalculatedDamage(); } @@ -221,7 +221,7 @@ public class Logic implements ApplicationListener{ public void play(){ state.set(State.playing); //grace period of 2x wave time before game starts - state.wavetime = state.rules.initialWaveSpacing <= 0 ? state.rules.waveSpacing * 2 : state.rules.initialWaveSpacing; + state.wavetime = (state.rules.initialWaveSpacing <= 0 ? state.rules.waveSpacing * 2 : state.rules.initialWaveSpacing) * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f);; Events.fire(new PlayEvent()); //add starting items @@ -270,7 +270,7 @@ public class Logic implements ApplicationListener{ public void runWave(){ spawner.spawnEnemies(); state.wave++; - state.wavetime = state.rules.waveSpacing; + state.wavetime = state.rules.waveSpacing * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f); Events.fire(new WaveEvent()); } diff --git a/core/src/mindustry/game/CampaignRules.java b/core/src/mindustry/game/CampaignRules.java new file mode 100644 index 0000000000..c0be0548a5 --- /dev/null +++ b/core/src/mindustry/game/CampaignRules.java @@ -0,0 +1,15 @@ +package mindustry.game; + +public class CampaignRules{ + public Difficulty difficulty = Difficulty.normal; + public boolean fog; + public boolean showSpawns; + public boolean sectorInvasion; + + public void apply(Rules rules){ + rules.staticFog = rules.fog = fog; + rules.showSpawns = showSpawns; + rules.teams.get(rules.waveTeam).blockHealthMultiplier = difficulty.enemyHealthMultiplier; + rules.teams.get(rules.waveTeam).unitHealthMultiplier = difficulty.enemyHealthMultiplier; + } +} diff --git a/core/src/mindustry/game/Difficulty.java b/core/src/mindustry/game/Difficulty.java new file mode 100644 index 0000000000..3018f9c3f1 --- /dev/null +++ b/core/src/mindustry/game/Difficulty.java @@ -0,0 +1,26 @@ +package mindustry.game; + +import arc.*; + +public enum Difficulty{ + //TODO these need tweaks + easy(1f, 0.75f, 1.5f), + normal(1f, 1f, 1f), + hard(1.25f, 1.5f, 0.6f), + eradication(1.5f, 2f, 0.4f); + + public static final Difficulty[] all = values(); + + //TODO add more fields + public float enemyHealthMultiplier, enemySpawnMultiplier, waveTimeMultiplier; + + Difficulty(float enemyHealthMultiplier, float enemySpawnMultiplier, float waveTimeMultiplier){ + this.enemySpawnMultiplier = enemySpawnMultiplier; + this.waveTimeMultiplier = waveTimeMultiplier; + this.enemyHealthMultiplier = enemyHealthMultiplier; + } + + public String localized(){ + return Core.bundle.get("difficulty." + name()); + } +} diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 4aa77d8be7..a22d073f9d 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -252,7 +252,7 @@ public class Universe{ } //queue random invasions - if(!sector.isAttacked() && sector.planet.allowSectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){ + if(!sector.isAttacked() && sector.planet.campaignRules.sectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){ int count = sector.near().count(s -> s.hasEnemyBase() && !s.hasBase()); //invasion chance depends on # of nearby bases diff --git a/core/src/mindustry/type/Planet.java b/core/src/mindustry/type/Planet.java index 047a288093..23cc55efec 100644 --- a/core/src/mindustry/type/Planet.java +++ b/core/src/mindustry/type/Planet.java @@ -19,6 +19,7 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.g3d.*; import mindustry.graphics.g3d.PlanetGrid.*; +import mindustry.io.*; import mindustry.maps.generators.*; import mindustry.world.*; import mindustry.world.blocks.*; @@ -127,15 +128,21 @@ public class Planet extends UnlockableContent{ public boolean allowWaves = false; /** If false, players are unable to land on this planet's numbered sectors. */ public boolean allowLaunchToNumbered = true; + /** If true, the player is allowed to change the difficulty/rules in the planet UI. */ + public boolean allowCampaignRules = false; /** Icon as displayed in the planet selection dialog. This is a string, as drawables are null at load time. */ public String icon = "planet"; /** Plays in the planet dialog when this planet is selected. */ public Music launchMusic = Musics.launch; /** Default core block for launching. */ public Block defaultCore = Blocks.coreShard; + /** Global difficulty/modifier settings for this planet's campaign. */ + public CampaignRules campaignRules = new CampaignRules(); + /** Defaults applied to the rules. */ + public CampaignRules campaignRuleDefaults = new CampaignRules(); /** Sets up rules on game load for any sector on this planet. */ public Cons ruleSetter = r -> {}; - /** Parent body that this planet orbits around. If null, this planet is considered to be in the middle of the solar system.*/ + /** Parent body that this planet orbits around. If null, this planet is considered to be in the middle of the solar system. */ public @Nullable Planet parent; /** The root parent of the whole solar system this planet is in. */ public Planet solarSystem; @@ -183,6 +190,7 @@ public class Planet extends UnlockableContent{ //calculate solar system for(solarSystem = this; solarSystem.parent != null; solarSystem = solarSystem.parent); + allowCampaignRules = isVanilla(); } public Planet(String name, Planet parent, float radius, int sectorSize){ @@ -200,17 +208,38 @@ public class Planet extends UnlockableContent{ } } + public void saveRules(){ + Core.settings.putJson(name + "-campaign-rules", campaignRules); + } + + public void loadRules(){ + campaignRules = Core.settings.getJson(name + "-campaign-rules", CampaignRules.class, () -> campaignRules); + } + public @Nullable Sector getStartSector(){ return sectors.size == 0 ? null : sectors.get(startSector); } public void applyRules(Rules rules){ + applyRules(rules, false); + } + + public void applyRules(Rules rules, boolean customGame){ ruleSetter.get(rules); rules.attributes.clear(); rules.attributes.add(defaultAttributes); rules.env = defaultEnv; rules.planet = this; + + if(!customGame){ + campaignRules.apply(rules); + } + } + + public void applyDefaultRules(CampaignRules rules){ + JsonIO.copy(campaignRuleDefaults, rules); + rules.sectorInvasion = allowSectorInvasion; } public @Nullable Sector getLastSector(){ @@ -327,6 +356,9 @@ public class Planet extends UnlockableContent{ @Override public void init(){ + applyDefaultRules(campaignRules); + loadRules(); + if(techTree == null){ techTree = TechTree.roots.find(n -> n.planet == this); } diff --git a/core/src/mindustry/ui/dialogs/CampaignRulesDialog.java b/core/src/mindustry/ui/dialogs/CampaignRulesDialog.java new file mode 100644 index 0000000000..0805234ef8 --- /dev/null +++ b/core/src/mindustry/ui/dialogs/CampaignRulesDialog.java @@ -0,0 +1,86 @@ +package mindustry.ui.dialogs; + +import arc.*; +import arc.func.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import mindustry.*; +import mindustry.game.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.ui.*; + +public class CampaignRulesDialog extends BaseDialog{ + Planet planet; + Table current; + + public CampaignRulesDialog(){ + super("@campaign.difficulty"); + + addCloseButton(); + + hidden(() -> { + if(planet != null){ + planet.saveRules(); + + if(Vars.state.isGame() && Vars.state.isCampaign() && Vars.state.getPlanet() == planet){ + planet.campaignRules.apply(Vars.state.rules); + Call.setRules(Vars.state.rules); + } + } + }); + } + + void rebuild(){ + CampaignRules rules = planet.campaignRules; + cont.clear(); + + cont.top().pane(inner -> { + inner.top().left().defaults().fillX().left().pad(5); + current = inner; + + current.table(Tex.button, t -> { + t.margin(10f); + var group = new ButtonGroup<>(); + var style = Styles.flatTogglet; + + t.defaults().size(140f, 50f); + + for(Difficulty diff : Difficulty.all){ + t.button(diff.localized(), style, () -> { + rules.difficulty = diff; + }).group(group).checked(b -> rules.difficulty == diff); + } + }).left().fill(false).expand(false, false).row(); + + if(planet.allowSectorInvasion){ + check("@rules.invasions", b -> rules.sectorInvasion = b, () -> rules.sectorInvasion); + } + + check("@rules.fog", b -> rules.fog = b, () -> rules.fog); + check("@rules.showspawns", b -> rules.showSpawns = b, () -> rules.showSpawns); + }).growY(); + } + + public void show(Planet planet){ + this.planet = planet; + + rebuild(); + show(); + } + + void check(String text, Boolc cons, Boolp prov){ + check(text, cons, prov, () -> true); + } + + void check(String text, Boolc cons, Boolp prov, Boolp condition){ + String infoText = text.substring(1) + ".info"; + var cell = current.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get())); + if(Core.bundle.has(infoText)){ + cell.tooltip(text + ".info"); + } + cell.get().left(); + current.row(); + } + +} diff --git a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java index 247625c248..4905cbad3e 100644 --- a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java @@ -329,7 +329,7 @@ public class CustomRulesDialog extends BaseDialog{ for(Planet planet : content.planets().select(p -> p.accessible && p.visible && p.isLandable())){ t.button(planet.localizedName, style, () -> { - planet.applyRules(rules); + planet.applyRules(rules, true); }).group(group).checked(b -> rules.planet == planet); if(t.getChildren().size % 3 == 0){ diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 6ef9bb6397..1a040bc00a 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -67,10 +67,11 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ public Label hoverLabel = new Label(""); private Texture[] planetTextures; + private CampaignRulesDialog campaignRules = new CampaignRulesDialog(); public PlanetDialog(){ super("", Styles.fullDialog); - + state.renderer = this; state.drawUi = true; @@ -387,7 +388,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ //preset sectors can only be selected once unlocked if(sector.preset != null){ TechNode node = sector.preset.techNode; - return node == null || node.parent == null || (node.parent.content.unlocked() && (!(node.parent.content instanceof SectorPreset preset) || preset.sector.hasBase())); + return sector.preset.unlocked() || node == null || node.parent == null || (node.parent.content.unlocked() && (!(node.parent.content instanceof SectorPreset preset) || preset.sector.hasBase())); } return sector.planet.generator != null ? @@ -474,7 +475,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(state.uiAlpha > 0.001f){ for(Sector sec : planet.sectors){ if(sec.hasBase()){ - if(planet.allowSectorInvasion){ + if(planet.campaignRules.sectorInvasion){ for(Sector enemy : sec.near()){ if(enemy.hasEnemyBase()){ planets.drawArc(planet, enemy.tile.v, sec.tile.v, Team.crux.color.write(Tmp.c2).a(state.uiAlpha), Color.clear, 0.24f, 110f, 25); @@ -612,6 +613,10 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ t.top().left(); ScrollPane pane = new ScrollPane(null, Styles.smallPane); t.add(pane).colspan(2).row(); + t.button("@campaign.difficulty", Icon.bookSmall, () -> { + campaignRules.show(state.planet); + }).margin(12f).size(208f, 40f).padTop(12f).visible(() -> state.planet.allowCampaignRules).row(); + t.add().height(64f); //padding for close button Table starsTable = new Table(Styles.black); pane.setWidget(starsTable); pane.setScrollingDisabled(true, false); @@ -1133,7 +1138,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(sector.isAttacked()){ addSurvivedInfo(sector, stable, false); - }else if(sector.hasBase() && sector.planet.allowSectorInvasion && sector.near().contains(Sector::hasEnemyBase)){ + }else if(sector.hasBase() && sector.planet.campaignRules.sectorInvasion && sector.near().contains(Sector::hasEnemyBase)){ stable.add("@sectors.vulnerable"); stable.row(); }else if(!sector.hasBase() && sector.hasEnemyBase()){ diff --git a/core/src/mindustry/world/meta/BlockFlag.java b/core/src/mindustry/world/meta/BlockFlag.java index 314c0424b6..257fa0c20a 100644 --- a/core/src/mindustry/world/meta/BlockFlag.java +++ b/core/src/mindustry/world/meta/BlockFlag.java @@ -32,5 +32,5 @@ public enum BlockFlag{ public final static BlockFlag[] all = values(); /** Values for logic only. Filters out some internal flags. */ - public final static BlockFlag[] allLogic = {core, storage, generator, turret, factory, repair, battery, reactor}; + public final static BlockFlag[] allLogic = {core, storage, generator, turret, factory, repair, battery, reactor, drill}; }