Merge branch 'master' of https://github.com/Anuken/Mindustry into tileable-logic-displays

# Conflicts:
#	core/assets/icons/icons.properties
#	core/assets/logicids.dat
This commit is contained in:
Anuken
2025-04-14 22:50:47 -04:00
494 changed files with 17743 additions and 9826 deletions
+5
View File
@@ -55,6 +55,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Log.info("[GL] Version: @", graphics.getGLVersion());
Log.info("[GL] Max texture size: @", maxTextureSize);
Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2");
if(gl30 == null) Log.warn("[GL] Your device or video drivers do not support OpenGL 3. This will cause performance issues.");
if(NvGpuInfo.hasMemoryInfo()){
Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024);
}
@@ -206,6 +207,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
@Override
public void update(){
PerfCounter.update.begin();
int targetfps = Core.settings.getInt("fpscap", 120);
boolean changed = lastTargetFps != targetfps && lastTargetFps != -1;
boolean limitFps = targetfps > 0 && targetfps <= 240;
@@ -256,6 +259,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Threads.sleep(toSleep / 1000000, (int)(toSleep % 1000000));
}
}
PerfCounter.update.end();
}
@Override
+15 -6
View File
@@ -28,7 +28,6 @@ import mindustry.net.*;
import mindustry.service.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*;
import java.io.*;
@@ -47,8 +46,14 @@ public class Vars implements Loadable{
public static boolean loadedLogger = false, loadedFileLogger = false;
/** Name of current Steam player. */
public static String steamPlayerName = "";
/** Min game version for all mods. */
public static final int minModGameVersion = 136;
/** Min game version for java mods specifically - this is higher, as Java mods have more breaking changes. */
public static final int minJavaModGameVersion = 147;
/** If true, the BE server list is always used. */
public static boolean forceBeServers = false;
/** If true, mod code and scripts do not run. For internal testing only. This WILL break mods if enabled. */
public static boolean skipModCode = false;
/** Default accessible content types used for player-selectable icons. */
public static final ContentType[] defaultContentIcons = {ContentType.item, ContentType.liquid, ContentType.block, ContentType.unit};
/** Default rule environment. */
@@ -69,7 +74,7 @@ public class Vars implements Loadable{
public static final String ghApi = "https://api.github.com";
/** URL for discord invite. */
public static final String discordURL = "https://discord.gg/mindustry";
/** URL the links to the wiki's modding guide.*/
/** Link to the wiki's modding guide.*/
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/1-modding/";
/** URLs to the JSON file containing all the BE servers. Only queried in BE. */
public static final String[] serverJsonBeURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_be.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_be.json"};
@@ -81,6 +86,8 @@ public class Vars implements Loadable{
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?labels=bug&template=bug_report.md";
/** list of built-in servers.*/
public static final Seq<ServerGroup> defaultServers = Seq.with();
/** maximum openGL errors logged */
public static final int maxGlErrors = 100;
/** maximum size of any block, do not change unless you know what you're doing */
public static final int maxBlockSize = 16;
/** maximum distance between mine and core that supports automatic transferring */
@@ -109,8 +116,6 @@ public class Vars implements Loadable{
public static final float invasionGracePeriod = 20;
/** min armor fraction damage; e.g. 0.05 = at least 5% damage */
public static final float minArmorDamage = 0.1f;
/** @deprecated see {@link CoreBlock#landDuration} instead! */
public static final @Deprecated float coreLandDuration = 160f;
/** size of tiles in units */
public static final int tilesize = 8;
/** size of one tile payload (^2) */
@@ -265,7 +270,7 @@ public class Vars implements Loadable{
public static NetServer netServer;
public static NetClient netClient;
public static Player player;
public static @Nullable Player player;
@Override
public void loadAsync(){
@@ -494,7 +499,11 @@ public class Vars implements Loadable{
//router
if(locale.toString().equals("router")){
bundle.debug("router");
I18NBundle defBundle = I18NBundle.createBundle(Core.files.internal("bundles/bundle"));
String router = Character.toString(Iconc.blockRouter);
for(String s : bundle.getKeys()){
bundle.getProperties().put(s, Strings.stripColors(defBundle.get(s)).replaceAll("\\S", router));
}
}
}
}
+1 -1
View File
@@ -258,7 +258,7 @@ public class BaseBuilderAI{
//queue it
for(Stile tile : result.tiles){
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block.id, tile.config));
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block, tile.config));
}
return true;
+1 -1
View File
@@ -82,7 +82,7 @@ public class BaseRegistry{
}
schem.tiles.removeAll(s -> s.block.buildVisibility == BuildVisibility.sandboxOnly);
part.tier = schem.tiles.sumf(s -> Mathf.pow(s.block.buildCost / s.block.buildCostMultiplier, 1.4f));
part.tier = schem.tiles.sumf(s -> Mathf.pow(s.block.buildTime / s.block.buildCostMultiplier, 1.4f));
if(part.core != null){
cores.add(part);
+144 -42
View File
@@ -7,6 +7,8 @@ import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.Units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
@@ -14,6 +16,7 @@ import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
@@ -28,11 +31,13 @@ public class BlockIndexer{
private int quadWidth, quadHeight;
/** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */
private IntSeq[][][] ores;
private IntSeq[][][] ores, wallOres;
/** Stores all damaged tile entities by team. */
private Seq<Building>[] damagedTiles = new Seq[Team.all.length];
/** All ores present on the map - can be wall or floor. */
private Seq<Item> allPresentOres = new Seq<>();
/** All ores available on this map. */
private ObjectIntMap<Item> allOres = new ObjectIntMap<>();
private ObjectIntMap<Item> allOres = new ObjectIntMap<>(), allWallOres = new ObjectIntMap<>();
/** Stores teams that are present here as tiles. */
private Seq<Team> activeTeams = new Seq<>(Team.class);
/** Maps teams to a map of flagged tiles by flag. */
@@ -41,6 +46,8 @@ public class BlockIndexer{
private boolean[] blocksPresent;
/** Array used for returning and reusing. */
private Seq<Building> breturnArray = new Seq<>(Building.class);
/** Maps block flag to a list of floor tiles that have it. */
private Seq<Tile>[] floorMap;
public BlockIndexer(){
clearFlags();
@@ -53,15 +60,23 @@ public class BlockIndexer{
addIndex(event.tile);
});
Events.on(TileFloorChangeEvent.class, event -> {
removeFloorIndex(event.tile, event.previous);
addFloorIndex(event.tile, event.floor);
});
Events.on(WorldLoadEvent.class, event -> {
damagedTiles = new Seq[Team.all.length];
flagMap = new Seq[Team.all.length][BlockFlag.all.length];
floorMap = new Seq[BlockFlag.all.length];
activeTeams = new Seq<>(Team.class);
clearFlags();
allOres.clear();
allWallOres.clear();
ores = new IntSeq[content.items().size][][];
wallOres = new IntSeq[content.items().size][][];
quadWidth = Mathf.ceil(world.width() / (float)quadrantSize);
quadHeight = Mathf.ceil(world.height() / (float)quadrantSize);
blocksPresent = new boolean[content.blocks().size];
@@ -78,28 +93,67 @@ public class BlockIndexer{
for(Tile tile : world.tiles){
process(tile);
var drop = tile.drop();
addFloorIndex(tile, tile.floor());
if(drop != null){
int qx = (tile.x / quadrantSize);
int qy = (tile.y / quadrantSize);
//add position of quadrant to list
if(tile.block() == Blocks.air){
if(ores[drop.id] == null){
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
}
if(ores[drop.id][qx][qy] == null){
ores[drop.id][qx][qy] = new IntSeq(false, 16);
}
Item drop;
int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
if(tile.block() == Blocks.air){
if((drop = tile.drop()) != null){
//add position of quadrant to list
if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight];
if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
ores[drop.id][qx][qy].add(tile.pos());
allOres.increment(drop);
}
}else if((drop = tile.wallDrop()) != null){
//add position of quadrant to list
if(wallOres[drop.id] == null) wallOres[drop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[drop.id][qx][qy] == null) wallOres[drop.id][qx][qy] = new IntSeq(false, 16);
wallOres[drop.id][qx][qy].add(tile.pos());
allWallOres.increment(drop);
}
}
updatePresentOres();
});
}
public Seq<Item> getAllPresentOres(){
return allPresentOres;
}
private void updatePresentOres(){
allPresentOres.clear();
for(Item item : content.items()){
if(hasOre(item) || hasWallOre(item)){
allPresentOres.add(item);
}
}
}
private void removeFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).remove(tile);
}
}
private void addFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0 || !floor.shouldIndex(tile)) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).add(tile);
}
}
public Seq<Tile> getFlaggedFloors(BlockFlag flag){
if(floorMap[flag.ordinal()] == null){
floorMap[flag.ordinal()] = new Seq<>(false);
}
return floorMap[flag.ordinal()];
}
public void removeIndex(Tile tile){
var team = tile.team();
if(tile.build != null && tile.isCenter()){
@@ -143,30 +197,37 @@ public class BlockIndexer{
public void addIndex(Tile tile){
process(tile);
var drop = tile.drop();
if(drop != null && ores != null){
int qx = tile.x / quadrantSize;
int qy = tile.y / quadrantSize;
Item drop = tile.drop(), wallDrop = tile.wallDrop();
if(drop == null && wallDrop == null) return;
int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
int pos = tile.pos();
if(ores[drop.id] == null){
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
}
if(ores[drop.id][qx][qy] == null){
ores[drop.id][qx][qy] = new IntSeq(false, 16);
}
int pos = tile.pos();
var seq = ores[drop.id][qx][qy];
if(tile.block() == Blocks.air){
//add the index if it is a valid new spot to mine at
if(!seq.contains(pos)){
seq.add(pos);
allOres.increment(drop);
if(tile.block() == Blocks.air){
if(drop != null){ //floor
if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight];
if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
if(ores[drop.id][qx][qy].addUnique(pos)){
int old = allOres.increment(drop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
}
}else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it
seq.removeValue(pos);
allOres.increment(drop, -1);
}
if(wallDrop != null && wallOres != null && wallOres[wallDrop.id] != null && wallOres[wallDrop.id][qx][qy] != null && wallOres[wallDrop.id][qx][qy].removeValue(pos)){ //wall
int old = allWallOres.increment(wallDrop, -1);
if(old == 1) updatePresentOres();
}
}else{
if(wallDrop != null){ //wall
if(wallOres[wallDrop.id] == null) wallOres[wallDrop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[wallDrop.id][qx][qy] == null) wallOres[wallDrop.id][qx][qy] = new IntSeq(false, 16);
if(wallOres[wallDrop.id][qx][qy].addUnique(pos)){
int old = allWallOres.increment(wallDrop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
}
}
if(drop != null && ores != null && ores[drop.id] != null&& ores[drop.id][qx][qy] != null && ores[drop.id][qx][qy].removeValue(pos)){ //floor
int old = allOres.increment(drop, -1);
if(old == 1) updatePresentOres();
}
}
@@ -194,6 +255,11 @@ public class BlockIndexer{
return allOres.get(item) > 0;
}
/** @return whether this item is present on this map as a wall ore. */
public boolean hasWallOre(Item item){
return allWallOres.get(item) > 0;
}
/** Returns all damaged tiles by team. */
public Seq<Building> getDamaged(Team team){
if(damagedTiles[team.id] == null){
@@ -348,7 +414,7 @@ public class BlockIndexer{
breturnArray.size = 0;
}
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
public Building findEnemyTile(Team team, float x, float y, float range, BuildingPriorityf priority, Boolf<Building> pred){
Building target = null;
float targetDist = 0;
@@ -362,10 +428,10 @@ public class BlockIndexer{
//if a block has the same priority, the closer one should be targeted
float dist = candidate.dst(x, y) - candidate.hitSize() / 2f;
if(target == null ||
//if its closer and is at least equal priority
(dist < targetDist && candidate.block.priority >= target.block.priority) ||
// block has higher priority (so range doesnt matter)
(candidate.block.priority > target.block.priority)){
//if it is closer and is at least equal priority
(dist < targetDist && priority.priority(candidate) >= priority.priority(target)) ||
// block has higher priority (so range doesn't matter)
priority.priority(candidate) > priority.priority(target)){
target = candidate;
targetDist = dist;
}
@@ -374,6 +440,10 @@ public class BlockIndexer{
return target;
}
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findEnemyTile(team, x, y, range, UnitSorts.buildingDefault, pred);
}
public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findTile(team, x, y, range, pred, false);
}
@@ -432,11 +502,43 @@ public class BlockIndexer{
return null;
}
/** Find the closest ore wall relative to a position. */
public Tile findClosestWallOre(float xp, float yp, Item item){
//(stolen from foo's client :))))
if(wallOres[item.id] != null){
float minDst = 0f;
Tile closest = null;
for(int qx = 0; qx < quadWidth; qx++){
for(int qy = 0; qy < quadHeight; qy++){
var arr = wallOres[item.id][qx][qy];
if(arr != null && arr.size > 0){
Tile tile = world.tile(arr.first());
if(tile.block() != Blocks.air){
float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy());
if(closest == null || dst < minDst){
closest = tile;
minDst = dst;
}
}
}
}
}
return closest;
}
return null;
}
/** Find the closest ore block relative to a position. */
public Tile findClosestOre(Unit unit, Item item){
return findClosestOre(unit.x, unit.y, item);
}
/** Find the closest ore block relative to a position. */
public Tile findClosestWallOre(Unit unit, Item item){
return findClosestWallOre(unit.x, unit.y, item);
}
private void process(Tile tile){
var team = tile.team();
//only process entity changes with centered tiles
@@ -1082,21 +1082,6 @@ public class ControlPathfinder implements Runnable{
return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2);
}
@Deprecated
public int nextTargetId(){
return 0;
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
return getPathPosition(unit, pathId, destination, out, null);
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound);
}
public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound);
}
+180 -84
View File
@@ -5,6 +5,7 @@ import arc.func.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.TaskQueue;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.core.*;
@@ -16,11 +17,14 @@ import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*;
import java.util.*;
import static mindustry.Vars.*;
import static mindustry.world.meta.BlockFlag.*;
public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(8);
private static final int neverRefresh = Integer.MAX_VALUE;
private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS;
@@ -30,44 +34,66 @@ public class Pathfinder implements Runnable{
static final int impassable = -1;
public static final int
fieldCore = 0;
fieldCore = 0,
maxFields = 10;
public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with(
EnemyCoreField::new
EnemyCoreField::new
);
public static final int
costGround = 0,
costLegs = 1,
costNaval = 2;
costGround = 0,
costLegs = 1,
costNaval = 2,
costNeoplasm = 3,
costNone = 4,
costHover = 5,
maxCosts = 8;
public static final Seq<PathCost> costTypes = Seq.with(
//ground
(team, tile) ->
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 6000 : 0) +
(PathTile.damages(tile) ? 30 : 0),
//ground
(team, tile) ->
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 6000 : 0) +
(PathTile.damages(tile) ? 30 : 0),
//legs
(team, tile) ->
PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown
(PathTile.solid(tile) ? 5 : 0),
//legs
(team, tile) ->
PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown
(PathTile.solid(tile) ? 5 : 0),
//water
(team, tile) ->
(!PathTile.liquid(tile) ? 6000 : 1) +
PathTile.health(tile) * 5 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? 0 : 1) +
(PathTile.damages(tile) ? 35 : 0)
//water
(team, tile) ->
(!PathTile.liquid(tile) ? 6000 : 1) +
PathTile.health(tile) * 5 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? 0 : 1) +
(PathTile.damages(tile) ? 35 : 0),
//neoplasm veins
(team, tile) ->
(PathTile.deep(tile) || (PathTile.team(tile) == 0 && PathTile.solid(tile))) ? impassable : 1 +
(PathTile.health(tile) * 3) +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 2 : 0),
//none (flat cost)
(team, tile) -> 1,
//hover
(team, tile) ->
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0)
);
/** tile data, see PathTileStruct - kept as a separate array for threading reasons */
int[] tiles = new int[0];
int[] tiles = {};
/** maps team, cost, type to flow field*/
Flowfield[][][] cache;
@@ -79,6 +105,8 @@ public class Pathfinder implements Runnable{
@Nullable Thread thread;
IntSeq tmpArray = new IntSeq();
boolean needsRefresh;
public Pathfinder(){
clearCache();
@@ -93,6 +121,7 @@ public class Pathfinder implements Runnable{
mainList = new Seq<>();
clearCache();
for(int i = 0; i < tiles.length; i++){
Tile tile = world.tiles.geti(i);
tiles[i] = packTile(tile);
@@ -146,10 +175,36 @@ public class Pathfinder implements Runnable{
}
}
});
Events.run(Trigger.afterGameUpdate, () -> {
//only refresh periodically (every 2 frames) to batch flowfield updates
//TODO: is it worth switching to a timestamp based system instead that updates every X milliseconds?
if(needsRefresh && Core.graphics.getFrameId() % 2 == 0){
needsRefresh = false;
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
//paths with a refresh rate should not be updated by tiles changing
if(path != null && path.needsRefresh()){
synchronized(path.targets){
//TODO: this is super slow and forces a refresh for every tile changed!
path.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
}
});
}
private void clearCache(){
cache = new Flowfield[256][5][5];
cache = new Flowfield[256][maxCosts][maxFields];
}
/** Packs a tile into its internal representation. */
@@ -178,19 +233,19 @@ public class Pathfinder implements Runnable{
int tid = tile.getTeamID();
return PathTile.get(
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid,
tile.floor().isLiquid,
tile.legSolid(),
nearLiquid,
nearGround,
nearSolid,
nearLegSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f,
allDeep,
tile.block().teamPassable
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid,
tile.floor().isLiquid,
tile.legSolid(),
nearLiquid,
nearGround,
nearSolid,
nearLegSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f,
allDeep,
tile.block().teamPassable
);
}
@@ -216,6 +271,7 @@ public class Pathfinder implements Runnable{
thread = null;
}
queue.clear();
needsRefresh = false;
}
/** Update a tile in the internal pathfinding grid.
@@ -230,23 +286,10 @@ public class Pathfinder implements Runnable{
}
});
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
if(path != null){
synchronized(path.targets){
path.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
controlPath.updateTile(tile);
//queue a refresh sometime in the future
needsRefresh = true;
}
/** Thread implementation. */
@@ -300,48 +343,55 @@ public class Pathfinder implements Runnable{
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path){
return getTargetTile(tile, path, true);
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals){
if(tile == null) return null;
//uninitialized flowfields are not applicable
if(!path.initialized){
//also ignore paths with no targets, there is no destination
if(!path.initialized || path.targets.size == 0){
return tile;
}
//if refresh rate is positive, queue a refresh
if(path.refreshRate > 0 && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate){
if(path.refreshRate > 0 && path.refreshRate != neverRefresh && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate && path.frontier.size == 0){
path.lastUpdateTime = Time.millis();
tmpArray.clear();
path.getPositions(tmpArray);
synchronized(path.targets){
//make sure the position actually changed
if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){
path.updateTargetPositions();
path.updateTargetPositions();
//queue an update
queue.post(() -> updateTargets(path));
}
//queue an update
queue.post(() -> updateTargets(path));
}
}
//use complete weights if possible; these contain a complete flow field that is not being updated
int[] values = path.hasComplete ? path.completeWeights : path.weights;
int apos = tile.array();
int res = path.resolution;
int ww = path.width;
int apos = tile.x/res + tile.y/res * ww;
int value = values[apos];
var points = diagonals ? Geometry.d8 : Geometry.d4;
Tile current = null;
int tl = 0;
for(Point2 point : Geometry.d8){
int dx = tile.x + point.x, dy = tile.y + point.y;
for(Point2 point : points){
int dx = tile.x + point.x * res, dy = tile.y + point.y * res;
Tile other = world.tile(dx, dy);
if(other == null) continue;
int packed = world.packArray(dx, dy);
int packed = dx/res + dy/res * ww;
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
!(point.x != 0 && point.y != 0 && (!path.passable(world.packArray(tile.x + point.x, tile.y)) || !path.passable(world.packArray(tile.x, tile.y + point.y))))){ //diagonal corner trap
!(point.x != 0 && point.y != 0 && (!path.passable(((tile.x + point.x)/res + tile.y/res*ww)) || !path.passable((tile.x/res + (tile.y + point.y)/res*ww))))){ //diagonal corner trap
current = other;
tl = values[packed];
}
@@ -358,13 +408,21 @@ public class Pathfinder implements Runnable{
//increment search, but do not clear the frontier
path.search++;
//search overflow; reset everything.
if(path.search >= Short.MAX_VALUE){
Arrays.fill(path.searches, (short)0);
path.search = 1;
}
synchronized(path.targets){
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
if(pos >= path.weights.length) continue;
path.weights[pos] = 0;
path.searches[pos] = path.search;
path.searches[pos] = (short)path.search;
path.frontier.addFirst(pos);
}
}
@@ -383,7 +441,7 @@ public class Pathfinder implements Runnable{
*/
private void registerPath(Flowfield path){
path.lastUpdateTime = Time.millis();
path.setup(tiles.length);
path.setup();
threadList.add(path);
@@ -391,9 +449,7 @@ public class Pathfinder implements Runnable{
Core.app.post(() -> mainList.add(path));
//fill with impassables by default
for(int i = 0; i < tiles.length; i++){
path.weights[i] = impassable;
}
Arrays.fill(path.weights, impassable);
//add targets
for(int i = 0; i < path.targets.size; i++){
@@ -409,6 +465,7 @@ public class Pathfinder implements Runnable{
long start = Time.nanos();
int counter = 0;
int w = path.width, h = path.height;
while(path.frontier.size > 0){
int tile = path.frontier.removeLast();
@@ -416,7 +473,7 @@ public class Pathfinder implements Runnable{
int cost = path.weights[tile];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){
if(path.frontier.size >= w * h){
path.frontier.clear();
return;
}
@@ -424,12 +481,12 @@ public class Pathfinder implements Runnable{
if(cost != impassable){
for(Point2 point : Geometry.d4){
int dx = (tile % wwidth) + point.x, dy = (tile / wwidth) + point.y;
int dx = (tile % w) + point.x, dy = (tile / w) + point.y;
if(dx < 0 || dy < 0 || dx >= wwidth || dy >= wheight) continue;
if(dx < 0 || dy < 0 || dx >= w || dy >= h) continue;
int newPos = tile + point.x + point.y * wwidth;
int otherCost = path.cost.getCost(path.team.id, tiles[newPos]);
int newPos = dx + dy * w;
int otherCost = path.getCost(tiles, newPos);
if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){
path.frontier.addFirst(newPos);
@@ -516,7 +573,7 @@ public class Pathfinder implements Runnable{
* Concrete subclasses must specify a way to fetch costs and destinations.
*/
public static abstract class Flowfield{
/** Refresh rate in milliseconds. Return any number <= 0 to disable. */
/** Refresh rate in milliseconds. <= 0 to disable. */
protected int refreshRate;
/** Team this path is for. Set before using. */
protected Team team = Team.derelict;
@@ -530,12 +587,16 @@ public class Pathfinder implements Runnable{
/** costs of getting to a specific tile */
public int[] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
public int[] searches;
public short[] searches;
/** the last "complete" weights of this tilemap. */
public int[] completeWeights;
/** Scaling factor. For example, resolution = 2 means tiles are twice as large. */
public final int resolution;
public final int width, height;
/** search frontier, these are Pos objects */
IntQueue frontier = new IntQueue();
final IntQueue frontier = new IntQueue();
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
final IntSeq targets = new IntSeq();
/** current search ID */
@@ -545,14 +606,44 @@ public class Pathfinder implements Runnable{
/** whether this flow field is ready to be used */
boolean initialized;
void setup(int length){
public Flowfield(){
this(1);
}
public Flowfield(int resolution){
this.resolution = resolution;
this.width = Mathf.ceil((float)wwidth / resolution);
this.height = Mathf.ceil((float)wheight / resolution);
}
void setup(){
int length = width * height;
this.weights = new int[length];
this.searches = new int[length];
this.searches = new short[length];
this.completeWeights = new int[length];
this.frontier.ensureCapacity((length) / 4);
this.initialized = true;
}
public int getCost(int[] tiles, int pos){
return cost.getCost(team.id, tiles[pos]);
}
public boolean hasTargets(){
return targets.size > 0;
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from, boolean diagonals){
return pathfinder.getTargetTile(from, this, diagonals);
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from){
return pathfinder.getTargetTile(from, this);
}
public boolean hasCompleteWeights(){
return hasComplete && completeWeights != null;
}
@@ -562,6 +653,11 @@ public class Pathfinder implements Runnable{
getPositions(targets);
}
/** @return whether this flow field should be refreshed after the current block update */
public boolean needsRefresh(){
return refreshRate == 0;
}
protected boolean passable(int pos){
int amount = cost.getCost(team.id, pathfinder.tiles[pos]);
//edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable
+6 -7
View File
@@ -17,7 +17,6 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.turrets.BaseTurret.*;
import mindustry.world.blocks.defense.turrets.*;
import mindustry.world.blocks.storage.*;
@@ -35,7 +34,7 @@ public class RtsAI{
//in order of priority??
static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill};
static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>();
static final boolean debug = OS.hasProp("mindustry.debug");
static final boolean debug = OS.hasProp("mindustry.debug") && false;
final Interval timer = new Interval(10);
final TeamData data;
@@ -210,12 +209,12 @@ public class RtsAI{
//defendTarget = aggressor;
defendPos = new Vec2(aggressor.x, aggressor.y);
defendTarget = aggressor;
}else if(false){ //TODO currently ignored, no use defending against nothing
//}else if(false){ //TODO currently ignored, no use defending against nothing
//should it even go there if there's no aggressor found?
Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
if(closest != null){
defendPos = new Vec2(closest.worldx(), closest.worldy());
}
// Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
// if(closest != null){
// defendPos = new Vec2(closest.worldx(), closest.worldy());
// }
}else{
float mindst = Float.MAX_VALUE;
Building build = null;
-7
View File
@@ -3,7 +3,6 @@ package mindustry.ai;
import arc.*;
import arc.func.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.types.*;
import mindustry.ctype.*;
@@ -13,10 +12,6 @@ import mindustry.input.*;
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
public class UnitCommand extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitCommand> all = new Seq<>();
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand;
/** Name of UI icon (from Icon class). */
@@ -39,8 +34,6 @@ public class UnitCommand extends MappableContent{
this.icon = icon;
this.controller = controller == null ? u -> null : controller;
all.add(this);
}
public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){
+2 -9
View File
@@ -2,30 +2,23 @@ package mindustry.ai;
import arc.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.input.*;
public class UnitStance extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitStance> all = new Seq<>();
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
/** Name of UI icon (from Icon class). */
public final String icon;
public String icon;
/** Key to press for this stance. */
public @Nullable Binding keybind = null;
public @Nullable Binding keybind;
public UnitStance(String name, String icon, Binding keybind){
super(name);
this.icon = icon;
this.keybind = keybind;
all.add(this);
}
public String localized(){
+7 -13
View File
@@ -82,9 +82,7 @@ public class WaveSpawner{
eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> {
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);
spawnUnit(group, spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
}
});
}else{
@@ -95,9 +93,7 @@ public class WaveSpawner{
for(int i = 0; i < spawnedf; i++){
Tmp.v1.rnd(spread);
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
spawnEffect(unit);
spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
}
});
}
@@ -106,6 +102,11 @@ public class WaveSpawner{
Time.run(121f, () -> spawning = false);
}
public void spawnUnit(SpawnGroup group, float x, float y){
group.createUnit(group.team == null ? state.rules.waveTeam : group.team, x, y,
Angles.angle(x, y, world.width()/2f * tilesize, world.height()/2f * tilesize), state.wave - 1, this::spawnEffect);
}
public void doShockwave(float x, float y){
Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius);
Damage.damage(state.rules.waveTeam, x, y, state.rules.dropZoneRadius, 99999999f, true);
@@ -217,15 +218,8 @@ public class WaveSpawner{
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit){
spawnEffect(unit, unit.angleTo(world.width()/2f * tilesize, world.height()/2f * tilesize));
}
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit, float rotation){
unit.rotation = rotation;
unit.apply(StatusEffects.unmoving, 30f);
unit.apply(StatusEffects.invincible, 60f);
unit.add();
unit.unloaded();
Events.fire(new UnitSpawnEvent(unit));
+6 -5
View File
@@ -118,9 +118,10 @@ public class BuilderAI extends AIController{
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
if(valid){
float range = Math.min(unit.type.buildRange - 20f, 100f);
//move toward the plan
moveTo(req.tile(), unit.type.buildRange - 20f, 20f);
moving = !unit.within(req.tile(), unit.type.buildRange - 10f);
moveTo(req.tile(), range - 10f, 20f);
moving = !unit.within(req.tile(), range);
}else{
//discard invalid plan
unit.plans.removeFirst();
@@ -179,12 +180,12 @@ public class BuilderAI extends AIController{
BlockPlan block = blocks.first();
//check if it's already been placed
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block().id == block.block){
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){
blocks.removeFirst();
}else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid
}else if(Build.validPlace(block.block, unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid
lastPlan = block;
//add build plan
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, content.block(block.block), block.config));
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, block.block, block.config));
//shift build plan to tail so next unit builds something else
blocks.addLast(blocks.removeFirst());
}else{
+6 -4
View File
@@ -74,18 +74,20 @@ public class CargoAI extends AIController{
//deposit items when it's possible
if(max > 0){
noDestTimer = 0f;
Call.transferItemTo(unit, unit.item(), max, unit.x, unit.y, unloadTarget);
//try the next target later
//reset wait timer if we can't fill the unload point.
if(!unit.hasItem()){
targetIndex ++;
noDestTimer = 0f;
}
}else if((noDestTimer += dropSpacing) >= emptyWaitTime){
}
//keep the target for at most emptyWaitTime, then we try change if other need.
if((noDestTimer += dropSpacing) >= emptyWaitTime){
//oh no, it's out of space - wait for a while, and if nothing changes, try the next destination
//next targeting attempt will try the next destination point
targetIndex = findDropTarget(unit.item(), targetIndex, unloadTarget) + 1;
noDestTimer = 0f;
//nothing found at all, clear item
if(unloadTarget == null){
+20 -23
View File
@@ -82,6 +82,15 @@ public class CommandAI extends AIController{
commandTarget(target, false);
}
//pursue the target for patrol, keeping the current position
if(stance == UnitStance.patrol && target != null && attackTarget == null){
//commanding a target overwrites targetPos, so add it to the queue
if(targetPos != null){
commandQueue.add(targetPos.cpy());
}
commandTarget(target, false);
}
//remove invalid targets
if(commandQueue.any()){
commandQueue.removeAll(e -> e instanceof Healthc h && !h.isValid());
@@ -123,6 +132,14 @@ public class CommandAI extends AIController{
}
}
@Override
public Teamc findMainTarget(float x, float y, float range, boolean air, boolean ground){
if(!unit.type.autoFindTarget && !(targetPos == null || nearAttackTarget(unit.x, unit.y, unit.range()))){
return null;
}
return super.findMainTarget(x, y, range, air, ground);
}
public void defaultBehavior(){
if(!net.client() && unit instanceof Payloadc pay){
@@ -164,28 +181,8 @@ public class CommandAI extends AIController{
}
}
//acquiring naval targets isn't supported yet, so use the fallback dumb AI
if(unit.team.isAI() && unit.team.rules().rtsAi && unit.type.naval){
if(fallback == null) fallback = new GroundAI();
if(fallback.unit() != unit) fallback.unit(unit);
fallback.updateUnit();
return;
}
updateVisuals();
//only autotarget if the unit supports it
if((targetPos == null || nearAttackTarget(unit.x, unit.y, unit.range())) || unit.type.autoFindTarget){
updateTargeting();
}else if(attackTarget == null){
//if the unit does not have an attack target, is currently moving, and does not have autotargeting, stop attacking stuff
target = null;
for(var mount : unit.mounts){
if(mount.weapon.controllable){
mount.target = null;
}
}
}
updateTargeting();
if(attackTarget != null && invalid(attackTarget)){
attackTarget = null;
@@ -204,7 +201,7 @@ public class CommandAI extends AIController{
}
targetPos.set(attackTarget);
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs && stance != UnitStance.ram){
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && stance != UnitStance.ram){
Tile best = build.findClosestEdge(unit, Tile::solid);
if(best != null){
targetPos.set(best);
@@ -473,7 +470,7 @@ public class CommandAI extends AIController{
@Override
public boolean retarget(){
//retarget faster when there is an explicit target
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20);
return timer.get(timerTarget, attackTarget != null ? 10f : 20f);
}
public boolean hasCommand(){
+1 -1
View File
@@ -28,7 +28,7 @@ public class DefenderAI extends AIController{
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
//Sort by max health and closer target.
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.type.playerControllable,
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.playerControllable(),
(u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f);
if(result != null) return result;
-1
View File
@@ -15,7 +15,6 @@ public class HugAI extends AIController{
@Override
public void updateMovement(){
Building core = unit.closestEnemyCore();
if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){
+5 -8
View File
@@ -1,6 +1,5 @@
package mindustry.ai.types;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.type.*;
@@ -17,7 +16,7 @@ public class MinerAI extends AIController{
public void updateMovement(){
Building core = unit.closestCore();
if(!(unit.canMine()) || core == null) return;
if(!unit.canMine() || core == null) return;
if(!unit.validMine(unit.mineTile)){
unit.mineTile(null);
@@ -40,19 +39,17 @@ public class MinerAI extends AIController{
mining = false;
}else{
if(timer.get(timerTarget3, 60) && targetItem != null){
ore = indexer.findClosestOre(unit, targetItem);
ore = null;
if(unit.type.mineFloor) ore = indexer.findClosestOre(unit, targetItem);
if(ore == null && unit.type.mineWalls) ore = indexer.findClosestWallOre(unit, targetItem);
}
if(ore != null){
moveTo(ore, unit.type.mineRange / 2f, 20f);
if(ore.block() == Blocks.air && unit.within(ore, unit.type.mineRange)){
if(unit.within(ore, unit.type.mineRange) && unit.validMine(ore)){
unit.mineTile = ore;
}
if(ore.block() != Blocks.air){
mining = false;
}
}
}
}else{
+6 -1
View File
@@ -10,6 +10,11 @@ import mindustry.gen.*;
public class MissileAI extends AIController{
public @Nullable Unit shooter;
@Override
protected void resetTimers(){
timer.reset(timerTarget, 5f);
}
@Override
public void updateMovement(){
unloadPayloads();
@@ -33,7 +38,7 @@ public class MissileAI extends AIController{
@Override
public Teamc target(float x, float y, float range, boolean air, boolean ground){
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground) && !u.isMissile(), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
}
@Override
+5 -4
View File
@@ -11,10 +11,11 @@ import mindustry.gen.*;
public class PhysicsProcess implements AsyncProcess{
public static final int
layers = 3,
layers = 4,
layerGround = 0,
layerLegs = 1,
layerFlying = 2;
layerFlying = 2,
layerUnderwater = 3;
private PhysicsWorld physics;
private Seq<PhysicRef> refs = new Seq<>(false);
@@ -153,15 +154,15 @@ public class PhysicsProcess implements AsyncProcess{
for(int i = 0; i < bodySize; i++){
PhysicsBody body = bodyItems[i];
if(body.layer < 0) continue;
body.collided = false;
trees[body.layer].insert(body);
}
for(int i = 0; i < bodySize; i++){
PhysicsBody body = bodyItems[i];
//for clients, the only body that collides is the local one; all other physics simulations are handled by the server.
if(!body.local) continue;
if(!body.local || body.layer < 0) continue;
body.hitbox(rect);
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -10,7 +10,7 @@ import mindustry.entities.bullet.*;
public class Bullets{
public static BulletType
placeholder, spaceLiquid, damageLightning, damageLightningGround, fireball;
placeholder, spaceLiquid, damageLightning, damageLightningGround, damageLightningAir, fireball;
public static void load(){
@@ -37,6 +37,10 @@ public class Bullets{
damageLightningGround = damageLightning.copy();
damageLightningGround.collidesAir = false;
damageLightningAir = damageLightning.copy();
damageLightningAir.collidesGround = false;
damageLightningAir.collidesTiles = false;
fireball = new FireBulletType(1f, 4){{
hittable = false;
}};
@@ -160,7 +160,9 @@ public class ErekirTechTree{
});
node(beamTower, Seq.with(new OnSector(peaks)), () -> {
node(beamLink, Seq.with(new OnSector(crossroads)), () -> {
});
});
+94 -1
View File
@@ -499,6 +499,14 @@ public class Fx{
}
}),
smokeAoeCloud = new Effect(60f * 3f, 250f, e -> {
color(e.color, 0.65f);
randLenVectors(e.id, 80, 90f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 6f * Mathf.clamp(e.fin() / 0.1f) * Mathf.clamp(e.fout() / 0.1f));
});
}),
missileTrailSmoke = new Effect(180f, 300f, b -> {
float intensity = 2f;
@@ -519,6 +527,26 @@ public class Fx{
}
}).layer(Layer.bullet - 1f),
missileTrailSmokeSmall = new Effect(120f, 200f, b -> {
float intensity = 1.3f;
color(b.color, 0.7f);
for(int i = 0; i < 3; i++){
rand.setSeed(b.id*2 + i);
float lenScl = rand.random(0.5f, 1f);
int fi = i;
b.scaled(b.lifetime * lenScl, e -> {
randLenVectors(e.id + fi - 1, e.fin(Interp.pow10Out), (int)(2.9f * intensity), 13f * intensity, (x, y, in, out) -> {
float fout = e.fout(Interp.pow5Out) * rand.random(0.5f, 1f);
float rad = fout * ((2f + intensity) * 2.35f);
Fill.circle(e.x + x, e.y + y, rad);
Drawf.light(e.x + x, e.y + y, rad * 2.5f, b.color, 0.5f);
});
});
}
}).layer(Layer.bullet - 1f),
neoplasmSplat = new Effect(400f, 300f, b -> {
float intensity = 3f;
@@ -557,6 +585,24 @@ public class Fx{
}
}),
scatheExplosionSmall = new Effect(40f, 160f, e -> {
color(e.color);
stroke(e.fout() * 4f);
float circleRad = 6f + e.finpow() * 40f;
Lines.circle(e.x, e.y, circleRad);
rand.setSeed(e.id);
for(int i = 0; i < 16; i++){
float angle = rand.random(360f);
float lenRand = rand.random(0.5f, 1f);
Tmp.v1.trns(angle, circleRad);
for(int s : Mathf.signs){
Drawf.tri(e.x + Tmp.v1.x, e.y + Tmp.v1.y, e.foutpow() * 30f, e.fout() * 25f * lenRand + 6f, angle + 90f + s * 90f);
}
}
}),
scatheLight = new Effect(60f, 160f, e -> {
float circleRad = 6f + e.finpow() * 60f;
@@ -564,6 +610,13 @@ public class Fx{
Fill.circle(e.x, e.y, circleRad);
}).layer(Layer.bullet + 2f),
scatheLightSmall = new Effect(60f, 160f, e -> {
float circleRad = 6f + e.finpow() * 40f;
color(e.color, e.foutpow());
Fill.circle(e.x, e.y, circleRad);
}).layer(Layer.bullet + 2f),
scatheSlash = new Effect(40f, 160f, e -> {
Draw.color(e.color);
for(int s : Mathf.signs){
@@ -1121,7 +1174,7 @@ public class Fx{
artilleryTrail = new Effect(50, e -> {
color(e.color);
Fill.circle(e.x, e.y, e.rotation * e.fout());
}),
}).layer(Layer.bullet - 0.01f),
incendTrail = new Effect(50, e -> {
color(Pal.lightOrange);
@@ -1421,6 +1474,12 @@ public class Fx{
Lines.circle(e.x, e.y, e.fin() * (e.rotation + 50f));
}),
podLandShockwave = new Effect(12f, 80f, e -> {
color(Pal.accent);
stroke(e.fout() * 2f + 0.2f);
Lines.circle(e.x, e.y, e.fin() * 26f);
}),
explosion = new Effect(30, e -> {
e.scaled(7, i -> {
stroke(3f * i.fout());
@@ -1571,6 +1630,15 @@ public class Fx{
});
}),
steamCoolSmoke = new Effect(35f, e -> {
color(Pal.water, Color.lightGray, e.fin(Interp.pow2Out));
alpha(e.fout(Interp.pow3Out));
randLenVectors(e.id, 4, e.finpow() * 7f, e.rotation, 30f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, Math.max(e.fout(), Math.min(1f, e.fin() * 8f)) * 2.8f);
});
}),
smokePuff = new Effect(30, e -> {
color(e.color);
@@ -1737,6 +1805,18 @@ public class Fx{
}
}),
shootSmokeMissileColor = new Effect(130f, 300f, e -> {
color(e.color);
alpha(0.5f);
rand.setSeed(e.id);
for(int i = 0; i < 35; i++){
v.trns(e.rotation + 180f + rand.range(21f), rand.random(e.finpow() * 90f)).add(rand.range(3f), rand.range(3f));
e.scaled(e.lifetime * rand.random(0.2f, 1f), b -> {
Fill.circle(e.x + v.x, e.y + v.y, b.fout() * 9f + 0.3f);
});
}
}),
regenParticle = new Effect(100f, e -> {
color(Pal.regen);
@@ -2399,6 +2479,12 @@ public class Fx{
});
}),
launchAccelerator = new Effect(22, e -> {
color(Pal.accent);
stroke(e.fout() * 2f);
Lines.circle(e.x, e.y, 4f + e.finpow() * 160f);
}),
launch = new Effect(28, e -> {
color(Pal.command);
stroke(e.fout() * 2f);
@@ -2503,6 +2589,13 @@ public class Fx{
Fill.circle(e.x + Tmp.v1.x, e.y + Tmp.v1.y, 8f * rand.random(0.6f, 1f) * e.fout(0.2f));
}).layer(Layer.groundUnit + 1f),
podLandDust = new Effect(70f, e -> {
color(e.color, e.fout(0.1f));
rand.setSeed(e.id);
Tmp.v1.trns(e.rotation, e.finpow() * 35f * rand.random(0.2f, 1f));
Fill.circle(e.x + Tmp.v1.x, e.y + Tmp.v1.y, 5f * rand.random(0.6f, 1f) * e.fout(0.2f));
}).layer(Layer.groundUnit + 1f),
unitShieldBreak = new Effect(35, e -> {
if(!(e.data instanceof Unit unit)) return;
+10 -6
View File
@@ -87,12 +87,13 @@ public class Planets{
};
campaignRuleDefaults.fog = true;
campaignRuleDefaults.showSpawns = true;
campaignRuleDefaults.rtsAI = true;
unlockedOnLand.add(Blocks.coreBastion);
}};
//TODO names
gier = makeAsteroid("gier", erekir, Blocks.ferricStoneWall, Blocks.carbonWall, 0.4f, 7, 1f, gen -> {
gier = makeAsteroid("gier", erekir, Blocks.ferricStoneWall, Blocks.carbonWall, -5, 0.4f, 7, 1f, gen -> {
gen.min = 25;
gen.max = 35;
gen.carbonChance = 0.6f;
@@ -100,7 +101,7 @@ public class Planets{
gen.berylChance = 0.1f;
});
notva = makeAsteroid("notva", sun, Blocks.ferricStoneWall, Blocks.beryllicStoneWall, 0.55f, 9, 1.3f, gen -> {
notva = makeAsteroid("notva", sun, Blocks.ferricStoneWall, Blocks.beryllicStoneWall, -4, 0.55f, 9, 1.3f, gen -> {
gen.berylChance = 0.8f;
gen.iceChance = 0f;
gen.carbonChance = 0.01f;
@@ -134,6 +135,7 @@ public class Planets{
launchCapacityMultiplier = 0.5f;
sectorSeed = 2;
allowWaves = true;
allowLegacyLaunchPads = true;
allowWaveSimulation = true;
allowSectorInvasion = true;
allowLaunchSchematics = true;
@@ -147,16 +149,18 @@ public class Planets{
r.showSpawns = false;
r.coreDestroyClear = true;
};
showRtsAIRule = true;
iconColor = Color.valueOf("7d4dff");
atmosphereColor = Color.valueOf("3c1b8f");
atmosphereRadIn = 0.02f;
atmosphereRadOut = 0.3f;
startSector = 15;
alwaysUnlocked = true;
allowSelfSectorLaunch = true;
landCloudColor = Pal.spore.cpy().a(0.5f);
}};
verilus = makeAsteroid("verlius", sun, Blocks.stoneWall, Blocks.iceWall, 0.5f, 12, 2f, gen -> {
verilus = makeAsteroid("verlius", sun, Blocks.stoneWall, Blocks.iceWall, -1, 0.5f, 12, 2f, gen -> {
gen.berylChance = 0f;
gen.iceChance = 0.6f;
gen.carbonChance = 0.1f;
@@ -164,7 +168,7 @@ public class Planets{
});
}
private static Planet makeAsteroid(String name, Planet parent, Block base, Block tint, float tintThresh, int pieces, float scale, Cons<AsteroidGenerator> cgen){
private static Planet makeAsteroid(String name, Planet parent, Block base, Block tint, int seed, float tintThresh, int pieces, float scale, Cons<AsteroidGenerator> cgen){
return new Planet(name, parent, 0.12f){{
hasAtmosphere = false;
updateLighting = false;
@@ -187,13 +191,13 @@ public class Planets{
Rand rand = new Rand(id + 2);
meshes.add(new NoiseMesh(
this, 0, 2, radius, 2, 0.55f, 0.45f, 14f,
this, seed, 2, radius, 2, 0.55f, 0.45f, 14f,
color, tinted, 3, 0.6f, 0.38f, tintThresh
));
for(int j = 0; j < pieces; j++){
meshes.add(new MatMesh(
new NoiseMesh(this, j + 1, 1, 0.022f + rand.random(0.039f) * scale, 2, 0.6f, 0.38f, 20f,
new NoiseMesh(this, seed + j + 1, 1, 0.022f + rand.random(0.039f) * scale, 2, 0.6f, 0.38f, 20f,
color, tinted, 3, 0.6f, 0.38f, tintThresh),
new Mat3D().setToTranslation(Tmp.v31.setToRandomDirection(rand).setLength(rand.random(0.44f, 1.4f) * scale)))
);
@@ -106,7 +106,7 @@ public class SectorPresets{
difficulty = 8;
}};
frontier = new SectorPreset("frontier", serpulo, 203){{
frontier = new SectorPreset("frontier", serpulo, 50){{
difficulty = 4;
}};
+75 -61
View File
@@ -19,11 +19,12 @@ public class SerpuloTechTree{
node(junction, () -> {
node(router, () -> {
node(launchPad, Seq.with(new SectorComplete(extractionOutpost)), () -> {
//no longer necessary to beat the campaign
//node(interplanetaryAccelerator, Seq.with(new SectorComplete(planetaryTerminal)), () -> {
node(advancedLaunchPad, Seq.with(new SectorComplete(extractionOutpost)), () -> {
node(landingPad, () -> {
node(interplanetaryAccelerator, Seq.with(new SectorComplete(planetaryTerminal)), () -> {
//});
});
});
});
node(distributor);
@@ -146,7 +147,7 @@ public class SerpuloTechTree{
});
});
node(kiln, Seq.with(new SectorComplete(craters)), () -> {
node(kiln, Seq.with(new OnSector(craters)), () -> {
node(pulverizer, () -> {
node(incinerator, () -> {
node(melter, () -> {
@@ -435,8 +436,8 @@ public class SerpuloTechTree{
});
});
node(additiveReconstructor, Seq.with(new SectorComplete(biomassFacility)), () -> {
node(multiplicativeReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
node(additiveReconstructor, Seq.with(new SectorComplete(craters)), () -> {
node(multiplicativeReconstructor, Seq.with(new SectorComplete(frontier)), () -> {
node(exponentialReconstructor, () -> {
node(tetrativeReconstructor, () -> {
@@ -457,14 +458,58 @@ public class SerpuloTechTree{
new Research(mender),
new Research(combustionGenerator)
), () -> {
node(frontier, Seq.with(
node(fungalPass, Seq.with(
new SectorComplete(craters),
new Research(groundFactory),
new Research(airFactory),
new Research(thermalGenerator),
new Research(dagger),
new Research(mono)
new Research(dagger)
), () -> {
node(frontier, Seq.with(
new SectorComplete(biomassFacility),
new SectorComplete(fungalPass),
new Research(groundFactory),
new Research(airFactory),
new Research(additiveReconstructor),
new Research(mace),
new Research(mono)
), () -> {
node(overgrowth, Seq.with(
new SectorComplete(frontier),
new SectorComplete(windsweptIslands),
new Research(multiplicativeReconstructor),
new Research(fortress),
new Research(ripple),
new Research(salvo),
new Research(cultivator),
new Research(sporePress)
), () -> {
node(mycelialBastion, Seq.with(
new Research(atrax),
new Research(spiroct),
new Research(arkyid),
new Research(multiplicativeReconstructor),
new Research(exponentialReconstructor)
), () -> {
});
node(atolls, Seq.with(
new SectorComplete(windsweptIslands),
new Research(multiplicativeReconstructor),
new Research(mega)
), () -> {
});
});
});
node(taintedWoods, Seq.with(
new SectorComplete(biomassFacility),
new SectorComplete(fungalPass),
new Research(Items.sporePod),
new Research(wave)
), () -> {
});
});
node(ruinousShores, Seq.with(
@@ -482,6 +527,8 @@ public class SerpuloTechTree{
), () -> {
node(seaPort, Seq.with(
new SectorComplete(biomassFacility),
new SectorComplete(frontier),
new SectorComplete(fungalPass),
new Research(navalFactory),
new Research(risso),
new Research(retusa),
@@ -523,7 +570,7 @@ public class SerpuloTechTree{
new Research(sei),
new Research(omura),
new Research(spectre),
new Research(launchPad),
new Research(advancedLaunchPad),
new Research(massDriver),
new Research(impactReactor),
new Research(additiveReconstructor),
@@ -564,6 +611,7 @@ public class SerpuloTechTree{
), () -> {
node(extractionOutpost, Seq.with(
new SectorComplete(windsweptIslands),
new SectorComplete(fungalPass),
new SectorComplete(facility32m),
new Research(groundFactory),
new Research(nova),
@@ -591,6 +639,8 @@ public class SerpuloTechTree{
node(saltFlats, Seq.with(
new SectorComplete(windsweptIslands),
new SectorComplete(fungalPass),
new SectorComplete(frontier),
new Research(groundFactory),
new Research(additiveReconstructor),
new Research(airFactory),
@@ -636,33 +686,6 @@ public class SerpuloTechTree{
});
});
});
node(overgrowth, Seq.with(
new SectorComplete(craters),
new SectorComplete(fungalPass),
new Research(cultivator),
new Research(sporePress),
new Research(additiveReconstructor),
new Research(UnitTypes.mace),
new Research(UnitTypes.flare)
), () -> {
node(mycelialBastion, Seq.with(
new Research(atrax),
new Research(spiroct),
new Research(multiplicativeReconstructor),
new Research(exponentialReconstructor)
), () -> {
});
node(atolls, Seq.with(
new SectorComplete(windsweptIslands),
new Research(multiplicativeReconstructor),
new Research(mega)
), () -> {
});
});
});
node(biomassFacility, Seq.with(
@@ -672,34 +695,23 @@ public class SerpuloTechTree{
new Research(scatter),
new Research(graphitePress)
), () -> {
node(taintedWoods, Seq.with(
new SectorComplete(biomassFacility),
new Research(Items.sporePod),
new Research(wave)
), () -> {
});
node(stainedMountains, Seq.with(
new SectorComplete(biomassFacility),
new Research(pneumaticDrill),
new Research(siliconSmelter)
), () -> {
node(fungalPass, Seq.with(
new SectorComplete(stainedMountains),
new Research(groundFactory),
new Research(door)
//TODO bad order
node(infestedCanyons, Seq.with(
new SectorComplete(fungalPass),
new SectorComplete(frontier),
new Research(navalFactory),
new Research(risso),
new Research(minke),
new Research(additiveReconstructor)
), () -> {
node(infestedCanyons, Seq.with(
new SectorComplete(fungalPass),
new Research(navalFactory),
new Research(risso),
new Research(minke),
new Research(additiveReconstructor)
), () -> {
});
//TODO difficulty jump!
node(nuclearComplex, Seq.with(
new SectorComplete(fungalPass),
new Research(thermalGenerator),
@@ -710,6 +722,8 @@ public class SerpuloTechTree{
});
});
});
});
});
+1
View File
@@ -99,6 +99,7 @@ public class TechTree{
public TechNode(@Nullable TechNode parent, UnlockableContent content, ItemStack[] requirements){
if(parent != null){
parent.children.add(this);
planet = parent.planet;
researchCostMultipliers = parent.researchCostMultipliers;
}else if(researchCostMultipliers == null){
researchCostMultipliers = new ObjectFloatMap<>();
+14 -5
View File
@@ -98,6 +98,7 @@ public class UnitTypes{
//region ground attack
dagger = new UnitType("dagger"){{
researchCostMultiplier = 0.5f;
speed = 0.5f;
hitSize = 8f;
health = 150;
@@ -606,11 +607,12 @@ public class UnitTypes{
//region ground legs
crawler = new UnitType("crawler"){{
researchCostMultiplier = 0.5f;
aiController = SuicideAI::new;
speed = 1f;
hitSize = 8f;
health = 200;
health = 150;
mechSideSway = 0.25f;
range = 40f;
ammoType = new ItemAmmoType(Items.coal);
@@ -629,12 +631,12 @@ public class UnitTypes{
collides = false;
hitSound = Sounds.explosion;
rangeOverride = 30f;
rangeOverride = 25f;
hitEffect = Fx.pulverize;
speed = 0f;
splashDamageRadius = 55f;
splashDamageRadius = 44f;
instantDisappear = true;
splashDamage = 90f;
splashDamage = 80f;
killShooter = true;
hittable = false;
collidesAir = true;
@@ -977,6 +979,7 @@ public class UnitTypes{
//region air attack
flare = new UnitType("flare"){{
researchCostMultiplier = 0.5f;
speed = 2.7f;
accel = 0.08f;
drag = 0.04f;
@@ -1017,6 +1020,7 @@ public class UnitTypes{
engineOffset = 7.8f;
range = 140f;
faceTarget = false;
autoDropBombs = true;
armor = 3f;
itemCapacity = 0;
targetFlags = new BlockFlag[]{BlockFlag.factory, null};
@@ -1390,6 +1394,7 @@ public class UnitTypes{
drag = 0.017f;
lowAltitude = false;
flying = true;
autoDropBombs = true;
circleTarget = true;
engineOffset = 13f;
engineSize = 7f;
@@ -1908,6 +1913,7 @@ public class UnitTypes{
mixColorTo = Color.white;
hitSound = Sounds.plasmaboom;
underwater = true;
ejectEffect = Fx.none;
hitSize = 22f;
@@ -3280,6 +3286,7 @@ public class UnitTypes{
}});
weapons.add(new Weapon(){{
shootSound = Sounds.none;
shootCone = 360f;
mirror = false;
reload = 1f;
@@ -3301,7 +3308,7 @@ public class UnitTypes{
tecta = new ErekirUnitType("tecta"){{
drag = 0.1f;
speed = 0.6f;
hitSize = 23f;
hitSize = 30f;
health = 7300;
armor = 5f;
@@ -3576,6 +3583,7 @@ public class UnitTypes{
elude = new ErekirUnitType("elude"){{
hovering = true;
canDrown = false;
shadowElevation = 0.1f;
drag = 0.07f;
@@ -3862,6 +3870,7 @@ public class UnitTypes{
loopSoundVolume = 0.1f;
weapons.add(new Weapon(){{
shootSound = Sounds.none;
shootCone = 360f;
mirror = false;
reload = 1f;
+7 -4
View File
@@ -199,9 +199,9 @@ public class Control implements ApplicationListener, Loadable{
float coreDelay = 0f;
if(!settings.getBool("skipcoreanimation") && !state.rules.pvp){
coreDelay = core.landDuration();
coreDelay = core.launchDuration();
//delay player respawn so animation can play.
player.deathTimer = Player.deathDelay - core.landDuration();
player.deathTimer = Player.deathDelay - core.launchDuration();
//TODO this sounds pretty bad due to conflict
if(settings.getInt("musicvol") > 0){
//TODO what to do if another core with different music is already playing?
@@ -215,6 +215,10 @@ public class Control implements ApplicationListener, Loadable{
}
if(state.isCampaign()){
if(state.rules.sector.info.importRateCache != null){
state.rules.sector.info.refreshImportRates(state.rules.sector.planet);
}
//don't run when hosting, that doesn't really work.
if(state.rules.sector.planet.prebuildBase){
toBePlaced.clear();
@@ -396,7 +400,6 @@ public class Control implements ApplicationListener, Loadable{
control.saves.resetSave();
}
//for planet launches, mostly
if(sector.preset != null){
sector.preset.quietUnlock();
}
@@ -458,7 +461,7 @@ public class Control implements ApplicationListener, Loadable{
for(var plan : state.rules.waveTeam.data().plans){
Tile tile = world.tile(plan.x, plan.y);
if(tile != null){
tile.setBlock(content.block(plan.block), state.rules.waveTeam, plan.rotation);
tile.setBlock(plan.block, state.rules.waveTeam, plan.rotation);
if(plan.config != null && tile.build != null){
tile.build.configureAny(plan.config);
}
+16 -6
View File
@@ -210,8 +210,7 @@ public class Logic implements ApplicationListener{
var bounds = tile.block().bounds(tile.x, tile.y, Tmp.r1);
while(it.hasNext()){
BlockPlan b = it.next();
Block block = content.block(b.block);
if(bounds.overlaps(block.bounds(b.x, b.y, Tmp.r2))){
if(bounds.overlaps(b.block.bounds(b.x, b.y, Tmp.r2))){
b.removed = true;
it.remove();
}
@@ -293,7 +292,7 @@ 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()) ||
(state.rules.attackMode && state.rules.waveTeam.cores().isEmpty())){
(state.rules.attackMode && !state.rules.waveTeam.isAlive())){
if(state.rules.sector.preset != null && state.rules.sector.preset.attackAfterWaves && !state.rules.attackMode){
//activate attack mode to destroy cores after waves are done.
@@ -310,11 +309,11 @@ public class Logic implements ApplicationListener{
Events.fire(new GameOverEvent(state.rules.waveTeam));
}else if(state.rules.attackMode){
//count # of teams alive
int countAlive = state.teams.getActive().count(t -> t.hasCore() && t.team != Team.derelict);
int countAlive = state.teams.getActive().count(t -> t.isAlive() && t.team != Team.derelict);
if((countAlive <= 1 || (!state.rules.pvp && state.rules.defaultTeam.core() == null)) && !state.gameOver){
//find team that won
TeamData left = state.teams.getActive().find(t -> t.hasCore() && t.team != Team.derelict);
TeamData left = state.teams.getActive().find(t -> t.isAlive() && t.team != Team.derelict);
Events.fire(new GameOverEvent(left == null ? Team.derelict : left.team));
state.gameOver = true;
}
@@ -406,12 +405,17 @@ public class Logic implements ApplicationListener{
@Override
public void dispose(){
//save the settings before quitting
netServer.admins.forceSave();
if(netServer != null){
netServer.admins.forceSave();
}
Core.settings.manualSave();
}
@Override
public void update(){
PerfCounter.frame.end();
PerfCounter.frame.begin();
Events.fire(Trigger.update);
universe.updateGlobal();
@@ -428,6 +432,8 @@ public class Logic implements ApplicationListener{
}
if(!state.isPaused()){
Events.fire(Trigger.beforeGameUpdate);
float delta = Core.graphics.getDeltaTime();
state.tick += Float.isNaN(delta) || Float.isInfinite(delta) ? 0f : delta * 60f;
state.updateId ++;
@@ -486,7 +492,11 @@ public class Logic implements ApplicationListener{
state.envAttrs.add(state.rules.attributes);
Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity));
PerfCounter.entityUpdate.begin();
Groups.update();
PerfCounter.entityUpdate.end();
Events.fire(Trigger.afterGameUpdate);
}
if(runStateCheck){
+20 -3
View File
@@ -34,6 +34,7 @@ import java.util.zip.*;
import static mindustry.Vars.*;
public class NetClient implements ApplicationListener{
private static final long entitySnapshotTimeout = 1000 * 20;
private static final float dataTimeout = 60 * 30;
/** ticks between syncs, e.g. 5 means 60/5 = 12 syncs/sec*/
private static final float playerSyncTime = 4;
@@ -50,6 +51,8 @@ public class NetClient implements ApplicationListener{
private boolean quietReset = false;
/** Counter for data timeout. */
private float timeoutTime = 0f;
/** Timestamp for last UDP state snapshot received. */
private long lastSnapshotTimestamp;
/** Last sent client snapshot ID. */
private int lastSent;
@@ -273,7 +276,7 @@ public class NetClient implements ApplicationListener{
Events.fire(new PlayerChatEvent(player, message));
//log commands before they are handled
if(message.startsWith(netServer.clientCommands.getPrefix())){
if(message.startsWith(netServer.clientCommands.getPrefix()) && Config.logCommands.bool()){
//log with brackets
Log.info("<&fi@: @&fr>", "&lk" + player.plainName(), "&lw" + message);
}
@@ -319,7 +322,7 @@ public class NetClient implements ApplicationListener{
ui.join.connect(ip, port);
}
@Remote(targets = Loc.client)
@Remote(targets = Loc.client, priority = PacketPriority.high)
public static void ping(Player player, long time){
Call.pingResponse(player.con, time);
}
@@ -478,6 +481,7 @@ public class NetClient implements ApplicationListener{
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
public static void entitySnapshot(short amount, byte[] data){
try{
netClient.lastSnapshotTimestamp = Time.millis();
netClient.byteStream.setBytes(data);
DataInputStream input = netClient.dataStream;
@@ -575,7 +579,18 @@ public class NetClient implements ApplicationListener{
if(!net.client()) return;
if(state.isGame()){
if(!connecting) sync();
if(!connecting){
sync();
//timeout if UDP snapshot packets are not received for a while
if(lastSnapshotTimestamp > 0 && Time.timeSinceMillis(lastSnapshotTimestamp) > entitySnapshotTimeout){
Log.err("Timed out after not received UDP snapshots.");
quiet = true;
ui.showErrorMessage("@disconnect.snapshottimeout");
net.disconnect();
lastSnapshotTimestamp = 0;
}
}
}else if(!connecting){
net.disconnect();
}else{ //...must be connecting
@@ -612,6 +627,7 @@ public class NetClient implements ApplicationListener{
Core.app.post(Call::connectConfirm);
Time.runTask(40f, platform::updateRPC);
Core.app.post(ui.loadfrag::hide);
lastSnapshotTimestamp = Time.millis();
}
private void reset(){
@@ -622,6 +638,7 @@ public class NetClient implements ApplicationListener{
quietReset = false;
quiet = false;
lastSent = 0;
lastSnapshotTimestamp = 0;
Groups.clear();
ui.chatfrag.clearMessages();
+5 -4
View File
@@ -632,7 +632,7 @@ public class NetServer implements ApplicationListener{
return Float.isInfinite(f) || Float.isNaN(f);
}
@Remote(targets = Loc.client, unreliable = true)
@Remote(targets = Loc.client, unreliable = true, priority = PacketPriority.high)
public static void clientSnapshot(
Player player,
int snapshotID,
@@ -815,7 +815,7 @@ public class NetServer implements ApplicationListener{
}
case trace -> {
PlayerInfo stats = netServer.admins.getInfo(other.uuid());
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.con.modclient, other.con.mobile, stats.timesJoined, stats.timesKicked, stats.ips.toArray(String.class), stats.names.toArray(String.class));
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.locale, other.con.modclient, other.con.mobile, stats.timesJoined, stats.timesKicked, stats.ips.toArray(String.class), stats.names.toArray(String.class));
if(player.con != null){
Call.traceInfo(player.con, other, info);
}else{
@@ -830,7 +830,7 @@ public class NetServer implements ApplicationListener{
}
}
@Remote(targets = Loc.client)
@Remote(targets = Loc.client, priority = PacketPriority.high)
public static void connectConfirm(Player player){
if(player.con.kicked) return;
@@ -990,6 +990,7 @@ public class NetServer implements ApplicationListener{
//write all entities now
dataStream.writeInt(entity.id()); //write id
dataStream.writeByte(entity.classId() & 0xFF); //write type ID
entity.beforeWrite();
entity.writeSync(Writes.get(dataStream)); //write entity
sent++;
@@ -1082,7 +1083,7 @@ public class NetServer implements ApplicationListener{
try{
writeEntitySnapshot(player);
}catch(IOException e){
e.printStackTrace();
Log.err(e);
}
});
+53
View File
@@ -0,0 +1,53 @@
package mindustry.core;
import arc.math.*;
import arc.util.*;
/** Simple per-frame time counter. */
public enum PerfCounter{
frame,
update,
entityUpdate,
render;
public static final PerfCounter[] all = values();
static final int meanWindow = 30;
static final int refreshTimeMillis = 500;
private long valueRefreshTime;
private float refreshValue;
private long beginTime;
private boolean began = false;
private WindowedMean mean = new WindowedMean(meanWindow);
public void begin(){
began = true;
beginTime = Time.nanos();
}
public void end(){
if(!began) return;
began = false;
mean.add(Time.timeSinceNanos(beginTime));
}
/** Value with a periodic refresh interval applied, to prevent jittery UI. */
public float valueMs(){
if(Time.timeSinceMillis(valueRefreshTime) > refreshTimeMillis){
refreshValue = rawValueMs();
valueRefreshTime = Time.millis();
}
return refreshValue;
}
/** Raw value without a refresh interval. This will be unstable. */
public float rawValueMs(){
return mean.rawMean() / Time.nanosPerMilli;
}
public long rawValueNs(){
return (long)mean.rawMean();
}
}
+60 -55
View File
@@ -20,15 +20,14 @@ import mindustry.graphics.*;
import mindustry.graphics.g3d.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.blocks.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class Renderer implements ApplicationListener{
/** These are global variables, for headless access. Cached. */
public static float laserOpacity = 0.5f, bridgeOpacity = 0.75f;
public static float laserOpacity = 0.5f, unitLaserOpacity = 1f, bridgeOpacity = 0.75f;
public final BlockRenderer blocks = new BlockRenderer();
public final FogRenderer fog = new FogRenderer();
@@ -43,16 +42,18 @@ public class Renderer implements ApplicationListener{
public FrameBuffer effectBuffer = new FrameBuffer();
public boolean animateShields, drawWeather = true, drawStatus, enableEffects, drawDisplays = true, drawLight = true, pixelate = false;
public float weatherAlpha;
/** minZoom = zooming out, maxZoom = zooming in */
/** minZoom = zooming out, maxZoom = zooming in, used by cutscenes */
public float minZoom = 1.5f, maxZoom = 6f;
/** minZoom = zooming out, maxZoom = zooming in, used by actual gameplay zoom and regulated by settings **/
public float minZoomInGame = 0.5f, maxZoomInGame = 6f;
public Seq<EnvRenderer> envRenderers = new Seq<>();
public ObjectMap<String, Runnable> customBackgrounds = new ObjectMap<>();
public TextureRegion[] bubbles = new TextureRegion[16], splashes = new TextureRegion[12];
public TextureRegion[][] fluidFrames;
//currently landing core, null if there are no cores or it has finished landing.
private @Nullable CoreBuild landCore;
private @Nullable CoreBlock launchCoreType;
private @Nullable LaunchAnimator launchAnimator;
private Color clearColor = new Color(0f, 0f, 0f, 1f);
private float
//target camera scale that is lerp-ed to
@@ -61,8 +62,6 @@ public class Renderer implements ApplicationListener{
camerascale = targetscale,
//starts at coreLandDuration, ends at 0. if positive, core is landing.
landTime,
//timer for core landing particles
landPTimer,
//intensity for screen shake
shakeIntensity,
//reduction rate of screen shake
@@ -72,6 +71,7 @@ public class Renderer implements ApplicationListener{
//for landTime > 0: if true, core is currently *launching*, otherwise landing.
private boolean launching;
private Vec2 camShakeOffset = new Vec2();
private int glErrors;
public Renderer(){
camera = new Camera();
@@ -151,6 +151,7 @@ public class Renderer implements ApplicationListener{
@Override
public void update(){
PerfCounter.render.begin();
Color.white.set(1f, 1f, 1f, 1f);
float baseTarget = targetscale;
@@ -162,31 +163,34 @@ public class Renderer implements ApplicationListener{
float dest = Mathf.clamp(Mathf.round(baseTarget, 0.5f), minScale(), maxScale());
camerascale = Mathf.lerpDelta(camerascale, dest, 0.1f);
if(Mathf.equal(camerascale, dest, 0.001f)) camerascale = dest;
unitLaserOpacity = settings.getInt("unitlaseropacity") / 100f;
laserOpacity = settings.getInt("lasersopacity") / 100f;
bridgeOpacity = settings.getInt("bridgeopacity") / 100f;
animateShields = settings.getBool("animatedshields");
drawStatus = settings.getBool("blockstatus");
enableEffects = settings.getBool("effects");
drawDisplays = !settings.getBool("hidedisplays");
maxZoomInGame = settings.getFloat("maxzoomingamemultiplier", 1) * maxZoom;
minZoomInGame = minZoom / settings.getFloat("minzoomingamemultiplier", 1);
drawLight = settings.getBool("drawlight", true);
pixelate = settings.getBool("pixelate");
//don't bother drawing landing animation if core is null
if(landCore == null) landTime = 0f;
if(launchAnimator == null) landTime = 0f;
if(landTime > 0){
if(!state.isPaused()) landCore.updateLaunching();
if(!state.isPaused()) launchAnimator.updateLaunch();
weatherAlpha = 0f;
camerascale = landCore.zoomLaunching();
camerascale = launchAnimator.zoomLaunch();
if(!state.isPaused()) landTime -= Time.delta;
}else{
weatherAlpha = Mathf.lerpDelta(weatherAlpha, 1f, 0.08f);
}
if(landCore != null && landTime <= 0f){
landCore.endLaunch();
landCore = null;
if(launchAnimator != null && landTime <= 0f){
launchAnimator.endLaunch();
launchAnimator = null;
}
camera.width = graphics.getWidth() / camerascale;
@@ -218,6 +222,26 @@ public class Renderer implements ApplicationListener{
camera.position.sub(camShakeOffset);
}
//glGetError can be expensive, so only check it periodically
if(glErrors < maxGlErrors && graphics.getFrameId() % 10 == 0){
int error = Gl.getError();
if(error != Gl.noError){
String message = switch(error){
case Gl.invalidValue -> "invalid value";
case Gl.invalidOperation -> "invalid operation";
case Gl.invalidFramebufferOperation -> "invalid framebuffer operation";
case Gl.invalidEnum -> "invalid enum";
case Gl.outOfMemory -> "out of memory";
default -> "unknown error " + (error);
};
Log.err("[GL] Error: @", message);
glErrors ++;
}
}
PerfCounter.render.end();
}
public void updateAllDarkness(){
@@ -311,7 +335,6 @@ public class Renderer implements ApplicationListener{
Draw.draw(Layer.block - 0.09f, () -> {
blocks.floor.beginDraw();
blocks.floor.drawLayer(CacheLayer.walls);
blocks.floor.endDraw();
});
Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader);
@@ -378,9 +401,14 @@ public class Renderer implements ApplicationListener{
Draw.draw(Layer.overlayUI, overlays::drawTop);
if(state.rules.fog) Draw.draw(Layer.fogOfWar, fog::drawFog);
Draw.draw(Layer.space, () -> {
if(landCore == null || landTime <= 0f) return;
landCore.drawLanding(launching && launchCoreType != null ? launchCoreType : (CoreBlock)landCore.block);
if(launchAnimator == null || landTime <= 0f) return;
launchAnimator.drawLaunch();
});
if(launchAnimator != null){
Draw.z(Layer.space);
launchAnimator.drawLaunchGlobalZ();
Draw.reset();
}
Events.fire(Trigger.drawOver);
blocks.drawBlocks();
@@ -484,11 +512,13 @@ public class Renderer implements ApplicationListener{
}
public float minScale(){
return Scl.scl(minZoom);
if(control.input.logicCutscene) return Scl.scl(minZoom);
return Scl.scl(minZoomInGame);
}
public float maxScale(){
return Mathf.round(Scl.scl(maxZoom));
if(control.input.logicCutscene) return Mathf.round(Scl.scl(maxZoom));
return Mathf.round(Scl.scl(maxZoomInGame));
}
public float getScale(){
@@ -504,66 +534,41 @@ public class Renderer implements ApplicationListener{
return launching;
}
public CoreBlock getLaunchCoreType(){
return launchCoreType;
}
public float getLandTime(){
return landTime;
}
public float getLandTimeIn(){
if(landCore == null) return 0f;
float fin = landTime / landCore.landDuration();
if(launchAnimator == null) return 0f;
float fin = landTime / launchAnimator.launchDuration();
if(!launching) fin = 1f - fin;
return fin;
}
public float getLandPTimer(){
return landPTimer;
}
public void setLandPTimer(float landPTimer){
this.landPTimer = landPTimer;
}
@Deprecated
public void showLanding(){
var core = player.bestCore();
if(core != null) showLanding(core);
}
public void showLanding(CoreBuild landCore){
this.landCore = landCore;
public void showLanding(LaunchAnimator landCore){
this.launchAnimator = landCore;
launching = false;
landTime = landCore.landDuration();
landTime = landCore.launchDuration();
landCore.beginLaunch(null);
camerascale = landCore.zoomLaunching();
landCore.beginLaunch(false);
camerascale = landCore.zoomLaunch();
}
@Deprecated
public void showLaunch(CoreBlock coreType){
var core = player.team().core();
if(core != null) showLaunch(core, coreType);
}
public void showLaunch(CoreBuild landCore, CoreBlock coreType){
public void showLaunch(LaunchAnimator landCore){
control.input.config.hideConfig();
control.input.planConfig.hide();
control.input.inv.hide();
this.landCore = landCore;
this.launchAnimator = landCore;
launching = true;
landTime = landCore.landDuration();
launchCoreType = coreType;
landTime = landCore.launchDuration();
Music music = landCore.launchMusic();
music.stop();
music.play();
music.setVolume(settings.getInt("musicvol") / 100f);
landCore.beginLaunch(coreType);
landCore.beginLaunch(true);
}
public void takeMapScreenshot(){
+36
View File
@@ -33,6 +33,8 @@ import static arc.scene.actions.Actions.*;
import static mindustry.Vars.*;
public class UI implements ApplicationListener, Loadable{
private static final StringBuilder buffer = new StringBuilder();
public static String billions, millions, thousands;
public static PixmapPacker packer;
@@ -682,6 +684,40 @@ public class UI implements ApplicationListener, Loadable{
followUpMenus.remove(menuId).hide();
}
/**
* Finds all :name: in a string and replaces them with the icon, if such exists.
* Based on TextFormatter::simpleFormat
*/
public static String formatIcons(String s){
if(!s.contains(":")) return s;
buffer.setLength(0);
boolean changed = false;
boolean checkIcon = false;
String[] tokens = s.split(":");
for(String token : tokens){
if(checkIcon){
if(Iconc.codes.containsKey(token)){
buffer.append((char)Iconc.codes.get(token));
changed = true;
checkIcon = false;
}else if(Fonts.hasUnicodeStr(token)){
buffer.append(Fonts.getUnicodeStr(token));
changed = true;
checkIcon = false;
}else{
buffer.append(":").append(token);
}
}else{
buffer.append(token);
checkIcon = true;
}
}
return changed ? buffer.toString() : s;
}
/** Formats time with hours:minutes:seconds. */
public static String formatTime(float ticks){
int seconds = (int)(ticks / 60);
+16 -6
View File
@@ -44,6 +44,11 @@ public class World{
Events.on(WorldLoadEvent.class, e -> {
tileChanges = -1;
//make each building check if it can update in the given map area
for(var build : Groups.build){
build.checkAllowUpdate();
}
});
}
@@ -123,7 +128,7 @@ public class World{
Tile tile = tiles.get(x, y);
if(tile == null) return null;
if(tile.build != null){
return tile.build.tile();
return tile.build.tile;
}
return tile;
}
@@ -458,11 +463,16 @@ public class World{
return 0;
}
public void checkMapArea(){
for(var build : Groups.build){
//reset map-area-based disabled blocks.
if(!build.enabled && build.block.autoResetEnabled){
build.enabled = true;
public void checkMapArea(int x, int y, int w, int h){
for(var team : state.teams.present){
for(var build : team.buildings){
//reset map-area-based disabled blocks that were not in the previous map area
if(!build.enabled && build.block.autoResetEnabled && !Rect.contains(x, y, w, h, build.tile.x, build.tile.y)){
build.enabled = true;
}
//if the map area contracts, disable the block
build.checkAllowUpdate();
}
}
}
@@ -32,8 +32,10 @@ public abstract class UnlockableContent extends MappableContent{
public boolean alwaysUnlocked = false;
/** Whether to show the description in the research dialog preview. */
public boolean inlineDescription = true;
/** Whether details of blocks are hidden in custom games if they haven't been unlocked in campaign mode. */
/** Whether details are hidden in custom games if this hasn't been unlocked in campaign mode. */
public boolean hideDetails = true;
/** Whether this is hidden from the Core Database. */
public boolean hideDatabase = false;
/** If false, all icon generation is disabled for this content; createIcons is not called. */
public boolean generateIcons = true;
/** How big the content appears in certain selection menus */
@@ -0,0 +1,210 @@
package mindustry.editor;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import static mindustry.Vars.*;
public class BannedContentDialog<T extends UnlockableContent> extends BaseDialog{
private final ContentType type;
private Table selectedTable;
private Table deselectedTable;
private ObjectSet<T> contentSet;
private final Boolf<T> pred;
private String contentSearch;
private Category selectedCategory;
private Seq<T> filteredContent;
public BannedContentDialog(String title, ContentType type, Boolf<T> pred){
super(title);
this.type = type;
this.pred = pred;
contentSearch = "";
selectedTable = new Table();
deselectedTable = new Table();
addCloseButton();
shown(this::build);
resized(this::build);
}
public void show(ObjectSet<T> contentSet){
this.contentSet = contentSet;
show();
}
public void build(){
cont.clear();
var cell = cont.table(t -> {
t.table(s -> {
s.label(() -> "@search").padRight(10);
var field = s.field(contentSearch, value -> {
contentSearch = value.trim().replaceAll(" +", " ").toLowerCase();
rebuildTables();
}).get();
s.button(Icon.cancel, Styles.emptyi, () -> {
contentSearch = "";
field.setText("");
rebuildTables();
}).padLeft(10f).size(35f);
});
if(type == ContentType.block){
t.row();
t.table(c -> {
c.marginTop(8f);
c.defaults().marginRight(4f);
for(Category category : Category.values()){
c.button(ui.getIcon(category.name()), Styles.squareTogglei, () -> {
if(selectedCategory == category){
selectedCategory = null;
}else{
selectedCategory = category;
}
rebuildTables();
}).size(45f).update(i -> i.setChecked(selectedCategory == category)).padLeft(4f);
}
c.add("").padRight(4f);
}).center();
}
});
cont.row();
if(!Core.graphics.isPortrait()) cell.colspan(2);
filteredContent = content.<T>getBy(type).select(pred);
if(!contentSearch.isEmpty()) filteredContent.removeAll(content -> !content.localizedName.toLowerCase().contains(contentSearch.toLowerCase()));
cont.table(table -> {
if(type == ContentType.block){
table.add("@bannedblocks").color(Color.valueOf("f25555")).padBottom(-1).top().row();
}else{
table.add("@bannedunits").color(Color.valueOf("f25555")).padBottom(-1).top().row();
}
table.image().color(Color.valueOf("f25555")).height(3f).padBottom(5f).fillX().expandX().top().row();
table.pane(table2 -> selectedTable = table2).fill().expand().row();
table.button("@addall", Icon.add, () -> {
contentSet.addAll(filteredContent);
rebuildTables();
}).disabled(button -> contentSet.toSeq().containsAll(filteredContent)).padTop(10f).bottom().fillX();
}).fill().expandY().uniform();
if(Core.graphics.isPortrait()) cont.row();
var cell2 = cont.table(table -> {
if(type == ContentType.block){
table.add("@unbannedblocks").color(Pal.accent).padBottom(-1).top().row();
}else{
table.add("@unbannedunits").color(Pal.accent).padBottom(-1).top().row();
}
table.image().color(Pal.accent).height(3f).padBottom(5f).fillX().top().row();
table.pane(table2 -> deselectedTable = table2).fill().expand().row();
table.button("@addall", Icon.add, () -> {
contentSet.removeAll(filteredContent);
rebuildTables();
}).disabled(button -> {
Seq<T> array = content.getBy(type);
array = array.copy();
array.removeAll(contentSet.toSeq());
return array.containsAll(filteredContent);
}).padTop(10f).bottom().fillX();
}).fill().expandY().uniform();
if(Core.graphics.isPortrait()){
cell2.padTop(10f);
}else{
cell2.padLeft(10f);
}
rebuildTables();
}
private void rebuildTables(){
filteredContent.clear();
filteredContent = content.getBy(type);
filteredContent = filteredContent.select(pred);
if(!contentSearch.isEmpty()) filteredContent.removeAll(content -> !content.localizedName.toLowerCase().contains(contentSearch.toLowerCase()));
if(type == ContentType.block){
filteredContent.removeAll(content -> selectedCategory != null && ((Block)content).category != selectedCategory);
}
rebuildTable(selectedTable, true);
rebuildTable(deselectedTable, false);
}
private void rebuildTable(Table table, boolean isSelected){
table.clear();
int cols;
if(Core.graphics.isPortrait()){
cols = Math.max(4, (int)((Core.graphics.getWidth() / Scl.scl() - 100f) / 50f));
}else{
cols = Math.max(4, (int)((Core.graphics.getWidth() / Scl.scl() - 300f) / 50f / 2));
}
if((isSelected && contentSet.isEmpty()) || (!isSelected && contentSet.size == content.<T>getBy(type).count(pred))){
table.add("@empty").width(50f * cols).padBottom(5f).get().setAlignment(Align.center);
}else{
Seq<T> array;
if(!isSelected){
array = content.getBy(type);
array = array.copy();
array.removeAll(contentSet.toSeq());
}else{
array = contentSet.toSeq();
}
array.sort();
array.removeAll(content -> !filteredContent.contains(content));
if(array.isEmpty()){
table.add("@empty").width(50f * cols).padBottom(5f).get().setAlignment(Align.center);
return;
}
int i = 0;
boolean requiresPad = true;
for(T content : array){
TextureRegion region = content.uiIcon;
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNonei);
button.getStyle().imageUp = new TextureRegionDrawable(region);
button.resizeImage(8 * 4f);
if(isSelected) button.clicked(() -> {
contentSet.remove(content);
rebuildTables();
});
else button.clicked(() -> {
contentSet.add(content);
rebuildTables();
});
table.add(button).size(50f).tooltip(content.localizedName);
if(++i % cols == 0){
table.row();
requiresPad = false;
}
}
if(requiresPad){
table.add("").padRight(50f * (cols - i));
}
}
}
}
+6 -4
View File
@@ -35,7 +35,9 @@ public class EditorTile extends Tile{
if(floor == type && overlayID() == 0) return;
if(overlayID() != 0) op(OpType.overlay, overlayID());
if(floor != type) op(OpType.floor, floor.id);
super.setFloor(type);
this.floor = type;
this.overlay = (Floor)Blocks.air;
}
@Override
@@ -141,14 +143,14 @@ public class EditorTile extends Tile{
if(block == null) block = Blocks.air;
if(floor == null) floor = (Floor)Blocks.air;
Block block = block();
if(block.hasBuilding()){
build = entityprov.get().init(this, team, false, rotation);
if(block.hasItems) build.items = new ItemModule();
if(block.hasLiquids) build.liquids(new LiquidModule());
if(block.hasPower) build.power(new PowerModule());
if(block.hasLiquids) build.liquids = new LiquidModule();
if(block.hasPower) build.power = new PowerModule();
}
}
@@ -757,7 +757,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
if(!Core.atlas.isFound(region) || !block.inEditor
|| block.buildVisibility == BuildVisibility.debugOnly
|| (!searchText.isEmpty() && !block.localizedName.toLowerCase().contains(searchText.toLowerCase()))
|| (!searchText.isEmpty() && !block.localizedName.toLowerCase().contains(searchText.trim().replaceAll(" +", " ").toLowerCase()))
) continue;
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
@@ -136,6 +136,7 @@ public class MapObjectivesDialog extends BaseDialog{
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)),
Styles.squarei,
() -> showTeamSelect(set)
).fill().pad(4f)).growX().fillY();
});
@@ -529,6 +530,8 @@ public class MapObjectivesDialog extends BaseDialog{
public void rebuildObjectives(Seq<MapObjective> objectives){
canvas.clearObjectives();
objectives.each(MapObjective::validate);
if(
objectives.any() && (
// If the objectives were previously programmatically made...
@@ -592,9 +595,23 @@ public class MapObjectivesDialog extends BaseDialog{
}
public static void showTeamSelect(Cons<Team> cons){
showTeamSelect(false, cons);
}
public static void showTeamSelect(boolean allowNull, Cons<Team> cons){
BaseDialog dialog = new BaseDialog("");
dialog.cont.defaults().size(40f).pad(4f);
if(allowNull){
dialog.cont.button(Icon.cancel, Styles.emptyi, () -> {
cons.get(null);
dialog.hide();
}).tooltip("@none");
}
for(var team : Team.baseTeams){
dialog.cont.image(Tex.whiteui).size(iconMed).color(team.color).pad(4)
dialog.cont.image(Tex.whiteui).color(team.color)
.with(i -> i.addListener(new HandCursorListener()))
.tooltip(team.localized()).get().clicked(() -> {
cons.get(team);
@@ -19,7 +19,7 @@ import mindustry.world.blocks.logic.LogicBlock.*;
import static mindustry.Vars.*;
public class MapProcessorsDialog extends BaseDialog{
private IconSelectDialog iconSelect = new IconSelectDialog();
private IconSelectDialog iconSelect = new IconSelectDialog(true);
private TextField search;
private Seq<Building> processors = new Seq<>();
private Table list;
@@ -47,7 +47,6 @@ public class WaveInfoDialog extends BaseDialog{
});
hidden(() -> state.rules.spawns = groups);
onResize(this::setup);
addCloseButton();
buttons.button("@waves.edit", Icon.edit, () -> {
@@ -289,6 +288,13 @@ public class WaveInfoDialog extends BaseDialog{
buildGroups();
}).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f).row();
t.table(a -> {
a.add("@waves.team").padRight(8);
a.button(b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(group.team == null ? Color.clear : group.team.color)), Styles.squarei,
() -> MapObjectivesDialog.showTeamSelect(true, team -> group.team = team)).size(38f);
}).padTop(0).row();
t.table(a -> {
a.add("@waves.spawn").padRight(8);
+18 -9
View File
@@ -70,23 +70,27 @@ public class Damage{
}
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, Fx.dynamicExplosion);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, Effect explosionFx){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, Effect explosionFx, float baseShake){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx, baseShake);
}
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, Fx.dynamicExplosion);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosionFx){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, explosionFx, 3f);
}
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosionFx, float baseShake){
if(damage){
for(int i = 0; i < Mathf.clamp(power / 700, 0, 8); i++){
int length = 5 + Mathf.clamp((int)(Mathf.pow(power, 0.98f) / 500), 1, 18);
@@ -123,7 +127,7 @@ public class Damage{
Fx.bigShockwave.at(x, y);
}
float shake = Math.min(explosiveness / 4f + 3f, 9f);
float shake = Math.min(explosiveness / 4f + baseShake, 9f);
Effect.shake(shake, shake, x, y);
explosionFx.at(x, y, radius / 8f);
}
@@ -514,7 +518,7 @@ public class Damage{
}
//TODO better velocity displacement
float dst = vec.set(unit.x - x, unit.y - y).len();
unit.vel.add(vec.setLength((1f - dst / radius) * 2f / unit.mass()));
unit.vel.add(vec.setLength((radius > 0f ? 1f - dst / radius : 1f) * 2f / unit.mass()));
if(complete && damage >= 9999999f && unit.isPlayer()){
Events.fire(Trigger.exclusionDeath);
@@ -532,7 +536,7 @@ public class Damage{
if(!complete){
tileDamage(team, World.toTile(x), World.toTile(y), radius / tilesize, damage * (source == null ? 1f : source.type.buildingDamageMultiplier), source);
}else{
completeDamage(team, x, y, radius, damage);
completeDamage(team, x, y, radius, damage * (source == null ? 1f : source.type.buildingDamageMultiplier));
}
}
}
@@ -549,7 +553,12 @@ public class Damage{
//this needs to be compensated
if(in != null && in.team != team && in.block.size > 1 && in.health > damage){
//deal the damage of an entire side, to be equivalent with maximum 'standard' damage
in.damage(team, damage * Math.min((in.block.size), baseRadius * 0.4f));
float d = damage * Math.min((in.block.size), baseRadius * 0.4f);
if(source != null){
in.damage(source, team, d);
}else{
in.damage(team, d);
}
//no need to continue with the explosion
return;
}
@@ -631,7 +640,7 @@ public class Damage{
private static float calculateDamage(float dist, float radius, float damage){
float falloff = 0.4f;
float scaled = Mathf.lerp(1f - dist / radius, 1f, falloff);
float scaled = radius <= 0.00001f ? 1f : Mathf.lerp(1f - dist / radius, 1f, falloff);
return damage * scaled;
}
@@ -166,7 +166,7 @@ public class EntityCollisions{
}
}
private boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1,
public static boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1,
float x2, float y2, float w2, float h2, float vx2, float vy2, Vec2 out){
float px = vx1, py = vy1;
+2 -2
View File
@@ -17,7 +17,7 @@ import static mindustry.Vars.*;
public class Lightning{
private static final Rand random = new Rand();
private static final Rect rect = new Rect();
private static final Seq<Unitc> entities = new Seq<>();
private static final Seq<Unit> entities = new Seq<>();
private static final IntSet hit = new IntSet();
private static final int maxChain = 8;
private static final float hitRange = 30f;
@@ -74,7 +74,7 @@ public class Lightning{
});
}
Unitc furthest = Geometry.findFurthest(x, y, entities);
Unit furthest = Geometry.findFurthest(x, y, entities);
if(furthest != null){
hit.add(furthest.id());
+7 -1
View File
@@ -97,6 +97,12 @@ public class Puddles{
}
}
public static boolean hasLiquid(Tile tile, Liquid liquid){
if(tile == null) return false;
var p = get(tile);
return p != null && p.liquid == liquid && p.amount >= 0.5f;
}
public static void remove(Tile tile){
if(tile == null) return;
@@ -126,7 +132,7 @@ public class Puddles{
if(Mathf.chance(0.8f * amount)){
Fx.steam.at(x, y);
}
return -0.4f * amount;
return -0.7f * amount;
}
return dest.react(liquid, amount, tile, x, y);
}
@@ -4,7 +4,9 @@ package mindustry.entities;
public class TargetPriority{
public static final float
//nobody cares about walls
wall = -2f,
wall = -3f,
//anything that has underBullets gets this priority (it's probably still more important than a wall)
under = -2f,
//transport infrastructure isn't as important as factories
transport = -1f,
//most blocks
@@ -1,6 +1,7 @@
package mindustry.entities;
import arc.math.*;
import mindustry.content.*;
import mindustry.entities.Units.*;
import mindustry.gen.*;
@@ -11,4 +12,9 @@ public class UnitSorts{
farthest = (u, x, y) -> -u.dst2(x, y),
strongest = (u, x, y) -> -u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f,
weakest = (u, x, y) -> u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f;
public static BuildingPriorityf
buildingDefault = b -> b.block.priority,
buildingWater = b -> b.block.priority + (b.liquids != null && b.liquids.get(Liquids.water) > 5f ? 10f : 0f);
}
+8 -14
View File
@@ -95,7 +95,7 @@ public class Units{
public static int getCap(Team team){
//wave team has no cap
if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam) || state.rules.disableUnitCap){
if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam) || state.rules.disableUnitCap || team.ignoreUnitCap){
return Integer.MAX_VALUE;
}
return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap);
@@ -197,18 +197,8 @@ public class Units{
/** Returns the nearest enemy tile in a range. */
public static Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findEnemyTile(team, x, y, range, false, pred);
}
/** Returns the nearest enemy tile in a range. */
public static Building findEnemyTile(Team team, float x, float y, float range, boolean checkUnder, Boolf<Building> pred){
if(team == Team.derelict) return null;
if(checkUnder){
Building target = indexer.findEnemyTile(team, x, y, range, build -> !build.block.underBullets && pred.get(build));
if(target != null) return target;
}
return indexer.findEnemyTile(team, x, y, range, pred);
}
@@ -258,7 +248,7 @@ public class Units{
if(unit != null){
return unit;
}else{
return findEnemyTile(team, x, y, range, true, tilePred);
return findEnemyTile(team, x, y, range, tilePred);
}
}
@@ -270,7 +260,7 @@ public class Units{
if(unit != null){
return unit;
}else{
return findEnemyTile(team, x, y, range, true, tilePred);
return findEnemyTile(team, x, y, range, tilePred);
}
}
@@ -409,7 +399,7 @@ public class Units{
/** @return whether any units exist in this rectangle */
public static boolean any(float x, float y, float width, float height, Boolf<Unit> filter){
return count(x, y, width, height, filter) > 0;
return Groups.unit.intersect(x, y, width, height, filter);
}
/** Iterates over all units in a rectangle. */
@@ -494,4 +484,8 @@ public class Units{
public interface Sortf{
float cost(Unit unit, float x, float y);
}
public interface BuildingPriorityf{
float priority(Building build);
}
}
@@ -4,6 +4,7 @@ import arc.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.ui.*;
public abstract class Ability implements Cloneable{
protected static final float descriptionWidth = 350f;
@@ -13,10 +14,26 @@ public abstract class Ability implements Cloneable{
public float data;
public void update(Unit unit){}
public void draw(Unit unit){}
public void death(Unit unit){}
public void created(Unit unit){}
public void init(UnitType type){}
public void displayBars(Unit unit, Table bars){}
public void display(Table t){
t.table(Styles.grayPanel, a -> {
a.add("[accent]" + localized()).padBottom(4).center().top().expandX();
a.row();
a.left().top().defaults().left();
addStats(a);
}).pad(5).margin(10).growX().top().uniformX();
}
public void addStats(Table t){
if(Core.bundle.has(getBundle() + ".description")){
t.add(Core.bundle.get(getBundle() + ".description")).wrap().width(descriptionWidth);
@@ -41,8 +41,7 @@ public class ForceFieldAbility extends Ability{
if(trait.team != paramUnit.team && trait.type.absorbable && Intersector.isInRegularPolygon(paramField.sides, paramUnit.x, paramUnit.y, realRad, paramField.rotation, trait.x(), trait.y()) && paramUnit.shield > 0){
trait.absorb();
Fx.absorb.at(trait);
paramUnit.shield -= trait.damage();
paramUnit.shield -= trait.type().shieldDamage(trait);
paramField.alpha = 1f;
}
};
@@ -105,6 +104,15 @@ public class ForceFieldAbility extends Ability{
}
}
@Override
public void death(Unit unit){
//self-destructing units can have a shield on death
if(unit.shield > 0f && !wasBroken){
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), this);
}
}
@Override
public void draw(Unit unit){
checkRadius(unit);
@@ -131,6 +139,11 @@ public class ForceFieldAbility extends Ability{
bars.add(new Bar("stat.shieldhealth", Pal.accent, () -> unit.shield / max)).row();
}
@Override
public void created(Unit unit){
unit.shield = max;
}
public void checkRadius(Unit unit){
//timer2 is used to store radius scale as an effect
realRad = radiusScale * radius;
@@ -13,8 +13,8 @@ import static mindustry.Vars.*;
public class LiquidRegenAbility extends Ability{
public Liquid liquid;
public float slurpSpeed = 9f;
public float regenPerSlurp = 2.9f;
public float slurpSpeed = 5f;
public float regenPerSlurp = 6f;
public float slurpEffectChance = 0.4f;
public Effect slurpEffect = Fx.heal;
@@ -31,7 +31,7 @@ public class LiquidRegenAbility extends Ability{
//TODO timer?
//TODO effects?
if(unit.damaged()){
if(unit.damaged() && !unit.isFlying()){
boolean healed = false;
int tx = unit.tileX(), ty = unit.tileY();
int rad = Math.max((int)(unit.hitSize / tilesize * 0.6f), 1);
@@ -1,6 +1,7 @@
package mindustry.entities.abilities;
import arc.graphics.*;
import arc.math.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
@@ -9,8 +10,9 @@ import mindustry.gen.*;
public class MoveEffectAbility extends Ability{
public float minVelocity = 0.08f;
public float interval = 3f;
public float x, y, rotation;
public float interval = 3f, chance = 0f;
public int amount = 1;
public float x, y, rotation, rangeX, rangeY, rangeLengthMin, rangeLengthMax;
public boolean rotateEffect = false;
public float effectParam = 3f;
public boolean teamColor = false;
@@ -38,10 +40,17 @@ public class MoveEffectAbility extends Ability{
if(Vars.headless) return;
counter += Time.delta;
if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval) && !unit.inFogTo(Vars.player.team())){
Tmp.v1.trns(unit.rotation - 90f, x, y);
if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval || (chance > 0 && Mathf.chanceDelta(chance))) && !unit.inFogTo(Vars.player.team())){
if(rangeLengthMax > 0){
Tmp.v1.trns(unit.rotation - 90f, x, y).add(Tmp.v2.rnd(Mathf.random(rangeLengthMin, rangeLengthMax)));
}else{
Tmp.v1.trns(unit.rotation - 90f, x + Mathf.range(rangeX), y + Mathf.range(rangeY));
}
counter %= interval;
effect.at(Tmp.v1.x + unit.x, Tmp.v1.y + unit.y, (rotateEffect ? unit.rotation : effectParam) + rotation, teamColor ? unit.team.color : color, parentizeEffects ? unit : null);
for(int i = 0; i < amount; i++){
effect.at(Tmp.v1.x + unit.x, Tmp.v1.y + unit.y, (rotateEffect ? unit.rotation : effectParam) + rotation, teamColor ? unit.team.color : color, parentizeEffects ? unit : null);
}
}
}
}
@@ -11,7 +11,6 @@ import mindustry.*;
import mindustry.content.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
public class ShieldArcAbility extends Ability{
@@ -102,7 +101,7 @@ public class ShieldArcAbility extends Ability{
}
@Override
public void init(UnitType type){
public void created(Unit unit){
data = max;
}
@@ -46,7 +46,7 @@ public class UnitSpawnAbility extends Ability{
timer += Time.delta * state.rules.unitBuildSpeed(unit.team);
if(timer >= spawnTime && Units.canCreate(unit.team, this.unit)){
float x = unit.x + Angles.trnsx(unit.rotation, spawnY, spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, spawnX);
float x = unit.x + Angles.trnsx(unit.rotation, spawnY, -spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, -spawnX);
spawnEffect.at(x, y, 0f, parentizeEffects ? unit : null);
Unit u = this.unit.create(unit.team);
u.set(x, y);
@@ -64,7 +64,7 @@ public class UnitSpawnAbility extends Ability{
public void draw(Unit unit){
if(Units.canCreate(unit.team, this.unit)){
Draw.draw(Draw.z(), () -> {
float x = unit.x + Angles.trnsx(unit.rotation, spawnY, spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, spawnX);
float x = unit.x + Angles.trnsx(unit.rotation, spawnY, -spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, -spawnX);
Drawf.construct(x, y, this.unit.fullIcon, unit.rotation - 90, timer / spawnTime, 1f, timer);
});
}
@@ -30,16 +30,24 @@ public class BulletType extends Content implements Cloneable{
/** Lifetime in ticks. */
public float lifetime = 40f;
/** Min/max multipliers for lifetime applied to this bullet when spawned. */
public float lifeScaleRandMin = 1f, lifeScaleRandMax = 1f;
/** Speed in units/tick. */
public float speed = 1f;
/** Min/max multipliers for velocity applied to this bullet when spawned. */
public float velocityScaleRandMin = 1f, velocityScaleRandMax = 1f;
/** Direct damage dealt on hit. */
public float damage = 1f;
/** Hitbox size. */
public float hitSize = 4;
/** Clipping hitbox. */
public float drawSize = 40f;
/** Angle offset applied to bullet when spawned each time. */
public float angleOffset = 0f, randomAngleOffset = 0f;
/** Drag as fraction of velocity. */
public float drag = 0f;
/** Acceleration per frame. */
public float accel = 0f;
/** Whether to pierce units. */
public boolean pierce;
/** Whether to pierce buildings. */
@@ -84,6 +92,8 @@ public class BulletType extends Content implements Cloneable{
public float reloadMultiplier = 1f;
/** Multiplier of how much base damage is done to tiles. */
public float buildingDamageMultiplier = 1f;
/** Multiplier of how much base damage is done to force shields. */
public float shieldDamageMultiplier = 1f;
/** Recoil from shooter entities. */
public float recoil;
/** Whether to kill the shooter when this is shot. For suicide bombers. */
@@ -141,14 +151,22 @@ public class BulletType extends Content implements Cloneable{
public float rangeOverride = -1f;
/** When used in a turret with multiple ammo types, this can be set to a non-zero value to influence range. */
public float rangeChange = 0f;
/** When used in turrets with limitRange() applied, this adds extra range to the bullets that extends past targeting range. Only particularly relevant in vanilla. */
public float extraRangeMargin = 0f;
/** Range initialized in init(). */
public float range = 0f;
/** When used in a turret with multiple ammoo types, this can be set to a non-zero value to influence minRange */
public float minRangeChange = 0f;
/** % of block health healed **/
public float healPercent = 0f;
/** flat amount of block health healed */
public float healAmount = 0f;
/** Fraction of bullet damage that heals that shooter. */
public float lifesteal = 0f;
/** Whether to make fire on impact */
public boolean makeFire = false;
/** Whether this bullet will always hit blocks under it. */
public boolean hitUnder = false;
/** Whether to create hit effects on despawn. Forced to true if this bullet has any special effects like splash damage. */
public boolean despawnHit = false;
/** If true, this bullet will create bullets when it hits anything, not just when it despawns. */
@@ -157,6 +175,10 @@ public class BulletType extends Content implements Cloneable{
public boolean fragOnAbsorb = true;
/** If true, unit armor is ignored in damage calculations. */
public boolean pierceArmor = false;
/** If true, the bullet will "stick" to enemies and get deactivated on collision. */
public boolean sticky = false;
/** Extra time added to bullet when it sticks to something. */
public float stickyExtraLifetime = 0f;
/** Whether status and despawnHit should automatically be set. */
public boolean setDefaults = true;
/** Amount of shaking produced when this bullet hits something or despawns. */
@@ -189,7 +211,7 @@ public class BulletType extends Content implements Cloneable{
public float bulletInterval = 20f;
/** Number of bullet spawned per interval. */
public int intervalBullets = 1;
/** Random spread of interval bullets. */
/** Random angle added to interval bullets. */
public float intervalRandomSpread = 360f;
/** Angle spread between individual interval bullets. */
public float intervalSpread = 0f;
@@ -198,6 +220,9 @@ public class BulletType extends Content implements Cloneable{
/** Use a negative value to disable interval bullet delay. */
public float intervalDelay = -1f;
/** If true, this bullet is rendered underwater. Highly experimental! */
public boolean underwater = false;
/** Color used for hit/despawn effects. */
public Color hitColor = Color.white;
/** Color used for block heal effects. */
@@ -206,6 +231,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<>();
/** Random angle spread of spawn bullets. */
public float spawnBulletRandomSpread = 0f;
/** Unit spawned _instead of_ this bullet. Useful for missiles. */
public @Nullable UnitType spawnUnit;
/** Unit spawned when this bullet hits something or despawns due to it hitting the end of its lifetime. */
@@ -227,8 +254,12 @@ public class BulletType extends Content implements Cloneable{
public float trailChance = -0.0001f;
/** Uniform interval in which trail effect is spawned. */
public float trailInterval = 0f;
/** Min velocity required for trail effect to spawn. */
public float trailMinVelocity = 0f;
/** Trail effect that is spawned. */
public Effect trailEffect = Fx.missileTrail;
/** Random offset of trail effect. */
public float trailSpread = 0f;
/** Rotation/size parameter that is passed to trail. Usually, this controls size. */
public float trailParam = 2f;
/** Whether the parameter passed to the trail is the bullet rotation, instead of a flat value. */
@@ -241,6 +272,14 @@ public class BulletType extends Content implements Cloneable{
public float trailWidth = 2f;
/** If trailSinMag > 0, these values are applied as a sine curve to trail width. */
public float trailSinMag = 0f, trailSinScl = 3f;
/** If true, the bullet will attempt to circle around its shooting entity. */
public boolean circleShooter = false;
/** Radius that the bullet attempts to circle at. */
public float circleShooterRadius = 13f;
/** Smooth extra radius value for circling. */
public float circleShooterRadiusSmooth = 10f;
/** Multiplier of speed that is used to adjust velocity when circling. */
public float circleShooterRotateSpeed = 0.3f;
/** Use a negative value to disable splash damage. */
public float splashDamageRadius = -1f;
@@ -293,6 +332,8 @@ public class BulletType extends Content implements Cloneable{
public float weaveMag = 0f;
/** If true, the bullet weave will randomly switch directions on spawn. */
public boolean weaveRandom = true;
/** Rotation speed of the bullet velocity as it travels. */
public float rotateSpeed = 0f;
/** Number of individual puddles created. */
public int puddles;
@@ -392,6 +433,11 @@ public class BulletType extends Content implements Cloneable{
build.heal(healPercent / 100f * build.maxHealth + healAmount);
}else if(build.team != b.team && direct){
hit(b);
if(lifesteal > 0f && b.owner instanceof Healthc o){
float result = Math.max(Math.min(build.health, damage), 0);
o.heal(result * lifesteal);
}
}
handlePierce(b, initialHealth, x, y);
@@ -411,6 +457,10 @@ public class BulletType extends Content implements Cloneable{
}else{
health += shield;
}
if(lifesteal > 0f && b.owner instanceof Healthc o){
float result = Math.max(Math.min(h.health(), damage), 0);
o.heal(result * lifesteal);
}
if(pierceArmor){
h.damagePierce(damage);
}else{
@@ -525,7 +575,7 @@ public class BulletType extends Content implements Cloneable{
if(fragBullet != null && (fragOnAbsorb || !b.absorbed) && !(b.frags >= pierceFragCap && pierceFragCap > 0)){
for(int i = 0; i < fragBullets; i++){
float len = Mathf.random(fragOffsetMin, fragOffsetMax);
float a = b.rotation() + Mathf.range(fragRandomSpread / 2) + fragAngle + ((i - fragBullets/2) * fragSpread);
float a = b.rotation() + Mathf.range(fragRandomSpread / 2) + fragAngle + fragSpread * i - (fragBullets - 1) * fragSpread / 2f;
fragBullet.create(b, x + Angles.trnsx(a, len), y + Angles.trnsy(a, len), a, Mathf.random(fragVelocityMin, fragVelocityMax), Mathf.random(fragLifeMin, fragLifeMax));
}
b.frags++;
@@ -567,6 +617,14 @@ public class BulletType extends Content implements Cloneable{
}
}
public float buildingDamage(Bullet b){
return b.damage() * buildingDamageMultiplier;
}
public float shieldDamage(Bullet b){
return b.damage() * shieldDamageMultiplier;
}
public void draw(Bullet b){
drawTrail(b);
drawParts(b);
@@ -610,7 +668,7 @@ public class BulletType extends Content implements Cloneable{
if(spawnBullets.size > 0){
for(var bullet : spawnBullets){
bullet.create(b, b.x, b.y, b.rotation());
bullet.create(b, b.x, b.y, b.rotation() + Mathf.range(spawnBulletRandomSpread));
}
}
}
@@ -664,18 +722,40 @@ public class BulletType extends Content implements Cloneable{
if(weaveMag != 0){
b.vel.rotateRadExact((float)Math.sin((b.time + Math.PI * weaveScale/2f) / weaveScale) * weaveMag * (weaveRandom ? (Mathf.randomSeed(b.id, 0, 1) == 1 ? -1 : 1) : 1f) * Time.delta * Mathf.degRad);
}
if(rotateSpeed != 0){
b.vel.rotate(rotateSpeed * Time.delta);
}
if(circleShooter && b.owner instanceof Healthc h && h.isValid()){
Tmp.v1.set(h).sub(b);
Tmp.v1.rotate(90f * Mathf.lerp(0f, 1f, 1f - Mathf.clamp((Tmp.v1.len() - circleShooterRadius) / circleShooterRadiusSmooth)));
b.vel.add(Tmp.v1.limit(speed * circleShooterRotateSpeed * Time.delta)).limit(speed);
}
}
public void updateTrailEffects(Bullet b){
if(trailChance > 0){
boolean canSpawn = trailMinVelocity <= 0f || b.vel.len2() >= trailMinVelocity * trailMinVelocity;
if(trailChance > 0 && canSpawn){
if(Mathf.chanceDelta(trailChance)){
trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor);
if(trailSpread > 0){
Tmp.v1.rnd(Mathf.random(trailSpread));
}else{
Tmp.v1.setZero();
}
trailEffect.at(b.x + Tmp.v1.x, b.y + Tmp.v1.y, trailRotation ? b.rotation() : trailParam, trailColor);
}
}
if(trailInterval > 0f){
if(trailInterval > 0f && canSpawn){
if(b.timer(0, trailInterval)){
trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor);
if(trailSpread > 0){
Tmp.v1.rnd(Mathf.random(trailSpread));
}else{
Tmp.v1.setZero();
}
trailEffect.at(b.x + Tmp.v1.x, b.y + Tmp.v1.y, trailRotation ? b.rotation() : trailParam, trailColor);
}
}
}
@@ -714,7 +794,10 @@ public class BulletType extends Content implements Cloneable{
}
if(lightningType == null){
lightningType = !collidesAir ? Bullets.damageLightningGround : Bullets.damageLightning;
lightningType =
!collidesAir ? Bullets.damageLightningGround :
!collidesGround ? Bullets.damageLightningAir :
Bullets.damageLightning;
}
if(lightRadius <= -1){
@@ -752,15 +835,15 @@ public class BulletType extends Content implements Cloneable{
}
public @Nullable Bullet create(Bullet parent, float x, float y, float angle){
return create(parent.owner, parent.team, x, y, angle);
return create(parent.owner, parent.shooter, parent.team, x, y, angle, -1, 1f, 1f, null, null, -1f, -1f);
}
public @Nullable Bullet create(Bullet parent, float x, float y, float angle, float velocityScl, float lifeScale){
return create(parent.owner, parent.team, x, y, angle, velocityScl, lifeScale);
return create(parent.owner, parent.shooter, parent.team, x, y, angle, -1, velocityScl, lifeScale, null, null, -1f, -1f);
}
public @Nullable Bullet create(Bullet parent, float x, float y, float angle, float velocityScl){
return create(parent.owner(), parent.team, x, y, angle, velocityScl);
return create(parent.owner, parent.shooter, parent.team, x, y, angle, -1, velocityScl, 1f, null, null, -1f, -1f);
}
public @Nullable Bullet create(@Nullable Entityc owner, Team team, float x, float y, float angle, float damage, float velocityScl, float lifetimeScl, Object data){
@@ -783,6 +866,8 @@ public class BulletType extends Content implements Cloneable{
@Nullable Entityc owner, @Nullable Entityc shooter, Team team, float x, float y, float angle, float damage, float velocityScl,
float lifetimeScl, Object data, @Nullable Mover mover, float aimX, float aimY, @Nullable Teamc target
){
angle += angleOffset + Mathf.range(randomAngleOffset);
if(!Mathf.chance(createChance)) return null;
if(ignoreSpawnAngle) angle = 0;
if(spawnUnit != null){
@@ -818,6 +903,7 @@ public class BulletType extends Content implements Cloneable{
Bullet bullet = Bullet.create();
bullet.type = this;
bullet.owner = owner;
bullet.shooter = (shooter == null ? owner : shooter);
bullet.team = team;
bullet.time = 0f;
bullet.originX = x;
@@ -828,13 +914,13 @@ public class BulletType extends Content implements Cloneable{
bullet.aimX = aimX;
bullet.aimY = aimY;
bullet.initVel(angle, speed * velocityScl);
bullet.initVel(angle, speed * velocityScl * (velocityScaleRandMin != 1f || velocityScaleRandMax != 1f ? Mathf.random(velocityScaleRandMin, velocityScaleRandMax) : 1f));
if(backMove){
bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta);
}else{
bullet.set(x, y);
}
bullet.lifetime = lifetime * lifetimeScl;
bullet.lifetime = lifetime * lifetimeScl * (lifeScaleRandMin != 1f || lifeScaleRandMax != 1f ? Mathf.random(lifeScaleRandMin, lifeScaleRandMax) : 1f);
bullet.data = data;
bullet.drag = drag;
bullet.hitSize = hitSize;
@@ -0,0 +1,10 @@
package mindustry.entities.bullet;
public class EmptyBulletType extends BulletType{
public EmptyBulletType(){
hittable = collidesGround = collidesAir = collidesTiles = false;
speed = 0f;
keepVelocity = false;
}
}
@@ -0,0 +1,53 @@
package mindustry.entities.bullet;
import arc.util.*;
import mindustry.entities.*;
import mindustry.gen.*;
/** This class can only be used with PointDefenseBulletWeapon. Attempting to spawn it in outside of that weapon will lead to standard behavior. */
public class InterceptorBulletType extends BasicBulletType{
public InterceptorBulletType(float speed, float damage){
super(speed, damage);
}
public InterceptorBulletType(){
}
public InterceptorBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
}
@Override
public void update(Bullet b){
super.update(b);
if(b.data instanceof Bullet other){
if(other.isAdded()){
//check for an overlap between the two bullet trajectories; it is the responsibility of the creator to make sure the bullet is a valid target
if(EntityCollisions.collide(
b.x, b.y,
b.hitSize, b.hitSize,
b.deltaX, b.deltaY,
other.x, other.y,
other.hitSize, other.hitSize,
other.deltaX, other.deltaY, Tmp.v1)){
b.set(Tmp.v1);
hit(b, b.x, b.y);
b.remove();
if(other.damage > damage){
other.damage -= b.damage;
}else{
other.remove();
}
}
}else{
b.data = null;
}
}
}
}
@@ -1,6 +1,5 @@
package mindustry.entities.bullet;
import arc.graphics.*;
import arc.math.*;
import mindustry.content.*;
import mindustry.entities.*;
@@ -8,8 +7,6 @@ import mindustry.gen.*;
import mindustry.graphics.*;
public class LightningBulletType extends BulletType{
public Color lightningColor = Pal.lancerLaser;
public int lightningLength = 25, lightningLengthRand = 0;
public LightningBulletType(){
damage = 1f;
@@ -21,6 +18,9 @@ public class LightningBulletType extends BulletType{
hittable = false;
//for stats
status = StatusEffects.shocked;
lightningLength = 25;
lightningLengthRand = 0;
lightningColor = Pal.lancerLaser;
}
@Override
@@ -33,10 +33,6 @@ public class LightningBulletType extends BulletType{
return super.estimateDPS() * Math.max(lightningLength / 10f, 1);
}
@Override
public void draw(Bullet b){
}
@Override
public void init(Bullet b){
super.init(b);
@@ -0,0 +1,61 @@
package mindustry.entities.bullet;
import arc.util.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.gen.*;
/** A fake bullet type that spawns multiple sub-bullets when "fired". */
public class MultiBulletType extends BulletType{
public BulletType[] bullets = {};
/** Amount of times the bullet array is repeated. */
public int repeat = 1;
public MultiBulletType(BulletType... bullets){
this.bullets = bullets;
}
public MultiBulletType(int repeat, BulletType... bullets){
this.repeat = repeat;
this.bullets = bullets;
}
public MultiBulletType(){
}
@Override
public float estimateDPS(){
float sum = 0f;
for(var b : bullets){
sum += b.estimateDPS();
}
return sum;
}
@Override
protected float calculateRange(){
float max = 0f;
for(var b : bullets){
max = Math.max(max, b.calculateRange());
}
return max;
}
@Override
public @Nullable Bullet create(
@Nullable Entityc owner, @Nullable Entityc shooter, Team team, float x, float y, float angle, float damage, float velocityScl,
float lifetimeScl, Object data, @Nullable Mover mover, float aimX, float aimY, @Nullable Teamc target
){
angle += angleOffset;
Bullet last = null;
for(int i = 0; i < repeat; i++){
for(var bullet : bullets){
last = bullet.create(owner, shooter, team, x, y, angle, damage, velocityScl, lifetimeScl, data, mover, aimX, aimY, target);
}
}
return last;
}
}
@@ -3,6 +3,7 @@ package mindustry.entities.bullet;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.content.*;
@@ -11,10 +12,22 @@ import mindustry.gen.*;
import mindustry.graphics.*;
public class SapBulletType extends BulletType{
public float length = 100f;
public float length = 100f, lengthRand = 0f;
public float sapStrength = 0.5f;
public Color color = Color.white.cpy();
public float width = 0.4f;
public String sprite = "laser";
public TextureRegion laserRegion;
public TextureRegion laserEndRegion;
@Override
public void load(){
super.load();
laserRegion = Core.atlas.find(sprite);
laserEndRegion = Core.atlas.find(sprite + "-end");
}
public SapBulletType(){
speed = 0f;
@@ -37,7 +50,7 @@ public class SapBulletType extends BulletType{
Tmp.v1.set(data).lerp(b, b.fin());
Draw.color(color);
Drawf.laser(Core.atlas.find("laser"), Core.atlas.find("laser-end"),
Drawf.laser(laserRegion, laserEndRegion,
b.x, b.y, Tmp.v1.x, Tmp.v1.y, width * b.fout());
Draw.reset();
@@ -60,7 +73,9 @@ public class SapBulletType extends BulletType{
public void init(Bullet b){
super.init(b);
Healthc target = Damage.linecast(b, b.x, b.y, b.rotation(), length);
float len = Mathf.random(length, length + lengthRand);
Healthc target = Damage.linecast(b, b.x, b.y, b.rotation(), len);
b.data = target;
if(target != null){
@@ -80,7 +95,7 @@ public class SapBulletType extends BulletType{
hit(b, tile.x, tile.y);
}
}else{
b.data = new Vec2().trns(b.rotation(), length).add(b.x, b.y);
b.data = new Vec2().trns(b.rotation(), len).add(b.x, b.y);
}
}
}
@@ -63,6 +63,11 @@ abstract class BlockUnitComp implements Unitc{
return tile != null && tile.isValid();
}
@Replace
public boolean isAdded(){
return tile != null && tile.isValid();
}
@Replace
public void team(Team team){
if(tile != null && this.team != team){
@@ -1,58 +0,0 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import static mindustry.Vars.*;
@Component
abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{
static final float warpDst = 30f;
@Import UnitType type;
@Import float x, y;
@Import Team team;
@Override
public void update(){
if(!type.bounded) return;
float bot = 0f, left = 0f, top = world.unitHeight(), right = world.unitWidth();
//TODO hidden map rules only apply to player teams? should they?
if(state.rules.limitMapArea && !team.isAI()){
bot = state.rules.limitY * tilesize;
left = state.rules.limitX * tilesize;
top = state.rules.limitHeight * tilesize + bot;
right = state.rules.limitWidth * tilesize + left;
}
if(!net.client() || isLocal()){
float dx = 0f, dy = 0f;
//repel unit out of bounds
if(x < left) dx += (-(x - left)/warpDst);
if(y < bot) dy += (-(y - bot)/warpDst);
if(x > right) dx -= (x - right)/warpDst;
if(y > top) dy -= (y - top)/warpDst;
velAddNet(dx * Time.delta, dy * Time.delta);
}
//clamp position if not flying
if(isGrounded()){
x = Mathf.clamp(x, left, right - tilesize);
y = Mathf.clamp(y, bot, top - tilesize);
}
//kill when out of bounds
if(x < -finalWorldBounds + left || y < -finalWorldBounds + bot || x >= right + finalWorldBounds || y >= top + finalWorldBounds){
kill();
}
}
}
@@ -136,7 +136,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
}
if(!(tile.build instanceof ConstructBuild cb)){
if(!current.initialized && !current.breaking && Build.validPlaceIgnoreUnits(current.block, team, current.x, current.y, current.rotation, true)){
if(!current.initialized && !current.breaking && Build.validPlaceIgnoreUnits(current.block, team, current.x, current.y, current.rotation, true, true)){
if(Build.checkNoUnitOverlap(current.block, current.x, current.y)){
boolean hasAll = infinite || current.isRotation(team) ||
//derelict repair
@@ -188,7 +188,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
//otherwise, update it.
if(current.breaking){
entity.deconstruct(self(), core, bs);
}else{
}else if(entity.current != null && entity.current.unlockedNowHost()){ //only allow building unlocked blocks
entity.construct(self(), core, bs, current.config);
}
@@ -16,7 +16,6 @@ import arc.util.*;
import arc.util.io.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.audio.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.ctype.*;
@@ -47,7 +46,7 @@ import java.util.*;
import static mindustry.Vars.*;
@EntityDef(value = {Buildingc.class}, isFinal = false, genio = false, serialize = false)
@Component(base = true)
@Component(base = true, genInterface = false)
abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, QuadTreeObject, Displayable, Sized, Senseable, Controllable, Settable{
//region vars and initialization
static final float timeToSleep = 60f * 1, recentDamageTime = 60f * 5f;
@@ -56,14 +55,14 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
static final BuildTeamChangeEvent teamChangeEvent = new BuildTeamChangeEvent();
static final BuildDamageEvent bulletDamageEvent = new BuildDamageEvent();
static int sleepingEntities = 0;
@Import float x, y, health, maxHealth;
@Import Team team;
@Import boolean dead;
transient Tile tile;
transient Block block;
transient Seq<Building> proximity = new Seq<>(6);
transient Seq<Building> proximity = new Seq<>(true, 6, Building.class);
transient int cdump;
transient int rotation;
transient float payloadRotation;
@@ -99,8 +98,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
private transient float timeScale = 1f, timeScaleDuration;
private transient float dumpAccum;
private transient @Nullable SoundLoop sound;
private transient boolean sleeping;
private transient float sleepTime;
private transient boolean initialized;
@@ -126,6 +123,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
add();
}
checkAllowUpdate();
created();
return self();
@@ -136,10 +134,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
this.block = block;
this.team = team;
if(block.loopSound != Sounds.none){
sound = new SoundLoop(block.loopSound, block.loopSoundVolume);
}
health = block.health;
maxHealth(block.health);
timer(new Interval(block.timers));
@@ -338,17 +332,18 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
data.plans.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, toAdd.id, overrideConfig == null ? config() : overrideConfig));
data.plans.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, toAdd, overrideConfig == null ? config() : overrideConfig));
}
public @Nullable Tile findClosestEdge(Position to, Boolf<Tile> solid){
if(to == null) return null;
Tile best = null;
float mindst = 0f;
for(var point : Edges.getEdges(block.size)){
Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y);
if(other != null && !solid.get(other) && (best == null || to.dst2(other) < mindst)){
best = other;
mindst = other.dst2(other);
mindst = other.dst2(to);
}
}
return best;
@@ -473,6 +468,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return lastDamageTime + recentDamageTime >= Time.time;
}
public void eachEdge(Cons<Tile> cons){
for(var edge : block.getEdges()){
Tile other = world.tile(tile.x + edge.x, tile.y + edge.y);
if(other != null){
cons.get(other);
}
}
}
public Building nearby(int dx, int dy){
return world.build(tile.x + dx, tile.y + dy);
}
@@ -585,8 +589,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** @return whether this block is allowed to update based on team/environment */
public boolean allowUpdate(){
return team != Team.derelict && block.supportsEnv(state.rules.env) &&
//check if outside map limit
(!state.rules.limitMapArea || !state.rules.disableOutsideArea || Rect.contains(state.rules.limitX, state.rules.limitY, state.rules.limitWidth, state.rules.limitHeight, tile.x, tile.y));
//check if outside map limit (privileged blocks are exempt)
(block.privileged || !state.rules.limitMapArea || !state.rules.disableOutsideArea || Rect.contains(state.rules.limitX, state.rules.limitY, state.rules.limitWidth, state.rules.limitHeight, tile.x, tile.y));
}
public BlockStatus status(){
@@ -809,6 +813,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return false;
}
public boolean canBeReplaced(Block other){
return other.canReplace(block);
}
public void handleItem(Building source, Item item){
items.add(item, 1);
}
@@ -1029,10 +1037,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
int itemSize = allItems.size;
Object[] itemArray = allItems.items;
for(int i = 0; i < proximity.size; i++){
Building other = proximity.get((i + dump) % proximity.size);
if(todump == null){
if(todump == null){
for(int i = 0; i < proximity.size; i++){
Building other = proximity.get((i + dump) % proximity.size);
for(int ii = 0; ii < itemSize; ii++){
if(!items.has(ii)) continue;
@@ -1045,23 +1052,32 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return true;
}
}
}else{
incrementDump(proximity.size);
}
}else{
for(int i = 0; i < proximity.size; i++){
Building other = proximity.get((i + dump) % proximity.size);
if(other.acceptItem(self(), todump) && canDump(other, todump)){
other.handleItem(self(), todump);
items.remove(todump, 1);
incrementDump(proximity.size);
return true;
}
}
incrementDump(proximity.size);
incrementDump(proximity.size);
}
}
return false;
}
public void incrementDump(int prox){
cdump = ((cdump + 1) % prox);
//this is possible if transferring an item changed a block
if(prox != 0){
cdump = ((cdump + 1) % prox);
}
}
/** Used for dumping items. */
@@ -1087,6 +1103,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
/** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */
//TODO ??? this is just onProximityUpdate ?
public void onProximityAdded(){
if(power != null){
updatePowerGraph();
@@ -1118,7 +1135,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
power.links.clear();
}
public boolean conductsTo(Building other){
return !block.insulated;
}
@@ -1151,16 +1168,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return getProgressIncrease(1f) / edelta();
}
/** @return whether this block should play its active sound.*/
public boolean shouldActiveSound(){
return false;
}
/** @return volume cale of active sound. */
public float activeSoundVolume(){
return 1f;
}
/** @return whether this block should play its idle sound.*/
public boolean shouldAmbientSound(){
return shouldConsume();
@@ -1196,6 +1203,16 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
block.drawOverlay(x, y, rotation);
}
public void drawItemSelection(@Nullable UnlockableContent selection){
if(selection != null){
float dx = x - block.size * tilesize/2f, dy = y + block.size * tilesize/2f, s = iconSmall / 4f;
Draw.mixcol(Color.darkGray, 1f);
Draw.rect(selection.fullIcon, dx, dy - 1, s, s);
Draw.reset();
Draw.rect(selection.fullIcon, dx, dy, s, s);
}
}
public void drawDisabled(){
Draw.color(Color.scarlet);
Draw.alpha(0.8f);
@@ -1303,14 +1320,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void onRemoved(){
}
/** Called every frame a unit is on this */
/** Called every frame a unit is on this. Hovering/flying/steppy units do not apply. */
public void unitOn(Unit unit){
}
/** Called every frame a unit is on this. Applies to any unit. */
public void unitOnAny(Unit unit){
}
/** Called when a unit that spawned at this tile is removed. */
public void unitRemoved(Unit unit){
}
/** Called when a puddle is on this building. Only called at an interval (40 ticks). */
public void puddleOn(Puddle puddle){
}
/** Called when arbitrary configuration is applied to a tile. */
public void configured(@Nullable Unit builder, @Nullable Object value){
//null is of type void.class; anonymous classes use their superclass.
@@ -1320,7 +1346,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(value instanceof Block) type = Block.class;
if(value instanceof Liquid) type = Liquid.class;
if(value instanceof UnitType) type = UnitType.class;
if(builder != null && builder.isPlayer()){
updateLastAccess(builder.getPlayer());
}
@@ -1360,6 +1386,19 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return block.itemCapacity;
}
public void splashLiquid(Liquid liquid, float amount){
float splash = Mathf.clamp(amount / 4f, 0f, 10f);
for(int i = 0; i < Mathf.clamp(amount / 5, 0, 30); i++){
Time.run(i / 2f, () -> {
Tile other = world.tileWorld(x + Mathf.range(block.size * tilesize / 2), y + Mathf.range(block.size * tilesize / 2));
if(other != null){
Puddles.deposit(other, liquid, splash);
}
});
}
}
/** Called when a block begins (not finishes!) deconstruction. The building is still present at this point. */
public void onDeconstructed(@Nullable Unit builder){
//deposit non-incinerable liquid on ground
@@ -1371,9 +1410,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Called when the block is destroyed. The tile is still intact at this stage. */
public void onDestroyed(){
if(sound != null){
sound.stop();
}
float explosiveness = block.baseExplosiveness;
float flammability = 0f;
@@ -1399,22 +1435,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(block.hasLiquids && state.rules.damageExplosions){
liquids.each((liquid, amount) -> {
float splash = Mathf.clamp(amount / 4f, 0f, 10f);
for(int i = 0; i < Mathf.clamp(amount / 5, 0, 30); i++){
Time.run(i / 2f, () -> {
Tile other = world.tileWorld(x + Mathf.range(block.size * tilesize / 2), y + Mathf.range(block.size * tilesize / 2));
if(other != null){
Puddles.deposit(other, liquid, splash);
}
});
}
});
liquids.each(this::splashLiquid);
}
//cap explosiveness so fluid tanks/vaults don't instakill units
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect);
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect, block.baseShake);
if(block.createRubble && !floor().solid && !floor().isLiquid){
Effect.rubble(x, y, block.size);
@@ -1640,13 +1665,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public boolean collision(Bullet other){
boolean wasDead = health <= 0;
float damage = other.damage() * other.type().buildingDamageMultiplier;
float damage = other.type.buildingDamage(other);
if(!other.type.pierceArmor){
damage = Damage.applyArmor(damage, block.armor);
}
damage(other.team, damage);
Events.fire(bulletDamageEvent.set(self(), other));
damage(other, other.team, damage);
if(health <= 0 && !wasDead){
Events.fire(new BuildingBulletDestroyEvent(self(), other));
@@ -1669,22 +1693,46 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Changes this building's team in a safe manner. */
public void changeTeam(Team next){
if(this.team == next) return;
if(block.forceTeam != null) team = block.forceTeam;
Team last = this.team;
if(last == next) return;
boolean was = isValid();
if(was) indexer.removeIndex(tile);
this.team = next;
if(power != null){
for(int i = 0; i < power.links.size; i++){
var other = world.build(power.links.items[i]);
if(other != null && other.team != team && other.power != null){
power.links.removeIndex(i);
other.power.links.removeValue(pos());
new PowerGraph().reflow(other);
i --;
}
}
new PowerGraph().reflow(self());
updatePowerGraph();
}
if(was){
indexer.addIndex(tile);
Events.fire(teamChangeEvent.set(last, self()));
}
checkAllowUpdate();
}
public boolean canPickup(){
return true;
return block.canPickup;
}
/** Called right before this building is picked up. */
@@ -1727,7 +1775,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void updateProximity(){
tmpTiles.clear();
proximity.clear();
Point2[] nearby = Edges.getEdges(block.size);
for(Point2 point : nearby){
Building other = world.build(tile.x + point.x, tile.y + point.y);
@@ -1752,6 +1800,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
public void onNearbyBuildAdded(Building other){}
public void consume(){
for(Consume cons : block.consumers){
cons.trigger(self());
@@ -1961,7 +2011,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
case powerNetCapacity -> power == null ? 0 : power.graph.getLastCapacity();
case enabled -> enabled ? 1 : 0;
case controlled -> this instanceof ControlBlock c && c.isControlled() ? GlobalVars.ctrlPlayer : 0;
case payloadCount -> getPayload() != null ? 1 : 0;
case payloadCount -> (getPayloads() != null ? getPayloads().total() : 0) + (getPayload() != null ? 1 : 0);
case size -> block.size;
case cameraX, cameraY, cameraWidth, cameraHeight -> this instanceof ControlBlock c ? c.unit().sense(sensor) : 0;
default -> Float.NaN; //gets converted to null in logic
@@ -1983,6 +2033,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public double sense(Content content){
if(content instanceof Item i && items != null) return items.get(i);
if(content instanceof Liquid l && liquids != null) return liquids.get(l);
if(getPayloads() != null){
if(content instanceof UnitType u) return getPayloads().get(u);
if(content instanceof Block b) return getPayloads().get(b);
}
return Float.NaN; //invalid sense
}
@@ -2078,18 +2132,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return true;
}
@Override
public void remove(){
if(sound != null){
sound.stop();
}
}
@Override
public void killed(){
dead = true;
Events.fire(new BlockDestroyEvent(tile));
block.destroySound.at(tile);
block.destroySound.at(tile, Mathf.random(block.destroyPitchMin, block.destroyPitchMax));
onDestroyed();
if(tile != emptyTile){
tile.remove();
@@ -2098,48 +2145,44 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
afterDestroyed();
}
public void checkAllowUpdate(){
if(!allowUpdate()){
enabled = false;
}
}
@Final
@Replace
@Override
public void update(){
//TODO should just avoid updating buildings instead
if(state.isEditor()) return;
//TODO refactor to timestamp-based system?
if((timeScaleDuration -= Time.delta) <= 0f || !block.canOverdrive){
timeScale = 1f;
}
if(!allowUpdate()){
enabled = false;
}
if(!headless && !wasVisible && state.rules.fog && !inFogTo(player.team())){
visibleFlags |= (1L << player.team().id);
wasVisible = true;
renderer.blocks.updateShadow(self());
renderer.minimap.update(tile);
}
//TODO separate system for sound? AudioSource, etc
if(!headless){
if(sound != null){
sound.update(x, y, shouldActiveSound(), activeSoundVolume());
}
if(block.ambientSound != Sounds.none && shouldAmbientSound()){
control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
}
//TODO separate multithreaded system for sound? AudioSource, etc
if(!headless && block.ambientSound != Sounds.none && shouldAmbientSound()){
control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
}
updateConsumption();
//TODO just handle per-block instead
if(enabled || !block.noUpdateDisabled){
updateTile();
}
}
/** When a block is newly revealed outside of camera view range, it is updated on the minimap. */
public void updateFogVisibility(){
if(!wasVisible && !inFogTo(player.team())){
visibleFlags |= (1L << player.team().id);
wasVisible = true;
renderer.blocks.updateShadow(self());
renderer.minimap.update(tile);
}
}
@Override
public void hitbox(Rect out){
out.setCentered(x, y, block.size * tilesize, block.size * tilesize);
@@ -39,6 +39,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
//setting this variable to true prevents lifetime from decreasing for a frame.
transient boolean keepAlive;
/** Unlike the owner, the shooter is the original entity that created this bullet. For a second-stage missile, the shooter would be the turret, but the owner would be the last missile stage.*/
transient Entityc shooter;
transient @Nullable Tile aimTile;
transient float aimX, aimY;
transient float originX, originY;
@@ -47,6 +49,9 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
transient @Nullable Trail trail;
transient int frags;
transient Posc stickyTarget;
transient float stickyX, stickyY, stickyRotation, stickyRotationOffset;
@Override
public void getCollisions(Cons<QuadTree> consumer){
Seq<TeamData> data = state.teams.present;
@@ -105,24 +110,43 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
@Override
public boolean collides(Hitboxc other){
return type.collides && (other instanceof Teamc t && t.team() != team)
&& !(other instanceof Flyingc f && !f.checkTarget(type.collidesAir, type.collidesGround))
&& !(type.pierce && hasCollided(other.id())); //prevent multiple collisions
&& !(other instanceof Unit f && !f.checkTarget(type.collidesAir, type.collidesGround))
&& !(type.pierce && hasCollided(other.id())) && stickyTarget == null; //prevent multiple collisions
}
@MethodPriority(100)
@Override
public void collision(Hitboxc other, float x, float y){
type.hit(self(), x, y);
//must be last.
if(!type.pierce){
hit = true;
remove();
if(type.sticky){
if(stickyTarget == null){
//tunnel into the target a bit for better visuals
this.x = x + vel.x;
this.y = y + vel.y;
stickTo(other);
}
}else{
collided.add(other.id());
}
type.hit(self(), x, y);
type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f);
//must be last.
if(!type.pierce){
hit = true;
remove();
}else{
collided.add(other.id());
}
type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f);
}
}
public void stickTo(Posc other){
lifetime += type.stickyExtraLifetime;
//sticky bullets don't actually hit anything.
stickyX = this.x - other.x();
stickyY = this.y - other.y();
stickyTarget = other;
stickyRotationOffset = rotation;
stickyRotation = (other instanceof Rotc rot ? rot.rotation() : 0f);
}
@Override
@@ -131,9 +155,21 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
mover.move(self());
}
if(type.accel != 0){
vel.setLength(vel.len() + type.accel * Time.delta);
}
type.update(self());
if(type.collidesTiles && type.collides && type.collidesGround){
if(stickyTarget != null){
//only stick to things that still exist in the world
if(stickyTarget instanceof Healthc h && h.isValid()){
float rotate = (stickyTarget instanceof Rotc rot ? rot.rotation() - stickyRotation : 0f);
set(Tmp.v1.set(stickyX, stickyY).rotate(rotate).add(stickyTarget));
this.rotation = rotate + stickyRotationOffset;
vel.setAngle(this.rotation);
}
}else if(type.collidesTiles && type.collides && type.collidesGround){
tileRaycast(World.toTile(lastX), World.toTile(lastY), tileX(), tileY());
}
@@ -164,6 +200,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
(!build.block.underBullets ||
//direct hit on correct tile
(aimTile != null && aimTile.build == build) ||
//bullet type allows hitting under bullets
type.hitUnder ||
//same team has no 'under build' mechanics
(build.team == team) ||
//a piercing bullet overshot the aim tile, it's fine to hit things now
@@ -200,31 +238,46 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
&& build.collide(self()) && type.testCollision(self(), build)
&& !build.dead() && (type.collidesTeam || build.team != team) && !(type.pierceBuilding && hasCollided(build.id))){
boolean remove = false;
float health = build.health;
if(type.sticky){
if(build.team != team){
//stick to edge of block
Vec2 hit = Geometry.raycastRect(lastX, lastY, x, y, Tmp.r1.setCentered(x * tilesize, y * tilesize, tilesize, tilesize));
if(hit != null){
this.x = hit.x;
this.y = hit.y;
}
if(build.team != team){
remove = build.collision(self());
}
stickTo(build);
if(remove || type.collidesTeam){
if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){
this.x = x * tilesize;
this.y = y * tilesize;
return;
}
}else{
boolean remove = false;
float health = build.health;
if(build.team != team){
remove = build.collision(self());
}
if(!type.pierceBuilding){
hit = true;
remove();
}else{
collided.add(build.id);
if(remove || type.collidesTeam){
if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){
this.x = x * tilesize;
this.y = y * tilesize;
}
if(!type.pierceBuilding){
hit = true;
remove();
}else{
collided.add(build.id);
}
}
type.hitTile(self(), build, x * tilesize, y * tilesize, health, true);
//stop raycasting when building is hit
if(type.pierceBuilding) return;
}
type.hitTile(self(), build, x * tilesize, y * tilesize, health, true);
//stop raycasting when building is hit
if(type.pierceBuilding) return;
}
if(x == x2 && y == y2) break;
@@ -246,9 +299,13 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
public void draw(){
Draw.z(type.layer);
type.draw(self());
if(type.underwater){
Drawf.underwater(() -> type.draw(self()));
}else{
type.draw(self());
}
type.drawLight(self());
Draw.reset();
}
@@ -1,10 +1,8 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
@@ -22,7 +20,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
@Import float x, y, speedMultiplier, rotation, hitSize;
@Import UnitType type;
@Import Team team;
@Import Vec2 vel;
transient Floor lastDeepFloor;
transient float lastCrawlSlowdown = 1f;
@@ -31,13 +28,7 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
@Replace
@Override
public SolidPred solidity(){
return EntityCollisions::legsSolid;
}
@Override
@Replace
public int pathType(){
return Pathfinder.costLegs;
return ignoreSolids() ? null : EntityCollisions::legsSolid;
}
@Override
@@ -110,6 +101,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
}
segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot);
crawlTime += vel.len() * Time.delta;
crawlTime += deltaLen();
}
}
@@ -6,13 +6,13 @@ import mindustry.entities.EntityCollisions.*;
import mindustry.gen.*;
@Component
abstract class ElevationMoveComp implements Velc, Posc, Flyingc, Hitboxc{
abstract class ElevationMoveComp implements Velc, Posc, Hitboxc, Unitc{
@Import float x, y;
@Replace
@Override
public SolidPred solidity(){
return isFlying() ? null : EntityCollisions::solid;
return isFlying() || ignoreSolids() ? null : EntityCollisions::solid;
}
}
@@ -59,12 +59,16 @@ abstract class EntityComp{
}
void beforeWrite(){
}
void afterRead(){
}
/** Called after *all* entities are read. */
void afterAllRead(){
//called after all entities have been read (useful for ID resolution)
void afterReadAll(){
}
}
@@ -1,123 +0,0 @@
package mindustry.entities.comp;
import arc.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class FlyingComp implements Posc, Velc, Healthc, Hitboxc{
private static final Vec2 tmp1 = new Vec2(), tmp2 = new Vec2();
@Import float x, y, speedMultiplier, hitSize;
@Import Vec2 vel;
@Import UnitType type;
@SyncLocal float elevation;
private transient boolean wasFlying;
transient boolean hovering;
transient float drownTime;
transient float splashTimer;
transient @Nullable Floor lastDrownFloor;
boolean checkTarget(boolean targetAir, boolean targetGround){
return (isGrounded() && targetGround) || (isFlying() && targetAir);
}
boolean isGrounded(){
return elevation < 0.001f;
}
boolean isFlying(){
return elevation >= 0.09f;
}
boolean canDrown(){
return isGrounded() && !hovering;
}
@Nullable Floor drownFloor(){
return canDrown() ? floorOn() : null;
}
boolean emitWalkSound(){
return true;
}
void landed(){
}
void wobble(){
x += Mathf.sin(Time.time + (id() % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
y += Mathf.cos(Time.time + (id() % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
}
void moveAt(Vec2 vector, float acceleration){
Vec2 t = tmp1.set(vector); //target vector
tmp2.set(t).sub(vel).limit(acceleration * vector.len() * Time.delta); //delta vector
vel.add(tmp2);
}
float floorSpeedMultiplier(){
Floor on = isFlying() || hovering ? Blocks.air.asFloor() : floorOn();
return on.speedMultiplier * speedMultiplier;
}
@Override
public void update(){
Floor floor = floorOn();
if(isFlying() != wasFlying){
if(wasFlying){
if(tileOn() != null){
Fx.unitLand.at(x, y, floorOn().isLiquid ? 1f : 0.5f, tileOn().floor().mapColor);
}
}
wasFlying = isFlying();
}
if(!hovering && isGrounded()){
if((splashTimer += Mathf.dst(deltaX(), deltaY())) >= (7f + hitSize()/8f)){
floor.walkEffect.at(x, y, hitSize() / 8f, floor.mapColor);
splashTimer = 0f;
if(emitWalkSound()){
floor.walkSound.at(x, y, Mathf.random(floor.walkSoundPitchMin, floor.walkSoundPitchMax), floor.walkSoundVolume);
}
}
}
updateDrowning();
}
public void updateDrowning(){
Floor floor = drownFloor();
if(floor != null && floor.isLiquid && floor.drownTime > 0){
lastDrownFloor = floor;
drownTime += Time.delta / floor.drownTime / type.drownTimeMultiplier;
if(Mathf.chanceDelta(0.05f)){
floor.drownUpdateEffect.at(x, y, hitSize, floor.mapColor);
}
if(drownTime >= 0.999f && !net.client()){
kill();
Events.fire(new UnitDrownEvent(self()));
}
}else{
drownTime -= Time.delta / 50f;
}
drownTime = Mathf.clamp(drownTime);
}
}
@@ -59,6 +59,8 @@ abstract class HealthComp implements Entityc, Posc{
}
void damage(float amount){
if(Float.isNaN(health)) health = 0f;
health -= amount;
hitTime = 1f;
if(health <= 0 && !dead){
@@ -86,6 +88,7 @@ abstract class HealthComp implements Entityc, Posc{
void clampHealth(){
health = Math.min(health, maxHealth);
if(Float.isNaN(health)) health = 0f;
}
/** Heals by a flat amount. */
@@ -4,7 +4,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
@@ -13,12 +12,13 @@ import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
abstract class LegsComp implements Posc, Rotc, Hitboxc, Unitc{
private static final Vec2 straightVec = new Vec2();
@Import float x, y, rotation, speedMultiplier;
@@ -36,13 +36,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
@Replace
@Override
public SolidPred solidity(){
return type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid;
}
@Override
@Replace
public int pathType(){
return type.allowLegStep ? Pathfinder.costLegs : Pathfinder.costGround;
return ignoreSolids() ? null : type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid;
}
@Override
@@ -110,6 +104,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
legs[i] = l;
}
totalLength = Mathf.random(100f);
}
@Override
@@ -194,6 +189,11 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
if(type.legSplashDamage > 0 && !disarmed){
Damage.damage(team, l.base.x, l.base.y, type.legSplashRange, type.legSplashDamage * state.rules.unitDamage(team), false, true);
var tile = Vars.world.tileWorld(l.base.x, l.base.y);
if(tile != null && tile.block().unitMoveBreakable){
ConstructBlock.deconstructFinish(tile, tile.block(), self());
}
}
}
@@ -12,7 +12,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class MechComp implements Posc, Flyingc, Hitboxc, Unitc, Mechc, ElevationMovec{
abstract class MechComp implements Posc, Hitboxc, Unitc, Mechc, ElevationMovec{
@Import float x, y, hitSize;
@Import UnitType type;
@@ -68,7 +68,7 @@ abstract class MechComp implements Posc, Flyingc, Hitboxc, Unitc, Mechc, Elevati
}
}
}
return canDrown() ? floorOn() : null;
return floorOn();
}
public float walkExtend(boolean scaled){
@@ -65,7 +65,7 @@ abstract class MinerComp implements Itemsc, Posc, Teamc, Rotc, Drawc{
}
public boolean canMine(){
return type.mineSpeed > 0 && type.mineTier >= 0;
return type.mineSpeed * state.rules.unitMineSpeed(team()) > 0 && type.mineTier >= 0;
}
@Override
@@ -89,7 +89,7 @@ abstract class MinerComp implements Itemsc, Posc, Teamc, Rotc, Drawc{
mineTile = null;
mineTimer = 0f;
}else if(mining() && item != null){
mineTimer += Time.delta * type.mineSpeed;
mineTimer += Time.delta * type.mineSpeed * state.rules.unitMineSpeed(team());
if(Mathf.chance(0.06 * Time.delta)){
Fx.pulverizeSmall.at(mineTile.worldx() + Mathf.range(tilesize / 2f), mineTile.worldy() + Mathf.range(tilesize / 2f), 0f, item.color);
@@ -61,6 +61,13 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{
}
@Override
public void remove(){
for(Payload pay : payloads){
pay.remove();
}
payloads.clear();
}
public void destroy(){
if(Vars.state.rules.unitPayloadsExplode) payloads.each(Payload::destroyed);
}
@@ -10,7 +10,7 @@ import mindustry.gen.*;
* Will bounce off of other objects that are at similar elevations.
* Has mass.*/
@Component
abstract class PhysicsComp implements Velc, Hitboxc, Flyingc{
abstract class PhysicsComp implements Velc, Hitboxc{
@Import float hitSize, x, y;
@Import Vec2 vel;
@@ -65,10 +65,13 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
return team.core();
}
/** @return largest/closest core, with largest cores getting priority */
/** @return largest/closest core, with the largest cores getting priority */
@Nullable
public CoreBuild bestCore(){
return team.cores().min(Structs.comps(Structs.comparingInt(c -> -c.block.size), Structs.comparingFloat(c -> c.dst(x, y))));
var cores = team.cores();
//if someone screws up the map and adds an invalid core, prioritize the core that's supported
//if there's only one core, there are no other options
return cores.min(b -> cores.size == 1 || ((CoreBlock)b.block).unitType.supportsEnv(state.rules.env), Structs.comps(Structs.comparingInt(c -> -c.block.size), Structs.comparingFloat(c -> c.dst2(x, y))));
}
public TextureRegion icon(){
@@ -23,7 +23,7 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{
private static Puddle paramPuddle;
private static Cons<Unit> unitCons = unit -> {
if(unit.isGrounded() && !unit.hovering){
if(unit.isGrounded() && !unit.type.hovering){
unit.hitbox(rect2);
if(rect.overlaps(rect2)){
unit.apply(paramPuddle.liquid.effect, 60 * 2);
@@ -104,6 +104,10 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{
}
updateTime = 40f;
if(tile.build != null){
tile.build.puddleOn(self());
}
}
if(!headless && liquid.particleEffect != Fx.none){
@@ -0,0 +1,158 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.async.*;
import mindustry.gen.*;
import mindustry.type.*;
@Component
abstract class SegmentComp implements Posc, Rotc, Hitboxc, Unitc, Segmentc{
@Import float x, y, rotation;
@Import UnitType type;
@Import Vec2 vel;
transient @Nullable Segmentc parentSegment, childSegment, headSegment;
transient int segmentIndex;
int parentId;
public boolean isHead(){
return parentSegment == null;
}
public void addChild(Unit other){
if(other == self()) return;
if(childSegment != null){
childSegment.parentSegment(null);
}
if(other instanceof Segmentc seg){
if(seg.parentSegment() != null){
seg.parentSegment().childSegment(null);
}
childSegment = seg;
seg.parentSegment(this);
}
}
@Override
@Replace
public boolean ignoreSolids(){
return isFlying() || parentSegment != null;
}
//TODO make it phase through things.
@Override
public void update(){
if(childSegment != null && !childSegment.isValid()){
childSegment = null;
}
if(parentSegment != null && !parentSegment.isValid()){
parentSegment = null;
}
if(parentSegment == null){
segmentIndex = 0;
if(childSegment != null){
headSegment = this;
childSegment.updateSegment(this, this, 1);
}
}
}
@Replace
@Override
public boolean playerControllable(){
return type.playerControllable && isHead();
}
@Override
@Replace
public boolean shouldUpdateController(){
return isHead();
}
@Override
@Replace
public boolean moving(){
if(isHead()){
return !vel.isZero(0.01f);
}else{
return deltaLen() / Time.delta >= 0.01f;
}
}
@Override
@Replace
public int collisionLayer(){
if(parentSegment != null) return -1;
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
}
@Replace
@Override
public boolean isCommandable(){
return parentSegment == null && controller() instanceof CommandAI;
}
@Override
public void afterSync(){
checkParent();
}
@Override
public void afterReadAll(){
checkParent();
}
@Override
public void beforeWrite(){
parentId = parentSegment == null ? -1 : parentSegment.id();
}
public void checkParent(){
if(parentId != -1){
var parent = Groups.unit.getByID(parentId);
if(parent instanceof Segmentc seg){
parentSegment = seg;
seg.childSegment(this);
return;
}
parentId = -1;
}
//TODO should this unassign the parent's child too?
parentSegment = null;
}
public void updateSegment(Segmentc head, Segmentc parent, int index){
rotation = Angles.clampRange(rotation, parent.rotation(), type.segmentRotationRange);
segmentIndex = index;
headSegment = head;
float headDelta = head.deltaLen();
//TODO should depend on the head's speed.
if(headDelta > 0.001f){
rotation = Mathf.slerpDelta(rotation, parent.rotation(), type.baseRotateSpeed * Mathf.clamp(headDelta / type().speed / Time.delta));
}
Vec2 moveVec = Tmp.v1.trns(rotation + 180f, type.segmentSpacing).add(parent).sub(x, y);
float prefSpeed = type.speed * Time.delta * 9999f;
move(moveVec.limit(prefSpeed)); //TODO other segments are left behind
if(childSegment != null){
childSegment.updateSegment(head, this, index + 1);
}
}
}
@@ -45,6 +45,8 @@ abstract class ShieldComp implements Healthc, Posc{
protected void rawDamage(float amount){
boolean hadShields = shield > 0.0001f;
if(Float.isNaN(health)) health = 0f;
if(hadShields){
shieldAlpha = 1f;
}
@@ -15,7 +15,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class StatusComp implements Posc, Flyingc{
abstract class StatusComp implements Posc{
private Seq<StatusEntry> statuses = new Seq<>(4);
private transient Bits applied = new Bits(content.getBy(ContentType.status).size);
@@ -28,12 +28,12 @@ abstract class StatusComp implements Posc, Flyingc{
@Import float maxHealth;
/** Apply a status effect for 1 tick (for permanent effects) **/
void apply(StatusEffect effect){
public void apply(StatusEffect effect){
apply(effect, 1);
}
/** Adds a status effect to this unit. */
void apply(StatusEffect effect, float duration){
public void apply(StatusEffect effect, float duration){
if(effect == StatusEffects.none || effect == null || isImmune(effect)) return; //don't apply empty or immune effects
//unlock status effects regardless of whether they were applied to friendly units
@@ -62,23 +62,24 @@ abstract class StatusComp implements Posc, Flyingc{
//otherwise, no opposites found, add direct effect
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
entry.set(effect, duration);
applied.set(effect.id);
statuses.add(entry);
effect.applied(self(), duration, false);
}
}
float getDuration(StatusEffect effect){
public float getDuration(StatusEffect effect){
var entry = statuses.find(e -> e.effect == effect);
return entry == null ? 0 : entry.time;
}
void clearStatuses(){
public void clearStatuses(){
statuses.each(e -> e.effect.onRemoved(self()));
statuses.clear();
}
/** Removes a status effect. */
void unapply(StatusEffect effect){
public void unapply(StatusEffect effect){
statuses.remove(e -> {
if(e.effect == effect){
e.effect.onRemoved(self());
@@ -89,13 +90,15 @@ abstract class StatusComp implements Posc, Flyingc{
});
}
boolean isBoss(){
public boolean isBoss(){
return hasEffect(StatusEffects.boss);
}
abstract boolean isImmune(StatusEffect effect);
public boolean isImmune(StatusEffect effect){
return type.immunities.contains(effect);
}
Color statusColor(){
public Color statusColor(){
if(statuses.size == 0){
return Tmp.c1.set(Color.white);
}
@@ -125,6 +128,7 @@ abstract class StatusComp implements Posc, Flyingc{
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
entry.set(StatusEffects.dynamic, Float.POSITIVE_INFINITY);
statuses.add(entry);
applied.set(StatusEffects.dynamic.id);
entry.effect.applied(self(), entry.time, false);
return entry;
}
@@ -168,6 +172,8 @@ abstract class StatusComp implements Posc, Flyingc{
applyDynamicStatus().armorOverride = armor;
}
public abstract boolean isGrounded();
@Override
public void update(){
Floor floor = floorOn();
@@ -237,7 +243,7 @@ abstract class StatusComp implements Posc, Flyingc{
}
}
boolean hasEffect(StatusEffect effect){
public boolean hasEffect(StatusEffect effect){
return applied.get(effect.id);
}
}
+26 -18
View File
@@ -11,14 +11,15 @@ import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec{
abstract class TankComp implements Posc, Hitboxc, Unitc, ElevationMovec{
@Import float x, y, hitSize, rotation, speedMultiplier;
@Import boolean hovering, disarmed;
@Import boolean disarmed;
@Import UnitType type;
@Import Team team;
@@ -26,6 +27,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
transient float treadTime;
transient boolean walked;
transient Floor lastDeepFloor;
@Override
public void update(){
@@ -50,6 +52,9 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
}
}
lastDeepFloor = null;
boolean anyNonDeep = false;
//calculate overlapping tiles so it slows down when going "over" walls
int r = Math.max((int)(hitSize * 0.6f / tilesize), 0);
@@ -57,20 +62,33 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
Tile t = Vars.world.tileWorld(x + dx*tilesize, y + dy*tilesize);
if(t == null || t.solid()){
if(t == null || t.solid()){
solids ++;
}
//TODO should this apply to the player team(s)? currently PvE due to balancing
if(type.crushDamage > 0 && !disarmed && (walked || deltaLen() >= 0.01f) && t != null && t.build != null && t.build.team != team
if(t != null && t.floor().isDeep()){
lastDeepFloor = t.floor();
}else{
anyNonDeep = true;
}
if(type.crushDamage > 0 && !disarmed && (walked || deltaLen() >= 0.01f) && t != null
//damage radius is 1 tile smaller to prevent it from just touching walls as it passes
&& Math.max(Math.abs(dx), Math.abs(dy)) <= r - 1){
t.build.damage(team, type.crushDamage * Time.delta * t.block().crushDamageMultiplier * state.rules.unitDamage(team));
if(t.build != null && t.build.team != team){
t.build.damage(team, type.crushDamage * Time.delta * t.block().crushDamageMultiplier * state.rules.unitDamage(team));
}else if(t.block().unitMoveBreakable){
ConstructBlock.deconstructFinish(t, t.block(), self());
}
}
}
}
if(anyNonDeep){
lastDeepFloor = null;
}
lastSlowdown = Mathf.lerp(1f, type.crawlSlowdown, Mathf.clamp((float)solids / total / type.crawlSlowdownFrac));
//trigger animation only when walking manually
@@ -84,7 +102,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
@Override
@Replace
public float floorSpeedMultiplier(){
Floor on = isFlying() || hovering ? Blocks.air.asFloor() : floorOn();
Floor on = isFlying() || type.hovering ? Blocks.air.asFloor() : floorOn();
//TODO take into account extra blocks
return on.speedMultiplier * speedMultiplier * lastSlowdown;
}
@@ -92,17 +110,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
@Replace
@Override
public @Nullable Floor drownFloor(){
//tanks can only drown when all the nearby floors are deep
//TODO implement properly
if(hitSize >= 12 && canDrown()){
for(Point2 p : Geometry.d8){
Floor f = world.floorWorld(x + p.x * tilesize, y + p.y * tilesize);
if(!f.isDeep()){
return null;
}
}
}
return canDrown() ? floorOn() : null;
return canDrown() ? lastDeepFloor : null;
}
@Override
@@ -0,0 +1,39 @@
package mindustry.entities.comp;
import mindustry.annotations.Annotations.*;
import mindustry.async.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
@Component
abstract class UnderwaterMoveComp implements WaterMovec{
@Import UnitType type;
@MethodPriority(10f)
@Replace
public void draw(){
//TODO draw status effects?
Drawf.underwater(() -> {
type.draw(self());
});
}
@Override
public int collisionLayer(){
return PhysicsProcess.layerUnderwater;
}
@Override
public boolean hittable(){
return false && type.hittable(self());
}
@Override
public boolean targetable(Team targeter){
return false && type.targetable(self(), targeter);
}
}
+170 -31
View File
@@ -7,7 +7,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.async.*;
@@ -34,10 +33,12 @@ import static mindustry.Vars.*;
import static mindustry.logic.GlobalVars.*;
@Component(base = true)
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{
private static final Vec2 tmp1 = new Vec2(), tmp2 = new Vec2();
static final float warpDst = 30f;
@Import boolean hovering, dead, disarmed;
@Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier;
@Import boolean dead, disarmed;
@Import float x, y, rotation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier;
@Import Team team;
@Import int id;
@Import @Nullable Tile mineTile;
@@ -62,6 +63,48 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
private transient boolean wasPlayer;
private transient boolean wasHealed;
@SyncLocal float elevation;
private transient boolean wasFlying;
transient float drownTime;
transient float splashTimer;
transient @Nullable Floor lastDrownFloor;
public boolean checkTarget(boolean targetAir, boolean targetGround){
return (isGrounded() && targetGround) || (isFlying() && targetAir);
}
public boolean isGrounded(){
return elevation < 0.001f;
}
public boolean isFlying(){
return elevation >= 0.09f;
}
public boolean canDrown(){
return isGrounded() && type.canDrown;
}
public @Nullable Floor drownFloor(){
return floorOn();
}
public void wobble(){
x += Mathf.sin(Time.time + (id % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
y += Mathf.cos(Time.time + (id % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
}
public void moveAt(Vec2 vector, float acceleration){
Vec2 t = tmp1.set(vector); //target vector
tmp2.set(t).sub(vel).limit(acceleration * vector.len() * Time.delta); //delta vector
vel.add(tmp2);
}
public float floorSpeedMultiplier(){
Floor on = isFlying() || type.hovering ? Blocks.air.asFloor() : floorOn();
return on.speedMultiplier * speedMultiplier;
}
/** Called when this unit was unloaded from a factory or spawn point. */
public void unloaded(){
@@ -241,6 +284,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
controller instanceof CommandAI command && command.hasCommand() ? ctrlCommand :
0;
case payloadCount -> ((Object)this) instanceof Payloadc pay ? pay.payloads().size : 0;
case totalPayload -> ((Object)this) instanceof Payloadc pay ? pay.payloadUsed() / (tilesize * tilesize) : 0;
case payloadCapacity -> type.payloadCapacity / tilePayload;
case size -> hitSize / tilesize;
case color -> Color.toDoubleBits(team.color.r, team.color.g, team.color.b, 1f);
default -> Float.NaN;
@@ -265,6 +310,16 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
@Override
public double sense(Content content){
if(content == stack().item) return stack().amount;
if(content instanceof UnitType u){
return ((Object)this) instanceof Payloadc pay ?
(pay.payloads().isEmpty() ? 0 :
pay.payloads().count(p -> p instanceof UnitPayload up && up.unit.type == u)) : 0;
}
if(content instanceof Block b){
return ((Object)this) instanceof Payloadc pay ?
(pay.payloads().isEmpty() ? 0 :
pay.payloads().count(p -> p instanceof BuildPayload bp && bp.build.block == b)) : 0;
}
return Float.NaN;
}
@@ -299,7 +354,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
case flag -> flag = value;
case speed -> statusSpeed(Math.max((float)value, 0f));
case speed -> statusSpeed(Mathf.clamp((float)value, 0f, 1000f));
case armor -> statusArmor(Math.max((float)value, 0f));
}
}
@@ -340,12 +395,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
@Override
@Replace
public boolean canDrown(){
return isGrounded() && !hovering && type.canDrown;
}
@Override
@Replace
public boolean canShoot(){
@@ -408,11 +457,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
}
/** @return pathfinder path type for calculating costs. This is used for wave AI only. (TODO: remove) */
public int pathType(){
return Pathfinder.costGround;
}
public void lookAt(float angle){
rotation = Angles.moveToward(rotation, angle, type.rotateSpeed * Time.delta * speedMultiplier());
}
@@ -433,8 +477,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
return controller instanceof CommandAI;
}
public boolean canTarget(Unit other){
return other != null && other.checkTarget(type.targetAir, type.targetGround);
public boolean canTarget(Teamc other){
return other != null && (other instanceof Unit u ? u.checkTarget(type.targetAir, type.targetGround) : (other instanceof Building b && type.targetGround));
}
public CommandAI command(){
@@ -445,6 +489,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
public boolean isMissile(){
return this instanceof TimedKillc;
}
public int count(){
return team.data().countType(type);
}
@@ -459,9 +507,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
this.drag = type.drag;
this.armor = type.armor;
this.hitSize = type.hitSize;
this.hovering = type.hovering;
if(controller == null) controller(type.createController(self()));
if(mounts().length != type.weapons.size) setupWeapons(type);
if(abilities.length != type.abilities.size){
abilities = new Ability[type.abilities.size];
@@ -469,6 +515,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
abilities[i] = type.abilities.get(i).copy();
}
}
if(controller == null) controller(type.createController(self()));
}
public boolean playerControllable(){
return type.playerControllable;
}
public boolean targetable(Team targeter){
@@ -488,7 +539,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
@Override
public void afterRead(){
afterSync();
setType(this.type);
controller.unit(self());
//reset controller state
if(!(controller instanceof AIController ai && ai.keepState())){
controller(type.createController(self()));
@@ -496,7 +548,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
@Override
public void afterAllRead(){
public void afterReadAll(){
controller.afterRead(self());
}
@@ -539,11 +591,98 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
public void updateDrowning(){
Floor floor = drownFloor();
if(floor != null && floor.isLiquid && floor.drownTime > 0 && canDrown()){
lastDrownFloor = floor;
drownTime += Time.delta / floor.drownTime / type.drownTimeMultiplier;
if(Mathf.chanceDelta(0.05f)){
floor.drownUpdateEffect.at(x, y, hitSize, floor.mapColor);
}
if(drownTime >= 0.999f && !net.client()){
kill();
Events.fire(new UnitDrownEvent(self()));
}
}else{
drownTime -= Time.delta / 50f;
}
drownTime = Mathf.clamp(drownTime);
}
@Override
public void update(){
type.update(self());
//update bounds
if(type.bounded){
float bot = 0f, left = 0f, top = world.unitHeight(), right = world.unitWidth();
//TODO hidden map rules only apply to player teams? should they?
if(state.rules.limitMapArea && !team.isAI()){
bot = state.rules.limitY * tilesize;
left = state.rules.limitX * tilesize;
top = state.rules.limitHeight * tilesize + bot;
right = state.rules.limitWidth * tilesize + left;
}
if(!net.client() || isLocal()){
float dx = 0f, dy = 0f;
//repel unit out of bounds
if(x < left) dx += (-(x - left)/warpDst);
if(y < bot) dy += (-(y - bot)/warpDst);
if(x > right) dx -= (x - right)/warpDst;
if(y > top) dy -= (y - top)/warpDst;
velAddNet(dx * Time.delta, dy * Time.delta);
}
//clamp position if not flying
if(isGrounded()){
x = Mathf.clamp(x, left, right - tilesize);
y = Mathf.clamp(y, bot, top - tilesize);
}
//kill when out of bounds
if(x < -finalWorldBounds + left || y < -finalWorldBounds + bot || x >= right + finalWorldBounds || y >= top + finalWorldBounds){
kill();
}
}
//update drown/flying state
Floor floor = floorOn();
Tile tile = tileOn();
if(isFlying() != wasFlying){
if(wasFlying){
if(tile != null){
Fx.unitLand.at(x, y, floor.isLiquid ? 1f : 0.5f, tile.floor().mapColor);
}
}
wasFlying = isFlying();
}
if(!type.hovering && isGrounded() && type.emitWalkEffect){
if((splashTimer += Mathf.dst(deltaX(), deltaY())) >= (7f + hitSize()/8f)){
floor.walkEffect.at(x, y, hitSize() / 8f, floor.mapColor);
splashTimer = 0f;
if(type.emitWalkSound){
floor.walkSound.at(x, y, Mathf.random(floor.walkSoundPitchMin, floor.walkSoundPitchMax), floor.walkSoundVolume);
}
}
}
updateDrowning();
if(wasHealed && healTime <= -1f){
healTime = 1f;
}
@@ -631,8 +770,9 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
Tile tile = tileOn();
Floor floor = floorOn();
if(tile != null && tile.build != null){
tile.build.unitOnAny(self());
}
if(tile != null && isGrounded() && !type.hovering){
//unit block update
@@ -657,7 +797,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
//AI only updates on the server
if(!net.client() && !dead){
if(!net.client() && !dead && shouldUpdateController()){
controller.updateUnit();
}
@@ -672,6 +812,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
public boolean shouldUpdateController(){
return true;
}
/** @return a preview UI icon for this unit. */
public TextureRegion icon(){
return type.uiIcon;
@@ -691,7 +835,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
type.deathExplosionEffect.at(x, y, bounds() / 2f / 8f);
}
float shake = hitSize / 3f;
float shake = type.deathShake < 0 ? hitSize / 3f : type.deathShake;
if(type.createScorch){
Effect.scorch(x, y, (int)(hitSize / 5));
@@ -754,11 +898,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
type.display(self(), table);
}
@Override
public boolean isImmune(StatusEffect effect){
return type.immunities.contains(effect);
}
@Override
public void draw(){
type.draw(self());
@@ -39,6 +39,10 @@ abstract class VelComp implements Posc{
return null;
}
boolean ignoreSolids(){
return false;
}
/** @return whether this entity can move through a location*/
boolean canPass(int tileX, int tileY){
SolidPred s = solidity();
@@ -0,0 +1,38 @@
package mindustry.entities.comp;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.EntityCollisions.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
@Component
abstract class WaterCrawlComp implements Posc, Velc, Hitboxc, Unitc, Crawlc{
@Import float x, y, rotation, speedMultiplier;
@Import UnitType type;
@Replace
public SolidPred solidity(){
return isFlying() || ignoreSolids() ? null : EntityCollisions::waterSolid;
}
@Replace
public boolean onSolid(){
return EntityCollisions.waterSolid(tileX(), tileY());
}
@Replace
public float floorSpeedMultiplier(){
Floor on = isFlying() ? Blocks.air.asFloor() : floorOn();
return (on.shallow ? 1f : 1.3f) * speedMultiplier;
}
public boolean onLiquid(){
Tile tile = tileOn();
return tile != null && tile.floor().isLiquid;
}
}
@@ -4,7 +4,6 @@ import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
@@ -18,7 +17,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Unitc{
@Import float x, y, rotation, speedMultiplier;
@Import UnitType type;
@@ -38,19 +37,6 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
}
}
@Override
@Replace
public int pathType(){
return Pathfinder.costNaval;
}
//don't want obnoxious splashing
@Override
@Replace
public boolean emitWalkSound(){
return false;
}
@Override
public void add(){
tleft.clear();
@@ -59,6 +45,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
@Override
public void draw(){
//TODO: move to UnitType
float z = Draw.z();
Draw.z(Layer.debris);
@@ -76,7 +63,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
@Replace
@Override
public SolidPred solidity(){
return isFlying() ? null : EntityCollisions::waterSolid;
return isFlying() || ignoreSolids() ? null : EntityCollisions::waterSolid;
}
@Replace
@@ -34,6 +34,8 @@ public class ParticleEffect extends Effect{
public float spin = 0f;
/** Controls the initial and final sprite sizes. */
public float sizeFrom = 2f, sizeTo = 0f;
/** Controls the amount of ticks the effect waits before changing size. */
public float sizeChangeStart = 0f;
/** Whether the rotation adds with the parent */
public boolean useRotation = true;
/** Rotation offset. */
@@ -51,6 +53,7 @@ public class ParticleEffect extends Effect{
@Override
public void init(){
clip = Math.max(clip, length + Math.max(sizeFrom, sizeTo));
sizeChangeStart = Mathf.clamp(sizeChangeStart, 0f, lifetime);
if(sizeInterp == null) sizeInterp = interp;
}
@@ -62,7 +65,7 @@ public class ParticleEffect extends Effect{
int flip = casingFlip ? -Mathf.sign(e.rotation) : 1;
float rawfin = e.fin();
float fin = e.fin(interp);
float rad = sizeInterp.apply(sizeFrom, sizeTo, rawfin) * 2;
float rad = sizeInterp.apply(sizeFrom, sizeTo, Mathf.curve(rawfin, sizeChangeStart / lifetime, 1f)) * 2;
float ox = e.x + Angles.trnsx(realRotation, offsetX * flip, offsetY), oy = e.y + Angles.trnsy(realRotation, offsetX * flip, offsetY);
Draw.color(colorFrom, colorTo, fin);
@@ -8,7 +8,7 @@ import mindustry.entities.*;
/** Renders one particle effect repeatedly at specified angle intervals. */
public class RadialEffect extends Effect{
public Effect effect = Fx.none;
public float rotationSpacing = 90f, rotationOffset = 0f;
public float rotationSpacing = 90f, rotationOffset = 0f, effectRotationOffset = 0f;
public float lengthOffset = 0f;
public int amount = 4;
@@ -16,14 +16,19 @@ public class RadialEffect extends Effect{
clip = 100f;
}
public RadialEffect(Effect effect, int amount, float spacing, float lengthOffset){
public RadialEffect(Effect effect, int amount, float spacing, float lengthOffset, float effectRotationOffset){
this();
this.amount = amount;
this.effect = effect;
this.effectRotationOffset = effectRotationOffset;
this.rotationSpacing = spacing;
this.lengthOffset = lengthOffset;
}
public RadialEffect(Effect effect, int amount, float spacing, float lengthOffset){
this(effect, amount, spacing, lengthOffset, 0f);
}
@Override
public void create(float x, float y, float rotation, Color color, Object data){
if(!shouldCreate()) return;
@@ -31,7 +36,7 @@ public class RadialEffect extends Effect{
rotation += rotationOffset;
for(int i = 0; i < amount; i++){
effect.create(x + Angles.trnsx(rotation, lengthOffset), y + Angles.trnsy(rotation, lengthOffset), rotation, color, data);
effect.create(x + Angles.trnsx(rotation, lengthOffset), y + Angles.trnsy(rotation, lengthOffset), rotation + effectRotationOffset, color, data);
rotation += rotationSpacing;
}
}
@@ -18,7 +18,7 @@ public abstract class DrawPart{
public int recoilIndex = -1;
public abstract void draw(PartParams params);
public abstract void load(String name);
public void load(String name){}
public void getOutlines(Seq<TextureRegion> out){}
/** Parameters for drawing a part in draw(). */
@@ -88,7 +88,7 @@ public abstract class DrawPart{
life = p -> p.life,
/** Current unscaled value of Time.time. */
time = p -> Time.time;
float get(PartParams p);
static PartProgress constant(float value){
@@ -98,11 +98,11 @@ public abstract class DrawPart{
default float getClamp(PartParams p){
return getClamp(p, true);
}
default float getClamp(PartParams p, boolean clamp){
return clamp ? Mathf.clamp(get(p)) : get(p);
}
default PartProgress inv(){
return p -> 1f - get(p);
}
@@ -173,11 +173,11 @@ public abstract class DrawPart{
default PartProgress absin(float scl, float mag){
return p -> get(p) + Mathf.absin(scl, mag);
}
default PartProgress mod(float amount){
return p -> Mathf.mod(get(p), amount);
}
default PartProgress loop(float time){
return p -> Mathf.mod(get(p)/time, 1);
}
@@ -0,0 +1,57 @@
package mindustry.entities.part;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.graphics.*;
import static arc.math.Mathf.random;
import static arc.util.Tmp.*;
/**Spawns effects in a rectangle centered on x and y.*/
public class EffectSpawnerPart extends DrawPart{
public float x, y, width, height, rotation;
public boolean mirror = false;
public float effectChance = 0.1f, effectRot, effectRandRot;
public Effect effect = Fx.sparkShoot;
public Color effectColor = Color.white;
public boolean useProgress = true;
public PartProgress progress = PartProgress.warmup;
/**Shows the spawn rectangles in red.*/
public boolean debugDraw = false;
@Override
public void draw(PartParams params){
if(debugDraw){
for(int i = 0; i < (mirror ? 2 : 1); i++){
float sign = (i == 0 ? 1f : -1f), rot = params.rotation + (rotation * sign);
v1.set(x * sign, y).rotate(params.rotation - 90).add(params.x, params.y);
float z = Draw.z();
Draw.z(Layer.buildBeam);
Draw.color(Color.red);
Draw.rect("error", v1.x, v1.y, width, height, rot - 90f);
Draw.color();
Draw.z(z);
}
}
if(Vars.state.isPaused()) return;
for(int i = 0; i < (mirror ? 2 : 1); i++){
if(!Vars.state.isPaused() && Mathf.chanceDelta(effectChance * (useProgress ? progress.getClamp(params) : 1f))){
float sign = (i == 0 ? 1f : -1f), rot = params.rotation + (rotation * sign);
v1.set(x * sign, y).rotate(params.rotation - 90).add(params.x, params.y);
v1.add(v2.set(random(-height * 0.5f, height * 0.5f), random(-width * 0.5f, width * 0.5f)).rotate(rot));
effect.at(v1.x, v1.y, rot + (effectRot * sign) + random(-effectRandRot, effectRandRot), effectColor);
}
}
}
}
@@ -23,6 +23,8 @@ public class RegionPart extends DrawPart{
public boolean mirror = false;
/** If true, an outline is drawn under the part. */
public boolean outline = true;
/** If true, this part has an outline created 'in-place'. Currently vanilla only, do not use this! */
public boolean replaceOutline = false;
/** If true, the base + outline regions are drawn. Set to false for heat-only regions. */
public boolean drawRegion = true;
/** If true, the heat region produces light. */
@@ -38,7 +40,8 @@ public class RegionPart extends DrawPart{
public Blending blending = Blending.normal;
public float layer = -1, layerOffset = 0f, heatLayerOffset = 1f, turretHeatLayer = Layer.turretHeat;
public float outlineLayerOffset = -0.001f;
public float x, y, xScl = 1f, yScl = 1f, rotation;
//note that origin DOES NOT AFFECT child parts
public float x, y, xScl = 1f, yScl = 1f, rotation, originX, originY;
public float moveX, moveY, growX, growY, moveRot;
public float heatLightOpacity = 0.3f;
public @Nullable Color color, colorTo, mixColor, mixColorTo;
@@ -99,16 +102,21 @@ public class RegionPart extends DrawPart{
float sign = (i == 0 ? 1 : -1) * params.sideMultiplier;
Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad);
Draw.xscl *= sign;
if(originX != 0f || originY != 0f){
//correct for offset caused by origin shift
Tmp.v1.sub(Tmp.v2.set(-originX * Draw.xscl, -originY * Draw.yscl).rotate(params.rotation - 90f).add(originX * Draw.xscl, originY * Draw.yscl));
}
float
rx = params.x + Tmp.v1.x,
ry = params.y + Tmp.v1.y,
rot = mr * sign + params.rotation - 90;
Draw.xscl *= sign;
if(outline && drawRegion){
Draw.z(prevZ + outlineLayerOffset);
Draw.rect(outlines[Math.min(i, regions.length - 1)], rx, ry, rot);
rect(outlines[Math.min(i, regions.length - 1)], rx, ry, rot);
Draw.z(prevZ);
}
@@ -126,7 +134,7 @@ public class RegionPart extends DrawPart{
}
Draw.blend(blending);
Draw.rect(region, rx, ry, rot);
rect(region, rx, ry, rot);
Draw.blend();
if(color != null) Draw.color();
}
@@ -134,7 +142,7 @@ public class RegionPart extends DrawPart{
if(heat.found()){
float hprog = heatProgress.getClamp(params, clampProgress);
heatColor.write(Tmp.c1).a(hprog * heatColor.a);
Drawf.additive(heat, Tmp.c1, rx, ry, rot, turretShading ? turretHeatLayer : Draw.z() + heatLayerOffset);
Drawf.additive(heat, Tmp.c1, 1f, rx, ry, rot, turretShading ? turretHeatLayer : Draw.z() + heatLayerOffset, originX, originY);
if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog);
}
@@ -167,6 +175,11 @@ public class RegionPart extends DrawPart{
Draw.scl(preXscl, preYscl);
}
void rect(TextureRegion region, float x, float y, float rotation){
float w = region.width * region.scl() * Draw.xscl, h = region.height * region.scl() * Draw.yscl;
Draw.rect(region, x, y, w, h, w / 2f + originX * Draw.xscl, h / 2f + originY * Draw.yscl, rotation);
}
@Override
public void load(String name){
String realName = this.name == null ? name + suffix : this.name;
@@ -1,5 +1,6 @@
package mindustry.entities.pattern;
import arc.math.*;
import arc.util.*;
public class ShootAlternate extends ShootPattern{
@@ -9,6 +10,8 @@ public class ShootAlternate extends ShootPattern{
public float spread = 5f;
/** offset of barrel to start on */
public int barrelOffset = 0;
/** If true, the shoot order is flipped. */
public boolean mirror = false;
public ShootAlternate(float spread){
this.spread = spread;
@@ -17,11 +20,16 @@ public class ShootAlternate extends ShootPattern{
public ShootAlternate(){
}
@Override
public void flip(){
mirror = !mirror;
}
@Override
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
for(int i = 0; i < shots; i++){
float index = ((totalShots + i + barrelOffset) % barrels) - (barrels-1)/2f;
handler.shoot(index * spread, 0, 0f, firstShotDelay + shotDelay * i);
handler.shoot(index * spread * -Mathf.sign(mirror), 0, 0f, firstShotDelay + shotDelay * i);
if(barrelIncrementer != null) barrelIncrementer.run();
}
}
@@ -6,6 +6,20 @@ import arc.util.*;
public class ShootHelix extends ShootPattern{
public float scl = 2f, mag = 1.5f, offset = Mathf.PI * 1.25f;
public ShootHelix(float scl, float mag){
this.scl = scl;
this.mag = mag;
}
public ShootHelix(float scl, float mag, float offset){
this.scl = scl;
this.mag = mag;
this.offset = offset;
}
public ShootHelix(){
}
@Override
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
for(int i = 0; i < shots; i++){
@@ -14,6 +14,10 @@ public class ShootSpread extends ShootPattern{
public ShootSpread(){
}
public static ShootSpread circle(int points){
return new ShootSpread(points, 360f / points);
}
@Override
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
for(int i = 0; i < shots; i++){
@@ -21,15 +21,20 @@ public class AIController implements UnitController{
protected Unit unit;
protected Interval timer = new Interval(4);
protected AIController fallback;
protected @Nullable AIController fallback;
protected float noTargetTime;
/** main target that is being faced */
protected Teamc target;
protected @Nullable Teamc target;
protected @Nullable Teamc bomberTarget;
{
timer.reset(0, Mathf.random(40f));
timer.reset(1, Mathf.random(60f));
resetTimers();
}
protected void resetTimers(){
timer.reset(timerTarget, Mathf.random(40f));
timer.reset(timerTarget2, Mathf.random(60f));
}
@Override
@@ -120,17 +125,31 @@ public class AIController implements UnitController{
}
public void pathfind(int pathTarget){
int costType = unit.pathType();
pathfind(pathTarget, true);
}
public void pathfind(int pathTarget, boolean stopAtTargetTile){
int costType = unit.type.flowfieldPathType;
Tile tile = unit.tileOn();
if(tile == null) return;
Tile targetTile = pathfinder.getTargetTile(tile, pathfinder.getField(unit.team, costType, pathTarget));
Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile);
if(tile == targetTile || !unit.canPass(targetTile.x, targetTile.y)) return;
if((tile == targetTile && stopAtTargetTile) || !unit.canPass(targetTile.x, targetTile.y)) return;
//TODO: this may be buggy, figure out if it's the cause of the issue
//unit.movePref(alterPathfind(vec.set(targetTile.worldx(), targetTile.worldy()).sub(tile.worldx(), tile.worldy()).setLength(prefSpeed())));
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed()));
}
public Vec2 alterPathfind(Vec2 vec){
return vec;
}
public void targetInvalidated(){
//TODO: try this for normal units, reset the target timer
}
public void updateWeapons(){
float rotation = unit.rotation - 90;
boolean ret = retarget();
@@ -142,6 +161,9 @@ public class AIController implements UnitController{
noTargetTime += Time.delta;
if(invalid(target)){
if(target != null && !target.isAdded()){
targetInvalidated();
}
target = null;
}else{
noTargetTime = 0f;
@@ -181,6 +203,13 @@ public class AIController implements UnitController{
if(mount.target != null){
shoot = mount.target.within(mountX, mountY, wrange + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot();
if(unit.type.autoDropBombs && !shoot){
if(bomberTarget == null || !bomberTarget.isAdded() || !bomberTarget.within(unit, unit.hitSize/2f + ((Sized)bomberTarget).hitSize()/2f)){
bomberTarget = Units.closestTarget(unit.team, unit.x, unit.y, unit.hitSize, u -> !u.isFlying(), t -> true);
}
shoot = bomberTarget != null;
}
Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed);
mount.aimX = to.x;
mount.aimY = to.y;
@@ -31,8 +31,4 @@ public interface UnitController{
default void afterRead(Unit unit){
}
default boolean isBeingControlled(Unit player){
return false;
}
}
@@ -1,5 +1,7 @@
package mindustry.game;
import mindustry.*;
import mindustry.gen.*;
import mindustry.type.*;
public class CampaignRules{
@@ -8,12 +10,27 @@ public class CampaignRules{
public boolean showSpawns;
public boolean sectorInvasion;
public boolean randomWaveAI;
public boolean legacyLaunchPads;
public boolean rtsAI;
public void apply(Planet planet, Rules rules){
rules.staticFog = rules.fog = fog;
rules.showSpawns = showSpawns;
rules.randomWaveAI = randomWaveAI;
rules.objectiveTimerMultiplier = difficulty.waveTimeMultiplier;
if(planet.showRtsAIRule && rules.attackMode){
boolean swapped = rules.teams.get(rules.waveTeam).rtsAi != rtsAI;
rules.teams.get(rules.waveTeam).rtsAi = rtsAI;
rules.teams.get(rules.waveTeam).rtsMinWeight = 1.2f * difficulty.enemyHealthMultiplier;
if(swapped && Vars.state.isGame()){
Groups.unit.each(u -> {
if(u.team == rules.waveTeam && !u.isPlayer()){
u.resetController();
}
});
}
}
rules.teams.get(rules.waveTeam).blockHealthMultiplier = difficulty.enemyHealthMultiplier;
rules.teams.get(rules.waveTeam).unitHealthMultiplier = difficulty.enemyHealthMultiplier;
rules.teams.get(rules.waveTeam).unitCostMultiplier = 1f / difficulty.enemySpawnMultiplier;
+17
View File
@@ -21,7 +21,24 @@ public enum Difficulty{
this.enemyHealthMultiplier = enemyHealthMultiplier;
}
public String info(){
String res =
(enemyHealthMultiplier == 1f ? "" : Core.bundle.format("difficulty.enemyHealthMultiplier", percentStat(enemyHealthMultiplier)) + "\n") +
(enemySpawnMultiplier == 1f ? "" : Core.bundle.format("difficulty.enemySpawnMultiplier", percentStat(enemySpawnMultiplier)) + "\n") +
(waveTimeMultiplier == 1f ? "" : Core.bundle.format("difficulty.waveTimeMultiplier", percentStatNeg(waveTimeMultiplier)) + "\n");
return res.isEmpty() ? Core.bundle.get("difficulty.nomodifiers") : res;
}
public String localized(){
return Core.bundle.get("difficulty." + name());
}
static String percentStat(float val){
return ((int)(val * 100 - 100) > 0 ? "[negstat]+" : "[stat]") + (int)(val * 100 - 100) + "%[]";
}
static String percentStatNeg(float val){
return ((int)(val * 100 - 100) > 0 ? "[stat]+" : "[negstat]") + (int)(val * 100 - 100) + "%[]";
}
}

Some files were not shown because too many files have changed in this diff Show More