Various database entry stats (#11583)

* Add Firerate Stat to Lustre and sublimate

* 8 votes

* forgot comment

* Show shock mine damage stats

* make spawnBullets appear in core database

* spawned instead

* typo h

* fuck impact stats

* segment and point defense stuff

* Change repair & shieldregen field's shown stats

* Grant "Airborne" achievement when commanding units

* more database entries

* navanax entries (holy hell there are so many)

* boosting speed entry

* add arrow icon and stuff

* dont break tests

* junction storage per side

* Apply suggestion from @Anuken

---------

Co-authored-by: Anuken <arnukren@gmail.com>
This commit is contained in:
EggleEgg
2026-02-10 18:05:42 +01:00
committed by GitHub
parent cb1ba51238
commit 121afa0c95
22 changed files with 228 additions and 75 deletions

View File

@@ -10,7 +10,7 @@ public class BoostAI extends AIController{
public void updateUnit(){
if(unit.controller() instanceof CommandAI ai){
ai.defaultBehavior();
unit.updateBoosting(true);
unit.updateBoosting(true, true);
//auto land when near target
if(ai.attackTarget != null && unit.within(ai.attackTarget, unit.range())){

View File

@@ -2368,64 +2368,6 @@ public class UnitTypes{
buildSpeed = 3.5f;
rotateToBuilding = false;
for(float mountY : new float[]{-117/4f, 50/4f}){
for(float sign : Mathf.signs){
weapons.add(new Weapon("plasma-laser-mount"){{
shadow = 20f;
controllable = false;
autoTarget = true;
mirror = false;
shake = 3f;
shootY = 7f;
rotate = true;
x = 84f/4f * sign;
y = mountY;
targetInterval = 20f;
targetSwitchInterval = 35f;
rotateSpeed = 3.5f;
reload = 170f;
recoil = 1f;
shootSound = Sounds.beamPlasmaSmall;
initialShootSound = Sounds.shootBeamPlasmaSmall;
continuous = true;
cooldownTime = reload;
immunities.add(StatusEffects.burning);
bullet = new ContinuousLaserBulletType(){{
maxRange = 90f;
damage = 27f;
length = 95f;
hitEffect = Fx.hitMeltHeal;
drawSize = 200f;
lifetime = 155f;
shake = 1f;
shootEffect = Fx.shootHeal;
smokeEffect = Fx.none;
width = 4f;
largeHit = false;
incendChance = 0.03f;
incendSpread = 5f;
incendAmount = 1;
healPercent = 0.4f;
collidesTeam = true;
colors = new Color[]{Pal.heal.cpy().a(.2f), Pal.heal.cpy().a(.5f), Pal.heal.cpy().mul(1.2f), Color.white};
}};
}});
}
}
abilities.add(new SuppressionFieldAbility(){{
orbRadius = 5;
particleSize = 3;
y = -10f;
particles = 10;
color = particleColor = effectColor = Pal.heal;
}});
weapons.add(new Weapon("emp-cannon-mount"){{
rotate = true;
@@ -2508,6 +2450,66 @@ public class UnitTypes{
});
}};
}});
for(float mountY : new float[]{-117/4f, 50/4f}){
for(float sign : Mathf.signs){
weapons.add(new Weapon("plasma-laser-mount"){{
shadow = 20f;
controllable = false;
autoTarget = true;
mirror = false;
shake = 3f;
shootY = 7f;
rotate = true;
x = 84f/4f * sign;
y = mountY;
targetInterval = 20f;
targetSwitchInterval = 35f;
rotateSpeed = 3.5f;
reload = 170f;
recoil = 1f;
shootSound = Sounds.beamPlasmaSmall;
initialShootSound = Sounds.shootBeamPlasmaSmall;
continuous = true;
cooldownTime = reload;
immunities.add(StatusEffects.burning);
bullet = new ContinuousLaserBulletType(){{
maxRange = 90f;
damage = 27f;
length = 95f;
hitEffect = Fx.hitMeltHeal;
drawSize = 200f;
lifetime = 155f;
shake = 1f;
shootEffect = Fx.shootHeal;
smokeEffect = Fx.none;
width = 4f;
largeHit = false;
incendChance = 0.03f;
incendSpread = 5f;
incendAmount = 1;
healPercent = 0.4f;
collidesTeam = true;
colors = new Color[]{Pal.heal.cpy().a(.2f), Pal.heal.cpy().a(.5f), Pal.heal.cpy().mul(1.2f), Color.white};
}};
}});
}
}
abilities.add(new SuppressionFieldAbility(){{
orbRadius = 5;
particleSize = 3;
y = -10f;
particles = 10;
color = particleColor = effectColor = Pal.heal;
}});
}};
//endregion
@@ -3115,6 +3117,7 @@ public class UnitTypes{
float fin = 0.05f + (j + 1) / (float)count;
float spd = speed;
float life = lifetime / Mathf.lerp(fin, 1f, 0.5f);
boolean show = j == 0 && i > 0;
spawnBullets.add(new BasicBulletType(spd * fin, 60){{
drag = 0.002f;
width = 12f;
@@ -3124,6 +3127,7 @@ public class UnitTypes{
hitSize = 5f;
pierceCap = 2;
pierce = true;
showStats = show;
pierceBuilding = true;
hitColor = backColor = trailColor = Color.valueOf("feb380");
frontColor = Color.white;

View File

@@ -43,11 +43,15 @@ public class RepairFieldAbility extends Ability{
super.addStats(t);
t.add(Core.bundle.format("bullet.range", Strings.autoFixed(range / tilesize, 2)));
t.row();
t.add(abilityStat("repairspeed", Strings.autoFixed(amount * 60f / reload, 2)));
t.add(abilityStat("firingrate", Strings.autoFixed(60f / reload, 2)));
t.row();
if(amount > 0){
t.add(Core.bundle.format("bullet.healamount", Strings.autoFixed(amount, 2)) + "[lightgray] ~ []" + abilityStat("repairspeed", Strings.autoFixed(amount * 60f / reload, 2)));
t.row();
}
if(healPercent > 0f){
t.row();
t.add(Core.bundle.format("bullet.healpercent", Strings.autoFixed(healPercent, 2)));
t.add(Core.bundle.format("bullet.healpercent", Strings.autoFixed(healPercent, 2)) + "[lightgray] ~ []" + abilityStat("repairspeed", Strings.autoFixed(healPercent * 60f / reload, 2) + "%"));
}
if(sameTypeHealMult != 1f){
t.row();

View File

@@ -38,7 +38,7 @@ public class ShieldRegenFieldAbility extends Ability{
t.row();
t.add(abilityStat("firingrate", Strings.autoFixed(60f / reload, 2)));
t.row();
t.add(abilityStat("pulseregen", Strings.autoFixed(amount, 2)));
t.add(abilityStat("pulseregen", Strings.autoFixed(amount, 2)) + "[lightgray] ~ []" + abilityStat("regen", Strings.autoFixed(amount * 60f / reload, 2)));
t.row();
t.add(abilityStat("shield", Strings.autoFixed(max, 2)));
}

View File

@@ -241,6 +241,8 @@ public class BulletType extends Content implements Cloneable{
public Effect healEffect = Fx.healBlockFull;
/** Bullets spawned when this bullet is created. Rarely necessary, used for visuals. */
public Seq<BulletType> spawnBullets = new Seq<>();
/** Whether to display the stats of the spawned bullet. */
public boolean showStats = false;
/** Random angle spread of spawn bullets. */
public float spawnBulletRandomSpread = 0f;
/** Unit spawned _instead of_ this bullet. Useful for missiles. */

View File

@@ -113,10 +113,17 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
public void updateBoosting(boolean boost){
updateBoosting(boost, false);
}
public void updateBoosting(boolean boost, boolean event){
if(!type.canBoost || dead) return;
boolean shouldBoost = boost || onSolid() || (isFlying() && !canLand());
elevation = Mathf.approachDelta(elevation, type.canBoost ? Mathf.num(shouldBoost) : 0f, shouldBoost ? type.riseSpeed : type.descentSpeed);
if(event){
Events.fire(Trigger.unitCommandBoost);
}
}
/** Move based on preferred unit movement type. */

View File

@@ -46,6 +46,7 @@ public class EventType{
unitCommandChange,
unitCommandPosition,
unitCommandAttack,
unitCommandBoost,
importMod,
draw,
drawOver,

View File

@@ -177,6 +177,12 @@ public class GameService{
}
});
Events.run(Trigger.unitCommandBoost, () -> {
if(campaign()){
boostUnit.complete();
}
});
Events.run(Trigger.newGame, () -> Core.app.post(() -> {
if(campaign() && player.core() != null && player.core().items.total() >= 10 * 1000){
drop10kitems.complete();

View File

@@ -794,8 +794,10 @@ public class UnitType extends UnlockableContent implements Senseable{
}
if(legSplashDamage > 0 && legSplashRange > 0){
stats.add(Stat.legSplashDamage, legSplashDamage, StatUnit.perLeg);
stats.add(Stat.legSplashRange, Strings.autoFixed(legSplashRange / tilesize, 1), StatUnit.blocks);
stats.add(Stat.legSplashDamage, table -> {
table.add((String)(Core.bundle.format("bullet.splashdamage", Strings.autoFixed(legSplashDamage, 2),
Strings.autoFixed(legSplashRange / tilesize, 2))).replace("[stat]", "[white]") + " " + StatUnit.perLeg.localized());
});
}
stats.add(Stat.targetsAir, targetAir);
@@ -809,6 +811,9 @@ public class UnitType extends UnlockableContent implements Senseable{
if(!flying){
stats.add(Stat.canBoost, canBoost);
if(canBoost){
stats.add(Stat.boostingSpeed, boostMultiplier * speed * 60f / tilesize, StatUnit.tilesSecond);
}
}
if(mineTier >= 1){

View File

@@ -1,13 +1,18 @@
package mindustry.type.weapons;
import arc.*;
import arc.graphics.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
@@ -38,6 +43,12 @@ public class PointDefenseWeapon extends Weapon{
targetInterval = 10f;
}
@Override
public void addStats(UnitType u, Table t){
t.add(Core.bundle.format("weapon.pointdefense"));
super.addStats(u, t);
}
@Override
protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){
return Groups.bullet.intersect(x - range, y - range, range*2, range*2).min(b -> b.team != unit.team && b.type().hittable, b -> b.dst2(x, y));

View File

@@ -1,5 +1,6 @@
package mindustry.world.blocks.defense;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
@@ -11,6 +12,7 @@ import mindustry.entities.bullet.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.world.*;
import mindustry.world.meta.*;
public class ShockMine extends Block{
public final int timerDamage = timers++;
@@ -35,6 +37,14 @@ public class ShockMine extends Block{
targetable = false;
}
@Override
public void setStats(){
super.setStats();
stats.add(Stat.damage, table -> {
table.add((String)(Core.bundle.format("bullet.lightning", tendrils, Strings.autoFixed(damage, 2)).replace("[stat]", "[white]")));
});
}
public class ShockMineBuild extends Building{
@Override

View File

@@ -52,6 +52,7 @@ public class PointDefenseTurret extends ReloadTurret{
super.setStats();
stats.add(Stat.reload, 60f / reload, StatUnit.perSecond);
stats.add(Stat.damage, bulletDamage, StatUnit.none);
}
public class PointDefenseBuild extends ReloadTurretBuild{

View File

@@ -30,7 +30,9 @@ public class Junction extends Block{
//(60f / speed * capacity) returns 13.84 which is not the actual value (non linear, depends on fps)
stats.add(Stat.itemsMoved, displayedSpeed, StatUnit.itemsSecond);
stats.add(Stat.itemCapacity, capacity, StatUnit.items);
stats.add(Stat.itemCapacity, table -> {
table.add(Strings.autoFixed(capacity, 2) + " " + StatUnit.items.localized() + " " + StatUnit.perSide.localized());
});
}
@Override

View File

@@ -59,6 +59,13 @@ public class ImpactReactor extends PowerGenerator{
if(hasItems){
stats.add(Stat.productionTime, itemDuration / 60f, StatUnit.seconds);
}
//exponential decay formula
float max = -(float)Math.log(0.001f) / warmupSpeed / 60f;
float equal = -(float)Math.log(1f - Mathf.pow(consPower.usage / powerProduction, 1f / 5f)) / warmupSpeed / 60f;
stats.add(Stat.warmupTime, t -> {
t.add(Strings.autoFixed(max, 2) + " " + StatUnit.seconds.localized() + (consPower != null ?
" ~ " + Strings.autoFixed(equal, 2) + " " + StatUnit.seconds.localized() + " " + StatUnit.powerEquilibrium.localized() : ""));
});
}
public class ImpactReactorBuild extends GeneratorBuild{

View File

@@ -36,6 +36,7 @@ public class Stat implements Comparable<Stat>{
lightningDamage = new Stat("lightningDamage"),
abilities = new Stat("abilities"),
canBoost = new Stat("canBoost"),
boostingSpeed = new Stat("boostingspeed"),
maxUnits = new Stat("maxUnits"),
damageMultiplier = new Stat("damageMultiplier"),
@@ -60,6 +61,7 @@ public class Stat implements Comparable<Stat>{
powerRange = new Stat("powerRange", StatCat.power),
powerConnections = new Stat("powerConnections", StatCat.power),
basePowerGeneration = new Stat("basePowerGeneration", StatCat.power),
warmupTime = new Stat("warmupTime", StatCat.power),
tiles = new Stat("tiles", StatCat.crafting),
input = new Stat("input", StatCat.crafting),

View File

@@ -20,6 +20,7 @@ public class StatUnit{
itemsSecond = new StatUnit("itemsSecond"),
liquidUnits = new StatUnit("liquidUnits", "[sky]" + Iconc.liquid + "[]"),
powerUnits = new StatUnit("powerUnits", "[accent]" + Iconc.power + "[]"),
powerEquilibrium = new StatUnit("powerEquilibrium"),
heatUnits = new StatUnit("heatUnits", "[red]" + Iconc.waves + "[]"),
degrees = new StatUnit("degrees"),
seconds = new StatUnit("seconds"),
@@ -29,6 +30,7 @@ public class StatUnit{
perMinute = new StatUnit("perMinute", false),
perShot = new StatUnit("perShot", false),
perLeg = new StatUnit("perLeg"),
perSide = new StatUnit("perSide"),
timesSpeed = new StatUnit("timesSpeed", false),
multiplier = new StatUnit("multiplier", false),
percent = new StatUnit("percent", false),

View File

@@ -7,6 +7,7 @@ import arc.graphics.g2d.*;
import arc.math.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.Tooltip.*;
import arc.scene.ui.layout.*;
@@ -19,6 +20,7 @@ import mindustry.ctype.*;
import mindustry.entities.abilities.*;
import mindustry.entities.bullet.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.ui.*;
@@ -31,6 +33,9 @@ import static mindustry.Vars.*;
/** Utilities for displaying certain stats in a table. */
public class StatValues{
//only allocate once, dont break unit tests
static @Nullable TextureRegionDrawable noteIcon = Icon.arrowNoteSmall != null ? new TextureRegionDrawable(Icon.arrowNoteSmall) : null;
public static StatValue string(String value, Object... args){
String result = Strings.format(value, args);
return table -> table.add(result);
@@ -640,11 +645,8 @@ public class StatValues{
}
if(type.damage > 0 && (type.collides || type.splashDamage <= 0)){
if(type.continuousDamage() > 0){
bt.add(Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized());
}else{
bt.add(Core.bundle.format("bullet.damage", type.damage));
}
bt.add(Core.bundle.format("bullet.damage", type.damage) + (type.continuousDamage() > 0 ?
"[lightgray] ~ [stat]" + Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized() : ""));
}
if(type.buildingDamageMultiplier != 1){
@@ -700,17 +702,44 @@ public class StatValues{
sep(bt, Core.bundle.format("bullet.lightning", type.lightning, type.lightningDamage < 0 ? type.damage : type.lightningDamage));
}
if(type instanceof LaserBulletType b && b.lightningSpacing > 0){
int count = (int)(b.length / b.lightningSpacing) * 2 + 2;
float damage = b.lightningDamage < 0 ? b.damage : b.lightningDamage;
sep(bt, Core.bundle.format("bullet.lightning", count, damage));
note(bt, Core.bundle.format("bullet.lightninginterval", Strings.autoFixed(b.lightningSpacing / tilesize, 2), Strings.autoFixed(b.lightningLength, 2))).left();
}
if(type instanceof EmpBulletType b && b.radius > 0f){
sep(bt, Core.bundle.format("bullet.empradius", Strings.fixed(b.radius / tilesize, 1)));
if(b.timeDuration > 0f && b.timeIncrease > 1f){
sep(bt, Core.bundle.format("bullet.empboost", Strings.autoFixed(b.timeIncrease * 100f, 2),
Strings.autoFixed(b.timeDuration / 60f, 1)) + " " + StatUnit.seconds.localized());
}
if(b.timeDuration > 0f && b.powerSclDecrease < 1f){
sep(bt, Core.bundle.format("bullet.empslowdown",
(b.powerSclDecrease < 1f ? "[negstat]" : "") + Strings.autoFixed((b.powerSclDecrease - 1f) * 100f, 2),
Strings.autoFixed(b.timeDuration / 60f, 1)) + " " + StatUnit.seconds.localized());
}
if(!Mathf.equal(b.powerDamageScl, 1f)){
sep(bt, Core.bundle.format("bullet.empdamage", Strings.autoFixed(b.powerDamageScl * 100f, 2)));
}
if(b.hitUnits){
sep(bt, Core.bundle.format("bullet.empunitdamage",
(b.unitDamageScl < 1f ? "[negstat]" : "") + Strings.autoFixed(b.unitDamageScl * 100f, 2)));
}
}
if(type.pierceArmor){
sep(bt, "@bullet.armorpierce");
}
if(type.armorMultiplier != 1f){
if(type.armorMultiplier > 1f){
sep(bt, Core.bundle.format("bullet.armorweakness", (int)(type.armorMultiplier * 100)));
sep(bt, Core.bundle.format("bullet.armorweakness", (int)(type.armorMultiplier * 100)));
}else if(Mathf.sign(type.armorMultiplier) == 1){
sep(bt, Core.bundle.format("bullet.armorpiercing", (int)((1 - type.armorMultiplier) * 100)));
sep(bt, Core.bundle.format("bullet.armorpiercing", (int)((1 - type.armorMultiplier) * 100)));
}else{
sep(bt, Core.bundle.format("bullet.antiarmor", (-type.armorMultiplier)));
sep(bt, Core.bundle.format("bullet.antiarmor", (-type.armorMultiplier)));
}
}
@@ -770,6 +799,27 @@ public class StatValues{
bt.row();
bt.add(coll);
}
if(type.spawnBullets != null && type.spawnBullets.size > 0){
bt.row();
Table sc = new Table();
for(BulletType spawn : type.spawnBullets){
if(spawn.showStats) ammo(ObjectMap.of(t, spawn), true, false).display(sc);
}
Collapser coll = new Collapser(sc, true);
coll.setDuration(0.1f);
bt.table(st -> {
st.left().defaults().left();
st.add(Core.bundle.format("bullet.spawnBullets", type.spawnBullets.size));
if(sc.getChildren().size > 0) st.button(Icon.downOpen, Styles.emptyi, () -> coll.toggle(false)).update(i -> i.getStyle().imageUp = (!coll.isCollapsed() ? Icon.upOpen : Icon.downOpen)).size(8).padLeft(16f).expandX();
});
bt.row();
bt.add(coll);
}
}).padLeft(5).padTop(5).padBottom(compact ? 0 : 5).growX().margin(compact ? 0 : 10);
table.row();
}
@@ -782,6 +832,19 @@ public class StatValues{
return table.add(text);
}
//add a note under a value
private static Cell<?> note(Table table, String text){
table.row();
return table.table(t -> {
if(noteIcon != null){
noteIcon.setMinWidth(15f);
noteIcon.setMinHeight(15f);
t.image(noteIcon).color(Pal.stat).scaling(Scaling.fit).padRight(6).padLeft(12);
}
t.add(text);
});
}
//for AmmoListValue
private static String ammoStat(float val){
return (val > 0 ? "[stat]+" : "[negstat]") + Strings.autoFixed(val, 1);