Merge branch 'master' into balancing_burst-drill-optional-multiplier

This commit is contained in:
SomeonesShade
2025-01-22 17:46:10 +08:00
committed by GitHub
329 changed files with 9170 additions and 3995 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 */

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();
@@ -85,8 +88,8 @@ public class CommandAI extends AIController{
}
//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 +116,18 @@ 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);
}
}
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 +135,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
@@ -218,8 +227,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 +275,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 +343,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 +402,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 +417,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 +463,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 +516,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){
@@ -1703,6 +1790,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);

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,6 +85,8 @@ public class Planets{
r.coreDestroyClear = true;
r.onlyDepositCore = true;
};
campaignRuleDefaults.fog = true;
campaignRuleDefaults.showSpawns = true;
unlockedOnLand.add(Blocks.coreBastion);
}};
@@ -144,6 +145,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");
@@ -152,7 +154,6 @@ public class Planets{
startSector = 15;
alwaysUnlocked = 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 -> {

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;
@@ -122,7 +123,7 @@ public class SerpuloTechTree{
});
node(pyratiteMixer, () -> {
node(blastMixer, () -> {
node(blastMixer, Seq.with(new SectorComplete(facility32m)), () -> {
});
});
@@ -138,7 +139,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 +262,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 +369,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 +408,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 +436,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 +457,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 +480,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 +520,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(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 +596,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 +646,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 +672,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 +690,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

@@ -139,6 +139,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));
@@ -617,6 +617,7 @@ public class UnitTypes{
weapons.add(new Weapon(){{
shootOnDeath = true;
targetUnderBlocks = false;
reload = 24f;
shootCone = 180f;
ejectEffect = Fx.none;
@@ -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());
}
@@ -171,6 +173,13 @@ public class ContentLoader{
public void handleMappableContent(MappableContent content){
if(contentNameMap[content.getContentType().ordinal()].containsKey(content.name)){
var list = contentMap[content.getContentType().ordinal()];
//this method is only called when registering content, and after handleContent.
//If this is the last registered content, and it is invalid, make sure to remove it from the list to prevent invalid stuff from being registered
if(list.size > 0 && list.peek() == content){
list.pop();
}
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + content.name + "')");
}
if(currentMod != null){
@@ -180,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 -> {
@@ -404,7 +404,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 +417,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 +441,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 +458,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));

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);
}
}
@@ -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);
@@ -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;
@@ -1058,7 +1082,7 @@ public class NetServer implements ApplicationListener{
try{
writeEntitySnapshot(player);
}catch(IOException e){
e.printStackTrace();
Log.err(e);
}
});

View File

