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:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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(){
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)){
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}};
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user