Merge branch 'master' of https://github.com/Anuken/Mindustry into async-ping
Conflicts: core/src/mindustry/net/ArcNetProvider.java gradle.properties
This commit is contained in:
@@ -27,8 +27,9 @@ import static mindustry.Vars.*;
|
||||
public abstract class ClientLauncher extends ApplicationCore implements Platform{
|
||||
private static final int loadingFPS = 20;
|
||||
|
||||
private long lastTime;
|
||||
private long nextFrame;
|
||||
private long beginTime;
|
||||
private long lastTargetFps = -1;
|
||||
private boolean finished = false;
|
||||
private LoadRenderer loader;
|
||||
|
||||
@@ -199,6 +200,18 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
int targetfps = Core.settings.getInt("fpscap", 120);
|
||||
boolean changed = lastTargetFps != targetfps && lastTargetFps != -1;
|
||||
boolean limitFps = targetfps > 0 && targetfps <= 240;
|
||||
|
||||
lastTargetFps = targetfps;
|
||||
|
||||
if(limitFps && !changed){
|
||||
nextFrame += (1000 * 1000000) / targetfps;
|
||||
}else{
|
||||
nextFrame = Time.nanos();
|
||||
}
|
||||
|
||||
if(!finished){
|
||||
if(loader != null){
|
||||
loader.draw();
|
||||
@@ -230,17 +243,13 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
asyncCore.end();
|
||||
}
|
||||
|
||||
int targetfps = Core.settings.getInt("fpscap", 120);
|
||||
|
||||
if(targetfps > 0 && targetfps <= 240){
|
||||
long target = (1000 * 1000000) / targetfps; //target in nanos
|
||||
long elapsed = Time.timeSinceNanos(lastTime);
|
||||
if(elapsed < target){
|
||||
Threads.sleep((target - elapsed) / 1000000, (int)((target - elapsed) % 1000000));
|
||||
if(limitFps){
|
||||
long current = Time.nanos();
|
||||
if(nextFrame > current){
|
||||
long toSleep = nextFrame - current;
|
||||
Threads.sleep(toSleep / 1000000, (int)(toSleep % 1000000));
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = Time.nanos();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -251,6 +260,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
nextFrame = Time.nanos();
|
||||
setup();
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,8 @@ public class Vars implements Loadable{
|
||||
public static final float tilePayload = tilesize * tilesize;
|
||||
/** icon sizes for UI */
|
||||
public static final float iconXLarge = 8*6f, iconLarge = 8*5f, iconMed = 8*4f, iconSmall = 8*3f;
|
||||
/** macbook screen notch height */
|
||||
public static float macNotchHeight = 32f;
|
||||
/** for map generator dialog */
|
||||
public static boolean updateEditorOnChange = false;
|
||||
/** all choosable player colors in join/host dialog */
|
||||
|
||||
272
core/src/mindustry/ai/BaseBuilderAI.java
Normal file
272
core/src/mindustry/ai/BaseBuilderAI.java
Normal file
@@ -0,0 +1,272 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.BaseRegistry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Schematic.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.blocks.production.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class BaseBuilderAI{
|
||||
private static final Vec2 axis = new Vec2(), rotator = new Vec2();
|
||||
private static final int attempts = 6, coreUnitMultiplier = 2;
|
||||
private static final float emptyChance = 0.01f;
|
||||
private static final int timerStep = 0, timerSpawn = 1, timerRefreshPath = 2;
|
||||
private static final float placeIntervalMin = 12f, placeIntervalMax = 2f;
|
||||
private static final int pathStep = 50;
|
||||
private static final Seq<Tile> tmpTiles = new Seq<>();
|
||||
|
||||
private static int correct = 0, incorrect = 0;
|
||||
|
||||
private int lastX, lastY, lastW, lastH;
|
||||
private boolean foundPath;
|
||||
|
||||
final TeamData data;
|
||||
final Interval timer = new Interval(4);
|
||||
|
||||
IntSet path = new IntSet();
|
||||
IntSet calcPath = new IntSet();
|
||||
@Nullable Tile calcTile;
|
||||
boolean calculating, startedCalculating;
|
||||
int calcCount = 0;
|
||||
int totalCalcs = 0;
|
||||
|
||||
public BaseBuilderAI(TeamData data){
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void update(){
|
||||
|
||||
//fill cores.
|
||||
if(data.team.cores().size > 0){
|
||||
var core = data.team.cores().first();
|
||||
for(Item item : content.items()){
|
||||
core.items.set(item, core.getMaximumAccepted(item));
|
||||
}
|
||||
}
|
||||
|
||||
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 6f) && data.hasCore()){
|
||||
CoreBlock block = (CoreBlock)data.core().block;
|
||||
int coreUnits = data.countType(block.unitType);
|
||||
|
||||
//create AI core unit(s)
|
||||
if(!state.isEditor() && coreUnits < data.cores.size * coreUnitMultiplier){
|
||||
Unit unit = block.unitType.create(data.team);
|
||||
unit.set(data.cores.random());
|
||||
unit.add();
|
||||
Fx.spawn.at(unit);
|
||||
}
|
||||
}
|
||||
|
||||
//refresh path
|
||||
if(!calculating && (timer.get(timerRefreshPath, 3f * Time.toMinutes) || !startedCalculating) && data.hasCore()){
|
||||
calculating = true;
|
||||
startedCalculating = true;
|
||||
calcPath.clear();
|
||||
}
|
||||
|
||||
//didn't find tile in time
|
||||
if(calculating && calcCount >= world.width() * world.height()){
|
||||
calculating = false;
|
||||
calcCount = 0;
|
||||
calcPath.clear();
|
||||
totalCalcs ++;
|
||||
}
|
||||
|
||||
//calculate path for units so schematics are not placed on it
|
||||
if(calculating){
|
||||
if(calcTile == null){
|
||||
Vars.spawner.eachGroundSpawn((x, y) -> calcTile = world.tile(x, y));
|
||||
if(calcTile == null){
|
||||
calculating = false;
|
||||
}
|
||||
}else{
|
||||
var field = pathfinder.getField(data.team, Pathfinder.costGround, Pathfinder.fieldCore);
|
||||
|
||||
if(field.hasCompleteWeights()){
|
||||
int[] weights = field.completeWeights;
|
||||
for(int i = 0; i < pathStep; i++){
|
||||
int minCost = Integer.MAX_VALUE;
|
||||
int cx = calcTile.x, cy = calcTile.y;
|
||||
boolean foundAny = false;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int nx = cx + p.x, ny = cy + p.y, packed = world.packArray(nx, ny);
|
||||
|
||||
Tile other = world.tile(nx, ny);
|
||||
if(other != null && weights[packed] < minCost && weights[packed] != -1){
|
||||
minCost = weights[packed];
|
||||
calcTile = other;
|
||||
foundAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
//didn't find anything, break out of loop, this will trigger a clear later
|
||||
if(!foundAny){
|
||||
calcCount = Integer.MAX_VALUE;
|
||||
break;
|
||||
}
|
||||
|
||||
calcPath.add(calcTile.pos());
|
||||
for(Point2 p : Geometry.d8){
|
||||
calcPath.add(Point2.pack(p.x + calcTile.x, p.y + calcTile.y));
|
||||
}
|
||||
|
||||
//found the end.
|
||||
if(calcTile.build instanceof CoreBuild b && b.team != data.team){
|
||||
//clean up calculations and flush results
|
||||
calculating = false;
|
||||
calcCount = 0;
|
||||
path.clear();
|
||||
path.addAll(calcPath);
|
||||
calcPath.clear();
|
||||
calcTile = null;
|
||||
totalCalcs ++;
|
||||
foundPath = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
calcCount ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//only schedule when there's something to build.
|
||||
if((foundPath || !calculating) && data.plans.isEmpty() && timer.get(timerStep, Mathf.lerp(placeIntervalMin, placeIntervalMax, data.team.rules().buildAiTier))){
|
||||
|
||||
for(int i = 0; i < attempts; i++){
|
||||
int range = 150;
|
||||
|
||||
Position pos = randomPosition();
|
||||
|
||||
//when there are no random positions, do nothing.
|
||||
if(pos == null) return;
|
||||
|
||||
Tmp.v1.rnd(Mathf.random(range));
|
||||
int wx = (int)(World.toTile(pos.getX()) + Tmp.v1.x), wy = (int)(World.toTile(pos.getY()) + Tmp.v1.y);
|
||||
Tile tile = world.tiles.getc(wx, wy);
|
||||
|
||||
//try not to block the spawn point
|
||||
if(spawner.getSpawns().contains(t -> t.within(tile, tilesize * 40f))){
|
||||
continue;
|
||||
}
|
||||
|
||||
Seq<BasePart> parts = null;
|
||||
|
||||
//pick a completely random base part, and place it a random location
|
||||
//((yes, very intelligent))
|
||||
if(tile.drop() != null && Vars.bases.forResource(tile.drop()).any()){
|
||||
parts = Vars.bases.forResource(tile.drop());
|
||||
}else if(Mathf.chance(emptyChance)){
|
||||
parts = Vars.bases.parts;
|
||||
}
|
||||
|
||||
if(parts != null){
|
||||
BasePart part = parts.random();
|
||||
if(tryPlace(part, tile.x, tile.y)){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return a random position from which to seed building. */
|
||||
private Position randomPosition(){
|
||||
if(data.hasCore()){
|
||||
return data.cores.random();
|
||||
}else if(data.team == state.rules.waveTeam){
|
||||
return spawner.getSpawns().random();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean tryPlace(BasePart part, int x, int y){
|
||||
int rotation = Mathf.range(2);
|
||||
axis.set((int)(part.schematic.width / 2f), (int)(part.schematic.height / 2f));
|
||||
Schematic result = Schematics.rotate(part.schematic, rotation);
|
||||
int rotdeg = rotation*90;
|
||||
rotator.set(part.centerX, part.centerY).rotateAround(axis, rotdeg);
|
||||
//bottom left schematic corner
|
||||
int cx = x - (int)rotator.x;
|
||||
int cy = y - (int)rotator.y;
|
||||
|
||||
//check valid placeability
|
||||
for(Stile tile : result.tiles){
|
||||
int realX = tile.x + cx, realY = tile.y + cy;
|
||||
if(!Build.validPlace(tile.block, data.team, realX, realY, tile.rotation)){
|
||||
return false;
|
||||
}
|
||||
Tile wtile = world.tile(realX, realY);
|
||||
|
||||
if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadBlock){
|
||||
//near a building
|
||||
for(Point2 point : Edges.getEdges(tile.block.size)){
|
||||
var t = world.build(tile.x + point.x, tile.y + point.y);
|
||||
if(t != null){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//may intersect AI path
|
||||
tmpTiles.clear();
|
||||
if(tile.block.solid && wtile != null && wtile.getLinkedTilesAs(tile.block, tmpTiles).contains(t -> path.contains(t.pos()))){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//make sure at least X% of resource requirements are met
|
||||
correct = incorrect = 0;
|
||||
boolean anyDrills = false;
|
||||
|
||||
if(part.required instanceof Item){
|
||||
for(Stile tile : result.tiles){
|
||||
if(tile.block instanceof Drill){
|
||||
anyDrills = true;
|
||||
|
||||
tile.block.iterateTaken(tile.x + cx, tile.y + cy, (ex, ey) -> {
|
||||
Tile res = world.rawTile(ex, ey);
|
||||
if(res.drop() == part.required){
|
||||
correct ++;
|
||||
}else if(res.drop() != null){
|
||||
incorrect ++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fail if not enough fit requirements
|
||||
if(anyDrills && (incorrect != 0 || correct == 0)){
|
||||
return false;
|
||||
}
|
||||
|
||||
//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));
|
||||
}
|
||||
|
||||
lastX = cx - 1;
|
||||
lastY = cy - 1;
|
||||
lastW = result.width + 2;
|
||||
lastH = result.height + 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -158,12 +158,13 @@ public class BlockIndexer{
|
||||
int pos = tile.pos();
|
||||
var seq = ores[drop.id][qx][qy];
|
||||
|
||||
//when the drop can be mined, record the ore position
|
||||
if(tile.block() == Blocks.air && !seq.contains(pos)){
|
||||
seq.add(pos);
|
||||
allOres.increment(drop);
|
||||
}else{
|
||||
//otherwise, it likely became blocked, remove it (even if it wasn't there)
|
||||
if(tile.block() == Blocks.air){
|
||||
//add the index if it is a valid new spot to mine at
|
||||
if(!seq.contains(pos)){
|
||||
seq.add(pos);
|
||||
allOres.increment(drop);
|
||||
}
|
||||
}else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it
|
||||
seq.removeValue(pos);
|
||||
allOres.increment(drop, -1);
|
||||
}
|
||||
@@ -286,7 +287,7 @@ public class BlockIndexer{
|
||||
//when team data is not initialized, scan through every team. this is terrible
|
||||
if(data.isEmpty()){
|
||||
for(Team enemy : Team.all){
|
||||
if(enemy == team) continue;
|
||||
if(enemy == team || (enemy == Team.derelict && !state.rules.coreCapture)) continue;
|
||||
var set = getFlagged(enemy)[type.ordinal()];
|
||||
if(set != null){
|
||||
breturnArray.addAll(set);
|
||||
@@ -295,7 +296,7 @@ public class BlockIndexer{
|
||||
}else{
|
||||
for(int i = 0; i < data.size; i++){
|
||||
Team enemy = data.items[i].team;
|
||||
if(enemy == team) continue;
|
||||
if(enemy == team || (enemy == Team.derelict && !state.rules.coreCapture)) continue;
|
||||
var set = getFlagged(enemy)[type.ordinal()];
|
||||
if(set != null){
|
||||
breturnArray.addAll(set);
|
||||
|
||||
@@ -51,10 +51,14 @@ public class ControlPathfinder{
|
||||
costLegs = (team, tile) ->
|
||||
PathTile.legSolid(tile) ? impassable : 1 +
|
||||
(PathTile.deep(tile) ? 6000 : 0) +
|
||||
(PathTile.nearSolid(tile) || PathTile.solid(tile) ? 3 : 0),
|
||||
(PathTile.nearLegSolid(tile) ? 3 : 0),
|
||||
|
||||
costNaval = (team, tile) ->
|
||||
(PathTile.solid(tile) || !PathTile.liquid(tile) ? impassable : 1) +
|
||||
//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 +
|
||||
//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);
|
||||
|
||||
public static boolean showDebug = false;
|
||||
@@ -190,7 +194,7 @@ public class ControlPathfinder{
|
||||
}
|
||||
|
||||
//destination is impassable, can't go there.
|
||||
if(solid(team, costType, world.packArray(World.toTile(destination.x), World.toTile(destination.y)))){
|
||||
if(solid(team, costType, world.packArray(World.toTile(destination.x), World.toTile(destination.y)), false)){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -229,11 +233,11 @@ public class ControlPathfinder{
|
||||
req.curId = pathId;
|
||||
|
||||
//check for the unit getting stuck every N seconds
|
||||
if((req.stuckTimer += Time.delta) >= 60f * 2.5f){
|
||||
if(req.done && (req.stuckTimer += Time.delta) >= 60f * 1.5f){
|
||||
req.stuckTimer = 0f;
|
||||
//force recalculate
|
||||
if(req.lastPos.within(unit, 1.5f)){
|
||||
req.lastWorldUpdate = -1;
|
||||
req.forceRecalculate();
|
||||
}
|
||||
req.lastPos.set(unit);
|
||||
}
|
||||
@@ -253,8 +257,14 @@ public class ControlPathfinder{
|
||||
float dst = unit.dst2(tile);
|
||||
//TODO maybe put this on a timer since raycasts can be expensive?
|
||||
if(dst < minDst && !permissiveRaycast(team, costType, tileX, tileY, tile.x, tile.y)){
|
||||
if(avoid(req.team, req.cost, items[i + 1])){
|
||||
range = 0.5f;
|
||||
}
|
||||
|
||||
req.pathIndex = Math.max(dst <= range * range ? i + 1 : i, req.pathIndex);
|
||||
minDst = Math.min(dst, minDst);
|
||||
}else if(dst <= 1f){
|
||||
req.pathIndex = Math.min(Math.max(i + 1, req.pathIndex), len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +287,19 @@ public class ControlPathfinder{
|
||||
Tile tile = tile(items[req.rayPathIndex]);
|
||||
out.set(tile);
|
||||
|
||||
if(req.rayPathIndex > 0){
|
||||
float angleToNext = tile(items[req.rayPathIndex - 1]).angleTo(tile);
|
||||
float angleToDest = unit.angleTo(tile);
|
||||
//force recalculate when the unit moves backwards
|
||||
if(Angles.angleDist(angleToNext, angleToDest) > 80f && !unit.within(tile, 1f)){
|
||||
req.forceRecalculate();
|
||||
}
|
||||
}
|
||||
|
||||
if(avoid(req.team, req.cost, items[req.rayPathIndex])){
|
||||
range = 0.5f;
|
||||
}
|
||||
|
||||
if(unit.within(tile, range)){
|
||||
req.pathIndex = req.rayPathIndex = Math.max(req.pathIndex, req.rayPathIndex + 1);
|
||||
}
|
||||
@@ -323,8 +346,12 @@ public class ControlPathfinder{
|
||||
requests.clear();
|
||||
}
|
||||
|
||||
public static boolean isNearObstacle(Unit unit, int x1, int y1, int x2, int y2){
|
||||
return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int ww = wwidth, wh = wheight;
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
@@ -362,13 +389,13 @@ public class ControlPathfinder{
|
||||
}
|
||||
|
||||
private static boolean permissiveRaycast(int team, PathCost type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int ww = wwidth, wh = wheight;
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while(x >= 0 && y >= 0 && x < ww && y < wh){
|
||||
if(solid(team, type, x + y * wwidth)) return true;
|
||||
if(solid(team, type, x + y * wwidth, true)) return true;
|
||||
if(x == x2 && y == y2) return false;
|
||||
|
||||
//no diagonals
|
||||
@@ -384,6 +411,30 @@ public class ControlPathfinder{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return 0 if nothing was hit, otherwise the packed coordinates. This is an internal function and will likely be moved - do not use!*/
|
||||
public static int raycastFast(int team, PathCost type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while(x >= 0 && y >= 0 && x < ww && y < wh){
|
||||
if(solid(team, type, x + y * wwidth, true)) return Point2.pack(x, y);
|
||||
if(x == x2 && y == y2) return 0;
|
||||
|
||||
//no diagonals
|
||||
if(2 * err + dy > dx - 2 * err){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}else{
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static boolean cast(int team, PathCost cost, int from, int to){
|
||||
return raycast(team, cost, from % wwidth, from / wwidth, to % wwidth, to / wwidth);
|
||||
}
|
||||
@@ -417,9 +468,9 @@ public class ControlPathfinder{
|
||||
return cost == impassable || cost >= 2;
|
||||
}
|
||||
|
||||
private static boolean solid(int team, PathCost type, int tilePos){
|
||||
private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){
|
||||
int cost = cost(team, type, tilePos);
|
||||
return cost == impassable || cost >= 6000;
|
||||
return cost == impassable || (checkWall && cost >= 6000);
|
||||
}
|
||||
|
||||
private static float tileCost(int team, PathCost type, int a, int b){
|
||||
@@ -479,6 +530,7 @@ public class ControlPathfinder{
|
||||
volatile PathCost cost;
|
||||
volatile int team;
|
||||
volatile int lastWorldUpdate;
|
||||
volatile boolean forcedRecalc;
|
||||
|
||||
final Vec2 lastPos = new Vec2();
|
||||
float stuckTimer = 0f;
|
||||
@@ -503,6 +555,7 @@ public class ControlPathfinder{
|
||||
|
||||
long lastUpdateId;
|
||||
long lastTime;
|
||||
long forceRecalcTime;
|
||||
|
||||
volatile int lastId, curId;
|
||||
|
||||
@@ -510,6 +563,13 @@ public class ControlPathfinder{
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
public void forceRecalculate(){
|
||||
//keep it at 3 times/sec
|
||||
if(Time.timeSinceMillis(forceRecalcTime) < 1000 / 3) return;
|
||||
forcedRecalc = true;
|
||||
forceRecalcTime = Time.millis();
|
||||
}
|
||||
|
||||
void update(long maxUpdateNs){
|
||||
if(curId != lastId){
|
||||
clear(true);
|
||||
@@ -517,9 +577,10 @@ public class ControlPathfinder{
|
||||
lastId = curId;
|
||||
|
||||
//re-do everything when world updates, but keep the old path around
|
||||
if(Time.timeSinceMillis(lastTime) > 1000 * 3 && (worldUpdateId != lastWorldUpdate || !destination.epsilonEquals(lastDestination, 2f))){
|
||||
if(forcedRecalc || (Time.timeSinceMillis(lastTime) > 1000 * 3 && (worldUpdateId != lastWorldUpdate || !destination.epsilonEquals(lastDestination, 2f)))){
|
||||
lastTime = Time.millis();
|
||||
lastWorldUpdate = worldUpdateId;
|
||||
forcedRecalc = false;
|
||||
clear(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
@@ -58,7 +57,8 @@ public class Pathfinder implements Runnable{
|
||||
|
||||
//water
|
||||
(team, tile) ->
|
||||
(PathTile.solid(tile) || !PathTile.liquid(tile) ? 6000 : 1) +
|
||||
(!PathTile.liquid(tile) ? 6000 : 1) +
|
||||
PathTile.health(tile) * 5 +
|
||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
|
||||
(PathTile.deep(tile) ? 0 : 1) +
|
||||
(PathTile.damages(tile) ? 35 : 0)
|
||||
@@ -152,18 +152,19 @@ public class Pathfinder implements Runnable{
|
||||
|
||||
/** Packs a tile into its internal representation. */
|
||||
public int packTile(Tile tile){
|
||||
boolean nearLiquid = false, nearSolid = false, nearGround = false, solid = tile.solid(), allDeep = tile.floor().isDeep();
|
||||
boolean nearLiquid = false, nearSolid = false, nearLegSolid = false, nearGround = false, solid = tile.solid(), allDeep = tile.floor().isDeep();
|
||||
|
||||
for(int i = 0; i < 4; i++){
|
||||
Tile other = tile.nearby(i);
|
||||
if(other != null){
|
||||
Floor floor = other.floor();
|
||||
boolean osolid = other.solid();
|
||||
if(floor.isLiquid) nearLiquid = true;
|
||||
if(floor.isLiquid && floor.isDeep()) nearLiquid = true;
|
||||
//TODO potentially strange behavior when teamPassable is false for other teams?
|
||||
if(osolid && !other.block().teamPassable) nearSolid = true;
|
||||
if(!floor.isLiquid) nearGround = true;
|
||||
if(!floor.isDeep()) allDeep = false;
|
||||
if(other.legSolid()) nearLegSolid = true;
|
||||
|
||||
//other tile is now near solid
|
||||
if(solid && !tile.block().teamPassable){
|
||||
@@ -179,10 +180,11 @@ public class Pathfinder implements Runnable{
|
||||
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
|
||||
solid,
|
||||
tile.floor().isLiquid,
|
||||
tile.staticDarkness() >= 2 || (tile.floor().solid && tile.block() == Blocks.air),
|
||||
tile.legSolid(),
|
||||
nearLiquid,
|
||||
nearGround,
|
||||
nearSolid,
|
||||
nearLegSolid,
|
||||
tile.floor().isDeep(),
|
||||
tile.floor().damageTaken > 0.00001f,
|
||||
allDeep,
|
||||
@@ -521,13 +523,19 @@ public class Pathfinder implements Runnable{
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public boolean hasCompleteWeights(){
|
||||
return hasComplete && completeWeights != null;
|
||||
}
|
||||
|
||||
public void updateTargetPositions(){
|
||||
targets.clear();
|
||||
getPositions(targets);
|
||||
}
|
||||
|
||||
protected boolean passable(int pos){
|
||||
return cost.getCost(team.id, pathfinder.tiles[pos]) != impassable;
|
||||
int amount = cost.getCost(team.id, pathfinder.tiles[pos]);
|
||||
//edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable
|
||||
return amount != impassable && !(cost == costTypes.get(costNaval) && amount >= 6000);
|
||||
}
|
||||
|
||||
/** Gets targets to pathfind towards. This must run on the main thread. */
|
||||
@@ -557,6 +565,8 @@ public class Pathfinder implements Runnable{
|
||||
boolean nearGround;
|
||||
//whether this block is near a solid object
|
||||
boolean nearSolid;
|
||||
//whether this block is near a block that is solid for legged units
|
||||
boolean nearLegSolid;
|
||||
//whether this block is deep / drownable
|
||||
boolean deep;
|
||||
//whether the floor damages
|
||||
|
||||
@@ -2,40 +2,23 @@ package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
|
||||
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
|
||||
public class UnitCommand{
|
||||
/** List of all commands by ID. */
|
||||
public class UnitCommand extends MappableContent{
|
||||
/** @deprecated now a content type, use the methods in Vars.content instead */
|
||||
@Deprecated
|
||||
public static final Seq<UnitCommand> all = new Seq<>();
|
||||
|
||||
public static final UnitCommand
|
||||
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand;
|
||||
|
||||
moveCommand = new UnitCommand("move", "right", u -> null){{
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}},
|
||||
repairCommand = new UnitCommand("repair", "modeSurvival", u -> new RepairAI()),
|
||||
rebuildCommand = new UnitCommand("rebuild", "hammer", u -> new BuilderAI()),
|
||||
assistCommand = new UnitCommand("assist", "players", u -> {
|
||||
var ai = new BuilderAI();
|
||||
ai.onlyAssist = true;
|
||||
return ai;
|
||||
}),
|
||||
mineCommand = new UnitCommand("mine", "production", u -> new MinerAI()),
|
||||
boostCommand = new UnitCommand("boost", "up", u -> new BoostAI()){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
|
||||
/** Unique ID number. */
|
||||
public final int id;
|
||||
/** Named used for tooltip/description. */
|
||||
public final String name;
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
/** Controller that this unit will use when this command is used. Return null for "default" behavior. */
|
||||
@@ -46,22 +29,86 @@ public class UnitCommand{
|
||||
public boolean drawTarget = false;
|
||||
/** Whether to reset targets when switching to or from this command. */
|
||||
public boolean resetTarget = true;
|
||||
/** */
|
||||
public boolean exactArrival = false;
|
||||
/** Key to press for this command. */
|
||||
public @Nullable Binding keybind = null;
|
||||
|
||||
public UnitCommand(String name, String icon, Func<Unit, AIController> controller){
|
||||
this.name = name;
|
||||
super(name);
|
||||
|
||||
this.icon = icon;
|
||||
this.controller = controller;
|
||||
this.controller = controller == null ? u -> null : controller;
|
||||
|
||||
id = all.size;
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){
|
||||
this(name, icon, controller);
|
||||
this.keybind = keybind;
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("command." + name);
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(){
|
||||
return Icon.icons.get(icon, Icon.cancel);
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char)Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.unitCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "UnitCommand:" + name;
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
|
||||
moveCommand = new UnitCommand("move", "right", Binding.unit_command_move, null){{
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
repairCommand = new UnitCommand("repair", "modeSurvival", Binding.unit_command_repair, u -> new RepairAI());
|
||||
rebuildCommand = new UnitCommand("rebuild", "hammer", Binding.unit_command_rebuild, u -> new BuilderAI());
|
||||
assistCommand = new UnitCommand("assist", "players", Binding.unit_command_assist, u -> {
|
||||
var ai = new BuilderAI();
|
||||
ai.onlyAssist = true;
|
||||
return ai;
|
||||
});
|
||||
mineCommand = new UnitCommand("mine", "production", Binding.unit_command_mine, u -> new MinerAI());
|
||||
boostCommand = new UnitCommand("boost", "up", Binding.unit_command_boost, u -> new BoostAI()){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
enterPayloadCommand = new UnitCommand("enterPayload", "downOpen", Binding.unit_command_enter_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loadUnitsCommand = new UnitCommand("loadUnits", "upload", Binding.unit_command_load_units, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loadBlocksCommand = new UnitCommand("loadBlocks", "up", Binding.unit_command_load_blocks, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
exactArrival = true;
|
||||
}};
|
||||
unloadPayloadCommand = new UnitCommand("unloadPayload", "download", Binding.unit_command_unload_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
193
core/src/mindustry/ai/UnitGroup.java
Normal file
193
core/src/mindustry/ai/UnitGroup.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.Pathfinder.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
|
||||
public class UnitGroup{
|
||||
public Seq<Unit> units = new Seq<>();
|
||||
public int collisionLayer;
|
||||
public volatile float[] positions, originalPositions;
|
||||
public volatile boolean valid;
|
||||
public float minSpeed = 999999f;
|
||||
|
||||
public void calculateFormation(Vec2 dest, int collisionLayer){
|
||||
this.collisionLayer = collisionLayer;
|
||||
|
||||
float cx = 0f, cy = 0f;
|
||||
for(Unit unit : units){
|
||||
cx += unit.x;
|
||||
cy += unit.y;
|
||||
}
|
||||
cx /= units.size;
|
||||
cy /= units.size;
|
||||
positions = new float[units.size * 2];
|
||||
|
||||
//all positions are relative to the center
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
Unit unit = units.get(i);
|
||||
positions[i * 2] = unit.x - cx;
|
||||
positions[i * 2 + 1] = unit.y - cy;
|
||||
unit.command().groupIndex = i;
|
||||
|
||||
//don't factor in the floor speed multiplier
|
||||
Floor on = unit.isFlying() ? Blocks.air.asFloor() : unit.floorOn();
|
||||
minSpeed = Math.min(unit.speed() / on.speedMultiplier, minSpeed);
|
||||
}
|
||||
|
||||
if(Float.isInfinite(minSpeed) || Float.isNaN(minSpeed)) minSpeed = 999999f;
|
||||
|
||||
//run on new thread to prevent stutter
|
||||
Vars.mainExecutor.submit(() -> {
|
||||
//unused space between circles that needs to be reached for compression to end
|
||||
float maxSpaceUsage = 0.7f;
|
||||
boolean compress = true;
|
||||
|
||||
int compressionIterations = 0;
|
||||
int physicsIterations = 0;
|
||||
int totalIterations = 0;
|
||||
int maxPhysicsIterations = Math.min(1 + (int)(Math.pow(units.size, 0.65) / 10), 6);
|
||||
|
||||
//yep, new allocations, because this is a new thread.
|
||||
IntQuadTree tree = new IntQuadTree(new Rect(0f, 0f, Vars.world.unitWidth(), Vars.world.unitHeight()),
|
||||
(index, hitbox) -> hitbox.setCentered(positions[index * 2], positions[index * 2 + 1], units.get(index).hitSize));
|
||||
IntSeq tmpseq = new IntSeq();
|
||||
Vec2 v1 = new Vec2();
|
||||
Vec2 v2 = new Vec2();
|
||||
|
||||
//this algorithm basically squeezes all the circle colliders together, then proceeds to simulate physics to push them apart across several iterations.
|
||||
//it's rather slow, but shouldn't be too much of an issue when run in a different thread
|
||||
while(totalIterations++ < 40 && physicsIterations < maxPhysicsIterations){
|
||||
float spaceUsed = 0f;
|
||||
|
||||
if(compress){
|
||||
compressionIterations ++;
|
||||
|
||||
float maxDst = 1f, totalArea = 0f;
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
v1.set(positions[a * 2], positions[a * 2 + 1]).lerp(v2.set(0f, 0f), 0.3f);
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
|
||||
float rad = units.get(a).hitSize/2f;
|
||||
|
||||
maxDst = Math.max(maxDst, v1.dst(0f, 0f) + rad);
|
||||
totalArea += Mathf.PI * rad * rad;
|
||||
}
|
||||
|
||||
//total area of bounding circle
|
||||
float boundingArea = Mathf.PI * maxDst * maxDst;
|
||||
spaceUsed = totalArea / boundingArea;
|
||||
|
||||
//ex: 60% (0.6) of the total area is used, this will not be enough to satisfy a maxSpaceUsage of 70% (0.7)
|
||||
compress = spaceUsed <= maxSpaceUsage && compressionIterations < 20;
|
||||
}
|
||||
|
||||
//uncompress units
|
||||
if(!compress || spaceUsed > 0.5f){
|
||||
physicsIterations++;
|
||||
|
||||
tree.clear();
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
tree.insert(a);
|
||||
}
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
Unit unit = units.get(a);
|
||||
float x = positions[a * 2], y = positions[a * 2 + 1], radius = unit.hitSize/2f;
|
||||
|
||||
tmpseq.clear();
|
||||
tree.intersect(x - radius, y - radius, radius * 2f, radius * 2f, tmpseq);
|
||||
for(int res = 0; res < tmpseq.size; res ++){
|
||||
int b = tmpseq.items[res];
|
||||
|
||||
//simulate collision physics
|
||||
if(a != b){
|
||||
float ox = positions[b * 2], oy = positions[b * 2 + 1];
|
||||
Unit other = units.get(b);
|
||||
|
||||
float rs = (radius + other.hitSize/2f) * 1.2f;
|
||||
float dst = Mathf.dst(x, y, ox, oy);
|
||||
|
||||
if(dst < rs){
|
||||
v2.set(x - ox, y - oy).setLength(rs - dst);
|
||||
float mass1 = unit.hitSize, mass2 = other.hitSize;
|
||||
float ms = mass1 + mass2;
|
||||
float m1 = mass2 / ms, m2 = mass1 / ms;
|
||||
float scl = 1f;
|
||||
|
||||
positions[a * 2] += v2.x * m1 * scl;
|
||||
positions[a * 2 + 1] += v2.y * m1 * scl;
|
||||
|
||||
positions[b * 2] -= v2.x * m2 * scl;
|
||||
positions[b * 2 + 1] -= v2.y * m2 * scl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalPositions = positions.clone();
|
||||
|
||||
//raycast from the destination to the offset to make sure it's reachable
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
updateRaycast(a, dest, v1);
|
||||
}
|
||||
|
||||
valid = true;
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> {
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
float x = positions[i * 2], y = positions[i * 2 + 1];
|
||||
|
||||
Fx.placeBlock.at(x + dest.x, y + dest.y, 1f, Color.green);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateRaycast(int index, Vec2 dest){
|
||||
updateRaycast(index, dest, Tmp.v1);
|
||||
}
|
||||
|
||||
private void updateRaycast(int index, Vec2 dest, Vec2 v1){
|
||||
if(collisionLayer != PhysicsProcess.layerFlying){
|
||||
|
||||
//coordinates in world space
|
||||
float
|
||||
x = originalPositions[index * 2] + dest.x,
|
||||
y = originalPositions[index * 2 + 1] + dest.y;
|
||||
|
||||
Unit unit = units.get(index);
|
||||
|
||||
PathCost cost = unit.type.pathCost;
|
||||
int res = ControlPathfinder.raycastFast(unit.team.id, cost, World.toTile(dest.x), World.toTile(dest.y), World.toTile(x), World.toTile(y));
|
||||
|
||||
//collision found, make th destination the point right before the collision
|
||||
if(res != 0){
|
||||
v1.set(Point2.x(res) * Vars.tilesize - dest.x, Point2.y(res) * Vars.tilesize - dest.y);
|
||||
v1.setLength(Math.max(v1.len() - Vars.tilesize - 4f, 0));
|
||||
positions[index * 2] = v1.x;
|
||||
positions[index * 2 + 1] = v1.y;
|
||||
}
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> Fx.debugLine.at(unit.x, unit.y, 0f, Color.green, new Vec2[]{new Vec2(dest.x, dest.y), new Vec2(x, y)}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
core/src/mindustry/ai/UnitStance.java
Normal file
61
core/src/mindustry/ai/UnitStance.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
|
||||
public class UnitStance extends MappableContent{
|
||||
/** @deprecated now a content type, use the methods in Vars.content instead */
|
||||
@Deprecated
|
||||
public static final Seq<UnitStance> all = new Seq<>();
|
||||
|
||||
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
|
||||
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
/** Key to press for this stance. */
|
||||
public @Nullable Binding keybind = null;
|
||||
|
||||
public UnitStance(String name, String icon, Binding keybind){
|
||||
super(name);
|
||||
this.icon = icon;
|
||||
this.keybind = keybind;
|
||||
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("stance." + name);
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(){
|
||||
return Icon.icons.get(icon, Icon.cancel);
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.unitStance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "UnitStance:" + name;
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
stop = new UnitStance("stop", "cancel", Binding.cancel_orders);
|
||||
shoot = new UnitStance("shoot", "commandAttack", Binding.unit_stance_shoot);
|
||||
holdFire = new UnitStance("holdfire", "none", Binding.unit_stance_hold_fire);
|
||||
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unit_stance_pursue_target);
|
||||
patrol = new UnitStance("patrol", "refresh", Binding.unit_stance_patrol);
|
||||
ram = new UnitStance("ram", "rightOpen", Binding.unit_stance_ram);
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,14 @@ import mindustry.world.blocks.ConstructBlock.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class BuilderAI extends AIController{
|
||||
public static float buildRadius = 1500, retreatDst = 110f, retreatDelay = Time.toSeconds * 2f;
|
||||
public static float buildRadius = 1500, retreatDst = 110f, retreatDelay = Time.toSeconds * 2f, defaultRebuildPeriod = 60f * 2f;
|
||||
|
||||
public @Nullable Unit assistFollowing;
|
||||
public @Nullable Unit following;
|
||||
public @Nullable Teamc enemy;
|
||||
public @Nullable BlockPlan lastPlan;
|
||||
|
||||
public float fleeRange = 370f, rebuildPeriod = 60f * 2f;
|
||||
public float fleeRange = 370f, rebuildPeriod = defaultRebuildPeriod;
|
||||
public boolean alwaysFlee;
|
||||
public boolean onlyAssist;
|
||||
|
||||
@@ -34,6 +34,14 @@ public class BuilderAI extends AIController{
|
||||
public BuilderAI(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
//rebuild much faster with buildAI; there are usually few builder units so this is fine
|
||||
if(rebuildPeriod == defaultRebuildPeriod && unit.team.rules().buildAi){
|
||||
rebuildPeriod = 10f;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
|
||||
|
||||
@@ -11,25 +11,38 @@ import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class CommandAI extends AIController{
|
||||
protected static final float localInterval = 40f;
|
||||
protected static final Vec2 vecOut = new Vec2(), flockVec = new Vec2(), separation = new Vec2(), cohesion = new Vec2(), massCenter = new Vec2();
|
||||
protected static final int maxCommandQueueSize = 50, avoidInterval = 10;
|
||||
protected static final Vec2 vecOut = new Vec2(), vecMovePos = new Vec2();
|
||||
protected static final boolean[] noFound = {false};
|
||||
protected static final UnitPayload tmpPayload = new UnitPayload(null);
|
||||
|
||||
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.. */
|
||||
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. */
|
||||
public IntSeq unreachableBuildings = new IntSeq(8);
|
||||
/** ID of unit read as target. This is set up after reading. Do not access! */
|
||||
public int readAttackTarget = -1;
|
||||
|
||||
protected boolean stopAtTarget, stopWhenInRange;
|
||||
protected Vec2 lastTargetPos;
|
||||
protected int pathId = -1;
|
||||
protected Seq<Unit> local = new Seq<>(false);
|
||||
protected boolean flocked;
|
||||
protected boolean blockingUnit;
|
||||
protected float timeSpentBlocked;
|
||||
|
||||
/** Stance, usually related to firing mode. */
|
||||
public UnitStance stance = UnitStance.shoot;
|
||||
/** Current command this unit is following. */
|
||||
public @Nullable UnitCommand command;
|
||||
public UnitCommand command = UnitCommand.moveCommand;
|
||||
/** Current controller instance based on command. */
|
||||
protected @Nullable AIController commandController;
|
||||
/** Last command type assigned. Used for detecting command changes. */
|
||||
@@ -60,6 +73,18 @@ public class CommandAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
//this should not be possible
|
||||
if(stance == UnitStance.stop) stance = UnitStance.shoot;
|
||||
|
||||
//pursue the target if relevant
|
||||
if(stance == UnitStance.pursueTarget && target != null && attackTarget == null && targetPos == null){
|
||||
commandTarget(target, false);
|
||||
}
|
||||
|
||||
//remove invalid targets
|
||||
if(commandQueue.any()){
|
||||
commandQueue.removeAll(e -> e instanceof Healthc h && !h.isValid());
|
||||
}
|
||||
|
||||
//assign defaults
|
||||
if(command == null && unit.type.commands.length > 0){
|
||||
@@ -84,8 +109,54 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
public void clearCommands(){
|
||||
commandQueue.clear();
|
||||
targetPos = null;
|
||||
attackTarget = null;
|
||||
}
|
||||
|
||||
public void defaultBehavior(){
|
||||
|
||||
if(!net.client() && unit instanceof Payloadc pay){
|
||||
//auto-drop everything
|
||||
if(command == UnitCommand.unloadPayloadCommand && pay.hasPayload()){
|
||||
Call.payloadDropped(unit, unit.x, unit.y);
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
||||
//try to pick up a block
|
||||
if(command == UnitCommand.loadBlocksCommand && (targetPos == null || unit.within(targetPos, 1f))){
|
||||
Building build = world.buildWorld(unit.x, unit.y);
|
||||
|
||||
if(build != null && state.teams.canInteract(unit.team, build.team)){
|
||||
//pick up block's payload
|
||||
Payload current = build.getPayload();
|
||||
if(current != null && pay.canPickupPayload(current)){
|
||||
Call.pickedBuildPayload(unit, build, false);
|
||||
//pick up whole building directly
|
||||
}else if(build.block.buildVisibility != BuildVisibility.hidden && build.canPickup() && pay.canPickup(build)){
|
||||
Call.pickedBuildPayload(unit, build, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!net.client() && command == UnitCommand.enterPayloadCommand && unit.buildOn() != null && (targetPos == null || (world.buildWorld(targetPos.x, targetPos.y) != null && world.buildWorld(targetPos.x, targetPos.y) == unit.buildOn()))){
|
||||
var build = unit.buildOn();
|
||||
tmpPayload.unit = unit;
|
||||
if(build.team == unit.team && build.acceptPayload(build, tmpPayload)){
|
||||
Call.unitEnteredPayload(unit, build);
|
||||
return; //no use updating after this, the unit is gone!
|
||||
}
|
||||
}
|
||||
|
||||
//acquiring naval targets isn't supported yet, so use the fallback dumb AI
|
||||
if(unit.team.isAI() && unit.team.rules().rtsAi && unit.type.naval){
|
||||
if(fallback == null) fallback = new GroundAI();
|
||||
@@ -114,22 +185,9 @@ public class CommandAI extends AIController{
|
||||
targetPos = null;
|
||||
}
|
||||
|
||||
if(targetPos != null){
|
||||
if(timer.get(timerTarget3, localInterval) || !flocked){
|
||||
if(!flocked){
|
||||
//make sure updates are staggered randomly
|
||||
timer.reset(timerTarget3, Mathf.random(localInterval));
|
||||
}
|
||||
|
||||
local.clear();
|
||||
//TODO experiment with 2/3/4
|
||||
float size = unit.hitSize * 3f;
|
||||
unit.team.data().tree().intersect(unit.x - size / 2f, unit.y - size/2f, size, size, local);
|
||||
local.remove(unit);
|
||||
flocked = true;
|
||||
}
|
||||
}else{
|
||||
flocked = false;
|
||||
//move on to the next target
|
||||
if(attackTarget == null && targetPos == null){
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(attackTarget != null){
|
||||
@@ -139,7 +197,7 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
targetPos.set(attackTarget);
|
||||
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs){
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs && stance != UnitStance.ram){
|
||||
Tile best = build.findClosestEdge(unit, Tile::solid);
|
||||
if(best != null){
|
||||
targetPos.set(best);
|
||||
@@ -148,21 +206,64 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
if(targetPos != null){
|
||||
boolean move = true;
|
||||
boolean move = true, isFinalPoint = commandQueue.size == 0;
|
||||
vecOut.set(targetPos);
|
||||
vecMovePos.set(targetPos);
|
||||
|
||||
if(unit.isGrounded()){
|
||||
move = Vars.controlPath.getPathPosition(unit, pathId, targetPos, vecOut, noFound);
|
||||
//the enter payload command requires an exact position
|
||||
if(group != null && group.valid && groupIndex < group.units.size && command != UnitCommand.enterPayloadCommand){
|
||||
vecMovePos.add(group.positions[groupIndex * 2], group.positions[groupIndex * 2 + 1]);
|
||||
}
|
||||
|
||||
//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){
|
||||
move = false;
|
||||
}
|
||||
|
||||
if(unit.isGrounded() && stance != UnitStance.ram){
|
||||
if(timer.get(timerTarget3, avoidInterval)){
|
||||
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
|
||||
float max = unit.hitSize/2f;
|
||||
float radius = Math.max(7f, max);
|
||||
float margin = 4f;
|
||||
blockingUnit = Units.nearbyCheck(unit.x + dstPos.x - radius/2f, unit.y + dstPos.y - radius/2f, radius, radius,
|
||||
u -> u != unit && u.within(unit, u.hitSize/2f + unit.hitSize/2f + margin) && u.controller() instanceof CommandAI ai && ai.targetPos != null &&
|
||||
//stop for other unit only if it's closer to the target
|
||||
(ai.targetPos.equals(targetPos) && u.dst2(targetPos) < unit.dst2(targetPos)) &&
|
||||
//don't stop if they're facing the same way
|
||||
!Angles.within(unit.rotation, u.rotation, 15f) &&
|
||||
//must be near an obstacle, stopping in open ground is pointless
|
||||
ControlPathfinder.isNearObstacle(unit, unit.tileX(), unit.tileY(), u.tileX(), u.tileY()));
|
||||
}
|
||||
|
||||
float maxBlockTime = 60f * 5f;
|
||||
|
||||
if(blockingUnit){
|
||||
timeSpentBlocked += Time.delta;
|
||||
|
||||
if(timeSpentBlocked >= maxBlockTime*2f){
|
||||
timeSpentBlocked = 0f;
|
||||
}
|
||||
}else{
|
||||
timeSpentBlocked = 0f;
|
||||
}
|
||||
|
||||
//if you've spent 3 seconds stuck, something is wrong, move regardless
|
||||
move = Vars.controlPath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
|
||||
//we've reached the final point if the returned coordinate is equal to the supplied input
|
||||
isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f);
|
||||
|
||||
//if the path is invalid, stop trying and record the end as unreachable
|
||||
if(unit.team.isAI() && (noFound[0] || unit.isPathImpassable(World.toTile(targetPos.x), World.toTile(targetPos.y)) )){
|
||||
if(unit.team.isAI() && (noFound[0] || unit.isPathImpassable(World.toTile(vecMovePos.x), World.toTile(vecMovePos.y)))){
|
||||
if(attackTarget instanceof Building build){
|
||||
unreachableBuildings.addUnique(build.pos());
|
||||
}
|
||||
attackTarget = null;
|
||||
targetPos = null;
|
||||
finishPath();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
vecOut.set(vecMovePos);
|
||||
}
|
||||
|
||||
float engageRange = unit.type.range - 10f;
|
||||
@@ -173,10 +274,10 @@ public class CommandAI extends AIController{
|
||||
circleAttack(80f);
|
||||
}else{
|
||||
moveTo(vecOut,
|
||||
attackTarget != null && unit.within(attackTarget, engageRange) ? engageRange :
|
||||
attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram ? engageRange :
|
||||
unit.isGrounded() ? 0f :
|
||||
attackTarget != null ? engageRange :
|
||||
0f, unit.isFlying() ? 40f : 100f, false, null, targetPos.epsilonEquals(vecOut, 4.1f));
|
||||
attackTarget != null && stance != UnitStance.ram ? engageRange :
|
||||
0f, unit.isFlying() ? 40f : 100f, false, null, isFinalPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,33 +286,19 @@ public class CommandAI extends AIController{
|
||||
attackTarget = null;
|
||||
}
|
||||
|
||||
if(unit.isFlying()){
|
||||
unit.lookAt(targetPos);
|
||||
if(unit.isFlying() && move && (attackTarget == null || !unit.within(attackTarget, unit.type.range))){
|
||||
unit.lookAt(vecMovePos);
|
||||
}else{
|
||||
faceTarget();
|
||||
}
|
||||
|
||||
if(attackTarget == null){
|
||||
if(unit.within(targetPos, Math.max(5f, unit.hitSize / 2f))){
|
||||
targetPos = null;
|
||||
}else if(local.size > 1){
|
||||
int count = 0;
|
||||
for(var near : local){
|
||||
//has arrived - no current command, but last one is equal
|
||||
if(near.isCommandable() && !near.command().hasCommand() && targetPos.epsilonEquals(near.command().lastTargetPos, 0.001f)){
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
|
||||
//others have arrived at destination, so this one will too
|
||||
if(count >= Math.max(3, local.size / 2)){
|
||||
targetPos = null;
|
||||
}
|
||||
}
|
||||
//reached destination, end pathfinding
|
||||
if(attackTarget == null && unit.within(vecMovePos, command.exactArrival && commandQueue.size == 0 ? 1f : Math.max(5f, unit.hitSize / 2f))){
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(stopWhenInRange && targetPos != null && unit.within(targetPos, engageRange * 0.9f)){
|
||||
targetPos = null;
|
||||
if(stopWhenInRange && targetPos != null && unit.within(vecMovePos, engageRange * 0.9f)){
|
||||
finishPath();
|
||||
stopWhenInRange = false;
|
||||
}
|
||||
|
||||
@@ -220,6 +307,68 @@ 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){
|
||||
return;
|
||||
}
|
||||
|
||||
Vec2 prev = targetPos;
|
||||
targetPos = null;
|
||||
|
||||
if(commandQueue.size > 0){
|
||||
var next = commandQueue.remove(0);
|
||||
if(next instanceof Teamc target){
|
||||
commandTarget(target, this.stopAtTarget);
|
||||
}else if(next instanceof Vec2 position){
|
||||
commandPosition(position);
|
||||
}
|
||||
|
||||
if(prev != null && stance == UnitStance.patrol){
|
||||
commandQueue.add(prev.cpy());
|
||||
}
|
||||
|
||||
//make sure spot in formation is reachable
|
||||
if(group != null){
|
||||
group.updateRaycast(groupIndex, next instanceof Vec2 position ? position : Tmp.v3.set(next));
|
||||
}
|
||||
}else{
|
||||
if(group != null){
|
||||
group = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void commandQueue(Position location){
|
||||
if(targetPos == null && attackTarget == null){
|
||||
if(location instanceof Teamc target){
|
||||
commandTarget(target, this.stopAtTarget);
|
||||
}else if(location instanceof Vec2 position){
|
||||
commandPosition(position);
|
||||
}
|
||||
}else if(commandQueue.size < maxCommandQueueSize && !commandQueue.contains(location)){
|
||||
commandQueue.add(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRead(Unit unit){
|
||||
if(readAttackTarget != -1){
|
||||
attackTarget = Groups.unit.getByID(readAttackTarget);
|
||||
readAttackTarget = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float prefSpeed(){
|
||||
return group == null ? super.prefSpeed() : Math.min(group.minSpeed, unit.speed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFire(){
|
||||
return stance != UnitStance.holdFire;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet bullet){
|
||||
if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null &&
|
||||
@@ -259,6 +408,8 @@ public class CommandAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void commandPosition(Vec2 pos){
|
||||
if(pos == null) return;
|
||||
|
||||
commandPosition(pos, false);
|
||||
if(commandController != null){
|
||||
commandController.commandPosition(pos);
|
||||
@@ -266,8 +417,10 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
public void commandPosition(Vec2 pos, boolean stopWhenInRange){
|
||||
targetPos = pos;
|
||||
lastTargetPos = pos;
|
||||
if(pos == null) return;
|
||||
|
||||
//this is an allocation, but it's relatively rarely called anyway, and outside mutations must be prevented
|
||||
targetPos = lastTargetPos = pos.cpy();
|
||||
attackTarget = null;
|
||||
pathId = Vars.controlPath.nextTargetId();
|
||||
this.stopWhenInRange = stopWhenInRange;
|
||||
|
||||
@@ -4,9 +4,13 @@ import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class LogicAI extends AIController{
|
||||
/** Minimum delay between item transfers. */
|
||||
@@ -40,6 +44,12 @@ public class LogicAI extends AIController{
|
||||
private float lastMoveX, lastMoveY;
|
||||
private int lastPathId = 0;
|
||||
|
||||
// LogicAI state should not be reset after reading.
|
||||
@Override
|
||||
public boolean keepState(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(control == LUnitControl.pathfind){
|
||||
@@ -81,6 +91,30 @@ public class LogicAI extends AIController{
|
||||
}
|
||||
}
|
||||
}
|
||||
case autoPathfind -> {
|
||||
Building core = unit.closestEnemyCore();
|
||||
|
||||
if((core == null || !unit.within(core, unit.range() * 0.5f))){
|
||||
boolean move = true;
|
||||
Tile spawner = null;
|
||||
|
||||
if(state.rules.waves && unit.team == state.rules.defaultTeam){
|
||||
spawner = getClosestSpawner();
|
||||
if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)) move = false;
|
||||
}
|
||||
|
||||
if(move){
|
||||
if(unit.isFlying()){
|
||||
var target = core == null ? spawner : core;
|
||||
if(target != null){
|
||||
moveTo(target, unit.range() * 0.5f);
|
||||
}
|
||||
}else{
|
||||
pathfind(Pathfinder.fieldCore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case stop -> {
|
||||
unit.clearBuilding();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class MinerAI extends AIController{
|
||||
|
||||
if(!(unit.canMine()) || core == null) return;
|
||||
|
||||
if(unit.mineTile != null && !unit.mineTile.within(unit, unit.type.mineRange)){
|
||||
if(!unit.validMine(unit.mineTile)){
|
||||
unit.mineTile(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public class MissileAI extends AIController{
|
||||
|
||||
float time = unit instanceof TimedKillc t ? t.time() : 1000000f;
|
||||
|
||||
if(time >= unit.type.homingDelay && shooter != null){
|
||||
if(time >= unit.type.homingDelay && shooter != null && !shooter.dead()){
|
||||
unit.lookAt(shooter.aimX, shooter.aimY);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class PhysicsProcess implements AsyncProcess{
|
||||
private static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
public static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
|
||||
private PhysicsWorld physics;
|
||||
private Seq<PhysicRef> refs = new Seq<>(false);
|
||||
@@ -58,9 +58,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
//save last position
|
||||
PhysicRef ref = entity.physref;
|
||||
|
||||
ref.body.layer =
|
||||
entity.type.allowLegStep && entity.type.legPhysicsLayer ? layerLegs :
|
||||
entity.isGrounded() ? layerGround : layerFlying;
|
||||
ref.body.layer = entity.collisionLayer();
|
||||
ref.x = entity.x;
|
||||
ref.y = entity.y;
|
||||
ref.body.local = local || entity.isLocal();
|
||||
|
||||
@@ -17,7 +17,7 @@ import static mindustry.Vars.*;
|
||||
|
||||
/** Controls playback of multiple audio tracks.*/
|
||||
public class SoundControl{
|
||||
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.6f, musicWaveChance = 0.46f;
|
||||
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.8f, musicWaveChance = 0.46f;
|
||||
|
||||
/** normal, ambient music, plays at any time */
|
||||
public Seq<Music> ambientMusic = Seq.with();
|
||||
@@ -28,6 +28,7 @@ public class SoundControl{
|
||||
|
||||
protected Music lastRandomPlayed;
|
||||
protected Interval timer = new Interval(4);
|
||||
protected long lastPlayed;
|
||||
protected @Nullable Music current;
|
||||
protected float fade;
|
||||
protected boolean silenced;
|
||||
@@ -55,6 +56,10 @@ public class SoundControl{
|
||||
}));
|
||||
|
||||
setupFilters();
|
||||
|
||||
Events.on(ResetEvent.class, e -> {
|
||||
lastPlayed = Time.millis();
|
||||
});
|
||||
}
|
||||
|
||||
protected void setupFilters(){
|
||||
@@ -146,7 +151,7 @@ public class SoundControl{
|
||||
if(state.isMenu()){
|
||||
silenced = false;
|
||||
if(ui.planet.isShown()){
|
||||
play(Musics.launch);
|
||||
play(ui.planet.state.planet.launchMusic);
|
||||
}else if(ui.editor.isShown()){
|
||||
play(Musics.editor);
|
||||
}else{
|
||||
@@ -160,9 +165,10 @@ public class SoundControl{
|
||||
silence();
|
||||
|
||||
//play music at intervals
|
||||
if(timer.get(musicInterval)){
|
||||
if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
|
||||
//chance to play it per interval
|
||||
if(Mathf.chance(musicChance)){
|
||||
lastPlayed = Time.millis();
|
||||
playRandom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public class Blocks{
|
||||
//logic
|
||||
message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank,
|
||||
canvas, reinforcedMessage,
|
||||
worldProcessor, worldCell, worldMessage,
|
||||
worldProcessor, worldCell, worldMessage, worldSwitch,
|
||||
|
||||
//campaign
|
||||
launchPad, interplanetaryAccelerator
|
||||
@@ -570,7 +570,7 @@ public class Blocks{
|
||||
snowWall = new StaticWall("snow-wall");
|
||||
|
||||
duneWall = new StaticWall("dune-wall"){{
|
||||
basalt.asFloor().wall = darksandWater.asFloor().wall = darksandTaintedWater.asFloor().wall = this;
|
||||
hotrock.asFloor().wall = magmarock.asFloor().wall = basalt.asFloor().wall = darksandWater.asFloor().wall = darksandTaintedWater.asFloor().wall = this;
|
||||
attributes.set(Attribute.sand, 2f);
|
||||
}};
|
||||
|
||||
@@ -1315,6 +1315,7 @@ public class Blocks{
|
||||
|
||||
researchCostMultiplier = 10f;
|
||||
|
||||
group = BlockGroup.heat;
|
||||
size = 3;
|
||||
drawer = new DrawMulti(new DrawDefault(), new DrawHeatOutput(), new DrawHeatInput("-heat"));
|
||||
regionRotated1 = 1;
|
||||
@@ -1325,6 +1326,7 @@ public class Blocks{
|
||||
|
||||
researchCostMultiplier = 10f;
|
||||
|
||||
group = BlockGroup.heat;
|
||||
size = 3;
|
||||
drawer = new DrawMulti(new DrawDefault(), new DrawHeatOutput(-1, false), new DrawHeatOutput(), new DrawHeatOutput(1, false), new DrawHeatInput("-heat"));
|
||||
regionRotated1 = 1;
|
||||
@@ -2406,7 +2408,7 @@ public class Blocks{
|
||||
largeSolarPanel = new SolarGenerator("solar-panel-large"){{
|
||||
requirements(Category.power, with(Items.lead, 80, Items.silicon, 110, Items.phaseFabric, 15));
|
||||
size = 3;
|
||||
powerProduction = 1.3f;
|
||||
powerProduction = 1.6f;
|
||||
}};
|
||||
|
||||
thoriumReactor = new NuclearReactor("thorium-reactor"){{
|
||||
@@ -2431,6 +2433,7 @@ public class Blocks{
|
||||
itemDuration = 140f;
|
||||
ambientSound = Sounds.pulse;
|
||||
ambientSoundVolume = 0.07f;
|
||||
liquidCapacity = 60f;
|
||||
|
||||
consumePower(25f);
|
||||
consumeItem(Items.blastCompound);
|
||||
@@ -2456,6 +2459,7 @@ public class Blocks{
|
||||
consumesPower = outputsPower = true;
|
||||
range = 23;
|
||||
scaledHealth = 90;
|
||||
fogRadius = 2;
|
||||
|
||||
consumePowerBuffered(40000f);
|
||||
}};
|
||||
@@ -2548,7 +2552,6 @@ public class Blocks{
|
||||
researchCostMultiplier = 0.4f;
|
||||
}};
|
||||
|
||||
//TODO stats
|
||||
fluxReactor = new VariableReactor("flux-reactor"){{
|
||||
requirements(Category.power, with(Items.graphite, 300, Items.carbide, 200, Items.oxide, 100, Items.silicon, 600, Items.surgeAlloy, 300));
|
||||
powerProduction = 120f;
|
||||
@@ -2585,7 +2588,6 @@ public class Blocks{
|
||||
);
|
||||
}};
|
||||
|
||||
//TODO stats
|
||||
neoplasiaReactor = new HeaterGenerator("neoplasia-reactor"){{
|
||||
requirements(Category.power, with(Items.tungsten, 1000, Items.carbide, 300, Items.oxide, 150, Items.silicon, 500, Items.phaseFabric, 300, Items.surgeAlloy, 200));
|
||||
|
||||
@@ -2609,7 +2611,6 @@ public class Blocks{
|
||||
explodeSound = Sounds.largeExplosion;
|
||||
|
||||
powerProduction = 140f;
|
||||
rebuildable = false;
|
||||
|
||||
ambientSound = Sounds.bioLoop;
|
||||
ambientSoundVolume = 0.2f;
|
||||
@@ -2937,6 +2938,7 @@ public class Blocks{
|
||||
armor = 5f;
|
||||
alwaysUnlocked = true;
|
||||
incinerateNonBuildable = true;
|
||||
requiresCoreZone = true;
|
||||
|
||||
//TODO should this be higher?
|
||||
buildCostMultiplier = 0.7f;
|
||||
@@ -2956,6 +2958,7 @@ public class Blocks{
|
||||
armor = 10f;
|
||||
incinerateNonBuildable = true;
|
||||
buildCostMultiplier = 0.7f;
|
||||
requiresCoreZone = true;
|
||||
|
||||
unitCapModifier = 15;
|
||||
researchCostMultipliers.put(Items.silicon, 0.5f);
|
||||
@@ -2973,6 +2976,7 @@ public class Blocks{
|
||||
armor = 15f;
|
||||
incinerateNonBuildable = true;
|
||||
buildCostMultiplier = 0.7f;
|
||||
requiresCoreZone = true;
|
||||
|
||||
unitCapModifier = 15;
|
||||
researchCostMultipliers.put(Items.silicon, 0.4f);
|
||||
@@ -3055,7 +3059,7 @@ public class Blocks{
|
||||
progress = PartProgress.recoil;
|
||||
recoilIndex = f;
|
||||
under = true;
|
||||
moves.add(new PartMove(PartProgress.recoil, 0f, -1.5f, 0f));
|
||||
moveY = -1.5f;
|
||||
}});
|
||||
}
|
||||
}};
|
||||
@@ -3122,6 +3126,15 @@ public class Blocks{
|
||||
}};
|
||||
}}
|
||||
);
|
||||
|
||||
drawer = new DrawTurret(){{
|
||||
parts.add(new RegionPart("-mid"){{
|
||||
progress = PartProgress.recoil;
|
||||
under = false;
|
||||
moveY = -1.25f;
|
||||
}});
|
||||
}};
|
||||
|
||||
reload = 18f;
|
||||
range = 220f;
|
||||
size = 2;
|
||||
@@ -3130,7 +3143,7 @@ public class Blocks{
|
||||
shoot.shotDelay = 5f;
|
||||
shoot.shots = 2;
|
||||
|
||||
recoil = 2f;
|
||||
recoil = 1f;
|
||||
rotateSpeed = 15f;
|
||||
inaccuracy = 17f;
|
||||
shootCone = 35f;
|
||||
@@ -3178,6 +3191,7 @@ public class Blocks{
|
||||
reload = 6f;
|
||||
coolantMultiplier = 1.5f;
|
||||
range = 60f;
|
||||
shootY = 3;
|
||||
shootCone = 50f;
|
||||
targetAir = false;
|
||||
ammoUseEffect = Fx.none;
|
||||
@@ -3407,15 +3421,18 @@ public class Blocks{
|
||||
lightningLength = 10;
|
||||
}}
|
||||
);
|
||||
|
||||
shoot = new ShootAlternate(){{
|
||||
|
||||
shoot = new ShootBarrel(){{
|
||||
barrels = new float[]{
|
||||
-4, -1.25f, 0,
|
||||
0, 0, 0,
|
||||
4, -1.25f, 0
|
||||
};
|
||||
shots = 4;
|
||||
barrels = 3;
|
||||
spread = 3.5f;
|
||||
shotDelay = 5f;
|
||||
}};
|
||||
|
||||
shootY = 7f;
|
||||
shootY = 4.5f;
|
||||
reload = 30f;
|
||||
inaccuracy = 10f;
|
||||
range = 240f;
|
||||
@@ -3479,12 +3496,26 @@ public class Blocks{
|
||||
}}
|
||||
);
|
||||
|
||||
drawer = new DrawTurret(){{
|
||||
parts.add(new RegionPart("-side"){{
|
||||
progress = PartProgress.warmup;
|
||||
moveX = 0.6f;
|
||||
moveRot = -15f;
|
||||
mirror = true;
|
||||
layerOffset = 0.001f;
|
||||
moves.add(new PartMove(PartProgress.recoil, 0.5f, -0.5f, -8f));
|
||||
}}, new RegionPart("-barrel"){{
|
||||
progress = PartProgress.recoil;
|
||||
moveY = -2.5f;
|
||||
}});
|
||||
}};
|
||||
|
||||
size = 2;
|
||||
range = 190f;
|
||||
reload = 31f;
|
||||
consumeAmmoOnce = false;
|
||||
ammoEjectBack = 3f;
|
||||
recoil = 3f;
|
||||
recoil = 0f;
|
||||
shake = 1f;
|
||||
shoot.shots = 4;
|
||||
shoot.shotDelay = 3f;
|
||||
@@ -3776,7 +3807,8 @@ public class Blocks{
|
||||
explodeRange = 20f;
|
||||
}}
|
||||
);
|
||||
shootY = 8.75f;
|
||||
shootY = 10f;
|
||||
|
||||
shoot = new ShootBarrel(){{
|
||||
barrels = new float[]{
|
||||
0f, 1f, 0f,
|
||||
@@ -3784,10 +3816,25 @@ public class Blocks{
|
||||
-3f, 0f, 0f,
|
||||
};
|
||||
}};
|
||||
|
||||
recoils = 3;
|
||||
drawer = new DrawTurret(){{
|
||||
for(int i = 3; i > 0; i--){
|
||||
int f = i;
|
||||
parts.add(new RegionPart("-barrel-" + i){{
|
||||
progress = PartProgress.recoil;
|
||||
recoilIndex = f - 1;
|
||||
under = true;
|
||||
moveY = -2f;
|
||||
}});
|
||||
}
|
||||
}};
|
||||
|
||||
reload = 8f;
|
||||
range = 200f;
|
||||
size = 3;
|
||||
recoil = 3f;
|
||||
recoil = 1.5f;
|
||||
recoilTime = 10;
|
||||
rotateSpeed = 10f;
|
||||
inaccuracy = 10f;
|
||||
shootCone = 30f;
|
||||
@@ -3803,16 +3850,19 @@ public class Blocks{
|
||||
|
||||
requirements(Category.turret, with(Items.copper, 1000, Items.metaglass, 600, Items.surgeAlloy, 300, Items.plastanium, 200, Items.silicon, 600));
|
||||
ammo(
|
||||
Items.surgeAlloy, new PointBulletType(){{
|
||||
Items.surgeAlloy, new RailBulletType(){{
|
||||
shootEffect = Fx.instShoot;
|
||||
hitEffect = Fx.instHit;
|
||||
pierceEffect = Fx.railHit;
|
||||
smokeEffect = Fx.smokeCloud;
|
||||
trailEffect = Fx.instTrail;
|
||||
pointEffect = Fx.instTrail;
|
||||
despawnEffect = Fx.instBomb;
|
||||
trailSpacing = 20f;
|
||||
pointEffectSpace = 20f;
|
||||
damage = 1350;
|
||||
buildingDamageMultiplier = 0.2f;
|
||||
speed = brange;
|
||||
maxDamageFraction = 0.6f;
|
||||
pierceDamageFactor = 1f;
|
||||
length = brange;
|
||||
hitShake = 6f;
|
||||
ammoMultiplier = 1f;
|
||||
}}
|
||||
@@ -3919,6 +3969,7 @@ public class Blocks{
|
||||
hitColor = Pal.meltdownHit;
|
||||
status = StatusEffects.melting;
|
||||
drawSize = 420f;
|
||||
timescaleDamage = true;
|
||||
|
||||
incendChance = 0.4f;
|
||||
incendSpread = 5f;
|
||||
@@ -4491,7 +4542,7 @@ public class Blocks{
|
||||
}};
|
||||
|
||||
scathe = new ItemTurret("scathe"){{
|
||||
requirements(Category.turret, with(Items.silicon, 450, Items.graphite, 400, Items.tungsten, 500, Items.carbide, 300));
|
||||
requirements(Category.turret, with(Items.silicon, 450, Items.graphite, 400, Items.tungsten, 500, Items.oxide, 100, Items.carbide, 200));
|
||||
|
||||
ammo(
|
||||
Items.carbide, new BasicBulletType(0f, 1){{
|
||||
@@ -4528,7 +4579,7 @@ public class Blocks{
|
||||
deathExplosionEffect = Fx.massiveExplosion;
|
||||
shootOnDeath = true;
|
||||
shake = 10f;
|
||||
bullet = new ExplosionBulletType(640f, 65f){{
|
||||
bullet = new ExplosionBulletType(1500f, 65f){{
|
||||
hitColor = Pal.redLight;
|
||||
shootEffect = new MultiEffect(Fx.massiveExplosion, Fx.scatheExplosion, Fx.scatheLight, new WaveEffect(){{
|
||||
lifetime = 10f;
|
||||
@@ -4537,7 +4588,7 @@ public class Blocks{
|
||||
}});
|
||||
|
||||
collidesAir = false;
|
||||
buildingDamageMultiplier = 0.3f;
|
||||
buildingDamageMultiplier = 0.25f;
|
||||
|
||||
ammoMultiplier = 1f;
|
||||
fragLifeMin = 0.1f;
|
||||
@@ -4552,7 +4603,7 @@ public class Blocks{
|
||||
width = height = 18f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 40f;
|
||||
splashDamage = 80f;
|
||||
splashDamage = 160f;
|
||||
backColor = trailColor = hitColor = Pal.redLight;
|
||||
frontColor = Color.white;
|
||||
smokeEffect = Fx.shootBigSmoke2;
|
||||
@@ -4624,7 +4675,7 @@ public class Blocks{
|
||||
|
||||
recoil = 0.5f;
|
||||
|
||||
fogRadiusMultiuplier = 0.4f;
|
||||
fogRadiusMultiplier = 0.4f;
|
||||
coolantMultiplier = 6f;
|
||||
shootSound = Sounds.missileLaunch;
|
||||
|
||||
@@ -4634,7 +4685,7 @@ public class Blocks{
|
||||
targetUnderBlocks = false;
|
||||
|
||||
shake = 6f;
|
||||
ammoPerShot = 20;
|
||||
ammoPerShot = 15;
|
||||
maxAmmo = 30;
|
||||
shootY = -1;
|
||||
outlineColor = Pal.darkOutline;
|
||||
@@ -5780,7 +5831,7 @@ public class Blocks{
|
||||
}};
|
||||
|
||||
interplanetaryAccelerator = new Accelerator("interplanetary-accelerator"){{
|
||||
requirements(Category.effect, BuildVisibility.campaignOnly, with(Items.copper, 16000, Items.silicon, 11000, Items.thorium, 13000, Items.titanium, 12000, Items.surgeAlloy, 6000, Items.phaseFabric, 5000));
|
||||
requirements(Category.effect, BuildVisibility.hidden, with(Items.copper, 16000, Items.silicon, 11000, Items.thorium, 13000, Items.titanium, 12000, Items.surgeAlloy, 6000, Items.phaseFabric, 5000));
|
||||
researchCostMultiplier = 0.1f;
|
||||
size = 7;
|
||||
hasPower = true;
|
||||
@@ -5856,7 +5907,7 @@ public class Blocks{
|
||||
}};
|
||||
|
||||
canvas = new CanvasBlock("canvas"){{
|
||||
requirements(Category.logic, BuildVisibility.shown, with(Items.silicon, 30, Items.beryllium, 10));
|
||||
requirements(Category.logic, BuildVisibility.shown, with(Items.silicon, 10, Items.beryllium, 10));
|
||||
|
||||
canvasSize = 12;
|
||||
padding = 7f / 4f * 2f;
|
||||
@@ -5878,7 +5929,7 @@ public class Blocks{
|
||||
forceDark = true;
|
||||
privileged = true;
|
||||
size = 1;
|
||||
maxInstructionsPerTick = 500;
|
||||
maxInstructionsPerTick = 1000;
|
||||
range = Float.MAX_VALUE;
|
||||
}};
|
||||
|
||||
@@ -5898,6 +5949,13 @@ public class Blocks{
|
||||
privileged = true;
|
||||
}};
|
||||
|
||||
worldSwitch = new SwitchBlock("world-switch"){{
|
||||
requirements(Category.logic, BuildVisibility.editorOnly, with());
|
||||
|
||||
targetable = false;
|
||||
privileged = true;
|
||||
}};
|
||||
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,11 +447,11 @@ public class ErekirTechTree{
|
||||
|
||||
//nodeProduce(Liquids.gallium, () -> {});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Items.surgeAlloy, () -> {
|
||||
nodeProduce(Items.phaseFabric, () -> {
|
||||
nodeProduce(Items.surgeAlloy, () -> {
|
||||
nodeProduce(Items.phaseFabric, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,11 +29,11 @@ public class Fx{
|
||||
|
||||
none = new Effect(0, 0f, e -> {}),
|
||||
|
||||
blockCrash = new Effect(100f, e -> {
|
||||
blockCrash = new Effect(90f, e -> {
|
||||
if(!(e.data instanceof Block block)) return;
|
||||
|
||||
alpha(e.fin() + 0.5f);
|
||||
float offset = Mathf.lerp(0f, 200f, e.fout());
|
||||
float offset = Mathf.lerp(0f, 180f, e.fout());
|
||||
color(0f, 0f, 0f, 0.44f);
|
||||
rect(block.fullIcon, e.x - offset * 4f, e.y, (float)block.size * 8f, (float)block.size * 8f);
|
||||
color(Color.white);
|
||||
@@ -417,6 +417,20 @@ public class Fx{
|
||||
Lines.spikes(e.x, e.y, 1f + e.fin() * 6f, e.fout() * 4f, 6);
|
||||
}),
|
||||
|
||||
sparkExplosion = new Effect(30f, 160f, e -> {
|
||||
color(e.color);
|
||||
stroke(e.fout() * 3f);
|
||||
float circleRad = 6f + e.finpow() * e.rotation;
|
||||
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);
|
||||
Lines.lineAngle(e.x, e.y, angle, e.foutpow() * e.rotation * 0.8f * rand.random(1f, 0.6f) + 2f, e.finpow() * e.rotation * 1.2f * lenRand + 6f);
|
||||
}
|
||||
}),
|
||||
|
||||
titanExplosion = new Effect(30f, 160f, e -> {
|
||||
color(e.color);
|
||||
stroke(e.fout() * 3f);
|
||||
@@ -609,6 +623,12 @@ public class Fx{
|
||||
Lines.circle(e.x, e.y, 2f + e.finpow() * 7f);
|
||||
}),
|
||||
|
||||
dynamicWave = new Effect(22, e -> {
|
||||
color(e.color, 0.7f);
|
||||
stroke(e.fout() * 2f);
|
||||
Lines.circle(e.x, e.y, 4f + e.finpow() * e.rotation);
|
||||
}),
|
||||
|
||||
shieldWave = new Effect(22, e -> {
|
||||
color(e.color, 0.7f);
|
||||
stroke(e.fout() * 2f);
|
||||
@@ -1517,6 +1537,15 @@ public class Fx{
|
||||
});
|
||||
}),
|
||||
|
||||
smokePuff = new Effect(30, e -> {
|
||||
color(e.color);
|
||||
|
||||
randLenVectors(e.id, 6, 4f + 30f * e.finpow(), (x, y) -> {
|
||||
Fill.circle(e.x + x, e.y + y, e.fout() * 3f);
|
||||
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout());
|
||||
});
|
||||
}),
|
||||
|
||||
shootSmall = new Effect(8, e -> {
|
||||
color(Pal.lighterOrange, Pal.lightOrange, e.fin());
|
||||
float w = 1f + 5 * e.fout();
|
||||
@@ -1681,7 +1710,7 @@ public class Fx{
|
||||
}),
|
||||
|
||||
regenSuppressParticle = new Effect(35f, e -> {
|
||||
color(Pal.sapBullet, e.color, e.fin());
|
||||
color(e.color, Color.white, e.fin());
|
||||
stroke(e.fout() * 1.4f + 0.5f);
|
||||
|
||||
randLenVectors(e.id, 4, 17f * e.fin(), (x, y) -> {
|
||||
@@ -1700,7 +1729,7 @@ public class Fx{
|
||||
|
||||
Tmp.bz2.valueAt(Tmp.v4, e.fout());
|
||||
|
||||
color(Pal.sapBullet);
|
||||
color(e.color);
|
||||
Fill.circle(Tmp.v4.x, Tmp.v4.y, e.fslope() * 2f + 0.1f);
|
||||
}).followParent(false).rotWithParent(false),
|
||||
|
||||
@@ -2416,6 +2445,26 @@ public class Fx{
|
||||
Lines.poly(e.x, e.y, 6, e.rotation + e.fin());
|
||||
}).followParent(true),
|
||||
|
||||
arcShieldBreak = new Effect(40, e -> {
|
||||
Lines.stroke(3 * e.fout(), e.color);
|
||||
if(e.data instanceof Unit u){
|
||||
ShieldArcAbility ab = (ShieldArcAbility) Structs.find(u.abilities, a -> a instanceof ShieldArcAbility);
|
||||
if(ab != null){
|
||||
Vec2 pos = Tmp.v1.set(ab.x, ab.y).rotate(u.rotation - 90f).add(u);
|
||||
Lines.arc(pos.x, pos.y, ab.radius + ab.width/2, ab.angle / 360f, u.rotation + ab.angleOffset - ab.angle / 2f);
|
||||
Lines.arc(pos.x, pos.y, ab.radius - ab.width/2, ab.angle / 360f, u.rotation + ab.angleOffset - ab.angle / 2f);
|
||||
for(int i : Mathf.signs){
|
||||
float
|
||||
px = pos.x + Angles.trnsx(u.rotation + ab.angleOffset - ab.angle / 2f * i, ab.radius + ab.width / 2),
|
||||
py = pos.y + Angles.trnsy(u.rotation + ab.angleOffset - ab.angle / 2f * i, ab.radius + ab.width / 2),
|
||||
px1 = pos.x + Angles.trnsx(u.rotation + ab.angleOffset - ab.angle / 2f * i, ab.radius - ab.width / 2),
|
||||
py1 = pos.y + Angles.trnsy(u.rotation + ab.angleOffset - ab.angle / 2f * i, ab.radius - ab.width / 2);
|
||||
Lines.line(px, py, px1, py1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).followParent(true),
|
||||
|
||||
coreLandDust = new Effect(100f, e -> {
|
||||
color(e.color, e.fout(0.1f));
|
||||
rand.setSeed(e.id);
|
||||
@@ -2529,5 +2578,23 @@ public class Fx{
|
||||
|
||||
stroke(data.region.height * scl);
|
||||
line(data.region, data.a.x + ox, data.a.y + oy, data.b.x + ox, data.b.y + oy, false);
|
||||
}).layer(Layer.groundUnit + 5f);
|
||||
}).layer(Layer.groundUnit + 5f),
|
||||
|
||||
debugLine = new Effect(90f, 1000000000000f, e -> {
|
||||
if(!(e.data instanceof Vec2[] vec)) return;
|
||||
|
||||
Draw.color(e.color);
|
||||
Lines.stroke(1f);
|
||||
|
||||
if(vec.length == 2){
|
||||
Lines.line(vec[0].x, vec[0].y, vec[1].x, vec[1].y);
|
||||
}else{
|
||||
Lines.beginLine();
|
||||
for(Vec2 v : vec)
|
||||
Lines.linePoint(v.x, v.y);
|
||||
Lines.endLine();
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class Liquids{
|
||||
capPuddles = false;
|
||||
spreadTarget = Liquids.water;
|
||||
moveThroughBlocks = true;
|
||||
incinerable = true;
|
||||
incinerable = false;
|
||||
blockReactive = false;
|
||||
canStayOn.addAll(water, oil, cryofluid);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import mindustry.type.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class StatusEffects{
|
||||
public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed, electrified, invincible;
|
||||
public static StatusEffect none, burning, freezing, unmoving, slow, fast, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed, electrified, invincible, dynamic;
|
||||
|
||||
public static void load(){
|
||||
|
||||
@@ -60,6 +60,15 @@ public class StatusEffects{
|
||||
slow = new StatusEffect("slow"){{
|
||||
color = Pal.lightishGray;
|
||||
speedMultiplier = 0.4f;
|
||||
|
||||
init(() -> opposite(fast));
|
||||
}};
|
||||
|
||||
fast = new StatusEffect("fast"){{
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
|
||||
init(() -> opposite(slow));
|
||||
}};
|
||||
|
||||
wet = new StatusEffect("wet"){{
|
||||
@@ -71,7 +80,8 @@ public class StatusEffects{
|
||||
|
||||
init(() -> {
|
||||
affinity(shocked, (unit, result, time) -> {
|
||||
unit.damagePierce(transitionDamage);
|
||||
unit.damage(transitionDamage);
|
||||
|
||||
if(unit.team == state.rules.waveTeam){
|
||||
Events.fire(Trigger.shock);
|
||||
}
|
||||
@@ -193,5 +203,11 @@ public class StatusEffects{
|
||||
invincible = new StatusEffect("invincible"){{
|
||||
healthMultiplier = Float.POSITIVE_INFINITY;
|
||||
}};
|
||||
|
||||
dynamic = new StatusEffect("dynamic"){{
|
||||
show = false;
|
||||
dynamic = true;
|
||||
permanent = true;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ public class TechTree{
|
||||
});
|
||||
|
||||
content.techNode = this;
|
||||
content.techNodes.add(this);
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,8 @@ public class UnitTypes{
|
||||
singleTarget = true;
|
||||
drownTimeMultiplier = 4f;
|
||||
|
||||
abilities.add(new ShieldRegenFieldAbility(25f, 250f, 60f * 1, 60f));
|
||||
|
||||
BulletType smallBullet = new BasicBulletType(3f, 10){{
|
||||
width = 7f;
|
||||
height = 9f;
|
||||
@@ -219,10 +221,10 @@ public class UnitTypes{
|
||||
shoot.shots = 3;
|
||||
shoot.shotDelay = 4f;
|
||||
|
||||
bullet = new BasicBulletType(7f, 50){{
|
||||
bullet = new BasicBulletType(8f, 80){{
|
||||
width = 11f;
|
||||
height = 20f;
|
||||
lifetime = 25f;
|
||||
lifetime = 27f;
|
||||
shootEffect = Fx.shootBig;
|
||||
lightning = 2;
|
||||
lightningLength = 6;
|
||||
@@ -252,11 +254,11 @@ public class UnitTypes{
|
||||
}};
|
||||
|
||||
reign = new UnitType("reign"){{
|
||||
speed = 0.35f;
|
||||
speed = 0.4f;
|
||||
hitSize = 26f;
|
||||
rotateSpeed = 1.65f;
|
||||
health = 24000;
|
||||
armor = 14f;
|
||||
armor = 18f;
|
||||
mechStepParticles = true;
|
||||
stepShake = 0.75f;
|
||||
drownTimeMultiplier = 6f;
|
||||
@@ -1285,7 +1287,6 @@ public class UnitTypes{
|
||||
lowAltitude = true;
|
||||
|
||||
ammoType = new PowerAmmoType(900);
|
||||
|
||||
mineTier = 2;
|
||||
mineSpeed = 3.5f;
|
||||
|
||||
@@ -1844,6 +1845,7 @@ public class UnitTypes{
|
||||
armor = 3f;
|
||||
|
||||
buildSpeed = 1.5f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{
|
||||
x = 0f;
|
||||
@@ -1934,6 +1936,7 @@ public class UnitTypes{
|
||||
abilities.add(new StatusFieldAbility(StatusEffects.overclock, 60f * 6, 60f * 6f, 60f));
|
||||
|
||||
buildSpeed = 2f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
weapons.add(new Weapon("plasma-mount-weapon"){{
|
||||
|
||||
@@ -2008,6 +2011,7 @@ public class UnitTypes{
|
||||
trailScl = 2f;
|
||||
|
||||
buildSpeed = 2f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{
|
||||
x = 11f;
|
||||
@@ -2149,17 +2153,20 @@ public class UnitTypes{
|
||||
trailScl = 3.2f;
|
||||
|
||||
buildSpeed = 3f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
abilities.add(new EnergyFieldAbility(40f, 65f, 180f){{
|
||||
statusDuration = 60f * 6f;
|
||||
maxTargets = 25;
|
||||
healPercent = 1.5f;
|
||||
sameTypeHealMult = 0.5f;
|
||||
}});
|
||||
|
||||
for(float mountY : new float[]{-18f, 14}){
|
||||
weapons.add(new PointDefenseWeapon("point-defense-mount"){{
|
||||
x = 12.5f;
|
||||
y = mountY;
|
||||
reload = 6f;
|
||||
reload = 4f;
|
||||
targetInterval = 8f;
|
||||
targetSwitchInterval = 8f;
|
||||
|
||||
@@ -2167,7 +2174,7 @@ public class UnitTypes{
|
||||
shootEffect = Fx.sparkShoot;
|
||||
hitEffect = Fx.pointHit;
|
||||
maxRange = 180f;
|
||||
damage = 25f;
|
||||
damage = 30f;
|
||||
}};
|
||||
}});
|
||||
}
|
||||
@@ -2190,6 +2197,7 @@ public class UnitTypes{
|
||||
trailScl = 3.5f;
|
||||
|
||||
buildSpeed = 3.5f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
for(float mountY : new float[]{-117/4f, 50/4f}){
|
||||
for(float sign : Mathf.signs){
|
||||
@@ -2241,7 +2249,13 @@ public class UnitTypes{
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
abilities.add(new SuppressionFieldAbility(){{
|
||||
orbRadius = 5;
|
||||
particleSize = 3;
|
||||
y = -10f;
|
||||
particles = 10;
|
||||
color = particleColor = effectColor = Pal.heal;
|
||||
}});
|
||||
weapons.add(new Weapon("emp-cannon-mount"){{
|
||||
rotate = true;
|
||||
|
||||
@@ -2343,6 +2357,7 @@ public class UnitTypes{
|
||||
speed = 3f;
|
||||
rotateSpeed = 15f;
|
||||
accel = 0.1f;
|
||||
fogRadius = 0f;
|
||||
itemCapacity = 30;
|
||||
health = 150f;
|
||||
engineOffset = 6f;
|
||||
@@ -2379,6 +2394,7 @@ public class UnitTypes{
|
||||
speed = 3.3f;
|
||||
rotateSpeed = 17f;
|
||||
accel = 0.1f;
|
||||
fogRadius = 0f;
|
||||
itemCapacity = 50;
|
||||
health = 170f;
|
||||
engineOffset = 6f;
|
||||
@@ -2420,6 +2436,7 @@ public class UnitTypes{
|
||||
speed = 3.55f;
|
||||
rotateSpeed = 19f;
|
||||
accel = 0.11f;
|
||||
fogRadius = 0f;
|
||||
itemCapacity = 70;
|
||||
health = 220f;
|
||||
engineOffset = 6f;
|
||||
@@ -2628,7 +2645,10 @@ public class UnitTypes{
|
||||
width = 5f;
|
||||
height = 7f;
|
||||
lifetime = 15f;
|
||||
hitSize = 4f;
|
||||
hitSize = 4f;
|
||||
pierceCap = 3;
|
||||
pierce = true;
|
||||
pierceBuilding = true;
|
||||
hitColor = backColor = trailColor = Color.valueOf("feb380");
|
||||
frontColor = Color.white;
|
||||
trailWidth = 1.7f;
|
||||
@@ -3258,7 +3278,7 @@ public class UnitTypes{
|
||||
drag = 0.1f;
|
||||
speed = 0.6f;
|
||||
hitSize = 23f;
|
||||
health = 6700;
|
||||
health = 7300;
|
||||
armor = 5f;
|
||||
|
||||
lockLegBase = true;
|
||||
@@ -3271,13 +3291,14 @@ public class UnitTypes{
|
||||
|
||||
abilities.add(new ShieldArcAbility(){{
|
||||
region = "tecta-shield";
|
||||
radius = 34f;
|
||||
radius = 36f;
|
||||
angle = 82f;
|
||||
regen = 0.6f;
|
||||
cooldown = 60f * 8f;
|
||||
max = 1500f;
|
||||
max = 2000f;
|
||||
y = -20f;
|
||||
width = 6f;
|
||||
whenShooting = false;
|
||||
}});
|
||||
|
||||
rotateSpeed = 2.1f;
|
||||
@@ -3319,14 +3340,14 @@ public class UnitTypes{
|
||||
velocityRnd = 0.33f;
|
||||
heatColor = Color.red;
|
||||
|
||||
bullet = new MissileBulletType(4.2f, 47){{
|
||||
bullet = new MissileBulletType(4.2f, 60){{
|
||||
homingPower = 0.2f;
|
||||
weaveMag = 4;
|
||||
weaveScale = 4;
|
||||
lifetime = 55f;
|
||||
shootEffect = Fx.shootBig2;
|
||||
smokeEffect = Fx.shootSmokeTitan;
|
||||
splashDamage = 60f;
|
||||
splashDamage = 70f;
|
||||
splashDamageRadius = 30f;
|
||||
frontColor = Color.white;
|
||||
hitSound = Sounds.none;
|
||||
|
||||
@@ -6,6 +6,7 @@ import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
@@ -40,6 +41,8 @@ public class ContentLoader{
|
||||
|
||||
/** Creates all base types. */
|
||||
public void createBaseContent(){
|
||||
UnitCommand.loadAll();
|
||||
UnitStance.loadAll();
|
||||
TeamEntries.load();
|
||||
Items.load();
|
||||
StatusEffects.load();
|
||||
@@ -310,4 +313,28 @@ public class ContentLoader{
|
||||
public Planet planet(String name){
|
||||
return getByName(ContentType.planet, name);
|
||||
}
|
||||
|
||||
public Seq<UnitStance> unitStances(){
|
||||
return getBy(ContentType.unitStance);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(int id){
|
||||
return getByID(ContentType.unitStance, id);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(String name){
|
||||
return getByName(ContentType.unitStance, name);
|
||||
}
|
||||
|
||||
public Seq<UnitCommand> unitCommands(){
|
||||
return getBy(ContentType.unitCommand);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(int id){
|
||||
return getByID(ContentType.unitCommand, id);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(String name){
|
||||
return getByName(ContentType.unitCommand, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
private Interval timer = new Interval(2);
|
||||
private boolean hiscore = false;
|
||||
private boolean wasPaused = false;
|
||||
private boolean wasPaused = false, backgroundPaused = false;
|
||||
private Seq<Building> toBePlaced = new Seq<>(false);
|
||||
|
||||
public Control(){
|
||||
@@ -332,6 +332,13 @@ public class Control implements ApplicationListener, Loadable{
|
||||
void createPlayer(){
|
||||
player = Player.create();
|
||||
player.name = Core.settings.getString("name");
|
||||
|
||||
String locale = Core.settings.getString("locale");
|
||||
if(locale.equals("default")){
|
||||
locale = Locale.getDefault().toString();
|
||||
}
|
||||
player.locale = locale;
|
||||
|
||||
player.color.set(Core.settings.getInt("color-0"));
|
||||
|
||||
if(mobile){
|
||||
@@ -551,6 +558,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
@Override
|
||||
public void pause(){
|
||||
if(settings.getBool("backgroundpause", true) && !net.active()){
|
||||
backgroundPaused = true;
|
||||
wasPaused = state.is(State.paused);
|
||||
if(state.is(State.playing)) state.set(State.paused);
|
||||
}
|
||||
@@ -561,6 +569,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
if(state.is(State.paused) && !wasPaused && settings.getBool("backgroundpause", true) && !net.active()){
|
||||
state.set(State.playing);
|
||||
}
|
||||
backgroundPaused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -652,6 +661,10 @@ public class Control implements ApplicationListener, Loadable{
|
||||
core.items.each((i, a) -> i.unlock());
|
||||
}
|
||||
|
||||
if(backgroundPaused && settings.getBool("backgroundpause") && !net.active()){
|
||||
state.set(State.paused);
|
||||
}
|
||||
|
||||
//cannot launch while paused
|
||||
if(state.isPaused() && renderer.isCutscene()){
|
||||
state.set(State.playing);
|
||||
|
||||
@@ -32,6 +32,10 @@ public class GameState{
|
||||
public Rules rules = new Rules();
|
||||
/** Statistics for this save/game. Displayed after game over. */
|
||||
public GameStats stats = new GameStats();
|
||||
/** Markers not linked to objectives. Controlled by world processors. */
|
||||
public MapMarkers markers = new MapMarkers();
|
||||
/** Locale-specific string bundles of current map */
|
||||
public MapLocales mapLocales = new MapLocales();
|
||||
/** Global attributes of the environment, calculated by weather. */
|
||||
public Attributes envAttrs = new Attributes();
|
||||
/** Team data. Gets reset every new game. */
|
||||
|
||||
@@ -445,6 +445,12 @@ public class Logic implements ApplicationListener{
|
||||
updateWeather();
|
||||
|
||||
for(TeamData data : state.teams.getActive()){
|
||||
//does not work on PvP so built-in attack maps can have it on by default without issues
|
||||
if(data.team.rules().buildAi && !state.rules.pvp){
|
||||
if(data.buildAi == null) data.buildAi = new BaseBuilderAI(data);
|
||||
data.buildAi.update();
|
||||
}
|
||||
|
||||
if(data.team.rules().rtsAi){
|
||||
if(data.rtsAi == null) data.rtsAi = new RtsAI(data);
|
||||
data.rtsAi.update();
|
||||
@@ -452,7 +458,6 @@ public class Logic implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
//TODO objectives clientside???
|
||||
if(!state.isEditor()){
|
||||
state.rules.objectives.update();
|
||||
if(state.rules.objectives.checkChanged() && net.server()){
|
||||
|
||||
@@ -165,14 +165,14 @@ public class NetClient implements ApplicationListener{
|
||||
public static void sound(Sound sound, float volume, float pitch, float pan){
|
||||
if(sound == null || headless) return;
|
||||
|
||||
sound.play(Mathf.clamp(volume, 0, 8f) * Core.settings.getInt("sfxvol") / 100f, pitch, pan, false, false);
|
||||
sound.play(Mathf.clamp(volume, 0, 8f) * Core.settings.getInt("sfxvol") / 100f, Mathf.clamp(pitch, 0f, 20f), pan, false, false);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true, called = Loc.server)
|
||||
public static void soundAt(Sound sound, float x, float y, float volume, float pitch){
|
||||
if(sound == null || headless) return;
|
||||
|
||||
sound.at(x, y, pitch, Mathf.clamp(volume, 0, 4f));
|
||||
sound.at(x, y, Mathf.clamp(pitch, 0f, 20f), Mathf.clamp(volume, 0, 4f));
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true)
|
||||
@@ -233,6 +233,8 @@ public class NetClient implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
if(message == null) return;
|
||||
|
||||
if(message.length() > maxTextLength){
|
||||
throw new ValidateException(player, "Player has sent a message above the text limit.");
|
||||
}
|
||||
@@ -340,19 +342,15 @@ public class NetClient implements ApplicationListener{
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setObjectives(MapObjectives executor){
|
||||
//clear old markers
|
||||
for(var objective : state.rules.objectives){
|
||||
for(var marker : objective.markers){
|
||||
if(marker.wasAdded){
|
||||
marker.removed();
|
||||
marker.wasAdded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.rules.objectives = executor;
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void objectiveCompleted(String[] flagsRemoved, String[] flagsAdded){
|
||||
state.rules.objectiveFlags.removeAll(flagsRemoved);
|
||||
state.rules.objectiveFlags.addAll(flagsAdded);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void worldDataBegin(){
|
||||
Groups.clear();
|
||||
@@ -406,7 +404,7 @@ public class NetClient implements ApplicationListener{
|
||||
|
||||
//entity must not be added yet, so create it
|
||||
if(entity == null){
|
||||
entity = (Syncc)EntityMapping.map(typeID).get();
|
||||
entity = (Syncc)EntityMapping.map(typeID & 0xFF).get();
|
||||
entity.id(id);
|
||||
if(!netClient.isEntityUsed(entity.id())){
|
||||
add = true;
|
||||
|
||||
@@ -98,6 +98,15 @@ public class NetServer implements ApplicationListener{
|
||||
private boolean closing = false, pvpAutoPaused = true;
|
||||
private Interval timer = new Interval(10);
|
||||
private IntSet buildHealthChanged = new IntSet();
|
||||
|
||||
/** Current kick session. */
|
||||
public @Nullable VoteSession currentlyKicking = null;
|
||||
/** Duration of a kick in seconds. */
|
||||
public static int kickDuration = 60 * 60;
|
||||
/** Voting round duration in seconds. */
|
||||
public static float voteDuration = 0.5f * 60;
|
||||
/** Cooldown between votes in seconds. */
|
||||
public static int voteCooldown = 60 * 5;
|
||||
|
||||
private ReusableByteOutStream writeBuffer = new ReusableByteOutStream(127);
|
||||
private Writes outputBuffer = new Writes(new DataOutputStream(writeBuffer));
|
||||
@@ -138,7 +147,7 @@ public class NetServer implements ApplicationListener{
|
||||
|
||||
String uuid = packet.uuid;
|
||||
|
||||
if(admins.isIPBanned(con.address) || admins.isSubnetBanned(con.address)) return;
|
||||
if(admins.isIPBanned(con.address) || admins.isSubnetBanned(con.address) || con.kicked || !con.isConnected()) return;
|
||||
|
||||
if(con.hasBegunConnecting){
|
||||
con.kick(KickReason.idInUse);
|
||||
@@ -340,60 +349,10 @@ public class NetServer implements ApplicationListener{
|
||||
Groups.player.each(Player::admin, a -> a.sendMessage(raw, player, args[0]));
|
||||
});
|
||||
|
||||
//duration of a kick in seconds
|
||||
int kickDuration = 60 * 60;
|
||||
//voting round duration in seconds
|
||||
float voteDuration = 0.5f * 60;
|
||||
//cooldown between votes in seconds
|
||||
int voteCooldown = 60 * 5;
|
||||
|
||||
class VoteSession{
|
||||
Player target;
|
||||
ObjectSet<String> voted = new ObjectSet<>();
|
||||
VoteSession[] map;
|
||||
Timer.Task task;
|
||||
int votes;
|
||||
|
||||
public VoteSession(VoteSession[] map, Player target){
|
||||
this.target = target;
|
||||
this.map = map;
|
||||
this.task = Timer.schedule(() -> {
|
||||
if(!checkPass()){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote failed. Not enough votes to kick[orange] @[lightgray].", target.name));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
}
|
||||
}, voteDuration);
|
||||
}
|
||||
|
||||
void vote(Player player, int d){
|
||||
votes += d;
|
||||
voted.addAll(player.uuid(), admins.getInfo(player.uuid()).lastIP);
|
||||
|
||||
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[lightgray].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
|
||||
player.name, target.name, votes, votesRequired()));
|
||||
|
||||
checkPass();
|
||||
}
|
||||
|
||||
boolean checkPass(){
|
||||
if(votes >= votesRequired()){
|
||||
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] @[orange] will be banned from the server for @ minutes.", target.name, (kickDuration / 60)));
|
||||
Groups.player.each(p -> p.uuid().equals(target.uuid()), p -> p.kick(KickReason.vote, kickDuration * 1000));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//cooldowns per player
|
||||
ObjectMap<String, Timekeeper> cooldowns = new ObjectMap<>();
|
||||
//current kick sessions
|
||||
VoteSession[] currentlyKicking = {null};
|
||||
|
||||
clientCommands.<Player>register("votekick", "[player...]", "Vote to kick a player.", (args, player) -> {
|
||||
clientCommands.<Player>register("votekick", "[player] [reason...]", "Vote to kick a player with a valid reason.", (args, player) -> {
|
||||
if(!Config.enableVotekick.bool()){
|
||||
player.sendMessage("[scarlet]Vote-kick is disabled on this server.");
|
||||
return;
|
||||
@@ -409,7 +368,7 @@ public class NetServer implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0] != null){
|
||||
if(currentlyKicking != null){
|
||||
player.sendMessage("[scarlet]A vote is already in progress.");
|
||||
return;
|
||||
}
|
||||
@@ -422,6 +381,8 @@ public class NetServer implements ApplicationListener{
|
||||
builder.append("[lightgray] ").append(p.name).append("[accent] (#").append(p.id()).append(")\n");
|
||||
});
|
||||
player.sendMessage(builder.toString());
|
||||
}else if(args.length == 1){
|
||||
player.sendMessage("[orange]You need a valid reason to kick the player. Add a reason after the player name.");
|
||||
}else{
|
||||
Player found;
|
||||
if(args[0].length() > 1 && args[0].startsWith("#") && Strings.canParseInt(args[0].substring(1))){
|
||||
@@ -448,10 +409,11 @@ public class NetServer implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
VoteSession session = new VoteSession(currentlyKicking, found);
|
||||
VoteSession session = new VoteSession(found);
|
||||
session.vote(player, 1);
|
||||
Call.sendMessage(Strings.format("[lightgray]Reason:[orange] @[lightgray].", args[1]));
|
||||
vtime.reset();
|
||||
currentlyKicking[0] = session;
|
||||
currentlyKicking = session;
|
||||
}
|
||||
}else{
|
||||
player.sendMessage("[scarlet]No player [orange]'" + args[0] + "'[scarlet] found.");
|
||||
@@ -459,43 +421,50 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
});
|
||||
|
||||
clientCommands.<Player>register("vote", "<y/n>", "Vote to kick the current player.", (arg, player) -> {
|
||||
if(currentlyKicking[0] == null){
|
||||
clientCommands.<Player>register("vote", "<y/n/c>", "Vote to kick the current player. Admin can cancel the voting with 'c'.", (arg, player) -> {
|
||||
if(currentlyKicking == null){
|
||||
player.sendMessage("[scarlet]Nobody is being voted on.");
|
||||
}else{
|
||||
if(player.admin && arg[0].equalsIgnoreCase("c")){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote canceled by admin[orange] @[lightgray].", player.name));
|
||||
currentlyKicking.task.cancel();
|
||||
currentlyKicking = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.isLocal()){
|
||||
player.sendMessage("[scarlet]Local players can't vote. Kick the player yourself instead.");
|
||||
return;
|
||||
}
|
||||
|
||||
//hosts can vote all they want
|
||||
if((currentlyKicking[0].voted.contains(player.uuid()) || currentlyKicking[0].voted.contains(admins.getInfo(player.uuid()).lastIP))){
|
||||
player.sendMessage("[scarlet]You've already voted. Sit down.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0].target == player){
|
||||
player.sendMessage("[scarlet]You can't vote on your own trial.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0].target.team() != player.team()){
|
||||
player.sendMessage("[scarlet]You can't vote for other teams.");
|
||||
return;
|
||||
}
|
||||
|
||||
int sign = switch(arg[0].toLowerCase()){
|
||||
case "y", "yes" -> 1;
|
||||
case "n", "no" -> -1;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
//hosts can vote all they want
|
||||
if((currentlyKicking.voted.get(player.uuid(), 2) == sign || currentlyKicking.voted.get(admins.getInfo(player.uuid()).lastIP, 2) == sign)){
|
||||
player.sendMessage(Strings.format("[scarlet]You've already voted @. Sit down.", arg[0].toLowerCase()));
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking.target == player){
|
||||
player.sendMessage("[scarlet]You can't vote on your own trial.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking.target.team() != player.team()){
|
||||
player.sendMessage("[scarlet]You can't vote for other teams.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(sign == 0){
|
||||
player.sendMessage("[scarlet]Vote either 'y' (yes) or 'n' (no).");
|
||||
return;
|
||||
}
|
||||
|
||||
currentlyKicking[0].vote(player, sign);
|
||||
currentlyKicking.vote(player, sign);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -727,7 +696,6 @@ public class NetServer implements ApplicationListener{
|
||||
vector.limit(maxMove);
|
||||
|
||||
float prevx = unit.x, prevy = unit.y;
|
||||
//unit.set(con.lastPosition);
|
||||
if(!unit.isFlying()){
|
||||
unit.move(vector.x, vector.y);
|
||||
}else{
|
||||
@@ -770,7 +738,7 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, called = Loc.server)
|
||||
public static void adminRequest(Player player, Player other, AdminAction action){
|
||||
public static void adminRequest(Player player, Player other, AdminAction action, Object params){
|
||||
if(!player.admin && !player.isLocal()){
|
||||
warn("ACCESS DENIED: Player @ / @ attempted to perform admin action '@' on '@' without proper security access.",
|
||||
player.plainName(), player.con == null ? "null" : player.con.address, action.name(), other == null ? null : other.plainName());
|
||||
@@ -784,28 +752,37 @@ public class NetServer implements ApplicationListener{
|
||||
|
||||
Events.fire(new EventType.AdminRequestEvent(player, other, action));
|
||||
|
||||
if(action == AdminAction.wave){
|
||||
//no verification is done, so admins can hypothetically spam waves
|
||||
//not a real issue, because server owners may want to do just that
|
||||
logic.skipWave();
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has skipped the wave.", player.plainName(), player.uuid());
|
||||
}else if(action == AdminAction.ban){
|
||||
netServer.admins.banPlayerID(other.con.uuid);
|
||||
netServer.admins.banPlayerIP(other.con.address);
|
||||
other.kick(KickReason.banned);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has banned @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}else if(action == AdminAction.kick){
|
||||
other.kick(KickReason.kick);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has kicked @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}else if(action == AdminAction.trace){
|
||||
PlayerInfo stats = netServer.admins.getInfo(other.uuid());
|
||||
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.con.modclient, other.con.mobile, stats.timesJoined, stats.timesKicked);
|
||||
if(player.con != null){
|
||||
Call.traceInfo(player.con, other, info);
|
||||
}else{
|
||||
NetClient.traceInfo(other, info);
|
||||
switch(action){
|
||||
case wave -> {
|
||||
//no verification is done, so admins can hypothetically spam waves
|
||||
//not a real issue, because server owners may want to do just that
|
||||
logic.skipWave();
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has skipped the wave.", player.plainName(), player.uuid());
|
||||
}
|
||||
case ban -> {
|
||||
netServer.admins.banPlayerID(other.con.uuid);
|
||||
netServer.admins.banPlayerIP(other.con.address);
|
||||
other.kick(KickReason.banned);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has banned @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}
|
||||
case kick -> {
|
||||
other.kick(KickReason.kick);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has kicked @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}
|
||||
case trace -> {
|
||||
PlayerInfo stats = netServer.admins.getInfo(other.uuid());
|
||||
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.con.modclient, other.con.mobile, stats.timesJoined, stats.timesKicked, stats.ips.toArray(String.class), stats.names.toArray(String.class));
|
||||
if(player.con != null){
|
||||
Call.traceInfo(player.con, other, info);
|
||||
}else{
|
||||
NetClient.traceInfo(other, info);
|
||||
}
|
||||
}
|
||||
case switchTeam -> {
|
||||
if(params instanceof Team team){
|
||||
other.team(team);
|
||||
}
|
||||
}
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has requested trace info of @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,7 +945,7 @@ public class NetServer implements ApplicationListener{
|
||||
|
||||
//write all entities now
|
||||
dataStream.writeInt(entity.id()); //write id
|
||||
dataStream.writeByte(entity.classId()); //write type ID
|
||||
dataStream.writeByte(entity.classId() & 0xFF); //write type ID
|
||||
entity.writeSync(Writes.get(dataStream)); //write entity
|
||||
|
||||
sent++;
|
||||
@@ -1101,6 +1078,49 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
public class VoteSession{
|
||||
Player target;
|
||||
ObjectIntMap<String> voted = new ObjectIntMap<>();
|
||||
Timer.Task task;
|
||||
int votes;
|
||||
|
||||
public VoteSession(Player target){
|
||||
this.target = target;
|
||||
this.task = Timer.schedule(() -> {
|
||||
if(!checkPass()){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote failed. Not enough votes to kick[orange] @[lightgray].", target.name));
|
||||
currentlyKicking = null;
|
||||
task.cancel();
|
||||
}
|
||||
}, voteDuration);
|
||||
}
|
||||
|
||||
void vote(Player player, int d){
|
||||
int lastVote = voted.get(player.uuid(), 0) | voted.get(admins.getInfo(player.uuid()).lastIP, 0);
|
||||
votes -= lastVote;
|
||||
|
||||
votes += d;
|
||||
voted.put(player.uuid(), d);
|
||||
voted.put(admins.getInfo(player.uuid()).lastIP, d);
|
||||
|
||||
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[lightgray].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
|
||||
player.name, target.name, votes, votesRequired()));
|
||||
|
||||
checkPass();
|
||||
}
|
||||
|
||||
boolean checkPass(){
|
||||
if(votes >= votesRequired()){
|
||||
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] @[orange] will be banned from the server for @ minutes.", target.name, (kickDuration / 60)));
|
||||
Groups.player.each(p -> p.uuid().equals(target.uuid()), p -> p.kick(KickReason.vote, kickDuration * 1000));
|
||||
currentlyKicking = null;
|
||||
task.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface TeamAssigner{
|
||||
Team assign(Player player, Iterable<Player> players);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import mindustry.type.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import rhino.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
@@ -140,6 +141,67 @@ public interface Platform{
|
||||
* @param title The title of the native dialog
|
||||
*/
|
||||
default void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){
|
||||
if(OS.isLinux && !OS.isAndroid){
|
||||
showZenity(open, title, new String[]{extension}, cons, () -> defaultFileDialog(open, title, extension, cons));
|
||||
}else{
|
||||
defaultFileDialog(open, title, extension, cons);
|
||||
}
|
||||
}
|
||||
|
||||
/** attempt to use the native file picker with zenity, or runs the fallback Runnable if the operation fails */
|
||||
static void showZenity(boolean open, String title, String[] extensions, Cons<Fi> cons, Runnable fallback){
|
||||
Threads.daemon(() -> {
|
||||
try{
|
||||
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
|
||||
|
||||
String last = FileChooser.getLastDirectory().absolutePath();
|
||||
if(!last.endsWith("/")) last += "/";
|
||||
|
||||
//zenity doesn't support filtering by extension
|
||||
Seq<String> args = Seq.with("zenity",
|
||||
"--file-selection",
|
||||
"--title=" + formatted,
|
||||
"--filename=" + last,
|
||||
"--confirm-overwrite",
|
||||
"--file-filter=" + Seq.with(extensions).toString(" ", s -> "*." + s),
|
||||
"--file-filter=All files | *" //allow anything if the user wants
|
||||
);
|
||||
|
||||
if(!open){
|
||||
args.add("--save");
|
||||
}
|
||||
|
||||
String result = OS.exec(args.toArray(String.class));
|
||||
//first line.
|
||||
if(result.length() > 1 && result.contains("\n")){
|
||||
result = result.split("\n")[0];
|
||||
}
|
||||
|
||||
//cancelled selection, ignore result
|
||||
if(result.isEmpty() || result.equals("\n")) return;
|
||||
|
||||
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
|
||||
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
|
||||
|
||||
Fi file = Core.files.absolute(result);
|
||||
Core.app.post(() -> {
|
||||
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent());
|
||||
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extensions[0]));
|
||||
}else{
|
||||
cons.get(file);
|
||||
}
|
||||
});
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
Log.warn("zenity not found, using non-native file dialog. Consider installing `zenity` for native file dialogs.");
|
||||
Core.app.post(fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void defaultFileDialog(boolean open, String title, String extension, Cons<Fi> cons){
|
||||
new FileChooser(title, file -> file.extEquals(extension), open, file -> {
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
|
||||
@@ -161,11 +223,17 @@ public interface Platform{
|
||||
default void showMultiFileChooser(Cons<Fi> cons, String... extensions){
|
||||
if(mobile){
|
||||
showFileChooser(true, extensions[0], cons);
|
||||
}else if(OS.isLinux && !OS.isAndroid){
|
||||
showZenity(true, "@open", extensions, cons, () -> defaultMultiFileChooser(cons, extensions));
|
||||
}else{
|
||||
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
|
||||
defaultMultiFileChooser(cons, extensions);
|
||||
}
|
||||
}
|
||||
|
||||
static void defaultMultiFileChooser(Cons<Fi> cons, String... extensions){
|
||||
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
|
||||
}
|
||||
|
||||
/** Hide the app. Android only. */
|
||||
default void hide(){
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.graphics.g3d.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
@@ -45,7 +46,7 @@ public class Renderer implements ApplicationListener{
|
||||
public @Nullable Bloom bloom;
|
||||
public @Nullable FrameBuffer backgroundBuffer;
|
||||
public FrameBuffer effectBuffer = new FrameBuffer();
|
||||
public boolean animateShields, drawWeather = true, drawStatus, enableEffects, drawDisplays = true;
|
||||
public boolean animateShields, drawWeather = true, drawStatus, enableEffects, drawDisplays = true, drawLight = true, pixelate = false;
|
||||
public float weatherAlpha;
|
||||
/** minZoom = zooming out, maxZoom = zooming in */
|
||||
public float minZoom = 1.5f, maxZoom = 6f;
|
||||
@@ -179,11 +180,15 @@ public class Renderer implements ApplicationListener{
|
||||
drawStatus = settings.getBool("blockstatus");
|
||||
enableEffects = settings.getBool("effects");
|
||||
drawDisplays = !settings.getBool("hidedisplays");
|
||||
drawLight = settings.getBool("drawlight", true);
|
||||
pixelate = Core.settings.getBool("pixelate");
|
||||
|
||||
if(landTime > 0){
|
||||
if(!state.isPaused()){
|
||||
CoreBuild build = landCore == null ? player.bestCore() : landCore;
|
||||
build.updateLandParticles();
|
||||
if(build != null){
|
||||
build.updateLandParticles();
|
||||
}
|
||||
}
|
||||
|
||||
if(!state.isPaused()){
|
||||
@@ -209,6 +214,8 @@ public class Renderer implements ApplicationListener{
|
||||
landTime = 0f;
|
||||
graphics.clear(Color.black);
|
||||
}else{
|
||||
minimap.update();
|
||||
|
||||
if(shakeTime > 0){
|
||||
float intensity = shakeIntensity * (settings.getInt("screenshake", 4) / 4f) * 0.75f;
|
||||
camShakeOffset.setToRandomDirection().scl(Mathf.random(intensity));
|
||||
@@ -221,7 +228,7 @@ public class Renderer implements ApplicationListener{
|
||||
shakeIntensity = 0f;
|
||||
}
|
||||
|
||||
if(pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
pixelator.drawPixelate();
|
||||
}else{
|
||||
draw();
|
||||
@@ -286,6 +293,7 @@ public class Renderer implements ApplicationListener{
|
||||
|
||||
public void draw(){
|
||||
Events.fire(Trigger.preDraw);
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
camera.update();
|
||||
|
||||
@@ -309,8 +317,9 @@ public class Renderer implements ApplicationListener{
|
||||
Draw.sort(true);
|
||||
|
||||
Events.fire(Trigger.draw);
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
if(pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
pixelator.register();
|
||||
}
|
||||
|
||||
@@ -332,7 +341,7 @@ public class Renderer implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
if(state.rules.lighting){
|
||||
if(state.rules.lighting && drawLight){
|
||||
Draw.draw(Layer.light, lights::draw);
|
||||
}
|
||||
|
||||
@@ -342,7 +351,7 @@ public class Renderer implements ApplicationListener{
|
||||
|
||||
if(bloom != null){
|
||||
bloom.resize(graphics.getWidth(), graphics.getHeight());
|
||||
bloom.setBloomIntesity(settings.getInt("bloomintensity", 6) / 4f + 1f);
|
||||
bloom.setBloomIntensity(settings.getInt("bloomintensity", 6) / 4f + 1f);
|
||||
bloom.blurPasses = settings.getInt("bloomblur", 1);
|
||||
Draw.draw(Layer.bullet - 0.02f, bloom::capture);
|
||||
Draw.draw(Layer.effect + 0.02f, bloom::render);
|
||||
@@ -363,6 +372,23 @@ public class Renderer implements ApplicationListener{
|
||||
});
|
||||
}
|
||||
|
||||
//draw objective markers
|
||||
state.rules.objectives.eachRunning(obj -> {
|
||||
for(var marker : obj.markers){
|
||||
if(!marker.minimap){
|
||||
marker.drawWorld();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for(var marker : state.markers){
|
||||
if(!marker.isHidden() && !marker.minimap){
|
||||
marker.drawWorld();
|
||||
}
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
|
||||
Draw.draw(Layer.overlayUI, overlays::drawTop);
|
||||
if(state.rules.fog) Draw.draw(Layer.fogOfWar, fog::drawFog);
|
||||
Draw.draw(Layer.space, this::drawLanding);
|
||||
|
||||
@@ -51,7 +51,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
public AboutDialog about;
|
||||
public GameOverDialog restart;
|
||||
public CustomGameDialog custom;
|
||||
public MapsDialog maps;
|
||||
public EditorMapsDialog maps;
|
||||
public LoadDialog load;
|
||||
public DiscordDialog discord;
|
||||
public JoinDialog join;
|
||||
@@ -71,13 +71,14 @@ public class UI implements ApplicationListener, Loadable{
|
||||
public SchematicsDialog schematics;
|
||||
public ModsDialog mods;
|
||||
public ColorPicker picker;
|
||||
public EffectsDialog effects;
|
||||
public LogicDialog logic;
|
||||
public FullTextDialog fullText;
|
||||
public CampaignCompleteDialog campaignComplete;
|
||||
|
||||
public IntMap<Dialog> followUpMenus;
|
||||
|
||||
public Cursor drillCursor, unloadCursor, targetCursor;
|
||||
public Cursor drillCursor, unloadCursor, targetCursor, repairCursor;
|
||||
|
||||
private @Nullable Element lastAnnouncement;
|
||||
|
||||
@@ -85,6 +86,14 @@ public class UI implements ApplicationListener, Loadable{
|
||||
Fonts.loadFonts();
|
||||
}
|
||||
|
||||
public static void loadColors(){
|
||||
Colors.put("accent", Pal.accent);
|
||||
Colors.put("unlaunched", Color.valueOf("8982ed"));
|
||||
Colors.put("highlight", Pal.accent.cpy().lerp(Color.white, 0.3f));
|
||||
Colors.put("stat", Pal.stat);
|
||||
Colors.put("negstat", Pal.negativeStat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
|
||||
@@ -92,6 +101,8 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
@Override
|
||||
public void loadSync(){
|
||||
loadColors();
|
||||
|
||||
Fonts.outline.getData().markupEnabled = true;
|
||||
Fonts.def.getData().markupEnabled = true;
|
||||
Fonts.def.setOwnsTexture(false);
|
||||
@@ -117,6 +128,9 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
Tooltips.getInstance().animations = false;
|
||||
Tooltips.getInstance().textProvider = text -> new Tooltip(t -> t.background(Styles.black6).margin(4f).add(text));
|
||||
if(mobile){
|
||||
Tooltips.getInstance().offsetY += Scl.scl(60f);
|
||||
}
|
||||
|
||||
Core.settings.setErrorHandler(e -> {
|
||||
Log.err(e);
|
||||
@@ -125,15 +139,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
ClickListener.clicked = () -> Sounds.press.play();
|
||||
|
||||
Colors.put("accent", Pal.accent);
|
||||
Colors.put("unlaunched", Color.valueOf("8982ed"));
|
||||
Colors.put("highlight", Pal.accent.cpy().lerp(Color.white, 0.3f));
|
||||
Colors.put("stat", Pal.stat);
|
||||
Colors.put("negstat", Pal.negativeStat);
|
||||
|
||||
drillCursor = Core.graphics.newCursor("drill", Fonts.cursorScale());
|
||||
unloadCursor = Core.graphics.newCursor("unload", Fonts.cursorScale());
|
||||
targetCursor = Core.graphics.newCursor("target", Fonts.cursorScale());
|
||||
repairCursor = Core.graphics.newCursor("repair", Fonts.cursorScale());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,6 +188,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
consolefrag = new ConsoleFragment();
|
||||
|
||||
picker = new ColorPicker();
|
||||
effects = new EffectsDialog();
|
||||
editor = new MapEditorDialog();
|
||||
controls = new KeybindDialog();
|
||||
restart = new GameOverDialog();
|
||||
@@ -195,7 +205,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
bans = new BansDialog();
|
||||
admins = new AdminsDialog();
|
||||
traces = new TraceDialog();
|
||||
maps = new MapsDialog();
|
||||
maps = new EditorMapsDialog();
|
||||
content = new ContentInfoDialog();
|
||||
planet = new PlanetDialog();
|
||||
research = new ResearchDialog();
|
||||
@@ -264,15 +274,24 @@ public class UI implements ApplicationListener, Loadable{
|
||||
});
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, boolean numbers, Cons<String> confirmed, Runnable closed){
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, boolean numbers, Cons<String> confirmed, Runnable closed) {
|
||||
showTextInput(titleText, text, textLength, def, numbers, false, confirmed, closed);
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, boolean numbers, boolean allowEmpty, Cons<String> confirmed, Runnable closed){
|
||||
if(mobile){
|
||||
var description = text;
|
||||
var empty = allowEmpty;
|
||||
Core.input.getTextInput(new TextInput(){{
|
||||
this.title = (titleText.startsWith("@") ? Core.bundle.get(titleText.substring(1)) : titleText);
|
||||
this.text = def;
|
||||
this.numeric = numbers;
|
||||
this.maxLength = textLength;
|
||||
this.accepted = confirmed;
|
||||
this.allowEmpty = false;
|
||||
this.canceled = closed;
|
||||
this.allowEmpty = empty;
|
||||
this.message = description;
|
||||
}});
|
||||
}else{
|
||||
new Dialog(titleText){{
|
||||
@@ -289,11 +308,11 @@ public class UI implements ApplicationListener, Loadable{
|
||||
buttons.button("@ok", () -> {
|
||||
confirmed.get(field.getText());
|
||||
hide();
|
||||
}).disabled(b -> field.getText().isEmpty());
|
||||
}).disabled(b -> !allowEmpty && field.getText().isEmpty());
|
||||
|
||||
keyDown(KeyCode.enter, () -> {
|
||||
String text = field.getText();
|
||||
if(!text.isEmpty()){
|
||||
if(allowEmpty || !text.isEmpty()){
|
||||
confirmed.get(text);
|
||||
hide();
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ 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);
|
||||
@@ -365,8 +366,8 @@ public class World{
|
||||
|
||||
if(!headless){
|
||||
if(state.teams.cores(checkRules.defaultTeam).size == 0 && !checkRules.pvp){
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn", checkRules.defaultTeam.color, checkRules.defaultTeam.localized()));
|
||||
invalidMap = true;
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn", checkRules.defaultTeam.coloredName()));
|
||||
}else if(checkRules.pvp){ //pvp maps need two cores to be valid
|
||||
if(state.teams.getActive().count(TeamData::hasCore) < 2){
|
||||
invalidMap = true;
|
||||
@@ -375,7 +376,7 @@ public class World{
|
||||
}else if(checkRules.attackMode){ //attack maps need two cores to be valid
|
||||
invalidMap = state.rules.waveTeam.data().noCores();
|
||||
if(invalidMap){
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn.attack", checkRules.waveTeam.color, checkRules.waveTeam.localized()));
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn.attack", checkRules.waveTeam.coloredName()));
|
||||
}
|
||||
}
|
||||
}else{
|
||||
|
||||
@@ -44,6 +44,11 @@ public abstract class Content implements Comparable<Content>{
|
||||
return minfo.mod == null;
|
||||
}
|
||||
|
||||
/** @return whether this content is from a mod. */
|
||||
public boolean isModded(){
|
||||
return !isVanilla();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Content c){
|
||||
return Integer.compare(id, c.id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
@@ -22,7 +23,9 @@ public enum ContentType{
|
||||
error(null),
|
||||
planet(Planet.class),
|
||||
ammo_UNUSED(null),
|
||||
team(TeamEntry.class);
|
||||
team(TeamEntry.class),
|
||||
unitCommand(UnitCommand.class),
|
||||
unitStance(UnitStance.class);
|
||||
|
||||
public static final ContentType[] all = values();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.g2d.TextureAtlas.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.TechTree.*;
|
||||
@@ -44,6 +45,8 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
public TextureRegion fullIcon;
|
||||
/** 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. */
|
||||
protected boolean unlocked;
|
||||
|
||||
@@ -68,8 +71,12 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon);
|
||||
}
|
||||
|
||||
public int getLogicId(){
|
||||
return logicVars.lookupLogicId(this);
|
||||
}
|
||||
|
||||
public String displayDescription(){
|
||||
return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName());
|
||||
return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName);
|
||||
}
|
||||
|
||||
/** Checks stat initialization state. Call before displaying stats. */
|
||||
|
||||
@@ -56,7 +56,9 @@ public class DrawOperation{
|
||||
void setTile(Tile tile, byte type, short to){
|
||||
editor.load(() -> {
|
||||
if(type == OpType.floor.ordinal()){
|
||||
tile.setFloor((Floor)content.block(to));
|
||||
if(content.block(to) instanceof Floor floor){
|
||||
tile.setFloor(floor);
|
||||
}
|
||||
}else if(type == OpType.block.ordinal()){
|
||||
tile.getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ public enum EditorTool{
|
||||
});
|
||||
}
|
||||
},
|
||||
fill(KeyCode.g, "replaceall", "fillteams"){
|
||||
fill(KeyCode.g, "replaceall", "fillteams", "fillerase"){
|
||||
{
|
||||
edit = true;
|
||||
}
|
||||
@@ -104,13 +104,15 @@ public enum EditorTool{
|
||||
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
|
||||
Tile tile = editor.tile(x, y);
|
||||
|
||||
if(editor.drawBlock.isMultiblock()){
|
||||
if(tile == null) return;
|
||||
|
||||
if(editor.drawBlock.isMultiblock() && (mode == 0 || mode == -1)){
|
||||
//don't fill multiblocks, thanks
|
||||
pencil.touched(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
//mode 0 or 1, fill everything with the floor/tile or replace it
|
||||
//mode 0 or standard, fill everything with the floor/tile or replace it
|
||||
if(mode == 0 || mode == -1){
|
||||
//can't fill parts or multiblocks
|
||||
if(tile.block().isMultiblock()){
|
||||
@@ -147,6 +149,27 @@ public enum EditorTool{
|
||||
if(dest == editor.drawTeam) return;
|
||||
fill(x, y, true, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam));
|
||||
}
|
||||
}else if(mode == 2){ //erase mode
|
||||
Boolf<Tile> tester;
|
||||
Cons<Tile> setter;
|
||||
|
||||
if(tile.block() != Blocks.air){
|
||||
Block dest = tile.block();
|
||||
tester = t -> t.block() == dest;
|
||||
setter = t -> t.setBlock(Blocks.air);
|
||||
}else if(tile.overlay() != Blocks.air){
|
||||
Block dest = tile.overlay();
|
||||
tester = t -> t.overlay() == dest;
|
||||
setter = t -> t.setOverlay(Blocks.air);
|
||||
}else{
|
||||
//trying to erase floor (no)
|
||||
tester = null;
|
||||
setter = null;
|
||||
}
|
||||
|
||||
if(setter != null){
|
||||
fill(x, y, false, tester, setter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
@@ -803,7 +804,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
}
|
||||
|
||||
if(i == 0){
|
||||
blockSelection.add("@none.found").color(Color.lightGray).padLeft(54f).padTop(10f);
|
||||
blockSelection.add("@none.found").padLeft(54f).padTop(10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
@@ -17,6 +18,7 @@ public class MapInfoDialog extends BaseDialog{
|
||||
private final MapGenerateDialog generate;
|
||||
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
|
||||
private final MapObjectivesDialog objectives = new MapObjectivesDialog();
|
||||
private final MapLocalesDialog locales = new MapLocalesDialog();
|
||||
|
||||
public MapInfoDialog(){
|
||||
super("@editor.mapinfo");
|
||||
@@ -94,6 +96,19 @@ public class MapInfoDialog extends BaseDialog{
|
||||
});
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.row();
|
||||
|
||||
r.button("@editor.locales", Icon.fileText, style, () -> {
|
||||
try{
|
||||
MapLocales res = JsonIO.read(MapLocales.class, editor.tags.get("locales", "{}"));
|
||||
locales.show(res);
|
||||
}catch(Throwable e){
|
||||
locales.show(new MapLocales());
|
||||
ui.showException(e);
|
||||
}
|
||||
hide();
|
||||
}).marginLeft(10f).width(0f).colspan(2).center().growX();
|
||||
}).colspan(2).center();
|
||||
|
||||
name.change();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
@@ -11,65 +13,60 @@ import mindustry.ui.dialogs.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapLoadDialog extends BaseDialog{
|
||||
private Map selected = null;
|
||||
private @Nullable Map selected = null;
|
||||
|
||||
public MapLoadDialog(Cons<Map> loader){
|
||||
super("@editor.loadmap");
|
||||
|
||||
shown(this::rebuild);
|
||||
hidden(() -> selected = null);
|
||||
onResize(this::rebuild);
|
||||
|
||||
TextButton button = new TextButton("@load");
|
||||
button.setDisabled(() -> selected == null);
|
||||
button.clicked(() -> {
|
||||
buttons.defaults().size(210f, 64f);
|
||||
buttons.button("@cancel", Icon.cancel, this::hide);
|
||||
buttons.button("@load", Icon.ok, () -> {
|
||||
if(selected != null){
|
||||
loader.get(selected);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.button("@cancel", this::hide);
|
||||
buttons.add(button);
|
||||
}).disabled(b -> selected == null);
|
||||
addCloseListener();
|
||||
makeButtonOverlay();
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
cont.clear();
|
||||
if(maps.all().size > 0){
|
||||
selected = maps.all().first();
|
||||
}
|
||||
|
||||
ButtonGroup<TextButton> group = new ButtonGroup<>();
|
||||
|
||||
int maxcol = 3;
|
||||
ButtonGroup<Button> group = new ButtonGroup<>();
|
||||
|
||||
int i = 0;
|
||||
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(250f)), 1);
|
||||
|
||||
Table table = new Table();
|
||||
table.defaults().size(200f, 90f).pad(4f);
|
||||
table.defaults().size(250f, 90f).pad(4f);
|
||||
table.margin(10f);
|
||||
|
||||
ScrollPane pane = new ScrollPane(table);
|
||||
pane.setFadeScrollBars(false);
|
||||
pane.setScrollingDisabledX(true);
|
||||
|
||||
for(Map map : maps.all()){
|
||||
table.button(b -> {
|
||||
b.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).padLeft(5f).size(16 * 4f);
|
||||
b.add(map.name()).wrap().grow().labelAlign(Align.center).padLeft(5f);
|
||||
}, Styles.squareTogglet, () -> selected = map).group(group).checked(b -> selected == map);
|
||||
|
||||
TextButton button = new TextButton(map.name(), Styles.flatTogglet);
|
||||
button.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).padLeft(5f).size(16 * 4f);
|
||||
button.getCells().reverse();
|
||||
button.clicked(() -> selected = map);
|
||||
button.getLabelCell().grow().left().padLeft(5f);
|
||||
group.add(button);
|
||||
table.add(button);
|
||||
if(++i % maxcol == 0) table.row();
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
|
||||
group.uncheckAll();
|
||||
|
||||
if(maps.all().isEmpty()){
|
||||
table.add("@maps.none").center();
|
||||
}else{
|
||||
cont.add("@editor.selectmap");
|
||||
}
|
||||
|
||||
cont.row();
|
||||
cont.add(pane).growX();
|
||||
cont.add(pane);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
767
core/src/mindustry/editor/MapLocalesDialog.java
Normal file
767
core/src/mindustry/editor/MapLocalesDialog.java
Normal file
@@ -0,0 +1,767 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.Core;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.Button.*;
|
||||
import arc.scene.ui.TextButton.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.scene.utils.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapLocalesDialog extends BaseDialog{
|
||||
/** Width of UI property card. */
|
||||
private static final float cardWidth = 400f;
|
||||
/** Style for filter options buttons */
|
||||
private static final TextButtonStyle filterStyle = new TextButtonStyle(){{
|
||||
up = down = checked = over = Tex.whitePane;
|
||||
font = Fonts.outline;
|
||||
fontColor = Color.lightGray;
|
||||
overFontColor = Pal.accent;
|
||||
disabledFontColor = Color.gray;
|
||||
disabled = Styles.black;
|
||||
}};
|
||||
/** Icons for use in map locales dialog. */
|
||||
private static final ContentType[] contentIcons = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit};
|
||||
|
||||
private MapLocales locales;
|
||||
private MapLocales lastSaved;
|
||||
private boolean saved = true;
|
||||
private Table langs;
|
||||
private Table main;
|
||||
private Table propView;
|
||||
private String selectedLocale;
|
||||
|
||||
private boolean applytoall = true;
|
||||
private boolean collapsed = false;
|
||||
private String searchString = "";
|
||||
private boolean searchByValue = false;
|
||||
private boolean showCorrect = true;
|
||||
private boolean showMissing = true;
|
||||
private boolean showSame = true;
|
||||
|
||||
public MapLocalesDialog(){
|
||||
super("@editor.locales");
|
||||
|
||||
selectedLocale = MapLocales.currentLocale();
|
||||
|
||||
langs = new Table(Tex.button);
|
||||
main = new Table();
|
||||
propView = new Table();
|
||||
|
||||
buttons.add("").uniform();
|
||||
|
||||
buttons.table(t -> {
|
||||
t.defaults().pad(3).center();
|
||||
|
||||
t.button("@back", Icon.left, () -> {
|
||||
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
});
|
||||
hide();
|
||||
}).size(210f, 64f);
|
||||
closeOnBack(() -> {
|
||||
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
});
|
||||
});
|
||||
|
||||
t.button("@editor.apply", Icon.ok, () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
lastSaved = locales.copy();
|
||||
saved = true;
|
||||
}).size(210f, 64f).disabled(b -> saved);
|
||||
|
||||
t.button("@edit", Icon.edit, this::editDialog).size(210f, 64f);
|
||||
}).growX();
|
||||
|
||||
buttons.button("?", () -> ui.showInfo("@locales.info")).size(60f, 64f).uniform();
|
||||
|
||||
shown(this::setup);
|
||||
}
|
||||
|
||||
public void show(MapLocales locales){
|
||||
this.locales = locales;
|
||||
lastSaved = locales.copy();
|
||||
saved = true;
|
||||
show();
|
||||
}
|
||||
|
||||
private void setup(){
|
||||
cont.clear();
|
||||
|
||||
buildTables();
|
||||
|
||||
cont.add(langs).left();
|
||||
|
||||
cont.table(t -> {
|
||||
// search/collapse all/filter
|
||||
t.table(a -> {
|
||||
a.button(Icon.downOpen, Styles.emptyTogglei, () -> {
|
||||
collapsed = !collapsed;
|
||||
buildMain();
|
||||
}).update(b -> {
|
||||
b.replaceImage(new Image(collapsed ? Icon.upOpen : Icon.downOpen));
|
||||
b.setChecked(collapsed);
|
||||
}).size(35f);
|
||||
|
||||
a.button(Icon.filter, Styles.emptyi, () -> filterDialog(this::buildMain)).padLeft(10f).size(35f);
|
||||
|
||||
var field = a.field("", v -> {
|
||||
searchString = v;
|
||||
buildMain();
|
||||
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue": "@locales.searchname")).get();
|
||||
|
||||
a.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
searchString = "";
|
||||
field.setText("");
|
||||
buildMain();
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
t.check("@locales.applytoall", applytoall, b -> applytoall = b).pad(10f).row();
|
||||
|
||||
t.add(main).center().grow().row();
|
||||
}).pad(10f).grow();
|
||||
|
||||
// property addition
|
||||
cont.table(Tex.button, t -> {
|
||||
TextField name = t.field("name", s -> {}).maxTextLength(64).fillX().padTop(10f).get();
|
||||
t.row();
|
||||
TextField value = t.area("text", s -> {}).maxTextLength(1000).fillX().height(140f).get();
|
||||
t.row();
|
||||
|
||||
t.button("@add", Icon.add, () -> {
|
||||
if(applytoall){
|
||||
for(var locale : locales.values()){
|
||||
locale.put(name.getText(), value.getText());
|
||||
}
|
||||
}else{
|
||||
locales.get(selectedLocale).put(name.getText(), value.getText());
|
||||
}
|
||||
|
||||
saved = false;
|
||||
buildMain();
|
||||
}).padTop(10f).size(400f, 50f).fillX().row();
|
||||
}).right();
|
||||
}
|
||||
|
||||
private void buildTables(){
|
||||
if(!locales.containsKey(selectedLocale)){
|
||||
locales.put(selectedLocale, new StringMap());
|
||||
}
|
||||
|
||||
buildLocalesTable();
|
||||
buildMain();
|
||||
}
|
||||
|
||||
private void buildLocalesTable(){
|
||||
langs.clear();
|
||||
|
||||
langs.pane(p -> {
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
|
||||
if(locales.containsKey(name)){
|
||||
p.button(loc.getDisplayName(Core.bundle.getLocale()), Styles.flatTogglet, () -> {
|
||||
if(name.equals(selectedLocale)) return;
|
||||
|
||||
selectedLocale = name;
|
||||
buildTables();
|
||||
}).update(b -> b.setChecked(selectedLocale.equals(name))).size(300f, 50f);
|
||||
p.button(Icon.edit, Styles.flati, () -> localeEditDialog(name)).size(50f);
|
||||
p.button(Icon.trash, Styles.flati, () -> ui.showConfirm("@confirm", "@locales.deletelocale", () -> {
|
||||
locales.remove(name);
|
||||
|
||||
selectedLocale = (locales.size != 0 ? locales.keys().next() : Core.settings.getString("locale"));
|
||||
saved = false;
|
||||
buildTables();
|
||||
})).size(50f).row();
|
||||
}
|
||||
}
|
||||
}).row();
|
||||
langs.button("@add", Icon.add, this::addLocaleDialog).padTop(10f).width(400f);
|
||||
}
|
||||
|
||||
private void buildMain(){
|
||||
main.clear();
|
||||
|
||||
StringMap props = locales.get(selectedLocale);
|
||||
|
||||
main.image().color(Pal.gray).height(3f).growX().expandY().top().row();
|
||||
main.pane(p -> {
|
||||
int cols = Math.max(1, (Core.graphics.getWidth() - 630) / ((int)cardWidth + 10));
|
||||
if(props.size == 0){
|
||||
main.add("@empty").center().row();
|
||||
return;
|
||||
}
|
||||
p.defaults().top();
|
||||
|
||||
Table[] colTables = new Table[cols];
|
||||
for(var i = 0; i < cols; i++){
|
||||
colTables[i] = new Table();
|
||||
}
|
||||
int i = 0;
|
||||
|
||||
// To sort properties in alphabetic order
|
||||
Seq<String> keys = props.keys().toSeq().sort();
|
||||
|
||||
for(var key : keys){
|
||||
var comparsionString = (searchByValue ? props.get(key).toLowerCase() : key.toLowerCase());
|
||||
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
|
||||
|
||||
PropertyStatus status = getPropertyStatus(key, props.get(key), selectedLocale, false);
|
||||
if(status == PropertyStatus.correct && !showCorrect) continue;
|
||||
if(status == PropertyStatus.missing && !showMissing) continue;
|
||||
if(status == PropertyStatus.same && !showSame) continue;
|
||||
|
||||
colTables[i].table(Tex.whitePane, t -> {
|
||||
boolean[] shown = {!collapsed};
|
||||
String[] propKey = {key};
|
||||
String[] propValue = {props.get(key)};
|
||||
|
||||
// collapse button
|
||||
t.button(Icon.downOpen, Styles.emptyTogglei, () -> shown[0] = !shown[0]).update(b -> {
|
||||
b.replaceImage(new Image(shown[0] ? Icon.upOpen : Icon.downOpen));
|
||||
b.setChecked(shown[0]);
|
||||
}).size(35f);
|
||||
|
||||
// property name field
|
||||
t.field(propKey[0], (f, c) -> c != '=' && c != ':', v -> {
|
||||
if(props.containsKey(v)){
|
||||
t.setColor(Color.valueOf("f25555"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(applytoall){
|
||||
for(var bundle : locales.values()){
|
||||
if(!bundle.containsKey(v)){
|
||||
String value = bundle.get(propKey[0]);
|
||||
if(value == null) continue;
|
||||
|
||||
bundle.remove(propKey[0]);
|
||||
bundle.put(v, value);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(!props.containsKey(v)){
|
||||
props.remove(propKey[0]);
|
||||
props.put(v, propValue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
propKey[0] = v;
|
||||
updateCard(t, v, propValue[0]);
|
||||
saved = false;
|
||||
}).maxTextLength(64).width(cardWidth - 125f);
|
||||
|
||||
// remove button
|
||||
t.button(Icon.trash, Styles.emptyi, () -> {
|
||||
if(applytoall){
|
||||
for(var bundle : locales.values()){
|
||||
bundle.remove(propKey[0]);
|
||||
}
|
||||
}else{
|
||||
props.remove(propKey[0]);
|
||||
}
|
||||
saved = false;
|
||||
buildMain();
|
||||
}).size(35f);
|
||||
|
||||
// more actions
|
||||
t.button(Icon.edit, Styles.emptyi, () -> propEditDialog(t, propKey[0], propValue[0])).size(35f).row();
|
||||
|
||||
// property value area
|
||||
t.collapser(c -> c.area(propValue[0], v -> {
|
||||
props.put(propKey[0], v);
|
||||
updateCard(t, propKey[0], v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).update(a -> {
|
||||
propValue[0] = props.get(propKey[0]);
|
||||
a.setText(props.get(propKey[0]));
|
||||
}).growX(), () -> shown[0]).colspan(4).growX();
|
||||
|
||||
updateCard(t, propKey[0], propValue[0]);
|
||||
}).top().width(cardWidth).pad(5f).row();
|
||||
|
||||
i = ++i % cols;
|
||||
}
|
||||
|
||||
if(!colTables[0].hasChildren()){
|
||||
main.add("@empty").center().row();
|
||||
}else{
|
||||
p.add(colTables);
|
||||
}
|
||||
}).growX().row();
|
||||
main.image().color(Pal.gray).height(3f).growX().expandY().bottom().row();
|
||||
}
|
||||
|
||||
private void updateCard(Table table, String propKey, String propValue){
|
||||
updateCard(table, propKey, propValue, selectedLocale, false);
|
||||
}
|
||||
|
||||
private void updateCard(Table table, String propKey, String propValue, String locale, boolean viewCard){
|
||||
switch(getPropertyStatus(propKey, propValue, locale, viewCard)){
|
||||
case missing -> table.setColor(Pal.accent);
|
||||
case same -> table.setColor(Pal.techBlue);
|
||||
case correct -> table.setColor(Pal.gray);
|
||||
}
|
||||
}
|
||||
|
||||
// Property statuses for main dialog and property view dialog are a bit different
|
||||
private PropertyStatus getPropertyStatus(String propKey, String propValue, String locale, boolean forView){
|
||||
if(forView && propValue == null) return PropertyStatus.missing;
|
||||
|
||||
for(var bundle : locales.entries()){
|
||||
if(!forView && bundle.key.equals(selectedLocale)) continue;
|
||||
if(forView && bundle.key.equals(locale)) continue;
|
||||
|
||||
StringMap props = bundle.value;
|
||||
|
||||
if(!props.containsKey(propKey)){
|
||||
if(!forView) return PropertyStatus.missing;
|
||||
}else{
|
||||
if(props.get(propKey).equals(propValue)){
|
||||
return PropertyStatus.same;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PropertyStatus.correct;
|
||||
}
|
||||
|
||||
private void addLocaleDialog(){
|
||||
BaseDialog dialog = new BaseDialog("@add");
|
||||
|
||||
dialog.cont.pane(t -> {
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
|
||||
if(!locales.containsKey(name)){
|
||||
t.button(loc.getDisplayName(Core.bundle.getLocale()), Styles.flatTogglet, () -> {
|
||||
if(name.equals(selectedLocale)) return;
|
||||
|
||||
locales.put(name, new StringMap());
|
||||
|
||||
selectedLocale = name;
|
||||
saved = false;
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).update(b -> b.setChecked(selectedLocale.equals(name))).size(400f, 50f).row();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void propEditDialog(Table card, String key, String value){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(450f, 60f).left();
|
||||
|
||||
t.button("@locales.addtoother", Icon.add, Styles.flatt, () -> {
|
||||
for(var bundle : locales.values()){
|
||||
if(!bundle.containsKey(key)){
|
||||
bundle.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
saved = false;
|
||||
updateCard(card, key, value);
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.viewproperty", Icon.zoom, Styles.flatt, () -> {
|
||||
viewPropertyDialog(key);
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.addicon", Icon.image, Styles.flatt, () -> {
|
||||
addIconDialog(res -> {
|
||||
locales.get(selectedLocale).put(key, value + res);
|
||||
saved = false;
|
||||
});
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
|
||||
locales.get(selectedLocale).put(key, lastSaved.get(selectedLocale).get(key));
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).disabled(b -> {
|
||||
if(!lastSaved.containsKey(selectedLocale)) return true;
|
||||
StringMap savedMap = lastSaved.get(selectedLocale);
|
||||
return !savedMap.containsKey(key) || savedMap.get(key).equals(locales.get(selectedLocale).get(key));
|
||||
}).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void localeEditDialog(String locale){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(350f, 60f).left();
|
||||
|
||||
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
|
||||
Core.app.setClipboardText(writeLocale(locale));
|
||||
ui.showInfoFade("@copied");
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
|
||||
locales.put(locale, readLocale(Core.app.getClipboardText()));
|
||||
buildTables();
|
||||
saved = false;
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void editDialog(){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(450f, 60f).left();
|
||||
|
||||
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
|
||||
Core.app.setClipboardText(writeBundles());
|
||||
ui.showInfoFade("@copied");
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
|
||||
locales = readBundles(Core.app.getClipboardText());
|
||||
buildTables();
|
||||
saved = false;
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
|
||||
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
|
||||
locales = lastSaved.copy();
|
||||
saved = true;
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).disabled(b -> saved).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void viewPropertyDialog(String key){
|
||||
BaseDialog dialog = new BaseDialog(Core.bundle.format("locales.viewing", key));
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.button(Icon.filter, Styles.emptyi, () -> filterDialog(() -> buildPropView(key))).size(35f);
|
||||
|
||||
var field = t.field(searchString, v -> {
|
||||
searchString = v;
|
||||
buildPropView(key);
|
||||
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue" : "@locales.searchlocale")).get();
|
||||
|
||||
t.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
searchString = "";
|
||||
field.setText("");
|
||||
buildPropView(key);
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
buildPropView(key);
|
||||
dialog.cont.add(propView).grow().center().row();
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.closeOnBack();
|
||||
dialog.hidden(this::buildMain);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void buildPropView(String key){
|
||||
propView.clear();
|
||||
|
||||
propView.image().color(Pal.gray).height(3f).fillX().top().row();
|
||||
propView.pane(p -> {
|
||||
int cols = (Core.graphics.getWidth() - 100) / ((int)cardWidth + 10);
|
||||
if(cols == 0){
|
||||
propView.add("@empty").center().row();
|
||||
return;
|
||||
}
|
||||
p.defaults().top();
|
||||
|
||||
Table[] colTables = new Table[cols];
|
||||
for(var i = 0; i < cols; i++){
|
||||
colTables[i] = new Table();
|
||||
}
|
||||
int i = 0;
|
||||
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
if(!locales.containsKey(name)) continue;
|
||||
|
||||
PropertyStatus status = getPropertyStatus(key, locales.get(name).get(key), name, true);
|
||||
if(status == PropertyStatus.correct && !showCorrect) continue;
|
||||
if(status == PropertyStatus.missing && !showMissing) continue;
|
||||
if(status == PropertyStatus.same && !showSame) continue;
|
||||
|
||||
if(status != PropertyStatus.missing){
|
||||
var comparsionString = (searchByValue ? locales.get(name).get(key).toLowerCase() : loc.getDisplayName(Core.bundle.getLocale()).toLowerCase());
|
||||
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
|
||||
}
|
||||
|
||||
colTables[i].table(Tex.whitePane, t -> {
|
||||
t.add(loc.getDisplayName(Core.bundle.getLocale())).left().color(Pal.accent).row();
|
||||
t.image().color(Pal.accent).fillX().row();
|
||||
|
||||
if(status == PropertyStatus.missing){
|
||||
t.table(b ->
|
||||
b.button("@add", Icon.add, () -> {
|
||||
locales.get(name).put(key, "moai");
|
||||
|
||||
t.getCells().get(2).clearElement();
|
||||
t.getCells().remove(2);
|
||||
|
||||
t.area(locales.get(name).get(key), v -> {
|
||||
locales.get(name).put(key, v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).growX().row();
|
||||
}).size(160f, 50f)).height(140f).growX().row();
|
||||
}else{
|
||||
t.area(locales.get(name).get(key), v -> {
|
||||
locales.get(name).put(key, v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).growX().row();
|
||||
}
|
||||
}).update(t -> updateCard(t, key, locales.get(name).get(key), name, true)).top().width(cardWidth).pad(5f).row();
|
||||
|
||||
i = ++i % cols;
|
||||
}
|
||||
|
||||
if(!colTables[0].hasChildren()){
|
||||
propView.add("@empty").center().row();
|
||||
}else{
|
||||
p.add(colTables);
|
||||
}
|
||||
}).grow().row();
|
||||
propView.image().color(Pal.gray).height(3f).fillX().bottom().row();
|
||||
}
|
||||
|
||||
private void filterDialog(Runnable hidden){
|
||||
BaseDialog dialog = new BaseDialog("@locales.filter");
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.add("@search").row();
|
||||
t.table(b -> {
|
||||
b.button("@locales.byname", Styles.togglet, () -> searchByValue = false).size(300f, 50f).checked(v -> !searchByValue);
|
||||
b.button("@locales.byvalue", Styles.togglet, () -> searchByValue = true).padLeft(10f).size(300f, 50f).checked(v -> searchByValue);
|
||||
}).padTop(5f);
|
||||
}).row();
|
||||
|
||||
dialog.cont.button("@locales.showcorrect", Icon.ok, filterStyle, () -> showCorrect = !showCorrect).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showCorrect ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showCorrect);
|
||||
}).size(450f, 100f).color(Pal.gray).padTop(65f);
|
||||
|
||||
dialog.cont.row();
|
||||
|
||||
dialog.cont.button("@locales.showmissing", Icon.ok, filterStyle, () -> showMissing = !showMissing).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showMissing ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showMissing);
|
||||
}).size(450f, 100f).color(Pal.accent).padTop(65f);
|
||||
|
||||
dialog.cont.row();
|
||||
|
||||
dialog.cont.button("@locales.showsame", Icon.ok, filterStyle, () -> showSame = !showSame).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showSame ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showSame);
|
||||
}).size(450f, 100f).color(Pal.techBlue).padTop(65f);
|
||||
|
||||
dialog.buttons.button("@back", Icon.left, () -> {
|
||||
hidden.run();
|
||||
dialog.hide();
|
||||
}).size(210f, 64f);
|
||||
dialog.closeOnBack(hidden);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void addIconDialog(Cons<String> cons){
|
||||
BaseDialog dialog = new BaseDialog("@locales.addicon");
|
||||
|
||||
Table icons = new Table();
|
||||
TextField search = Elem.newField("", v -> iconsTable(icons, v.replace(" ", "").toLowerCase(), dialog, cons));
|
||||
search.setMessageText("@search");
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.add(search).maxTextLength(64).padLeft(10f).width(250f);
|
||||
|
||||
t.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
search.setText("");
|
||||
iconsTable(icons, "", dialog, cons);
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
dialog.cont.pane(icons).scrollX(false);
|
||||
dialog.resized(true, () -> iconsTable(icons, search.getText().replace(" ", "").toLowerCase(), dialog, cons));
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.closeOnBack();
|
||||
dialog.setFillParent(true);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void iconsTable(Table table, String search, Dialog dialog, Cons<String> cons){
|
||||
table.clear();
|
||||
|
||||
table.marginRight(19f).marginLeft(12f);
|
||||
table.defaults().size(48f);
|
||||
|
||||
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
|
||||
|
||||
int i = 0;
|
||||
|
||||
var codes = new ObjectIntMap<>(Iconc.codes);
|
||||
|
||||
for(var name : codes.keys()){
|
||||
if(!name.toLowerCase().contains(search)) codes.remove(name);
|
||||
}
|
||||
|
||||
if(codes.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
for(var icon : codes){
|
||||
String res = (char)icon.value + "";
|
||||
|
||||
table.button(Icon.icons.get(icon.key), Styles.flati, iconMed, () -> {
|
||||
cons.get(res);
|
||||
dialog.hide();
|
||||
}).tooltip(icon.key);
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
|
||||
for(ContentType ctype : contentIcons){
|
||||
var all = content.getBy(ctype).<UnlockableContent>as().select(u -> u.localizedName.replace(" ", "").toLowerCase().contains(search) && u.uiIcon.found());
|
||||
|
||||
table.row();
|
||||
if(all.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
i = 0;
|
||||
for(UnlockableContent u : all){
|
||||
table.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
|
||||
cons.get(u.emoji() + "");
|
||||
dialog.hide();
|
||||
}).tooltip(u.localizedName);
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
}
|
||||
|
||||
var teams = new Seq<>(Team.baseTeams);
|
||||
teams = teams.select(u -> u.localized().toLowerCase().contains(search) && Core.atlas.has("team-" + u.name));
|
||||
|
||||
table.row();
|
||||
if(teams.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
for(Team team : teams){
|
||||
var region = Core.atlas.find("team-" + team.name);
|
||||
|
||||
table.button(new TextureRegionDrawable(region), Styles.flati, iconMed, () -> {
|
||||
cons.get(team.emoji);
|
||||
dialog.hide();
|
||||
}).tooltip(team.localized());
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
}
|
||||
|
||||
private String writeBundles(){
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
for(var locale : locales.keys()){
|
||||
data.append(locale).append(":\n").append(writeLocale(locale));
|
||||
}
|
||||
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
private String writeLocale(String key){
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
if(!locales.containsKey(key)) return "";
|
||||
|
||||
for(var prop : locales.get(key).entries()){
|
||||
data.append(prop.key).append(" = ").append(prop.value).append("\n");
|
||||
}
|
||||
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
private MapLocales readBundles(String data){
|
||||
MapLocales bundles = new MapLocales();
|
||||
|
||||
String currentLocale = "";
|
||||
|
||||
for(var line : data.split("\\r?\\n|\\r")){
|
||||
if(line.endsWith(":") && !line.contains("=")){
|
||||
currentLocale = line.substring(0, line.length() - 1);
|
||||
bundles.put(currentLocale, new StringMap());
|
||||
}else{
|
||||
int sepIndex = line.indexOf(" = ");
|
||||
if(sepIndex != -1 && !currentLocale.isEmpty()){
|
||||
bundles.get(currentLocale).put(line.substring(0, sepIndex), line.substring(sepIndex + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
private StringMap readLocale(String data){
|
||||
StringMap map = new StringMap();
|
||||
|
||||
for(var line : data.split("\\r?\\n|\\r")){
|
||||
int sepIndex = line.indexOf(" = ");
|
||||
if(sepIndex != -1){
|
||||
map.put(line.substring(0, sepIndex), line.substring(sepIndex + 3));
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private enum PropertyStatus{
|
||||
correct,
|
||||
missing,
|
||||
same
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,8 @@ public class MapObjectivesCanvas extends WidgetGroup{
|
||||
public void clearObjectives(){
|
||||
stopQuery();
|
||||
tilemap.clearTiles();
|
||||
tilemap.x = 0f;
|
||||
tilemap.y = 0f;
|
||||
}
|
||||
|
||||
protected void stopQuery(){
|
||||
|
||||
@@ -98,7 +98,7 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
setInterpreter(UnlockableContent.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
b -> b.image().size(iconSmall).scaling(Scaling.fit).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
() -> showContentSelect(null, set, b -> (field != null && !field.isAnnotationPresent(Researchable.class)) || b.techNode != null)
|
||||
).fill().pad(4)).growX().fillY();
|
||||
});
|
||||
@@ -505,7 +505,7 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
content.getBy(type).<UnlockableContent>as()
|
||||
)){
|
||||
if(content.isHidden() || !check.get((T)content)) continue;
|
||||
t.image(content == Blocks.air ? Icon.none.getRegion() : content.uiIcon).size(iconMed).pad(3)
|
||||
t.image(content == Blocks.air ? Icon.none.getRegion() : content.uiIcon).size(iconMed).pad(3).scaling(Scaling.fit)
|
||||
.with(b -> b.addListener(new HandCursorListener()))
|
||||
.tooltip(content.localizedName).get().clicked(() -> {
|
||||
cons.get((T)content);
|
||||
|
||||
@@ -100,6 +100,7 @@ public class MapRenderer implements Disposable{
|
||||
|
||||
private void render(int wx, int wy){
|
||||
int x = wx / chunkSize, y = wy / chunkSize;
|
||||
if(x >= chunks.length || y >= chunks[0].length) return;
|
||||
IndexedRenderer mesh = chunks[x][y];
|
||||
Tile tile = editor.tiles().getn(wx, wy);
|
||||
|
||||
@@ -138,12 +139,16 @@ public class MapRenderer implements Disposable{
|
||||
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
|
||||
}
|
||||
|
||||
float offsetX = -(wall.size / 3) * tilesize, offsetY = -(wall.size / 3) * tilesize;
|
||||
float offsetX = -((wall.size + 1) / 3) * tilesize, offsetY = -((wall.size + 1) / 3) * tilesize;
|
||||
|
||||
//draw non-synthetic wall or ore
|
||||
if((wall.update || wall.destructible) && center){
|
||||
mesh.setColor(team.color);
|
||||
region = Core.atlas.find("block-border-editor");
|
||||
if(wall.size == 2){
|
||||
offsetX += tilesize;
|
||||
offsetY += tilesize;
|
||||
}
|
||||
}else if(!useSyntheticWall && wall != Blocks.air && center){
|
||||
region = getIcon(wall, idxWall);
|
||||
|
||||
|
||||
@@ -184,12 +184,12 @@ public class MapView extends Element implements GestureListener{
|
||||
offsety -= ay * 15 * Time.delta / zoom;
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.shiftLeft)){
|
||||
if(Core.input.keyTap(KeyCode.shiftLeft) || Core.input.keyTap(KeyCode.altLeft)){
|
||||
lastTool = tool;
|
||||
tool = EditorTool.pick;
|
||||
}
|
||||
|
||||
if(Core.input.keyRelease(KeyCode.shiftLeft) && lastTool != null){
|
||||
if((Core.input.keyRelease(KeyCode.shiftLeft) || Core.input.keyRelease(KeyCode.altLeft)) && lastTool != null){
|
||||
tool = lastTool;
|
||||
lastTool = null;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
|
||||
private Table table;
|
||||
private int search = -1;
|
||||
private UnitType lastType = UnitTypes.dagger;
|
||||
private @Nullable UnitType filterType;
|
||||
private Sort sort = Sort.begin;
|
||||
private boolean reverseSort = false;
|
||||
@@ -160,7 +159,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
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.unit")
|
||||
s.button(Icon.units, Styles.emptyi, () -> showUnits(type -> filterType = type, true)).size(46f).tooltip("@waves.filter")
|
||||
.update(b -> b.getStyle().imageUp = filterType != null ? new TextureRegionDrawable(filterType.uiIcon) : Icon.filter);
|
||||
}).growX().pad(6f).row();
|
||||
|
||||
@@ -168,10 +167,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
|
||||
main.table(t -> {
|
||||
t.button("@add", () -> {
|
||||
SpawnGroup newGroup = new SpawnGroup(lastType);
|
||||
groups.add(newGroup);
|
||||
expandedGroup = newGroup;
|
||||
showUnits(type -> newGroup.type = lastType = type, false);
|
||||
showUnits(type -> groups.add(expandedGroup = new SpawnGroup(type)), false);
|
||||
buildGroups();
|
||||
}).growX().height(70f);
|
||||
|
||||
@@ -233,9 +229,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
b.label(() -> (group.begin + 1) + "").color(Color.lightGray).minWidth(45f).labelAlign(Align.left).left();
|
||||
|
||||
b.button(Icon.copySmall, Styles.emptyi, () -> {
|
||||
SpawnGroup copy = group.copy();
|
||||
expandedGroup = copy;
|
||||
groups.insert(groups.indexOf(group) + 1, copy);
|
||||
groups.insert(groups.indexOf(group) + 1, expandedGroup = group.copy());
|
||||
buildGroups();
|
||||
}).pad(-6).size(46f).tooltip("@editor.copy");
|
||||
|
||||
@@ -244,7 +238,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
Icon.logicSmall,
|
||||
Styles.emptyi, () -> showEffects(group)).pad(-6).size(46f).scaling(Scaling.fit).tooltip(group.effect != null ? group.effect.localizedName : "@none");
|
||||
|
||||
b.button(Icon.unitsSmall, Styles.emptyi, () -> showUnits(type -> group.type = lastType = type, false)).pad(-6).size(46f).tooltip("@stat.unittype");
|
||||
b.button(Icon.unitsSmall, Styles.emptyi, () -> showUnits(type -> group.type = type, false)).pad(-6).size(46f).tooltip("@stat.unittype");
|
||||
b.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
groups.remove(group);
|
||||
if(expandedGroup == group) expandedGroup = null;
|
||||
@@ -253,9 +247,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
buildGroups();
|
||||
}).pad(-6).size(46f).padRight(-12f).tooltip("@waves.remove");
|
||||
b.clicked(KeyCode.mouseMiddle, () -> {
|
||||
SpawnGroup copy = group.copy();
|
||||
groups.insert(groups.indexOf(group) + 1, copy);
|
||||
expandedGroup = copy;
|
||||
groups.insert(groups.indexOf(group) + 1, expandedGroup = group.copy());
|
||||
buildGroups();
|
||||
});
|
||||
}, () -> {
|
||||
@@ -415,8 +407,7 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
}
|
||||
|
||||
void showUnits(Cons<UnitType> cons, boolean reset){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.setFillParent(true);
|
||||
BaseDialog dialog = new BaseDialog(reset ? "@waves.filter" : "");
|
||||
dialog.cont.pane(p -> {
|
||||
p.defaults().pad(2).fillX();
|
||||
if(reset){
|
||||
@@ -451,30 +442,29 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
|
||||
void showEffects(SpawnGroup group){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.setFillParent(true);
|
||||
dialog.cont.pane(p -> {
|
||||
int i = 0;
|
||||
p.defaults().pad(2).fillX();
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
t.image(Icon.none).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add("@settings.resetKey");
|
||||
}, () -> {
|
||||
group.effect = null;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).margin(12f);
|
||||
int i = 1;
|
||||
for(StatusEffect effect : content.statusEffects()){
|
||||
if(effect != StatusEffects.none && (effect.isHidden() || effect.reactive)) continue;
|
||||
|
||||
if(effect.isHidden() || effect.reactive) continue;
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
if(effect.uiIcon != null && effect != StatusEffects.none){
|
||||
t.image(effect.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
}else{
|
||||
t.image(Icon.none).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
}
|
||||
|
||||
if(effect != StatusEffects.none){
|
||||
t.add(effect.localizedName);
|
||||
}else{
|
||||
t.add("@settings.resetKey");
|
||||
}
|
||||
t.image(effect.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add(effect.localizedName);
|
||||
}, () -> {
|
||||
group.effect = effect != StatusEffects.none ? effect : null;
|
||||
group.effect = effect;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).pad(2).margin(12f).fillX();
|
||||
}).margin(12f);
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
}).growX().scrollX(false);
|
||||
|
||||
@@ -2,6 +2,7 @@ package mindustry.entities;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
@@ -38,10 +39,13 @@ public class Damage{
|
||||
private static Unit tmpUnit;
|
||||
|
||||
public static void applySuppression(Team team, float x, float y, float range, float reload, float maxDelay, float applyParticleChance, @Nullable Position source){
|
||||
applySuppression(team, x, y, range, reload, maxDelay, applyParticleChance, source, Pal.sapBullet);
|
||||
}
|
||||
public static void applySuppression(Team team, float x, float y, float range, float reload, float maxDelay, float applyParticleChance, @Nullable Position source, Color effectColor){
|
||||
builds.clear();
|
||||
indexer.eachBlock(null, x, y, range, build -> build.team != team, build -> {
|
||||
float prev = build.healSuppressionTime;
|
||||
build.applyHealSuppression(reload + 1f);
|
||||
build.applyHealSuppression(reload + 1f, effectColor);
|
||||
|
||||
//TODO maybe should be block field instead of instanceof check
|
||||
if(build.wasRecentlyHealed(60f * 12f) || build.block.suppressable){
|
||||
@@ -58,7 +62,7 @@ public class Damage{
|
||||
for(var build : builds){
|
||||
if(Mathf.chance(scaledChance)){
|
||||
Time.run(Mathf.random(maxDelay), () -> {
|
||||
Fx.regenSuppressSeek.at(build.x + Mathf.range(build.block.size * tilesize / 2f), build.y + Mathf.range(build.block.size * tilesize / 2f), 0f, source);
|
||||
Fx.regenSuppressSeek.at(build.x + Mathf.range(build.block.size * tilesize / 2f), build.y + Mathf.range(build.block.size * tilesize / 2f), 0f, effectColor, source);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -94,11 +98,12 @@ public class Damage{
|
||||
}
|
||||
|
||||
int waves = explosiveness <= 2 ? 0 : Mathf.clamp((int)(explosiveness / 11), 1, 25);
|
||||
float damagePerWave = explosiveness / 2f;
|
||||
|
||||
for(int i = 0; i < waves; i++){
|
||||
int f = i;
|
||||
Time.run(i * 2f, () -> {
|
||||
damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f, false);
|
||||
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));
|
||||
});
|
||||
}
|
||||
@@ -137,6 +142,16 @@ public class Damage{
|
||||
return found ? tmpBuilding : null;
|
||||
}
|
||||
|
||||
public static float findLength(Bullet b, float length, boolean laser, int pierceCap){
|
||||
if(pierceCap > 0){
|
||||
length = findPierceLength(b, pierceCap, laser, length);
|
||||
}else if(laser){
|
||||
length = findLaserLength(b, length);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public static float findLaserLength(Bullet b, float length){
|
||||
vec.trnsExact(b.rotation(), length);
|
||||
|
||||
@@ -149,6 +164,10 @@ 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);
|
||||
|
||||
@@ -163,7 +182,7 @@ public class Damage{
|
||||
if(build != null && build.team != b.team && build.collide(b) && b.checkUnderBuild(build, x * tilesize, y * tilesize)){
|
||||
distances.add(b.dst(build));
|
||||
|
||||
if(b.type.laserAbsorb && build.absorbLasers()){
|
||||
if(laser && build.absorbLasers()){
|
||||
maxDst = Math.min(maxDst, b.dst(build));
|
||||
return true;
|
||||
}
|
||||
@@ -189,7 +208,7 @@ public class Damage{
|
||||
|
||||
/** Collides a bullet with blocks in a laser, taking into account absorption blocks. Resulting length is stored in the bullet's fdata. */
|
||||
public static float collideLaser(Bullet b, float length, boolean large, boolean laser, int pierceCap){
|
||||
float resultLength = findPierceLength(b, pierceCap, length);
|
||||
float resultLength = findPierceLength(b, pierceCap, laser, length);
|
||||
|
||||
collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), resultLength, large, laser, pierceCap);
|
||||
|
||||
@@ -223,11 +242,7 @@ public class Damage{
|
||||
* Only enemies of the specified team are damaged.
|
||||
*/
|
||||
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large, boolean laser, int pierceCap){
|
||||
if(laser){
|
||||
length = findLaserLength(hitter, length);
|
||||
}else if(pierceCap > 0){
|
||||
length = findPierceLength(hitter, pierceCap, length);
|
||||
}
|
||||
length = findLength(hitter, length, laser, pierceCap);
|
||||
|
||||
collidedBlocks.clear();
|
||||
vec.trnsExact(angle, length);
|
||||
@@ -600,7 +615,7 @@ public class Damage{
|
||||
for(int dx = -trad; dx <= trad; dx++){
|
||||
for(int dy = -trad; dy <= trad; dy++){
|
||||
Tile tile = world.tile(Math.round(x / tilesize) + dx, Math.round(y / tilesize) + dy);
|
||||
if(tile != null && tile.build != null && (team == null ||team.isEnemy(tile.team())) && dx*dx + dy*dy <= trad*trad){
|
||||
if(tile != null && tile.build != null && (team == null || team != tile.team()) && dx*dx + dy*dy <= trad*trad){
|
||||
tile.build.damage(team, damage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
@@ -82,8 +81,8 @@ public class EntityCollisions{
|
||||
|
||||
if(tmp.overlaps(r1)){
|
||||
Vec2 v = Geometry.overlap(r1, tmp, x);
|
||||
if(x) r1.x += v.x;
|
||||
if(!x) r1.y += v.y;
|
||||
r1.x += v.x;
|
||||
r1.y += v.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +128,7 @@ public class EntityCollisions{
|
||||
|
||||
public static boolean legsSolid(int x, int y){
|
||||
Tile tile = world.tile(x, y);
|
||||
return tile == null || tile.staticDarkness() >= 2 || (tile.floor().solid && tile.block() == Blocks.air);
|
||||
return tile == null || tile.legSolid();
|
||||
}
|
||||
|
||||
public static boolean waterSolid(int x, int y){
|
||||
|
||||
@@ -28,6 +28,7 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
private int index;
|
||||
|
||||
public static int nextId(){
|
||||
if(lastId >= Integer.MAX_VALUE - 2) lastId = 0;
|
||||
return lastId++;
|
||||
}
|
||||
|
||||
@@ -84,6 +85,10 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
}
|
||||
}
|
||||
|
||||
public Seq<T> copy(){
|
||||
return copy(new Seq<>());
|
||||
}
|
||||
|
||||
public Seq<T> copy(Seq<T> arr){
|
||||
arr.addAll(array);
|
||||
return arr;
|
||||
@@ -141,6 +146,12 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
tree.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
public boolean intersect(float x, float y, float width, float height, Boolf<? super T> out){
|
||||
//don't waste time for empty groups
|
||||
if(isEmpty()) return false;
|
||||
return tree.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
public Seq<T> intersect(float x, float y, float width, float height){
|
||||
intersectArray.clear();
|
||||
//don't waste time for empty groups
|
||||
@@ -225,6 +236,12 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
if(type == null) throw new RuntimeException("Cannot remove a null entity!");
|
||||
if(position != -1 && position < array.size){
|
||||
|
||||
//rarely the entity index is wrong; fallback to slow implementation
|
||||
if(array.items[position] != type){
|
||||
remove(type);
|
||||
return;
|
||||
}
|
||||
|
||||
//swap head with current
|
||||
if(array.size > 1){
|
||||
var head = array.items[array.size - 1];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.EventType.*;
|
||||
@@ -14,13 +12,12 @@ import static mindustry.Vars.*;
|
||||
|
||||
public class Fires{
|
||||
private static final float baseLifetime = 1000f;
|
||||
private static final IntMap<Fire> map = new IntMap<>();
|
||||
|
||||
/** Start a fire on the tile. If there already is a fire there, refreshes its lifetime. */
|
||||
public static void create(Tile tile){
|
||||
if(net.client() || tile == null || !state.rules.fire || !state.rules.hasEnv(Env.oxygen)) return; //not clientside.
|
||||
|
||||
Fire fire = map.get(tile.pos());
|
||||
Fire fire = get(tile);
|
||||
|
||||
if(fire == null){
|
||||
fire = Fire.create();
|
||||
@@ -28,48 +25,58 @@ public class Fires{
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.set(tile.worldx(), tile.worldy());
|
||||
fire.add();
|
||||
map.put(tile.pos(), fire);
|
||||
set(tile, fire);
|
||||
}else{
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.time = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public static Fire get(int x, int y){
|
||||
return map.get(Point2.pack(x, y));
|
||||
public static @Nullable Fire get(Tile tile){
|
||||
return tile == null ? null : world.tiles.getFire(tile.array());
|
||||
}
|
||||
|
||||
public static @Nullable Fire get(int x, int y){
|
||||
return Structs.inBounds(x, y, world.width(), world.height()) ? world.tiles.getFire(world.packArray(x, y)) : null;
|
||||
}
|
||||
|
||||
private static void set(Tile tile, Fire fire){
|
||||
world.tiles.setFire(tile.array(), fire);
|
||||
}
|
||||
|
||||
public static boolean has(int x, int y){
|
||||
if(!Structs.inBounds(x, y, world.width(), world.height()) || !map.containsKey(Point2.pack(x, y))){
|
||||
if(!Structs.inBounds(x, y, world.width(), world.height())){
|
||||
return false;
|
||||
}
|
||||
Fire fire = map.get(Point2.pack(x, y));
|
||||
return fire.isAdded() && fire.fin() < 1f && fire.tile() != null && fire.tile().x == x && fire.tile().y == y;
|
||||
Fire fire = get(x, y);
|
||||
return fire != null && fire.isAdded() && fire.fin() < 1f && fire.tile != null && fire.tile.x == x && fire.tile.y == y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extinguish a fire by shortening its life. If there is no fire here, does nothing.
|
||||
*/
|
||||
public static void extinguish(Tile tile, float intensity){
|
||||
if(tile != null && map.containsKey(tile.pos())){
|
||||
Fire fire = map.get(tile.pos());
|
||||
fire.time(fire.time + intensity * Time.delta);
|
||||
Fx.steam.at(fire);
|
||||
if(fire.time >= fire.lifetime){
|
||||
Events.fire(Trigger.fireExtinguish);
|
||||
if(tile != null){
|
||||
Fire fire = get(tile);
|
||||
if(fire != null){
|
||||
fire.time(fire.time + intensity * Time.delta);
|
||||
Fx.steam.at(fire);
|
||||
if(fire.time >= fire.lifetime){
|
||||
Events.fire(Trigger.fireExtinguish);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(Tile tile){
|
||||
if(tile != null){
|
||||
map.remove(tile.pos());
|
||||
set(tile, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void register(Fire fire){
|
||||
if(fire.tile != null){
|
||||
map.put(fire.tile.pos(), fire);
|
||||
set(fire.tile, fire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
@@ -11,9 +10,9 @@ import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class Puddles{
|
||||
private static final IntMap<Puddle> map = new IntMap<>();
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Puddles{
|
||||
public static final float maxLiquid = 70f;
|
||||
|
||||
/** Deposits a Puddle between tile and source. */
|
||||
@@ -27,8 +26,8 @@ public class Puddles{
|
||||
}
|
||||
|
||||
/** Returns the Puddle on the specified tile. May return null. */
|
||||
public static Puddle get(Tile tile){
|
||||
return map.get(tile.pos());
|
||||
public static @Nullable Puddle get(Tile tile){
|
||||
return tile == null ? null : world.tiles.getPuddle(tile.array());
|
||||
}
|
||||
|
||||
public static void deposit(Tile tile, Tile source, Liquid liquid, float amount, boolean initial){
|
||||
@@ -57,7 +56,7 @@ public class Puddles{
|
||||
if(tile.floor().isLiquid && !canStayOn(liquid, tile.floor().liquidDrop)){
|
||||
reactPuddle(tile.floor().liquidDrop, liquid, amount, tile, ax, ay);
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
Puddle p = get(tile);
|
||||
|
||||
if(initial && p != null && p.lastRipple <= Time.time - 40f){
|
||||
Fx.ripple.at(ax, ay, 1f, tile.floor().liquidDrop.color);
|
||||
@@ -68,15 +67,18 @@ public class Puddles{
|
||||
|
||||
if(tile.floor().solid) return;
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
Puddle p = get(tile);
|
||||
if(p == null || p.liquid == null){
|
||||
Puddle puddle = Puddle.create();
|
||||
puddle.tile = tile;
|
||||
puddle.liquid = liquid;
|
||||
puddle.amount = amount;
|
||||
puddle.set(ax, ay);
|
||||
map.put(tile.pos(), puddle);
|
||||
puddle.add();
|
||||
if(!Vars.net.client()){
|
||||
//do not create puddles clientside as that destroys syncing
|
||||
Puddle puddle = Puddle.create();
|
||||
puddle.tile = tile;
|
||||
puddle.liquid = liquid;
|
||||
puddle.amount = amount;
|
||||
puddle.set(ax, ay);
|
||||
register(puddle);
|
||||
puddle.add();
|
||||
}
|
||||
}else if(p.liquid == liquid){
|
||||
p.accepting = Math.max(amount, p.accepting);
|
||||
|
||||
@@ -98,11 +100,11 @@ public class Puddles{
|
||||
public static void remove(Tile tile){
|
||||
if(tile == null) return;
|
||||
|
||||
map.remove(tile.pos());
|
||||
world.tiles.setPuddle(tile.array(), null);
|
||||
}
|
||||
|
||||
public static void register(Puddle puddle){
|
||||
map.put(puddle.tile().pos(), puddle);
|
||||
world.tiles.setPuddle(puddle.tile().array(), puddle);
|
||||
}
|
||||
|
||||
/** Reacts two liquids together at a location. */
|
||||
|
||||
@@ -20,22 +20,18 @@ public class Units{
|
||||
private static final Rect hitrect = new Rect();
|
||||
private static Unit result;
|
||||
private static float cdist, cpriority;
|
||||
private static boolean boolResult;
|
||||
private static int intResult;
|
||||
private static Building buildResult;
|
||||
|
||||
//prevents allocations in anyEntities
|
||||
private static boolean anyEntityGround;
|
||||
private static float aeX, aeY, aeW, aeH;
|
||||
private static final Cons<Unit> anyEntityLambda = unit -> {
|
||||
if(boolResult) return;
|
||||
private static final Boolf<Unit> anyEntityLambda = unit -> {
|
||||
if((unit.isGrounded() && !unit.type.allowLegStep) == anyEntityGround){
|
||||
unit.hitboxTile(hitrect);
|
||||
|
||||
if(hitrect.overlaps(aeX, aeY, aeW, aeH)){
|
||||
boolResult = true;
|
||||
}
|
||||
return hitrect.overlaps(aeX, aeY, aeW, aeH);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
@@ -93,7 +89,7 @@ public class Units{
|
||||
|
||||
/** @return whether a new instance of a unit of this team can be created. */
|
||||
public static boolean canCreate(Team team, UnitType type){
|
||||
return team.data().countType(type) < getCap(team) && !type.isBanned();
|
||||
return !type.useUnitCap || (team.data().countType(type) < getCap(team) && !type.isBanned());
|
||||
}
|
||||
|
||||
public static int getCap(Team team){
|
||||
@@ -112,7 +108,7 @@ public class Units{
|
||||
|
||||
/** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/
|
||||
public static boolean canInteract(Player player, Building tile){
|
||||
return player == null || tile == null || tile.interactable(player.team());
|
||||
return player == null || tile == null || tile.interactable(player.team()) || state.rules.editor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,31 +158,26 @@ public class Units{
|
||||
}
|
||||
|
||||
public static boolean anyEntities(float x, float y, float width, float height, boolean ground){
|
||||
boolResult = false;
|
||||
anyEntityGround = ground;
|
||||
aeX = x;
|
||||
aeY = y;
|
||||
aeW = width;
|
||||
aeH = height;
|
||||
|
||||
nearby(x, y, width, height, anyEntityLambda);
|
||||
return boolResult;
|
||||
return nearbyCheck(x, y, width, height, anyEntityLambda);
|
||||
}
|
||||
|
||||
/** Note that this checks the tile hitbox, not the standard hitbox. */
|
||||
public static boolean anyEntities(float x, float y, float width, float height, Boolf<Unit> check){
|
||||
boolResult = false;
|
||||
|
||||
nearby(x, y, width, height, unit -> {
|
||||
if(boolResult) return;
|
||||
return nearbyCheck(x, y, width, height, unit -> {
|
||||
if(check.get(unit)){
|
||||
unit.hitboxTile(hitrect);
|
||||
|
||||
if(hitrect.overlaps(x, y, width, height)){
|
||||
boolResult = true;
|
||||
}
|
||||
return hitrect.overlaps(x, y, width, height);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return boolResult;
|
||||
}
|
||||
|
||||
/** Returns the nearest damaged tile. */
|
||||
@@ -223,7 +214,10 @@ public class Units{
|
||||
}
|
||||
});
|
||||
|
||||
return buildResult;
|
||||
var result = buildResult;
|
||||
buildResult = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Iterates through all buildings in a range. */
|
||||
@@ -333,7 +327,7 @@ public class Units{
|
||||
cdist = 0f;
|
||||
|
||||
nearby(team, x, y, range, e -> {
|
||||
if(!predicate.get(e)) return;
|
||||
if(!e.isValid() || !predicate.get(e)) return;
|
||||
|
||||
float dist = e.dst2(x, y);
|
||||
if(result == null || dist < cdist){
|
||||
@@ -351,7 +345,7 @@ public class Units{
|
||||
cdist = 0f;
|
||||
|
||||
nearby(team, x, y, range, e -> {
|
||||
if(!predicate.get(e)) return;
|
||||
if(!e.isValid() || !predicate.get(e)) return;
|
||||
|
||||
float dist = sort.cost(e, x, y);
|
||||
if(result == null || dist < cdist){
|
||||
@@ -370,7 +364,7 @@ public class Units{
|
||||
cdist = 0f;
|
||||
|
||||
nearby(team, x - range, y - range, range*2f, range*2f, e -> {
|
||||
if(!predicate.get(e)) return;
|
||||
if(!e.isValid() || !predicate.get(e)) return;
|
||||
|
||||
float dist = e.dst2(x, y);
|
||||
if(result == null || dist < cdist){
|
||||
@@ -408,7 +402,7 @@ public class Units{
|
||||
if(team != null){
|
||||
team.data().tree().intersect(x, y, width, height, cons);
|
||||
}else{
|
||||
for(var other : state.teams.getActive()){
|
||||
for(var other : state.teams.present){
|
||||
other.tree().intersect(x, y, width, height, cons);
|
||||
}
|
||||
}
|
||||
@@ -428,6 +422,14 @@ public class Units{
|
||||
Groups.unit.intersect(x, y, width, height, cons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all units in a rectangle.
|
||||
* @return whether a unit was found.
|
||||
* */
|
||||
public static boolean nearbyCheck(float x, float y, float width, float height, Boolf<Unit> cons){
|
||||
return Groups.unit.intersect(x, y, width, height, cons);
|
||||
}
|
||||
|
||||
/** Iterates over all units in a rectangle. */
|
||||
public static void nearby(Rect rect, Cons<Unit> cons){
|
||||
nearby(rect.x, rect.y, rect.width, rect.height, cons);
|
||||
|
||||
@@ -15,6 +15,8 @@ public abstract class Ability implements Cloneable{
|
||||
public void draw(Unit unit){}
|
||||
public void death(Unit unit){}
|
||||
public void init(UnitType type){}
|
||||
public void displayBars(Unit unit, Table bars){}
|
||||
public void addStats(Table t){}
|
||||
|
||||
public Ability copy(){
|
||||
try{
|
||||
@@ -25,10 +27,6 @@ public abstract class Ability implements Cloneable{
|
||||
}
|
||||
}
|
||||
|
||||
public void displayBars(Unit unit, Table bars){
|
||||
|
||||
}
|
||||
|
||||
/** @return localized ability name; mods should override this. */
|
||||
public String localized(){
|
||||
var type = getClass();
|
||||
|
||||
@@ -4,9 +4,11 @@ import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.Table;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class ArmorPlateAbility extends Ability{
|
||||
public TextureRegion plateRegion;
|
||||
@@ -25,6 +27,11 @@ public class ArmorPlateAbility extends Ability{
|
||||
unit.healthMultiplier += warmup * healthMultiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.healthMultiplier.localized() + ": [white]" + Math.round(healthMultiplier * 100f) + 100 + "%");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Unit unit){
|
||||
if(warmup > 0.001f){
|
||||
|
||||
@@ -5,15 +5,16 @@ import arc.audio.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -29,6 +30,9 @@ public class EnergyFieldAbility extends Ability{
|
||||
public boolean targetGround = true, targetAir = true, hitBuildings = true, hitUnits = true;
|
||||
public int maxTargets = 25;
|
||||
public float healPercent = 3f;
|
||||
/** Multiplies healing to units of the same type by this amount. */
|
||||
public float sameTypeHealMult = 1f;
|
||||
public boolean displayHeal = true;
|
||||
|
||||
public float layer = Layer.bullet - 0.001f, blinkScl = 20f, blinkSize = 0.1f;
|
||||
public float effectRadius = 5f, sectorRad = 0.14f, rotateSpeed = 0.5f;
|
||||
@@ -48,8 +52,25 @@ public class EnergyFieldAbility extends Ability{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String localized(){
|
||||
return Core.bundle.format("ability.energyfield", damage, range / Vars.tilesize, maxTargets);
|
||||
public void addStats(Table t){
|
||||
t.add(Core.bundle.format("bullet.damage", damage));
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.reload.localized() + ": [white]" + Strings.autoFixed(60f / reload, 2) + " " + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.shootRange.localized() + ": [white]" + Strings.autoFixed(range / tilesize, 2) + " " + StatUnit.blocks.localized());
|
||||
t.row();
|
||||
t.add(Core.bundle.format("ability.energyfield.maxtargets", maxTargets));
|
||||
|
||||
if(displayHeal){
|
||||
t.row();
|
||||
t.add(Core.bundle.format("bullet.healpercent", Strings.autoFixed(healPercent, 2)));
|
||||
t.row();
|
||||
t.add(Core.bundle.format("ability.energyfield.sametypehealmultiplier", Math.round(sameTypeHealMult * 100f)));
|
||||
}
|
||||
if(status != StatusEffects.none){
|
||||
t.row();
|
||||
t.add(status.emoji() + " " + status.localizedName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,7 +150,8 @@ public class EnergyFieldAbility extends Ability{
|
||||
if(((Teamc)other).team() == unit.team){
|
||||
if(other.damaged()){
|
||||
anyNearby = true;
|
||||
other.heal(healPercent / 100f * other.maxHealth());
|
||||
float healMult = (other instanceof Unit u && u.type == unit.type) ? sameTypeHealMult : 1f;
|
||||
other.heal(healPercent / 100f * other.maxHealth() * healMult);
|
||||
healEffect.at(other);
|
||||
damageEffect.at(rx, ry, 0f, color, other);
|
||||
hitEffect.at(rx, ry, unit.angleTo(other), color);
|
||||
|
||||
@@ -12,6 +12,9 @@ import mindustry.content.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class ForceFieldAbility extends Ability{
|
||||
/** Shield radius. */
|
||||
@@ -68,6 +71,18 @@ public class ForceFieldAbility extends Ability{
|
||||
|
||||
ForceFieldAbility(){}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.health.localized() + ": [white]" + Math.round(max));
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.shootRange.localized() + ": [white]" + Strings.autoFixed(radius / tilesize, 2) + " " + StatUnit.blocks.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.repairSpeed.localized() + ": [white]" + Strings.autoFixed(regen * 60f, 2) + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.cooldownTime.localized() + ": [white]" + Strings.autoFixed(cooldown / 60f, 2) + " " + StatUnit.seconds.localized());
|
||||
t.row();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
if(unit.shield < max){
|
||||
|
||||
@@ -30,6 +30,7 @@ public class MoveEffectAbility extends Ability{
|
||||
}
|
||||
|
||||
public MoveEffectAbility(){
|
||||
display = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.Core;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class RegenAbility extends Ability{
|
||||
/** Amount healed as percent per tick. */
|
||||
@@ -9,6 +12,18 @@ public class RegenAbility extends Ability{
|
||||
/** Amount healed as a flat amount per tick. */
|
||||
public float amount = 0f;
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
if(amount > 0.01f){
|
||||
t.add("[lightgray]" + Stat.repairSpeed.localized() + ": [white]" + Strings.autoFixed(amount * 60f, 2) + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
}
|
||||
|
||||
if(percentAmount > 0.01f){
|
||||
t.add(Core.bundle.format("bullet.healpercent", Strings.autoFixed(percentAmount * 60f, 2)) + StatUnit.perSecond.localized()); //stupid but works
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
unit.heal((unit.maxHealth * percentAmount / 100f + amount) * Time.delta);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.tilesize;
|
||||
|
||||
public class RepairFieldAbility extends Ability{
|
||||
public float amount = 1, reload = 100, range = 60;
|
||||
@@ -22,6 +26,13 @@ public class RepairFieldAbility extends Ability{
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.repairSpeed.localized() + ": [white]" + Strings.autoFixed(amount * 60f / reload, 2) + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.shootRange.localized() + ": [white]" + Strings.autoFixed(range / tilesize, 2) + " " + StatUnit.blocks.localized());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
timer += Time.delta;
|
||||
|
||||
@@ -13,6 +13,7 @@ import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class ShieldArcAbility extends Ability{
|
||||
private static Unit paramUnit;
|
||||
@@ -20,7 +21,7 @@ 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 &&
|
||||
!paramPos.within(b, paramField.radius + paramField.width/2f) &&
|
||||
!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)){
|
||||
|
||||
@@ -31,7 +32,7 @@ public class ShieldArcAbility extends Ability{
|
||||
if(paramField.data <= b.damage()){
|
||||
paramField.data -= paramField.cooldown * paramField.regen;
|
||||
|
||||
//TODO fx
|
||||
Fx.arcShieldBreak.at(paramPos.x, paramPos.y, 0, paramUnit.team.color, paramUnit);
|
||||
}
|
||||
|
||||
paramField.data -= b.damage();
|
||||
@@ -66,8 +67,19 @@ public class ShieldArcAbility extends Ability{
|
||||
/** State. */
|
||||
protected float widthScale, alpha;
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.health.localized() + ": [white]" + Math.round(max));
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.repairSpeed.localized() + ": [white]" + Strings.autoFixed(regen * 60f, 2) + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.cooldownTime.localized() + ": [white]" + Strings.autoFixed(cooldown / 60f, 2) + " " + StatUnit.seconds.localized());
|
||||
t.row();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
|
||||
if(data < max){
|
||||
data += Time.delta * regen;
|
||||
}
|
||||
@@ -81,7 +93,8 @@ public class ShieldArcAbility extends Ability{
|
||||
paramField = this;
|
||||
paramPos.set(x, y).rotate(unit.rotation - 90f).add(unit);
|
||||
|
||||
Groups.bullet.intersect(unit.x - radius, unit.y - radius, radius * 2f, radius * 2f, shieldConsumer);
|
||||
float reach = radius + width / 2f;
|
||||
Groups.bullet.intersect(paramPos.x - reach, paramPos.y - reach, reach * 2f, reach * 2f, shieldConsumer);
|
||||
}else{
|
||||
widthScale = Mathf.lerpDelta(widthScale, 0f, 0.11f);
|
||||
}
|
||||
@@ -94,7 +107,6 @@ public class ShieldArcAbility extends Ability{
|
||||
|
||||
@Override
|
||||
public void draw(Unit unit){
|
||||
|
||||
if(widthScale > 0.001f){
|
||||
Draw.z(Layer.shields);
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.Core;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.tilesize;
|
||||
|
||||
public class ShieldRegenFieldAbility extends Ability{
|
||||
public float amount = 1, max = 100f, reload = 100, range = 60;
|
||||
@@ -23,6 +28,16 @@ public class ShieldRegenFieldAbility extends Ability{
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Core.bundle.get("waves.shields") + ": [white]" + Math.round(max)); //extremely stupid usage
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.shootRange.localized() + ": [white]" + Strings.autoFixed(range / tilesize, 2) + " " + StatUnit.blocks.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.reload.localized() + ": [white]" + Strings.autoFixed(60f / reload, 2) + " " + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
timer += Time.delta;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.gen.*;
|
||||
@@ -24,6 +25,11 @@ public class SpawnDeathAbility extends Ability{
|
||||
public SpawnDeathAbility(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add((randAmount > 0 ? amount + "-" + (amount + randAmount) : amount) + " " + unit.emoji() + " " + unit.localizedName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void death(Unit unit){
|
||||
if(!Vars.net.client()){
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package mindustry.entities.abilities;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.Table;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.tilesize;
|
||||
|
||||
public class StatusFieldAbility extends Ability{
|
||||
public StatusEffect effect;
|
||||
public float duration = 60, reload = 100, range = 20;
|
||||
public float duration = 60, reload = 100, range = 20; //
|
||||
public boolean onShoot = false;
|
||||
public Effect applyEffect = Fx.none;
|
||||
public Effect activeEffect = Fx.overdriveWave;
|
||||
@@ -29,8 +32,12 @@ public class StatusFieldAbility extends Ability{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String localized(){
|
||||
return Core.bundle.format("ability.statusfield", effect.emoji());
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.reload.localized() + ": [white]" + Strings.autoFixed(60f / reload, 2) + " " + StatUnit.perSecond.localized());
|
||||
t.row();
|
||||
t.add("[lightgray]" + Stat.shootRange.localized() + ": [white]" + Strings.autoFixed(range / tilesize, 2) + " " + StatUnit.blocks.localized());
|
||||
t.row();
|
||||
t.add(effect.emoji() + " " + effect.localizedName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,7 @@ public class SuppressionFieldAbility extends Ability{
|
||||
public boolean active = true;
|
||||
public Interp particleInterp = f -> Interp.circleOut.apply(Interp.slope.apply(f));
|
||||
public Color particleColor = Pal.sap.cpy();
|
||||
public Color effectColor = Pal.sapBullet;
|
||||
|
||||
public float applyParticleChance = 13f;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class SuppressionFieldAbility extends Ability{
|
||||
|
||||
if((timer += Time.delta) >= reload){
|
||||
Tmp.v1.set(x, y).rotate(unit.rotation - 90f).add(unit);
|
||||
Damage.applySuppression(unit.team, Tmp.v1.x, Tmp.v1.y, range, reload, reload, applyParticleChance, unit);
|
||||
Damage.applySuppression(unit.team, Tmp.v1.x, Tmp.v1.y, range, reload, reload, applyParticleChance, unit, effectColor);
|
||||
timer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package mindustry.entities.abilities;
|
||||
import arc.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
@@ -11,6 +12,7 @@ import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -32,6 +34,13 @@ public class UnitSpawnAbility extends Ability{
|
||||
public UnitSpawnAbility(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStats(Table t){
|
||||
t.add("[lightgray]" + Stat.buildTime.localized() + ": [white]" + Strings.autoFixed(spawnTime / 60f, 2) + " " + StatUnit.seconds.localized());
|
||||
t.row();
|
||||
t.add(unit.emoji() + " " + unit.localizedName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
timer += Time.delta * state.rules.unitBuildSpeed(unit.team);
|
||||
|
||||
@@ -48,6 +48,8 @@ public class BulletType extends Content implements Cloneable{
|
||||
public int pierceCap = -1;
|
||||
/** Multiplier of damage decreased per health pierced. */
|
||||
public float pierceDamageFactor = 0f;
|
||||
/** If positive, limits non-splash damage dealt to a fraction of the target's maximum health. */
|
||||
public float maxDamageFraction = -1f;
|
||||
/** If false, this bullet isn't removed after pierceCap is exceeded. Expert usage only. */
|
||||
public boolean removeAfterPierce = true;
|
||||
/** For piercing lasers, setting this to true makes it get absorbed by plastanium walls. */
|
||||
@@ -125,6 +127,10 @@ public class BulletType extends Content implements Cloneable{
|
||||
/** Whether to move the bullet back depending on delta to fix some delta-time related issues.
|
||||
* Do not change unless you know what you're doing. */
|
||||
public boolean backMove = true;
|
||||
/** If true, the angle param in create is ignored. */
|
||||
public boolean ignoreSpawnAngle = false;
|
||||
/** Chance for this bullet to be created. */
|
||||
public float createChance = 1;
|
||||
/** Bullet range positive override. */
|
||||
public float maxRange = -1f;
|
||||
/** When > 0, overrides range even if smaller than base range. */
|
||||
@@ -154,6 +160,8 @@ public class BulletType extends Content implements Cloneable{
|
||||
|
||||
/** Bullet type that is created when this bullet expires. */
|
||||
public @Nullable BulletType fragBullet = null;
|
||||
/** If true, frag bullets are delayed to the next frame. Fixes obscure bugs with piercing bullet types spawning frags immediately and screwing up the Damage temporary variables. */
|
||||
public boolean delayFrags = false;
|
||||
/** Degree spread range of fragmentation bullets. */
|
||||
public float fragRandomSpread = 360f;
|
||||
/** Uniform spread between each frag bullet in degrees. */
|
||||
@@ -194,10 +202,14 @@ public class BulletType extends Content implements Cloneable{
|
||||
public @Nullable UnitType spawnUnit;
|
||||
/** Unit spawned when this bullet hits something or despawns due to it hitting the end of its lifetime. */
|
||||
public @Nullable UnitType despawnUnit;
|
||||
/** The chance for despawn units to spawn. */
|
||||
public float despawnUnitChance = 1;
|
||||
/** Amount of units spawned when this bullet despawns. */
|
||||
public int despawnUnitCount = 1;
|
||||
/** Random offset distance from the original bullet despawn/hit coordinate. */
|
||||
public float despawnUnitRadius = 0.1f;
|
||||
/** If true, units spawned when this bullet despawns face away from the bullet instead of the same direction as the bullet. */
|
||||
public boolean faceOutwards = false;
|
||||
/** Extra visual parts for this bullet. */
|
||||
public Seq<DrawPart> parts = new Seq<>();
|
||||
|
||||
@@ -247,6 +259,8 @@ public class BulletType extends Content implements Cloneable{
|
||||
public float suppressionDuration = 60f * 8f;
|
||||
/** Chance of suppression effect occurring on block, scaled down by number of blocks. */
|
||||
public float suppressionEffectChance = 50f;
|
||||
/** Color used for the regenSuppressSeek effect. */
|
||||
public Color suppressColor = Pal.sapBullet;
|
||||
|
||||
/** Color of lightning created by bullet. */
|
||||
public Color lightningColor = Pal.surge;
|
||||
@@ -372,10 +386,20 @@ public class BulletType extends Content implements Cloneable{
|
||||
boolean wasDead = entity instanceof Unit u && u.dead;
|
||||
|
||||
if(entity instanceof Healthc h){
|
||||
float damage = b.damage;
|
||||
if(maxDamageFraction > 0){
|
||||
float cap = h.maxHealth() * maxDamageFraction;
|
||||
if(entity instanceof Shieldc s){
|
||||
cap += Math.max(s.shield(), 0f);
|
||||
}
|
||||
damage = Math.min(damage, cap);
|
||||
//cap health to effective health for handlePierce to handle it properly
|
||||
health = Math.min(health, cap);
|
||||
}
|
||||
if(pierceArmor){
|
||||
h.damagePierce(b.damage);
|
||||
h.damagePierce(damage);
|
||||
}else{
|
||||
h.damage(b.damage);
|
||||
h.damage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +448,11 @@ public class BulletType extends Content implements Cloneable{
|
||||
Effect.shake(hitShake, hitShake, b);
|
||||
|
||||
if(fragOnHit){
|
||||
createFrags(b, x, y);
|
||||
if(delayFrags && fragBullet != null && fragBullet.delayFrags){
|
||||
Core.app.post(() -> createFrags(b, x, y));
|
||||
}else{
|
||||
createFrags(b, x, y);
|
||||
}
|
||||
}
|
||||
createPuddles(b, x, y);
|
||||
createIncend(b, x, y);
|
||||
@@ -432,7 +460,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
|
||||
if(suppressionRange > 0){
|
||||
//bullets are pooled, require separate Vec2 instance
|
||||
Damage.applySuppression(b.team, b.x, b.y, suppressionRange, suppressionDuration, 0f, suppressionEffectChance, new Vec2(b.x, b.y));
|
||||
Damage.applySuppression(b.team, b.x, b.y, suppressionRange, suppressionDuration, 0f, suppressionEffectChance, new Vec2(b.x, b.y), suppressColor);
|
||||
}
|
||||
|
||||
createSplashDamage(b, x, y);
|
||||
@@ -489,9 +517,11 @@ public class BulletType extends Content implements Cloneable{
|
||||
}
|
||||
|
||||
public void createUnits(Bullet b, float x, float y){
|
||||
if(despawnUnit != null){
|
||||
if(despawnUnit != null && Mathf.chance(despawnUnitChance)){
|
||||
for(int i = 0; i < despawnUnitCount; i++){
|
||||
despawnUnit.spawn(b.team, x + Mathf.range(despawnUnitRadius), y + Mathf.range(despawnUnitRadius));
|
||||
Tmp.v1.rnd(Mathf.random(despawnUnitRadius));
|
||||
var u = despawnUnit.spawn(b.team, x + Tmp.v1.x, y + Tmp.v1.y);
|
||||
u.rotation = faceOutwards ? Tmp.v1.angle() : b.rotation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,7 +584,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
|
||||
public void init(Bullet b){
|
||||
|
||||
if(killShooter && b.owner() instanceof Healthc h){
|
||||
if(killShooter && b.owner() instanceof Healthc h && !h.dead()){
|
||||
h.kill();
|
||||
}
|
||||
|
||||
@@ -726,6 +756,12 @@ public class BulletType extends Content implements Cloneable{
|
||||
}
|
||||
|
||||
public @Nullable Bullet create(@Nullable Entityc owner, 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, owner, team, x, y, angle, damage, velocityScl, lifetimeScl, data, mover, aimX, aimY);
|
||||
}
|
||||
|
||||
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){
|
||||
if(!Mathf.chance(createChance)) return null;
|
||||
if(ignoreSpawnAngle) angle = 0;
|
||||
if(spawnUnit != null){
|
||||
//don't spawn units clientside!
|
||||
if(!net.client()){
|
||||
@@ -738,17 +774,19 @@ public class BulletType extends Content implements Cloneable{
|
||||
}
|
||||
//assign unit owner
|
||||
if(spawned.controller() instanceof MissileAI ai){
|
||||
if(owner instanceof Unit unit){
|
||||
if(shooter instanceof Unit unit){
|
||||
ai.shooter = unit;
|
||||
}
|
||||
|
||||
if(owner instanceof ControlBlock control){
|
||||
if(shooter instanceof ControlBlock control){
|
||||
ai.shooter = control.unit();
|
||||
}
|
||||
|
||||
}
|
||||
spawned.add();
|
||||
}
|
||||
//Since bullet init is never called, handle killing shooter here
|
||||
if(killShooter && owner instanceof Healthc h && !h.dead()) h.kill();
|
||||
|
||||
//no bullet returned
|
||||
return null;
|
||||
@@ -761,9 +799,12 @@ public class BulletType extends Content implements Cloneable{
|
||||
bullet.time = 0f;
|
||||
bullet.originX = x;
|
||||
bullet.originY = y;
|
||||
bullet.aimTile = world.tileWorld(aimX, aimY);
|
||||
if(!(aimX == -1f && aimY == -1f)){
|
||||
bullet.aimTile = world.tileWorld(aimX, aimY);
|
||||
}
|
||||
bullet.aimX = aimX;
|
||||
bullet.aimY = aimY;
|
||||
|
||||
bullet.initVel(angle, speed * velocityScl);
|
||||
if(backMove){
|
||||
bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta);
|
||||
|
||||
@@ -11,6 +11,8 @@ public class ContinuousBulletType extends BulletType{
|
||||
public float damageInterval = 5f;
|
||||
public boolean largeHit = false;
|
||||
public boolean continuous = true;
|
||||
/** If a building fired this, whether to multiply damage by its timescale. */
|
||||
public boolean timescaleDamage = false;
|
||||
|
||||
{
|
||||
removeAfterPierce = false;
|
||||
@@ -74,10 +76,17 @@ public class ContinuousBulletType extends BulletType{
|
||||
if(shake > 0){
|
||||
Effect.shake(shake, shake, b);
|
||||
}
|
||||
|
||||
updateBulletInterval(b);
|
||||
}
|
||||
|
||||
public void applyDamage(Bullet b){
|
||||
float damage = b.damage;
|
||||
if(timescaleDamage && b.owner instanceof Building build){
|
||||
b.damage *= build.timeScale();
|
||||
}
|
||||
Damage.collideLine(b, b.team, hitEffect, b.x, b.y, b.rotation(), currentLength(b), largeHit, laserAbsorb, pierceCap);
|
||||
b.damage = damage;
|
||||
}
|
||||
|
||||
public float currentLength(Bullet b){
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ContinuousFlameBulletType extends ContinuousBulletType{
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float mult = b.fin(lengthInterp);
|
||||
float realLength = (pierceCap <= 0 ? length : Damage.findPierceLength(b, pierceCap, length)) * mult;
|
||||
float realLength = Damage.findLength(b, length * mult, laserAbsorb, pierceCap);
|
||||
|
||||
float sin = Mathf.sin(Time.time, oscScl, oscMag);
|
||||
|
||||
|
||||
@@ -41,9 +41,8 @@ public class ContinuousLaserBulletType extends ContinuousBulletType{
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float realLength = Damage.findLaserLength(b, length);
|
||||
float fout = Mathf.clamp(b.time > b.lifetime - fadeTime ? 1f - (b.time - (lifetime - fadeTime)) / fadeTime : 1f);
|
||||
float baseLen = realLength * fout;
|
||||
float realLength = Damage.findLength(b, length * fout, laserAbsorb, pierceCap);
|
||||
float rot = b.rotation();
|
||||
|
||||
for(int i = 0; i < colors.length; i++){
|
||||
@@ -55,17 +54,17 @@ public class ContinuousLaserBulletType extends ContinuousBulletType{
|
||||
float ellipseLenScl = Mathf.lerp(1 - i / (float)(colors.length), 1f, pointyScaling);
|
||||
|
||||
Lines.stroke(stroke);
|
||||
Lines.lineAngle(b.x, b.y, rot, baseLen - frontLength, false);
|
||||
Lines.lineAngle(b.x, b.y, rot, realLength - frontLength, false);
|
||||
|
||||
//back ellipse
|
||||
Drawf.flameFront(b.x, b.y, divisions, rot + 180f, backLength, stroke / 2f);
|
||||
|
||||
//front ellipse
|
||||
Tmp.v1.trnsExact(rot, baseLen - frontLength);
|
||||
Tmp.v1.trnsExact(rot, realLength - frontLength);
|
||||
Drawf.flameFront(b.x + Tmp.v1.x, b.y + Tmp.v1.y, divisions, rot, frontLength * ellipseLenScl, stroke / 2f);
|
||||
}
|
||||
|
||||
Tmp.v1.trns(b.rotation(), baseLen * 1.1f);
|
||||
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);
|
||||
Draw.reset();
|
||||
@@ -76,4 +75,9 @@ public class ContinuousLaserBulletType extends ContinuousBulletType{
|
||||
//no light drawn here
|
||||
}
|
||||
|
||||
@Override
|
||||
public float currentLength(Bullet b){
|
||||
float fout = Mathf.clamp(b.time > b.lifetime - fadeTime ? 1f - (b.time - (lifetime - fadeTime)) / fadeTime : 1f);
|
||||
return length * fout;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public class LaserBulletType extends BulletType{
|
||||
hittable = false;
|
||||
absorbable = false;
|
||||
removeAfterPierce = false;
|
||||
delayFrags = true;
|
||||
}
|
||||
|
||||
public LaserBulletType(){
|
||||
|
||||
@@ -39,6 +39,8 @@ public class LightningBulletType extends BulletType{
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
super.init(b);
|
||||
|
||||
Lightning.create(b, lightningColor, damage, b.x, b.y, b.rotation(), lightningLength + Mathf.random(lightningLengthRand));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ public class PointBulletType extends BulletType{
|
||||
Building build = Vars.world.buildWorld(px, py);
|
||||
if(build != null && build.team != b.team){
|
||||
build.collision(b);
|
||||
hit(b, px, py);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ public class PointLaserBulletType extends BulletType{
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
super.update(b);
|
||||
updateTrail(b);
|
||||
updateTrailEffects(b);
|
||||
updateBulletInterval(b);
|
||||
|
||||
if(b.timer.get(0, damageInterval)){
|
||||
Damage.collidePoint(b, b.team, hitEffect, b.aimX, b.aimY);
|
||||
@@ -115,4 +117,13 @@ public class PointLaserBulletType extends BulletType{
|
||||
b.trail.update(b.aimX, b.aimY, b.fslope() * (1f - (trailSinMag > 0 ? Mathf.absin(Time.time, trailSinScl, trailSinMag) : 0f)));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBulletInterval(Bullet b){
|
||||
if(intervalBullet != null && b.time >= intervalDelay && b.timer.get(2, bulletInterval)){
|
||||
float ang = b.rotation();
|
||||
for(int i = 0; i < intervalBullets; i++){
|
||||
intervalBullet.create(b, b.aimX, b.aimY, ang + Mathf.range(intervalRandomSpread) + intervalAngle + ((i - (intervalBullets - 1f)/2f) * intervalSpread));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class RailBulletType extends BulletType{
|
||||
//for calculating the furthest point
|
||||
static float furthest = 0;
|
||||
static boolean any = false;
|
||||
|
||||
public Effect pierceEffect = Fx.hitBulletSmall, pointEffect = Fx.none, lineEffect = Fx.none;
|
||||
public Effect endEffect = Fx.none;
|
||||
|
||||
@@ -28,6 +24,7 @@ public class RailBulletType extends BulletType{
|
||||
collides = false;
|
||||
keepVelocity = false;
|
||||
lifetime = 1f;
|
||||
delayFrags = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,8 +43,6 @@ public class RailBulletType extends BulletType{
|
||||
|
||||
if(b.damage > 0){
|
||||
pierceEffect.at(x, y, b.rotation());
|
||||
|
||||
hitEffect.at(x, y);
|
||||
}
|
||||
|
||||
//subtract health from each consecutive pierce
|
||||
@@ -55,10 +50,8 @@ public class RailBulletType extends BulletType{
|
||||
|
||||
//bullet was stopped, decrease furthest distance
|
||||
if(b.damage <= 0f){
|
||||
furthest = Math.min(furthest, b.dst(x, y));
|
||||
b.fdata = Math.min(b.fdata, b.dst(x, y));
|
||||
}
|
||||
|
||||
any = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,10 +59,8 @@ public class RailBulletType extends BulletType{
|
||||
super.init(b);
|
||||
|
||||
b.fdata = length;
|
||||
furthest = length;
|
||||
any = false;
|
||||
Damage.collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), length, false, false);
|
||||
float resultLen = furthest;
|
||||
Damage.collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), length, false, false, pierceCap);
|
||||
float resultLen = b.fdata;
|
||||
|
||||
Vec2 nor = Tmp.v1.trns(b.rotation(), 1f).nor();
|
||||
if(pointEffect != Fx.none){
|
||||
@@ -78,6 +69,8 @@ public class RailBulletType extends BulletType{
|
||||
}
|
||||
}
|
||||
|
||||
boolean any = b.collided.size > 0;
|
||||
|
||||
if(!any && endEffect != Fx.none){
|
||||
endEffect.at(b.x + nor.x * resultLen, b.y + nor.y * resultLen, b.rotation(), hitColor);
|
||||
}
|
||||
|
||||
@@ -47,13 +47,27 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
updateBuildLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRead(){
|
||||
//why would this happen?
|
||||
if(plans == null){
|
||||
plans = new Queue<>(1);
|
||||
}
|
||||
}
|
||||
|
||||
public void validatePlans(){
|
||||
if(plans.size > 0){
|
||||
Iterator<BuildPlan> it = plans.iterator();
|
||||
while(it.hasNext()){
|
||||
BuildPlan plan = it.next();
|
||||
Tile tile = world.tile(plan.x, plan.y);
|
||||
if(tile == null || (plan.breaking && tile.block() == Blocks.air) || (!plan.breaking && ((tile.build != null && tile.build.rotation == plan.rotation) || !plan.block.rotate) && tile.block() == plan.block)){
|
||||
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
|
||||
((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())))))){
|
||||
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
@@ -86,17 +100,17 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
buildCounter = Math.min(buildCounter, 10f);
|
||||
|
||||
//random attempt to fix a freeze that only occurs on Android
|
||||
int maxPerFrame = 10, count = 0;
|
||||
int maxPerFrame = state.rules.instantBuild ? plans.size : 10, count = 0;
|
||||
|
||||
while(buildCounter >= 1 && count++ < maxPerFrame){
|
||||
while((buildCounter >= 1 || state.rules.instantBuild) && count++ < maxPerFrame){
|
||||
buildCounter -= 1f;
|
||||
|
||||
validatePlans();
|
||||
|
||||
var core = core();
|
||||
|
||||
//nothing to build.
|
||||
if(buildPlan() == null) return;
|
||||
//nothing to build, or core doesn't exist
|
||||
if(buildPlan() == null || (core == null && !infinite)) return;
|
||||
|
||||
//find the next build plan
|
||||
if(plans.size > 1){
|
||||
@@ -120,7 +134,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
if(!within(tile, finalPlaceDst)) continue;
|
||||
|
||||
if(!headless){
|
||||
Vars.control.sound.loop(Sounds.build, tile, 0.51f);
|
||||
Vars.control.sound.loop(Sounds.build, tile, 0.15f);
|
||||
}
|
||||
|
||||
if(!(tile.build instanceof ConstructBuild cb)){
|
||||
@@ -149,7 +163,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
}
|
||||
|
||||
//if there is no core to build with or no build entity, stop building!
|
||||
if((core == null && !infinite) || !(tile.build instanceof ConstructBuild entity)){
|
||||
if(!(tile.build instanceof ConstructBuild entity)){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
transient float healSuppressionTime = -1f;
|
||||
transient float lastHealTime = -120f * 10f;
|
||||
transient Color suppressColor = Pal.sapBullet;
|
||||
|
||||
private transient float lastDamageTime = -recentDamageTime;
|
||||
private transient float timeScale = 1f, timeScaleDuration;
|
||||
@@ -377,13 +378,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
float heat = 0f;
|
||||
|
||||
for(var edge : block.getEdges()){
|
||||
Building build = nearby(edge.x, edge.y);
|
||||
for(var build : proximity){
|
||||
if(build != null && build.team == team && build instanceof HeatBlock heater){
|
||||
//massive hack but I don't really care anymore
|
||||
if(heater instanceof HeatConductorBuild cond){
|
||||
cond.updateHeat();
|
||||
}
|
||||
|
||||
|
||||
boolean split = build.block instanceof HeatConductor cond && cond.splitHeat;
|
||||
// non-routers must face us, routers must face away - next to a redirector, they're forced to face away due to cycles anyway
|
||||
@@ -391,8 +388,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
//if there's a cycle, ignore its heat
|
||||
if(!(build instanceof HeatConductorBuild hc && hc.cameFrom.contains(id()))){
|
||||
//x/y coordinate difference across point of contact
|
||||
float diff = (Math.min(Math.abs(build.x - x), Math.abs(build.y - y)) / tilesize);
|
||||
//number of points that this block had contact with
|
||||
int contactPoints = Math.min((int)(block.size/2f + build.block.size/2f - diff), Math.min(build.block.size, block.size));
|
||||
|
||||
//heat is distributed across building size
|
||||
float add = heater.heat() / build.block.size;
|
||||
float add = heater.heat() / build.block.size * contactPoints;
|
||||
if(split){
|
||||
//heat routers split heat across 3 surfaces
|
||||
add /= 3f;
|
||||
@@ -409,6 +411,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
cameFrom.addAll(hc.cameFrom);
|
||||
}
|
||||
}
|
||||
|
||||
//massive hack but I don't really care anymore
|
||||
if(heater instanceof HeatConductorBuild cond){
|
||||
cond.updateHeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,7 +439,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
|
||||
public void applyHealSuppression(float amount){
|
||||
applyHealSuppression(amount, Pal.sapBullet);
|
||||
}
|
||||
public void applyHealSuppression(float amount, Color suppressColor){
|
||||
healSuppressionTime = Math.max(healSuppressionTime, Time.time + amount);
|
||||
this.suppressColor = suppressColor;
|
||||
}
|
||||
|
||||
public boolean isHealSuppressed(){
|
||||
@@ -775,7 +786,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
for(int i = 0; i < proximity.size; i++){
|
||||
Building other = proximity.get((i + dump) % proximity.size);
|
||||
|
||||
if(other.team == team && other.acceptPayload(self(), todump)){
|
||||
if(other.acceptPayload(self(), todump)){
|
||||
other.handlePayload(self(), todump);
|
||||
incrementDump(proximity.size);
|
||||
return true;
|
||||
@@ -828,7 +839,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
other = other.getLiquidDestination(self(), liquid);
|
||||
|
||||
if(other != null && other.team == team && other.block.hasLiquids && canDumpLiquid(other, liquid) && other.liquids != null){
|
||||
if(other != null && other.block.hasLiquids && canDumpLiquid(other, liquid) && other.liquids != null){
|
||||
float ofract = other.liquids.get(liquid) / other.block.liquidCapacity;
|
||||
float fract = liquids.get(liquid) / block.liquidCapacity;
|
||||
|
||||
@@ -935,7 +946,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
for(int i = 0; i < proximity.size; i++){
|
||||
incrementDump(proximity.size);
|
||||
Building other = proximity.get((i + dump) % proximity.size);
|
||||
if(other.team == team && other.acceptItem(self(), item) && canDump(other, item)){
|
||||
if(other.acceptItem(self(), item) && canDump(other, item)){
|
||||
other.handleItem(self(), item);
|
||||
return;
|
||||
}
|
||||
@@ -953,7 +964,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
for(int i = 0; i < proximity.size; i++){
|
||||
incrementDump(proximity.size);
|
||||
Building other = proximity.get((i + dump) % proximity.size);
|
||||
if(other.team == team && other.acceptItem(self(), item) && canDump(other, item)){
|
||||
if(other.acceptItem(self(), item) && canDump(other, item)){
|
||||
other.handleItem(self(), item);
|
||||
return true;
|
||||
}
|
||||
@@ -1000,21 +1011,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
* @param todump Item to dump. Can be null to dump anything.
|
||||
*/
|
||||
public boolean dump(Item todump){
|
||||
if(!block.hasItems || items.total() == 0 || (todump != null && !items.has(todump))) return false;
|
||||
if(!block.hasItems || items.total() == 0 || proximity.size == 0 || (todump != null && !items.has(todump))) return false;
|
||||
|
||||
int dump = this.cdump;
|
||||
|
||||
if(proximity.size == 0) return false;
|
||||
var allItems = content.items();
|
||||
int itemSize = allItems.size;
|
||||
Object[] itemArray = allItems.items;
|
||||
|
||||
for(int i = 0; i < proximity.size; i++){
|
||||
Building other = proximity.get((i + dump) % proximity.size);
|
||||
|
||||
if(todump == null){
|
||||
|
||||
for(int ii = 0; ii < content.items().size; ii++){
|
||||
Item item = content.item(ii);
|
||||
for(int ii = 0; ii < itemSize; ii++){
|
||||
if(!items.has(ii)) continue;
|
||||
Item item = (Item)itemArray[ii];
|
||||
|
||||
if(other.team == team && items.has(item) && other.acceptItem(self(), item) && canDump(other, item)){
|
||||
if(other.acceptItem(self(), item) && canDump(other, item)){
|
||||
other.handleItem(self(), item);
|
||||
items.remove(item, 1);
|
||||
incrementDump(proximity.size);
|
||||
@@ -1022,7 +1035,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(other.team == team && other.acceptItem(self(), todump) && canDump(other, todump)){
|
||||
if(other.acceptItem(self(), todump) && canDump(other, todump)){
|
||||
other.handleItem(self(), todump);
|
||||
items.remove(todump, 1);
|
||||
incrementDump(proximity.size);
|
||||
@@ -1234,7 +1247,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
public boolean checkSuppression(){
|
||||
if(isHealSuppressed()){
|
||||
if(Mathf.chanceDelta(0.03)){
|
||||
Fx.regenSuppressParticle.at(x + Mathf.range(block.size * tilesize/2f - 1f), y + Mathf.range(block.size * tilesize/2f - 1f));
|
||||
Fx.regenSuppressParticle.at(x + Mathf.range(block.size * tilesize/2f - 1f), y + Mathf.range(block.size * tilesize/2f - 1f), suppressColor);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1322,7 +1335,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
//I really do not like that the bullet will not destroy derelict
|
||||
//but I can't do anything about it without using a random team
|
||||
//which may or may not cause issues with servers and js
|
||||
block.destroyBullet.create(this, Team.derelict, x, y, 0);
|
||||
block.destroyBullet.create(this, block.destroyBulletSameTeam ? team : Team.derelict, x, y, Mathf.randomSeed(id(), 360f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1331,6 +1344,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
return block.itemCapacity;
|
||||
}
|
||||
|
||||
/** Called when a block begins (not finishes!) deconstruction. The building is still present at this point. */
|
||||
public void onDeconstructed(@Nullable Unit builder){
|
||||
//deposit non-incinerable liquid on ground
|
||||
if(liquids != null && liquids.currentAmount() > 0 && (!liquids.current().incinerable || block.deconstructDropAllLiquid)){
|
||||
float perCell = liquids.currentAmount() / (block.size * block.size) * 2f;
|
||||
tile.getLinkedTiles(other -> Puddles.deposit(other, liquids.current(), perCell));
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the block is destroyed. The tile is still intact at this stage. */
|
||||
public void onDestroyed(){
|
||||
float explosiveness = block.baseExplosiveness;
|
||||
@@ -1371,6 +1393,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
});
|
||||
}
|
||||
|
||||
//cap explosiveness so fluid tanks/vaults don't instakill units
|
||||
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect);
|
||||
|
||||
if(block.createRubble && !floor().solid && !floor().isLiquid){
|
||||
@@ -1689,7 +1712,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
for(Point2 point : nearby){
|
||||
Building other = world.build(tile.x + point.x, tile.y + point.y);
|
||||
|
||||
if(other == null || !(other.tile.interactable(team))) continue;
|
||||
if(other == null || other.team != team) continue;
|
||||
|
||||
other.proximity.addUnique(self());
|
||||
|
||||
@@ -1709,8 +1732,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
}
|
||||
|
||||
//TODO probably should not have a shouldConsume() check? should you even *use* consValid?
|
||||
|
||||
public void consume(){
|
||||
for(Consume cons : block.consumers){
|
||||
cons.trigger(self());
|
||||
@@ -1912,6 +1933,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
case controlled -> this instanceof ControlBlock c && c.isControlled() ? GlobalVars.ctrlPlayer : 0;
|
||||
case payloadCount -> getPayload() != null ? 1 : 0;
|
||||
case size -> block.size;
|
||||
case cameraX, cameraY, cameraWidth, cameraHeight -> this instanceof ControlBlock c ? c.unit().sense(sensor) : 0;
|
||||
default -> Float.NaN; //gets converted to null in logic
|
||||
};
|
||||
}
|
||||
@@ -1956,6 +1978,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
case health -> {
|
||||
health = (float)Mathf.clamp(value, 0, maxHealth);
|
||||
healthChanged();
|
||||
if(health <= 0f && !dead()){
|
||||
Call.buildDestroyed(self());
|
||||
}
|
||||
}
|
||||
case team -> {
|
||||
Team team = Team.get((int)value);
|
||||
@@ -1996,7 +2021,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}else if(content instanceof Liquid liquid && liquids != null){
|
||||
float amount = Mathf.clamp((float)value, 0f, block.liquidCapacity);
|
||||
//decreasing amount is always allowed
|
||||
if(amount < liquids.get(liquid) || (acceptLiquid(self(), liquid) && (liquids.current() == liquid || liquids.currentAmount() <= 0.1f))){
|
||||
if(amount < liquids.get(liquid) || (acceptLiquid(self(), liquid) && (liquids.current() == liquid || liquids.currentAmount() <= 0.1f || block.consumesLiquid(liquid)))){
|
||||
liquids.set(liquid, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
|
||||
}
|
||||
|
||||
if(build != null && isAdded()
|
||||
&& checkUnderBuild(build, x, y)
|
||||
&& checkUnderBuild(build, x * tilesize, y * tilesize)
|
||||
&& build.collide(self()) && type.testCollision(self(), build)
|
||||
&& !build.dead() && (type.collidesTeam || build.team != team) && !(type.pierceBuilding && hasCollided(build.id))){
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
@Override
|
||||
public void update(){
|
||||
if(moving()){
|
||||
segmentRot = Angles.moveToward(segmentRot, rotation, type.segmentRotSpeed);
|
||||
segmentRot = Angles.moveToward(segmentRot, rotation, type.segmentRotSpeed * Time.delta);
|
||||
|
||||
int radius = (int)Math.max(0, hitSize / tilesize * 2f);
|
||||
int count = 0, solids = 0, deeps = 0;
|
||||
@@ -106,10 +106,10 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
lastDeepFloor = null;
|
||||
}
|
||||
|
||||
lastCrawlSlowdown = Mathf.lerp(1f, type.crawlSlowdown, Mathf.clamp((float)solids / count / type.crawlSlowdownFrac));
|
||||
lastCrawlSlowdown = Mathf.lerpDelta(1f, type.crawlSlowdown, Mathf.clamp((float)solids / count / type.crawlSlowdownFrac));
|
||||
}
|
||||
segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot);
|
||||
|
||||
crawlTime += vel.len();
|
||||
crawlTime += vel.len() * Time.delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,9 @@ abstract class EntityComp{
|
||||
void afterRead(){
|
||||
|
||||
}
|
||||
|
||||
/** Called after *all* entities are read. */
|
||||
void afterAllRead(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.pooling.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.units.*;
|
||||
@@ -36,6 +38,8 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
@ReadOnly Team team = Team.sharded;
|
||||
@SyncLocal boolean typing, shooting, boosting;
|
||||
@SyncLocal float mouseX, mouseY;
|
||||
/** command the unit had before it was controlled. */
|
||||
@Nullable @NoSync UnitCommand lastCommand;
|
||||
boolean admin;
|
||||
String name = "frog";
|
||||
Color color = new Color();
|
||||
@@ -179,6 +183,9 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
if(!unit.isNull()){
|
||||
clearUnit();
|
||||
}
|
||||
|
||||
lastReadUnit = Nulls.unit;
|
||||
justSwitchTo = justSwitchFrom = null;
|
||||
}
|
||||
|
||||
public void team(Team team){
|
||||
@@ -203,9 +210,18 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead.");
|
||||
if(this.unit == unit) return;
|
||||
|
||||
//save last command this unit had
|
||||
if(unit.controller() instanceof CommandAI ai){
|
||||
lastCommand = ai.command;
|
||||
}
|
||||
|
||||
if(this.unit != Nulls.unit){
|
||||
//un-control the old unit
|
||||
this.unit.resetController();
|
||||
//restore last command issued before it was controlled
|
||||
if(lastCommand != null && this.unit.controller() instanceof CommandAI ai){
|
||||
ai.command(lastCommand);
|
||||
}
|
||||
}
|
||||
this.unit = unit;
|
||||
if(unit != Nulls.unit){
|
||||
@@ -262,6 +278,9 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
public void draw(){
|
||||
if(unit != null && unit.inFogTo(Vars.player.team())) return;
|
||||
|
||||
//??????
|
||||
if(name == null) return;
|
||||
|
||||
Draw.z(Layer.playerName);
|
||||
float z = Drawf.text();
|
||||
|
||||
@@ -323,13 +342,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
}
|
||||
|
||||
void sendMessage(String text){
|
||||
if(isLocal()){
|
||||
if(ui != null){
|
||||
ui.chatfrag.addMessage(text);
|
||||
}
|
||||
}else{
|
||||
Call.sendMessage(con, text, null, null);
|
||||
}
|
||||
sendMessage(text, null, null);
|
||||
}
|
||||
|
||||
void sendMessage(String text, Player from){
|
||||
@@ -346,6 +359,14 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
|
||||
}
|
||||
}
|
||||
|
||||
void sendUnformatted(String unformatted){
|
||||
sendUnformatted(null, unformatted);
|
||||
}
|
||||
|
||||
void sendUnformatted(Player from, String unformatted){
|
||||
sendMessage(netServer.chatFormatter.format(from, unformatted), from, unformatted);
|
||||
}
|
||||
|
||||
PlayerInfo getInfo(){
|
||||
if(isLocal()){
|
||||
throw new IllegalArgumentException("Local players cannot be traced and do not have info.");
|
||||
|
||||
@@ -11,7 +11,7 @@ import mindustry.type.*;
|
||||
|
||||
@Component
|
||||
abstract class ShieldComp implements Healthc, Posc{
|
||||
@Import float health, hitTime, x, y, healthMultiplier;
|
||||
@Import float health, hitTime, x, y, healthMultiplier, armorOverride;
|
||||
@Import boolean dead;
|
||||
@Import Team team;
|
||||
@Import UnitType type;
|
||||
@@ -27,7 +27,7 @@ abstract class ShieldComp implements Healthc, Posc{
|
||||
@Override
|
||||
public void damage(float amount){
|
||||
//apply armor and scaling effects
|
||||
rawDamage(Damage.applyArmor(amount, armor) / healthMultiplier / Vars.state.rules.unitHealth(team));
|
||||
rawDamage(Damage.applyArmor(amount, armorOverride >= 0f ? armorOverride : armor) / healthMultiplier / Vars.state.rules.unitHealth(team));
|
||||
}
|
||||
|
||||
@Replace
|
||||
|
||||
@@ -16,14 +16,16 @@ import static mindustry.Vars.*;
|
||||
|
||||
@Component
|
||||
abstract class StatusComp implements Posc, Flyingc{
|
||||
private Seq<StatusEntry> statuses = new Seq<>();
|
||||
private Seq<StatusEntry> statuses = new Seq<>(4);
|
||||
private transient Bits applied = new Bits(content.getBy(ContentType.status).size);
|
||||
|
||||
//these are considered read-only
|
||||
transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1;
|
||||
//note: armor is a special case; it is an override when >= 0, otherwise ignored
|
||||
transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1, armorOverride = -1f;
|
||||
transient boolean disarmed = false;
|
||||
|
||||
@Import UnitType type;
|
||||
@Import float maxHealth;
|
||||
|
||||
/** Apply a status effect for 1 tick (for permanent effects) **/
|
||||
void apply(StatusEffect effect){
|
||||
@@ -108,6 +110,62 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
return Tmp.c1.set(r / count, g / count, b / count, 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a dynamic status effect, with stat multipliers that can be customized.
|
||||
* @return the entry to write multipliers to. If the dynamic status was already applied, returns the previous entry.
|
||||
* */
|
||||
public StatusEntry applyDynamicStatus(){
|
||||
if(hasEffect(StatusEffects.dynamic)){
|
||||
StatusEntry entry = statuses.find(s -> s.effect.dynamic);
|
||||
if(entry != null) return entry;
|
||||
}
|
||||
|
||||
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
|
||||
entry.set(StatusEffects.dynamic, Float.POSITIVE_INFINITY);
|
||||
statuses.add(entry);
|
||||
entry.effect.applied(self(), entry.time, false);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override speed. */
|
||||
public void statusSpeed(float speed){
|
||||
//type.speed should never be 0
|
||||
applyDynamicStatus().speedMultiplier = speed / type.speed;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to change damage. */
|
||||
public void statusDamageMultiplier(float damageMultiplier){
|
||||
applyDynamicStatus().damageMultiplier = damageMultiplier;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to change reload. */
|
||||
public void statusReloadMultiplier(float reloadMultiplier){
|
||||
applyDynamicStatus().reloadMultiplier = reloadMultiplier;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override max health. */
|
||||
public void statusMaxHealth(float health){
|
||||
//maxHealth should never be zero
|
||||
applyDynamicStatus().healthMultiplier = health / maxHealth;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override build speed. */
|
||||
public void statusBuildSpeed(float buildSpeed){
|
||||
//build speed should never be zero
|
||||
applyDynamicStatus().buildSpeedMultiplier = buildSpeed / type.buildSpeed;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override drag. */
|
||||
public void statusDrag(float drag){
|
||||
//prevent divide by 0 (drag can be zero, if someone makes a broken unit)
|
||||
applyDynamicStatus().dragMultiplier = type.drag == 0f ? 0f : drag / type.drag;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override armor. */
|
||||
public void statusArmor(float armor){
|
||||
applyDynamicStatus().armorOverride = armor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
Floor floor = floorOn();
|
||||
@@ -117,6 +175,7 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
}
|
||||
|
||||
applied.clear();
|
||||
armorOverride = -1f;
|
||||
speedMultiplier = damageMultiplier = healthMultiplier = reloadMultiplier = buildSpeedMultiplier = dragMultiplier = 1f;
|
||||
disarmed = false;
|
||||
|
||||
@@ -136,12 +195,24 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
}else{
|
||||
applied.set(entry.effect.id);
|
||||
|
||||
speedMultiplier *= entry.effect.speedMultiplier;
|
||||
healthMultiplier *= entry.effect.healthMultiplier;
|
||||
damageMultiplier *= entry.effect.damageMultiplier;
|
||||
reloadMultiplier *= entry.effect.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.effect.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.effect.dragMultiplier;
|
||||
//TODO this is very ugly...
|
||||
if(entry.effect.dynamic){
|
||||
speedMultiplier *= entry.speedMultiplier;
|
||||
healthMultiplier *= entry.healthMultiplier;
|
||||
damageMultiplier *= entry.damageMultiplier;
|
||||
reloadMultiplier *= entry.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.dragMultiplier;
|
||||
//armor is a special case; many units have it set it to 0, so an override at values >= 0 is used
|
||||
if(entry.armorOverride >= 0f) armorOverride = entry.armorOverride;
|
||||
}else{
|
||||
speedMultiplier *= entry.effect.speedMultiplier;
|
||||
healthMultiplier *= entry.effect.healthMultiplier;
|
||||
damageMultiplier *= entry.effect.damageMultiplier;
|
||||
reloadMultiplier *= entry.effect.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.effect.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.effect.dragMultiplier;
|
||||
}
|
||||
|
||||
disarmed |= entry.effect.disarm;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -34,7 +35,7 @@ import static mindustry.logic.GlobalVars.*;
|
||||
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{
|
||||
|
||||
@Import boolean hovering, dead, disarmed;
|
||||
@Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, ammo, dragMultiplier;
|
||||
@Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier;
|
||||
@Import Team team;
|
||||
@Import int id;
|
||||
@Import @Nullable Tile mineTile;
|
||||
@@ -91,7 +92,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
moveAt(Tmp.v2.trns(rotation, vec.len()));
|
||||
|
||||
if(!vec.isZero()){
|
||||
rotation = Angles.moveToward(rotation, vec.angle(), type.rotateSpeed * Time.delta);
|
||||
rotation = Angles.moveToward(rotation, vec.angle(), type.rotateSpeed * Time.delta * speedMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +110,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
return !type.flying && world.tiles.in(tileX, tileY) && type.pathCost.getCost(team.id, pathfinder.get(tileX, tileY)) == -1;
|
||||
}
|
||||
|
||||
|
||||
/** @return approx. square size of the physical hitbox for physics */
|
||||
public float physicSize(){
|
||||
return hitSize * 0.7f;
|
||||
@@ -208,6 +208,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
case itemCapacity -> type.itemCapacity;
|
||||
case rotation -> rotation;
|
||||
case health -> health;
|
||||
case shield -> shield;
|
||||
case maxHealth -> maxHealth;
|
||||
case ammo -> !state.rules.unitAmmo ? type.ammoCapacity : ammo;
|
||||
case ammoCapacity -> type.ammoCapacity;
|
||||
@@ -220,11 +221,16 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
case range -> range() / tilesize;
|
||||
case shootX -> World.conv(aimX());
|
||||
case shootY -> World.conv(aimY());
|
||||
case cameraX -> controller instanceof Player player ? World.conv(player.con == null ? Core.camera.position.x : player.con.viewX) : 0;
|
||||
case cameraY -> controller instanceof Player player ? World.conv(player.con == null ? Core.camera.position.y : player.con.viewY) : 0;
|
||||
case cameraWidth -> controller instanceof Player player ? World.conv(player.con == null ? Core.camera.width : player.con.viewWidth) : 0;
|
||||
case cameraHeight -> controller instanceof Player player ? World.conv(player.con == null ? Core.camera.height : player.con.viewHeight) : 0;
|
||||
case mining -> mining() ? 1 : 0;
|
||||
case mineX -> mining() ? mineTile.x : -1;
|
||||
case mineY -> mining() ? mineTile.y : -1;
|
||||
case armor -> armorOverride >= 0f ? armorOverride : armor;
|
||||
case flag -> flag;
|
||||
case speed -> type.speed * 60f / tilesize;
|
||||
case speed -> type.speed * 60f / tilesize * speedMultiplier;
|
||||
case controlled -> !isValid() ? 0 :
|
||||
controller instanceof LogicAI ? ctrlProcessor :
|
||||
controller instanceof Player ? ctrlPlayer :
|
||||
@@ -261,9 +267,21 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
@Override
|
||||
public void setProp(LAccess prop, double value){
|
||||
switch(prop){
|
||||
case health -> health = (float)Mathf.clamp(value, 0, maxHealth);
|
||||
case x -> x = World.unconv((float)value);
|
||||
case y -> y = World.unconv((float)value);
|
||||
case health -> {
|
||||
health = (float)Mathf.clamp(value, 0, maxHealth);
|
||||
if(health <= 0f && !dead){
|
||||
kill();
|
||||
}
|
||||
}
|
||||
case shield -> shield = Math.max((float)value, 0f);
|
||||
case x -> {
|
||||
x = World.unconv((float)value);
|
||||
if(!isLocal()) snapInterpolation();
|
||||
}
|
||||
case y -> {
|
||||
y = World.unconv((float)value);
|
||||
if(!isLocal()) snapInterpolation();
|
||||
}
|
||||
case rotation -> rotation = (float)value;
|
||||
case team -> {
|
||||
if(!net.client()){
|
||||
@@ -275,6 +293,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
}
|
||||
}
|
||||
case flag -> flag = value;
|
||||
case speed -> statusSpeed(Math.max((float)value, 0f));
|
||||
case armor -> statusArmor(Math.max((float)value, 0f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,13 +311,15 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
//only serverside
|
||||
if(((Object)this) instanceof Payloadc pay && !net.client()){
|
||||
if(value instanceof Block b){
|
||||
Building build = b.newBuilding().create(b, team());
|
||||
if(pay.canPickup(build)) pay.addPayload(new BuildPayload(build));
|
||||
if(b.synthetic()){
|
||||
Building build = b.newBuilding().create(b, team());
|
||||
if(pay.canPickup(build)) pay.addPayload(new BuildPayload(build));
|
||||
}
|
||||
}else if(value instanceof UnitType ut){
|
||||
Unit unit = ut.create(team());
|
||||
if(pay.canPickup(unit)) pay.addPayload(new UnitPayload(unit));
|
||||
}else if(value == null && pay.payloads().size > 0){
|
||||
pay.dropLastPayload();
|
||||
pay.payloads().pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,6 +397,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
controller(controller);
|
||||
}
|
||||
|
||||
/** @return the collision layer to use for unit physics. Returning anything outside of PhysicsProcess contents will crash the game. */
|
||||
public int collisionLayer(){
|
||||
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
|
||||
}
|
||||
|
||||
/** @return pathfinder path type for calculating costs */
|
||||
public int pathType(){
|
||||
return Pathfinder.costGround;
|
||||
@@ -400,6 +427,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
return controller instanceof CommandAI;
|
||||
}
|
||||
|
||||
public boolean canTarget(Unit other){
|
||||
return other != null && other.checkTarget(type.targetAir, type.targetGround);
|
||||
}
|
||||
|
||||
public CommandAI command(){
|
||||
if(controller instanceof CommandAI ai){
|
||||
return ai;
|
||||
@@ -458,6 +489,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterAllRead(){
|
||||
controller.afterRead(self());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(){
|
||||
team.data().updateCount(type, 1);
|
||||
|
||||
38
core/src/mindustry/entities/effect/SoundEffect.java
Normal file
38
core/src/mindustry/entities/effect/SoundEffect.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.*;
|
||||
import arc.audio.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
/** Plays a sound effect when created and simultaneously renders an effect. */
|
||||
public class SoundEffect extends Effect{
|
||||
public Sound sound = Sounds.none;
|
||||
public float minPitch = 0.8f;
|
||||
public float maxPitch = 1.2f;
|
||||
public float minVolume = 1f;
|
||||
public float maxVolume = 1f;
|
||||
public Effect effect;
|
||||
|
||||
public SoundEffect(){
|
||||
}
|
||||
|
||||
public SoundEffect(Sound sound, Effect effect){
|
||||
this.sound = sound;
|
||||
this.effect = effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(float x, float y, float rotation, Color color, Object data){
|
||||
if(!shouldCreate()) return;
|
||||
|
||||
sound.at(x, y, Mathf.random(minPitch, maxPitch), Mathf.random(minVolume, maxVolume));
|
||||
effect.create(x, y, rotation, color, data);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import mindustry.graphics.*;
|
||||
public class FlarePart extends DrawPart{
|
||||
public int sides = 4;
|
||||
public float radius = 100f, radiusTo = -1f, stroke = 6f, innerScl = 0.5f, innerRadScl = 0.33f;
|
||||
public float x, y, rotation, rotMove;
|
||||
public float x, y, rotation, rotMove, spinSpeed;
|
||||
public boolean followRotation;
|
||||
public Color color1 = Pal.techBlue, color2 = Color.white;
|
||||
public PartProgress progress = PartProgress.warmup;
|
||||
@@ -29,7 +29,7 @@ public class FlarePart extends DrawPart{
|
||||
float
|
||||
rx = params.x + Tmp.v1.x,
|
||||
ry = params.y + Tmp.v1.y,
|
||||
rot = (followRotation ? params.rotation : 0f) + rotMove * prog + rotation,
|
||||
rot = (followRotation ? params.rotation : 0f) + rotMove * prog + rotation + Time.time * spinSpeed,
|
||||
rad = radiusTo < 0 ? radius : Mathf.lerp(radius, radiusTo, prog);
|
||||
|
||||
Draw.color(color1);
|
||||
|
||||
@@ -7,7 +7,7 @@ import arc.util.*;
|
||||
public class HoverPart extends DrawPart{
|
||||
public float radius = 4f;
|
||||
public float x, y, rotation, phase = 50f, stroke = 3f, minStroke = 0.12f;
|
||||
public int circles = 2;
|
||||
public int circles = 2, sides = 4;
|
||||
public Color color = Color.white;
|
||||
public boolean mirror = false;
|
||||
public float layer = -1f, layerOffset = 0f;
|
||||
@@ -40,7 +40,7 @@ public class HoverPart extends DrawPart{
|
||||
rx = params.x + Tmp.v1.x,
|
||||
ry = params.y + Tmp.v1.y;
|
||||
|
||||
Lines.square(rx, ry, radius * fin, params.rotation - 45f);
|
||||
Lines.poly(rx, ry, sides, radius * fin, params.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ public class RegionPart extends DrawPart{
|
||||
mx += move.x * p;
|
||||
my += move.y * p;
|
||||
mr += move.rot * p;
|
||||
gx += move.gx;
|
||||
gy += move.gy;
|
||||
gx += move.gx * p;
|
||||
gy += move.gy * p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class RegionPart extends DrawPart{
|
||||
//can be null
|
||||
var region = drawRegion ? regions[Math.min(i, regions.length - 1)] : null;
|
||||
float sign = (i == 0 ? 1 : -1) * params.sideMultiplier;
|
||||
Tmp.v1.set((x + mx) * sign * Draw.xscl, (y + my) * Draw.yscl).rotateRadExact((params.rotation - 90) * Mathf.degRad);
|
||||
Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad);
|
||||
|
||||
float
|
||||
rx = params.x + Tmp.v1.x,
|
||||
@@ -152,7 +152,7 @@ public class RegionPart extends DrawPart{
|
||||
float sign = (i == 1 ? -1 : 1) * params.sideMultiplier;
|
||||
Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad);
|
||||
|
||||
childParam.set(params.warmup, params.reload, params.smoothReload, params.heat, params.recoil, params.charge, params.x + Tmp.v1.x, params.y + Tmp.v1.y, i * sign + mr * sign + params.rotation);
|
||||
childParam.set(params.warmup, params.reload, params.smoothReload, params.heat, params.recoil, params.charge, params.x + Tmp.v1.x, params.y + Tmp.v1.y, mr * sign + params.rotation);
|
||||
childParam.sideMultiplier = params.sideMultiplier;
|
||||
childParam.life = params.life;
|
||||
childParam.sideOverride = i;
|
||||
|
||||
@@ -13,6 +13,9 @@ public class ShootSummon extends ShootPattern{
|
||||
this.spread = spread;
|
||||
}
|
||||
|
||||
public ShootSummon(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
|
||||
for(int i = 0; i < shots; i++){
|
||||
|
||||
@@ -55,6 +55,11 @@ public class AIController implements UnitController{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRead(Unit unit){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogicControllable(){
|
||||
return true;
|
||||
@@ -124,7 +129,7 @@ public class AIController implements UnitController{
|
||||
|
||||
if(tile == targetTile || (costType == Pathfinder.costNaval && !targetTile.floor().isLiquid)) return;
|
||||
|
||||
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), unit.speed()));
|
||||
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed()));
|
||||
}
|
||||
|
||||
public void updateWeapons(){
|
||||
@@ -182,7 +187,12 @@ public class AIController implements UnitController{
|
||||
mount.aimY = to.y;
|
||||
}
|
||||
|
||||
unit.isShooting |= (mount.shoot = mount.rotate = shoot);
|
||||
mount.shoot = mount.rotate = shoot;
|
||||
if(!shouldFire()){
|
||||
mount.shoot = false;
|
||||
}
|
||||
|
||||
unit.isShooting |= mount.shoot;
|
||||
|
||||
if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){
|
||||
mount.rotate = true;
|
||||
@@ -202,6 +212,11 @@ public class AIController implements UnitController{
|
||||
return Units.invalidateTarget(target, unit.team, x, y, range);
|
||||
}
|
||||
|
||||
/** @return whether the unit should actually fire bullets (as opposed to just targeting something) */
|
||||
public boolean shouldFire(){
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean shouldShoot(){
|
||||
return true;
|
||||
}
|
||||
@@ -260,13 +275,13 @@ public class AIController implements UnitController{
|
||||
vec.setAngle(Angles.moveToward(unit.vel().angle(), vec.angle(), 6f));
|
||||
}
|
||||
|
||||
vec.setLength(unit.speed());
|
||||
vec.setLength(prefSpeed());
|
||||
|
||||
unit.moveAt(vec);
|
||||
unit.movePref(vec);
|
||||
}
|
||||
|
||||
public void circle(Position target, float circleLength){
|
||||
circle(target, circleLength, unit.speed());
|
||||
circle(target, circleLength, prefSpeed());
|
||||
}
|
||||
|
||||
public void circle(Position target, float circleLength, float speed){
|
||||
@@ -280,7 +295,7 @@ public class AIController implements UnitController{
|
||||
|
||||
vec.setLength(speed);
|
||||
|
||||
unit.moveAt(vec);
|
||||
unit.movePref(vec);
|
||||
}
|
||||
|
||||
public void moveTo(Position target, float circleLength){
|
||||
@@ -298,15 +313,17 @@ public class AIController implements UnitController{
|
||||
public void moveTo(Position target, float circleLength, float smooth, boolean keepDistance, @Nullable Vec2 offset, boolean arrive){
|
||||
if(target == null) return;
|
||||
|
||||
float speed = prefSpeed();
|
||||
|
||||
vec.set(target).sub(unit);
|
||||
|
||||
float length = circleLength <= 0.001f ? 1f : Mathf.clamp((unit.dst(target) - circleLength) / smooth, -1f, 1f);
|
||||
|
||||
vec.setLength(unit.speed() * length);
|
||||
vec.setLength(speed * length);
|
||||
|
||||
if(arrive){
|
||||
Tmp.v3.set(-unit.vel.x / unit.type.accel * 2f, -unit.vel.y / unit.type.accel * 2f).add((target.getX() - unit.x), (target.getY() - unit.y));
|
||||
vec.add(Tmp.v3).limit(unit.speed() * length);
|
||||
vec.add(Tmp.v3).limit(speed * length);
|
||||
}
|
||||
|
||||
if(length < -0.5f){
|
||||
@@ -321,7 +338,7 @@ public class AIController implements UnitController{
|
||||
|
||||
if(offset != null){
|
||||
vec.add(offset);
|
||||
vec.setLength(unit.speed() * length);
|
||||
vec.setLength(speed * length);
|
||||
}
|
||||
|
||||
//do not move when infinite vectors are used or if its zero.
|
||||
@@ -338,6 +355,10 @@ public class AIController implements UnitController{
|
||||
}
|
||||
}
|
||||
|
||||
public float prefSpeed(){
|
||||
return unit.speed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unit(Unit unit){
|
||||
if(this.unit == unit) return;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class BuildPlan implements Position, QuadTreeObject{
|
||||
public BuildPlan(int x, int y, int rotation, Block block){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
if(block != null) this.rotation = block.planRotation(rotation);
|
||||
this.block = block;
|
||||
this.breaking = false;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class BuildPlan implements Position, QuadTreeObject{
|
||||
public BuildPlan(int x, int y, int rotation, Block block, Object config){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
if(block != null) this.rotation = block.planRotation(rotation);
|
||||
this.block = block;
|
||||
this.breaking = false;
|
||||
this.config = config;
|
||||
@@ -138,7 +138,7 @@ public class BuildPlan implements Position, QuadTreeObject{
|
||||
public BuildPlan set(int x, int y, int rotation, Block block){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
if(block != null) this.rotation = block.planRotation(rotation);
|
||||
this.block = block;
|
||||
this.breaking = false;
|
||||
return this;
|
||||
|
||||
@@ -6,6 +6,9 @@ public class StatusEntry{
|
||||
public StatusEffect effect;
|
||||
public float time;
|
||||
|
||||
//all of these are for the dynamic effect only!
|
||||
public float damageMultiplier = 1f, healthMultiplier = 1f, speedMultiplier = 1f, reloadMultiplier = 1f, buildSpeedMultiplier = 1f, dragMultiplier = 1f, armorOverride = -1f;
|
||||
|
||||
public StatusEntry set(StatusEffect effect, float time){
|
||||
this.effect = effect;
|
||||
this.time = time;
|
||||
|
||||
@@ -27,6 +27,10 @@ public interface UnitController{
|
||||
|
||||
}
|
||||
|
||||
default void afterRead(Unit unit){
|
||||
|
||||
}
|
||||
|
||||
default boolean isBeingControlled(Unit player){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class WeaponMount{
|
||||
public int totalShots;
|
||||
/** counter for which barrel bullets have been fired from; used for alternating patterns */
|
||||
public int barrelCounter;
|
||||
/** Last aim length of weapon. Only used for point lasers. */
|
||||
public float lastLength;
|
||||
/** current bullet for continuous weapons */
|
||||
public @Nullable Bullet bullet;
|
||||
/** sound loop for continuous weapons */
|
||||
|
||||
@@ -257,13 +257,13 @@ public class EventType{
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the player configures a specific building. */
|
||||
/** Called when a specific building has its configuration changed. */
|
||||
public static class ConfigEvent{
|
||||
public final Building tile;
|
||||
public final Player player;
|
||||
public final @Nullable Player player;
|
||||
public final Object value;
|
||||
|
||||
public ConfigEvent(Building tile, Player player, Object value){
|
||||
public ConfigEvent(Building tile, @Nullable Player player, Object value){
|
||||
this.tile = tile;
|
||||
this.player = player;
|
||||
this.value = value;
|
||||
@@ -473,6 +473,18 @@ public class EventType{
|
||||
}
|
||||
}
|
||||
|
||||
public static class BuildRotateEvent{
|
||||
public final Building build;
|
||||
public final @Nullable Unit unit;
|
||||
public final int previous;
|
||||
|
||||
public BuildRotateEvent(Building build, @Nullable Unit unit, int previous){
|
||||
this.build = build;
|
||||
this.unit = unit;
|
||||
this.previous = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player or drone begins building something.
|
||||
* This does not necessarily happen when a new ConstructBlock is created.
|
||||
|
||||
@@ -35,6 +35,7 @@ public enum Gamemode{
|
||||
}, map -> map.teams.size > 1),
|
||||
editor(true, rules -> {
|
||||
rules.infiniteResources = true;
|
||||
rules.instantBuild = true;
|
||||
rules.editor = true;
|
||||
rules.waves = false;
|
||||
rules.waveTimer = false;
|
||||
|
||||
72
core/src/mindustry/game/MapMarkers.java
Normal file
72
core/src/mindustry/game/MapMarkers.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.io.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
public class MapMarkers implements Iterable<ObjectiveMarker>{
|
||||
/** Maps marker unique ID to marker. */
|
||||
private IntMap<ObjectiveMarker> map = new IntMap<>();
|
||||
/** Sequential list of markers. This allows for faster iteration than a map. */
|
||||
private Seq<ObjectiveMarker> all = new Seq<>(false);
|
||||
|
||||
public void add(int id, ObjectiveMarker marker){
|
||||
if(marker == null) return;
|
||||
|
||||
var prev = map.put(id, marker);
|
||||
if(prev != null){
|
||||
all.set(prev.arrayIndex, marker);
|
||||
}else{
|
||||
all.add(marker);
|
||||
marker.arrayIndex = all.size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(int id){
|
||||
var prev = map.remove(id);
|
||||
if(prev != null){
|
||||
if(all.size > prev.arrayIndex + 1){ //there needs to be something above the index to replace it with
|
||||
all.remove(prev.arrayIndex);
|
||||
//update its index
|
||||
all.get(prev.arrayIndex).arrayIndex = prev.arrayIndex;
|
||||
}else{
|
||||
//no sense updating the index of the replaced element when it was not replaced
|
||||
all.remove(prev.arrayIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable ObjectiveMarker get(int id){
|
||||
return map.get(id);
|
||||
}
|
||||
|
||||
public boolean has(int id){
|
||||
return get(id) != null;
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return all.size;
|
||||
}
|
||||
|
||||
public void write(DataOutput stream) throws IOException{
|
||||
JsonIO.writeBytes(map, ObjectiveMarker.class, (DataOutputStream)stream);
|
||||
}
|
||||
|
||||
public void read(DataInput stream) throws IOException{
|
||||
all.clear();
|
||||
map = JsonIO.readBytes(IntMap.class, ObjectiveMarker.class, (DataInputStream)stream);
|
||||
for(var entry : map.entries()){
|
||||
all.add(entry.value);
|
||||
entry.value.arrayIndex = all.size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ObjectiveMarker> iterator(){
|
||||
return all.iterator();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
@@ -30,6 +31,8 @@ import static mindustry.Vars.*;
|
||||
public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObjective>{
|
||||
public static final Seq<Prov<? extends MapObjective>> allObjectiveTypes = new Seq<>();
|
||||
public static final Seq<Prov<? extends ObjectiveMarker>> allMarkerTypes = new Seq<>();
|
||||
public static final ObjectMap<String, Prov<? extends ObjectiveMarker>> markerNameToType = new ObjectMap<>();
|
||||
public static final Seq<String> allMarkerTypeNames = new Seq<>();
|
||||
|
||||
/**
|
||||
* All objectives the executor contains. Do not modify directly, ever!
|
||||
@@ -60,7 +63,9 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
ShapeTextMarker::new,
|
||||
MinimapMarker::new,
|
||||
ShapeMarker::new,
|
||||
TextMarker::new
|
||||
TextMarker::new,
|
||||
LineMarker::new,
|
||||
TextureMarker::new
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,8 +75,9 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
allObjectiveTypes.add(prov);
|
||||
|
||||
Class<? extends MapObjective> type = prov.get().getClass();
|
||||
JsonIO.classTag(Strings.camelize(type.getSimpleName().replace("Objective", "")), type);
|
||||
JsonIO.classTag(type.getSimpleName().replace("Objective", ""), type);
|
||||
String name = type.getSimpleName().replace("Objective", "");
|
||||
JsonIO.classTag(Strings.camelize(name), type);
|
||||
JsonIO.classTag(name, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +87,12 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
allMarkerTypes.add(prov);
|
||||
|
||||
Class<? extends ObjectiveMarker> type = prov.get().getClass();
|
||||
JsonIO.classTag(Strings.camelize(type.getSimpleName().replace("Marker", "")), type);
|
||||
JsonIO.classTag(type.getSimpleName().replace("Marker", ""), type);
|
||||
String name = type.getSimpleName().replace("Marker", "");
|
||||
allMarkerTypeNames.add(Strings.camelize(name));
|
||||
markerNameToType.put(name, prov);
|
||||
markerNameToType.put(Strings.camelize(name), prov);
|
||||
JsonIO.classTag(Strings.camelize(name), type);
|
||||
JsonIO.classTag(name, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,23 +112,10 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
/** Updates all objectives this executor contains. */
|
||||
public void update(){
|
||||
eachRunning(obj -> {
|
||||
for(var marker : obj.markers){
|
||||
if(!marker.wasAdded){
|
||||
marker.wasAdded = true;
|
||||
marker.added();
|
||||
}
|
||||
}
|
||||
|
||||
//objectives cannot get completed on the client, but they do try to update for timers and such
|
||||
if(obj.update() && !net.client()){
|
||||
obj.completed = true;
|
||||
obj.done();
|
||||
for(var marker : obj.markers){
|
||||
if(marker.wasAdded){
|
||||
marker.removed();
|
||||
marker.wasAdded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changed |= obj.changed;
|
||||
@@ -193,8 +190,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
/** Called once after {@link #update()} returns true, before this objective is removed. */
|
||||
public void done(){
|
||||
changed();
|
||||
state.rules.objectiveFlags.removeAll(flagsRemoved);
|
||||
state.rules.objectiveFlags.addAll(flagsAdded);
|
||||
Call.objectiveCompleted(flagsRemoved, flagsAdded);
|
||||
}
|
||||
|
||||
/** Notifies the executor that map rules should be synced. */
|
||||
@@ -206,12 +202,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public final boolean dependencyFinished(){
|
||||
if(depFinished) return true;
|
||||
|
||||
boolean f = true;
|
||||
for(var parent : parents){
|
||||
if(!parent.isCompleted()) return false;
|
||||
}
|
||||
|
||||
return f && (depFinished = true);
|
||||
return depFinished = true;
|
||||
}
|
||||
|
||||
/** @return True if this objective is done (practically, has been removed from the executor). */
|
||||
@@ -476,6 +471,14 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
timeString.append(s);
|
||||
|
||||
if(text.startsWith("@")){
|
||||
if(state.mapLocales.containsProperty(text.substring(1))){
|
||||
try{
|
||||
return state.mapLocales.getFormatted(text.substring(1), timeString.toString());
|
||||
}catch(IllegalArgumentException e){
|
||||
//illegal text.
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
return Core.bundle.format(text.substring(1), timeString.toString());
|
||||
}else{
|
||||
try{
|
||||
@@ -582,9 +585,17 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
return state.rules.objectiveFlags.contains(flag);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String text(){
|
||||
return text != null && text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text;
|
||||
if(text == null) return null;
|
||||
|
||||
if(text.startsWith("@")){
|
||||
if(state.mapLocales.containsProperty(text.substring(1))) return state.mapLocales.getProperty(text.substring(1));
|
||||
return Core.bundle.get(text.substring(1));
|
||||
}else{
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,19 +612,49 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
}
|
||||
}
|
||||
|
||||
/** Marker used for drawing UI to indicate something along with an objective. */
|
||||
/** Marker used for drawing various content to indicate something along with an objective. Mostly used as UI overlay. */
|
||||
public static abstract class ObjectiveMarker{
|
||||
/** Makes sure markers are only added once. */
|
||||
public transient boolean wasAdded;
|
||||
/** Internal use only! Do not access. */
|
||||
public transient int arrayIndex;
|
||||
|
||||
/** Whether to display marker on minimap instead of world. {@link MinimapMarker} ignores this value. */
|
||||
public boolean minimap = false;
|
||||
/** Whether to scale marker corresponding to player's zoom level. {@link MinimapMarker} ignores this value. */
|
||||
public boolean autoscale = false;
|
||||
/** Hides the marker, used by world processors. */
|
||||
protected boolean hidden = false;
|
||||
/** On which z-sorting layer is marker drawn. */
|
||||
protected float drawLayer = Layer.overlayUI;
|
||||
|
||||
/** Draws the marker. Actual marker position and scale are calculated in {@link #drawWorld()} and {@link #drawMinimap(MinimapRenderer)}. */
|
||||
public void baseDraw(float x, float y, float scaleFactor){}
|
||||
|
||||
/** Called in the main renderer. */
|
||||
public void drawWorld(){}
|
||||
|
||||
/** Called in the overlay draw layer.*/
|
||||
public void draw(){}
|
||||
/** Called in the small and large map. */
|
||||
public void drawMinimap(MinimapRenderer minimap){}
|
||||
/** Add any UI elements necessary. */
|
||||
public void added(){}
|
||||
/** Remove any UI elements, if necessary. */
|
||||
public void removed(){}
|
||||
|
||||
/** Whether the marker is hidden */
|
||||
public boolean isHidden(){
|
||||
return hidden;
|
||||
}
|
||||
|
||||
/** Control marker with world processor code. Ignores NaN (null) values. */
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(Double.isNaN(p1)) return;
|
||||
|
||||
switch(type){
|
||||
case visibility -> hidden = Mathf.equal((float)p1, 0f);
|
||||
case drawLayer -> drawLayer = (float)p1;
|
||||
case minimap -> minimap = !Mathf.equal((float)p1, 0f);
|
||||
case autoscale -> autoscale = !Mathf.equal((float)p1, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(String text, boolean fetch){}
|
||||
|
||||
public void setTexture(String textureName){}
|
||||
|
||||
/** @return The localized type-name of this objective, defaulting to the class simple name without the "Marker" prefix. */
|
||||
public String typeName(){
|
||||
@@ -622,19 +663,67 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
}
|
||||
|
||||
public static String fetchText(String text){
|
||||
return text.startsWith("@") ?
|
||||
//on mobile, try ${text}.mobile first for mobile-specific hints.
|
||||
mobile ? Core.bundle.get(text.substring(1) + ".mobile", Core.bundle.get(text.substring(1))) :
|
||||
Core.bundle.get(text.substring(1)) :
|
||||
text;
|
||||
if(text == null) return "";
|
||||
|
||||
if(text.startsWith("@")){
|
||||
String key = text.substring(1);
|
||||
|
||||
if(mobile){
|
||||
return state.mapLocales.containsProperty(key + ".mobile") ?
|
||||
state.mapLocales.getProperty(key + ".mobile") :
|
||||
Core.bundle.get(key + ".mobile", Core.bundle.get(key));
|
||||
}else{
|
||||
return state.mapLocales.containsProperty(key) ?
|
||||
state.mapLocales.getProperty(key) :
|
||||
Core.bundle.get(key);
|
||||
}
|
||||
}else{
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A marker that has a position in the world in world coordinates. */
|
||||
public static abstract class PosMarker extends ObjectiveMarker{
|
||||
/** Position of marker, in world coordinates */
|
||||
public @TilePos Vec2 pos = new Vec2();
|
||||
|
||||
/** Called in the main renderer. */
|
||||
@Override
|
||||
public void drawWorld(){
|
||||
baseDraw(pos.x, pos.y, autoscale ? 4f / renderer.getDisplayScale() : 1f);
|
||||
}
|
||||
|
||||
/** Called in the small and large map. */
|
||||
@Override
|
||||
public void drawMinimap(MinimapRenderer minimap){
|
||||
minimap.transform(Tmp.v1.set(pos.x + 4f, pos.y + 4f));
|
||||
baseDraw(Tmp.v1.x, Tmp.v1.y, minimap.getScaleFactor(autoscale));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
if(type == LMarkerControl.pos){
|
||||
pos.x = (float)p1 * tilesize;
|
||||
}else{
|
||||
super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
if(type == LMarkerControl.pos){
|
||||
pos.y = (float)p2 * tilesize;
|
||||
}else{
|
||||
super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays text above a shape. */
|
||||
public static class ShapeTextMarker extends ObjectiveMarker{
|
||||
public static class ShapeTextMarker extends PosMarker{
|
||||
public @Multiline String text = "frog";
|
||||
public @TilePos Vec2 pos = new Vec2();
|
||||
public float fontSize = 1f, textHeight = 7f;
|
||||
public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline;
|
||||
|
||||
@@ -674,18 +763,70 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public ShapeTextMarker(){}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
Lines.stroke(3f, Pal.gray);
|
||||
Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation);
|
||||
Lines.stroke(1f, color);
|
||||
Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation);
|
||||
public void baseDraw(float x, float y, float scaleFactor){
|
||||
//in case some idiot decides to make 9999999 sides and freeze the game
|
||||
int sides = Math.min(this.sides, 300);
|
||||
|
||||
Draw.z(drawLayer);
|
||||
Lines.stroke(3f * scaleFactor, Pal.gray);
|
||||
Lines.poly(x, y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
Lines.stroke(scaleFactor, color);
|
||||
Lines.poly(x, y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
Draw.reset();
|
||||
|
||||
if(fetchedText == null){
|
||||
fetchedText = fetchText(text);
|
||||
}
|
||||
|
||||
WorldLabel.drawAt(fetchedText, pos.x, pos.y + radius + textHeight, Draw.z(), flags, fontSize);
|
||||
// font size cannot be 0
|
||||
if(Mathf.equal(fontSize, 0f)) return;
|
||||
|
||||
WorldLabel.drawAt(fetchedText, x, y + radius * scaleFactor + textHeight * scaleFactor, drawLayer, flags, fontSize * scaleFactor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case fontSize -> fontSize = (float)p1;
|
||||
case textHeight -> textHeight = (float)p1;
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p1, 0f)){
|
||||
flags |= WorldLabel.flagBackground;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagBackground;
|
||||
}
|
||||
}
|
||||
case radius -> radius = (float)p1;
|
||||
case rotation -> rotation = (float)p1;
|
||||
case color -> color.set(Tmp.c1.fromDouble(p1));
|
||||
case shape -> sides = (int)p1;
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p2, 0f)){
|
||||
flags |= WorldLabel.flagOutline;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagOutline;
|
||||
}
|
||||
}
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(String text, boolean fetch){
|
||||
this.text = text;
|
||||
if(fetch){
|
||||
fetchedText = fetchText(this.text);
|
||||
}else{
|
||||
fetchedText = this.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,6 +843,7 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public MinimapMarker(int x, int y, Color color){
|
||||
this.pos.set(x, y);
|
||||
this.color = color;
|
||||
minimap = true;
|
||||
}
|
||||
|
||||
public MinimapMarker(int x, int y, float radius, float stroke, Color color){
|
||||
@@ -709,26 +851,59 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
this.stroke = stroke;
|
||||
this.radius = radius;
|
||||
this.color = color;
|
||||
minimap = true;
|
||||
}
|
||||
|
||||
public MinimapMarker(){}
|
||||
|
||||
@Override
|
||||
public void drawMinimap(MinimapRenderer minimap){
|
||||
minimap.transform(Tmp.v1.set(pos.x * tilesize, pos.y * tilesize));
|
||||
|
||||
float rad = minimap.scale(radius * tilesize);
|
||||
public void baseDraw(float x, float y, float scaleFactor){
|
||||
float rad = radius * tilesize * scaleFactor;
|
||||
float fin = Interp.pow2Out.apply((Time.globalTime / 100f) % 1f);
|
||||
|
||||
Draw.z(drawLayer);
|
||||
Lines.stroke(Scl.scl((1f - fin) * stroke + 0.1f), color);
|
||||
Lines.circle(Tmp.v1.x, Tmp.v1.y, rad * fin);
|
||||
Lines.circle(x, y, rad * fin);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawWorld(){
|
||||
minimap = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawMinimap(MinimapRenderer minimap){
|
||||
minimap.transform(Tmp.v1.set(pos.x * tilesize, pos.y * tilesize));
|
||||
baseDraw(Tmp.v1.x, Tmp.v1.y, minimap.getScaleFactor(autoscale));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case pos -> pos.x = (int)p1;
|
||||
case radius -> radius = (float)p1;
|
||||
case stroke -> stroke = (float)p1;
|
||||
case color -> color.set(Tmp.c1.fromDouble(p1));
|
||||
case minimap -> minimap = true;
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
if(type == LMarkerControl.pos){
|
||||
pos.y = (int)p2;
|
||||
}else{
|
||||
super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays a shape with an outline and color. */
|
||||
public static class ShapeMarker extends ObjectiveMarker{
|
||||
public @TilePos Vec2 pos = new Vec2();
|
||||
public static class ShapeMarker extends PosMarker{
|
||||
public float radius = 8f, rotation = 0f, stroke = 1f;
|
||||
public boolean fill = false, outline = true;
|
||||
public int sides = 4;
|
||||
@@ -747,31 +922,60 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public ShapeMarker(){}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
public void baseDraw(float x, float y, float scaleFactor){
|
||||
//in case some idiot decides to make 9999999 sides and freeze the game
|
||||
int sides = Math.min(this.sides, 200);
|
||||
|
||||
Draw.z(drawLayer);
|
||||
if(!fill){
|
||||
if(outline){
|
||||
Lines.stroke(stroke + 2f, Pal.gray);
|
||||
Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation);
|
||||
Lines.stroke((stroke + 2f) * scaleFactor, Pal.gray);
|
||||
Lines.poly(x, y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
}
|
||||
|
||||
Lines.stroke(stroke, color);
|
||||
Lines.poly(pos.x, pos.y, sides, radius + 1f, rotation);
|
||||
Lines.stroke(stroke * scaleFactor, color);
|
||||
Lines.poly(x, y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
}else{
|
||||
Draw.color(color);
|
||||
Fill.poly(pos.x, pos.y, sides, radius, rotation);
|
||||
Fill.poly(x, y, sides, radius * scaleFactor, rotation);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case radius -> radius = (float)p1;
|
||||
case stroke -> stroke = (float)p1;
|
||||
case rotation -> rotation = (float)p1;
|
||||
case color -> color.set(Tmp.c1.fromDouble(p1));
|
||||
case shape -> sides = (int)p1;
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case shape -> fill = !Mathf.equal((float)p2, 0f);
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p3)){
|
||||
if(type == LMarkerControl.shape){
|
||||
outline = !Mathf.equal((float)p3, 0f);
|
||||
}else{
|
||||
super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays text at a location. */
|
||||
public static class TextMarker extends ObjectiveMarker{
|
||||
public static class TextMarker extends PosMarker{
|
||||
public @Multiline String text = "uwu";
|
||||
public @TilePos Vec2 pos = new Vec2();
|
||||
public float fontSize = 1f;
|
||||
public @LabelFlag byte flags = WorldLabel.flagBackground | WorldLabel.flagOutline;
|
||||
// Cached localized text.
|
||||
@@ -792,13 +996,195 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public TextMarker(){}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
public void baseDraw(float x, float y, float scaleFactor){
|
||||
// font size cannot be 0
|
||||
if(Mathf.equal(fontSize, 0f)) return;
|
||||
|
||||
if(fetchedText == null){
|
||||
fetchedText = fetchText(text);
|
||||
}
|
||||
|
||||
WorldLabel.drawAt(fetchedText, pos.x, pos.y, Draw.z(), flags, fontSize);
|
||||
WorldLabel.drawAt(fetchedText, x, y, drawLayer, flags, fontSize * scaleFactor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case fontSize -> fontSize = (float)p1;
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p1, 0f)){
|
||||
flags |= WorldLabel.flagBackground;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagBackground;
|
||||
}
|
||||
}
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p2, 0f)){
|
||||
flags |= WorldLabel.flagOutline;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagOutline;
|
||||
}
|
||||
}
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(String text, boolean fetch){
|
||||
this.text = text;
|
||||
if(fetch){
|
||||
fetchedText = fetchText(this.text);
|
||||
}else{
|
||||
fetchedText = this.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays a line from pos1 to pos2. */
|
||||
public static class LineMarker extends PosMarker{
|
||||
public @TilePos Vec2 endPos = new Vec2();
|
||||
public float stroke = 1f;
|
||||
public boolean outline = true;
|
||||
public Color color = Color.valueOf("ffd37f");
|
||||
|
||||
public LineMarker(float x1, float y1, float x2, float y2, float stroke){
|
||||
this.stroke = stroke;
|
||||
this.pos.set(x1, y1);
|
||||
this.endPos.set(x2, y2);
|
||||
}
|
||||
|
||||
public LineMarker(float x1, float y1, float x2, float y2){
|
||||
this.pos.set(x1, y1);
|
||||
this.endPos.set(x2, y2);
|
||||
}
|
||||
|
||||
public LineMarker(){}
|
||||
|
||||
public void baseLineDraw(float x1, float y1, float x2, float y2, float scaleFactor){
|
||||
Draw.z(drawLayer);
|
||||
if(outline){
|
||||
Lines.stroke((stroke + 2f) * scaleFactor, Pal.gray);
|
||||
Lines.line(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
Lines.stroke(stroke * scaleFactor, color);
|
||||
Lines.line(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawWorld(){
|
||||
baseLineDraw(pos.x, pos.y, endPos.x, endPos.y, autoscale ? 4f / renderer.getDisplayScale() : 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawMinimap(MinimapRenderer minimap){
|
||||
minimap.transform(Tmp.v1.set(pos.x + 4f, pos.y + 4f));
|
||||
minimap.transform(Tmp.v2.set(endPos.x + 4f, endPos.y + 4f));
|
||||
baseLineDraw(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y, minimap.getScaleFactor(autoscale));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case endPos -> endPos.x = (float)p1 * tilesize;
|
||||
case stroke -> stroke = (float)p1;
|
||||
case color -> color.set(Tmp.c1.fromDouble(p1));
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case endPos -> endPos.y = (float)p2 * tilesize;
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays a texture with specified name. */
|
||||
public static class TextureMarker extends PosMarker{
|
||||
public float rotation = 0f, width = 0f, height = 0f; // Zero width/height scales marker to original texture's size
|
||||
public String textureName = "";
|
||||
public Color color = Color.white.cpy();
|
||||
|
||||
private transient TextureRegion fetchedRegion;
|
||||
|
||||
public TextureMarker(String textureName, float x, float y, float width, float height){
|
||||
this.textureName = textureName;
|
||||
this.pos.set(x, y);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public TextureMarker(String textureName, float x, float y){
|
||||
this.textureName = textureName;
|
||||
this.pos.set(x, y);
|
||||
}
|
||||
|
||||
public TextureMarker(){}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case rotation -> rotation = (float)p1;
|
||||
case textureSize -> width = (float)p1 * tilesize;
|
||||
case color -> color.set(Tmp.c1.fromDouble(p1));
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case textureSize -> height = (float)p2 * tilesize;
|
||||
default -> super.control(type, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void baseDraw(float x, float y, float scaleFactor){
|
||||
if(textureName.isEmpty()) return;
|
||||
|
||||
if(fetchedRegion == null) setTexture(textureName);
|
||||
|
||||
// Zero width/height scales marker to original texture's size
|
||||
if(Mathf.equal(width, 0f)) width = fetchedRegion.width * fetchedRegion.scl() * Draw.xscl;
|
||||
if(Mathf.equal(height, 0f)) height = fetchedRegion.height * fetchedRegion.scl() * Draw.yscl;
|
||||
|
||||
Draw.z(drawLayer);
|
||||
Draw.color(color);
|
||||
Draw.rect(fetchedRegion, x, y, width * scaleFactor, height * scaleFactor, rotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexture(String textureName){
|
||||
this.textureName = textureName;
|
||||
|
||||
if(fetchedRegion == null) fetchedRegion = new TextureRegion();
|
||||
|
||||
TextureRegion region = Core.atlas.find(textureName);
|
||||
if(region.found()){
|
||||
fetchedRegion.set(region);
|
||||
}else{
|
||||
if(Core.assets.isLoaded(textureName, Texture.class)){
|
||||
fetchedRegion.set(Core.assets.get(textureName, Texture.class));
|
||||
}else{
|
||||
fetchedRegion.set(Core.atlas.find("error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** For arrays or {@link Seq}s; does not create element rearrangement buttons. */
|
||||
|
||||
@@ -26,7 +26,7 @@ public class Objectives{
|
||||
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 instanceof Item) ?
|
||||
(content.techNode == null || content.techNode.parent == null || content.techNode.parent.content.unlocked()) ?
|
||||
(content.emoji() + " " + content.localizedName) : "???");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ public class Rules{
|
||||
public boolean attackMode = false;
|
||||
/** Whether this is the editor gamemode. */
|
||||
public boolean editor = false;
|
||||
/** Whether blocks can be repaired by clicking them. */
|
||||
public boolean derelictRepair = true;
|
||||
/** Whether a gameover can happen at all. Set this to false to implement custom gameover conditions. */
|
||||
public boolean canGameOver = true;
|
||||
/** Whether cores change teams when they are destroyed. */
|
||||
@@ -103,6 +105,10 @@ public class Rules{
|
||||
public boolean coreDestroyClear = false;
|
||||
/** If true, banned blocks are hidden from the build menu. */
|
||||
public boolean hideBannedBlocks = false;
|
||||
/** If true, most blocks (including environmental walls) can be deconstructed. This is only meant to be used internally in sandbox/test maps. */
|
||||
public boolean allowEnvironmentDeconstruct = false;
|
||||
/** If true, buildings will be constructed instantly, with no limit on blocks placed per second. This is highly experimental and may cause lag! */
|
||||
public boolean instantBuild = false;
|
||||
/** If true, bannedBlocks becomes a whitelist. */
|
||||
public boolean blockWhitelist = false;
|
||||
/** If true, bannedUnits becomes a whitelist. */
|
||||
@@ -191,6 +197,8 @@ public class Rules{
|
||||
public float backgroundOffsetX = 0.1f, backgroundOffsetY = 0.1f;
|
||||
/** Parameters for planet rendered in the background. Cannot be changed once a map is loaded. */
|
||||
public @Nullable PlanetParams planetBackground;
|
||||
/** Rules from this planet are applied. If it's {@code sun}, mixed tech is enabled. */
|
||||
public Planet planet = Planets.serpulo;
|
||||
|
||||
/** Copies this ruleset exactly. Not efficient at all, do not use often. */
|
||||
public Rules copy(){
|
||||
@@ -267,6 +275,11 @@ public class Rules{
|
||||
/** If true, this team has infinite unit ammo. */
|
||||
public boolean infiniteAmmo;
|
||||
|
||||
/** AI that builds random schematics. */
|
||||
public boolean buildAi;
|
||||
/** Tier of builder AI. [0, 1] */
|
||||
public float buildAiTier = 1f;
|
||||
|
||||
/** Enables "RTS" unit AI. */
|
||||
public boolean rtsAi;
|
||||
/** Minimum size of attack squads. */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user