@@ -339,6 +339,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){
@@ -548,6 +550,7 @@ public class Renderer implements ApplicationListener{
public void showLaunch(CoreBuild landCore, CoreBlock coreType){
control.input.config.hideConfig();
control.input.planConfig.hide();
control.input.inv.hide();
this.landCore = landCore;

View File

@@ -158,7 +158,7 @@ public class UI implements ApplicationListener, Loadable{
Core.scene.draw();
if(Core.input.keyTap(KeyCode.mouseLeft) && Core.scene.hasField()){
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(!(e instanceof TextField)){
Core.scene.setKeyboardFocus(null);
}
@@ -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.*;
@@ -35,8 +36,6 @@ public abstract class UnlockableContent extends MappableContent{
public boolean hideDetails = true;
/** 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 +44,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,10 +73,17 @@ 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 =
Core.atlas.find(fullOverride,
Core.atlas.find(fullOverride == null ? "" : fullOverride,
Core.atlas.find(getContentType().name() + "-" + name + "-full",
Core.atlas.find(name + "-full",
Core.atlas.find(name,
@@ -74,6 +93,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 +223,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 +252,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

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

@@ -329,7 +329,7 @@ public class MapView extends Element implements GestureListener{
return Core.scene != null && Core.scene.getKeyboardFocus() != null
&& Core.scene.getKeyboardFocus().isDescendantOf(ui.editor)
&& ui.editor.isShown() && tool == EditorTool.zoom &&
Core.scene.hit(Core.input.mouse().x, Core.input.mouse().y, true) == this;
Core.scene.getHoverElement() == this;
}
@Override

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

@@ -105,6 +105,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 +140,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

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

@@ -102,6 +102,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,6 +141,8 @@ 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;
/** % of block health healed **/
@@ -346,7 +352,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 +527,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 +555,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);
@@ -675,7 +681,7 @@ public class BulletType extends Content implements Cloneable{
}
}
}
public void updateTrail(Bullet b){
if(!headless && trailLength > 0){
if(b.trail == null){
@@ -710,13 +716,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 +757,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 +781,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 +823,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

@@ -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){
@@ -1217,6 +1217,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
public void payloadDraw(){
if(block.isAir()) return;
draw();
}
@@ -1370,6 +1371,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Called when the block is destroyed. The tile is still intact at this stage. */
public void onDestroyed(){
if(sound != null){
sound.stop();
}
float explosiveness = block.baseExplosiveness;
float flammability = 0f;
float power = 0f;

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

@@ -90,6 +90,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 +131,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 +148,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 +162,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

@@ -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.*;
@@ -443,6 +445,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);
}
@@ -713,7 +719,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

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

@@ -15,7 +15,7 @@ public class RegionPart extends DrawPart{
public String suffix = "";
/** Overrides suffix if set. */
public @Nullable String name;
public TextureRegion heat;
public TextureRegion heat, light;
public TextureRegion[] regions = {};
public TextureRegion[] outlines = {};
@@ -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,10 +132,10 @@ 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, heat, rot, Tmp.c1, heatLightOpacity * hprog);
if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog);
}
Draw.xscl *= sign;
@@ -187,6 +189,7 @@ public class RegionPart extends DrawPart{
}
heat = Core.atlas.find(realName + "-heat");
light = Core.atlas.find(realName + "-light");
for(var child : children){
child.turretShading = turretShading;
child.load(name);

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

@@ -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. */
@@ -85,6 +88,8 @@ public class Rules{
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 +102,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 +138,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 +161,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. */

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

@@ -38,6 +38,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. */
@@ -82,7 +84,7 @@ public class SectorInfo{
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;
@@ -175,6 +177,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;

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.*;
@@ -239,6 +240,8 @@ public class Teams{
}
public static class TeamData{
private static final IntSeq derelictBuffer = new IntSeq();
public final Team team;
/** Handles building ""bases"". */
@@ -316,6 +319,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 +330,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 +338,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 +424,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

@@ -252,7 +252,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);

View File

@@ -60,6 +60,7 @@ public enum Binding implements KeyBind{
unit_command_load_units(KeyCode.unset),
unit_command_load_blocks(KeyCode.unset),
unit_command_unload_payload(KeyCode.unset),
unit_command_loop_payload(KeyCode.unset),
category_prev(KeyCode.comma, "blocks"),
category_next(KeyCode.period),
@@ -80,6 +81,7 @@ public enum Binding implements KeyBind{
block_select_10(KeyCode.num0),
zoom(new Axis(KeyCode.scroll), "view"),
detach_camera(KeyCode.unset),
menu(Vars.android ? KeyCode.back : KeyCode.escape),
fullscreen(KeyCode.f11),
pause(KeyCode.space),

View File

@@ -56,6 +56,8 @@ public class DesktopInput extends InputHandler{
/** Time of most recent control group selection */
public long lastCtrlGroupSelectMillis;
private float buildPlanMouseOffsetX, buildPlanMouseOffsetY;
boolean showHint(){
return ui.hudfrag.shown && Core.settings.getBool("hints") && selectPlans.isEmpty() && !player.dead() &&
(!isBuilding && !Core.settings.getBool("buildautopause") || player.unit().isBuilding() || !player.dead() && !player.unit().spawnedByCore());
@@ -108,6 +110,10 @@ public class DesktopInput extends InputHandler{
@Override
public void drawTop(){
if(cursorType != SystemCursor.arrow && scene.hasMouse()){
graphics.cursor(cursorType = SystemCursor.arrow);
}
Lines.stroke(1f);
int cursorX = tileX(Core.input.mouseX());
int cursorY = tileY(Core.input.mouseY());
@@ -126,9 +132,6 @@ public class DesktopInput extends InputHandler{
}
}
drawCommanded();
Draw.reset();
}
@@ -222,28 +225,46 @@ public class DesktopInput extends InputHandler{
boolean locked = locked();
boolean panCam = false;
float camSpeed = (!Core.input.keyDown(Binding.boost) ? panSpeed : panBoostSpeed) * Time.delta;
boolean detached = settings.getBool("detach-camera", false);
if(input.keyDown(Binding.pan) && !scene.hasField() && !scene.hasDialog()){
panCam = true;
panning = true;
if(!scene.hasField() && !scene.hasDialog()){
if(input.keyTap(Binding.detach_camera)){
settings.put("detach-camera", detached = !detached);
if(!detached){
panning = false;
}
spectating = null;
}
if(input.keyDown(Binding.pan)){
panCam = true;
panning = true;
spectating = null;
}
if((Math.abs(Core.input.axis(Binding.move_x)) > 0 || Math.abs(Core.input.axis(Binding.move_y)) > 0 || input.keyDown(Binding.mouse_move))){
panning = false;
spectating = null;
}
}
if((Math.abs(Core.input.axis(Binding.move_x)) > 0 || Math.abs(Core.input.axis(Binding.move_y)) > 0 || input.keyDown(Binding.mouse_move)) && (!scene.hasField())){
panning = false;
}
panning |= detached;
if(!locked){
if(((player.dead() || state.isPaused()) && !ui.chatfrag.shown()) && !scene.hasField() && !scene.hasDialog()){
if(((player.dead() || state.isPaused() || detached) && !ui.chatfrag.shown()) && !scene.hasField() && !scene.hasDialog()){
if(input.keyDown(Binding.mouse_move)){
panCam = true;
}
Core.camera.position.add(Tmp.v1.setZero().add(Core.input.axis(Binding.move_x), Core.input.axis(Binding.move_y)).nor().scl(camSpeed));
}else if(!player.dead() && !panning){
}else if((!player.dead() || spectating != null) && !panning){
//TODO do not pan
Team corePanTeam = state.won ? state.rules.waveTeam : player.team();
Position coreTarget = state.gameOver && !state.rules.pvp && corePanTeam.data().lastCore != null ? corePanTeam.data().lastCore : null;
Core.camera.position.lerpDelta(coreTarget != null ? coreTarget : player, Core.settings.getBool("smoothcamera") ? 0.08f : 1f);
Position panTarget = coreTarget != null ? coreTarget : spectating != null ? spectating : player;
Core.camera.position.lerpDelta(panTarget, Core.settings.getBool("smoothcamera") ? 0.08f : 1f);
}
if(panCam){
@@ -446,12 +467,14 @@ public class DesktopInput extends InputHandler{
Tile cursor = tileAt(Core.input.mouseX(), Core.input.mouseY());
cursorType = SystemCursor.arrow;
if(cursor != null){
if(cursor.build != null && cursor.build.interactable(player.team())){
cursorType = cursor.build.getCursor();
}
if(cursor.build != null && player.team() != Team.derelict && cursor.build.team == Team.derelict && Build.validPlace(cursor.block(), player.team(), cursor.build.tileX(), cursor.build.tileY(), cursor.build.rotation)){
if(canRepairDerelict(cursor)){
cursorType = ui.repairCursor;
}
@@ -498,9 +521,9 @@ public class DesktopInput extends InputHandler{
if(!Core.scene.hasMouse()){
Core.graphics.cursor(cursorType);
}else{
cursorType = SystemCursor.arrow;
}
cursorType = SystemCursor.arrow;
}
@Override
@@ -521,8 +544,6 @@ public class DesktopInput extends InputHandler{
@Override
public void buildPlacementUI(Table table){
table.image().color(Pal.gray).height(4f).colspan(4).growX();
table.row();
table.left().margin(0f).defaults().size(48f).left();
table.button(Icon.paste, Styles.clearNonei, () -> {
@@ -622,11 +643,10 @@ public class DesktopInput extends InputHandler{
}
if(splan != null){
float offset = ((splan.block.size + 2) % 2) * tilesize / 2f;
float x = Core.input.mouseWorld().x + offset;
float y = Core.input.mouseWorld().y + offset;
splan.x = (int)(x / tilesize);
splan.y = (int)(y / tilesize);
float x = Core.input.mouseWorld().x + buildPlanMouseOffsetX;
float y = Core.input.mouseWorld().y + buildPlanMouseOffsetY;
splan.x = Math.round(x / tilesize);
splan.y = Math.round(y / tilesize);
}
if(block == null || mode != placing){
@@ -657,6 +677,15 @@ public class DesktopInput extends InputHandler{
tappedOne = false;
BuildPlan plan = getPlan(cursorX, cursorY);
if(plan != null){
//move selected to front
int index = player.unit().plans.indexOf(plan, true);
if(index != -1){
player.unit().plans.removeIndex(index);
player.unit().plans.addFirst(plan);
}
}
if(Core.input.keyDown(Binding.break_block)){
mode = none;
}else if(!selectPlans.isEmpty()){
@@ -668,8 +697,10 @@ public class DesktopInput extends InputHandler{
lastLineY = cursorY;
mode = placing;
updateLine(selectX, selectY);
}else if(plan != null && !plan.breaking && mode == none && !plan.initialized){
}else if(plan != null && !plan.breaking && mode == none && !plan.initialized && plan.progress <= 0f){
splan = plan;
buildPlanMouseOffsetX = splan.x * tilesize - Core.input.mouseWorld().x;
buildPlanMouseOffsetY = splan.y * tilesize - Core.input.mouseWorld().y;
}else if(plan != null && plan.breaking){
deleting = true;
}else if(commandMode){
@@ -754,6 +785,15 @@ public class DesktopInput extends InputHandler{
if(getPlan(splan.x, splan.y, splan.block.size, splan) != null){
player.unit().plans().remove(splan, true);
}
if(input.ctrl()){
inv.hide();
config.hideConfig();
planConfig.showConfig(splan);
}else{
planConfig.hide();
}
splan = null;
}
@@ -853,9 +893,20 @@ public class DesktopInput extends InputHandler{
float ya = Core.input.axis(Binding.move_y);
boolean boosted = (unit instanceof Mechc && unit.isFlying());
movement.set(xa, ya).nor().scl(speed);
if(Core.input.keyDown(Binding.mouse_move)){
movement.add(input.mouseWorld().sub(player).scl(1f / 25f * speed)).limit(speed);
if(settings.getBool("detach-camera")){
Vec2 targetPos = camera.position;
movement.set(targetPos).sub(player).limit(speed);
if(player.within(targetPos, 15f)){
movement.setZero();
unit.vel.approachDelta(Vec2.ZERO, unit.speed() * unit.type().accel / 2f);
}
}else{
movement.set(xa, ya).nor().scl(speed);
if(Core.input.keyDown(Binding.mouse_move)){
movement.add(input.mouseWorld().sub(player).scl(1f / 25f * speed)).limit(speed);
}
}
float mouseAngle = Angles.mouseAngle(unit.x, unit.y);

View File

@@ -12,6 +12,7 @@ import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.struct.Queue;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
@@ -52,7 +53,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
/** Used for dropping items. */
final static float playerSelectRange = mobile ? 17f : 11f;
final static float unitSelectRadScl = 1f;
final static IntSeq removed = new IntSeq();
final static IntSet intSet = new IntSet();
/** Maximum line length. */
final static int maxLength = 100;
final static Rect r1 = new Rect(), r2 = new Rect();
@@ -96,6 +99,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
public BuildPlan bplan = new BuildPlan();
public Seq<BuildPlan> linePlans = new Seq<>();
public Seq<BuildPlan> selectPlans = new Seq<>(BuildPlan.class);
public Queue<BuildPlan> lastPlans = new Queue<>();
public @Nullable Unit lastUnit;
public @Nullable Unit spectating;
//for RTS controls
public Seq<Unit> selectedUnits = new Seq<>();
@@ -112,6 +118,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
public final BlockInventoryFragment inv;
public final BlockConfigFragment config;
public final PlanConfigFragment planConfig;
private WidgetGroup group = new WidgetGroup();
@@ -132,6 +139,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
group.touchable = Touchable.childrenOnly;
inv = new BlockInventoryFragment();
config = new BlockConfigFragment();
planConfig = new PlanConfigFragment();
Events.on(UnitDestroyEvent.class, e -> {
if(e.unit != null && e.unit.isPlayer() && e.unit.getPlayer().isLocal() && e.unit.type.weapons.contains(w -> w.bullet.killShooter)){
@@ -147,6 +155,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
logicCutscene = false;
itemDepositCooldown = 0f;
Arrays.fill(controlGroups, null);
lastUnit = null;
lastPlans.clear();
});
}
@@ -793,7 +803,16 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
return !selectPlans.isEmpty();
}
public void spectate(Unit unit){
spectating = unit;
camera.position.set(unit);
}
public void update(){
if(spectating != null && (!spectating.isValid() || spectating.team != player.team())){
spectating = null;
}
if(logicCutscene && !renderer.isCutscene()){
Core.camera.position.lerpDelta(logicCamPan, logicCamSpeed);
}else{
@@ -808,6 +827,24 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
commandRect = false;
}
if(player.isBuilder()){
var playerPlans = player.unit().plans;
if(player.unit() != lastUnit && playerPlans.size <= 1){
playerPlans.ensureCapacity(lastPlans.size);
for(var plan : lastPlans){
playerPlans.addLast(plan);
}
}
if(lastPlans.size != playerPlans.size || (lastPlans.size > 0 && playerPlans.size > 0 && lastPlans.first() != playerPlans.first())){
lastPlans.clear();
for(var plan : playerPlans){
lastPlans.addLast(plan);
}
}
}
lastUnit = player.unit();
playerPlanTree.clear();
if(!player.dead()){
player.unit().plans.each(playerPlanTree::insert);
@@ -830,7 +867,6 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//you don't want selected blocks while locked, looks weird
if(locked()){
block = null;
}
wasShooting = player.shooting;
@@ -1035,30 +1071,69 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
public void drawCommand(Unit sel){
Drawf.square(sel.x, sel.y, sel.hitSize / 1.4f + Mathf.absin(4f, 1f), selectedUnits.contains(sel) ? Pal.remove : Pal.accent);
Drawf.poly(sel.x, sel.y, 6, sel.hitSize / unitSelectRadScl + Mathf.absin(4f, 1f), 0f, selectedUnits.contains(sel) ? Pal.remove : Pal.accent);
}
public void drawCommanded(){
Draw.draw(Layer.plans, () -> {
drawCommanded(true);
});
Draw.draw(Layer.groundUnit - 1, () -> {
drawCommanded(false);
});
}
public void drawCommanded(boolean flying){
float lineLimit = 6.5f;
Color color = Pal.accent;
int sides = 6;
float alpha = 0.5f;
if(commandMode){
//happens sometimes
selectedUnits.removeAll(u -> !u.isCommandable());
//draw command overlay UI
for(Unit unit : selectedUnits){
if(unit.isFlying() != flying) continue;
CommandAI ai = unit.command();
Position lastPos = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
//draw target line
if(ai.targetPos != null && ai.currentCommand().drawTarget){
Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
Drawf.limitLine(unit, lineDest, unit.hitSize / 2f, 3.5f);
Drawf.limitLine(unit, lineDest, unit.hitSize / unitSelectRadScl + 1f, lineLimit, color.write(Tmp.c1).a(alpha));
if(ai.attackTarget == null){
Drawf.square(lineDest.getX(), lineDest.getY(), 3.5f);
Drawf.square(lineDest.getX(), lineDest.getY(), 3.5f, color.write(Tmp.c1).a(alpha));
if(ai.currentCommand() == UnitCommand.enterPayloadCommand){
var build = world.buildWorld(lineDest.getX(), lineDest.getY());
if(build != null && build.block.acceptsUnitPayloads && build.team == unit.team){
Drawf.selected(build, color);
}
}
}
}
Drawf.square(unit.x, unit.y, unit.hitSize / 1.4f + 1f);
float rad = unit.hitSize / unitSelectRadScl + 1f;
Fill.lightInner(unit.x, unit.y, sides,
Math.max(0f, rad * 0.8f),
rad,
0f,
Tmp.c3.set(color).a(0f),
Tmp.c2.set(color).a(0.7f)
);
Lines.stroke(1f);
Draw.color(color);
Lines.poly(unit.x, unit.y, sides, rad + 0.5f);
//uncomment for a dark border
//Draw.color(Pal.gray);
//Lines.poly(unit.x, unit.y, sides, rad + 1.5f);
Draw.reset();
if(ai.attackTarget != null && ai.currentCommand().drawTarget){
Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove);
@@ -1071,54 +1146,72 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//draw command queue
if(ai.currentCommand().drawTarget && ai.commandQueue.size > 0){
for(var next : ai.commandQueue){
Drawf.limitLine(lastPos, next, 3.5f, 3.5f);
Drawf.limitLine(lastPos, next, lineLimit, lineLimit, color.write(Tmp.c1).a(alpha));
lastPos = next;
if(next instanceof Vec2 vec){
Drawf.square(vec.x, vec.y, 3.5f);
Drawf.square(vec.x, vec.y, 3.5f, color.write(Tmp.c1).a(alpha));
}else{
Drawf.target(next.getX(), next.getY(), 6f, Pal.remove);
}
}
}
if(ai.targetPos != null && ai.currentCommand() == UnitCommand.loopPayloadCommand && unit instanceof Payloadc pay){
Draw.color(color, 0.4f + Mathf.absin(5f, 0.5f));
TextureRegion region = pay.hasPayload() ? Icon.download.getRegion() : Icon.upload.getRegion();
float offset = 11f;
float size = 8f;
Draw.rect(region, ai.targetPos.x, ai.targetPos.y + offset, size, size / region.ratio());
if(ai.commandQueue.size > 0){
region = !pay.hasPayload() ? Icon.download.getRegion() : Icon.upload.getRegion();
Draw.rect(region, ai.commandQueue.first().getX(), ai.commandQueue.first().getY() + offset, size, size / region.ratio());
}
Draw.color();
}
}
for(var commandBuild : commandBuildings){
if(commandBuild != null){
Drawf.square(commandBuild.x, commandBuild.y, commandBuild.hitSize() / 1.4f + 1f);
var cpos = commandBuild.getCommandPosition();
if(flying){
for(var commandBuild : commandBuildings){
if(commandBuild != null){
Drawf.square(commandBuild.x, commandBuild.y, commandBuild.hitSize() / 1.4f + 1f);
var cpos = commandBuild.getCommandPosition();
if(cpos != null){
Drawf.limitLine(commandBuild, cpos, commandBuild.hitSize() / 2f, 3.5f);
Drawf.square(cpos.x, cpos.y, 3.5f);
if(cpos != null){
Drawf.limitLine(commandBuild, cpos, commandBuild.hitSize() / 2f, lineLimit, color.write(Tmp.c1).a(alpha));
Drawf.square(cpos.x, cpos.y, 3.5f, color.write(Tmp.c1).a(alpha));
}
}
}
}
if(commandMode && !commandRect){
Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY());
if(sel != null && !(!multiUnitSelect() && selectedUnits.size == 1 && selectedUnits.contains(sel))){
drawCommand(sel);
}
}
if(commandRect){
float x2 = input.mouseWorldX(), y2 = input.mouseWorldY();
var units = selectedCommandUnits(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY);
for(var unit : units){
drawCommand(unit);
}
Draw.color(Pal.accent, 0.3f);
Fill.crect(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY);
}
}
Draw.reset();
}
public void drawUnitSelection(){
if(commandRect && commandMode){
float x2 = input.mouseWorldX(), y2 = input.mouseWorldY();
var units = selectedCommandUnits(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY);
for(var unit : units){
drawCommand(unit);
}
Draw.color(Pal.accent, 0.3f);
Fill.crect(commandRectX, commandRectY, x2 - commandRectX, y2 - commandRectY);
}
if(commandMode && !commandRect){
Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY());
if(sel != null && !(!multiUnitSelect() && selectedUnits.size == 1 && selectedUnits.contains(sel))){
drawCommand(sel);
}
}
}
public void drawBottom(){
}
@@ -1348,9 +1441,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
for(BlockPlan plan : player.team().data().plans){
Block block = content.block(plan.block);
Block block = plan.block;
if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){
drawSelected(plan.x, plan.y, content.block(plan.block), Pal.remove);
drawSelected(plan.x, plan.y, plan.block, Pal.remove);
}
}
@@ -1362,17 +1455,31 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
Lines.rect(result.x, result.y, result.x2 - result.x, result.y2 - result.y);
}
protected void drawRebuildSelection(int x, int y, int x2, int y2){
drawSelection(x, y, x2, y2, 0, Pal.sapBulletBack, Pal.sapBullet);
protected void drawRebuildSelection(int x1, int y1, int x2, int y2){
drawSelection(x1, y1, x2, y2, 0, Pal.sapBulletBack, Pal.sapBullet);
NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x, y, x2, y2, false, 0, 1f);
NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x1, y1, x2, y2, false, 0, 1f);
Tmp.r1.set(result.x, result.y, result.x2 - result.x, result.y2 - result.y);
for(BlockPlan plan : player.team().data().plans){
Block block = content.block(plan.block);
Block block = plan.block;
if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){
drawSelected(plan.x, plan.y, content.block(plan.block), Pal.sapBullet);
drawSelected(plan.x, plan.y, plan.block, Pal.sapBullet);
}
}
NormalizeResult dresult = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, 999999999);
intSet.clear();
for(int x = dresult.x; x <= dresult.x2; x++){
for(int y = dresult.y; y <= dresult.y2; y++){
Tile tile = world.tileBuilding(x, y);
if(tile != null && intSet.add(tile.pos()) && canRepairDerelict(tile)){
drawSelected(tile.x, tile.y, tile.block(), Pal.sapBullet);
}
}
}
}
@@ -1398,7 +1505,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
protected void flushSelectPlans(Seq<BuildPlan> plans){
for(BuildPlan plan : plans){
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation)){
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){
BuildPlan other = getPlan(plan.x, plan.y, plan.block.size, null);
if(other == null){
selectPlans.add(plan.copy());
@@ -1414,7 +1521,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//reversed iteration.
for(int i = plans.size - 1; i >= 0; i--){
var plan = plans.get(i);
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation)){
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){
BuildPlan copy = plan.copy();
plan.block.onNewPlan(copy);
player.unit().addBuild(copy, false);
@@ -1424,7 +1531,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
protected void flushPlans(Seq<BuildPlan> plans){
for(var plan : plans){
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation)){
if(plan.block != null && validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){
BuildPlan copy = plan.copy();
plan.block.onNewPlan(copy);
player.unit().addBuild(copy);
@@ -1522,7 +1629,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
Iterator<BlockPlan> broken = player.team().data().plans.iterator();
while(broken.hasNext()){
BlockPlan plan = broken.next();
Block block = content.block(plan.block);
Block block = plan.block;
if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){
removed.add(Point2.pack(plan.x, plan.y));
plan.removed = true;
@@ -1567,6 +1674,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
/** Handles tile tap events that are not platform specific. */
boolean tileTapped(@Nullable Building build){
planConfig.hide();
if(build == null){
inv.hide();
config.hideConfig();
@@ -1638,7 +1746,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
/** Tries to begin mining a tile, returns true if successful. */
boolean tryBeginMine(Tile tile){
if(canMine(tile)){
if(!player.dead() && canMine(tile)){
player.unit().mineTile = tile;
return true;
}
@@ -1647,7 +1755,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
/** Tries to stop mining, returns true if mining was stopped. */
boolean tryStopMine(){
if(player.unit().mining()){
if(!player.dead() && player.unit().mining()){
player.unit().mineTile = null;
return true;
}
@@ -1655,7 +1763,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
boolean tryStopMine(Tile tile){
if(player.unit().mineTile == tile){
if(!player.dead() && player.unit().mineTile == tile){
player.unit().mineTile = null;
return true;
}
@@ -1663,13 +1771,20 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
boolean tryRepairDerelict(Tile selected){
if(selected != null && player.team() != Team.derelict && selected.build != null && selected.build.block.unlockedNow() && selected.build.team == Team.derelict && Build.validPlace(selected.block(), player.team(), selected.build.tileX(), selected.build.tileY(), selected.build.rotation)){
if(selected != null && !state.rules.editor && player.team() != Team.derelict && selected.build != null && selected.build.block.unlockedNow() && selected.build.team == Team.derelict &&
Build.validPlace(selected.block(), player.team(), selected.build.tileX(), selected.build.tileY(), selected.build.rotation)){
player.unit().addBuild(new BuildPlan(selected.build.tileX(), selected.build.tileY(), selected.build.rotation, selected.block(), selected.build.config()));
return true;
}
return false;
}
boolean canRepairDerelict(Tile tile){
return tile != null && tile.build != null && !state.rules.editor && player.team() != Team.derelict && tile.build.team == Team.derelict && tile.build.block.unlockedNowHost() &&
Build.validPlace(tile.block(), player.team(), tile.build.tileX(), tile.build.tileY(), tile.build.rotation);
}
boolean canMine(Tile tile){
return !Core.scene.hasMouse()
&& player.unit().validMine(tile)
@@ -1835,6 +1950,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
inv.build(group);
config.build(group);
planConfig.build(group);
}
}
@@ -1877,16 +1993,28 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
}
public void rebuildArea(int x, int y, int x2, int y2){
NormalizeResult result = Placement.normalizeArea(x, y, x2, y2, rotation, false, 999999999);
public void rebuildArea(int x1, int y1, int x2, int y2){
NormalizeResult result = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, 999999999);
Tmp.r1.set(result.x * tilesize, result.y * tilesize, (result.x2 - result.x) * tilesize, (result.y2 - result.y) * tilesize);
Iterator<BlockPlan> broken = player.team().data().plans.iterator();
while(broken.hasNext()){
BlockPlan plan = broken.next();
Block block = content.block(plan.block);
Block block = plan.block;
if(block.bounds(plan.x, plan.y, Tmp.r2).overlaps(Tmp.r1)){
player.unit().addBuild(new BuildPlan(plan.x, plan.y, plan.rotation, content.block(plan.block), plan.config));
player.unit().addBuild(new BuildPlan(plan.x, plan.y, plan.rotation, plan.block, plan.config));
}
}
intSet.clear();
for(int x = result.x; x <= result.x2; x++){
for(int y = result.y; y <= result.y2; y++){
Tile tile = world.tileBuilding(x, y);
if(tile != null && tile.build != null && intSet.add(tile.pos())){
tryRepairDerelict(tile);
}
}
}
}
@@ -1900,9 +2028,12 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
public boolean validPlace(int x, int y, Block type, int rotation){
return validPlace(x, y, type, rotation, null);
}
public boolean validPlace(int x, int y, Block type, int rotation, @Nullable BuildPlan ignore){
return validPlace(x, y, type, rotation, ignore, false);
}
public boolean validPlace(int x, int y, Block type, int rotation, BuildPlan ignore){
if(player.unit().plans.size > 0){
public boolean validPlace(int x, int y, Block type, int rotation, @Nullable BuildPlan ignore, boolean ignoreUnits){
if(player.isBuilder() && player.unit().plans.size > 0){
Tmp.r1.setCentered(x * tilesize + type.offset, y * tilesize + type.offset, type.size * tilesize);
plansOut.clear();
playerPlanTree.intersect(Tmp.r1, plansOut);
@@ -1918,7 +2049,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
}
return Build.validPlace(type, player.team(), x, y, rotation);
return ignoreUnits ? Build.validPlaceIgnoreUnits(type, player.team(), x, y, rotation, true) : Build.validPlace(type, player.team(), x, y, rotation);
}
public boolean validBreak(int x, int y){
@@ -1926,6 +2057,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
public void breakBlock(int x, int y){
if(!player.isBuilder()) return;
Tile tile = world.tile(x, y);
if(tile != null && tile.build != null) tile = tile.build.tile;
player.unit().addBuild(new BuildPlan(tile.x, tile.y));

View File

@@ -88,9 +88,11 @@ public class MobileInput extends InputHandler implements GestureListener{
/** Check and assign targets for a specific position. */
void checkTargets(float x, float y){
if(player.dead()) return;
Unit unit = Units.closestEnemy(player.team(), x, y, 20f, u -> !u.dead);
if(unit != null && !player.dead() && player.unit().type.canAttack){
if(unit != null && player.unit().type.canAttack){
player.unit().mineTile = null;
target = unit;
}else{
@@ -188,8 +190,6 @@ public class MobileInput extends InputHandler implements GestureListener{
@Override
public void buildPlacementUI(Table table){
table.image().color(Pal.gray).height(4f).colspan(4).growX();
table.row();
table.left().margin(0f).defaults().size(48f);
table.button(Icon.hammer, Styles.clearNoneTogglei, () -> {
@@ -234,7 +234,7 @@ public class MobileInput extends InputHandler implements GestureListener{
//actually place/break all selected blocks
if(tile != null){
if(!plan.breaking){
if(validPlace(plan.x, plan.y, plan.block, plan.rotation)){
if(validPlace(plan.x, plan.y, plan.block, plan.rotation, null, true)){
BuildPlan other = getPlan(plan.x, plan.y, plan.block.size, null);
BuildPlan copy = plan.copy();
@@ -265,11 +265,11 @@ public class MobileInput extends InputHandler implements GestureListener{
}).name("confirmplace");
}
boolean showCancel(){
return !player.dead() && (player.unit().isBuilding() || block != null || mode == breaking || !selectPlans.isEmpty()) && !hasSchem();
public boolean showCancel(){
return !player.dead() && (player.unit().isBuilding() || block != null || mode == breaking || !selectPlans.isEmpty()) && !hasSchematic();
}
boolean hasSchem(){
public boolean hasSchematic(){
return lastSchematic != null && !selectPlans.isEmpty();
}
@@ -290,7 +290,7 @@ public class MobileInput extends InputHandler implements GestureListener{
});
group.fill(t -> {
t.visible(() -> !showCancel() && block == null && !hasSchem());
t.visible(() -> !showCancel() && block == null && !hasSchematic() && !state.rules.editor);
t.bottom().left();
t.button("@command.queue", Icon.rightOpen, Styles.clearTogglet, () -> {
@@ -310,7 +310,7 @@ public class MobileInput extends InputHandler implements GestureListener{
});
group.fill(t -> {
t.visible(this::hasSchem);
t.visible(this::hasSchematic);
t.bottom().left();
t.table(Tex.pane, b -> {
b.defaults().size(50f);
@@ -391,8 +391,6 @@ public class MobileInput extends InputHandler implements GestureListener{
}else if(mode == rebuildSelect){
drawRebuildSelection(lineStartX, lineStartY, lastLineX, lastLineY);
}
drawCommanded();
}
@Override
@@ -759,7 +757,7 @@ public class MobileInput extends InputHandler implements GestureListener{
payloadTarget = null;
}
if(locked || block != null || scene.hasField() || hasSchem() || selectPlans.size > 0){
if(locked || block != null || scene.hasField() || hasSchematic() || selectPlans.size > 0){
commandMode = false;
}
@@ -772,14 +770,18 @@ public class MobileInput extends InputHandler implements GestureListener{
}
//zoom camera
if(!locked && Math.abs(Core.input.axisTap(Binding.zoom)) > 0 && !Core.input.keyDown(Binding.rotateplaced) && (Core.input.keyDown(Binding.diagonal_placement) || ((!player.isBuilder() || !isPlacing() || !block.rotate) && selectPlans.isEmpty()))){
if(!locked && !scene.hasKeyboard() && !scene.hasScroll() && Math.abs(Core.input.axisTap(Binding.zoom)) > 0 && !Core.input.keyDown(Binding.rotateplaced) && (Core.input.keyDown(Binding.diagonal_placement) || ((!player.isBuilder() || !isPlacing() || !block.rotate) && selectPlans.isEmpty()))){
renderer.scaleCamera(Core.input.axisTap(Binding.zoom));
}
if(!Core.settings.getBool("keyboard") && !locked && !scene.hasKeyboard()){
//move camera around
float camSpeed = 6f;
Core.camera.position.add(Tmp.v1.setZero().add(Core.input.axis(Binding.move_x), Core.input.axis(Binding.move_y)).nor().scl(Time.delta * camSpeed));
Vec2 delta = Tmp.v1.setZero().add(Core.input.axis(Binding.move_x), Core.input.axis(Binding.move_y)).nor().scl(Time.delta * camSpeed);
Core.camera.position.add(delta);
if(!delta.isZero()){
spectating = null;
}
}
if(Core.settings.getBool("keyboard")){
@@ -940,6 +942,7 @@ public class MobileInput extends InputHandler implements GestureListener{
//pan player
Core.camera.position.x -= deltaX;
Core.camera.position.y -= deltaY;
spectating = null;
}
camera.position.clamp(-camera.width/4f, -camera.height/4f, world.unitWidth() + camera.width/4f, world.unitHeight() + camera.height/4f);

View File

@@ -142,7 +142,7 @@ public class Placement{
Boolf<BuildPlan> placeable = plan ->
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
!(plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
!(plan != plans.first() && plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
var result = plans1.clear();
var rotated = plans.first().tile() != null && plans.first().tile().absoluteRelativeTo(plans.peek().x, plans.peek().y) == Mathf.mod(plans.first().rotation + 2, 4);
@@ -217,7 +217,7 @@ public class Placement{
Boolf<BuildPlan> placeable = plan ->
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
!(plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
!(plan != plans.first() && plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
var result = plans1.clear();

View File

@@ -261,15 +261,8 @@ public class JsonIO{
public UnlockableContent read(Json json, JsonValue jsonData, Class type){
if(jsonData.isNull()) return null;
String str = jsonData.asString();
Item item = Vars.content.item(str);
Liquid liquid = Vars.content.liquid(str);
Block block = Vars.content.block(str);
UnitType unit = Vars.content.unit(str);
return
item != null ? item :
liquid != null ? liquid :
block != null ? block :
unit;
var map = Vars.content.byName(str);
return map instanceof UnlockableContent u ? u : null;
}
});

View File

@@ -232,7 +232,8 @@ public abstract class SaveVersion extends SaveFileReader{
Tile tile = world.rawTile(i % world.width(), i / world.width());
stream.writeShort(tile.blockID());
boolean savedata = tile.block().saveData;
boolean savedata = tile.floor().saveData || tile.overlay().saveData || tile.block().saveData;
byte packed = (byte)((tile.build != null ? 1 : 0) | (savedata ? 2 : 0));
//make note of whether there was an entity/rotation here
@@ -367,7 +368,7 @@ public abstract class SaveVersion extends SaveFileReader{
stream.writeShort(block.x);
stream.writeShort(block.y);
stream.writeShort(block.rotation);
stream.writeShort(block.block);
stream.writeShort(block.block.id);
TypeIO.writeObject(Writes.get(stream), block.config);
}
}
@@ -425,7 +426,7 @@ public abstract class SaveVersion extends SaveFileReader{
var obj = TypeIO.readObject(reads);
//cannot have two in the same position
if(set.add(Point2.pack(x, y))){
data.plans.addLast(new BlockPlan(x, y, rot, content.block(bid).id, obj));
data.plans.addLast(new BlockPlan(x, y, rot, content.block(bid), obj));
}
}
}

View File

@@ -246,7 +246,7 @@ public class TypeIO{
//this is irrelevant.
static final WeaponMount[] noMounts = {};
public static WeaponMount[] readMounts(Reads read){
read.skip(read.b() * (1 + 4 + 4));
@@ -581,7 +581,7 @@ public class TypeIO{
if(ai.command == null) ai.command = UnitCommand.moveCommand;
}
//command queue only in type 7
//command queue only in type 7/8
if(type == 7 || type == 8){
ai.commandQueue.clear();
int length = read.ub();

View File

@@ -21,7 +21,7 @@ public class Save3 extends LegacySaveVersion{
TeamData data = team.data();
int blocks = stream.readInt();
for(int j = 0; j < blocks; j++){
data.plans.addLast(new BlockPlan(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt()));
data.plans.addLast(new BlockPlan(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()), stream.readInt()));
}
}

View File

@@ -1,6 +1,7 @@
package mindustry.logic;
import arc.*;
import arc.audio.*;
import arc.files.*;
import arc.graphics.*;
import arc.math.*;
@@ -8,6 +9,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.game.*;
import mindustry.type.*;
import mindustry.world.*;
@@ -25,7 +27,7 @@ public class GlobalVars{
public static final Rand rand = new Rand();
//non-constants that depend on state
private static LVar varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varMapW, varMapH, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
private static LVar varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varMapW, varMapH, varWait, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
private ObjectMap<String, LVar> vars = new ObjectMap<>();
private Seq<VarEntry> varEntries = new Seq<>();
@@ -33,6 +35,8 @@ public class GlobalVars{
private UnlockableContent[][] logicIdToContent;
private int[][] contentIdToLogicId;
public static final Seq<String> soundNames = new Seq<>();
public void init(){
putEntryOnly("sectionProcessor");
@@ -69,6 +73,7 @@ public class GlobalVars{
varMapW = putEntry("@mapw", 0);
varMapH = putEntry("@maph", 0);
varWait = putEntry("@wait", null);
putEntryOnly("sectionNetwork");
@@ -87,6 +92,17 @@ public class GlobalVars{
put("@ctrlPlayer", ctrlPlayer);
put("@ctrlCommand", ctrlCommand);
//sounds
if(Core.assets != null){
for(Sound sound : Core.assets.getAll(Sound.class, new Seq<>(Sound.class))){
if(sound != Sounds.none && sound != Sounds.swish && sound.file != null){
String name = sound.file.nameWithoutExtension();
soundNames.add(name);
put("@sfx-" + name, Sounds.getSoundId(sound));
}
}
}
//store base content
for(Team team : Team.baseTeams){
@@ -116,7 +132,9 @@ public class GlobalVars{
}
for(UnitType type : Vars.content.units()){
put("@" + type.name, type);
if(!type.internal){
put("@" + type.name, type);
}
}
for(Weather weather : Vars.content.weathers()){
@@ -185,7 +203,7 @@ public class GlobalVars{
varClient.numval = net.client() ? 1 : 0;
//client
if(!net.server() && player != null){
if(player != null){
varClientLocale.objval = player.locale();
varClientUnit.objval = player.unit();
varClientName.objval = player.name();
@@ -194,6 +212,10 @@ public class GlobalVars{
}
}
public LVar waitVar(){
return varWait;
}
public Seq<VarEntry> getEntries(){
return varEntries;
}

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