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:
Anuken
2024-01-27 10:11:09 -05:00
551 changed files with 15915 additions and 9680 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -447,11 +447,11 @@ public class ErekirTechTree{
//nodeProduce(Liquids.gallium, () -> {});
});
});
nodeProduce(Items.surgeAlloy, () -> {
nodeProduce(Items.phaseFabric, () -> {
nodeProduce(Items.surgeAlloy, () -> {
nodeProduce(Items.phaseFabric, () -> {
});
});
});
});

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@ public class TechTree{
});
content.techNode = this;
content.techNodes.add(this);
all.add(this);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -98,6 +98,8 @@ public class MapObjectivesCanvas extends WidgetGroup{
public void clearObjectives(){
stopQuery();
tilemap.clearTiles();
tilemap.x = 0f;
tilemap.y = 0f;
}
protected void stopQuery(){

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ public class MoveEffectAbility extends Ability{
}
public MoveEffectAbility(){
display = false;
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,7 @@ public class LaserBulletType extends BulletType{
hittable = false;
absorbable = false;
removeAfterPierce = false;
delayFrags = true;
}
public LaserBulletType(){

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,4 +66,9 @@ abstract class EntityComp{
void afterRead(){
}
/** Called after *all* entities are read. */
void afterAllRead(){
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,10 @@ public interface UnitController{
}
default void afterRead(Unit unit){
}
default boolean isBeingControlled(Unit player){
return false;
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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