Merge branch 'master' into mod-dependencies

This commit is contained in:
MEEPofFaith
2025-02-08 20:48:39 -08:00
424 changed files with 17880 additions and 8956 deletions

View File

@@ -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;

View File

@@ -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));
}
}
}
}

View File

@@ -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){

View File

@@ -258,7 +258,7 @@ public class BaseBuilderAI{
//queue it
for(Stile tile : result.tiles){
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block.id, tile.config));
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block, tile.config));
}
return true;

View File

@@ -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)

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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;
}};
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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;
}*/
}

View File

@@ -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;

View File

@@ -10,6 +10,11 @@ import mindustry.gen.*;
public class MissileAI extends AIController{
public @Nullable Unit shooter;
@Override
protected void resetTimers(){
timer.reset(timerTarget, 5f);
}
@Override
public void updateMovement(){
unloadPayloads();
@@ -33,7 +38,7 @@ public class MissileAI extends AIController{
@Override
public Teamc target(float x, float y, float range, boolean air, boolean ground){
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground) && !u.isMissile(), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
}
@Override

View File

@@ -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

View File

@@ -10,7 +10,7 @@ import mindustry.entities.bullet.*;
public class Bullets{
public static BulletType
placeholder, spaceLiquid, damageLightning, damageLightningGround, fireball;
placeholder, spaceLiquid, damageLightning, damageLightningGround, damageLightningAir, fireball;
public static void load(){
@@ -37,6 +37,10 @@ public class Bullets{
damageLightningGround = damageLightning.copy();
damageLightningGround.collidesAir = false;
damageLightningAir = damageLightning.copy();
damageLightningAir.collidesGround = false;
damageLightningAir.collidesTiles = false;
fireball = new FireBulletType(1f, 4){{
hittable = false;
}};

View File

@@ -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, () -> {
});
});
});
});

View File

@@ -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;

View File

@@ -60,7 +60,7 @@ public class Items{
}};
scrap = new Item("scrap", Color.valueOf("777777")){{
cost = 0.5f;
}};
silicon = new Item("silicon", Color.valueOf("53565c")){{

View File

@@ -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)))
);

View File

@@ -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

View File

@@ -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),

View File

@@ -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;

View File

@@ -99,6 +99,7 @@ public class TechTree{
public TechNode(@Nullable TechNode parent, UnlockableContent content, ItemStack[] requirements){
if(parent != null){
parent.children.add(this);
planet = parent.planet;
researchCostMultipliers = parent.researchCostMultipliers;
}else if(researchCostMultipliers == null){
researchCostMultipliers = new ObjectFloatMap<>();
@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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(){

View File

@@ -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){

View File

@@ -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();

View File

@@ -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);
}
});

View File

@@ -20,15 +20,14 @@ import mindustry.graphics.*;
import mindustry.graphics.g3d.*;
import mindustry.maps.*;
import mindustry.type.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.blocks.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class Renderer implements ApplicationListener{
/** These are global variables, for headless access. Cached. */
public static float laserOpacity = 0.5f, bridgeOpacity = 0.75f;
public static float laserOpacity = 0.5f, unitLaserOpacity = 1f, bridgeOpacity = 0.75f;
public final BlockRenderer blocks = new BlockRenderer();
public final FogRenderer fog = new FogRenderer();
@@ -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(){

View File

@@ -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;

View File

@@ -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 + ")");
}
}

View File

@@ -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)));

View File

@@ -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.

View File

@@ -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();
}

View 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));
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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){

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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){

View File

@@ -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);
}
});

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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)));
}

View File

@@ -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;
}

View File

@@ -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);
});
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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();
}

View 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;
}
}

View File

@@ -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);

View File

@@ -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{

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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. */

View File

@@ -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());
}
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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());
}
}
}
}

View File

@@ -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){

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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),

View File

@@ -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);

View File

@@ -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);

View File

@@ -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()));
}

View File

@@ -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);
}

View File

@@ -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){

View 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;
}
}

View 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());
}
}

View File

@@ -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. */

View File

@@ -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);
}

View File

@@ -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) : "???");
}
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View 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;
}
}

View File

@@ -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);
}
});
}

View File

@@ -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"),

View File

@@ -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