WIP campaign difficulty dialog

This commit is contained in:
Anuken 2024-09-13 17:58:11 -04:00
parent efb86724a1
commit d8eabece7c
15 changed files with 204 additions and 29 deletions

View File

@ -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

View File

@ -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 = <Any>
rules.explosions = Block/Unit Explosion Damage

View File

@ -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);

View File

@ -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);

View File

@ -85,6 +85,8 @@ public class Planets{
r.coreDestroyClear = true;
r.onlyDepositCore = true;
};
campaignRuleDefaults.fog = true;
campaignRuleDefaults.showSpawns = true;
unlockedOnLand.add(Blocks.coreBastion);
}};

View File

@ -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;

View File

@ -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());
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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<Rules> 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);
}

View File

@ -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();
}
}

View File

@ -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){

View File

@ -67,6 +67,7 @@ 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);
@ -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()){

View File

@ -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};
}