Merge branch 'master' into mod-dependencies
This commit is contained in:
@@ -35,7 +35,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
@Override
|
||||
public void setup(){
|
||||
String dataDir = OS.env("MINDUSTRY_DATA_DIR");
|
||||
String dataDir = System.getProperty("mindustry.data.dir", OS.env("MINDUSTRY_DATA_DIR"));
|
||||
if(dataDir != null){
|
||||
Core.settings.setDataDirectory(files.absolute(dataDir));
|
||||
}
|
||||
@@ -55,6 +55,9 @@ 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(NvGpuInfo.hasMemoryInfo()){
|
||||
Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024);
|
||||
}
|
||||
if(maxTextureSize < 4096) Log.warn("[GL] Your maximum texture size is below the recommended minimum of 4096. This will cause severe performance issues.");
|
||||
Log.info("[JAVA] Version: @", OS.javaVersion);
|
||||
if(Core.app.isAndroid()){
|
||||
@@ -62,7 +65,9 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
}
|
||||
long ram = Runtime.getRuntime().maxMemory();
|
||||
boolean gb = ram >= 1024 * 1024 * 1024;
|
||||
Log.info("[RAM] Available: @ @", Strings.fixed(gb ? ram / 1024f / 1024 / 1024f : ram / 1024f / 1024f, 1), gb ? "GB" : "MB");
|
||||
if(!OS.isIos){
|
||||
Log.info("[RAM] Available: @ @", Strings.fixed(gb ? ram / 1024f / 1024 / 1024f : ram / 1024f / 1024f, 1), gb ? "GB" : "MB");
|
||||
}
|
||||
|
||||
Time.setDeltaProvider(() -> {
|
||||
float result = Core.graphics.getDeltaTime() * 60f;
|
||||
|
||||
@@ -45,10 +45,10 @@ public class Vars implements Loadable{
|
||||
public static boolean loadLocales = true;
|
||||
/** Whether the logger is loaded. */
|
||||
public static boolean loadedLogger = false, loadedFileLogger = false;
|
||||
/** Whether to enable various experimental features (e.g. spawn positions for spawn groups) TODO change */
|
||||
public static boolean experimental = true;
|
||||
/** Name of current Steam player. */
|
||||
public static String steamPlayerName = "";
|
||||
/** If true, the BE server list is always used. */
|
||||
public static boolean forceBeServers = 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. */
|
||||
@@ -71,11 +71,12 @@ public class Vars implements Loadable{
|
||||
public static final String discordURL = "https://discord.gg/mindustry";
|
||||
/** URL the links to the wiki's modding guide.*/
|
||||
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/1-modding/";
|
||||
/** URL to the JSON file containing all the BE servers. Only queried in BE. */
|
||||
public static final String serverJsonBeURL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers_be.json";
|
||||
/** URL to the JSON file containing all the stable servers. */
|
||||
//TODO merge with v6 list upon release
|
||||
public static final String serverJsonURL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers_v7.json";
|
||||
/** 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"};
|
||||
/** URLs to the JSON file containing all the stable servers. */
|
||||
public static final String[] serverJsonURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_v8.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_v8.json"};
|
||||
/** URLs to the JSON files containing the list of mods. */
|
||||
public static final String[] modJsonURLs = {"https://raw.githubusercontent.com/Anuken/MindustryMods/master/mods.json", "https://cdn.jsdelivr.net/gh/anuken/mindustrymods/mods.json"};
|
||||
/** URL of the github issue report template.*/
|
||||
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?labels=bug&template=bug_report.md";
|
||||
/** list of built-in servers.*/
|
||||
@@ -94,6 +95,8 @@ public class Vars implements Loadable{
|
||||
public static final float finalWorldBounds = 250;
|
||||
/** default range for building */
|
||||
public static final float buildingRange = 220f;
|
||||
/** scaling for unit circle collider radius, based on hitbox size */
|
||||
public static final float unitCollisionRadiusScale = 0.6f;
|
||||
/** range for moving items */
|
||||
public static final float itemTransferRange = 220f;
|
||||
/** range for moving items for logic units */
|
||||
@@ -145,7 +148,7 @@ public class Vars implements Loadable{
|
||||
"modeSurvival", "commandRally", "commandAttack",
|
||||
};
|
||||
/** maximum TCP packet size */
|
||||
public static final int maxTcpSize = 900;
|
||||
public static final int maxTcpSize = 1100;
|
||||
/** default server port */
|
||||
public static final int port = 6567;
|
||||
/** multicast discovery port.*/
|
||||
@@ -170,6 +173,8 @@ public class Vars implements Loadable{
|
||||
public static boolean confirmExit = true;
|
||||
/** if true, UI is not drawn */
|
||||
public static boolean disableUI;
|
||||
/** if true, most autosaving is disabled. internal use only! */
|
||||
public static boolean disableSave;
|
||||
/** if true, game is set up in mobile mode, even on desktop. used for debugging */
|
||||
public static boolean testMobile;
|
||||
/** whether the game is running on a mobile device */
|
||||
@@ -489,7 +494,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,15 @@ public class Astar{
|
||||
private static float[] costs;
|
||||
private static byte[][] rotations;
|
||||
|
||||
public static Seq<Tile> pathfind(Tile from, Tile to, TileHueristic th, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(Tile from, Tile to, TileHeuristic th, Boolf<Tile> passable){
|
||||
return pathfind(from.x, from.y, to.x, to.y, th, manhattan, passable);
|
||||
}
|
||||
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHueristic th, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHeuristic th, Boolf<Tile> passable){
|
||||
return pathfind(startX, startY, endX, endY, th, manhattan, passable);
|
||||
}
|
||||
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHueristic th, DistanceHeuristic dh, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHeuristic th, DistanceHeuristic dh, Boolf<Tile> passable){
|
||||
Tiles tiles = world.tiles;
|
||||
|
||||
Tile start = tiles.getn(startX, startY);
|
||||
@@ -94,7 +94,7 @@ public class Astar{
|
||||
float cost(int x1, int y1, int x2, int y2);
|
||||
}
|
||||
|
||||
public interface TileHueristic{
|
||||
public interface TileHeuristic{
|
||||
float cost(Tile tile);
|
||||
|
||||
default float cost(Tile from, Tile tile){
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -58,8 +58,8 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
costNaval = (team, tile) ->
|
||||
//impassable same-team neutral block, or non-liquid
|
||||
(PathTile.solid(tile) && ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0)) || !PathTile.liquid(tile) ? impassable :
|
||||
1 +
|
||||
(PathTile.solid(tile) && ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0)) ? impassable :
|
||||
(!PathTile.liquid(tile) ? 6000 : 1) +
|
||||
//impassable synthetic enemy block
|
||||
((PathTile.team(tile) != team && PathTile.team(tile) != 0) && PathTile.solid(tile) ? wallImpassableCap : 0) +
|
||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 6 : 0);
|
||||
@@ -124,7 +124,7 @@ public class ControlPathfinder implements Runnable{
|
||||
//TODO: very dangerous usage;
|
||||
//TODO - it is accessed from the main thread
|
||||
//TODO - it is written to on the pathfinding thread
|
||||
//maps position in world in (x + y * width format) | type (bitpacked to long) to a cache of flow fields
|
||||
//maps position in world in (x + y * width format) | path type | team (bitpacked to long with FieldIndex.get) to a cache of flow fields
|
||||
LongMap<FieldCache> fields = new LongMap<>();
|
||||
//MAIN THREAD ONLY
|
||||
Seq<FieldCache> fieldList = new Seq<>(false);
|
||||
@@ -188,6 +188,7 @@ public class ControlPathfinder implements Runnable{
|
||||
final IntQueue frontier = new IntQueue();
|
||||
//maps cluster index to field weights; 0 means uninitialized
|
||||
final IntMap<int[]> fields = new IntMap<>();
|
||||
//packed (goalPos | costId | team) long key to use in the global fields map
|
||||
final long mapKey;
|
||||
|
||||
//main thread only!
|
||||
@@ -200,7 +201,7 @@ public class ControlPathfinder implements Runnable{
|
||||
this.team = team;
|
||||
this.goalPos = goalPos;
|
||||
this.costId = costId;
|
||||
this.mapKey = Pack.longInt(goalPos, costId);
|
||||
this.mapKey = FieldIndex.get(goalPos, costId, team);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,24 +233,7 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
Events.on(TileChangeEvent.class, e -> {
|
||||
|
||||
e.tile.getLinkedTiles(t -> {
|
||||
int x = t.x, y = t.y, mx = x % clusterSize, my = y % clusterSize, cx = x / clusterSize, cy = y / clusterSize, cluster = cx + cy * cwidth;
|
||||
|
||||
//is at the edge of a cluster; this means the portals may have changed.
|
||||
if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){
|
||||
|
||||
if(mx == 0) queueClusterUpdate(cx - 1, cy); //left
|
||||
if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom
|
||||
if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right
|
||||
if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top
|
||||
|
||||
queueClusterUpdate(cx, cy);
|
||||
//TODO: recompute edge clusters too.
|
||||
}else{
|
||||
//there is no need to recompute portals for block updates that are not on the edge.
|
||||
queue.post(() -> clustersToInnerUpdate.add(cluster));
|
||||
}
|
||||
});
|
||||
updateTile(e.tile);
|
||||
|
||||
//TODO: recalculate affected flow fields? or just all of them? how to reflow?
|
||||
});
|
||||
@@ -258,7 +242,7 @@ public class ControlPathfinder implements Runnable{
|
||||
Events.run(Trigger.update, () -> {
|
||||
for(var req : unitRequests.values()){
|
||||
//skipped N update -> drop it
|
||||
if(req.lastUpdateId <= state.updateId - 10){
|
||||
if(req.lastUpdateId <= state.updateId - 10 || !req.unit.isAdded()){
|
||||
req.invalidated = true;
|
||||
//concurrent modification!
|
||||
queue.post(() -> threadPathRequests.remove(req));
|
||||
@@ -358,6 +342,29 @@ public class ControlPathfinder implements Runnable{
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTile(Tile tile){
|
||||
tile.getLinkedTiles(this::updateSingleTile);
|
||||
}
|
||||
|
||||
public void updateSingleTile(Tile t){
|
||||
int x = t.x, y = t.y, mx = x % clusterSize, my = y % clusterSize, cx = x / clusterSize, cy = y / clusterSize, cluster = cx + cy * cwidth;
|
||||
|
||||
//is at the edge of a cluster; this means the portals may have changed.
|
||||
if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){
|
||||
|
||||
if(mx == 0) queueClusterUpdate(cx - 1, cy); //left
|
||||
if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom
|
||||
if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right
|
||||
if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top
|
||||
|
||||
queueClusterUpdate(cx, cy);
|
||||
//TODO: recompute edge clusters too.
|
||||
}else{
|
||||
//there is no need to recompute portals for block updates that are not on the edge.
|
||||
queue.post(() -> clustersToInnerUpdate.add(cluster));
|
||||
}
|
||||
}
|
||||
|
||||
void queueClusterUpdate(int cx, int cy){
|
||||
if(cx >= 0 && cy >= 0 && cx < cwidth && cy < cheight){
|
||||
queue.post(() -> clustersToUpdate.add(cx + cy * cwidth));
|
||||
@@ -534,7 +541,7 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
void updateInnerEdges(int team, PathCost cost, int cx, int cy, Cluster cluster){
|
||||
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
|
||||
|
||||
|
||||
usedEdges.clear();
|
||||
|
||||
//clear all connections, since portals changed, they need to be recomputed.
|
||||
@@ -548,7 +555,7 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
for(int i = 0; i < portals.size; i++){
|
||||
usedEdges.add(Point2.pack(direction, i));
|
||||
|
||||
|
||||
int
|
||||
portal = portals.items[i],
|
||||
from = Point2.x(portal), to = Point2.y(portal),
|
||||
@@ -1020,10 +1027,12 @@ public class ControlPathfinder implements Runnable{
|
||||
//no result found, bail out.
|
||||
if(nodePath == null){
|
||||
request.notFound = true;
|
||||
//stop following the old path, it's not relevant now, it's just not possible to reach the destination anymore
|
||||
request.oldCache = null;
|
||||
return;
|
||||
}
|
||||
|
||||
FieldCache cache = fields.get(Pack.longInt(goalPos, costId));
|
||||
FieldCache cache = fields.get(FieldIndex.get(goalPos, costId, team));
|
||||
//if true, extra values are added on the sides of existing field cells that face new cells.
|
||||
boolean addingFrontier = true;
|
||||
|
||||
@@ -1093,6 +1102,10 @@ public class ControlPathfinder implements Runnable{
|
||||
}
|
||||
|
||||
public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){
|
||||
if(noResultFound != null){
|
||||
noResultFound[0] = false;
|
||||
}
|
||||
|
||||
int costId = unit.type.pathCostId;
|
||||
PathCost cost = idToCost(costId);
|
||||
|
||||
@@ -1139,7 +1152,7 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
boolean any = false;
|
||||
|
||||
long fieldKey = Pack.longInt(destPos, costId);
|
||||
long fieldKey = FieldIndex.get(destPos, costId, team);
|
||||
|
||||
//use existing request if it exists.
|
||||
if(request != null && request.destination == destPos){
|
||||
@@ -1147,14 +1160,19 @@ public class ControlPathfinder implements Runnable{
|
||||
|
||||
Tile tileOn = unit.tileOn(), initialTileOn = tileOn;
|
||||
//TODO: should fields be accessible from this thread?
|
||||
FieldCache fieldCache = fields.get(fieldKey);
|
||||
FieldCache fieldCache = null;
|
||||
try{
|
||||
fieldCache = fields.get(fieldKey);
|
||||
}catch(ArrayIndexOutOfBoundsException ignored){ //TODO fix this, rare crash due to remove() elsewhere
|
||||
}
|
||||
if(fieldCache == null) fieldCache = request.oldCache;
|
||||
|
||||
if(fieldCache != null && tileOn != null){
|
||||
FieldCache old = request.oldCache;
|
||||
FieldCache targetCache = old != null ? old : fieldCache;
|
||||
boolean requeue = old == null;
|
||||
//nullify the old field to be GCed, as it cannot be relevant anymore (this path is complete)
|
||||
if(fieldCache.frontier.isEmpty() && old != null){
|
||||
if(fieldCache != request.oldCache && fieldCache.frontier.isEmpty() && old != null){
|
||||
request.oldCache = null;
|
||||
}
|
||||
|
||||
@@ -1245,7 +1263,11 @@ public class ControlPathfinder implements Runnable{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}else if(request == null){
|
||||
}else{
|
||||
//destroy the old one immediately, it's invalid now
|
||||
if(request != null){
|
||||
request.lastUpdateId = -1000;
|
||||
}
|
||||
|
||||
//queue new request.
|
||||
unitRequests.put(unit, request = new PathRequest(unit, team, costId, destPos));
|
||||
@@ -1258,9 +1280,7 @@ public class ControlPathfinder implements Runnable{
|
||||
recalculatePath(f);
|
||||
});
|
||||
|
||||
out.set(destination);
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(noResultFound != null){
|
||||
@@ -1445,7 +1465,7 @@ public class ControlPathfinder implements Runnable{
|
||||
int index = cx + cy * cwidth;
|
||||
|
||||
for(var req : threadPathRequests){
|
||||
long mapKey = Pack.longInt(req.destination, pathCost);
|
||||
long mapKey = FieldIndex.get(req.destination, pathCost, team);
|
||||
var field = fields.get(mapKey);
|
||||
if((field != null && field.fields.containsKey(index)) || req.notFound){
|
||||
invalidRequests.add(req);
|
||||
@@ -1531,7 +1551,7 @@ public class ControlPathfinder implements Runnable{
|
||||
continue;
|
||||
}
|
||||
|
||||
long mapKey = Pack.longInt(request.destination, request.costId);
|
||||
long mapKey = FieldIndex.get(request.destination, request.costId, request.team);
|
||||
|
||||
var field = fields.get(mapKey);
|
||||
|
||||
@@ -1539,7 +1559,7 @@ public class ControlPathfinder implements Runnable{
|
||||
//it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete.
|
||||
if(field.frontier.isEmpty()){
|
||||
|
||||
//remove the field, to be recalculated next update one recalculatePath is processed
|
||||
//remove the field, to be recalculated next update once recalculatePath is processed
|
||||
fields.remove(field.mapKey);
|
||||
Core.app.post(() -> fieldList.remove(field));
|
||||
|
||||
@@ -1547,6 +1567,10 @@ public class ControlPathfinder implements Runnable{
|
||||
for(var otherRequest : threadPathRequests){
|
||||
if(otherRequest.destination == request.destination){
|
||||
otherRequest.oldCache = field;
|
||||
|
||||
if(otherRequest != request){
|
||||
queue.post(() -> recalculatePath(otherRequest));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1580,6 +1604,15 @@ public class ControlPathfinder implements Runnable{
|
||||
}
|
||||
}
|
||||
|
||||
@Struct
|
||||
static class FieldIndexStruct{
|
||||
int pos;
|
||||
@StructField(8)
|
||||
int costId;
|
||||
@StructField(8)
|
||||
int team;
|
||||
}
|
||||
|
||||
@Struct
|
||||
static class IntraEdgeStruct{
|
||||
@StructField(8)
|
||||
|
||||
@@ -2,6 +2,7 @@ package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
@@ -16,6 +17,7 @@ import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.world.meta.BlockFlag.*;
|
||||
|
||||
public class Pathfinder implements Runnable{
|
||||
private static final long maxUpdate = Time.millisToNanos(8);
|
||||
@@ -37,7 +39,8 @@ public class Pathfinder implements Runnable{
|
||||
public static final int
|
||||
costGround = 0,
|
||||
costLegs = 1,
|
||||
costNaval = 2;
|
||||
costNaval = 2,
|
||||
costHover = 3;
|
||||
|
||||
public static final Seq<PathCost> costTypes = Seq.with(
|
||||
//ground
|
||||
@@ -61,7 +64,13 @@ public class Pathfinder implements Runnable{
|
||||
PathTile.health(tile) * 5 +
|
||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
|
||||
(PathTile.deep(tile) ? 0 : 1) +
|
||||
(PathTile.damages(tile) ? 35 : 0)
|
||||
(PathTile.damages(tile) ? 35 : 0),
|
||||
|
||||
//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 */
|
||||
@@ -243,6 +252,8 @@ public class Pathfinder implements Runnable{
|
||||
data.dirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
controlPath.updateTile(tile);
|
||||
}
|
||||
|
||||
/** Thread implementation. */
|
||||
@@ -452,8 +463,34 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
|
||||
public static class EnemyCoreField extends Flowfield{
|
||||
private final static BlockFlag[] randomTargets = {storage, generator, launchPad, factory, repair, battery, reactor, drill};
|
||||
private Rand rand = new Rand();
|
||||
|
||||
@Override
|
||||
protected void getPositions(IntSeq out){
|
||||
if(state.rules.randomWaveAI && team == state.rules.waveTeam){
|
||||
rand.setSeed(state.rules.waves ? state.wave : (int)(state.tick / (5400)) + hashCode());
|
||||
|
||||
//maximum amount of different target flag types they will attack
|
||||
int max = 1;
|
||||
|
||||
for(int attempt = 0; attempt < 5 && max > 0; attempt++){
|
||||
var targets = indexer.getEnemy(team, randomTargets[rand.random(randomTargets.length - 1)]);
|
||||
if(!targets.isEmpty()){
|
||||
boolean any = false;
|
||||
for(Building other : targets){
|
||||
if((other.items != null && other.items.any()) || other.status() != BlockStatus.noInput){
|
||||
out.add(other.tile.array());
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if(any){
|
||||
max --;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Building other : indexer.getEnemy(team, BlockFlag.core)){
|
||||
out.add(other.tile.array());
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ public class RtsAI{
|
||||
//other can never be destroyed | other destroys self instantly
|
||||
if(Float.isInfinite(timeDestroyOther) || Mathf.zero(timeDestroySelf)) return 0f;
|
||||
//self can never be destroyed | self destroys other instantly
|
||||
if(Float.isInfinite(timeDestroySelf) || Mathf.zero(timeDestroyOther)) return 1f;
|
||||
if(Float.isInfinite(timeDestroySelf) || Mathf.zero(timeDestroyOther)) return 100000f;
|
||||
|
||||
//examples:
|
||||
// self 10 sec / other 10 sec -> can destroy target with 100 % losses -> returns 1
|
||||
|
||||
@@ -17,7 +17,7 @@ public class UnitCommand extends MappableContent{
|
||||
@Deprecated
|
||||
public static final Seq<UnitCommand> all = new Seq<>();
|
||||
|
||||
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand;
|
||||
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand;
|
||||
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
@@ -110,5 +110,10 @@ public class UnitCommand extends MappableContent{
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loopPayloadCommand = new UnitCommand("loopPayload", "resize", Binding.unit_command_loop_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public class UnitGroup{
|
||||
public int collisionLayer;
|
||||
public volatile float[] positions, originalPositions;
|
||||
public volatile boolean valid;
|
||||
|
||||
|
||||
public void calculateFormation(Vec2 dest, int collisionLayer){
|
||||
this.collisionLayer = collisionLayer;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class UnitGroup{
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
|
||||
float rad = units.get(a).hitSize/2f;
|
||||
float rad = units.get(a).hitSize * Vars.unitCollisionRadiusScale;
|
||||
|
||||
maxDst = Math.max(maxDst, v1.dst(0f, 0f) + rad);
|
||||
totalArea += Mathf.PI * rad * rad;
|
||||
|
||||
@@ -66,12 +66,22 @@ public class WaveSpawner{
|
||||
if(group.type == null) continue;
|
||||
|
||||
int spawned = group.getSpawned(state.wave - 1);
|
||||
if(spawned == 0) continue;
|
||||
|
||||
if(state.isCampaign()){
|
||||
//when spawning a boss, round down, so 1.5x (hard) * 1 boss does not result in 2 bosses
|
||||
spawned = Math.max(1, group.effect == StatusEffects.boss ?
|
||||
(int)(spawned * state.getPlanet().campaignRules.difficulty.enemySpawnMultiplier) :
|
||||
Mathf.round(spawned * state.getPlanet().campaignRules.difficulty.enemySpawnMultiplier));
|
||||
}
|
||||
|
||||
int spawnedf = spawned;
|
||||
|
||||
if(group.type.flying){
|
||||
float spread = margin / 1.5f;
|
||||
|
||||
eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> {
|
||||
for(int i = 0; i < spawned; i++){
|
||||
for(int i = 0; i < spawnedf; i++){
|
||||
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
|
||||
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
|
||||
spawnEffect(unit);
|
||||
@@ -82,7 +92,7 @@ public class WaveSpawner{
|
||||
|
||||
eachGroundSpawn(group.spawn, (spawnX, spawnY, doShockwave) -> {
|
||||
|
||||
for(int i = 0; i < spawned; i++){
|
||||
for(int i = 0; i < spawnedf; i++){
|
||||
Tmp.v1.rnd(spread);
|
||||
|
||||
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
|
||||
@@ -153,7 +163,7 @@ public class WaveSpawner{
|
||||
|
||||
private void eachFlyerSpawn(int filterPos, Floatc2 cons){
|
||||
boolean airUseSpawns = state.rules.airUseSpawns;
|
||||
|
||||
|
||||
for(Tile tile : spawns){
|
||||
if(filterPos != -1 && filterPos != tile.pos()) continue;
|
||||
|
||||
|
||||
@@ -179,12 +179,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{
|
||||
@@ -195,7 +195,7 @@ public class BuilderAI extends AIController{
|
||||
}
|
||||
|
||||
if(!unit.type.flying){
|
||||
unit.updateBoosting(moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f);
|
||||
unit.updateBoosting(unit.type.boostWhenBuilding || moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ public class CommandAI extends AIController{
|
||||
protected static final Vec2 vecOut = new Vec2(), vecMovePos = new Vec2();
|
||||
protected static final boolean[] noFound = {false};
|
||||
protected static final UnitPayload tmpPayload = new UnitPayload(null);
|
||||
protected static final int transferStateNone = 0, transferStateLoad = 1, transferStateUnload = 2;
|
||||
|
||||
public Seq<Position> commandQueue = new Seq<>(5);
|
||||
public @Nullable Vec2 targetPos;
|
||||
public @Nullable Teamc attackTarget;
|
||||
/** Group of units that were all commanded to reach the same point.. */
|
||||
/** Group of units that were all commanded to reach the same point. */
|
||||
public @Nullable UnitGroup group;
|
||||
public int groupIndex = 0;
|
||||
/** All encountered unreachable buildings of this AI. Why a sequence? Because contains() is very rarely called on it. */
|
||||
@@ -36,6 +37,8 @@ public class CommandAI extends AIController{
|
||||
protected Vec2 lastTargetPos;
|
||||
protected boolean blockingUnit;
|
||||
protected float timeSpentBlocked;
|
||||
protected float payloadPickupCooldown;
|
||||
protected int transferState = transferStateNone;
|
||||
|
||||
/** Stance, usually related to firing mode. */
|
||||
public UnitStance stance = UnitStance.shoot;
|
||||
@@ -52,7 +55,7 @@ public class CommandAI extends AIController{
|
||||
|
||||
/** Attempts to assign a command to this unit. If not supported by the unit type, does nothing. */
|
||||
public void command(UnitCommand command){
|
||||
if(Structs.contains(unit.type.commands, command)){
|
||||
if(unit.type.commands.contains(command)){
|
||||
//clear old state.
|
||||
unit.mineTile = null;
|
||||
unit.clearBuilding();
|
||||
@@ -79,14 +82,23 @@ 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());
|
||||
}
|
||||
|
||||
//assign defaults
|
||||
if(command == null && unit.type.commands.length > 0){
|
||||
command = unit.type.defaultCommand == null ? unit.type.commands[0] : unit.type.defaultCommand;
|
||||
if(command == null && unit.type.commands.size > 0){
|
||||
command = unit.type.defaultCommand == null ? unit.type.commands.first() : unit.type.defaultCommand;
|
||||
}
|
||||
|
||||
//update command controller based on index.
|
||||
@@ -113,9 +125,26 @@ public class CommandAI extends AIController{
|
||||
attackTarget = null;
|
||||
}
|
||||
|
||||
void tryPickupUnit(Payloadc pay){
|
||||
Unit target = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u.isAI() && u != unit && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize));
|
||||
if(target != null){
|
||||
Call.pickedUnitPayload(unit, target);
|
||||
}
|
||||
}
|
||||
|
||||
@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){
|
||||
payloadPickupCooldown -= Time.delta;
|
||||
|
||||
//auto-drop everything
|
||||
if(command == UnitCommand.unloadPayloadCommand && pay.hasPayload()){
|
||||
Call.payloadDropped(unit, unit.x, unit.y);
|
||||
@@ -123,10 +152,7 @@ public class CommandAI extends AIController{
|
||||
|
||||
//try to pick up what's under it
|
||||
if(command == UnitCommand.loadUnitsCommand){
|
||||
Unit target = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u.isAI() && u != unit && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize));
|
||||
if(target != null){
|
||||
Call.pickedUnitPayload(unit, target);
|
||||
}
|
||||
tryPickupUnit(pay);
|
||||
}
|
||||
|
||||
//try to pick up a block
|
||||
@@ -155,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;
|
||||
@@ -218,8 +224,14 @@ public class CommandAI extends AIController{
|
||||
vecMovePos.add(group.positions[groupIndex * 2], group.positions[groupIndex * 2 + 1]);
|
||||
}
|
||||
|
||||
Building targetBuild = world.buildWorld(targetPos.x, targetPos.y);
|
||||
|
||||
//TODO: should the unit stop when it finds a target?
|
||||
if(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget){
|
||||
if(
|
||||
(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
|
||||
(command == UnitCommand.enterPayloadCommand && unit.within(targetPos, 4f) || (targetBuild != null && unit.within(targetBuild, targetBuild.block.size * tilesize/2f * 0.9f))) ||
|
||||
(command == UnitCommand.loopPayloadCommand && unit.within(targetPos, 10f))
|
||||
){
|
||||
move = false;
|
||||
}
|
||||
|
||||
@@ -260,6 +272,13 @@ public class CommandAI extends AIController{
|
||||
vecOut.set(vecMovePos);
|
||||
}else{
|
||||
move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
|
||||
|
||||
//TODO: what to do when there's a target and it can't be reached?
|
||||
/*
|
||||
if(noFound[0] && attackTarget != null && attackTarget.within(unit, unit.type.range * 2f)){
|
||||
move = true;
|
||||
vecOut.set(targetPos);
|
||||
}*/
|
||||
}
|
||||
|
||||
//rare case where unit must be perfectly aligned (happens with 1-tile gaps)
|
||||
@@ -321,10 +340,54 @@ public class CommandAI extends AIController{
|
||||
|
||||
void finishPath(){
|
||||
//the enter payload command never finishes until they are actually accepted
|
||||
if(command == UnitCommand.enterPayloadCommand && commandQueue.size == 0 && targetPos != null && world.buildWorld(targetPos.x, targetPos.y) != null && world.buildWorld(targetPos.x, targetPos.y).block.acceptsPayloads){
|
||||
if(command == UnitCommand.enterPayloadCommand && commandQueue.size == 0 && targetPos != null && world.buildWorld(targetPos.x, targetPos.y) != null && world.buildWorld(targetPos.x, targetPos.y).block.acceptsUnitPayloads){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!net.client() && command == UnitCommand.loopPayloadCommand && unit instanceof Payloadc pay){
|
||||
|
||||
if(transferState == transferStateNone){
|
||||
transferState = pay.hasPayload() ? transferStateUnload : transferStateLoad;
|
||||
}
|
||||
|
||||
if(payloadPickupCooldown > 0f) return;
|
||||
|
||||
if(transferState == transferStateUnload){
|
||||
//drop until there's a failure
|
||||
int prev = -1;
|
||||
while(pay.hasPayload() && prev != pay.payloads().size){
|
||||
prev = pay.payloads().size;
|
||||
Call.payloadDropped(unit, unit.x, unit.y);
|
||||
}
|
||||
|
||||
//wait for everything to unload before running code below
|
||||
if(pay.hasPayload()){
|
||||
return;
|
||||
}
|
||||
payloadPickupCooldown = 60f;
|
||||
}else if(transferState == transferStateLoad){
|
||||
//pick up units until there's a failure
|
||||
int prev = -1;
|
||||
while(prev != pay.payloads().size){
|
||||
prev = pay.payloads().size;
|
||||
tryPickupUnit(pay);
|
||||
}
|
||||
|
||||
//wait to load things before running code below
|
||||
if(!pay.hasPayload()){
|
||||
return;
|
||||
}
|
||||
payloadPickupCooldown = 60f;
|
||||
}
|
||||
|
||||
//it will never finish
|
||||
if(commandQueue.size == 0){
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
transferState = transferStateNone;
|
||||
|
||||
Vec2 prev = targetPos;
|
||||
targetPos = null;
|
||||
|
||||
@@ -336,7 +399,7 @@ public class CommandAI extends AIController{
|
||||
commandPosition(position);
|
||||
}
|
||||
|
||||
if(prev != null && stance == UnitStance.patrol){
|
||||
if(prev != null && (stance == UnitStance.patrol || command == UnitCommand.loopPayloadCommand)){
|
||||
commandQueue.add(prev.cpy());
|
||||
}
|
||||
|
||||
@@ -351,10 +414,15 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Unit unit){
|
||||
clearCommands();
|
||||
}
|
||||
|
||||
public void commandQueue(Position location){
|
||||
if(targetPos == null && attackTarget == null){
|
||||
if(location instanceof Teamc target){
|
||||
commandTarget(target, this.stopAtTarget);
|
||||
if(location instanceof Teamc t){
|
||||
commandTarget(t, this.stopAtTarget);
|
||||
}else if(location instanceof Vec2 position){
|
||||
commandPosition(position);
|
||||
}
|
||||
@@ -392,7 +460,7 @@ public class CommandAI extends AIController{
|
||||
|
||||
@Override
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
return !nearAttackTarget(x, y, range) ? super.findTarget(x, y, range, air, ground) : attackTarget;
|
||||
return !nearAttackTarget(x, y, range) ? super.findTarget(x, y, range, air, ground) : Units.isHittable(attackTarget, air, ground) ? attackTarget : null;
|
||||
}
|
||||
|
||||
public boolean nearAttackTarget(float x, float y, float range){
|
||||
@@ -445,52 +513,4 @@ public class CommandAI extends AIController{
|
||||
this.stopAtTarget = stopAtTarget;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
//TODO ひどい
|
||||
(does not work)
|
||||
|
||||
public static float cohesionScl = 0.3f;
|
||||
public static float cohesionRad = 3f, separationRad = 1.1f, separationScl = 1f, flockMult = 0.5f;
|
||||
|
||||
Vec2 calculateFlock(){
|
||||
if(local.isEmpty()) return flockVec.setZero();
|
||||
|
||||
flockVec.setZero();
|
||||
separation.setZero();
|
||||
cohesion.setZero();
|
||||
massCenter.set(unit);
|
||||
|
||||
float rad = unit.hitSize;
|
||||
float sepDst = rad * separationRad, cohDst = rad * cohesionRad;
|
||||
|
||||
//"cohesed" isn't even a word smh
|
||||
int separated = 0, cohesed = 1;
|
||||
|
||||
for(var other : local){
|
||||
float dst = other.dst(unit);
|
||||
if(dst < sepDst){
|
||||
separation.add(Tmp.v1.set(unit).sub(other).scl(1f / sepDst));
|
||||
separated ++;
|
||||
}
|
||||
|
||||
if(dst < cohDst){
|
||||
massCenter.add(other);
|
||||
cohesed ++;
|
||||
}
|
||||
}
|
||||
|
||||
if(separated > 0){
|
||||
separation.scl(1f / separated);
|
||||
flockVec.add(separation.scl(separationScl));
|
||||
}
|
||||
|
||||
if(cohesed > 1){
|
||||
massCenter.scl(1f / cohesed);
|
||||
flockVec.add(Tmp.v1.set(massCenter).sub(unit).limit(cohesionScl * unit.type.speed));
|
||||
//seek mass center?
|
||||
}
|
||||
|
||||
return flockVec;
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.world.meta.BlockFlag.*;
|
||||
|
||||
//TODO very strange idle behavior sometimes
|
||||
public class FlyingAI extends AIController{
|
||||
final static Rand rand = new Rand();
|
||||
final static BlockFlag[] randomTargets = {core, storage, generator, launchPad, factory, repair, battery, reactor, drill};
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
@@ -28,6 +31,30 @@ public class FlyingAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc targetFlag(float x, float y, BlockFlag flag, boolean enemy){
|
||||
if(state.rules.randomWaveAI){
|
||||
if(unit.team == Team.derelict) return null;
|
||||
var list = enemy ? indexer.getEnemy(unit.team, flag) : indexer.getFlagged(unit.team, flag);
|
||||
if(list.isEmpty()) return null;
|
||||
|
||||
Building closest = null;
|
||||
float cdist = 0f;
|
||||
for(Building t : list){
|
||||
if((t.items != null && t.items.any()) || t.status() != BlockStatus.noInput){
|
||||
float dst = t.dst2(x, y);
|
||||
if(closest == null || dst < cdist){
|
||||
closest = t;
|
||||
cdist = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}else{
|
||||
return super.targetFlag(x, y, flag, enemy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
var result = findMainTarget(x, y, range, air, ground);
|
||||
@@ -44,14 +71,27 @@ public class FlyingAI extends AIController{
|
||||
return core;
|
||||
}
|
||||
|
||||
for(var flag : unit.type.targetFlags){
|
||||
if(flag == null){
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
}else if(ground){
|
||||
Teamc result = targetFlag(x, y, flag, true);
|
||||
if(state.rules.randomWaveAI){
|
||||
//when there are no waves, it's just random based on the unit
|
||||
rand.setSeed(unit.type.id + (state.rules.waves ? state.wave : unit.id));
|
||||
//try a few random flags first
|
||||
for(int attempt = 0; attempt < 5; attempt++){
|
||||
Teamc result = targetFlag(x, y, randomTargets[rand.random(randomTargets.length - 1)], true);
|
||||
if(result != null) return result;
|
||||
}
|
||||
//try the closest target
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
}else{
|
||||
for(var flag : unit.type.targetFlags){
|
||||
if(flag == null){
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
}else if(ground){
|
||||
Teamc result = targetFlag(x, y, flag, true);
|
||||
if(result != null) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return core;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,7 +45,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
body.x = entity.x;
|
||||
body.y = entity.y;
|
||||
body.mass = entity.mass();
|
||||
body.radius = entity.hitSize / 2f;
|
||||
body.radius = entity.hitSize * Vars.unitCollisionRadiusScale;
|
||||
|
||||
PhysicRef ref = new PhysicRef(entity, body);
|
||||
refs.add(ref);
|
||||
|
||||
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;
|
||||
}};
|
||||
|
||||
@@ -137,6 +137,10 @@ public class ErekirTechTree{
|
||||
node(eruptionDrill, Seq.with(new OnSector(stronghold)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(largeCliffCrusher, Seq.with(new OnSector(stronghold)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -220,7 +224,9 @@ public class ErekirTechTree{
|
||||
});
|
||||
|
||||
node(heatRouter, () -> {
|
||||
node(smallHeatRedirector, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ public class Fx{
|
||||
public static final Effect
|
||||
|
||||
none = new Effect(0, 0f, e -> {}),
|
||||
|
||||
|
||||
blockCrash = new Effect(90f, e -> {
|
||||
if(!(e.data instanceof Block block)) return;
|
||||
|
||||
@@ -445,6 +445,20 @@ public class Fx{
|
||||
}
|
||||
}),
|
||||
|
||||
titanExplosionLarge = new Effect(45f, 220f, e -> {
|
||||
color(e.color);
|
||||
stroke(e.fout() * 3f);
|
||||
float circleRad = 6f + e.finpow() * 110f;
|
||||
Lines.circle(e.x, e.y, circleRad);
|
||||
|
||||
rand.setSeed(e.id);
|
||||
for(int i = 0; i < 21; i++){
|
||||
float angle = rand.random(360f);
|
||||
float lenRand = rand.random(0.5f, 1f);
|
||||
Lines.lineAngle(e.x, e.y, angle, e.foutpow() * 50f * rand.random(1f, 0.6f) + 2f, e.finpow() * 100f * lenRand + 6f);
|
||||
}
|
||||
}),
|
||||
|
||||
titanSmoke = new Effect(300f, 300f, b -> {
|
||||
float intensity = 3f;
|
||||
|
||||
@@ -465,6 +479,34 @@ public class Fx{
|
||||
}
|
||||
}),
|
||||
|
||||
titanSmokeLarge = new Effect(400f, 400f, b -> {
|
||||
float intensity = 4f;
|
||||
|
||||
color(b.color, 0.65f);
|
||||
for(int i = 0; i < 4; 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), 26f * 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
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;
|
||||
|
||||
@@ -485,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;
|
||||
|
||||
@@ -523,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;
|
||||
|
||||
@@ -530,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){
|
||||
@@ -761,7 +848,7 @@ public class Fx{
|
||||
Fill.circle(e.x + x, e.y + y, e.fout() * 2f);
|
||||
});
|
||||
}),
|
||||
|
||||
|
||||
hitLaserBlast = new Effect(12, e -> {
|
||||
color(e.color);
|
||||
stroke(e.fout() * 1.5f);
|
||||
@@ -1114,7 +1201,7 @@ public class Fx{
|
||||
stroke(2f * e.fout());
|
||||
Lines.circle(e.x, e.y, 5f * e.fout());
|
||||
}),
|
||||
|
||||
|
||||
forceShrink = new Effect(20, e -> {
|
||||
color(e.color, e.fout());
|
||||
if(renderer.animateShields){
|
||||
@@ -1387,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());
|
||||
@@ -1537,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);
|
||||
|
||||
@@ -1703,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);
|
||||
|
||||
@@ -2365,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);
|
||||
@@ -2469,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;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public class Items{
|
||||
}};
|
||||
|
||||
scrap = new Item("scrap", Color.valueOf("777777")){{
|
||||
|
||||
cost = 0.5f;
|
||||
}};
|
||||
|
||||
silicon = new Item("silicon", Color.valueOf("53565c")){{
|
||||
|
||||
@@ -65,7 +65,6 @@ public class Planets{
|
||||
clearSectorOnLose = true;
|
||||
defaultCore = Blocks.coreBastion;
|
||||
iconColor = Color.valueOf("ff9266");
|
||||
hiddenItems.addAll(Items.serpuloItems).removeAll(Items.erekirItems);
|
||||
enemyBuildSpeedMultiplier = 0.4f;
|
||||
|
||||
//TODO disallowed for now
|
||||
@@ -86,12 +85,14 @@ public class Planets{
|
||||
r.coreDestroyClear = true;
|
||||
r.onlyDepositCore = true;
|
||||
};
|
||||
campaignRuleDefaults.fog = true;
|
||||
campaignRuleDefaults.showSpawns = 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;
|
||||
@@ -99,7 +100,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;
|
||||
@@ -133,6 +134,7 @@ public class Planets{
|
||||
launchCapacityMultiplier = 0.5f;
|
||||
sectorSeed = 2;
|
||||
allowWaves = true;
|
||||
allowLegacyLaunchPads = true;
|
||||
allowWaveSimulation = true;
|
||||
allowSectorInvasion = true;
|
||||
allowLaunchSchematics = true;
|
||||
@@ -144,6 +146,7 @@ public class Planets{
|
||||
r.waveTeam = Team.crux;
|
||||
r.placeRangeCheck = false;
|
||||
r.showSpawns = false;
|
||||
r.coreDestroyClear = true;
|
||||
};
|
||||
iconColor = Color.valueOf("7d4dff");
|
||||
atmosphereColor = Color.valueOf("3c1b8f");
|
||||
@@ -151,11 +154,11 @@ public class Planets{
|
||||
atmosphereRadOut = 0.3f;
|
||||
startSector = 15;
|
||||
alwaysUnlocked = true;
|
||||
allowSelfSectorLaunch = true;
|
||||
landCloudColor = Pal.spore.cpy().a(0.5f);
|
||||
hiddenItems.addAll(Items.erekirItems).removeAll(Items.serpuloItems);
|
||||
}};
|
||||
|
||||
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;
|
||||
@@ -163,7 +166,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;
|
||||
@@ -186,13 +189,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)))
|
||||
);
|
||||
|
||||
@@ -7,10 +7,12 @@ import static mindustry.content.Planets.*;
|
||||
public class SectorPresets{
|
||||
public static SectorPreset
|
||||
groundZero,
|
||||
craters, biomassFacility, frozenForest, ruinousShores, windsweptIslands, stainedMountains, tarFields,
|
||||
fungalPass, extractionOutpost, saltFlats, overgrowth,
|
||||
craters, biomassFacility, taintedWoods, frozenForest, ruinousShores, facility32m, windsweptIslands, stainedMountains, tarFields,
|
||||
frontier, fungalPass, infestedCanyons, atolls, mycelialBastion, extractionOutpost, saltFlats, testingGrounds, overgrowth, //polarAerodrome,
|
||||
impact0078, desolateRift, nuclearComplex, planetaryTerminal,
|
||||
coastline, navalFortress,
|
||||
coastline, navalFortress, weatheredChannels, seaPort,
|
||||
|
||||
geothermalStronghold, cruxscape,
|
||||
|
||||
onset, aegis, lake, intersect, basin, atlas, split, marsh, peaks, ravine, caldera,
|
||||
stronghold, crevice, siege, crossroads, karst, origin;
|
||||
@@ -32,6 +34,11 @@ public class SectorPresets{
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
testingGrounds = new SectorPreset("testingGrounds", serpulo, 3){{
|
||||
difficulty = 7;
|
||||
captureWave = 33;
|
||||
}};
|
||||
|
||||
frozenForest = new SectorPreset("frozenForest", serpulo, 86){{
|
||||
captureWave = 15;
|
||||
difficulty = 2;
|
||||
@@ -42,6 +49,11 @@ public class SectorPresets{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
taintedWoods = new SectorPreset("taintedWoods", serpulo, 221){{
|
||||
captureWave = 33;
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
craters = new SectorPreset("craters", serpulo, 18){{
|
||||
captureWave = 20;
|
||||
difficulty = 2;
|
||||
@@ -52,6 +64,15 @@ public class SectorPresets{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
seaPort = new SectorPreset("seaPort", serpulo, 47){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
facility32m = new SectorPreset("facility32m", serpulo, 64){{
|
||||
captureWave = 25;
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
windsweptIslands = new SectorPreset("windsweptIslands", serpulo, 246){{
|
||||
captureWave = 30;
|
||||
difficulty = 4;
|
||||
@@ -66,19 +87,45 @@ public class SectorPresets{
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
//TODO: removed for now
|
||||
//polarAerodrome = new SectorPreset("polarAerodrome", serpulo, 68){{
|
||||
// difficulty = 7;
|
||||
//}};
|
||||
|
||||
coastline = new SectorPreset("coastline", serpulo, 108){{
|
||||
captureWave = 30;
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
navalFortress = new SectorPreset("navalFortress", serpulo, 216){{
|
||||
weatheredChannels = new SectorPreset("weatheredChannels", serpulo, 39){{
|
||||
captureWave = 40;
|
||||
difficulty = 9;
|
||||
}};
|
||||
|
||||
navalFortress = new SectorPreset("navalFortress", serpulo, 216){{
|
||||
difficulty = 8;
|
||||
}};
|
||||
|
||||
frontier = new SectorPreset("frontier", serpulo, 203){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
fungalPass = new SectorPreset("fungalPass", serpulo, 21){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
infestedCanyons = new SectorPreset("infestedCanyons", serpulo, 210){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
atolls = new SectorPreset("atolls", serpulo, 1){{
|
||||
difficulty = 7;
|
||||
}};
|
||||
|
||||
mycelialBastion = new SectorPreset("mycelialBastion", serpulo, 260){{
|
||||
difficulty = 8;
|
||||
}};
|
||||
|
||||
overgrowth = new SectorPreset("overgrowth", serpulo, 134){{
|
||||
difficulty = 5;
|
||||
}};
|
||||
@@ -108,6 +155,14 @@ public class SectorPresets{
|
||||
isLastSector = true;
|
||||
}};
|
||||
|
||||
geothermalStronghold = new SectorPreset("geothermalStronghold", serpulo, 264){{
|
||||
difficulty = 10;
|
||||
}};
|
||||
|
||||
cruxscape = new SectorPreset("cruxscape", serpulo, 54){{
|
||||
difficulty = 10;
|
||||
}};
|
||||
|
||||
//endregion
|
||||
//region erekir
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package mindustry.content;
|
||||
|
||||
import arc.struct.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.content.Blocks.*;
|
||||
import static mindustry.content.SectorPresets.craters;
|
||||
@@ -18,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);
|
||||
@@ -122,7 +124,7 @@ public class SerpuloTechTree{
|
||||
});
|
||||
|
||||
node(pyratiteMixer, () -> {
|
||||
node(blastMixer, () -> {
|
||||
node(blastMixer, Seq.with(new SectorComplete(facility32m)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -138,7 +140,7 @@ public class SerpuloTechTree{
|
||||
});
|
||||
});
|
||||
|
||||
node(plastaniumCompressor, Seq.with(new SectorComplete(windsweptIslands)), () -> {
|
||||
node(plastaniumCompressor, Seq.with(new SectorComplete(windsweptIslands), new OnSector(tarFields)), () -> {
|
||||
node(phaseWeaver, Seq.with(new SectorComplete(tarFields)), () -> {
|
||||
|
||||
});
|
||||
@@ -261,12 +263,21 @@ public class SerpuloTechTree{
|
||||
node(duo, () -> {
|
||||
node(copperWall, () -> {
|
||||
node(copperWallLarge, () -> {
|
||||
node(scrapWall, () -> {
|
||||
node(scrapWallLarge, () -> {
|
||||
node(scrapWallHuge, () -> {
|
||||
node(scrapWallGigantic);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(titaniumWall, () -> {
|
||||
node(titaniumWallLarge);
|
||||
|
||||
node(door, () -> {
|
||||
node(doorLarge);
|
||||
});
|
||||
|
||||
node(plastaniumWall, () -> {
|
||||
node(plastaniumWallLarge, () -> {
|
||||
|
||||
@@ -359,11 +370,12 @@ public class SerpuloTechTree{
|
||||
});
|
||||
});
|
||||
|
||||
node(crawler, () -> {
|
||||
//override research requirements to have graphite, not coal
|
||||
node(crawler, ItemStack.with(Items.silicon, 400, Items.graphite, 400), () -> {
|
||||
node(atrax, () -> {
|
||||
node(spiroct, () -> {
|
||||
node(arkyid, () -> {
|
||||
node(toxopid, () -> {
|
||||
node(toxopid, Seq.with(new SectorComplete(mycelialBastion)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -397,7 +409,7 @@ public class SerpuloTechTree{
|
||||
});
|
||||
});
|
||||
|
||||
node(navalFactory, Seq.with(new SectorComplete(ruinousShores)), () -> {
|
||||
node(navalFactory, Seq.with(new OnSector(windsweptIslands)), () -> {
|
||||
node(risso, () -> {
|
||||
node(minke, () -> {
|
||||
node(bryde, () -> {
|
||||
@@ -425,8 +437,8 @@ public class SerpuloTechTree{
|
||||
});
|
||||
|
||||
node(additiveReconstructor, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
node(multiplicativeReconstructor, () -> {
|
||||
node(exponentialReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
|
||||
node(multiplicativeReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
|
||||
node(exponentialReconstructor, () -> {
|
||||
node(tetrativeReconstructor, () -> {
|
||||
|
||||
});
|
||||
@@ -446,6 +458,16 @@ public class SerpuloTechTree{
|
||||
new Research(mender),
|
||||
new Research(combustionGenerator)
|
||||
), () -> {
|
||||
node(frontier, Seq.with(
|
||||
new Research(groundFactory),
|
||||
new Research(airFactory),
|
||||
new Research(thermalGenerator),
|
||||
new Research(dagger),
|
||||
new Research(mono)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(ruinousShores, Seq.with(
|
||||
new SectorComplete(craters),
|
||||
new Research(graphitePress),
|
||||
@@ -459,6 +481,18 @@ public class SerpuloTechTree{
|
||||
new Research(siliconSmelter),
|
||||
new Research(steamGenerator)
|
||||
), () -> {
|
||||
node(seaPort, Seq.with(
|
||||
new SectorComplete(biomassFacility),
|
||||
new Research(navalFactory),
|
||||
new Research(risso),
|
||||
new Research(retusa),
|
||||
new Research(steamGenerator),
|
||||
new Research(cultivator),
|
||||
new Research(coalCentrifuge)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(tarFields, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new Research(coalCentrifuge),
|
||||
@@ -487,28 +521,73 @@ public class SerpuloTechTree{
|
||||
new Research(risso),
|
||||
new Research(minke),
|
||||
new Research(bryde),
|
||||
new Research(sei),
|
||||
new Research(omura),
|
||||
new Research(spectre),
|
||||
new Research(launchPad),
|
||||
new Research(advancedLaunchPad),
|
||||
new Research(massDriver),
|
||||
new Research(impactReactor),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(exponentialReconstructor)
|
||||
new Research(exponentialReconstructor),
|
||||
new Research(tetrativeReconstructor)
|
||||
), () -> {
|
||||
node(geothermalStronghold, Seq.with(
|
||||
new Research(omura),
|
||||
new Research(navanax),
|
||||
new Research(eclipse),
|
||||
new Research(oct),
|
||||
new Research(reign),
|
||||
new Research(corvus),
|
||||
new Research(toxopid)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(cruxscape, Seq.with(
|
||||
new Research(omura),
|
||||
new Research(navanax),
|
||||
new Research(eclipse),
|
||||
new Research(oct),
|
||||
new Research(reign),
|
||||
new Research(corvus),
|
||||
new Research(toxopid)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(extractionOutpost, Seq.with(
|
||||
new SectorComplete(stainedMountains),
|
||||
new SectorComplete(windsweptIslands),
|
||||
new Research(groundFactory),
|
||||
new Research(nova),
|
||||
new Research(airFactory),
|
||||
new Research(mono)
|
||||
node(facility32m, Seq.with(
|
||||
new Research(pneumaticDrill),
|
||||
new SectorComplete(stainedMountains)
|
||||
), () -> {
|
||||
node(extractionOutpost, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new SectorComplete(facility32m),
|
||||
new Research(groundFactory),
|
||||
new Research(nova),
|
||||
new Research(airFactory),
|
||||
new Research(mono)
|
||||
), () -> {
|
||||
//TODO: removed for now
|
||||
/*node(polarAerodrome, Seq.with(
|
||||
new SectorComplete(fungalPass),
|
||||
new SectorComplete(desolateRift),
|
||||
new SectorComplete(overgrowth),
|
||||
new Research(multiplicativeReconstructor),
|
||||
new Research(zenith),
|
||||
new Research(swarmer),
|
||||
new Research(cyclone),
|
||||
new Research(blastDrill),
|
||||
new Research(blastDrill),
|
||||
new Research(massDriver)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
node(saltFlats, Seq.with(
|
||||
@@ -518,21 +597,41 @@ public class SerpuloTechTree{
|
||||
new Research(airFactory),
|
||||
new Research(door)
|
||||
), () -> {
|
||||
node(testingGrounds, Seq.with(
|
||||
new Research(cryofluidMixer),
|
||||
new Research(Liquids.cryofluid),
|
||||
new Research(waterExtractor),
|
||||
new Research(ripple)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(coastline, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new SectorComplete(saltFlats),
|
||||
new Research(navalFactory),
|
||||
new Research(payloadConveyor)
|
||||
), () -> {
|
||||
|
||||
node(navalFortress, Seq.with(
|
||||
new SectorComplete(coastline),
|
||||
new SectorComplete(extractionOutpost),
|
||||
new Research(coreNucleus),
|
||||
new Research(massDriver),
|
||||
new Research(oxynoe),
|
||||
new Research(minke),
|
||||
new Research(bryde),
|
||||
new Research(cyclone),
|
||||
new Research(ripple)
|
||||
), () -> {
|
||||
node(weatheredChannels, Seq.with(
|
||||
new SectorComplete(impact0078),
|
||||
new Research(bryde),
|
||||
new Research(surgeSmelter),
|
||||
new Research(overdriveProjector)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -548,7 +647,22 @@ public class SerpuloTechTree{
|
||||
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)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -559,6 +673,14 @@ 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),
|
||||
@@ -569,6 +691,16 @@ public class SerpuloTechTree{
|
||||
new Research(groundFactory),
|
||||
new Research(door)
|
||||
), () -> {
|
||||
node(infestedCanyons, Seq.with(
|
||||
new SectorComplete(fungalPass),
|
||||
new Research(navalFactory),
|
||||
new Research(risso),
|
||||
new Research(minke),
|
||||
new Research(additiveReconstructor)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(nuclearComplex, Seq.with(
|
||||
new SectorComplete(fungalPass),
|
||||
new Research(thermalGenerator),
|
||||
|
||||
@@ -61,12 +61,12 @@ public class StatusEffects{
|
||||
color = Pal.lightishGray;
|
||||
speedMultiplier = 0.4f;
|
||||
|
||||
init(() -> opposite(fast));
|
||||
init(() -> opposite(fast));
|
||||
}};
|
||||
|
||||
fast = new StatusEffect("fast"){{
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
|
||||
init(() -> opposite(slow));
|
||||
}};
|
||||
@@ -89,7 +89,7 @@ public class StatusEffects{
|
||||
opposite(burning, melting);
|
||||
});
|
||||
}};
|
||||
|
||||
|
||||
muddy = new StatusEffect("muddy"){{
|
||||
color = Color.valueOf("46382a");
|
||||
speedMultiplier = 0.94f;
|
||||
|
||||
@@ -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<>();
|
||||
@@ -139,6 +140,16 @@ public class TechTree{
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds the specified database tab to all the content in this tree. */
|
||||
public void addDatabaseTab(UnlockableContent tab){
|
||||
each(node -> node.content.databaseTabs.add(tab));
|
||||
}
|
||||
|
||||
/** Adds the specified planet to the shownPlanets of all the content in this tree. */
|
||||
public void addPlanet(Planet planet){
|
||||
each(node -> node.content.shownPlanets.add(planet));
|
||||
}
|
||||
|
||||
public Drawable icon(){
|
||||
return icon == null ? new TextureRegionDrawable(content.uiIcon) : icon;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ public class UnitTypes{
|
||||
|
||||
reign = new UnitType("reign"){{
|
||||
speed = 0.4f;
|
||||
hitSize = 26f;
|
||||
hitSize = 30f;
|
||||
rotateSpeed = 1.65f;
|
||||
health = 24000;
|
||||
armor = 18f;
|
||||
@@ -322,7 +322,7 @@ public class UnitTypes{
|
||||
speed = 0.55f;
|
||||
hitSize = 8f;
|
||||
health = 120f;
|
||||
buildSpeed = 0.35f;
|
||||
buildSpeed = 0.3f;
|
||||
armor = 1f;
|
||||
|
||||
abilities.add(new RepairFieldAbility(10f, 60f * 4, 60f));
|
||||
@@ -610,13 +610,14 @@ public class UnitTypes{
|
||||
|
||||
speed = 1f;
|
||||
hitSize = 8f;
|
||||
health = 200;
|
||||
health = 150;
|
||||
mechSideSway = 0.25f;
|
||||
range = 40f;
|
||||
ammoType = new ItemAmmoType(Items.coal);
|
||||
|
||||
weapons.add(new Weapon(){{
|
||||
shootOnDeath = true;
|
||||
targetUnderBlocks = false;
|
||||
reload = 24f;
|
||||
shootCone = 180f;
|
||||
ejectEffect = Fx.none;
|
||||
@@ -628,12 +629,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;
|
||||
@@ -1011,7 +1012,7 @@ public class UnitTypes{
|
||||
accel = 0.08f;
|
||||
drag = 0.016f;
|
||||
flying = true;
|
||||
hitSize = 10f;
|
||||
hitSize = 11f;
|
||||
targetAir = false;
|
||||
engineOffset = 7.8f;
|
||||
range = 140f;
|
||||
@@ -1041,6 +1042,7 @@ public class UnitTypes{
|
||||
|
||||
status = StatusEffects.blasted;
|
||||
statusDuration = 60f;
|
||||
damage = splashDamage * 0.5f;
|
||||
}};
|
||||
}});
|
||||
}};
|
||||
@@ -1254,6 +1256,7 @@ public class UnitTypes{
|
||||
controller = u -> new MinerAI();
|
||||
|
||||
defaultCommand = UnitCommand.mineCommand;
|
||||
allowChangeCommands = false;
|
||||
|
||||
flying = true;
|
||||
drag = 0.06f;
|
||||
@@ -1445,6 +1448,7 @@ public class UnitTypes{
|
||||
healPercent = 15f;
|
||||
splashDamage = 220f;
|
||||
splashDamageRadius = 80f;
|
||||
damage = splashDamage * 0.7f;
|
||||
}};
|
||||
}});
|
||||
}};
|
||||
@@ -1831,7 +1835,6 @@ public class UnitTypes{
|
||||
//region naval support
|
||||
retusa = new UnitType("retusa"){{
|
||||
speed = 0.9f;
|
||||
targetAir = false;
|
||||
drag = 0.14f;
|
||||
hitSize = 11f;
|
||||
health = 270;
|
||||
@@ -1861,6 +1864,23 @@ public class UnitTypes{
|
||||
}};
|
||||
}});
|
||||
|
||||
weapons.add(new Weapon("retusa-weapon"){{
|
||||
shootSound = Sounds.lasershoot;
|
||||
reload = 22f;
|
||||
x = 4.5f;
|
||||
y = -3.5f;
|
||||
rotateSpeed = 5f;
|
||||
mirror = true;
|
||||
rotate = true;
|
||||
bullet = new LaserBoltBulletType(5.2f, 12){{
|
||||
lifetime = 30f;
|
||||
healPercent = 5.5f;
|
||||
collidesTeam = true;
|
||||
backColor = Pal.heal;
|
||||
frontColor = Color.white;
|
||||
}};
|
||||
}});
|
||||
|
||||
weapons.add(new Weapon(){{
|
||||
mirror = false;
|
||||
rotate = true;
|
||||
@@ -1912,7 +1932,7 @@ public class UnitTypes{
|
||||
trailWidth = 3f;
|
||||
trailLength = 8;
|
||||
|
||||
splashDamage = 33f;
|
||||
splashDamage = 40f;
|
||||
splashDamageRadius = 32f;
|
||||
}};
|
||||
}});
|
||||
@@ -2346,7 +2366,8 @@ public class UnitTypes{
|
||||
//region core
|
||||
|
||||
alpha = new UnitType("alpha"){{
|
||||
aiController = BuilderAI::new;
|
||||
aiController = () -> new BuilderAI(true, 400f);
|
||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
||||
isEnemy = false;
|
||||
|
||||
lowAltitude = true;
|
||||
@@ -2384,7 +2405,8 @@ public class UnitTypes{
|
||||
}};
|
||||
|
||||
beta = new UnitType("beta"){{
|
||||
aiController = BuilderAI::new;
|
||||
aiController = () -> new BuilderAI(true, 400f);
|
||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
||||
isEnemy = false;
|
||||
|
||||
flying = true;
|
||||
@@ -2425,7 +2447,8 @@ public class UnitTypes{
|
||||
}};
|
||||
|
||||
gamma = new UnitType("gamma"){{
|
||||
aiController = BuilderAI::new;
|
||||
aiController = () -> new BuilderAI(true, 400f);
|
||||
controller = u -> u.team.isAI() ? aiController.get() : new CommandAI();
|
||||
isEnemy = false;
|
||||
|
||||
lowAltitude = true;
|
||||
@@ -2646,7 +2669,7 @@ public class UnitTypes{
|
||||
width = 5f;
|
||||
height = 7f;
|
||||
lifetime = 15f;
|
||||
hitSize = 4f;
|
||||
hitSize = 4f;
|
||||
pierceCap = 3;
|
||||
pierce = true;
|
||||
pierceBuilding = true;
|
||||
@@ -3524,7 +3547,7 @@ public class UnitTypes{
|
||||
trailWidth = 2.2f;
|
||||
trailLength = 7;
|
||||
trailChance = -1f;
|
||||
|
||||
|
||||
collidesAir = false;
|
||||
|
||||
despawnEffect = Fx.none;
|
||||
|
||||
@@ -27,6 +27,7 @@ import static mindustry.Vars.*;
|
||||
public class ContentLoader{
|
||||
private ObjectMap<String, MappableContent>[] contentNameMap = new ObjectMap[ContentType.all.length];
|
||||
private Seq<Content>[] contentMap = new Seq[ContentType.all.length];
|
||||
private ObjectMap<String, MappableContent> nameMap = new ObjectMap<>();
|
||||
private MappableContent[][] temporaryMapper;
|
||||
private @Nullable LoadedMod currentMod;
|
||||
private @Nullable Content lastAdded;
|
||||
@@ -81,13 +82,14 @@ public class ContentLoader{
|
||||
for(int k = 0; k < contentMap.length; k++){
|
||||
Log.debug("[@]: loaded @", ContentType.all[k].name(), contentMap[k].size);
|
||||
}
|
||||
Log.debug("Total content loaded: @", Seq.with(ContentType.all).mapInt(c -> contentMap[c.ordinal()].size).sum());
|
||||
Log.debug("Total content loaded: @", Seq.with(ContentType.all).sum(c -> contentMap[c.ordinal()].size));
|
||||
Log.debug("-------------------");
|
||||
}
|
||||
|
||||
/** Calls Content#init() on everything. Use only after all modules have been created. */
|
||||
public void init(){
|
||||
initialize(Content::init);
|
||||
initialize(Content::postInit);
|
||||
if(logicVars != null) logicVars.init();
|
||||
Events.fire(new ContentInitEvent());
|
||||
}
|
||||
@@ -187,12 +189,18 @@ public class ContentLoader{
|
||||
}
|
||||
}
|
||||
contentNameMap[content.getContentType().ordinal()].put(content.name, content);
|
||||
nameMap.put(content.name, content);
|
||||
}
|
||||
|
||||
public void setTemporaryMapper(MappableContent[][] temporaryMapper){
|
||||
this.temporaryMapper = temporaryMapper;
|
||||
}
|
||||
|
||||
/** @return the last registered content with the specified name. Note that the content loader makes no attempt to resolve name conflicts. This method can be unreliable. */
|
||||
public @Nullable MappableContent byName(String name){
|
||||
return nameMap.get(name);
|
||||
}
|
||||
|
||||
public Seq<Content>[] getContentMap(){
|
||||
return contentMap;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import mindustry.content.*;
|
||||
import mindustry.content.TechTree.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Saves.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
@@ -30,7 +30,6 @@ import mindustry.net.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
import java.io.*;
|
||||
@@ -75,6 +74,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
ui.showInfo("@mods.initfailed");
|
||||
});
|
||||
}
|
||||
checkAutoUnlocks();
|
||||
});
|
||||
|
||||
Events.on(StateChangeEvent.class, event -> {
|
||||
@@ -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();
|
||||
@@ -404,7 +408,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
ui.planet.hide();
|
||||
SaveSlot slot = sector.save;
|
||||
sector.planet.setLastSector(sector);
|
||||
if(slot != null && !clearSectors && (!sector.planet.clearSectorOnLose || sector.info.hasCore)){
|
||||
if(slot != null && !clearSectors && (!(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor) || sector.info.hasCore)){
|
||||
|
||||
try{
|
||||
boolean hadNoCore = !sector.info.hasCore;
|
||||
@@ -417,7 +421,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
//if there is no base, simulate a new game and place the right loadout at the spawn position
|
||||
if(state.rules.defaultTeam.cores().isEmpty() || hadNoCore){
|
||||
|
||||
if(sector.planet.clearSectorOnLose){
|
||||
if(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor){
|
||||
playNewSector(origin, sector, reloader);
|
||||
}else{
|
||||
//no spawn set -> delete the sector save
|
||||
@@ -441,6 +445,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
state.wave = 1;
|
||||
//set up default wave time
|
||||
state.wavetime = state.rules.initialWaveSpacing <= 0f ? (state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier)) : state.rules.initialWaveSpacing;
|
||||
state.wavetime *= sector.planet.campaignRules.difficulty.waveTimeMultiplier;
|
||||
//reset captured state
|
||||
sector.info.wasCaptured = false;
|
||||
|
||||
@@ -457,7 +462,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);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class GameState{
|
||||
}
|
||||
|
||||
public @Nullable Planet getPlanet(){
|
||||
return rules.sector != null ? rules.sector.planet : null;
|
||||
return rules.sector != null ? rules.sector.planet : rules.planet;
|
||||
}
|
||||
|
||||
public boolean isEditor(){
|
||||
|
||||
@@ -92,7 +92,7 @@ public class Logic implements ApplicationListener{
|
||||
if(wavesPassed > 0){
|
||||
//simulate wave counter moving forward
|
||||
state.wave += wavesPassed;
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
state.wavetime = state.rules.waveSpacing * state.getPlanet().campaignRules.difficulty.waveTimeMultiplier;
|
||||
|
||||
SectorDamage.applyCalculatedDamage();
|
||||
}
|
||||
@@ -131,6 +131,7 @@ public class Logic implements ApplicationListener{
|
||||
//enable building AI on campaign unless the preset disables it
|
||||
|
||||
state.rules.coreIncinerates = true;
|
||||
state.rules.allowEditWorldProcessors = false;
|
||||
state.rules.waveTeam.rules().infiniteResources = true;
|
||||
state.rules.waveTeam.rules().buildSpeedMultiplier *= state.getPlanet().enemyBuildSpeedMultiplier;
|
||||
|
||||
@@ -140,10 +141,6 @@ public class Logic implements ApplicationListener{
|
||||
core.items.set(item, core.block.itemCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
//set up hidden items
|
||||
state.rules.hiddenBuildItems.clear();
|
||||
state.rules.hiddenBuildItems.addAll(state.rules.sector.planet.hiddenItems);
|
||||
}
|
||||
|
||||
//save settings
|
||||
@@ -213,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();
|
||||
}
|
||||
@@ -225,7 +221,7 @@ public class Logic implements ApplicationListener{
|
||||
public void play(){
|
||||
state.set(State.playing);
|
||||
//grace period of 2x wave time before game starts
|
||||
state.wavetime = state.rules.initialWaveSpacing <= 0 ? state.rules.waveSpacing * 2 : state.rules.initialWaveSpacing;
|
||||
state.wavetime = (state.rules.initialWaveSpacing <= 0 ? state.rules.waveSpacing * 2 : state.rules.initialWaveSpacing) * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f);;
|
||||
Events.fire(new PlayEvent());
|
||||
|
||||
//add starting items
|
||||
@@ -274,7 +270,7 @@ public class Logic implements ApplicationListener{
|
||||
public void runWave(){
|
||||
spawner.spawnEnemies();
|
||||
state.wave++;
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
state.wavetime = state.rules.waveSpacing * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f);
|
||||
|
||||
Events.fire(new WaveEvent());
|
||||
}
|
||||
@@ -398,8 +394,8 @@ public class Logic implements ApplicationListener{
|
||||
public static void researched(Content content){
|
||||
if(!(content instanceof UnlockableContent u)) return;
|
||||
|
||||
boolean was = u.unlockedNow();
|
||||
state.rules.researched.add(u.name);
|
||||
boolean was = u.unlockedNowHost();
|
||||
state.rules.researched.add(u);
|
||||
|
||||
if(!was){
|
||||
Events.fire(new UnlockEvent(u));
|
||||
@@ -409,7 +405,9 @@ 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();
|
||||
}
|
||||
|
||||
@@ -431,6 +429,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 ++;
|
||||
@@ -490,6 +490,8 @@ public class Logic implements ApplicationListener{
|
||||
Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity));
|
||||
|
||||
Groups.update();
|
||||
|
||||
Events.fire(Trigger.afterGameUpdate);
|
||||
}
|
||||
|
||||
if(runStateCheck){
|
||||
|
||||
@@ -10,6 +10,7 @@ import arc.util.*;
|
||||
import arc.util.CommandHandler.*;
|
||||
import arc.util.io.*;
|
||||
import arc.util.serialization.*;
|
||||
import arc.util.serialization.JsonValue.*;
|
||||
import mindustry.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.core.GameState.*;
|
||||
@@ -18,6 +19,7 @@ import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.net.Administration.*;
|
||||
import mindustry.net.*;
|
||||
@@ -32,10 +34,12 @@ 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;
|
||||
private static final Reads dataReads = new Reads(null);
|
||||
private static final JsonValue tmpJsonMap = new JsonValue(ValueType.object);
|
||||
|
||||
private long ping;
|
||||
private Interval timer = new Interval(5);
|
||||
@@ -47,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;
|
||||
|
||||
@@ -57,6 +63,8 @@ public class NetClient implements ApplicationListener{
|
||||
private DataInputStream dataStream = new DataInputStream(byteStream);
|
||||
/** Packet handlers for custom types of messages. */
|
||||
private ObjectMap<String, Seq<Cons<String>>> customPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for custom types of messages, in binary. */
|
||||
private ObjectMap<String, Seq<Cons<byte[]>>> customBinaryPacketHandlers = new ObjectMap<>();
|
||||
|
||||
public NetClient(){
|
||||
|
||||
@@ -147,10 +155,34 @@ public class NetClient implements ApplicationListener{
|
||||
return customPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addBinaryPacketHandler(String type, Cons<byte[]> handler){
|
||||
customBinaryPacketHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
|
||||
public Seq<Cons<byte[]>> getBinaryPacketHandlers(String type){
|
||||
return customBinaryPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both)
|
||||
public static void clientBinaryPacketReliable(String type, byte[] contents){
|
||||
var arr = netClient.customBinaryPacketHandlers.get(type);
|
||||
if(arr != null){
|
||||
for(var c : arr){
|
||||
c.get(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void clientBinaryPacketUnreliable(String type, byte[] contents){
|
||||
clientBinaryPacketReliable(type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both)
|
||||
public static void clientPacketReliable(String type, String contents){
|
||||
if(netClient.customPacketHandlers.containsKey(type)){
|
||||
for(Cons<String> c : netClient.customPacketHandlers.get(type)){
|
||||
var arr = netClient.customPacketHandlers.get(type);
|
||||
if(arr != null){
|
||||
for(Cons<String> c : arr){
|
||||
c.get(contents);
|
||||
}
|
||||
}
|
||||
@@ -290,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);
|
||||
}
|
||||
@@ -340,6 +372,18 @@ public class NetClient implements ApplicationListener{
|
||||
state.rules = rules;
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setRule(String rule, String jsonData){
|
||||
try{
|
||||
//readField searches for the specified value, so create a fake parent for it.
|
||||
tmpJsonMap.child = null;
|
||||
tmpJsonMap.addChild(rule, new JsonReader().parse(jsonData));
|
||||
JsonIO.json.readField(state.rules, rule, tmpJsonMap);
|
||||
}catch(Throwable error){
|
||||
Log.err("Failed to read rule", error);
|
||||
}
|
||||
}
|
||||
|
||||
//NOTE: avoid using this, runs into packet/buffer size limitations
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setObjectives(MapObjectives executor){
|
||||
@@ -437,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;
|
||||
|
||||
@@ -534,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
|
||||
@@ -571,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(){
|
||||
@@ -581,6 +638,7 @@ public class NetClient implements ApplicationListener{
|
||||
quietReset = false;
|
||||
quiet = false;
|
||||
lastSent = 0;
|
||||
lastSnapshotTimestamp = 0;
|
||||
|
||||
Groups.clear();
|
||||
ui.chatfrag.clearMessages();
|
||||
|
||||
@@ -117,6 +117,8 @@ public class NetServer implements ApplicationListener{
|
||||
private DataOutputStream dataStream = new DataOutputStream(syncStream);
|
||||
/** Packet handlers for custom types of messages. */
|
||||
private ObjectMap<String, Seq<Cons2<Player, String>>> customPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for custom types of messages - binary version. */
|
||||
private ObjectMap<String, Seq<Cons2<Player, byte[]>>> customBinaryPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for logic client data */
|
||||
private ObjectMap<String, Seq<Cons2<Player, Object>>> logicClientDataHandlers = new ObjectMap<>();
|
||||
|
||||
@@ -423,7 +425,7 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
});
|
||||
|
||||
clientCommands.<Player>register("vote", "<y/n/c>", "Vote to kick the current player. Admin can cancel the voting with 'c'.", (arg, player) -> {
|
||||
clientCommands.<Player>register("vote", "<y/n/c>", "Vote to kick the current player. Admins can cancel the voting with 'c'.", (arg, player) -> {
|
||||
if(currentlyKicking == null){
|
||||
player.sendMessage("[scarlet]Nobody is being voted on.");
|
||||
}else{
|
||||
@@ -517,6 +519,14 @@ public class NetServer implements ApplicationListener{
|
||||
return customPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addBinaryPacketHandler(String type, Cons2<Player, byte[]> handler){
|
||||
customBinaryPacketHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
|
||||
public Seq<Cons2<Player, byte[]>> getBinaryPacketHandlers(String type){
|
||||
return customBinaryPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addLogicDataHandler(String type, Cons2<Player, Object> handler){
|
||||
logicClientDataHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
@@ -589,6 +599,20 @@ public class NetServer implements ApplicationListener{
|
||||
serverPacketReliable(player, type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void serverBinaryPacketReliable(Player player, String type, byte[] contents){
|
||||
if(netServer.customPacketHandlers.containsKey(type)){
|
||||
for(var c : netServer.customBinaryPacketHandlers.get(type)){
|
||||
c.get(player, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, unreliable = true)
|
||||
public static void serverBinaryPacketUnreliable(Player player, String type, byte[] contents){
|
||||
serverBinaryPacketReliable(player, type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void clientLogicDataReliable(Player player, String channel, Object value){
|
||||
Seq<Cons2<Player, Object>> handlers = netServer.logicClientDataHandlers.get(channel);
|
||||
@@ -608,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,
|
||||
@@ -674,7 +698,7 @@ public class NetServer implements ApplicationListener{
|
||||
//auto-skip done requests
|
||||
if(req.breaking && tile.block() == Blocks.air){
|
||||
continue;
|
||||
}else if(!req.breaking && tile.block() == req.block && (!req.block.rotate || (tile.build != null && tile.build.rotation == req.rotation))){
|
||||
}else if(!req.breaking && tile.block() == req.block && tile.team() != Team.derelict && (!req.block.rotate || (tile.build != null && tile.build.rotation == req.rotation))){
|
||||
continue;
|
||||
}else if(con.rejectedRequests.contains(r -> r.breaking == req.breaking && r.x == req.x && r.y == req.y)){ //check if request was recently rejected, and skip it if so
|
||||
continue;
|
||||
@@ -791,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{
|
||||
@@ -806,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;
|
||||
|
||||
@@ -1058,7 +1082,7 @@ public class NetServer implements ApplicationListener{
|
||||
try{
|
||||
writeEntitySnapshot(player);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
Log.err(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -51,8 +50,7 @@ public class Renderer implements ApplicationListener{
|
||||
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 +59,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
|
||||
@@ -162,6 +158,7 @@ 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");
|
||||
@@ -172,21 +169,21 @@ public class Renderer implements ApplicationListener{
|
||||
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;
|
||||
@@ -339,6 +336,8 @@ public class Renderer implements ApplicationListener{
|
||||
Draw.draw(Layer.effect + 0.02f, bloom::render);
|
||||
}
|
||||
|
||||
control.input.drawCommanded();
|
||||
|
||||
Draw.draw(Layer.plans, overlays::drawBottom);
|
||||
|
||||
if(animateShields && Shaders.shield != null){
|
||||
@@ -376,9 +375,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();
|
||||
@@ -502,65 +506,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(){
|
||||
|
||||
@@ -628,6 +628,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
int option = 0;
|
||||
for(var optionsRow : options){
|
||||
if(optionsRow.length == 0) continue;
|
||||
Table buttonRow = table.row().table().get().row();
|
||||
int fullWidth = 400 - (optionsRow.length - 1) * 8; // adjust to count padding as well
|
||||
int width = fullWidth / optionsRow.length;
|
||||
|
||||
@@ -12,6 +12,8 @@ public class Version{
|
||||
public static String type = "unknown";
|
||||
/** Build modifier, e.g. 'alpha' or 'release' */
|
||||
public static String modifier = "unknown";
|
||||
/** Git commit hash (short) */
|
||||
public static String commitHash = "unknown";
|
||||
/** Number specifying the major version, e.g. '4' */
|
||||
public static int number;
|
||||
/** Build number, e.g. '43'. set to '-1' for custom builds. */
|
||||
@@ -32,6 +34,7 @@ public class Version{
|
||||
type = map.get("type");
|
||||
number = Integer.parseInt(map.get("number", "4"));
|
||||
modifier = map.get("modifier");
|
||||
commitHash = map.get("commitHash");
|
||||
if(map.get("build").contains(".")){
|
||||
String[] split = map.get("build").split("\\.");
|
||||
try{
|
||||
@@ -73,6 +76,6 @@ public class Version{
|
||||
if(build == -1){
|
||||
return "custom build";
|
||||
}
|
||||
return (type.equals("official") ? modifier : type) + " build " + build + (revision == 0 ? "" : "." + revision);
|
||||
return (type.equals("official") ? modifier : type) + " build " + build + (revision == 0 ? "" : "." + revision) + (commitHash.equals("unknown") ? "" : " (" + commitHash + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,8 +321,6 @@ public class World{
|
||||
state.rules.cloudColor = sector.planet.landCloudColor;
|
||||
state.rules.env = sector.planet.defaultEnv;
|
||||
state.rules.planet = sector.planet;
|
||||
state.rules.hiddenBuildItems.clear();
|
||||
state.rules.hiddenBuildItems.addAll(sector.planet.hiddenItems);
|
||||
sector.planet.applyRules(state.rules);
|
||||
sector.info.resources = content.toSeq();
|
||||
sector.info.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
|
||||
|
||||
@@ -25,6 +25,9 @@ public abstract class Content implements Comparable<Content>{
|
||||
/** Called after all content and modules are created. Do not use to load regions or texture data! */
|
||||
public void init(){}
|
||||
|
||||
/** Called after init(). */
|
||||
public void postInit(){}
|
||||
|
||||
/**
|
||||
* Called after all content is created, only on non-headless versions.
|
||||
* Use for loading regions or other image data.
|
||||
|
||||
@@ -9,6 +9,7 @@ import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.content.TechTree.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.graphics.*;
|
||||
@@ -31,12 +32,12 @@ 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;
|
||||
/** Special logic icon ID. */
|
||||
public int iconId = 0;
|
||||
/** How big the content appears in certain selection menus */
|
||||
public float selectionSize = 24f;
|
||||
/** Icon of the content to use in UI. */
|
||||
@@ -45,11 +46,24 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
public TextureRegion fullIcon;
|
||||
/** Override for the full icon. Useful for mod content with duplicate icons. Overrides any other full icon.*/
|
||||
public String fullOverride = "";
|
||||
/** If true, this content will appear in all database tabs. */
|
||||
public boolean allDatabaseTabs = false;
|
||||
/**
|
||||
* Planets that this content is made for. If empty, a planet is decided based on item requirements.
|
||||
* Currently, this is only meaningful for blocks.
|
||||
* */
|
||||
public ObjectSet<Planet> shownPlanets = new ObjectSet<>();
|
||||
/**
|
||||
* Content - usually a planet - that dictates which database tab(s) this content will appear in.
|
||||
* If nothing is defined, it will use the values in shownPlanets.
|
||||
* If shownPlanets is also empty, it will use Serpulo as the "default" tab.
|
||||
* */
|
||||
public ObjectSet<UnlockableContent> databaseTabs = new ObjectSet<>();
|
||||
/** The tech tree node for this content, if applicable. Null if not part of a tech tree. */
|
||||
public @Nullable TechNode techNode;
|
||||
/** Tech nodes for all trees that this content is part of. */
|
||||
public Seq<TechNode> techNodes = new Seq<>();
|
||||
/** Unlock state. Loaded from settings. Do not modify outside of the constructor. */
|
||||
/** Unlock state. Loaded from settings. Do not modify outside the constructor. */
|
||||
protected boolean unlocked;
|
||||
|
||||
public UnlockableContent(String name){
|
||||
@@ -61,6 +75,13 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
this.unlocked = Core.settings != null && Core.settings.getBool(this.name + "-unlocked", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(){
|
||||
super.postInit();
|
||||
|
||||
databaseTabs.addAll(shownPlanets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadIcon(){
|
||||
fullIcon =
|
||||
@@ -74,6 +95,10 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon);
|
||||
}
|
||||
|
||||
public boolean isOnPlanet(@Nullable Planet planet){
|
||||
return planet == null || planet == Planets.sun || shownPlanets.isEmpty() || shownPlanets.contains(planet);
|
||||
}
|
||||
|
||||
public int getLogicId(){
|
||||
return logicVars.lookupLogicId(this);
|
||||
}
|
||||
@@ -200,15 +225,24 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
}
|
||||
|
||||
public boolean unlockedNowHost(){
|
||||
if(!state.isCampaign()) return true;
|
||||
return !state.isCampaign() || unlockedHost();
|
||||
}
|
||||
|
||||
/** @return in multiplayer, whether this is unlocked for the host player, otherwise, whether it is unlocked for the local player (same as unlocked()) */
|
||||
public boolean unlockedHost(){
|
||||
return net != null && net.client() ?
|
||||
alwaysUnlocked || state.rules.researched.contains(name) :
|
||||
alwaysUnlocked || state.rules.researched.contains(this) :
|
||||
unlocked || alwaysUnlocked;
|
||||
}
|
||||
|
||||
/** @return whether this content is unlocked, or the player is in a custom (non-campaign) game. */
|
||||
public boolean unlockedNow(){
|
||||
return unlocked() || !state.isCampaign();
|
||||
}
|
||||
|
||||
public boolean unlocked(){
|
||||
return net != null && net.client() ?
|
||||
alwaysUnlocked || unlocked || state.rules.researched.contains(name) :
|
||||
alwaysUnlocked || unlocked || state.rules.researched.contains(this) :
|
||||
unlocked || alwaysUnlocked;
|
||||
}
|
||||
|
||||
@@ -220,11 +254,6 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
}
|
||||
}
|
||||
|
||||
/** @return whether this content is unlocked, or the player is in a custom (non-campaign) game. */
|
||||
public boolean unlockedNow(){
|
||||
return unlocked() || !state.isCampaign();
|
||||
}
|
||||
|
||||
public boolean locked(){
|
||||
return !unlocked();
|
||||
}
|
||||
|
||||
210
core/src/mindustry/editor/BannedContentDialog.java
Normal file
210
core/src/mindustry/editor/BannedContentDialog.java
Normal file
@@ -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;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,21 +172,18 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
menu.cont.row();
|
||||
}
|
||||
|
||||
//wip feature
|
||||
if(experimental){
|
||||
menu.cont.button("@editor.sectorgenerate", Icon.terrain, () -> {
|
||||
menu.hide();
|
||||
sectorGenDialog.show();
|
||||
}).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
menu.cont.row();
|
||||
}
|
||||
menu.cont.button("@editor.sectorgenerate", Icon.terrain, () -> {
|
||||
menu.hide();
|
||||
sectorGenDialog.show();
|
||||
}).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.button("@quit", Icon.exit, () -> {
|
||||
tryExit();
|
||||
menu.hide();
|
||||
}).padTop(!steam && !experimental ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
}).padTop(1).size(swidth * 2f + 10, 60f);
|
||||
|
||||
resizeDialog = new MapResizeDialog((width, height, shiftX, shiftY) -> {
|
||||
if(!(editor.width() == width && editor.height() == height && shiftX == 0 && shiftY == 0)){
|
||||
@@ -271,6 +268,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
));
|
||||
world.endMapLoad();
|
||||
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
|
||||
Core.camera.position.set(player);
|
||||
player.clearUnit();
|
||||
|
||||
for(var unit : Groups.unit){
|
||||
@@ -695,28 +693,6 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
editor.undo();
|
||||
}
|
||||
|
||||
//more undocumented features, fantastic
|
||||
if(Core.input.keyTap(KeyCode.t)){
|
||||
|
||||
//clears all 'decoration' from the map
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
Tile tile = editor.tile(x, y);
|
||||
if(tile.block().breakable && tile.block() instanceof Prop){
|
||||
tile.setBlock(Blocks.air);
|
||||
editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
|
||||
if(tile.overlay() != Blocks.air && tile.overlay() != Blocks.spawn){
|
||||
tile.setOverlay(Blocks.air);
|
||||
editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.flushOp();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.y)){
|
||||
editor.redo();
|
||||
}
|
||||
@@ -737,7 +713,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
private void addBlockSelection(Table cont){
|
||||
blockSelection = new Table();
|
||||
pane = new ScrollPane(blockSelection);
|
||||
pane = new ScrollPane(blockSelection, Styles.smallPane);
|
||||
pane.setFadeScrollBars(false);
|
||||
pane.setOverscroll(true, false);
|
||||
pane.exited(() -> {
|
||||
@@ -754,7 +730,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
cont.row();
|
||||
cont.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
|
||||
cont.row();
|
||||
cont.add(pane).expandY().top().left();
|
||||
cont.add(pane).expandY().growX().top().left();
|
||||
|
||||
rebuildBlockSelection("");
|
||||
}
|
||||
@@ -784,7 +760,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|| (!searchText.isEmpty() && !block.localizedName.toLowerCase().contains(searchText.toLowerCase()))
|
||||
) continue;
|
||||
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.squareTogglei);
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
|
||||
button.getStyle().imageUp = new TextureRegionDrawable(region);
|
||||
button.clicked(() -> editor.drawBlock = block);
|
||||
button.resizeImage(8 * 4f);
|
||||
@@ -793,7 +769,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
if(i == 0) editor.drawBlock = block;
|
||||
|
||||
if(++i % 4 == 0){
|
||||
if(++i % 6 == 0){
|
||||
blockSelection.row();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ public class MapObjectivesCanvas extends WidgetGroup{
|
||||
() -> obj,
|
||||
res -> {}
|
||||
);
|
||||
}).width(400f).fillY()).grow();
|
||||
}).width(Math.min(Core.graphics.getWidth() * 0.95f / Scl.scl(1f) - Scl.scl(20f), 700f)).fillY()).grow();
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.geom.*;
|
||||
@@ -44,7 +45,7 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
name(cont, name, remover, indexer);
|
||||
|
||||
if(field != null && field.isAnnotationPresent(Multiline.class)){
|
||||
cont.area(get.get(), set).height(85f).growX();
|
||||
cont.area(get.get(), set).height(100f).growX();
|
||||
}else{
|
||||
cont.field(get.get(), set).growX();
|
||||
}
|
||||
@@ -465,10 +466,42 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
buttons.defaults().size(160f, 64f).pad(2f);
|
||||
buttons.button("@back", Icon.left, MapObjectivesDialog.this::hide);
|
||||
buttons.button("@add", Icon.add, () -> getProvider(MapObjective.class).get(new TypeInfo(MapObjective.class), canvas::query));
|
||||
buttons.button("@waves.edit", Icon.edit, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@waves.edit");
|
||||
dialog.addCloseButton();
|
||||
dialog.setFillParent(false);
|
||||
dialog.cont.table(Tex.button, t -> {
|
||||
var style = Styles.cleart;
|
||||
t.defaults().size(280f, 64f).pad(2f);
|
||||
|
||||
t.button("@waves.copy", Icon.copy, style, () -> {
|
||||
ui.showInfoFade("@copied");
|
||||
Core.app.setClipboardText(JsonIO.write(new MapObjectives(canvas.objectives)));
|
||||
dialog.hide();
|
||||
}).disabled(b -> canvas.objectives.isEmpty()).marginLeft(12f).row();
|
||||
|
||||
t.button("@waves.load", Icon.download, style, () -> {
|
||||
try{
|
||||
rebuildObjectives(new Seq<>(JsonIO.read(MapObjectives.class, Core.app.getClipboardText()).all));
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showErrorMessage("@waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("[")).marginLeft(12f).row();
|
||||
|
||||
t.button("@clear", Icon.none, style, () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
|
||||
rebuildObjectives(new Seq<>());
|
||||
dialog.hide();
|
||||
})).marginLeft(12f).row();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
if(mobile){
|
||||
buttons.button("@cancel", Icon.cancel, canvas::stopQuery).disabled(b -> !canvas.isQuerying());
|
||||
buttons.button("@ok", Icon.ok, canvas::placeQuery).disabled(b -> !canvas.isQuerying());
|
||||
buttons.button("@cancel", Icon.cancel, canvas::stopQuery).visible(() -> canvas.isQuerying());
|
||||
buttons.button("@ok", Icon.ok, canvas::placeQuery).visible(() -> canvas.isQuerying());
|
||||
}
|
||||
|
||||
setFillParent(true);
|
||||
@@ -490,22 +523,27 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
public void show(Seq<MapObjective> objectives, Cons<Seq<MapObjective>> out){
|
||||
this.out = out;
|
||||
|
||||
rebuildObjectives(objectives);
|
||||
show();
|
||||
}
|
||||
|
||||
public void rebuildObjectives(Seq<MapObjective> objectives){
|
||||
canvas.clearObjectives();
|
||||
if(
|
||||
objectives.any() && (
|
||||
// If the objectives were previously programmatically made...
|
||||
objectives.contains(obj -> obj.editorX == -1 || obj.editorY == -1) ||
|
||||
// ... or some idiot somehow made it not work...
|
||||
objectives.contains(obj -> !canvas.tilemap.createTile(obj))
|
||||
objectives.any() && (
|
||||
// If the objectives were previously programmatically made...
|
||||
objectives.contains(obj -> obj.editorX == -1 || obj.editorY == -1) ||
|
||||
// ... or some idiot somehow made it not work...
|
||||
objectives.contains(obj -> !canvas.tilemap.createTile(obj))
|
||||
)){
|
||||
// ... then rebuild the structure.
|
||||
canvas.clearObjectives();
|
||||
|
||||
// This is definitely NOT a good way to do it, but only insane people or people from the distant past would actually encounter this anyway.
|
||||
int w = objWidth + 2,
|
||||
len = objectives.size * w,
|
||||
columns = objectives.size,
|
||||
rows = 1;
|
||||
len = objectives.size * w,
|
||||
columns = objectives.size,
|
||||
rows = 1;
|
||||
|
||||
if(len > bounds){
|
||||
rows = len / bounds;
|
||||
@@ -525,7 +563,6 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
}
|
||||
|
||||
canvas.objectives.set(objectives);
|
||||
show();
|
||||
}
|
||||
|
||||
public static <T extends UnlockableContent> void showContentSelect(@Nullable ContentType type, Cons<T> cons, Boolf<T> check){
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -93,6 +93,7 @@ public class SectorGenerateDialog extends BaseDialog{
|
||||
var preset = sectorobj.preset;
|
||||
sectorobj.preset = null;
|
||||
|
||||
logic.reset(); //TODO: is this a good idea? all rules and map state are cleared, but it fixes inconsistent gen
|
||||
world.loadSector(sectorobj, seed, false);
|
||||
|
||||
sectorobj.preset = preset;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
@@ -17,7 +22,6 @@ import mindustry.ui.*;
|
||||
|
||||
public class WaveGraph extends Table{
|
||||
public Seq<SpawnGroup> groups = new Seq<>();
|
||||
public int from = 0, to = 20;
|
||||
|
||||
private Mode mode = Mode.counts;
|
||||
private int[][] values;
|
||||
@@ -26,47 +30,114 @@ public class WaveGraph extends Table{
|
||||
private float maxHealth;
|
||||
private Table colors;
|
||||
private ObjectSet<UnitType> hidden = new ObjectSet<>();
|
||||
private StringBuilder countStr = new StringBuilder();
|
||||
|
||||
private float pan;
|
||||
private float zoom = 1f;
|
||||
private int from = 0, to = 20;
|
||||
private int lastFrom = -1, lastTo = -1;
|
||||
private float lastZoom = -1f;
|
||||
|
||||
private float defaultSpace = Scl.scl(40f);
|
||||
private FloatSeq points = new FloatSeq(40);
|
||||
|
||||
public WaveGraph(){
|
||||
background(Tex.pane);
|
||||
|
||||
scrolled((scroll) -> {
|
||||
zoom -= scroll * 2f / 10f * zoom;
|
||||
clampZoom();
|
||||
});
|
||||
|
||||
touchable = Touchable.enabled;
|
||||
addListener(new InputListener(){
|
||||
|
||||
@Override
|
||||
public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){
|
||||
requestScroll();
|
||||
}
|
||||
});
|
||||
|
||||
addListener(new ElementGestureListener(){
|
||||
@Override
|
||||
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
|
||||
pan -= deltaX/zoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void zoom(InputEvent event, float initialDistance, float distance){
|
||||
if(lastZoom < 0) lastZoom = zoom;
|
||||
|
||||
zoom = distance / initialDistance * lastZoom;
|
||||
clampZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
lastZoom = zoom;
|
||||
}
|
||||
});
|
||||
|
||||
rect((x, y, width, height) -> {
|
||||
Lines.stroke(Scl.scl(3f));
|
||||
countStr.setLength(0);
|
||||
|
||||
Vec2 mouse = stageToLocalCoordinates(Core.input.mouse());
|
||||
|
||||
GlyphLayout lay = Pools.obtain(GlyphLayout.class, GlyphLayout::new);
|
||||
Font font = Fonts.outline;
|
||||
|
||||
lay.setText(font, "1");
|
||||
|
||||
int maxY = switch(mode){
|
||||
case counts -> nextStep(max);
|
||||
case health -> nextStep((int)maxHealth);
|
||||
case totals -> nextStep(maxTotal);
|
||||
};
|
||||
|
||||
float fh = lay.height;
|
||||
float offsetX = Scl.scl(lay.width * (maxY + "").length() * 2), offsetY = Scl.scl(22f) + fh + Scl.scl(5f);
|
||||
lay.setText(font, "1");
|
||||
|
||||
float graphX = x + offsetX, graphY = y + offsetY, graphW = width - offsetX, graphH = height - offsetY;
|
||||
float spacing = graphW / (values.length - 1);
|
||||
float spacing = zoom * defaultSpace;
|
||||
pan = Math.max(pan, (width/2f)/zoom-defaultSpace);
|
||||
|
||||
float fh = lay.height;
|
||||
float offsetX = 0f, offsetY = Scl.scl(22f) + fh + Scl.scl(5f);
|
||||
float graphX = x + offsetX - pan * zoom + width/2f, graphY = y + offsetY, graphW = width - offsetX, graphH = height - offsetY;
|
||||
|
||||
float left = (x-graphX)/spacing, right = (x + width - graphX)/spacing;
|
||||
|
||||
//int radius = Mathf.ceil(graphW / spacing / 2f);
|
||||
|
||||
from = (int)left - 1;
|
||||
to = (int)right + 1;
|
||||
|
||||
if(lastFrom != from || lastTo != to){
|
||||
rebuild();
|
||||
}
|
||||
|
||||
lastFrom = from;
|
||||
lastTo = to;
|
||||
|
||||
if(!clipBegin(x + offsetX, y + offsetY, graphW, graphH)) return;
|
||||
|
||||
int selcol = Rect.contains(x, y, width, height, mouse.x, mouse.y) ? Mathf.round((mouse.x - graphX - (from * spacing)) / spacing) : -1;
|
||||
if(selcol + from <= -1) selcol = -1;
|
||||
|
||||
if(mode == Mode.counts){
|
||||
for(UnitType type : used.orderedItems()){
|
||||
Draw.color(color(type));
|
||||
Draw.alpha(parentAlpha);
|
||||
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
for(int i = 0; i < values.length; i++){
|
||||
int val = values[i][type.id];
|
||||
float cx = graphX + i * spacing, cy = graphY + val * graphH / maxY;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + val * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}
|
||||
}else if(mode == Mode.totals){
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
Draw.color(Pal.accent);
|
||||
for(int i = 0; i < values.length; i++){
|
||||
@@ -75,13 +146,13 @@ public class WaveGraph extends Table{
|
||||
sum += values[i][type.id];
|
||||
}
|
||||
|
||||
float cx = graphX + i * spacing, cy = graphY + sum * graphH / maxY;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + sum * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}else if(mode == Mode.health){
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
Draw.color(Pal.health);
|
||||
for(int i = 0; i < values.length; i++){
|
||||
@@ -90,13 +161,32 @@ public class WaveGraph extends Table{
|
||||
sum += (type.health) * values[i][type.id];
|
||||
}
|
||||
|
||||
float cx = graphX + i * spacing, cy = graphY + sum * graphH / maxY;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + sum * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}
|
||||
|
||||
|
||||
if(selcol >= 0 && selcol < values.length){
|
||||
Draw.color(1f, 0f, 0f, 0.2f);
|
||||
Fill.crect((selcol+from) * spacing + graphX - spacing/2f, graphY, spacing, graphH);
|
||||
Draw.color();
|
||||
font.getData().setScale(1.5f);
|
||||
for(UnitType type : used.orderedItems()){
|
||||
int amount = values[Mathf.clamp(selcol, 0, values.length - 1)][type.id];
|
||||
if(amount > 0){
|
||||
countStr.append(type.emoji()).append(" ").append(amount).append("\n");
|
||||
}
|
||||
}
|
||||
float pad = Scl.scl(5f);
|
||||
font.draw(countStr, (selcol+from) * spacing + graphX - spacing/2f + pad, graphY + graphH - pad);
|
||||
font.getData().setScale(1f);
|
||||
}
|
||||
|
||||
clipEnd();
|
||||
|
||||
//how many numbers can fit here
|
||||
float totalMarks = Mathf.clamp(maxY, 1, 10);
|
||||
|
||||
@@ -106,13 +196,13 @@ public class WaveGraph extends Table{
|
||||
Draw.alpha(0.1f);
|
||||
|
||||
for(int i = 0; i < maxY; i += markSpace){
|
||||
float cy = graphY + i * graphH / maxY, cx = graphX;
|
||||
float cy = graphY + i * graphH / maxY, cx = x;
|
||||
|
||||
Lines.line(cx, cy, cx + graphW, cy);
|
||||
|
||||
lay.setText(font, "" + i);
|
||||
|
||||
font.draw("" + i, cx, cy + lay.height / 2f, Align.right);
|
||||
font.draw("" + i, cx, cy + lay.height / 2f, Align.left);
|
||||
}
|
||||
Draw.alpha(1f);
|
||||
|
||||
@@ -120,10 +210,12 @@ public class WaveGraph extends Table{
|
||||
font.setColor(Color.lightGray);
|
||||
|
||||
for(int i = 0; i < values.length; i++){
|
||||
float cy = y + fh, cx = graphX + graphW / (values.length - 1) * i;
|
||||
float cy = y + fh, cx = graphX + spacing * (i + from);
|
||||
|
||||
Lines.line(cx, cy, cx, cy + len);
|
||||
if(i == values.length / 2){
|
||||
if(cx >= x + offsetX && cx <= x + offsetX + graphW){
|
||||
Lines.line(cx, cy, cx, cy + len);
|
||||
}
|
||||
if(i == selcol){
|
||||
font.draw("" + (i + from + 1), cx, cy - Scl.scl(2f), Align.center);
|
||||
}
|
||||
}
|
||||
@@ -152,6 +244,28 @@ public class WaveGraph extends Table{
|
||||
}).growX();
|
||||
}
|
||||
|
||||
private void clampZoom(){
|
||||
zoom = Mathf.clamp(zoom, 0.5f / Scl.scl(1f), 40f / Scl.scl(1f));
|
||||
}
|
||||
|
||||
private void linePoint(float x, float y){
|
||||
points.add(x, y);
|
||||
}
|
||||
|
||||
private void beginLine(){
|
||||
points.clear();
|
||||
}
|
||||
|
||||
private void endLine(){
|
||||
var items = points.items;
|
||||
for(int i = 0; i < points.size - 2; i += 2){
|
||||
Lines.line(items[i], items[i + 1], items[i + 2], items[i + 3], false);
|
||||
Fill.circle(items[i], items[i + 1], Lines.getStroke()/2f);
|
||||
}
|
||||
Fill.circle(items[points.size - 2], items[points.size - 1], Lines.getStroke());
|
||||
points.clear();
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
values = new int[to - from + 1][Vars.content.units().size];
|
||||
used.clear();
|
||||
@@ -177,6 +291,8 @@ public class WaveGraph extends Table{
|
||||
maxHealth = Math.max(maxHealth, healthsum);
|
||||
}
|
||||
|
||||
used.orderedItems().sort();
|
||||
|
||||
ObjectSet<UnitType> usedCopy = new ObjectSet<>(used);
|
||||
|
||||
colors.clear();
|
||||
@@ -198,7 +314,7 @@ public class WaveGraph extends Table{
|
||||
t.button(b -> {
|
||||
Color tcolor = color(type).cpy();
|
||||
b.image().size(32f).update(i -> i.setColor(b.isChecked() ? Tmp.c1.set(tcolor).mul(0.5f) : tcolor)).get().act(1);
|
||||
b.image(type.uiIcon).size(32f).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1);
|
||||
b.image(type.uiIcon).size(32f).scaling(Scaling.fit).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1);
|
||||
b.margin(0f);
|
||||
}, Styles.fullTogglet, () -> {
|
||||
if(!hidden.add(type)){
|
||||
@@ -212,6 +328,8 @@ public class WaveGraph extends Table{
|
||||
}
|
||||
}).scrollY(false);
|
||||
|
||||
colors.act(0.000001f);
|
||||
|
||||
for(UnitType type : hidden){
|
||||
used.remove(type);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import static mindustry.Vars.*;
|
||||
import static mindustry.game.SpawnGroup.*;
|
||||
|
||||
public class WaveInfoDialog extends BaseDialog{
|
||||
private int start = 0, displayed = 20;
|
||||
Seq<SpawnGroup> groups = new Seq<>();
|
||||
private @Nullable SpawnGroup expandedGroup;
|
||||
|
||||
@@ -36,7 +35,6 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
private @Nullable UnitType filterType;
|
||||
private Sort sort = Sort.begin;
|
||||
private boolean reverseSort = false;
|
||||
private float updateTimer, updatePeriod = 1f;
|
||||
private boolean checkedSpawns;
|
||||
private WaveGraph graph = new WaveGraph();
|
||||
|
||||
@@ -49,7 +47,6 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
});
|
||||
hidden(() -> state.rules.spawns = groups);
|
||||
|
||||
onResize(this::setup);
|
||||
addCloseButton();
|
||||
|
||||
buttons.button("@waves.edit", Icon.edit, () -> {
|
||||
@@ -71,7 +68,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
groups = maps.readWaves(Core.app.getClipboardText());
|
||||
buildGroups();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
Log.err(e);
|
||||
ui.showErrorMessage("@waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
@@ -93,57 +90,11 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
dialog.show();
|
||||
}).size(250f, 64f);
|
||||
|
||||
buttons.defaults().width(60f);
|
||||
|
||||
buttons.button("<", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
shift(-1);
|
||||
}
|
||||
});
|
||||
buttons.button(">", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
shift(1);
|
||||
}
|
||||
});
|
||||
|
||||
buttons.button("-", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
view(-1);
|
||||
}
|
||||
});
|
||||
buttons.button("+", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
view(1);
|
||||
}
|
||||
});
|
||||
|
||||
if(experimental){
|
||||
buttons.button(Core.bundle.get("waves.random"), Icon.refresh, () -> {
|
||||
groups.clear();
|
||||
groups = Waves.generate(1f / 10f);
|
||||
buildGroups();
|
||||
}).width(200f);
|
||||
}
|
||||
}
|
||||
|
||||
void view(int amount){
|
||||
updateTimer += Time.delta;
|
||||
if(updateTimer >= updatePeriod){
|
||||
displayed += amount;
|
||||
if(displayed < 5) displayed = 5;
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
}
|
||||
|
||||
void shift(int amount){
|
||||
updateTimer += Time.delta;
|
||||
if(updateTimer >= updatePeriod){
|
||||
start += amount;
|
||||
if(start < 0) start = 0;
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
buttons.button(Core.bundle.get("waves.random"), Icon.refresh, () -> {
|
||||
groups.clear();
|
||||
groups = Waves.generate(1f / 10f);
|
||||
buildGroups();
|
||||
}).width(200f);
|
||||
}
|
||||
|
||||
void setup(){
|
||||
@@ -156,7 +107,6 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
s.image(Icon.zoom).padRight(8);
|
||||
s.field(search < 0 ? "" : (search + 1) + "", TextFieldFilter.digitsOnly, text -> {
|
||||
search = groups.any() ? Strings.parseInt(text, 0) - 1 : -1;
|
||||
start = Math.max(search - (displayed / 2) - (displayed % 2), 0);
|
||||
buildGroups();
|
||||
}).growX().maxTextLength(8).get().setMessageText("@waves.search");
|
||||
s.button(Icon.units, Styles.emptyi, () -> showUnits(type -> filterType = type, true)).size(46f).tooltip("@waves.filter")
|
||||
@@ -222,7 +172,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
t.button(b -> {
|
||||
b.left();
|
||||
b.image(group.type.uiIcon).size(32f).padRight(3).scaling(Scaling.fit);
|
||||
b.add(group.type.localizedName).color(Pal.accent);
|
||||
b.add(group.type.localizedName).ellipsis(true).width(110f).left().color(Pal.accent);
|
||||
|
||||
b.add().growX();
|
||||
|
||||
@@ -493,8 +443,6 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
|
||||
void updateWaves(){
|
||||
graph.groups = groups;
|
||||
graph.from = start;
|
||||
graph.to = start + displayed;
|
||||
graph.rebuild();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -101,9 +103,13 @@ public class Damage{
|
||||
float damagePerWave = explosiveness / 2f;
|
||||
|
||||
for(int i = 0; i < waves; i++){
|
||||
var shields = ignoreTeam == null ? null : indexer.getEnemy(ignoreTeam, BlockFlag.shield);
|
||||
int f = i;
|
||||
Time.run(i * 2f, () -> {
|
||||
damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), damagePerWave, false);
|
||||
if(shields == null || shields.isEmpty() || !shields.contains(b -> b instanceof ExplosionShield s && s.absorbExplosion(x, y, damagePerWave))){
|
||||
damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), damagePerWave, false);
|
||||
}
|
||||
|
||||
Fx.blockExplosionSmoke.at(x + Mathf.range(radius), y + Mathf.range(radius));
|
||||
});
|
||||
}
|
||||
@@ -166,7 +172,7 @@ public class Damage{
|
||||
public static float findPierceLength(Bullet b, int pierceCap, float length){
|
||||
return findPierceLength(b, pierceCap, b.type.laserAbsorb, length);
|
||||
}
|
||||
|
||||
|
||||
public static float findPierceLength(Bullet b, int pierceCap, boolean laser, float length){
|
||||
vec.trnsExact(b.rotation(), length);
|
||||
rect.setPosition(b.x, b.y).setSize(vec.x, vec.y).normalize().grow(3f);
|
||||
@@ -358,7 +364,7 @@ public class Damage{
|
||||
*/
|
||||
public static Healthc linecast(Bullet hitter, float x, float y, float angle, float length){
|
||||
vec.trns(angle, length);
|
||||
|
||||
|
||||
tmpBuilding = null;
|
||||
|
||||
if(hitter.type.collidesGround){
|
||||
@@ -644,7 +650,7 @@ public class Damage{
|
||||
this.target = target;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
target = null;
|
||||
|
||||
@@ -83,6 +83,7 @@ public class Units{
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void unitDespawn(Unit unit){
|
||||
if(unit == null) return;
|
||||
Fx.unitDespawn.at(unit.x, unit.y, 0, unit);
|
||||
unit.remove();
|
||||
}
|
||||
@@ -94,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)){
|
||||
if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam) || state.rules.disableUnitCap){
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap);
|
||||
@@ -111,6 +112,10 @@ public class Units{
|
||||
return player == null || tile == null || tile.interactable(player.team()) || state.rules.editor;
|
||||
}
|
||||
|
||||
public static boolean isHittable(@Nullable Posc target, boolean air, boolean ground){
|
||||
return target != null && (target instanceof Buildingc ? ground : (target instanceof Unit u && u.checkTarget(air, ground)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a target.
|
||||
* @param target The target to validate
|
||||
@@ -474,7 +479,7 @@ public class Units{
|
||||
Seq<TeamData> data = state.teams.present;
|
||||
for(int i = 0; i < data.size; i++){
|
||||
var other = data.items[i];
|
||||
if(other.team != team){
|
||||
if(other.team != team && other.team != Team.derelict){
|
||||
if(other.tree().any(x, y, width, height)){
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public abstract class Ability implements Cloneable{
|
||||
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 addStats(Table t){
|
||||
|
||||
@@ -14,6 +14,7 @@ import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -68,7 +69,7 @@ public class EnergyFieldAbility extends Ability{
|
||||
t.add(Core.bundle.format("bullet.damage", damage));
|
||||
if(status != StatusEffects.none){
|
||||
t.row();
|
||||
t.add((status.hasEmoji() ? status.emoji() : "") + "[stat]" + status.localizedName);
|
||||
t.add((status.hasEmoji() ? status.emoji() : "") + "[stat]" + status.localizedName).with(l -> StatValues.withTooltip(l, status));
|
||||
}
|
||||
if(displayHeal){
|
||||
t.row();
|
||||
@@ -135,7 +136,7 @@ public class EnergyFieldAbility extends Ability{
|
||||
|
||||
if(hitBuildings && targetGround){
|
||||
Units.nearbyBuildings(rx, ry, range, b -> {
|
||||
if((b.team != Team.derelict || state.rules.coreCapture) && (b.team != unit.team || b.damaged())){
|
||||
if((b.team != Team.derelict || state.rules.coreCapture) && ((b.team != unit.team && b.block.targetable) || b.damaged()) && !b.block.privileged){
|
||||
all.add(b);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
@@ -20,9 +19,9 @@ public class ShieldArcAbility extends Ability{
|
||||
private static Vec2 paramPos = new Vec2();
|
||||
private static final Cons<Bullet> shieldConsumer = b -> {
|
||||
if(b.team != paramUnit.team && b.type.absorbable && paramField.data > 0 &&
|
||||
!b.within(paramPos, paramField.radius - paramField.width/2f) &&
|
||||
Tmp.v1.set(b).add(b.vel).within(paramPos, paramField.radius + paramField.width/2f) &&
|
||||
Angles.within(paramPos.angleTo(b), paramUnit.rotation + paramField.angleOffset, paramField.angle / 2f)){
|
||||
!(b.within(paramPos, paramField.radius - paramField.width/2f) && paramPos.within(b.x - b.deltaX, b.y - b.deltaY, paramField.radius - paramField.width/2f)) &&
|
||||
(Tmp.v1.set(b).add(b.deltaX, b.deltaY).within(paramPos, paramField.radius + paramField.width/2f) || b.within(paramPos, paramField.radius + paramField.width/2f)) &&
|
||||
(Angles.within(paramPos.angleTo(b), paramUnit.rotation + paramField.angleOffset, paramField.angle / 2f) || Angles.within(paramPos.angleTo(b.x + b.deltaX, b.y + b.deltaY), paramUnit.rotation + paramField.angleOffset, paramField.angle / 2f))){
|
||||
|
||||
b.absorb();
|
||||
Fx.absorb.at(b);
|
||||
@@ -60,7 +59,7 @@ public class ShieldArcAbility extends Ability{
|
||||
public boolean drawArc = true;
|
||||
/** If not null, will be drawn on top. */
|
||||
public @Nullable String region;
|
||||
/** Color override of the shield. Uses unit shield colour by default. */
|
||||
/** Color override of the shield. Uses unit shield colour by default. */
|
||||
public @Nullable Color color;
|
||||
/** If true, sprite position will be influenced by x/y. */
|
||||
public boolean offsetRegion = false;
|
||||
@@ -80,7 +79,7 @@ public class ShieldArcAbility extends Ability{
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
|
||||
|
||||
if(data < max){
|
||||
data += Time.delta * regen;
|
||||
}
|
||||
@@ -102,7 +101,7 @@ public class ShieldArcAbility extends Ability{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(UnitType type){
|
||||
public void created(Unit unit){
|
||||
data = max;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ public class ShieldRegenFieldAbility extends Ability{
|
||||
t.row();
|
||||
t.add(abilityStat("firingrate", Strings.autoFixed(60f / reload, 2)));
|
||||
t.row();
|
||||
t.add(abilityStat("pulseregen", Strings.autoFixed(amount, 2)));
|
||||
t.row();
|
||||
t.add(abilityStat("shield", Strings.autoFixed(max, 2)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
@@ -19,6 +21,7 @@ public class StatusFieldAbility extends Ability{
|
||||
public Effect activeEffect = Fx.overdriveWave;
|
||||
public float effectX, effectY;
|
||||
public boolean parentizeEffects, effectSizeParam = true;
|
||||
public Color color = Pal.accent;
|
||||
|
||||
protected float timer;
|
||||
|
||||
@@ -52,7 +55,7 @@ public class StatusFieldAbility extends Ability{
|
||||
});
|
||||
|
||||
float x = unit.x + Angles.trnsx(unit.rotation, effectY, effectX), y = unit.y + Angles.trnsy(unit.rotation, effectY, effectX);
|
||||
activeEffect.at(x, y, effectSizeParam ? range : unit.rotation, parentizeEffects ? unit : null);
|
||||
activeEffect.at(x, y, effectSizeParam ? range : unit.rotation, color, parentizeEffects ? unit : null);
|
||||
|
||||
timer = 0f;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ArtilleryBulletType extends BasicBulletType{
|
||||
super.update(b);
|
||||
|
||||
if(b.timer(0, (3 + b.fslope() * 2f) * trailMult)){
|
||||
trailEffect.at(b.x, b.y, b.fslope() * trailSize, backColor);
|
||||
trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : b.fslope() * trailSize, backColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,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. */
|
||||
@@ -102,6 +104,10 @@ public class BulletType extends Content implements Cloneable{
|
||||
public StatusEffect status = StatusEffects.none;
|
||||
/** Intensity of applied status effect in terms of duration. */
|
||||
public float statusDuration = 60 * 8f;
|
||||
/** Turret only. If false, blocks will not be targeted. */
|
||||
public boolean targetBlocks = true;
|
||||
/** Turret only. If false, missiles will not be targeted. */
|
||||
public boolean targetMissiles = true;
|
||||
/** Whether this bullet type collides with tiles. */
|
||||
public boolean collidesTiles = true;
|
||||
/** Whether this bullet type collides with tiles that are of the same team. */
|
||||
@@ -137,8 +143,12 @@ 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 */
|
||||
@@ -346,7 +356,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
return spawnUnit.estimateDps();
|
||||
}
|
||||
|
||||
float sum = damage * (pierce ? pierceCap == -1 ? 2 : Mathf.clamp(pierceCap, 1, 2) : 1f) * splashDamage*0.75f;
|
||||
float sum = (damage + splashDamage*0.75f) * (pierce ? pierceCap == -1 ? 2 : Mathf.clamp(pierceCap, 1, 2) : 1f);
|
||||
if(fragBullet != null && fragBullet != this){
|
||||
sum += fragBullet.estimateDPS() * fragBullets / 2f;
|
||||
}
|
||||
@@ -521,7 +531,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++;
|
||||
@@ -549,7 +559,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
if(!fragOnHit){
|
||||
createFrags(b, b.x, b.y);
|
||||
}
|
||||
|
||||
|
||||
despawnEffect.at(b.x, b.y, b.rotation(), hitColor);
|
||||
despawnSound.at(b);
|
||||
|
||||
@@ -563,6 +573,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);
|
||||
@@ -675,7 +693,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void updateTrail(Bullet b){
|
||||
if(!headless && trailLength > 0){
|
||||
if(b.trail == null){
|
||||
@@ -710,13 +728,16 @@ 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){
|
||||
lightRadius = Math.max(18, hitSize * 5f);
|
||||
}
|
||||
|
||||
|
||||
drawSize = Math.max(drawSize, trailLength * speed * 2f);
|
||||
range = calculateRange();
|
||||
}
|
||||
@@ -748,15 +769,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){
|
||||
@@ -772,6 +793,13 @@ public class BulletType extends Content implements Cloneable{
|
||||
}
|
||||
|
||||
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){
|
||||
return create(owner, shooter, team, x, y, angle, damage, velocityScl, lifetimeScl, data, mover, aimX, aimY, null);
|
||||
}
|
||||
|
||||
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
|
||||
){
|
||||
if(!Mathf.chance(createChance)) return null;
|
||||
if(ignoreSpawnAngle) angle = 0;
|
||||
if(spawnUnit != null){
|
||||
@@ -807,12 +835,13 @@ 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;
|
||||
bullet.originY = y;
|
||||
if(!(aimX == -1f && aimY == -1f)){
|
||||
bullet.aimTile = world.tileWorld(aimX, aimY);
|
||||
bullet.aimTile = target instanceof Building b ? b.tile : world.tileWorld(aimX, aimY);
|
||||
}
|
||||
bullet.aimX = aimX;
|
||||
bullet.aimY = aimY;
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ContinuousLaserBulletType extends ContinuousBulletType{
|
||||
incendSpread = 5;
|
||||
incendChance = 0.4f;
|
||||
lightColor = Color.orange;
|
||||
lightOpacity = 0.7f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,7 +67,7 @@ public class ContinuousLaserBulletType extends ContinuousBulletType{
|
||||
|
||||
Tmp.v1.trns(b.rotation(), realLength * 1.1f);
|
||||
|
||||
Drawf.light(b.x, b.y, b.x + Tmp.v1.x, b.y + Tmp.v1.y, lightStroke, lightColor, 0.7f);
|
||||
Drawf.light(b.x, b.y, b.x + Tmp.v1.x, b.y + Tmp.v1.y, lightStroke, lightColor, lightOpacity);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
|
||||
10
core/src/mindustry/entities/bullet/EmptyBulletType.java
Normal file
10
core/src/mindustry/entities/bullet/EmptyBulletType.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
public class EmptyBulletType extends BulletType{
|
||||
|
||||
public EmptyBulletType(){
|
||||
hittable = collidesGround = collidesAir = collidesTiles = false;
|
||||
speed = 0f;
|
||||
keepVelocity = false;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package mindustry.entities.comp;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
@@ -63,7 +62,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
Tile tile = world.tile(plan.x, plan.y);
|
||||
boolean isSameDerelict = (tile != null && tile.build != null && tile.block() == plan.block && tile.build.tileX() == plan.x && tile.build.tileY() == plan.y && tile.team() == Team.derelict);
|
||||
if(tile == null || (plan.breaking && tile.block() == Blocks.air) || (!plan.breaking && ((tile.build != null && tile.build.rotation == plan.rotation && !isSameDerelict) || !plan.block.rotate) &&
|
||||
//th block must be the same, but not derelict and the same
|
||||
//the block must be the same, but not derelict and the same
|
||||
((tile.block() == plan.block && !isSameDerelict) ||
|
||||
//same floor or overlay
|
||||
(plan.block != null && (plan.block.isOverlay() && plan.block == tile.overlay() || (plan.block.isFloor() && plan.block == tile.floor())))))){
|
||||
@@ -137,17 +136,31 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
}
|
||||
|
||||
if(!(tile.build instanceof ConstructBuild cb)){
|
||||
if(!current.initialized && !current.breaking && Build.validPlace(current.block, team, current.x, current.y, current.rotation)){
|
||||
boolean hasAll = infinite || current.isRotation(team) ||
|
||||
if(!current.initialized && !current.breaking && Build.validPlaceIgnoreUnits(current.block, team, current.x, current.y, current.rotation, true)){
|
||||
if(Build.checkNoUnitOverlap(current.block, current.x, current.y)){
|
||||
boolean hasAll = infinite || current.isRotation(team) ||
|
||||
//derelict repair
|
||||
(tile.team() == Team.derelict && tile.block() == current.block && tile.build != null && tile.block().allowDerelictRepair && state.rules.derelictRepair) ||
|
||||
//make sure there's at least 1 item of each type first
|
||||
!Structs.contains(current.block.requirements, i -> core != null && !core.items.has(i.item, Math.min(Mathf.round(i.amount * state.rules.buildCostMultiplier), 1)));
|
||||
!Structs.contains(current.block.requirements, i -> !core.items.has(i.item, Math.min(Mathf.round(i.amount * state.rules.buildCostMultiplier), 1)));
|
||||
|
||||
if(hasAll){
|
||||
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation);
|
||||
if(hasAll){
|
||||
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation);
|
||||
|
||||
if(current.block.instantBuild){
|
||||
if(plans.size > 0){
|
||||
plans.removeFirst();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}else{
|
||||
current.stuck = true;
|
||||
}
|
||||
}else{
|
||||
current.stuck = true;
|
||||
//there's a unit blocking the plan, skip it
|
||||
plans.removeFirst();
|
||||
plans.addLast(current);
|
||||
continue;
|
||||
}
|
||||
}else if(!current.initialized && current.breaking && Build.validBreak(team, current.x, current.y)){
|
||||
Call.beginBreak(self(), team, current.x, current.y);
|
||||
@@ -186,11 +199,10 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
|
||||
/** Draw all current build plans. Does not draw the beam effect, only the positions. */
|
||||
void drawBuildPlans(){
|
||||
Boolf<BuildPlan> skip = plan -> plan.progress > 0.01f || (buildPlan() == plan && plan.initialized && (within(plan.x * tilesize, plan.y * tilesize, type.buildRange) || state.isEditor()));
|
||||
|
||||
for(int i = 0; i < 2; i++){
|
||||
for(BuildPlan plan : plans){
|
||||
if(skip.get(plan)) continue;
|
||||
if(plan.progress > 0.01f || (buildPlan() == plan && plan.initialized && (within(plan.x * tilesize, plan.y * tilesize, type.buildRange) || state.isEditor()))) continue;
|
||||
if(i == 0){
|
||||
drawPlan(plan, 1f);
|
||||
}else{
|
||||
|
||||
@@ -56,7 +56,7 @@ 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;
|
||||
@@ -338,7 +338,7 @@ 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){
|
||||
@@ -1029,10 +1029,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,16 +1044,22 @@ 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;
|
||||
@@ -1118,7 +1123,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
power.links.clear();
|
||||
}
|
||||
|
||||
|
||||
public boolean conductsTo(Building other){
|
||||
return !block.insulated;
|
||||
}
|
||||
@@ -1196,6 +1201,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
block.drawOverlay(x, y, rotation);
|
||||
}
|
||||
|
||||
public void drawItemSelection(UnlockableContent selection){
|
||||
if(selection != null && Core.settings.getBool("displayselection", true)){
|
||||
TextureRegion region = selection.fullIcon;
|
||||
Draw.rect(region, x, y + block.size * tilesize / 2f + 4, 8f * region.ratio(), 8f);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawDisabled(){
|
||||
Draw.color(Color.scarlet);
|
||||
Draw.alpha(0.8f);
|
||||
@@ -1217,6 +1229,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
|
||||
public void payloadDraw(){
|
||||
if(block.isAir()) return;
|
||||
draw();
|
||||
}
|
||||
|
||||
@@ -1319,7 +1332,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());
|
||||
}
|
||||
@@ -1639,7 +1652,7 @@ 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);
|
||||
}
|
||||
@@ -1726,7 +1739,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);
|
||||
@@ -1982,6 +1995,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
|
||||
}
|
||||
|
||||
@@ -2079,6 +2096,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
@Override
|
||||
public void remove(){
|
||||
stopSound();
|
||||
}
|
||||
|
||||
public void stopSound(){
|
||||
if(sound != null){
|
||||
sound.stop();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ 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;
|
||||
transient Entityc shooter;
|
||||
transient @Nullable Tile aimTile;
|
||||
transient float aimX, aimY;
|
||||
transient float originX, originY;
|
||||
@@ -248,7 +249,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
|
||||
|
||||
type.draw(self());
|
||||
type.drawLight(self());
|
||||
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.*;
|
||||
@@ -194,6 +195,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -90,6 +97,8 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
}
|
||||
|
||||
void pickup(Unit unit){
|
||||
if(unit.isAdded()) unit.team.data().updateCount(unit.type, 1);
|
||||
|
||||
unit.remove();
|
||||
addPayload(new UnitPayload(unit));
|
||||
Fx.unitPickup.at(unit);
|
||||
@@ -129,7 +138,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
}
|
||||
|
||||
//drop off payload on an acceptor if possible
|
||||
if(on != null && on.build != null && on.build.acceptPayload(on.build, payload)){
|
||||
if(on != null && on.build != null && on.build.team == team && on.build.acceptPayload(on.build, payload)){
|
||||
Fx.unitDrop.at(on.build);
|
||||
on.build.handlePayload(on.build, payload);
|
||||
return true;
|
||||
@@ -146,8 +155,12 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
boolean dropUnit(UnitPayload payload){
|
||||
Unit u = payload.unit;
|
||||
|
||||
//add random offset to prevent unit stacking
|
||||
Tmp.v1.rnd(Mathf.random(2f));
|
||||
|
||||
//can't drop ground units
|
||||
if(!u.canPass(tileX(), tileY()) || Units.count(x, y, u.physicSize(), o -> o.isGrounded()) > 1){
|
||||
//allow stacking for small units for now - otherwise, unit transfer would get annoying
|
||||
if(!u.canPass(World.toTile(x + Tmp.v1.x), World.toTile(y + Tmp.v1.y)) || Units.count(x, y, u.physicSize(), o -> o.isGrounded() && o.hitSize > 14f) > 1){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -156,8 +169,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
//clients do not drop payloads
|
||||
if(Vars.net.client()) return true;
|
||||
|
||||
u.set(this);
|
||||
u.trns(Tmp.v1.rnd(Mathf.random(2f)));
|
||||
u.set(x + Tmp.v1.x, y + Tmp.v1.y);
|
||||
u.rotation(rotation);
|
||||
//reset the ID to a new value to make sure it's synced
|
||||
u.id = EntityGroup.nextId();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.*;
|
||||
@@ -57,16 +58,20 @@ 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(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.logic.GlobalVars.*;
|
||||
@@ -239,6 +241,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() : 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;
|
||||
@@ -263,6 +267,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;
|
||||
}
|
||||
|
||||
@@ -443,6 +457,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);
|
||||
}
|
||||
@@ -689,7 +707,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));
|
||||
@@ -713,7 +731,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
|
||||
//if this unit crash landed (was flying), damage stuff in a radius
|
||||
if(type.flying && !spawnedByCore && type.createWreck && state.rules.unitCrashDamage(team) > 0){
|
||||
Damage.damage(team, x, y, Mathf.pow(hitSize, 0.94f) * 1.25f, Mathf.pow(hitSize, 0.75f) * type.crashDamageMultiplier * 5f * state.rules.unitCrashDamage(team), true, false, true);
|
||||
var shields = indexer.getEnemy(team, BlockFlag.shield);
|
||||
float crashDamage = Mathf.pow(hitSize, 0.75f) * type.crashDamageMultiplier * 5f * state.rules.unitCrashDamage(team);
|
||||
if(shields.isEmpty() || !shields.contains(b -> b instanceof ExplosionShield s && s.absorbExplosion(x, y, crashDamage))){
|
||||
Damage.damage(team, x, y, Mathf.pow(hitSize, 0.94f) * 1.25f, crashDamage, true, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if(!headless && type.createScorch){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,13 @@ public abstract class DrawPart{
|
||||
}
|
||||
|
||||
default float getClamp(PartParams p){
|
||||
return Mathf.clamp(get(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);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public class FlarePart extends DrawPart{
|
||||
public float x, y, rotation, rotMove, spinSpeed;
|
||||
public boolean followRotation;
|
||||
public Color color1 = Pal.techBlue, color2 = Color.white;
|
||||
public boolean clampProgress = true;
|
||||
public PartProgress progress = PartProgress.warmup;
|
||||
public float layer = Layer.effect;
|
||||
|
||||
@@ -20,7 +21,7 @@ public class FlarePart extends DrawPart{
|
||||
float z = Draw.z();
|
||||
if(layer > 0) Draw.z(layer);
|
||||
|
||||
float prog = progress.getClamp(params);
|
||||
float prog = progress.getClamp(params, clampProgress);
|
||||
int i = params.sideOverride == -1 ? 0 : params.sideOverride;
|
||||
|
||||
float sign = (i == 0 ? 1 : -1) * params.sideMultiplier;
|
||||
|
||||
@@ -20,6 +20,7 @@ public class HaloPart extends DrawPart{
|
||||
public Color color = Color.white;
|
||||
public @Nullable Color colorTo;
|
||||
public boolean mirror = false;
|
||||
public boolean clampProgress = true;
|
||||
public PartProgress progress = PartProgress.warmup;
|
||||
public float layer = -1f, layerOffset = 0f;
|
||||
|
||||
@@ -32,7 +33,7 @@ public class HaloPart extends DrawPart{
|
||||
Draw.z(Draw.z() + layerOffset);
|
||||
|
||||
float
|
||||
prog = progress.getClamp(params),
|
||||
prog = progress.getClamp(params, clampProgress),
|
||||
baseRot = Time.time * rotateSpeed,
|
||||
rad = radiusTo < 0 ? radius : Mathf.lerp(radius, radiusTo, prog),
|
||||
triLen = triLengthTo < 0 ? triLength : Mathf.lerp(triLength, triLengthTo, prog),
|
||||
|
||||
@@ -27,6 +27,8 @@ public class RegionPart extends DrawPart{
|
||||
public boolean drawRegion = true;
|
||||
/** If true, the heat region produces light. */
|
||||
public boolean heatLight = false;
|
||||
/** Whether to clamp progress to (0-1). If false, allows usage of interps that go past the range, but may have unwanted visual bugs depending on values. */
|
||||
public boolean clampProgress = true;
|
||||
/** Progress function for determining position/rotation. */
|
||||
public PartProgress progress = PartProgress.warmup;
|
||||
/** Progress function for scaling. */
|
||||
@@ -67,14 +69,14 @@ public class RegionPart extends DrawPart{
|
||||
Draw.z(Draw.z() + layerOffset);
|
||||
|
||||
float prevZ = Draw.z();
|
||||
float prog = progress.getClamp(params), sclProg = growProgress.getClamp(params);
|
||||
float prog = progress.getClamp(params, clampProgress), sclProg = growProgress.getClamp(params, clampProgress);
|
||||
float mx = moveX * prog, my = moveY * prog, mr = moveRot * prog + rotation,
|
||||
gx = growX * sclProg, gy = growY * sclProg;
|
||||
|
||||
if(moves.size > 0){
|
||||
for(int i = 0; i < moves.size; i++){
|
||||
var move = moves.get(i);
|
||||
float p = move.progress.getClamp(params);
|
||||
float p = move.progress.getClamp(params, clampProgress);
|
||||
mx += move.x * p;
|
||||
my += move.y * p;
|
||||
mr += move.rot * p;
|
||||
@@ -130,7 +132,7 @@ public class RegionPart extends DrawPart{
|
||||
}
|
||||
|
||||
if(heat.found()){
|
||||
float hprog = heatProgress.getClamp(params);
|
||||
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);
|
||||
if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog);
|
||||
|
||||
@@ -15,6 +15,7 @@ public class ShapePart extends DrawPart{
|
||||
public Color color = Color.white;
|
||||
public @Nullable Color colorTo;
|
||||
public boolean mirror = false;
|
||||
public boolean clampProgress = true;
|
||||
public PartProgress progress = PartProgress.warmup;
|
||||
public float layer = -1f, layerOffset = 0f;
|
||||
|
||||
@@ -26,7 +27,7 @@ public class ShapePart extends DrawPart{
|
||||
|
||||
Draw.z(Draw.z() + layerOffset);
|
||||
|
||||
float prog = progress.getClamp(params),
|
||||
float prog = progress.getClamp(params, clampProgress),
|
||||
baseRot = Time.time * rotateSpeed,
|
||||
rad = radiusTo < 0 ? radius : Mathf.lerp(radius, radiusTo, prog),
|
||||
str = strokeTo < 0 ? stroke : Mathf.lerp(stroke, strokeTo, prog);
|
||||
|
||||
@@ -4,7 +4,6 @@ import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
@@ -29,8 +28,12 @@ public class AIController implements UnitController{
|
||||
protected Teamc target;
|
||||
|
||||
{
|
||||
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
|
||||
@@ -127,7 +130,7 @@ public class AIController implements UnitController{
|
||||
if(tile == null) return;
|
||||
Tile targetTile = pathfinder.getTargetTile(tile, pathfinder.getField(unit.team, costType, pathTarget));
|
||||
|
||||
if(tile == targetTile || (costType == Pathfinder.costNaval && !targetTile.floor().isLiquid)) return;
|
||||
if(tile == targetTile || !unit.canPass(targetTile.x, targetTile.y)) return;
|
||||
|
||||
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed()));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import arc.func.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.math.geom.QuadTree.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
@@ -64,7 +65,6 @@ public class BuildPlan implements Position, QuadTreeObject{
|
||||
public BuildPlan(){
|
||||
|
||||
}
|
||||
|
||||
public boolean placeable(Team team){
|
||||
return Build.validPlace(block, team, x, y, rotation);
|
||||
}
|
||||
@@ -152,6 +152,17 @@ public class BuildPlan implements Position, QuadTreeObject{
|
||||
return y*tilesize + (block == null ? 0 : block.offset);
|
||||
}
|
||||
|
||||
public boolean isDone(){
|
||||
Tile tile = world.tile(x, y);
|
||||
if(tile == null) return true;
|
||||
Block tblock = tile.block();
|
||||
if(breaking){
|
||||
return tblock == Blocks.air || tblock == tile.floor();
|
||||
}else{
|
||||
return tblock == block && (tile.build == null || tile.build.rotation == rotation);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Tile tile(){
|
||||
return world.tile(x, y);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package mindustry.entities.units;
|
||||
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public interface UnitController{
|
||||
void unit(Unit unit);
|
||||
Unit unit();
|
||||
@Nullable Unit unit();
|
||||
|
||||
default void hit(Bullet bullet){
|
||||
|
||||
|
||||
23
core/src/mindustry/game/CampaignRules.java
Normal file
23
core/src/mindustry/game/CampaignRules.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package mindustry.game;
|
||||
|
||||
import mindustry.type.*;
|
||||
|
||||
public class CampaignRules{
|
||||
public Difficulty difficulty = Difficulty.normal;
|
||||
public boolean fog;
|
||||
public boolean showSpawns;
|
||||
public boolean sectorInvasion;
|
||||
public boolean randomWaveAI;
|
||||
public boolean legacyLaunchPads;
|
||||
|
||||
public void apply(Planet planet, Rules rules){
|
||||
rules.staticFog = rules.fog = fog;
|
||||
rules.showSpawns = showSpawns;
|
||||
rules.randomWaveAI = randomWaveAI;
|
||||
rules.objectiveTimerMultiplier = difficulty.waveTimeMultiplier;
|
||||
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;
|
||||
rules.teams.get(rules.waveTeam).unitBuildSpeedMultiplier = difficulty.enemySpawnMultiplier;
|
||||
}
|
||||
}
|
||||
27
core/src/mindustry/game/Difficulty.java
Normal file
27
core/src/mindustry/game/Difficulty.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
|
||||
public enum Difficulty{
|
||||
//TODO these need tweaks
|
||||
casual(0.75f, 0.5f, 2f),
|
||||
easy(1f, 0.75f, 1.5f),
|
||||
normal(1f, 1f, 1f),
|
||||
hard(1.25f, 1.5f, 0.8f),
|
||||
eradication(1.5f, 2f, 0.6f);
|
||||
|
||||
public static final Difficulty[] all = values();
|
||||
|
||||
//TODO add more fields
|
||||
public float enemyHealthMultiplier, enemySpawnMultiplier, waveTimeMultiplier;
|
||||
|
||||
Difficulty(float enemyHealthMultiplier, float enemySpawnMultiplier, float waveTimeMultiplier){
|
||||
this.enemySpawnMultiplier = enemySpawnMultiplier;
|
||||
this.waveTimeMultiplier = waveTimeMultiplier;
|
||||
this.enemyHealthMultiplier = enemyHealthMultiplier;
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("difficulty." + name());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ public class EventType{
|
||||
teamCoreDamage,
|
||||
socketConfigChanged,
|
||||
update,
|
||||
beforeGameUpdate,
|
||||
afterGameUpdate,
|
||||
unitCommandChange,
|
||||
unitCommandPosition,
|
||||
unitCommandAttack,
|
||||
@@ -80,6 +82,8 @@ public class EventType{
|
||||
public static class BlockInfoEvent{}
|
||||
/** Called *after* all content has been initialized. */
|
||||
public static class ContentInitEvent{}
|
||||
/** Called *after* all mod content has been loaded, but before it has been initialized. */
|
||||
public static class ModContentLoadEvent{}
|
||||
/** Called when the client game is first loaded. */
|
||||
public static class ClientLoadEvent{}
|
||||
/** Called after SoundControl registers its music. */
|
||||
|
||||
@@ -106,6 +106,13 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
JsonIO.classTag(name, type);
|
||||
}
|
||||
|
||||
public MapObjectives(Seq<MapObjective> all){
|
||||
this.all.addAll(all);
|
||||
}
|
||||
|
||||
public MapObjectives(){
|
||||
}
|
||||
|
||||
/** Adds all given objectives to the executor as root objectives. */
|
||||
public void add(MapObjective... objectives){
|
||||
for(var objective : objectives) flatten(objective);
|
||||
@@ -164,6 +171,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
|
||||
/** Base abstract class for any in-map objective. */
|
||||
public static abstract class MapObjective{
|
||||
public boolean hidden;
|
||||
public @Nullable @Multiline String details;
|
||||
public @Unordered String[] flagsAdded = {};
|
||||
public @Unordered String[] flagsRemoved = {};
|
||||
@@ -441,7 +449,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
|
||||
@Override
|
||||
public boolean update(){
|
||||
return (countup += Time.delta) >= duration;
|
||||
return (countup += Time.delta) >= duration * state.rules.objectiveTimerMultiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -453,7 +461,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
@Override
|
||||
public String text(){
|
||||
if(text != null){
|
||||
int i = (int)((duration - countup) / 60f);
|
||||
int i = (int)((duration * state.rules.objectiveTimerMultiplier - countup) / 60f);
|
||||
StringBuilder timeString = new StringBuilder();
|
||||
|
||||
int m = i / 60;
|
||||
@@ -1125,6 +1133,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public void setTexture(String textureName){
|
||||
this.textureName = textureName;
|
||||
|
||||
if(headless) return;
|
||||
if(fetchedRegion == null) fetchedRegion = new TextureRegion();
|
||||
lookupRegion(textureName, fetchedRegion);
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ public class Objectives{
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return content.unlocked();
|
||||
return content.unlockedHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
return Core.bundle.format("requirement.research",
|
||||
//TODO broken for multi tech nodes.
|
||||
(content.techNode == null || content.techNode.parent == null || content.techNode.parent.content.unlocked()) ?
|
||||
(content.techNode == null || content.techNode.parent == null || content.techNode.parent.content.unlockedHost()) ?
|
||||
(content.emoji() + " " + content.localizedName) : "???");
|
||||
}
|
||||
}
|
||||
@@ -42,13 +42,13 @@ public class Objectives{
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return content.unlocked();
|
||||
return content.unlockedHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
return Core.bundle.format("requirement.produce",
|
||||
content.unlocked() ? (content.emoji() + " " + content.localizedName) : "???");
|
||||
content.unlockedHost() ? (content.emoji() + " " + content.localizedName) : "???");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import arc.util.serialization.*;
|
||||
import arc.util.serialization.Json.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.graphics.g3d.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
@@ -61,6 +62,8 @@ public class Rules{
|
||||
public boolean fire = true;
|
||||
/** Whether units use and require ammo. */
|
||||
public boolean unitAmmo = false;
|
||||
/** EXPERIMENTAL! If true, air and ground units target random things each wave instead of only the core/generators. */
|
||||
public boolean randomWaveAI = false;
|
||||
/** EXPERIMENTAL! If true, blocks will update in units and share power. */
|
||||
public boolean unitPayloadUpdate = false;
|
||||
/** If true, units' payloads are destroy()ed when the unit is destroyed. */
|
||||
@@ -81,10 +84,14 @@ public class Rules{
|
||||
public float unitHealthMultiplier = 1f;
|
||||
/** How much damage unit crash damage deals. (Compounds with unitDamageMultiplier) */
|
||||
public float unitCrashDamageMultiplier = 1f;
|
||||
/** How fast units can mine. */
|
||||
public float unitMineSpeedMultiplier = 1f;
|
||||
/** If true, ghost blocks will appear upon destruction, letting builder blocks/units rebuild them. */
|
||||
public boolean ghostBlocks = true;
|
||||
/** Whether to allow units to build with logic. */
|
||||
public boolean logicUnitBuild = true;
|
||||
/** If true, world processors can be edited and placed on this map. */
|
||||
public boolean allowEditWorldProcessors = false;
|
||||
/** If true, world processors no longer update. Used for testing. */
|
||||
public boolean disableWorldProcessors = false;
|
||||
/** How much health blocks start with. */
|
||||
@@ -97,6 +104,8 @@ public class Rules{
|
||||
public float buildSpeedMultiplier = 1f;
|
||||
/** Multiplier for percentage of materials refunded when deconstructing. */
|
||||
public float deconstructRefundMultiplier = 0.5f;
|
||||
/** Multiplier for time in timer objectives. */
|
||||
public float objectiveTimerMultiplier = 1f;
|
||||
/** No-build zone around enemy core radius. */
|
||||
public float enemyCoreBuildRadius = 400f;
|
||||
/** If true, no-build zones are calculated based on the closest core. */
|
||||
@@ -131,6 +140,8 @@ public class Rules{
|
||||
public int winWave = 0;
|
||||
/** Base unit cap. Can still be increased by blocks. */
|
||||
public int unitCap = 0;
|
||||
/** If true, the unit cap is disabled. */
|
||||
public boolean disableUnitCap;
|
||||
/** Environment drag multiplier. */
|
||||
public float dragMultiplier = 1f;
|
||||
/** Environmental flags that dictate visuals & how blocks function. */
|
||||
@@ -152,9 +163,7 @@ public class Rules{
|
||||
/** Reveals blocks normally hidden by build visibility. */
|
||||
public ObjectSet<Block> revealedBlocks = new ObjectSet<>();
|
||||
/** Unlocked content names. Only used in multiplayer when the campaign is enabled. */
|
||||
public ObjectSet<String> researched = new ObjectSet<>();
|
||||
/** Block containing these items as requirements are hidden. */
|
||||
public ObjectSet<Item> hiddenBuildItems = Items.erekirOnlyItems.asSet();
|
||||
public ObjectSet<UnlockableContent> researched = new ObjectSet<>();
|
||||
/** In-map objective executor. */
|
||||
public MapObjectives objectives = new MapObjectives();
|
||||
/** Flags set by objectives. Used in world processors. */
|
||||
@@ -234,6 +243,10 @@ public class Rules{
|
||||
return (this.env & env) != 0;
|
||||
}
|
||||
|
||||
public float buildRadius(Team team){
|
||||
return enemyCoreBuildRadius + teams.get(team).extraCoreBuildRadius;
|
||||
}
|
||||
|
||||
public float unitBuildSpeed(Team team){
|
||||
return unitBuildSpeedMultiplier * teams.get(team).unitBuildSpeedMultiplier;
|
||||
}
|
||||
@@ -255,6 +268,10 @@ public class Rules{
|
||||
return unitDamage(team) * unitCrashDamageMultiplier * teams.get(team).unitCrashDamageMultiplier;
|
||||
}
|
||||
|
||||
public float unitMineSpeed(Team team){
|
||||
return unitMineSpeedMultiplier * teams.get(team).unitMineSpeedMultiplier;
|
||||
}
|
||||
|
||||
public float blockHealth(Team team){
|
||||
return blockHealthMultiplier * teams.get(team).blockHealthMultiplier;
|
||||
}
|
||||
@@ -305,6 +322,8 @@ public class Rules{
|
||||
public float unitDamageMultiplier = 1f;
|
||||
/** How much damage unit crash damage deals. (Compounds with unitDamageMultiplier) */
|
||||
public float unitCrashDamageMultiplier = 1f;
|
||||
/** How fast units can mine. */
|
||||
public float unitMineSpeedMultiplier = 1f;
|
||||
/** Multiplier of resources that units take to build. */
|
||||
public float unitCostMultiplier = 1f;
|
||||
/** How much health units start with. */
|
||||
@@ -315,6 +334,9 @@ public class Rules{
|
||||
public float blockDamageMultiplier = 1f;
|
||||
/** Multiplier for building speed. */
|
||||
public float buildSpeedMultiplier = 1f;
|
||||
/** Extra spacing added to the no-build zone around the core. */
|
||||
public float extraCoreBuildRadius = 0f;
|
||||
|
||||
|
||||
//build cost disabled due to technical complexity
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class Saves{
|
||||
|
||||
if(state.isGame() && !state.gameOver && current != null && current.isAutosave()){
|
||||
time += Time.delta;
|
||||
if(time > Core.settings.getInt("saveinterval") * 60){
|
||||
if(time > Core.settings.getInt("saveinterval") * 60 && !Vars.disableSave){
|
||||
saving = true;
|
||||
|
||||
try{
|
||||
|
||||
@@ -200,8 +200,7 @@ public class Schematics implements Loadable{
|
||||
Seq<Schematic> keys = previews.orderedKeys().copy();
|
||||
for(int i = 0; i < previews.size - maxPreviewsMobile; i++){
|
||||
//dispose and remove unneeded previews
|
||||
previews.get(keys.get(i)).dispose();
|
||||
previews.remove(keys.get(i));
|
||||
previews.remove(keys.get(i)).dispose();
|
||||
}
|
||||
//update last clear time
|
||||
lastClearTime = Time.millis();
|
||||
@@ -654,7 +653,7 @@ public class Schematics implements Loadable{
|
||||
|
||||
private static Schematic rotated(Schematic input, boolean counter){
|
||||
int direction = Mathf.sign(counter);
|
||||
Schematic schem = input == tmpSchem ? tmpSchem2 : tmpSchem2;
|
||||
Schematic schem = input == tmpSchem ? tmpSchem2 : tmpSchem;
|
||||
schem.width = input.width;
|
||||
schem.height = input.height;
|
||||
Pools.freeAll(schem.tiles);
|
||||
|
||||
@@ -30,6 +30,9 @@ public class SectorInfo{
|
||||
public ObjectMap<Item, ExportStat> rawProduction = new ObjectMap<>();
|
||||
/** Export statistics. */
|
||||
public ObjectMap<Item, ExportStat> export = new ObjectMap<>();
|
||||
//TODO: there is an obvious exploit with launch pad redirection here; pads can be redirected after leaving a sector, which doesn't update calculations.
|
||||
/** Import statistics, based on what launch pads are actually receiving. */
|
||||
public ObjectMap<Item, ExportStat> imports = new ObjectMap<>();
|
||||
/** Items stored in all cores. */
|
||||
public ItemSeq items = new ItemSeq();
|
||||
/** The best available core type. */
|
||||
@@ -38,6 +41,8 @@ public class SectorInfo{
|
||||
public int storageCapacity = 0;
|
||||
/** Whether a core is available here. */
|
||||
public boolean hasCore = true;
|
||||
/** Whether a world processor is on this map - implies that the map will get cleared. */
|
||||
public boolean hasWorldProcessor;
|
||||
/** Whether this sector was ever fully captured. */
|
||||
public boolean wasCaptured = false;
|
||||
/** Sector that was launched from. */
|
||||
@@ -78,14 +83,18 @@ public class SectorInfo{
|
||||
public int waveVersion = -1;
|
||||
/** Whether this sector was indicated to the player or not. */
|
||||
public boolean shown = false;
|
||||
/** Temporary seq for last imported items. Do not use. */
|
||||
public transient ItemSeq lastImported = new ItemSeq();
|
||||
|
||||
/** Special variables for simulation. */
|
||||
public float sumHealth, sumRps, sumDps, waveHealthBase, waveHealthSlope, waveDpsBase, waveDpsSlope, bossHealth, bossDps, curEnemyHealth, curEnemyDps;
|
||||
public float sumHealth, sumRps, sumDps, bossHealth, bossDps, curEnemyHealth, curEnemyDps;
|
||||
/** Wave where first boss shows up. */
|
||||
public int bossWave = -1;
|
||||
|
||||
public ObjectFloatMap<Item> importCooldownTimers = new ObjectFloatMap<>();
|
||||
public @Nullable transient float[] importRateCache;
|
||||
|
||||
/** Temporary seq for last imported items. Do not use. */
|
||||
public transient ItemSeq lastImported = new ItemSeq();
|
||||
|
||||
/** Counter refresh state. */
|
||||
private transient Interval time = new Interval();
|
||||
/** Core item storage input/output deltas. */
|
||||
@@ -105,12 +114,6 @@ public class SectorInfo{
|
||||
productionDeltas[item.id] += amount;
|
||||
}
|
||||
|
||||
/** @return the real location items go when launched on this sector */
|
||||
public Sector getRealDestination(){
|
||||
//on multiplayer the destination is, by default, the first captured sector (basically random)
|
||||
return !net.client() || destination != null ? destination : state.rules.sector.planet.sectors.find(Sector::hasBase);
|
||||
}
|
||||
|
||||
/** Updates export statistics. */
|
||||
public void handleItemExport(ItemStack stack){
|
||||
handleItemExport(stack.item, stack.amount);
|
||||
@@ -121,10 +124,43 @@ public class SectorInfo{
|
||||
export.get(item, ExportStat::new).counter += amount;
|
||||
}
|
||||
|
||||
/** Updates import statistics. */
|
||||
public void handleItemImport(Item item, int amount){
|
||||
imports.get(item, ExportStat::new).counter += amount;
|
||||
}
|
||||
|
||||
public float getExport(Item item){
|
||||
return export.get(item, ExportStat::new).mean;
|
||||
}
|
||||
|
||||
public boolean hasExport(Item item){
|
||||
var exp = export.get(item);
|
||||
return exp != null && exp.mean > 0f;
|
||||
}
|
||||
|
||||
public void refreshImportRates(Planet planet){
|
||||
if(importRateCache == null || importRateCache.length != content.items().size){
|
||||
importRateCache = new float[content.items().size];
|
||||
}else{
|
||||
Arrays.fill(importRateCache, 0f);
|
||||
}
|
||||
eachImport(planet, sector -> sector.info.export.each((item, stat) -> {
|
||||
importRateCache[item.id] += stat.mean;
|
||||
}));
|
||||
}
|
||||
|
||||
public float[] getImportRates(Planet planet){
|
||||
if(importRateCache == null){
|
||||
refreshImportRates(planet);
|
||||
}
|
||||
return importRateCache;
|
||||
}
|
||||
|
||||
/** @return the import rate of an item as item/second. This is the *raw* max import rate, not what landing pads are actually using. */
|
||||
public float getImportRate(Planet planet, Item item){
|
||||
return getImportRates(planet)[item.id];
|
||||
}
|
||||
|
||||
/** Write contents of meta into main storage. */
|
||||
public void write(){
|
||||
//enable attack mode when there's a core.
|
||||
@@ -175,6 +211,7 @@ public class SectorInfo{
|
||||
spawnPosition = entity.pos();
|
||||
}
|
||||
|
||||
hasWorldProcessor = state.teams.present.contains(t -> t.getBuildings(Blocks.worldProcessor).any());
|
||||
waveSpacing = state.rules.waveSpacing;
|
||||
wave = state.wave;
|
||||
winWave = state.rules.winWave;
|
||||
@@ -218,19 +255,8 @@ public class SectorInfo{
|
||||
//refresh throughput
|
||||
if(time.get(refreshPeriod)){
|
||||
|
||||
//refresh export
|
||||
export.each((item, stat) -> {
|
||||
//initialize stat after loading
|
||||
if(!stat.loaded){
|
||||
stat.means.fill(stat.mean);
|
||||
stat.loaded = true;
|
||||
}
|
||||
|
||||
//add counter, subtract how many items were taken from the core during this time
|
||||
stat.means.add(Math.max(stat.counter, 0));
|
||||
stat.counter = 0;
|
||||
stat.mean = stat.means.rawMean();
|
||||
});
|
||||
updateStats(export);
|
||||
updateStats(imports);
|
||||
|
||||
if(coreDeltas == null) coreDeltas = new int[content.items().size];
|
||||
if(productionDeltas == null) productionDeltas = new int[content.items().size];
|
||||
@@ -247,6 +273,11 @@ public class SectorInfo{
|
||||
//export can, at most, be the raw items being produced from factories + the items being taken from the core
|
||||
export.get(item).mean = Math.min(export.get(item).mean, rawProduction.get(item).mean + Math.max(-production.get(item).mean, 0));
|
||||
}
|
||||
|
||||
if(imports.containsKey(item)){
|
||||
//import can't exceed max import rate
|
||||
imports.get(item).mean = Math.min(imports.get(item).mean, getImportRate(state.getPlanet(), item));
|
||||
}
|
||||
}
|
||||
|
||||
Arrays.fill(coreDeltas, 0);
|
||||
@@ -254,6 +285,20 @@ public class SectorInfo{
|
||||
}
|
||||
}
|
||||
|
||||
void updateStats(ObjectMap<Item, ExportStat> map){
|
||||
map.each((item, stat) -> {
|
||||
//initialize stat after loading
|
||||
if(!stat.loaded){
|
||||
stat.means.fill(stat.mean);
|
||||
stat.loaded = true;
|
||||
}
|
||||
|
||||
stat.means.add(Math.max(stat.counter, 0));
|
||||
stat.counter = 0;
|
||||
stat.mean = stat.means.rawMean();
|
||||
});
|
||||
}
|
||||
|
||||
void updateDelta(Item item, ObjectMap<Item, ExportStat> map, int[] deltas){
|
||||
ExportStat stat = map.get(item, ExportStat::new);
|
||||
if(!stat.loaded){
|
||||
@@ -290,7 +335,7 @@ public class SectorInfo{
|
||||
/** Iterates through every sector this one imports from. */
|
||||
public void eachImport(Planet planet, Cons<Sector> cons){
|
||||
for(Sector sector : planet.sectors){
|
||||
Sector dest = sector.info.getRealDestination();
|
||||
Sector dest = sector.info.destination;
|
||||
if(sector.hasBase() && sector.info != this && dest != null && dest.info == this && sector.info.anyExports()){
|
||||
cons.get(sector);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import arc.util.*;
|
||||
import mindustry.game.Rules.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
import mindustry.world.modules.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Team implements Comparable<Team>{
|
||||
public class Team implements Comparable<Team>, Senseable{
|
||||
public final int id;
|
||||
public final Color color;
|
||||
public final Color[] palette;
|
||||
@@ -138,7 +139,7 @@ public class Team implements Comparable<Team>{
|
||||
public String localized(){
|
||||
return Core.bundle.get("team." + name + ".name", name);
|
||||
}
|
||||
|
||||
|
||||
public String coloredName(){
|
||||
return emoji + "[#" + color + "]" + localized() + "[]";
|
||||
}
|
||||
@@ -152,4 +153,10 @@ public class Team implements Comparable<Team>{
|
||||
public String toString(){
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sense(LAccess sensor){
|
||||
if(sensor == LAccess.id) return id;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
@@ -55,6 +56,19 @@ public class Teams{
|
||||
return Geometry.findClosest(x, y, get(team).cores);
|
||||
}
|
||||
|
||||
public boolean anyEnemyCoresWithinBuildRadius(Team team, float x, float y){
|
||||
for(TeamData data : active){
|
||||
if(team != data.team){
|
||||
for(CoreBuild tile : data.cores){
|
||||
if(tile.within(x, y, state.rules.buildRadius(tile.team) + tilesize)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean anyEnemyCoresWithin(Team team, float x, float y, float radius){
|
||||
for(TeamData data : active){
|
||||
if(team != data.team){
|
||||
@@ -239,6 +253,8 @@ public class Teams{
|
||||
}
|
||||
|
||||
public static class TeamData{
|
||||
private static final IntSeq derelictBuffer = new IntSeq();
|
||||
|
||||
public final Team team;
|
||||
|
||||
/** Handles building ""bases"". */
|
||||
@@ -316,6 +332,8 @@ public class Teams{
|
||||
}
|
||||
}
|
||||
|
||||
finishScheduleDerelict();
|
||||
|
||||
//kill all units randomly
|
||||
units.each(u -> Time.run(Mathf.random(0f, 60f * 5f), () -> {
|
||||
//ensure unit hasn't switched teams for whatever reason
|
||||
@@ -325,21 +343,7 @@ public class Teams{
|
||||
}));
|
||||
}
|
||||
|
||||
/** Make all buildings within this range derelict / explode. */
|
||||
public void makeDerelict(float x, float y, float range){
|
||||
var builds = new Seq<Building>();
|
||||
if(buildingTree != null){
|
||||
buildingTree.intersect(x - range, y - range, range * 2f, range * 2f, builds);
|
||||
}
|
||||
|
||||
for(var build : builds){
|
||||
if(build.within(x, y, range) && !build.block.privileged){
|
||||
scheduleDerelict(build);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Make all buildings within this range explode. */
|
||||
/** Make all buildings within this range derelict/explode. */
|
||||
public void timeDestroy(float x, float y, float range){
|
||||
var builds = new Seq<Building>();
|
||||
if(buildingTree != null){
|
||||
@@ -347,23 +351,31 @@ public class Teams{
|
||||
}
|
||||
|
||||
for(var build : builds){
|
||||
if(build.within(x, y, range) && !cores.contains(c -> c.within(build, range))){
|
||||
//TODO GPU driver bugs?
|
||||
build.kill();
|
||||
//Time.run(Mathf.random(0f, 60f * 6f), build::kill);
|
||||
if(!build.block.privileged && build.within(x, y, range) && !cores.contains(c -> c.within(build, range))){
|
||||
scheduleDerelict(build);
|
||||
}
|
||||
}
|
||||
finishScheduleDerelict();
|
||||
}
|
||||
|
||||
private void scheduleDerelict(Building build){
|
||||
//TODO this may cause a lot of packet spam, optimize?
|
||||
Call.setTeam(build, Team.derelict);
|
||||
//queue block to be handled later, avoid packet spam
|
||||
derelictBuffer.add(build.pos());
|
||||
|
||||
if(Mathf.chance(0.25)){
|
||||
if(build.getPayload() instanceof UnitPayload){
|
||||
Call.destroyPayload(build);
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.2)){
|
||||
Time.run(Mathf.random(0f, 60f * 6f), build::kill);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishScheduleDerelict(){
|
||||
derelictBuffer.chunked(1000, values -> Call.setTeams(values, Team.derelict));
|
||||
derelictBuffer.clear();
|
||||
}
|
||||
|
||||
//this is just an alias for consistency
|
||||
@Nullable
|
||||
public Seq<Unit> getUnits(UnitType type){
|
||||
@@ -425,14 +437,23 @@ public class Teams{
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void destroyPayload(Building build){
|
||||
if(build != null && build.getPayload() instanceof UnitPayload && build.takePayload() instanceof UnitPayload unit){
|
||||
unit.dump();
|
||||
unit.unit.killed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a block made by this team that was destroyed somewhere on the map.
|
||||
* This does not include deconstructed blocks.*/
|
||||
public static class BlockPlan{
|
||||
public final short x, y, rotation, block;
|
||||
public final short x, y, rotation;
|
||||
public final Block block;
|
||||
public final Object config;
|
||||
public boolean removed;
|
||||
|
||||
public BlockPlan(int x, int y, short rotation, short block, Object config){
|
||||
public BlockPlan(int x, int y, short rotation, Block block, Object config){
|
||||
this.x = (short)x;
|
||||
this.y = (short)y;
|
||||
this.rotation = rotation;
|
||||
|
||||
@@ -6,6 +6,7 @@ import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Schematic.*;
|
||||
import mindustry.game.SectorInfo.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.maps.*;
|
||||
@@ -115,6 +116,11 @@ public class Universe{
|
||||
Core.settings.putJson("launch-resources-seq", lastLaunchResources);
|
||||
}
|
||||
|
||||
/** Updates selected loadout for future deployment. Creates an empty schematic with a single core block. */
|
||||
public void updateLoadout(CoreBlock block){
|
||||
updateLoadout(block, new Schematic(Seq.with(new Stile(block, 0, 0, null, (byte)0)), new StringMap(), block.size, block.size));
|
||||
}
|
||||
|
||||
/** Updates selected loadout for future deployment. */
|
||||
public void updateLoadout(CoreBlock block, Schematic schem){
|
||||
Core.settings.put("lastloadout-" + block.name, schem.file == null ? "" : schem.file.nameWithoutExtension());
|
||||
@@ -157,26 +163,33 @@ public class Universe{
|
||||
continue;
|
||||
}
|
||||
|
||||
//first pass: clear import stats
|
||||
for(Sector sector : planet.sectors){
|
||||
if(sector.hasBase() && !sector.isBeingPlayed()){
|
||||
sector.info.lastImported.clear();
|
||||
}
|
||||
//don't simulate the planet if there is an in-progress mission on that planet
|
||||
if(!planet.allowWaveSimulation && planet.sectors.contains(s -> s.hasBase() && !s.isBeingPlayed() && s.isAttacked())){
|
||||
continue;
|
||||
}
|
||||
|
||||
//second pass: update export & import statistics
|
||||
for(Sector sector : planet.sectors){
|
||||
if(sector.hasBase() && !sector.isBeingPlayed()){
|
||||
if(planet.campaignRules.legacyLaunchPads){
|
||||
//first pass: clear import stats
|
||||
for(Sector sector : planet.sectors){
|
||||
if(sector.hasBase() && !sector.isBeingPlayed()){
|
||||
sector.info.lastImported.clear();
|
||||
}
|
||||
}
|
||||
|
||||
//export to another sector
|
||||
if(sector.info.destination != null){
|
||||
Sector to = sector.info.destination;
|
||||
if(to.hasBase() && to.planet == planet){
|
||||
ItemSeq items = new ItemSeq();
|
||||
//calculated exported items to this sector
|
||||
sector.info.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed * sector.getProductionScale())));
|
||||
to.addItems(items);
|
||||
to.info.lastImported.add(items);
|
||||
//second pass: update export & import statistics
|
||||
for(Sector sector : planet.sectors){
|
||||
if(sector.hasBase() && !sector.isBeingPlayed()){
|
||||
|
||||
//export to another sector
|
||||
if(sector.info.destination != null){
|
||||
Sector to = sector.info.destination;
|
||||
if(to.hasBase() && to.planet == planet){
|
||||
ItemSeq items = new ItemSeq();
|
||||
//calculated exported items to this sector
|
||||
sector.info.export.each((item, stat) -> items.add(item, (int)(stat.mean * newSecondsPassed * sector.getProductionScale())));
|
||||
to.addItems(items);
|
||||
to.info.lastImported.add(items);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +198,9 @@ public class Universe{
|
||||
//third pass: everything else
|
||||
for(Sector sector : planet.sectors){
|
||||
if(sector.hasBase()){
|
||||
if(sector.info.importRateCache != null){
|
||||
sector.info.refreshImportRates(planet);
|
||||
}
|
||||
|
||||
//if it is being attacked, capture time is 0; otherwise, increment the timer
|
||||
if(sector.isAttacked()){
|
||||
@@ -196,6 +212,8 @@ public class Universe{
|
||||
//increment seconds passed for this sector by the time that just passed with this turn
|
||||
if(!sector.isBeingPlayed()){
|
||||
|
||||
//TODO: if a planet has sectors under attack and simulation is OFF, just don't simulate it
|
||||
|
||||
//increment time if attacked
|
||||
if(sector.isAttacked()){
|
||||
sector.info.secondsPassed += turnDuration/60f;
|
||||
@@ -238,12 +256,15 @@ public class Universe{
|
||||
//add production, making sure that it's capped
|
||||
sector.info.production.each((item, stat) -> sector.info.items.add(item, Math.min((int)(stat.mean * newSecondsPassed * scl), sector.info.storageCapacity - sector.info.items.get(item))));
|
||||
|
||||
sector.info.export.each((item, stat) -> {
|
||||
if(sector.info.items.get(item) <= 0 && sector.info.production.get(item, ExportStat::new).mean < 0 && stat.mean > 0){
|
||||
//cap export by import when production is negative.
|
||||
stat.mean = Math.min(sector.info.lastImported.get(item) / (float)newSecondsPassed, stat.mean);
|
||||
}
|
||||
});
|
||||
if(planet.campaignRules.legacyLaunchPads){
|
||||
sector.info.export.each((item, stat) -> {
|
||||
if(sector.info.items.get(item) <= 0 && sector.info.production.get(item, ExportStat::new).mean < 0 && stat.mean > 0){
|
||||
//cap export by import when production is negative.
|
||||
//TODO remove
|
||||
stat.mean = Math.min(sector.info.lastImported.get(item) / (float)newSecondsPassed, stat.mean);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//prevent negative values with unloaders
|
||||
sector.info.items.checkNegative();
|
||||
@@ -252,7 +273,7 @@ public class Universe{
|
||||
}
|
||||
|
||||
//queue random invasions
|
||||
if(!sector.isAttacked() && sector.planet.allowSectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){
|
||||
if(!sector.isAttacked() && sector.planet.campaignRules.sectorInvasion && sector.info.minutesCaptured > invasionGracePeriod && sector.info.hasSpawns){
|
||||
int count = sector.near().count(s -> s.hasEnemyBase() && !s.hasBase());
|
||||
|
||||
//invasion chance depends on # of nearby bases
|
||||
|
||||
@@ -274,7 +274,7 @@ public class BlockRenderer{
|
||||
|
||||
if(brokenFade > 0.001f){
|
||||
for(BlockPlan block : player.team().data().plans){
|
||||
Block b = content.block(block.block);
|
||||
Block b = block.block;
|
||||
if(!camera.bounds(Tmp.r1).grow(tilesize * 2f).overlaps(Tmp.r2.setSize(b.size * tilesize).setCenter(block.x * tilesize + b.offset, block.y * tilesize + b.offset))) continue;
|
||||
|
||||
Draw.alpha(0.33f * brokenFade);
|
||||
|
||||
@@ -141,11 +141,18 @@ public class Drawf{
|
||||
Draw.z(pz);
|
||||
}
|
||||
|
||||
public static void limitLine(Position start, Position dest, float len1, float len2){
|
||||
public static void limitLine(Position start, Position dest, float len1, float len2, Color color){
|
||||
if(start.within(dest, len1 + len2)){
|
||||
return;
|
||||
}
|
||||
Tmp.v1.set(dest).sub(start).setLength(len1);
|
||||
Tmp.v2.set(Tmp.v1).scl(-1f).setLength(len2);
|
||||
|
||||
Drawf.line(Pal.accent, start.getX() + Tmp.v1.x, start.getY() + Tmp.v1.y, dest.getX() + Tmp.v2.x, dest.getY() + Tmp.v2.y);
|
||||
Drawf.line(color, start.getX() + Tmp.v1.x, start.getY() + Tmp.v1.y, dest.getX() + Tmp.v2.x, dest.getY() + Tmp.v2.y);
|
||||
}
|
||||
|
||||
public static void limitLine(Position start, Position dest, float len1, float len2){
|
||||
limitLine(start, dest, len1, len2, Pal.accent);
|
||||
}
|
||||
|
||||
public static void dashLineDst(Color color, float x, float y, float x2, float y2){
|
||||
@@ -306,7 +313,7 @@ public class Drawf{
|
||||
Draw.rect(region, x, y);
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
|
||||
public static void shadow(TextureRegion region, float x, float y, float width, float height, float rotation){
|
||||
Draw.color(Pal.shadow);
|
||||
Draw.rect(region, x, y, width, height, rotation);
|
||||
@@ -354,13 +361,21 @@ public class Drawf{
|
||||
}
|
||||
|
||||
public static void square(float x, float y, float radius, float rotation, Color color){
|
||||
Lines.stroke(3f, Pal.gray);
|
||||
Lines.stroke(3f, Pal.gray.write(Tmp.c3).a(color.a));
|
||||
Lines.square(x, y, radius + 1f, rotation);
|
||||
Lines.stroke(1f, color);
|
||||
Lines.square(x, y, radius + 1f, rotation);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public static void poly(float x, float y, int sides, float radius, float rotation, Color color){
|
||||
Lines.stroke(3f, Pal.gray);
|
||||
Lines.poly(x, y, sides, radius + 1f, rotation);
|
||||
Lines.stroke(1f, color);
|
||||
Lines.poly(x, y, sides, radius + 1f, rotation);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public static void square(float x, float y, float radius, float rotation){
|
||||
square(x, y, radius, rotation, Pal.accent);
|
||||
}
|
||||
@@ -436,7 +451,7 @@ public class Drawf{
|
||||
public static void construct(float x, float y, TextureRegion region, float rotation, float progress, float alpha, float time){
|
||||
construct(x, y, region, Pal.accent, rotation, progress, alpha, time);
|
||||
}
|
||||
|
||||
|
||||
public static void construct(float x, float y, TextureRegion region, Color color, float rotation, float progress, float alpha, float time){
|
||||
Shaders.build.region = region;
|
||||
Shaders.build.progress = progress;
|
||||
@@ -458,7 +473,7 @@ public class Drawf{
|
||||
public static void construct(Building t, TextureRegion region, Color color, float rotation, float progress, float alpha, float time){
|
||||
construct(t, region, color, rotation, progress, alpha, time, t.block.size * tilesize - 4f);
|
||||
}
|
||||
|
||||
|
||||
public static void construct(Building t, TextureRegion region, Color color, float rotation, float progress, float alpha, float time, float size){
|
||||
Shaders.build.region = region;
|
||||
Shaders.build.progress = progress;
|
||||
@@ -477,7 +492,7 @@ public class Drawf{
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
|
||||
/** Draws a sprite that should be light-wise correct, when rotated. Provided sprite must be symmetrical in shape. */
|
||||
public static void spinSprite(TextureRegion region, float x, float y, float r){
|
||||
float a = Draw.getColorAlpha();
|
||||
|
||||
@@ -16,7 +16,6 @@ import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Highly experimental fog-of-war renderer. */
|
||||
public final class FogRenderer{
|
||||
private FrameBuffer staticFog = new FrameBuffer(), dynamicFog = new FrameBuffer();
|
||||
private LongSeq events = new LongSeq();
|
||||
|
||||
@@ -116,8 +116,7 @@ public class MultiPacker implements Disposable{
|
||||
//main page can be massive, but 8192 throws GL_OUT_OF_MEMORY on some GPUs and I can't deal with it yet.
|
||||
main(4096),
|
||||
|
||||
//TODO stuff like this throws OOM on some devices
|
||||
environment(4096, 2048),
|
||||
environment(4096),
|
||||
ui(4096),
|
||||
rubble(4096, 2048),
|
||||
editor(4096, 2048);
|
||||
|
||||
28
core/src/mindustry/graphics/NvGpuInfo.java
Normal file
28
core/src/mindustry/graphics/NvGpuInfo.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package mindustry.graphics;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
|
||||
/** Nvidia-specific utility class for querying GPU VRAM information. */
|
||||
public class NvGpuInfo{
|
||||
private static final int GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX = 0x9048;
|
||||
private static final int GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX = 0x9049;
|
||||
|
||||
private static boolean supported, initialized;
|
||||
|
||||
public static int getMaxMemoryKB(){
|
||||
return hasMemoryInfo() ? Gl.getInt(GL_GPU_MEM_INFO_TOTAL_AVAILABLE_MEM_NVX) : 0;
|
||||
}
|
||||
|
||||
public static int getAvailableMemoryKB(){
|
||||
return hasMemoryInfo() ? Gl.getInt(GL_GPU_MEM_INFO_CURRENT_AVAILABLE_MEM_NVX) : 0;
|
||||
}
|
||||
|
||||
public static boolean hasMemoryInfo(){
|
||||
if(!initialized){
|
||||
supported = Core.graphics.supportsExtension("GL_NVX_gpu_memory_info");
|
||||
initialized = true;
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ public class OverlayRenderer{
|
||||
}
|
||||
|
||||
input.drawTop();
|
||||
input.drawUnitSelection();
|
||||
|
||||
buildFade = Mathf.lerpDelta(buildFade, input.isPlacing() || input.isUsingSchematic() ? 1f : 0f, 0.06f);
|
||||
|
||||
@@ -177,11 +178,12 @@ public class OverlayRenderer{
|
||||
}else{
|
||||
state.teams.eachEnemyCore(player.team(), core -> {
|
||||
//it must be clear that there is a core here.
|
||||
if(/*core.wasVisible && */Core.camera.bounds(Tmp.r1).overlaps(Tmp.r2.setCentered(core.x, core.y, state.rules.enemyCoreBuildRadius * 2f))){
|
||||
float br = state.rules.buildRadius(core.team);
|
||||
if(/*core.wasVisible && */Core.camera.bounds(Tmp.r1).overlaps(Tmp.r2.setCentered(core.x, core.y, br * 2f))){
|
||||
Draw.color(Color.darkGray);
|
||||
Lines.circle(core.x, core.y - 2, state.rules.enemyCoreBuildRadius);
|
||||
Lines.circle(core.x, core.y - 2,br);
|
||||
Draw.color(Pal.accent, core.team.color, 0.5f + Mathf.absin(Time.time, 10f, 0.5f));
|
||||
Lines.circle(core.x, core.y, state.rules.enemyCoreBuildRadius);
|
||||
Lines.circle(core.x, core.y, br);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import arc.graphics.*;
|
||||
public class Pal{
|
||||
public static Color
|
||||
|
||||
water = Color.valueOf("596ab8"),
|
||||
darkOutline = Color.valueOf("2d2f39"),
|
||||
thoriumPink = Color.valueOf("f9a3c7"),
|
||||
coalBlack = Color.valueOf("272727"),
|
||||
@@ -107,7 +108,7 @@ public class Pal{
|
||||
redderDust = Color.valueOf("ff7b69"),
|
||||
|
||||
plasticSmoke = Color.valueOf("f1e479"),
|
||||
|
||||
|
||||
adminChat = Color.valueOf("ff4000"),
|
||||
|
||||
neoplasmOutline = Color.valueOf("2e191d"),
|
||||
|
||||
@@ -27,7 +27,7 @@ public class HexSkyMesh extends PlanetMesh{
|
||||
|
||||
@Override
|
||||
public boolean skip(Vec3 position){
|
||||
return Simplex.noise3d(planet.id + seed, octaves, persistence, scl, position.x, position.y * 3f, position.z) >= thresh;
|
||||
return Simplex.noise3d(7 + seed, octaves, persistence, scl, position.x, position.y * 3f, position.z) >= thresh;
|
||||
}
|
||||
}, divisions, false, planet.radius, radius), Shaders.clouds);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user