diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties index 24e21b4060..bd5b3c37b7 100644 --- a/annotations/src/main/resources/classids.properties +++ b/annotations/src/main/resources/classids.properties @@ -6,6 +6,7 @@ atrax=1 beta=30 block=2 corvus=24 +dummy=49 elude=45 flare=3 gamma=31 diff --git a/annotations/src/main/resources/revisions/dummy/0.json b/annotations/src/main/resources/revisions/dummy/0.json new file mode 100644 index 0000000000..94cd31bf55 --- /dev/null +++ b/annotations/src/main/resources/revisions/dummy/0.json @@ -0,0 +1 @@ +{fields:[{name:abilities,type:"mindustry.entities.abilities.Ability[]"},{name:aimX,type:float},{name:aimY,type:float},{name:building,type:Building},{name:controller,type:mindustry.entities.units.UnitController},{name:elevation,type:float},{name:flag,type:double},{name:health,type:float},{name:isShooting,type:boolean},{name:mineTile,type:mindustry.world.Tile},{name:mounts,type:"mindustry.entities.units.WeaponMount[]"},{name:plans,type:arc.struct.Queue},{name:rotation,type:float},{name:shield,type:float},{name:spawnedByCore,type:boolean},{name:stack,type:mindustry.type.ItemStack},{name:statuses,type:arc.struct.Seq},{name:team,type:mindustry.game.Team},{name:type,type:mindustry.type.UnitType},{name:updateBuilding,type:boolean},{name:vel,type:arc.math.geom.Vec2},{name:x,type:float},{name:y,type:float}]} \ No newline at end of file diff --git a/core/assets-raw/sprites/blocks/defense/target-dummy-preview.png b/core/assets-raw/sprites/blocks/defense/target-dummy-preview.png new file mode 100644 index 0000000000..f90824a0de Binary files /dev/null and b/core/assets-raw/sprites/blocks/defense/target-dummy-preview.png differ diff --git a/core/assets-raw/sprites/blocks/defense/target-dummy-team.png b/core/assets-raw/sprites/blocks/defense/target-dummy-team.png new file mode 100644 index 0000000000..4875a72c1a Binary files /dev/null and b/core/assets-raw/sprites/blocks/defense/target-dummy-team.png differ diff --git a/core/assets-raw/sprites/blocks/defense/target-dummy-tether-end.png b/core/assets-raw/sprites/blocks/defense/target-dummy-tether-end.png new file mode 100644 index 0000000000..2e17e46fae Binary files /dev/null and b/core/assets-raw/sprites/blocks/defense/target-dummy-tether-end.png differ diff --git a/core/assets-raw/sprites/blocks/defense/target-dummy-tether.png b/core/assets-raw/sprites/blocks/defense/target-dummy-tether.png new file mode 100644 index 0000000000..0bcd1cda23 Binary files /dev/null and b/core/assets-raw/sprites/blocks/defense/target-dummy-tether.png differ diff --git a/core/assets-raw/sprites/blocks/defense/target-dummy.png b/core/assets-raw/sprites/blocks/defense/target-dummy.png new file mode 100644 index 0000000000..8ae840d918 Binary files /dev/null and b/core/assets-raw/sprites/blocks/defense/target-dummy.png differ diff --git a/core/assets-raw/sprites/units/dummy-cell.png b/core/assets-raw/sprites/units/dummy-cell.png new file mode 100644 index 0000000000..cdf4aac595 Binary files /dev/null and b/core/assets-raw/sprites/units/dummy-cell.png differ diff --git a/core/assets-raw/sprites/units/dummy.png b/core/assets-raw/sprites/units/dummy.png new file mode 100644 index 0000000000..988bb37e85 Binary files /dev/null and b/core/assets-raw/sprites/units/dummy.png differ diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 8e5fa406a2..198a9cd380 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -1237,6 +1237,8 @@ stat.reactive = Reacts stat.immunities = Immunities stat.healing = Healing stat.efficiency = [stat]{0}% Efficiency +stat.hitsize = Hitbox Size +stat.resettime = Reset Time ability.forcefield = Force Field ability.forcefield.description = Projects a force shield that absorbs bullets @@ -1324,6 +1326,9 @@ bar.activationtimer = Activates in {0} bar.activated = Activated bar.upto = Up to {0} bar.lerpwarmuptime = Affected by overdrive. Non-linear; uses the exponential decay formula.\nFor reference, this will reach 90% progress in {0} seconds, and 99% in {1} seconds. +bar.damagetimetotal = {0} dmg/{1} sec +bar.hits = {0} hits +bar.dps = {0} DPS units.processorcontrol = [lightgray]Processor Controlled @@ -1367,6 +1372,7 @@ bullet.notargetsbuildings = [stat] ignores buildings unit.blocks = blocks unit.blockssquared = blocks\u00B2 +unit.worldunits = world units unit.powersecond = power units/second unit.tilessecond = tiles/second unit.liquidsecond = liquid units/second @@ -2657,6 +2663,8 @@ block.large-logic-display.description = Displays arbitrary graphics from a logic block.tile-logic-display.description = Displays arbitrary graphics from a logic processor.\nSeamlessly connects to nearby tiled display blocks. block.interplanetary-accelerator.description = A massive electromagnetic railgun tower. Accelerates cores to escape velocity for interplanetary deployment. block.repair-turret.description = Continuously repairs the closest damaged unit in its vicinity. Optionally accepts coolant. +block.target-dummy.name = Target Dummy +block.target-dummy.description = Spawns a target dummy to test damage per second. Armor and flight can be configured.\nSandbox only. #Erekir block.core-bastion.description = Core of the base. Armored. Once destroyed, the sector is lost. diff --git a/core/assets/icons/icons.properties b/core/assets/icons/icons.properties index 15c0eb3ff2..524d60bdb7 100755 --- a/core/assets/icons/icons.properties +++ b/core/assets/icons/icons.properties @@ -624,3 +624,5 @@ 63058=metal-wall-3|block-metal-wall-3-ui 63057=ore-wall-graphite|block-ore-wall-graphite-ui 63056=large-canvas|block-large-canvas-ui +63055=target-dummy|block-target-dummy-ui +63054=dummy|unit-dummy-ui diff --git a/core/assets/logicids.dat b/core/assets/logicids.dat index f113f65920..1536e5d80a 100644 Binary files a/core/assets/logicids.dat and b/core/assets/logicids.dat differ diff --git a/core/assets/maps/serpulo/cruxscape.msav b/core/assets/maps/serpulo/cruxscape.msav index 696c4de480..7572a777bb 100644 Binary files a/core/assets/maps/serpulo/cruxscape.msav and b/core/assets/maps/serpulo/cruxscape.msav differ diff --git a/core/src/mindustry/ai/types/NoAI.java b/core/src/mindustry/ai/types/NoAI.java new file mode 100644 index 0000000000..da5ce16220 --- /dev/null +++ b/core/src/mindustry/ai/types/NoAI.java @@ -0,0 +1,29 @@ +package mindustry.ai.types; + +import mindustry.entities.units.*; +import mindustry.gen.*; + +public class NoAI implements UnitController{ + protected Unit unit; + + @Override + public Unit unit(){ + return unit; + } + + @Override + public void unit(Unit unit){ + this.unit = unit; + } + + @Override + public boolean isValidController(){ + return true; + } + + @Override + public void updateUnit(){} + + @Override + public void removed(Unit unit){} +} \ No newline at end of file diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index b616d762ed..5e1d3a85ae 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -105,7 +105,7 @@ public class Blocks{ shieldedWall, //defense - mender, mendProjector, overdriveProjector, overdriveDome, forceProjector, shockMine, + mender, mendProjector, overdriveProjector, overdriveDome, forceProjector, shockMine, targetDummy, //defense - erekir radar, @@ -6783,6 +6783,12 @@ public class Blocks{ allDatabaseTabs = true; }}; + targetDummy = new TargetDummy("target-dummy"){{ + requirements(Category.defense, BuildVisibility.sandboxOnly, with()); + size = 2; + alwaysUnlocked = true; + }}; + //TODO move illuminator = new LightBlock("illuminator"){{ requirements(Category.effect, BuildVisibility.lightingOnly, with(Items.graphite, 12, Items.silicon, 8, Items.lead, 8)); diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index ac15c2bb6a..decbe1c848 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -78,6 +78,7 @@ public class UnitTypes{ //special building tethered (has payload capability, because it's necessary sometimes) public static @EntityDef({Unitc.class, BuildingTetherc.class, Payloadc.class}) UnitType manifold, assemblyDrone; + public static @EntityDef({TargetDummyc.class, Unitc.class}) UnitType dummy; //tank public static @EntityDef({Unitc.class, Tankc.class}) UnitType stell, locus, precept, vanquish, conquer; @@ -4631,6 +4632,40 @@ public class UnitTypes{ envDisabled = Env.none; }}; + dummy = new UnitType("dummy"){{ + controller = u -> new NoAI(); + + envEnabled = Env.any; + envDisabled = 0; + isEnemy = false; + allowedInPayloads = false; + logicControllable = false; + playerControllable = false; + hidden = true; + hoverable = false; + canBoost = true; + useUnitCap = false; + killable = false; + physics = false; + + flyingLayer = Layer.flyingUnit - 1f; + drag = 0.33f; + hitSize = 12f; + hideDetails = false; + engineOffset = 7f; + engineSize = 2f; + for(int i = 0; i < 3; i++){ + engines.add(new UnitEngine(Geometry.d4x(i) * engineOffset, Geometry.d4y(i) * engineOffset, engineSize, i * 90)); + } + } + + @Override + public void drawBody(Unit unit) { + applyColor(unit); + Drawf.spinSprite(region, unit.x, unit.y, unit.rotation - 90); + Draw.reset(); + }}; + //endregion } } diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index eff3177eb2..e0bbef2c9c 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -506,9 +506,11 @@ public class BulletType extends Content implements Cloneable{ } if(entity instanceof Unit unit){ - Tmp.v3.set(unit).sub(b).nor().scl(knockback * 80f); - if(impact) Tmp.v3.setAngle(b.rotation() + (knockback < 0 ? 180f : 0f)); - unit.impulse(Tmp.v3); + if(unit.type.knockbackMultiplier > 0f){ + Tmp.v3.set(unit).sub(b).nor().scl(knockback * 80f * unit.type.knockbackMultiplier); + if(impact) Tmp.v3.setAngle(b.rotation() + (knockback < 0 ? 180f : 0f)); + unit.impulse(Tmp.v3); + } unit.apply(status, statusDuration); Events.fire(bulletDamageEvent.set(unit, b)); diff --git a/core/src/mindustry/entities/comp/TargetDummyComp.java b/core/src/mindustry/entities/comp/TargetDummyComp.java new file mode 100644 index 0000000000..c0e12ffb72 --- /dev/null +++ b/core/src/mindustry/entities/comp/TargetDummyComp.java @@ -0,0 +1,25 @@ +package mindustry.entities.comp; + +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.world.blocks.defense.TargetDummy.*; + +@Component +abstract class TargetDummyComp implements Unitc, Healthc{ + public @Nullable Building building; + + @Override + public void update(){ + if(building == null || (!building.isPayload() && !building.isValid())){ + Call.unitDespawn(self()); //Don't despawn even if the building is on another team + } + } + + @Override + public void rawDamage(float amount){ + if(building instanceof TargetDummyBuild td){ + td.dummyHit(amount); + } + } +} \ No newline at end of file diff --git a/core/src/mindustry/mod/ClassMap.java b/core/src/mindustry/mod/ClassMap.java index 1cfd5058dd..2c143e5105 100644 --- a/core/src/mindustry/mod/ClassMap.java +++ b/core/src/mindustry/mod/ClassMap.java @@ -23,6 +23,7 @@ public class ClassMap{ classes.put("PrebuildAI", mindustry.ai.types.PrebuildAI.class); classes.put("RepairAI", mindustry.ai.types.RepairAI.class); classes.put("SuicideAI", mindustry.ai.types.SuicideAI.class); + classes.put("NoAI", mindustry.ai.types.NoAI.class); classes.put("Ability", mindustry.entities.abilities.Ability.class); classes.put("ArmorPlateAbility", mindustry.entities.abilities.ArmorPlateAbility.class); classes.put("EmptyDataAbility", mindustry.entities.abilities.EmptyDataAbility.class); @@ -190,6 +191,8 @@ public class ClassMap{ classes.put("ThrusterBuild", mindustry.world.blocks.defense.Thruster.ThrusterBuild.class); classes.put("Wall", mindustry.world.blocks.defense.Wall.class); classes.put("WallBuild", mindustry.world.blocks.defense.Wall.WallBuild.class); + classes.put("TargetDummy", mindustry.world.blocks.defense.TargetDummy.class); + classes.put("TargetDummyBuild", mindustry.world.blocks.defense.TargetDummy.TargetDummyBuild.class); classes.put("BaseTurret", mindustry.world.blocks.defense.turrets.BaseTurret.class); classes.put("BaseTurretBuild", mindustry.world.blocks.defense.turrets.BaseTurret.BaseTurretBuild.class); classes.put("ContinuousLiquidTurret", mindustry.world.blocks.defense.turrets.ContinuousLiquidTurret.class); diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index c7e2860a8e..f3ef29b655 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -110,6 +110,8 @@ public class UnitType extends UnlockableContent implements Senseable{ strafePenalty = 0.5f, /** multiplier for cost of research in tech tree */ researchCostMultiplier = 50, + /** multiplier for knockback this unit receives */ + knockbackMultiplier = 1f, /** for ground units, the layer upon which this unit is drawn */ groundLayer = Layer.groundUnit, diff --git a/core/src/mindustry/world/blocks/defense/TargetDummy.java b/core/src/mindustry/world/blocks/defense/TargetDummy.java new file mode 100644 index 0000000000..be2cd52f04 --- /dev/null +++ b/core/src/mindustry/world/blocks/defense/TargetDummy.java @@ -0,0 +1,285 @@ +package mindustry.world.blocks.defense; + +import arc.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.TextField.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.graphics.*; +import mindustry.entities.units.*; +import mindustry.game.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class TargetDummy extends Block{ + public final int dpsUpdateTime = timers++; + public UnitType unitType = UnitTypes.dummy; + public float pullScale = 0.33f; + public String emptyStr = "---"; + public @Load(value = "@-tether-end", fallback = "@") TextureRegion tetherEnd; + public @Load(value = "@-tether", fallback = "@") TextureRegion tether; + public @Load(value = "@-preview", fallback = "@") TextureRegion previewRegion; + + public TargetDummy(String name){ + super(name); + + update = alwaysUpdateInUnits = true; + configurable = logicConfigurable = true; + underBullets = true; + targetable = false; + + saveConfig = true; + + config(FloatSeq.class, (TargetDummyBuild tile, FloatSeq config) -> { + if(config.size > 0){ + tile.unitTeam = tile.team; + if(config.get(0) == 1) tile.unitTeam = Team.get(tile.dummyTeam()); + } + if(config.size > 1) tile.boosting = config.get(1) == 1; + if(config.size > 2) tile.unitArmor = config.get(2); + if(config.size > 3) tile.resetTime = config.get(3); + if(config.size > 4) tile.dummySize = config.get(4); + }); + } + + @Override + protected TextureRegion[] icons(){ + return new TextureRegion[]{previewRegion}; + } + + @Override + public void setBars(){ + super.setBars(); + removeBar("health"); + + addBar("dps", (TargetDummyBuild entity) -> new Bar( + () -> entity.displayDPS(false), + () -> Pal.ammo, + () -> 1f - (entity.reset / entity.resetTime) + )); + } + + @Override + public void drawDefaultPlanRegion(BuildPlan plan, Eachable list){ + super.drawDefaultPlanRegion(plan, list); + + Draw.rect(unitType.fullIcon, plan.drawx(), plan.drawy()); + } + + public class TargetDummyBuild extends Building implements UnitTetherBlock{ + //needs to be "unboxed" after reading, since units are read after buildings. + public int readUnitId = -1; + public Unit unit; + public float resetTime = 120f; + public float total, reset = resetTime, time, dummySize = 12f; + public float dps, totalDisplay, timeDisplay; + public int hits; + public int hitsDisplay; + public boolean boosting; + public float unitArmor; + public Team unitTeam; + + @Override + public void updateTile(){ + //unit was lost/destroyed somehow + if(unit != null && (unit.dead || !unit.isAdded())){ + unit = null; + } + + if(readUnitId != -1){ + unit = Groups.unit.getByID(readUnitId); + if(unit != null || !net.client()){ + readUnitId = -1; + } + } + + if(unitTeam == null) unitTeam = team; + + if(unit == null){ + if(!net.client()){ + unit = unitType.create(team); + if(unit instanceof TargetDummyUnit td){ + td.building(this); + } + unit.set(x, y); + unit.rotation = 90f; + unit.add(); + Call.unitTetherBlockSpawned(tile, unit.id); + } + } + + if(unit != null){ + unit.updateBoosting(boosting); + unit.armor(unitArmor); + unit.team(unitTeam); + unit.hitSize = dummySize; + + //similar to impulseNet but does not factor in mass + Tmp.v1.set(this).sub(unit).limit(dst(unit) * pullScale * Time.delta); + unit.vel.add(Tmp.v1); + + //manually move units to simulate velocity for remote players + if(unit.isRemote()) unit.move(Tmp.v1); + + if(unit.moving()) unit.lookAt(unit.vel().angle()); + } + + time += Time.delta; + reset += Time.delta; + + if(timer(dpsUpdateTime, 20)){ + dps = total / time * 60f; + totalDisplay = total; + timeDisplay = time / 60f; + hitsDisplay = hits; + } + + if(reset >= resetTime){ + total = time = dps = hits = 0; + } + } + + public void spawned(int id){ + Fx.spawn.at(x, y); + if(net.client()){ + readUnitId = id; + } + } + + @Override + public void draw(){ + super.draw(); + + if(tether.found() && unit != null){ + float z = unit.elevation > 0.5f ? unit.type.flyingLayer - 0.01f : unitType.groundLayer + Mathf.clamp(unitType.hitSize / 4000f, 0, 0.01f); + Draw.z(z - 0.01f); + Draw.color(team.color); + Drawf.laser(tether, tetherEnd, x, y, unit.x, unit.y); + Draw.color(); + } + + Draw.z(Layer.overlayUI); + String text = displayDPS(true) + "\n" + "(" + displayTotal() + ")" + "\n" + displayHits(); + Drawf.text(text, x, y + size / (2.5f*2f), team.color, size / 2.5f); + } + + @Override + public void drawSelect(){ + Drawf.square(x, y, dummySize, 0f, team.color); + } + + public String displayDPS(boolean round){ + if(time > 0){ + return Core.bundle.format("bar.dps", (round ? (dps > 0 ? Mathf.round(dps) : emptyStr) : Strings.autoFixed(total / time * 60f, 2))); + }else{ + return Core.bundle.format("bar.dps", emptyStr); + } + } + + public String displayTotal(){ + if(time > 0){ + return Core.bundle.format("bar.damagetimetotal", Mathf.round(totalDisplay), Strings.autoFixed(timeDisplay, 2)); + }else{ + return Core.bundle.format("bar.damagetimetotal", emptyStr, emptyStr); + } + } + + public String displayHits(){ + if(time > 0){ + return Core.bundle.format("bar.hits", hitsDisplay); + }else{ + return Core.bundle.format("bar.hits", emptyStr); + } + } + + @Override + public boolean collide(Bullet other){ //Hit the unit, not the building + return false; + } + + @Override + public boolean collision(Bullet other){ //Hit the unit, not the building + return false; + } + + public void dummyHit(float damage){ + reset = 0f; + total += damage; + hits++; + } + + @Override + public void damage(float damage){ + //just in case + } + + @Override + public void buildConfiguration(Table table){ + table.table(t -> { + t.background(Styles.black6); + t.defaults().left(); + + t.check(Core.bundle.get("rules.enemyteam"), unitTeam != team, b -> configureFloat(0, Mathf.num(b))).colspan(3).row(); + t.check(Core.bundle.get("stat.flying"), boosting, b -> configureFloat(1, Mathf.num(b))).colspan(3).row(); + t.add(Core.bundle.get("stat.armor")); + t.field("" + unitArmor, TextFieldFilter.floatsOnly, s -> configureFloat(2, Strings.parseFloat(s))).width(200f).padLeft(8f).colspan(2).row(); + t.add(Core.bundle.get("stat.resettime")); + t.field(Strings.autoFixed(resetTime / 60f, 2), TextFieldFilter.floatsOnly, s -> configureFloat(3, Strings.parseFloat(s) * 60f)).padLeft(8f).growX(); + t.add(StatUnit.seconds.localized()).padLeft(8).row(); + t.add(Core.bundle.get("stat.hitsize")); + t.field("" + (dummySize/tilesize), TextFieldFilter.floatsOnly, s -> configureFloat(4, Strings.parseFloat(s) * tilesize)).padLeft(8f).growX(); + t.add(StatUnit.blocks.localized()).padLeft(8f); + }).top().grow().margin(8f); + } + + //helper to send the rest of values, since config does not support arrays + public void configureFloat(int index, float value){ + FloatSeq seq = FloatSeq.with(Mathf.num(unitTeam != team), Mathf.num(boosting), unitArmor, resetTime, dummySize); + seq.set(index, value); + configure(seq); + } + + public int dummyTeam(){ + if(unitTeam != team) return team.id; //Return to own team + return team != state.rules.waveTeam ? state.rules.waveTeam.id : (team.id % (Team.all.length - 1)) + 1; + } + + @Override + public Object config(){ + return FloatSeq.with(Mathf.num(unitTeam != team), Mathf.num(boosting), unitArmor, resetTime, dummySize); + } + + @Override + public void write(Writes write){ + super.write(write); + write.i(unit == null ? -1 : unit.id); + write.bool(boosting); + write.f(unitArmor); + write.f(resetTime); + write.f(dummySize); + write.i(unitTeam.id); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + readUnitId = read.i(); + boosting = read.bool(); + unitArmor = read.f(); + resetTime = read.f(); + dummySize = read.f(); + unitTeam = Team.get(read.i()); + } + } +} \ No newline at end of file diff --git a/core/src/mindustry/world/meta/StatUnit.java b/core/src/mindustry/world/meta/StatUnit.java index b4c1dc07ae..48770d871a 100644 --- a/core/src/mindustry/world/meta/StatUnit.java +++ b/core/src/mindustry/world/meta/StatUnit.java @@ -14,6 +14,7 @@ public class StatUnit{ blocks = new StatUnit("blocks"), blocksSquared = new StatUnit("blocksSquared"), + worldUnits = new StatUnit("worldUnits"), tilesSecond = new StatUnit("tilesSecond"), powerSecond = new StatUnit("powerSecond", "[accent]" + Iconc.power + "[]"), liquidSecond = new StatUnit("liquidSecond", "[sky]" + Iconc.liquid + "[]"),