Merge branch 'master' into pr-readwrite
This commit is contained in:
@@ -29,6 +29,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
private long nextFrame;
|
||||
private long beginTime;
|
||||
private long lastTargetFps = -1;
|
||||
private boolean finished = false;
|
||||
private LoadRenderer loader;
|
||||
|
||||
@@ -68,6 +69,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
|
||||
});
|
||||
|
||||
UI.loadColors();
|
||||
batch = new SortedSpriteBatch();
|
||||
assets = new AssetManager();
|
||||
assets.setLoader(Texture.class, "." + mapExtension, new MapPreviewLoader());
|
||||
@@ -200,9 +202,12 @@ 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;
|
||||
|
||||
if(limitFps){
|
||||
lastTargetFps = targetfps;
|
||||
|
||||
if(limitFps && !changed){
|
||||
nextFrame += (1000 * 1000000) / targetfps;
|
||||
}else{
|
||||
nextFrame = Time.nanos();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -257,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +296,10 @@ public class ControlPathfinder{
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -336,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;
|
||||
@@ -375,7 +389,7 @@ 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;
|
||||
@@ -397,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);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ public class Pathfinder implements Runnable{
|
||||
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;
|
||||
|
||||
@@ -4,39 +4,21 @@ 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. */
|
||||
@@ -47,16 +29,25 @@ 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);
|
||||
}
|
||||
@@ -66,11 +57,58 @@ public class UnitCommand{
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||
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;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
205
core/src/mindustry/ai/UnitGroup.java
Normal file
205
core/src/mindustry/ai/UnitGroup.java
Normal file
@@ -0,0 +1,205 @@
|
||||
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 long lastSpeedUpdate = -1;
|
||||
public float minSpeed = 999999f;
|
||||
|
||||
public void updateMinSpeed(){
|
||||
if(lastSpeedUpdate == Vars.state.updateId) return;
|
||||
|
||||
lastSpeedUpdate = Vars.state.updateId;
|
||||
|
||||
for(Unit unit : units){
|
||||
//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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
updateMinSpeed();
|
||||
|
||||
//run on new thread to prevent stutter
|
||||
Vars.mainExecutor.submit(() -> {
|
||||
//unused space between circles that needs to be reached for compression to end
|
||||
float maxSpaceUsage = 0.7f;
|
||||
boolean compress = true;
|
||||
|
||||
int compressionIterations = 0;
|
||||
int physicsIterations = 0;
|
||||
int totalIterations = 0;
|
||||
int maxPhysicsIterations = Math.min(1 + (int)(Math.pow(units.size, 0.65) / 10), 6);
|
||||
|
||||
//yep, new allocations, because this is a new thread.
|
||||
IntQuadTree tree = new IntQuadTree(new Rect(0f, 0f, Vars.world.unitWidth(), Vars.world.unitHeight()),
|
||||
(index, hitbox) -> hitbox.setCentered(positions[index * 2], positions[index * 2 + 1], units.get(index).hitSize));
|
||||
IntSeq tmpseq = new IntSeq();
|
||||
Vec2 v1 = new Vec2();
|
||||
Vec2 v2 = new Vec2();
|
||||
|
||||
//this algorithm basically squeezes all the circle colliders together, then proceeds to simulate physics to push them apart across several iterations.
|
||||
//it's rather slow, but shouldn't be too much of an issue when run in a different thread
|
||||
while(totalIterations++ < 40 && physicsIterations < maxPhysicsIterations){
|
||||
float spaceUsed = 0f;
|
||||
|
||||
if(compress){
|
||||
compressionIterations ++;
|
||||
|
||||
float maxDst = 1f, totalArea = 0f;
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
v1.set(positions[a * 2], positions[a * 2 + 1]).lerp(v2.set(0f, 0f), 0.3f);
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
|
||||
float rad = units.get(a).hitSize/2f;
|
||||
|
||||
maxDst = Math.max(maxDst, v1.dst(0f, 0f) + rad);
|
||||
totalArea += Mathf.PI * rad * rad;
|
||||
}
|
||||
|
||||
//total area of bounding circle
|
||||
float boundingArea = Mathf.PI * maxDst * maxDst;
|
||||
spaceUsed = totalArea / boundingArea;
|
||||
|
||||
//ex: 60% (0.6) of the total area is used, this will not be enough to satisfy a maxSpaceUsage of 70% (0.7)
|
||||
compress = spaceUsed <= maxSpaceUsage && compressionIterations < 20;
|
||||
}
|
||||
|
||||
//uncompress units
|
||||
if(!compress || spaceUsed > 0.5f){
|
||||
physicsIterations++;
|
||||
|
||||
tree.clear();
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
tree.insert(a);
|
||||
}
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
Unit unit = units.get(a);
|
||||
float x = positions[a * 2], y = positions[a * 2 + 1], radius = unit.hitSize/2f;
|
||||
|
||||
tmpseq.clear();
|
||||
tree.intersect(x - radius, y - radius, radius * 2f, radius * 2f, tmpseq);
|
||||
for(int res = 0; res < tmpseq.size; res ++){
|
||||
int b = tmpseq.items[res];
|
||||
|
||||
//simulate collision physics
|
||||
if(a != b){
|
||||
float ox = positions[b * 2], oy = positions[b * 2 + 1];
|
||||
Unit other = units.get(b);
|
||||
|
||||
float rs = (radius + other.hitSize/2f) * 1.2f;
|
||||
float dst = Mathf.dst(x, y, ox, oy);
|
||||
|
||||
if(dst < rs){
|
||||
v2.set(x - ox, y - oy).setLength(rs - dst);
|
||||
float mass1 = unit.hitSize, mass2 = other.hitSize;
|
||||
float ms = mass1 + mass2;
|
||||
float m1 = mass2 / ms, m2 = mass1 / ms;
|
||||
float scl = 1f;
|
||||
|
||||
positions[a * 2] += v2.x * m1 * scl;
|
||||
positions[a * 2 + 1] += v2.y * m1 * scl;
|
||||
|
||||
positions[b * 2] -= v2.x * m2 * scl;
|
||||
positions[b * 2 + 1] -= v2.y * m2 * scl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalPositions = positions.clone();
|
||||
|
||||
//raycast from the destination to the offset to make sure it's reachable
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
updateRaycast(a, dest, v1);
|
||||
}
|
||||
|
||||
valid = true;
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> {
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
float x = positions[i * 2], y = positions[i * 2 + 1];
|
||||
|
||||
Fx.placeBlock.at(x + dest.x, y + dest.y, 1f, Color.green);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateRaycast(int index, Vec2 dest){
|
||||
updateRaycast(index, dest, Tmp.v1);
|
||||
}
|
||||
|
||||
private void updateRaycast(int index, Vec2 dest, Vec2 v1){
|
||||
if(collisionLayer != PhysicsProcess.layerFlying){
|
||||
|
||||
//coordinates in world space
|
||||
float
|
||||
x = originalPositions[index * 2] + dest.x,
|
||||
y = originalPositions[index * 2 + 1] + dest.y;
|
||||
|
||||
Unit unit = units.get(index);
|
||||
|
||||
PathCost cost = unit.type.pathCost;
|
||||
int res = ControlPathfinder.raycastFast(unit.team.id, cost, World.toTile(dest.x), World.toTile(dest.y), World.toTile(x), World.toTile(y));
|
||||
|
||||
//collision found, make th destination the point right before the collision
|
||||
if(res != 0){
|
||||
v1.set(Point2.x(res) * Vars.tilesize - dest.x, Point2.y(res) * Vars.tilesize - dest.y);
|
||||
v1.setLength(Math.max(v1.len() - Vars.tilesize - 4f, 0));
|
||||
positions[index * 2] = v1.x;
|
||||
positions[index * 2 + 1] = v1.y;
|
||||
}
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> Fx.debugLine.at(unit.x, unit.y, 0f, Color.green, new Vec2[]{new Vec2(dest.x, dest.y), new Vec2(x, y)}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
core/src/mindustry/ai/UnitStance.java
Normal file
61
core/src/mindustry/ai/UnitStance.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
|
||||
public class UnitStance extends MappableContent{
|
||||
/** @deprecated now a content type, use the methods in Vars.content instead */
|
||||
@Deprecated
|
||||
public static final Seq<UnitStance> all = new Seq<>();
|
||||
|
||||
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
|
||||
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
/** Key to press for this stance. */
|
||||
public @Nullable Binding keybind = null;
|
||||
|
||||
public UnitStance(String name, String icon, Binding keybind){
|
||||
super(name);
|
||||
this.icon = icon;
|
||||
this.keybind = keybind;
|
||||
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("stance." + name);
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(){
|
||||
return Icon.icons.get(icon, Icon.cancel);
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.unitStance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "UnitStance:" + name;
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
stop = new UnitStance("stop", "cancel", Binding.cancel_orders);
|
||||
shoot = new UnitStance("shoot", "commandAttack", Binding.unit_stance_shoot);
|
||||
holdFire = new UnitStance("holdfire", "none", Binding.unit_stance_hold_fire);
|
||||
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unit_stance_pursue_target);
|
||||
patrol = new UnitStance("patrol", "refresh", Binding.unit_stance_patrol);
|
||||
ram = new UnitStance("ram", "rightOpen", Binding.unit_stance_ram);
|
||||
}
|
||||
}
|
||||
@@ -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,58 @@ 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(group != null){
|
||||
group.updateMinSpeed();
|
||||
}
|
||||
|
||||
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 +189,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 +201,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 +210,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 +278,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 +290,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 +311,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 +412,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 +421,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;
|
||||
|
||||
@@ -2,6 +2,8 @@ package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
@@ -29,6 +31,11 @@ public class MissileAI extends AIController{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retarget(){
|
||||
//more frequent retarget due to high speed. TODO won't this lag?
|
||||
|
||||
@@ -10,11 +10,11 @@ import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class PhysicsProcess implements AsyncProcess{
|
||||
private static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
public static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
|
||||
private PhysicsWorld physics;
|
||||
private Seq<PhysicRef> refs = new Seq<>(false);
|
||||
@@ -58,9 +58,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
//save last position
|
||||
PhysicRef ref = entity.physref;
|
||||
|
||||
ref.body.layer =
|
||||
entity.type.allowLegStep && entity.type.legPhysicsLayer ? layerLegs :
|
||||
entity.isGrounded() ? layerGround : layerFlying;
|
||||
ref.body.layer = entity.collisionLayer();
|
||||
ref.x = entity.x;
|
||||
ref.y = entity.y;
|
||||
ref.body.local = local || entity.isLocal();
|
||||
|
||||
@@ -17,7 +17,7 @@ import static mindustry.Vars.*;
|
||||
|
||||
/** Controls playback of multiple audio tracks.*/
|
||||
public class SoundControl{
|
||||
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.6f, musicWaveChance = 0.46f;
|
||||
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.8f, musicWaveChance = 0.46f;
|
||||
|
||||
/** normal, ambient music, plays at any time */
|
||||
public Seq<Music> ambientMusic = Seq.with();
|
||||
@@ -28,6 +28,7 @@ public class SoundControl{
|
||||
|
||||
protected Music lastRandomPlayed;
|
||||
protected Interval timer = new Interval(4);
|
||||
protected long lastPlayed;
|
||||
protected @Nullable Music current;
|
||||
protected float fade;
|
||||
protected boolean silenced;
|
||||
@@ -55,6 +56,10 @@ public class SoundControl{
|
||||
}));
|
||||
|
||||
setupFilters();
|
||||
|
||||
Events.on(ResetEvent.class, e -> {
|
||||
lastPlayed = Time.millis();
|
||||
});
|
||||
}
|
||||
|
||||
protected void setupFilters(){
|
||||
@@ -146,7 +151,7 @@ public class SoundControl{
|
||||
if(state.isMenu()){
|
||||
silenced = false;
|
||||
if(ui.planet.isShown()){
|
||||
play(Musics.launch);
|
||||
play(ui.planet.state.planet.launchMusic);
|
||||
}else if(ui.editor.isShown()){
|
||||
play(Musics.editor);
|
||||
}else{
|
||||
@@ -160,9 +165,10 @@ public class SoundControl{
|
||||
silence();
|
||||
|
||||
//play music at intervals
|
||||
if(timer.get(musicInterval)){
|
||||
if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
|
||||
//chance to play it per interval
|
||||
if(Mathf.chance(musicChance)){
|
||||
lastPlayed = Time.millis();
|
||||
playRandom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public class Blocks{
|
||||
//logic
|
||||
message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank,
|
||||
canvas, reinforcedMessage,
|
||||
worldProcessor, worldCell, worldMessage,
|
||||
worldProcessor, worldCell, worldMessage, worldSwitch,
|
||||
|
||||
//campaign
|
||||
launchPad, interplanetaryAccelerator
|
||||
@@ -570,7 +570,7 @@ public class Blocks{
|
||||
snowWall = new StaticWall("snow-wall");
|
||||
|
||||
duneWall = new StaticWall("dune-wall"){{
|
||||
basalt.asFloor().wall = darksandWater.asFloor().wall = darksandTaintedWater.asFloor().wall = this;
|
||||
hotrock.asFloor().wall = magmarock.asFloor().wall = basalt.asFloor().wall = darksandWater.asFloor().wall = darksandTaintedWater.asFloor().wall = this;
|
||||
attributes.set(Attribute.sand, 2f);
|
||||
}};
|
||||
|
||||
@@ -2408,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"){{
|
||||
@@ -2433,6 +2433,7 @@ public class Blocks{
|
||||
itemDuration = 140f;
|
||||
ambientSound = Sounds.pulse;
|
||||
ambientSoundVolume = 0.07f;
|
||||
liquidCapacity = 60f;
|
||||
|
||||
consumePower(25f);
|
||||
consumeItem(Items.blastCompound);
|
||||
@@ -3849,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;
|
||||
}}
|
||||
@@ -3965,6 +3969,7 @@ public class Blocks{
|
||||
hitColor = Pal.meltdownHit;
|
||||
status = StatusEffects.melting;
|
||||
drawSize = 420f;
|
||||
timescaleDamage = true;
|
||||
|
||||
incendChance = 0.4f;
|
||||
incendSpread = 5f;
|
||||
@@ -4562,6 +4567,7 @@ public class Blocks{
|
||||
loopSoundVolume = 0.6f;
|
||||
deathSound = Sounds.largeExplosion;
|
||||
targetAir = false;
|
||||
targetUnderBlocks = false;
|
||||
|
||||
fogRadius = 6f;
|
||||
|
||||
@@ -4574,7 +4580,7 @@ public class Blocks{
|
||||
deathExplosionEffect = Fx.massiveExplosion;
|
||||
shootOnDeath = true;
|
||||
shake = 10f;
|
||||
bullet = new ExplosionBulletType(700f, 65f){{
|
||||
bullet = new ExplosionBulletType(1500f, 65f){{
|
||||
hitColor = Pal.redLight;
|
||||
shootEffect = new MultiEffect(Fx.massiveExplosion, Fx.scatheExplosion, Fx.scatheLight, new WaveEffect(){{
|
||||
lifetime = 10f;
|
||||
@@ -4583,7 +4589,7 @@ public class Blocks{
|
||||
}});
|
||||
|
||||
collidesAir = false;
|
||||
buildingDamageMultiplier = 0.3f;
|
||||
buildingDamageMultiplier = 0.25f;
|
||||
|
||||
ammoMultiplier = 1f;
|
||||
fragLifeMin = 0.1f;
|
||||
@@ -4598,7 +4604,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;
|
||||
@@ -5924,7 +5930,7 @@ public class Blocks{
|
||||
forceDark = true;
|
||||
privileged = true;
|
||||
size = 1;
|
||||
maxInstructionsPerTick = 500;
|
||||
maxInstructionsPerTick = 1000;
|
||||
range = Float.MAX_VALUE;
|
||||
}};
|
||||
|
||||
@@ -5944,6 +5950,13 @@ public class Blocks{
|
||||
privileged = true;
|
||||
}};
|
||||
|
||||
worldSwitch = new SwitchBlock("world-switch"){{
|
||||
requirements(Category.logic, BuildVisibility.editorOnly, with());
|
||||
|
||||
targetable = false;
|
||||
privileged = true;
|
||||
}};
|
||||
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,11 +447,11 @@ public class ErekirTechTree{
|
||||
|
||||
//nodeProduce(Liquids.gallium, () -> {});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Items.surgeAlloy, () -> {
|
||||
nodeProduce(Items.phaseFabric, () -> {
|
||||
nodeProduce(Items.surgeAlloy, () -> {
|
||||
nodeProduce(Items.phaseFabric, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1710,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) -> {
|
||||
@@ -1729,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),
|
||||
|
||||
@@ -2445,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);
|
||||
@@ -2558,5 +2578,23 @@ public class Fx{
|
||||
|
||||
stroke(data.region.height * scl);
|
||||
line(data.region, data.a.x + ox, data.a.y + oy, data.b.x + ox, data.b.y + oy, false);
|
||||
}).layer(Layer.groundUnit + 5f);
|
||||
}).layer(Layer.groundUnit + 5f),
|
||||
|
||||
debugLine = new Effect(90f, 1000000000000f, e -> {
|
||||
if(!(e.data instanceof Vec2[] vec)) return;
|
||||
|
||||
Draw.color(e.color);
|
||||
Lines.stroke(1f);
|
||||
|
||||
if(vec.length == 2){
|
||||
Lines.line(vec[0].x, vec[0].y, vec[1].x, vec[1].y);
|
||||
}else{
|
||||
Lines.beginLine();
|
||||
for(Vec2 v : vec)
|
||||
Lines.linePoint(v.x, v.y);
|
||||
Lines.endLine();
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class Liquids{
|
||||
capPuddles = false;
|
||||
spreadTarget = Liquids.water;
|
||||
moveThroughBlocks = true;
|
||||
incinerable = true;
|
||||
incinerable = false;
|
||||
blockReactive = false;
|
||||
canStayOn.addAll(water, oil, cryofluid);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import mindustry.type.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class StatusEffects{
|
||||
public static StatusEffect none, burning, freezing, unmoving, slow, fast, 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(){
|
||||
|
||||
@@ -64,12 +64,12 @@ public class StatusEffects{
|
||||
init(() -> opposite(fast));
|
||||
}};
|
||||
|
||||
fast = new StatusEffect("fast"){{
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
fast = new StatusEffect("fast"){{
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
|
||||
init(() -> opposite(slow));
|
||||
}};
|
||||
init(() -> opposite(slow));
|
||||
}};
|
||||
|
||||
wet = new StatusEffect("wet"){{
|
||||
color = Color.royal;
|
||||
@@ -80,10 +80,8 @@ public class StatusEffects{
|
||||
|
||||
init(() -> {
|
||||
affinity(shocked, (unit, result, time) -> {
|
||||
float pierceFraction = 0.3f;
|
||||
unit.damage(transitionDamage);
|
||||
|
||||
unit.damagePierce(transitionDamage * pierceFraction);
|
||||
unit.damage(transitionDamage * (1f - pierceFraction));
|
||||
if(unit.team == state.rules.waveTeam){
|
||||
Events.fire(Trigger.shock);
|
||||
}
|
||||
@@ -205,5 +203,11 @@ public class StatusEffects{
|
||||
invincible = new StatusEffect("invincible"){{
|
||||
healthMultiplier = Float.POSITIVE_INFINITY;
|
||||
}};
|
||||
|
||||
dynamic = new StatusEffect("dynamic"){{
|
||||
show = false;
|
||||
dynamic = true;
|
||||
permanent = true;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,6 +2153,7 @@ public class UnitTypes{
|
||||
trailScl = 3.2f;
|
||||
|
||||
buildSpeed = 3f;
|
||||
rotateToBuilding = false;
|
||||
|
||||
abilities.add(new EnergyFieldAbility(40f, 65f, 180f){{
|
||||
statusDuration = 60f * 6f;
|
||||
@@ -2192,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){
|
||||
@@ -2243,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;
|
||||
|
||||
@@ -4051,6 +4063,8 @@ public class UnitTypes{
|
||||
isEnemy = false;
|
||||
envDisabled = 0;
|
||||
|
||||
range = 60f;
|
||||
faceTarget = true;
|
||||
targetPriority = -2;
|
||||
lowAltitude = false;
|
||||
mineWalls = true;
|
||||
@@ -4115,8 +4129,10 @@ public class UnitTypes{
|
||||
isEnemy = false;
|
||||
envDisabled = 0;
|
||||
|
||||
range = 60f;
|
||||
targetPriority = -2;
|
||||
lowAltitude = false;
|
||||
faceTarget = true;
|
||||
mineWalls = true;
|
||||
mineFloor = false;
|
||||
mineHardnessScaling = false;
|
||||
@@ -4192,6 +4208,8 @@ public class UnitTypes{
|
||||
isEnemy = false;
|
||||
envDisabled = 0;
|
||||
|
||||
range = 65f;
|
||||
faceTarget = true;
|
||||
targetPriority = -2;
|
||||
lowAltitude = false;
|
||||
mineWalls = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
@@ -40,6 +41,8 @@ public class ContentLoader{
|
||||
|
||||
/** Creates all base types. */
|
||||
public void createBaseContent(){
|
||||
UnitCommand.loadAll();
|
||||
UnitStance.loadAll();
|
||||
TeamEntries.load();
|
||||
Items.load();
|
||||
StatusEffects.load();
|
||||
@@ -310,4 +313,28 @@ public class ContentLoader{
|
||||
public Planet planet(String name){
|
||||
return getByName(ContentType.planet, name);
|
||||
}
|
||||
|
||||
public Seq<UnitStance> unitStances(){
|
||||
return getBy(ContentType.unitStance);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(int id){
|
||||
return getByID(ContentType.unitStance, id);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(String name){
|
||||
return getByName(ContentType.unitStance, name);
|
||||
}
|
||||
|
||||
public Seq<UnitCommand> unitCommands(){
|
||||
return getBy(ContentType.unitCommand);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(int id){
|
||||
return getByID(ContentType.unitCommand, id);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(String name){
|
||||
return getByName(ContentType.unitCommand, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
private Interval timer = new Interval(2);
|
||||
private boolean hiscore = false;
|
||||
private boolean wasPaused = false;
|
||||
private boolean wasPaused = false, backgroundPaused = false;
|
||||
private Seq<Building> toBePlaced = new Seq<>(false);
|
||||
|
||||
public Control(){
|
||||
@@ -332,6 +332,13 @@ public class Control implements ApplicationListener, Loadable{
|
||||
void createPlayer(){
|
||||
player = Player.create();
|
||||
player.name = Core.settings.getString("name");
|
||||
|
||||
String locale = Core.settings.getString("locale");
|
||||
if(locale.equals("default")){
|
||||
locale = Locale.getDefault().toString();
|
||||
}
|
||||
player.locale = locale;
|
||||
|
||||
player.color.set(Core.settings.getInt("color-0"));
|
||||
|
||||
if(mobile){
|
||||
@@ -551,6 +558,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
@Override
|
||||
public void pause(){
|
||||
if(settings.getBool("backgroundpause", true) && !net.active()){
|
||||
backgroundPaused = true;
|
||||
wasPaused = state.is(State.paused);
|
||||
if(state.is(State.playing)) state.set(State.paused);
|
||||
}
|
||||
@@ -561,6 +569,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
if(state.is(State.paused) && !wasPaused && settings.getBool("backgroundpause", true) && !net.active()){
|
||||
state.set(State.playing);
|
||||
}
|
||||
backgroundPaused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -652,6 +661,10 @@ public class Control implements ApplicationListener, Loadable{
|
||||
core.items.each((i, a) -> i.unlock());
|
||||
}
|
||||
|
||||
if(backgroundPaused && settings.getBool("backgroundpause") && !net.active()){
|
||||
state.set(State.paused);
|
||||
}
|
||||
|
||||
//cannot launch while paused
|
||||
if(state.isPaused() && renderer.isCutscene()){
|
||||
state.set(State.playing);
|
||||
|
||||
@@ -32,6 +32,10 @@ public class GameState{
|
||||
public Rules rules = new Rules();
|
||||
/** Statistics for this save/game. Displayed after game over. */
|
||||
public GameStats stats = new GameStats();
|
||||
/** Markers not linked to objectives. Controlled by world processors. */
|
||||
public MapMarkers markers = new MapMarkers();
|
||||
/** Locale-specific string bundles of current map */
|
||||
public MapLocales mapLocales = new MapLocales();
|
||||
/** Global attributes of the environment, calculated by weather. */
|
||||
public Attributes envAttrs = new Attributes();
|
||||
/** Team data. Gets reset every new game. */
|
||||
|
||||
@@ -342,16 +342,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -696,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{
|
||||
@@ -778,7 +777,6 @@ public class NetServer implements ApplicationListener{
|
||||
}else{
|
||||
NetClient.traceInfo(other, info);
|
||||
}
|
||||
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());
|
||||
}
|
||||
case switchTeam -> {
|
||||
if(params instanceof Team team){
|
||||
|
||||
@@ -46,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, drawLight = 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;
|
||||
@@ -181,11 +181,14 @@ public class Renderer implements ApplicationListener{
|
||||
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()){
|
||||
@@ -225,7 +228,7 @@ public class Renderer implements ApplicationListener{
|
||||
shakeIntensity = 0f;
|
||||
}
|
||||
|
||||
if(pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
pixelator.drawPixelate();
|
||||
}else{
|
||||
draw();
|
||||
@@ -316,7 +319,7 @@ public class Renderer implements ApplicationListener{
|
||||
Events.fire(Trigger.draw);
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
if(pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
pixelator.register();
|
||||
}
|
||||
|
||||
@@ -369,6 +372,25 @@ public class Renderer implements ApplicationListener{
|
||||
});
|
||||
}
|
||||
|
||||
float scaleFactor = 4f / renderer.getDisplayScale();
|
||||
|
||||
//draw objective markers
|
||||
state.rules.objectives.eachRunning(obj -> {
|
||||
for(var marker : obj.markers){
|
||||
if(marker.world){
|
||||
marker.draw(marker.autoscale ? scaleFactor : 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for(var marker : state.markers){
|
||||
if(marker.world){
|
||||
marker.draw(marker.autoscale ? scaleFactor : 1);
|
||||
}
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
|
||||
Draw.draw(Layer.overlayUI, overlays::drawTop);
|
||||
if(state.rules.fog) Draw.draw(Layer.fogOfWar, fog::drawFog);
|
||||
Draw.draw(Layer.space, this::drawLanding);
|
||||
|
||||
@@ -78,7 +78,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
public IntMap<Dialog> followUpMenus;
|
||||
|
||||
public Cursor drillCursor, unloadCursor, targetCursor;
|
||||
public Cursor drillCursor, unloadCursor, targetCursor, repairCursor;
|
||||
|
||||
private @Nullable Element lastAnnouncement;
|
||||
|
||||
@@ -101,8 +101,6 @@ 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);
|
||||
@@ -128,6 +126,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);
|
||||
@@ -139,6 +140,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
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
|
||||
@@ -270,8 +272,15 @@ 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.startsWith("@") ? Core.bundle.get(text.substring(1)) : text);
|
||||
var empty = allowEmpty;
|
||||
Core.input.getTextInput(new TextInput(){{
|
||||
this.title = (titleText.startsWith("@") ? Core.bundle.get(titleText.substring(1)) : titleText);
|
||||
this.text = def;
|
||||
@@ -279,7 +288,8 @@ public class UI implements ApplicationListener, Loadable{
|
||||
this.maxLength = textLength;
|
||||
this.accepted = confirmed;
|
||||
this.canceled = closed;
|
||||
this.allowEmpty = false;
|
||||
this.allowEmpty = empty;
|
||||
this.message = description;
|
||||
}});
|
||||
}else{
|
||||
new Dialog(titleText){{
|
||||
@@ -296,11 +306,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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -301,6 +301,14 @@ public class MapEditor{
|
||||
if(previous.in(px, py)){
|
||||
tiles.set(x, y, previous.getn(px, py));
|
||||
Tile tile = tiles.getn(x, y);
|
||||
|
||||
Object config = null;
|
||||
|
||||
//fetch the old config first, configs can be relative to block position (tileX/tileY) before those are reassigned
|
||||
if(tile.build != null && tile.isCenter()){
|
||||
config = tile.build.config();
|
||||
}
|
||||
|
||||
tile.x = (short)x;
|
||||
tile.y = (short)y;
|
||||
|
||||
@@ -309,9 +317,12 @@ public class MapEditor{
|
||||
tile.build.y = y * tilesize + tile.block().offset;
|
||||
|
||||
//shift links to account for map resize
|
||||
Object config = tile.build.config();
|
||||
if(config != null){
|
||||
Object out = BuildPlan.pointConfig(tile.block(), config, p -> p.sub(offsetX, offsetY));
|
||||
Object out = BuildPlan.pointConfig(tile.block(), config, p -> {
|
||||
if(!tile.build.block.ignoreResizeConfig){
|
||||
p.sub(offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
if(out != config){
|
||||
tile.build.configureAny(out);
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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();
|
||||
|
||||
775
core/src/mindustry/editor/MapLocalesDialog.java
Normal file
775
core/src/mindustry/editor/MapLocalesDialog.java
Normal file
@@ -0,0 +1,775 @@
|
||||
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();
|
||||
|
||||
resized(this::buildMain);
|
||||
|
||||
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(cardWidth, 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))).width(200f).minHeight(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(250f);
|
||||
}
|
||||
|
||||
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, (int)((Core.graphics.getWidth() / Scl.scl() - 410f) / cardWidth) - 1);
|
||||
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 = Math.max(1, (int)((Core.graphics.getWidth() / Scl.scl() - 100f) / cardWidth));
|
||||
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()){
|
||||
// Convert \n in plain text to \\n, then convert newlines to \n
|
||||
data.append(prop.key).append(" = ").append(prop.value
|
||||
.replace("\\n", "\\\\n").replace("\n", "\\n")).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()){
|
||||
// Convert \n in file to newlines in text, then revert newlines with escape characters
|
||||
bundles.get(currentLocale).put(line.substring(0, sepIndex), line.substring(sepIndex + 3)
|
||||
.replace("\\n", "\n").replace("\\\n", "\\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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){
|
||||
// Convert \n in file to newlines in text, then revert newlines with escape characters
|
||||
map.put(line.substring(0, sepIndex), line.substring(sepIndex + 3)
|
||||
.replace("\\n", "\n").replace("\\\n", "\\n"));
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private enum PropertyStatus{
|
||||
correct,
|
||||
missing,
|
||||
same
|
||||
}
|
||||
}
|
||||
@@ -246,6 +246,38 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
show();
|
||||
}});
|
||||
|
||||
setInterpreter(Vertices.class, float[].class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
|
||||
float[] data = get.get();
|
||||
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> {
|
||||
t.left().defaults().left();
|
||||
|
||||
String[] names = {"x", "y", "color", "u", "v"};
|
||||
int stride = 6;
|
||||
int vertices = data.length / stride;
|
||||
|
||||
for(int i = 0; i < vertices; i++){
|
||||
int offset = i * stride;
|
||||
|
||||
t.table(row -> {
|
||||
for(int j = 0; j < names.length; j++){
|
||||
int index = offset + j;
|
||||
|
||||
if("color".equals(names[j])) {
|
||||
getInterpreter(Color.class).build(row, names[j], new TypeInfo(Color.class), null, null, null, () -> new Color().abgr8888(data[index]), value -> data[index] = value.toFloatBits());
|
||||
}else{
|
||||
float scale = j <= 1 ? tilesize : 1;
|
||||
getInterpreter(float.class).build(row, names[j], new TypeInfo(float.class), null, null, null, () -> data[index] / scale, value -> data[index] = value * scale);
|
||||
}
|
||||
|
||||
row.add().pad(4);
|
||||
}
|
||||
}).row();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Types that use the default interpreter. It would be nice if all types could use it, but I don't know how to reliably prevent classes like [? extends Content] from using it.
|
||||
for(var obj : MapObjectives.allObjectiveTypes) setInterpreter(obj.get().getClass(), defaultInterpreter());
|
||||
for(var mark : MapObjectives.allMarkerTypes) setInterpreter(mark.get().getClass(), defaultInterpreter());
|
||||
@@ -290,10 +322,12 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f);
|
||||
}
|
||||
|
||||
t.button(Icon.add, Styles.emptyi, () -> getProvider(type.element.raw).get(type.element, res -> {
|
||||
arr.add(res);
|
||||
rebuild[0].run();
|
||||
})).fill();
|
||||
if(!field.isAnnotationPresent(Immutable.class)) {
|
||||
t.button(Icon.add, Styles.emptyi, () -> getProvider(type.element.raw).get(type.element, res -> {
|
||||
arr.add(res);
|
||||
rebuild[0].run();
|
||||
})).fill();
|
||||
}
|
||||
}).growX().height(46f).pad(0f, -10f, 0f, -10f).get();
|
||||
|
||||
main.row().table(Tex.button, t -> rebuild[0] = () -> {
|
||||
@@ -312,10 +346,10 @@ public class MapObjectivesDialog extends BaseDialog{
|
||||
|
||||
getInterpreter((Class<Object>)arr.get(index).getClass()).build(
|
||||
t, "", new TypeInfo(arr.get(index).getClass()),
|
||||
field, () -> {
|
||||
field, field == null || !field.isAnnotationPresent(Immutable.class) ? () -> {
|
||||
arr.remove(index);
|
||||
rebuild[0].run();
|
||||
}, field == null || !field.isAnnotationPresent(Unordered.class) ? in -> {
|
||||
} : null, field == null || !field.isAnnotationPresent(Unordered.class) ? in -> {
|
||||
if(in && index > 0){
|
||||
arr.swap(index, index - 1);
|
||||
rebuild[0].run();
|
||||
|
||||
@@ -139,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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -145,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
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.EventType.*;
|
||||
@@ -14,13 +12,12 @@ import static mindustry.Vars.*;
|
||||
|
||||
public class Fires{
|
||||
private static final float baseLifetime = 1000f;
|
||||
private static final IntMap<Fire> map = new IntMap<>();
|
||||
|
||||
/** Start a fire on the tile. If there already is a fire there, refreshes its lifetime. */
|
||||
public static void create(Tile tile){
|
||||
if(net.client() || tile == null || !state.rules.fire || !state.rules.hasEnv(Env.oxygen)) return; //not clientside.
|
||||
|
||||
Fire fire = map.get(tile.pos());
|
||||
Fire fire = get(tile);
|
||||
|
||||
if(fire == null){
|
||||
fire = Fire.create();
|
||||
@@ -28,48 +25,58 @@ public class Fires{
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.set(tile.worldx(), tile.worldy());
|
||||
fire.add();
|
||||
map.put(tile.pos(), fire);
|
||||
set(tile, fire);
|
||||
}else{
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.time = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public static Fire get(int x, int y){
|
||||
return map.get(Point2.pack(x, y));
|
||||
public static @Nullable Fire get(Tile tile){
|
||||
return tile == null ? null : world.tiles.getFire(tile.array());
|
||||
}
|
||||
|
||||
public static @Nullable Fire get(int x, int y){
|
||||
return Structs.inBounds(x, y, world.width(), world.height()) ? world.tiles.getFire(world.packArray(x, y)) : null;
|
||||
}
|
||||
|
||||
private static void set(Tile tile, Fire fire){
|
||||
world.tiles.setFire(tile.array(), fire);
|
||||
}
|
||||
|
||||
public static boolean has(int x, int y){
|
||||
if(!Structs.inBounds(x, y, world.width(), world.height()) || !map.containsKey(Point2.pack(x, y))){
|
||||
if(!Structs.inBounds(x, y, world.width(), world.height())){
|
||||
return false;
|
||||
}
|
||||
Fire fire = map.get(Point2.pack(x, y));
|
||||
return fire.isAdded() && fire.fin() < 1f && fire.tile() != null && fire.tile().x == x && fire.tile().y == y;
|
||||
Fire fire = get(x, y);
|
||||
return fire != null && fire.isAdded() && fire.fin() < 1f && fire.tile != null && fire.tile.x == x && fire.tile.y == y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extinguish a fire by shortening its life. If there is no fire here, does nothing.
|
||||
*/
|
||||
public static void extinguish(Tile tile, float intensity){
|
||||
if(tile != null && map.containsKey(tile.pos())){
|
||||
Fire fire = map.get(tile.pos());
|
||||
fire.time(fire.time + intensity * Time.delta);
|
||||
Fx.steam.at(fire);
|
||||
if(fire.time >= fire.lifetime){
|
||||
Events.fire(Trigger.fireExtinguish);
|
||||
if(tile != null){
|
||||
Fire fire = get(tile);
|
||||
if(fire != null){
|
||||
fire.time(fire.time + intensity * Time.delta);
|
||||
Fx.steam.at(fire);
|
||||
if(fire.time >= fire.lifetime){
|
||||
Events.fire(Trigger.fireExtinguish);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(Tile tile){
|
||||
if(tile != null){
|
||||
map.remove(tile.pos());
|
||||
set(tile, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void register(Fire fire){
|
||||
if(fire.tile != null){
|
||||
map.put(fire.tile.pos(), fire);
|
||||
set(fire.tile, fire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
@@ -11,9 +10,9 @@ import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class Puddles{
|
||||
private static final IntMap<Puddle> map = new IntMap<>();
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Puddles{
|
||||
public static final float maxLiquid = 70f;
|
||||
|
||||
/** Deposits a Puddle between tile and source. */
|
||||
@@ -27,8 +26,8 @@ public class Puddles{
|
||||
}
|
||||
|
||||
/** Returns the Puddle on the specified tile. May return null. */
|
||||
public static Puddle get(Tile tile){
|
||||
return map.get(tile.pos());
|
||||
public static @Nullable Puddle get(Tile tile){
|
||||
return tile == null ? null : world.tiles.getPuddle(tile.array());
|
||||
}
|
||||
|
||||
public static void deposit(Tile tile, Tile source, Liquid liquid, float amount, boolean initial){
|
||||
@@ -57,7 +56,7 @@ public class Puddles{
|
||||
if(tile.floor().isLiquid && !canStayOn(liquid, tile.floor().liquidDrop)){
|
||||
reactPuddle(tile.floor().liquidDrop, liquid, amount, tile, ax, ay);
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
Puddle p = get(tile);
|
||||
|
||||
if(initial && p != null && p.lastRipple <= Time.time - 40f){
|
||||
Fx.ripple.at(ax, ay, 1f, tile.floor().liquidDrop.color);
|
||||
@@ -68,7 +67,7 @@ public class Puddles{
|
||||
|
||||
if(tile.floor().solid) return;
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
Puddle p = get(tile);
|
||||
if(p == null || p.liquid == null){
|
||||
if(!Vars.net.client()){
|
||||
//do not create puddles clientside as that destroys syncing
|
||||
@@ -77,7 +76,7 @@ public class Puddles{
|
||||
puddle.liquid = liquid;
|
||||
puddle.amount = amount;
|
||||
puddle.set(ax, ay);
|
||||
map.put(tile.pos(), puddle);
|
||||
register(puddle);
|
||||
puddle.add();
|
||||
}
|
||||
}else if(p.liquid == liquid){
|
||||
@@ -101,11 +100,11 @@ public class Puddles{
|
||||
public static void remove(Tile tile){
|
||||
if(tile == null) return;
|
||||
|
||||
map.remove(tile.pos());
|
||||
world.tiles.setPuddle(tile.array(), null);
|
||||
}
|
||||
|
||||
public static void register(Puddle puddle){
|
||||
map.put(puddle.tile().pos(), puddle);
|
||||
world.tiles.setPuddle(puddle.tile().array(), puddle);
|
||||
}
|
||||
|
||||
/** Reacts two liquids together at a location. */
|
||||
|
||||
@@ -20,22 +20,18 @@ public class Units{
|
||||
private static final Rect hitrect = new Rect();
|
||||
private static Unit result;
|
||||
private static float cdist, cpriority;
|
||||
private static boolean boolResult;
|
||||
private static int intResult;
|
||||
private static Building buildResult;
|
||||
|
||||
//prevents allocations in anyEntities
|
||||
private static boolean anyEntityGround;
|
||||
private static float aeX, aeY, aeW, aeH;
|
||||
private static final Cons<Unit> anyEntityLambda = unit -> {
|
||||
if(boolResult) return;
|
||||
private static final Boolf<Unit> anyEntityLambda = unit -> {
|
||||
if((unit.isGrounded() && !unit.type.allowLegStep) == anyEntityGround){
|
||||
unit.hitboxTile(hitrect);
|
||||
|
||||
if(hitrect.overlaps(aeX, aeY, aeW, aeH)){
|
||||
boolResult = true;
|
||||
}
|
||||
return hitrect.overlaps(aeX, aeY, aeW, aeH);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
@@ -93,7 +89,7 @@ public class Units{
|
||||
|
||||
/** @return whether a new instance of a unit of this team can be created. */
|
||||
public static boolean canCreate(Team team, UnitType type){
|
||||
return team.data().countType(type) < getCap(team) && !type.isBanned();
|
||||
return !type.useUnitCap || (team.data().countType(type) < getCap(team) && !type.isBanned());
|
||||
}
|
||||
|
||||
public static int getCap(Team team){
|
||||
@@ -112,7 +108,7 @@ public class Units{
|
||||
|
||||
/** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/
|
||||
public static boolean canInteract(Player player, Building tile){
|
||||
return player == null || tile == null || tile.interactable(player.team());
|
||||
return player == null || tile == null || tile.interactable(player.team()) || state.rules.editor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,31 +158,26 @@ public class Units{
|
||||
}
|
||||
|
||||
public static boolean anyEntities(float x, float y, float width, float height, boolean ground){
|
||||
boolResult = false;
|
||||
anyEntityGround = ground;
|
||||
aeX = x;
|
||||
aeY = y;
|
||||
aeW = width;
|
||||
aeH = height;
|
||||
|
||||
nearby(x, y, width, height, anyEntityLambda);
|
||||
return boolResult;
|
||||
return nearbyCheck(x, y, width, height, anyEntityLambda);
|
||||
}
|
||||
|
||||
/** Note that this checks the tile hitbox, not the standard hitbox. */
|
||||
public static boolean anyEntities(float x, float y, float width, float height, Boolf<Unit> check){
|
||||
boolResult = false;
|
||||
|
||||
nearby(x, y, width, height, unit -> {
|
||||
if(boolResult) return;
|
||||
return nearbyCheck(x, y, width, height, unit -> {
|
||||
if(check.get(unit)){
|
||||
unit.hitboxTile(hitrect);
|
||||
|
||||
if(hitrect.overlaps(x, y, width, height)){
|
||||
boolResult = true;
|
||||
}
|
||||
return hitrect.overlaps(x, y, width, height);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return boolResult;
|
||||
}
|
||||
|
||||
/** Returns the nearest damaged tile. */
|
||||
@@ -201,8 +192,18 @@ public class Units{
|
||||
|
||||
/** Returns the nearest enemy tile in a range. */
|
||||
public static Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
|
||||
return findEnemyTile(team, x, y, range, false, pred);
|
||||
}
|
||||
|
||||
/** Returns the nearest enemy tile in a range. */
|
||||
public static Building findEnemyTile(Team team, float x, float y, float range, boolean checkUnder, Boolf<Building> pred){
|
||||
if(team == Team.derelict) return null;
|
||||
|
||||
if(checkUnder){
|
||||
Building target = indexer.findEnemyTile(team, x, y, range, build -> !build.block.underBullets && pred.get(build));
|
||||
if(target != null) return target;
|
||||
}
|
||||
|
||||
return indexer.findEnemyTile(team, x, y, range, pred);
|
||||
}
|
||||
|
||||
@@ -223,7 +224,10 @@ public class Units{
|
||||
}
|
||||
});
|
||||
|
||||
return buildResult;
|
||||
var result = buildResult;
|
||||
buildResult = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Iterates through all buildings in a range. */
|
||||
@@ -249,7 +253,7 @@ public class Units{
|
||||
if(unit != null){
|
||||
return unit;
|
||||
}else{
|
||||
return findEnemyTile(team, x, y, range, tilePred);
|
||||
return findEnemyTile(team, x, y, range, true, tilePred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +265,7 @@ public class Units{
|
||||
if(unit != null){
|
||||
return unit;
|
||||
}else{
|
||||
return findEnemyTile(team, x, y, range, tilePred);
|
||||
return findEnemyTile(team, x, y, range, true, tilePred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +337,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 +355,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 +374,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){
|
||||
@@ -428,6 +432,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);
|
||||
|
||||
@@ -21,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)){
|
||||
|
||||
@@ -32,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();
|
||||
@@ -79,6 +79,7 @@ public class ShieldArcAbility extends Ability{
|
||||
|
||||
@Override
|
||||
public void update(Unit unit){
|
||||
|
||||
if(data < max){
|
||||
data += Time.delta * regen;
|
||||
}
|
||||
@@ -92,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);
|
||||
}
|
||||
@@ -105,7 +107,6 @@ public class ShieldArcAbility extends Ability{
|
||||
|
||||
@Override
|
||||
public void draw(Unit unit){
|
||||
|
||||
if(widthScale > 0.001f){
|
||||
Draw.z(Layer.shields);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
@@ -158,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. */
|
||||
@@ -170,6 +174,8 @@ public class BulletType extends Content implements Cloneable{
|
||||
public float fragVelocityMin = 0.2f, fragVelocityMax = 1f;
|
||||
/** Random range of frag lifetime as a multiplier. */
|
||||
public float fragLifeMin = 1f, fragLifeMax = 1f;
|
||||
/** Random offset of frag bullets from the parent bullet. */
|
||||
public float fragOffsetMin = 1f, fragOffsetMax = 7f;
|
||||
|
||||
/** Bullet that is created at a fixed interval. */
|
||||
public @Nullable BulletType intervalBullet;
|
||||
@@ -255,6 +261,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;
|
||||
@@ -380,10 +388,20 @@ public class BulletType extends Content implements Cloneable{
|
||||
boolean wasDead = entity instanceof Unit u && u.dead;
|
||||
|
||||
if(entity instanceof Healthc h){
|
||||
if(pierceArmor){
|
||||
h.damagePierce(b.damage);
|
||||
float damage = b.damage;
|
||||
float shield = entity instanceof Shieldc s ? Math.max(s.shield(), 0f) : 0f;
|
||||
if(maxDamageFraction > 0){
|
||||
float cap = h.maxHealth() * maxDamageFraction + shield;
|
||||
damage = Math.min(damage, cap);
|
||||
//cap health to effective health for handlePierce to handle it properly
|
||||
health = Math.min(health, cap);
|
||||
}else{
|
||||
h.damage(b.damage);
|
||||
health += shield;
|
||||
}
|
||||
if(pierceArmor){
|
||||
h.damagePierce(damage);
|
||||
}else{
|
||||
h.damage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +450,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);
|
||||
@@ -440,7 +462,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,7 +511,7 @@ public class BulletType extends Content implements Cloneable{
|
||||
public void createFrags(Bullet b, float x, float y){
|
||||
if(fragBullet != null && (fragOnAbsorb || !b.absorbed)){
|
||||
for(int i = 0; i < fragBullets; i++){
|
||||
float len = Mathf.random(1f, 7f);
|
||||
float len = Mathf.random(fragOffsetMin, fragOffsetMax);
|
||||
float a = b.rotation() + Mathf.range(fragRandomSpread / 2) + fragAngle + ((i - fragBullets/2) * fragSpread);
|
||||
fragBullet.create(b, x + Angles.trnsx(a, len), y + Angles.trnsy(a, len), a, Mathf.random(fragVelocityMin, fragVelocityMax), Mathf.random(fragLifeMin, fragLifeMax));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -79,7 +81,12 @@ public class ContinuousBulletType extends BulletType{
|
||||
}
|
||||
|
||||
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){
|
||||
|
||||
@@ -38,6 +38,7 @@ public class LaserBulletType extends BulletType{
|
||||
hittable = false;
|
||||
absorbable = false;
|
||||
removeAfterPierce = false;
|
||||
delayFrags = true;
|
||||
}
|
||||
|
||||
public LaserBulletType(){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -61,8 +61,12 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
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 || (plan.block != null && (plan.block.isOverlay() && plan.block == tile.overlay() || (plan.block.isFloor() && plan.block == tile.floor())))))){
|
||||
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();
|
||||
}
|
||||
@@ -82,9 +86,9 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
buildAlpha = Mathf.lerpDelta(buildAlpha, activelyBuilding() ? 1f : 0f, 0.15f);
|
||||
}
|
||||
|
||||
//validate regardless of whether building is enabled.
|
||||
validatePlans();
|
||||
|
||||
if(!updateBuilding || !canBuild()){
|
||||
validatePlans();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,19 +99,18 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
if(Float.isNaN(buildCounter) || Float.isInfinite(buildCounter)) buildCounter = 0f;
|
||||
buildCounter = Math.min(buildCounter, 10f);
|
||||
|
||||
boolean instant = state.rules.instantBuild && state.rules.infiniteResources;
|
||||
|
||||
//random attempt to fix a freeze that only occurs on Android
|
||||
int maxPerFrame = 10, count = 0;
|
||||
int maxPerFrame = instant ? plans.size : 10, count = 0;
|
||||
|
||||
while(buildCounter >= 1 && count++ < maxPerFrame){
|
||||
var core = core();
|
||||
|
||||
if((core == null && !infinite)) return;
|
||||
|
||||
while((buildCounter >= 1 || instant) && count++ < maxPerFrame && plans.size > 0){
|
||||
buildCounter -= 1f;
|
||||
|
||||
validatePlans();
|
||||
|
||||
var core = core();
|
||||
|
||||
//nothing to build.
|
||||
if(buildPlan() == null) return;
|
||||
|
||||
//find the next build plan
|
||||
if(plans.size > 1){
|
||||
int total = 0;
|
||||
@@ -159,7 +162,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
|
||||
}
|
||||
|
||||
//if there is no core to build with or no build entity, stop building!
|
||||
if((core == null && !infinite) || !(tile.build instanceof ConstructBuild entity)){
|
||||
if(!(tile.build instanceof ConstructBuild entity)){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
transient float healSuppressionTime = -1f;
|
||||
transient float lastHealTime = -120f * 10f;
|
||||
transient Color suppressColor = Pal.sapBullet;
|
||||
|
||||
private transient float lastDamageTime = -recentDamageTime;
|
||||
private transient float timeScale = 1f, timeScaleDuration;
|
||||
@@ -377,13 +378,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
float heat = 0f;
|
||||
|
||||
for(var edge : block.getEdges()){
|
||||
Building build = nearby(edge.x, edge.y);
|
||||
for(var build : proximity){
|
||||
if(build != null && build.team == team && build instanceof HeatBlock heater){
|
||||
//massive hack but I don't really care anymore
|
||||
if(heater instanceof HeatConductorBuild cond){
|
||||
cond.updateHeat();
|
||||
}
|
||||
|
||||
|
||||
boolean split = build.block instanceof HeatConductor cond && cond.splitHeat;
|
||||
// non-routers must face us, routers must face away - next to a redirector, they're forced to face away due to cycles anyway
|
||||
@@ -391,8 +388,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
|
||||
//if there's a cycle, ignore its heat
|
||||
if(!(build instanceof HeatConductorBuild hc && hc.cameFrom.contains(id()))){
|
||||
//x/y coordinate difference across point of contact
|
||||
float diff = (Math.min(Math.abs(build.x - x), Math.abs(build.y - y)) / tilesize);
|
||||
//number of points that this block had contact with
|
||||
int contactPoints = Math.min((int)(block.size/2f + build.block.size/2f - diff), Math.min(build.block.size, block.size));
|
||||
|
||||
//heat is distributed across building size
|
||||
float add = heater.heat() / build.block.size;
|
||||
float add = heater.heat() / build.block.size * contactPoints;
|
||||
if(split){
|
||||
//heat routers split heat across 3 surfaces
|
||||
add /= 3f;
|
||||
@@ -409,6 +411,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
cameFrom.addAll(hc.cameFrom);
|
||||
}
|
||||
}
|
||||
|
||||
//massive hack but I don't really care anymore
|
||||
if(heater instanceof HeatConductorBuild cond){
|
||||
cond.updateHeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,7 +439,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
|
||||
}
|
||||
|
||||
public void applyHealSuppression(float amount){
|
||||
applyHealSuppression(amount, Pal.sapBullet);
|
||||
}
|
||||
public void applyHealSuppression(float amount, Color suppressColor){
|
||||
healSuppressionTime = Math.max(healSuppressionTime, Time.time + amount);
|
||||
this.suppressColor = suppressColor;
|
||||
}
|
||||
|
||||
public boolean isHealSuppressed(){
|
||||
@@ -1236,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;
|
||||
@@ -1333,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;
|
||||
@@ -1913,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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
@Override
|
||||
public void update(){
|
||||
if(moving()){
|
||||
segmentRot = Angles.moveToward(segmentRot, rotation, type.segmentRotSpeed);
|
||||
segmentRot = Angles.moveToward(segmentRot, rotation, type.segmentRotSpeed * Time.delta);
|
||||
|
||||
int radius = (int)Math.max(0, hitSize / tilesize * 2f);
|
||||
int count = 0, solids = 0, deeps = 0;
|
||||
@@ -106,10 +106,10 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
|
||||
lastDeepFloor = null;
|
||||
}
|
||||
|
||||
lastCrawlSlowdown = Mathf.lerp(1f, type.crawlSlowdown, Mathf.clamp((float)solids / count / type.crawlSlowdownFrac));
|
||||
lastCrawlSlowdown = Mathf.lerpDelta(1f, type.crawlSlowdown, Mathf.clamp((float)solids / count / type.crawlSlowdownFrac));
|
||||
}
|
||||
segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot);
|
||||
|
||||
crawlTime += vel.len();
|
||||
crawlTime += vel.len() * Time.delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,9 @@ abstract class EntityComp{
|
||||
void afterRead(){
|
||||
|
||||
}
|
||||
|
||||
/** Called after *all* entities are read. */
|
||||
void afterAllRead(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,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){
|
||||
|
||||
@@ -11,7 +11,7 @@ import mindustry.type.*;
|
||||
|
||||
@Component
|
||||
abstract class ShieldComp implements Healthc, Posc{
|
||||
@Import float health, hitTime, x, y, healthMultiplier;
|
||||
@Import float health, hitTime, x, y, healthMultiplier, armorOverride;
|
||||
@Import boolean dead;
|
||||
@Import Team team;
|
||||
@Import UnitType type;
|
||||
@@ -27,7 +27,7 @@ abstract class ShieldComp implements Healthc, Posc{
|
||||
@Override
|
||||
public void damage(float amount){
|
||||
//apply armor and scaling effects
|
||||
rawDamage(Damage.applyArmor(amount, armor) / healthMultiplier / Vars.state.rules.unitHealth(team));
|
||||
rawDamage(Damage.applyArmor(amount, armorOverride >= 0f ? armorOverride : armor) / healthMultiplier / Vars.state.rules.unitHealth(team));
|
||||
}
|
||||
|
||||
@Replace
|
||||
|
||||
@@ -16,14 +16,16 @@ import static mindustry.Vars.*;
|
||||
|
||||
@Component
|
||||
abstract class StatusComp implements Posc, Flyingc{
|
||||
private Seq<StatusEntry> statuses = new Seq<>();
|
||||
private Seq<StatusEntry> statuses = new Seq<>(4);
|
||||
private transient Bits applied = new Bits(content.getBy(ContentType.status).size);
|
||||
|
||||
//these are considered read-only
|
||||
transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1;
|
||||
//note: armor is a special case; it is an override when >= 0, otherwise ignored
|
||||
transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1, armorOverride = -1f;
|
||||
transient boolean disarmed = false;
|
||||
|
||||
@Import UnitType type;
|
||||
@Import float maxHealth;
|
||||
|
||||
/** Apply a status effect for 1 tick (for permanent effects) **/
|
||||
void apply(StatusEffect effect){
|
||||
@@ -108,6 +110,62 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
return Tmp.c1.set(r / count, g / count, b / count, 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a dynamic status effect, with stat multipliers that can be customized.
|
||||
* @return the entry to write multipliers to. If the dynamic status was already applied, returns the previous entry.
|
||||
* */
|
||||
public StatusEntry applyDynamicStatus(){
|
||||
if(hasEffect(StatusEffects.dynamic)){
|
||||
StatusEntry entry = statuses.find(s -> s.effect.dynamic);
|
||||
if(entry != null) return entry;
|
||||
}
|
||||
|
||||
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
|
||||
entry.set(StatusEffects.dynamic, Float.POSITIVE_INFINITY);
|
||||
statuses.add(entry);
|
||||
entry.effect.applied(self(), entry.time, false);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override speed (in tiles/second). */
|
||||
public void statusSpeed(float speed){
|
||||
//type.speed should never be 0
|
||||
applyDynamicStatus().speedMultiplier = speed / (type.speed * 60f / tilesize);
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to change damage. */
|
||||
public void statusDamageMultiplier(float damageMultiplier){
|
||||
applyDynamicStatus().damageMultiplier = damageMultiplier;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to change reload. */
|
||||
public void statusReloadMultiplier(float reloadMultiplier){
|
||||
applyDynamicStatus().reloadMultiplier = reloadMultiplier;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override max health. */
|
||||
public void statusMaxHealth(float health){
|
||||
//maxHealth should never be zero
|
||||
applyDynamicStatus().healthMultiplier = health / maxHealth;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override build speed. */
|
||||
public void statusBuildSpeed(float buildSpeed){
|
||||
//build speed should never be zero
|
||||
applyDynamicStatus().buildSpeedMultiplier = buildSpeed / type.buildSpeed;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override drag. */
|
||||
public void statusDrag(float drag){
|
||||
//prevent divide by 0 (drag can be zero, if someone makes a broken unit)
|
||||
applyDynamicStatus().dragMultiplier = type.drag == 0f ? 0f : drag / type.drag;
|
||||
}
|
||||
|
||||
/** Uses a dynamic status effect to override armor. */
|
||||
public void statusArmor(float armor){
|
||||
applyDynamicStatus().armorOverride = armor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
Floor floor = floorOn();
|
||||
@@ -117,6 +175,7 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
}
|
||||
|
||||
applied.clear();
|
||||
armorOverride = -1f;
|
||||
speedMultiplier = damageMultiplier = healthMultiplier = reloadMultiplier = buildSpeedMultiplier = dragMultiplier = 1f;
|
||||
disarmed = false;
|
||||
|
||||
@@ -136,12 +195,24 @@ abstract class StatusComp implements Posc, Flyingc{
|
||||
}else{
|
||||
applied.set(entry.effect.id);
|
||||
|
||||
speedMultiplier *= entry.effect.speedMultiplier;
|
||||
healthMultiplier *= entry.effect.healthMultiplier;
|
||||
damageMultiplier *= entry.effect.damageMultiplier;
|
||||
reloadMultiplier *= entry.effect.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.effect.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.effect.dragMultiplier;
|
||||
//TODO this is very ugly...
|
||||
if(entry.effect.dynamic){
|
||||
speedMultiplier *= entry.speedMultiplier;
|
||||
healthMultiplier *= entry.healthMultiplier;
|
||||
damageMultiplier *= entry.damageMultiplier;
|
||||
reloadMultiplier *= entry.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.dragMultiplier;
|
||||
//armor is a special case; many units have it set it to 0, so an override at values >= 0 is used
|
||||
if(entry.armorOverride >= 0f) armorOverride = entry.armorOverride;
|
||||
}else{
|
||||
speedMultiplier *= entry.effect.speedMultiplier;
|
||||
healthMultiplier *= entry.effect.healthMultiplier;
|
||||
damageMultiplier *= entry.effect.damageMultiplier;
|
||||
reloadMultiplier *= entry.effect.reloadMultiplier;
|
||||
buildSpeedMultiplier *= entry.effect.buildSpeedMultiplier;
|
||||
dragMultiplier *= entry.effect.dragMultiplier;
|
||||
}
|
||||
|
||||
disarmed |= entry.effect.disarm;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -34,7 +35,7 @@ import static mindustry.logic.GlobalVars.*;
|
||||
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{
|
||||
|
||||
@Import boolean hovering, dead, disarmed;
|
||||
@Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, shield, 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;
|
||||
@@ -221,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 :
|
||||
@@ -268,8 +273,15 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
|
||||
kill();
|
||||
}
|
||||
}
|
||||
case x -> x = World.unconv((float)value);
|
||||
case y -> y = World.unconv((float)value);
|
||||
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()){
|
||||
@@ -281,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,8 +311,10 @@ 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));
|
||||
@@ -381,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;
|
||||
@@ -406,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;
|
||||
@@ -464,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);
|
||||
|
||||
52
core/src/mindustry/entities/effect/SoundEffect.java
Normal file
52
core/src/mindustry/entities/effect/SoundEffect.java
Normal file
@@ -0,0 +1,52 @@
|
||||
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(){
|
||||
startDelay = -1;
|
||||
}
|
||||
|
||||
public SoundEffect(Sound sound, Effect effect){
|
||||
this();
|
||||
this.sound = sound;
|
||||
this.effect = effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
if(startDelay < 0){
|
||||
startDelay = effect.startDelay;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(float x, float y, float rotation, Color color, Object data){
|
||||
if(!shouldCreate()) return;
|
||||
|
||||
if(startDelay > 0){
|
||||
Time.run(startDelay, () -> sound.at(x, y, Mathf.random(minPitch, maxPitch), Mathf.random(minVolume, maxVolume)));
|
||||
}else{
|
||||
sound.at(x, y, Mathf.random(minPitch, maxPitch), Mathf.random(minVolume, maxVolume));
|
||||
}
|
||||
|
||||
effect.create(x, y, rotation, color, data);
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,10 @@ public abstract class DrawPart{
|
||||
/** Weapon heat, 1 when just fired, 0, when it has cooled down (duration depends on weapon) */
|
||||
heat = p -> p.heat,
|
||||
/** Lifetime fraction, 0 to 1. Only for missiles. */
|
||||
life = p -> p.life;
|
||||
|
||||
life = p -> p.life,
|
||||
/** Current unscaled value of Time.time. */
|
||||
time = p -> Time.time;
|
||||
|
||||
float get(PartParams p);
|
||||
|
||||
static PartProgress constant(float value){
|
||||
@@ -167,6 +169,14 @@ public abstract class DrawPart{
|
||||
default PartProgress absin(float scl, float mag){
|
||||
return p -> get(p) + Mathf.absin(scl, mag);
|
||||
}
|
||||
|
||||
default PartProgress mod(float amount){
|
||||
return p -> Mathf.mod(get(p), amount);
|
||||
}
|
||||
|
||||
default PartProgress loop(float time){
|
||||
return p -> Mathf.mod(get(p)/time, 1);
|
||||
}
|
||||
|
||||
default PartProgress apply(PartProgress other, PartFunc func){
|
||||
return p -> func.get(get(p), other.get(p));
|
||||
|
||||
@@ -152,7 +152,7 @@ public class RegionPart extends DrawPart{
|
||||
float sign = (i == 1 ? -1 : 1) * params.sideMultiplier;
|
||||
Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad);
|
||||
|
||||
childParam.set(params.warmup, params.reload, params.smoothReload, params.heat, params.recoil, params.charge, params.x + Tmp.v1.x, params.y + Tmp.v1.y, i * sign + mr * sign + params.rotation);
|
||||
childParam.set(params.warmup, params.reload, params.smoothReload, params.heat, params.recoil, params.charge, params.x + Tmp.v1.x, params.y + Tmp.v1.y, mr * sign + params.rotation);
|
||||
childParam.sideMultiplier = params.sideMultiplier;
|
||||
childParam.life = params.life;
|
||||
childParam.sideOverride = i;
|
||||
|
||||
@@ -13,6 +13,9 @@ public class ShootSummon extends ShootPattern{
|
||||
this.spread = spread;
|
||||
}
|
||||
|
||||
public ShootSummon(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
|
||||
for(int i = 0; i < shots; i++){
|
||||
|
||||
@@ -55,6 +55,11 @@ public class AIController implements UnitController{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRead(Unit unit){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogicControllable(){
|
||||
return true;
|
||||
@@ -124,7 +129,7 @@ public class AIController implements UnitController{
|
||||
|
||||
if(tile == targetTile || (costType == Pathfinder.costNaval && !targetTile.floor().isLiquid)) return;
|
||||
|
||||
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), unit.speed()));
|
||||
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed()));
|
||||
}
|
||||
|
||||
public void updateWeapons(){
|
||||
@@ -182,7 +187,12 @@ public class AIController implements UnitController{
|
||||
mount.aimY = to.y;
|
||||
}
|
||||
|
||||
unit.isShooting |= (mount.shoot = mount.rotate = shoot);
|
||||
mount.shoot = mount.rotate = shoot;
|
||||
if(!shouldFire()){
|
||||
mount.shoot = false;
|
||||
}
|
||||
|
||||
unit.isShooting |= mount.shoot;
|
||||
|
||||
if(mount.target == null && !shoot && !Angles.within(mount.rotation, mount.weapon.baseRotation, 0.01f) && noTargetTime >= rotateBackTimer){
|
||||
mount.rotate = true;
|
||||
@@ -202,6 +212,11 @@ public class AIController implements UnitController{
|
||||
return Units.invalidateTarget(target, unit.team, x, y, range);
|
||||
}
|
||||
|
||||
/** @return whether the unit should actually fire bullets (as opposed to just targeting something) */
|
||||
public boolean shouldFire(){
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean shouldShoot(){
|
||||
return true;
|
||||
}
|
||||
@@ -212,7 +227,7 @@ public class AIController implements UnitController{
|
||||
}
|
||||
|
||||
public Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground);
|
||||
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground && (unit.type.targetUnderBlocks || !t.block.underBullets));
|
||||
}
|
||||
|
||||
public boolean retarget(){
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,9 @@ public class StatusEntry{
|
||||
public StatusEffect effect;
|
||||
public float time;
|
||||
|
||||
//all of these are for the dynamic effect only!
|
||||
public float damageMultiplier = 1f, healthMultiplier = 1f, speedMultiplier = 1f, reloadMultiplier = 1f, buildSpeedMultiplier = 1f, dragMultiplier = 1f, armorOverride = -1f;
|
||||
|
||||
public StatusEntry set(StatusEffect effect, float time){
|
||||
this.effect = effect;
|
||||
this.time = time;
|
||||
|
||||
@@ -27,6 +27,10 @@ public interface UnitController{
|
||||
|
||||
}
|
||||
|
||||
default void afterRead(Unit unit){
|
||||
|
||||
}
|
||||
|
||||
default boolean isBeingControlled(Unit player){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ public class WeaponMount{
|
||||
public int totalShots;
|
||||
/** counter for which barrel bullets have been fired from; used for alternating patterns */
|
||||
public int barrelCounter;
|
||||
/** Last aim length of weapon. Only used for point lasers. */
|
||||
public float lastLength;
|
||||
/** current bullet for continuous weapons */
|
||||
public @Nullable Bullet bullet;
|
||||
/** sound loop for continuous weapons */
|
||||
|
||||
@@ -35,6 +35,7 @@ public enum Gamemode{
|
||||
}, map -> map.teams.size > 1),
|
||||
editor(true, rules -> {
|
||||
rules.infiniteResources = true;
|
||||
rules.instantBuild = true;
|
||||
rules.editor = true;
|
||||
rules.waves = false;
|
||||
rules.waveTimer = false;
|
||||
|
||||
72
core/src/mindustry/game/MapMarkers.java
Normal file
72
core/src/mindustry/game/MapMarkers.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.io.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
public class MapMarkers implements Iterable<ObjectiveMarker>{
|
||||
/** Maps marker unique ID to marker. */
|
||||
private IntMap<ObjectiveMarker> map = new IntMap<>();
|
||||
/** Sequential list of markers. This allows for faster iteration than a map. */
|
||||
private Seq<ObjectiveMarker> all = new Seq<>(false);
|
||||
|
||||
public void add(int id, ObjectiveMarker marker){
|
||||
if(marker == null) return;
|
||||
|
||||
var prev = map.put(id, marker);
|
||||
if(prev != null){
|
||||
all.set(prev.arrayIndex, marker);
|
||||
}else{
|
||||
all.add(marker);
|
||||
marker.arrayIndex = all.size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(int id){
|
||||
var prev = map.remove(id);
|
||||
if(prev != null){
|
||||
if(all.size > prev.arrayIndex + 1){ //there needs to be something above the index to replace it with
|
||||
all.remove(prev.arrayIndex);
|
||||
//update its index
|
||||
all.get(prev.arrayIndex).arrayIndex = prev.arrayIndex;
|
||||
}else{
|
||||
//no sense updating the index of the replaced element when it was not replaced
|
||||
all.remove(prev.arrayIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable ObjectiveMarker get(int id){
|
||||
return map.get(id);
|
||||
}
|
||||
|
||||
public boolean has(int id){
|
||||
return get(id) != null;
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return all.size;
|
||||
}
|
||||
|
||||
public void write(DataOutput stream) throws IOException{
|
||||
JsonIO.writeBytes(map, ObjectiveMarker.class, (DataOutputStream)stream);
|
||||
}
|
||||
|
||||
public void read(DataInput stream) throws IOException{
|
||||
all.clear();
|
||||
map = JsonIO.readBytes(IntMap.class, ObjectiveMarker.class, (DataInputStream)stream);
|
||||
for(var entry : map.entries()){
|
||||
all.add(entry.value);
|
||||
entry.value.arrayIndex = all.size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ObjectiveMarker> iterator(){
|
||||
return all.iterator();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
@@ -30,6 +31,8 @@ import static mindustry.Vars.*;
|
||||
public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObjective>{
|
||||
public static final Seq<Prov<? extends MapObjective>> allObjectiveTypes = new Seq<>();
|
||||
public static final Seq<Prov<? extends ObjectiveMarker>> allMarkerTypes = new Seq<>();
|
||||
public static final ObjectMap<String, Prov<? extends ObjectiveMarker>> markerNameToType = new ObjectMap<>();
|
||||
public static final Seq<String> allMarkerTypeNames = new Seq<>();
|
||||
|
||||
/**
|
||||
* All objectives the executor contains. Do not modify directly, ever!
|
||||
@@ -58,10 +61,15 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
|
||||
registerMarker(
|
||||
ShapeTextMarker::new,
|
||||
MinimapMarker::new,
|
||||
PointMarker::new,
|
||||
ShapeMarker::new,
|
||||
TextMarker::new
|
||||
TextMarker::new,
|
||||
LineMarker::new,
|
||||
TextureMarker::new,
|
||||
QuadMarker::new
|
||||
);
|
||||
|
||||
registerLegacyMarker("Minimap", PointMarker::new);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@@ -70,8 +78,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,11 +90,24 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerLegacyMarker(String name, Prov<? extends ObjectiveMarker> prov) {
|
||||
Class<?> type = prov.get().getClass();
|
||||
|
||||
markerNameToType.put(name, prov);
|
||||
markerNameToType.put(Strings.camelize(name), prov);
|
||||
JsonIO.classTag(Strings.camelize(name), type);
|
||||
JsonIO.classTag(name, type);
|
||||
}
|
||||
|
||||
/** Adds all given objectives to the executor as root objectives. */
|
||||
public void add(MapObjective... objectives){
|
||||
for(var objective : objectives) flatten(objective);
|
||||
@@ -102,23 +124,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;
|
||||
@@ -474,6 +483,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{
|
||||
@@ -580,9 +597,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,19 +624,37 @@ 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;
|
||||
|
||||
/** 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 to display marker in the world. */
|
||||
public boolean world = true;
|
||||
/** Whether to display marker on minimap. */
|
||||
public boolean minimap = false;
|
||||
/** Whether to scale marker corresponding to player's zoom level. */
|
||||
public boolean autoscale = false;
|
||||
/** On which z-sorting layer is marker drawn. */
|
||||
protected float drawLayer = Layer.overlayUI;
|
||||
|
||||
public void draw(float scaleFactor){}
|
||||
|
||||
/** 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 world -> world = !Mathf.equal((float)p1, 0f);
|
||||
case minimap -> minimap = !Mathf.equal((float)p1, 0f);
|
||||
case autoscale -> autoscale = !Mathf.equal((float)p1, 0f);
|
||||
case drawLayer -> drawLayer = (float)p1;
|
||||
}
|
||||
}
|
||||
|
||||
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(){
|
||||
@@ -620,19 +663,52 @@ 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();
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, p3);
|
||||
|
||||
if(!Double.isNaN(p1)){
|
||||
if(type == LMarkerControl.pos){
|
||||
pos.x = (float)p1 * tilesize;
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
if(type == LMarkerControl.pos){
|
||||
pos.y = (float)p2 * tilesize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -672,61 +748,124 @@ 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 draw(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(pos.x, pos.y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
Lines.stroke(scaleFactor, color);
|
||||
Lines.poly(pos.x, pos.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, pos.x, pos.y + radius * scaleFactor + textHeight * scaleFactor, drawLayer, flags, fontSize * scaleFactor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, 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.fromDouble(p1);
|
||||
case shape -> sides = (int)p1;
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p2, 0f)){
|
||||
flags |= WorldLabel.flagOutline;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagOutline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(String text, boolean fetch){
|
||||
this.text = text;
|
||||
if(fetch){
|
||||
fetchedText = fetchText(this.text);
|
||||
}else{
|
||||
fetchedText = this.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays a circle on the minimap. */
|
||||
public static class MinimapMarker extends ObjectiveMarker{
|
||||
public Point2 pos = new Point2();
|
||||
/** Displays a circle in the world. */
|
||||
public static class PointMarker extends PosMarker{
|
||||
public float radius = 5f, stroke = 11f;
|
||||
public Color color = Color.valueOf("f25555");
|
||||
|
||||
public MinimapMarker(int x, int y){
|
||||
public PointMarker(int x, int y){
|
||||
this.pos.set(x, y);
|
||||
}
|
||||
|
||||
public MinimapMarker(int x, int y, Color color){
|
||||
public PointMarker(int x, int y, Color color){
|
||||
this.pos.set(x, y);
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public MinimapMarker(int x, int y, float radius, float stroke, Color color){
|
||||
public PointMarker(int x, int y, float radius, float stroke, Color color){
|
||||
this.pos.set(x, y);
|
||||
this.stroke = stroke;
|
||||
this.radius = radius;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public MinimapMarker(){}
|
||||
public PointMarker(){}
|
||||
|
||||
@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 draw(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(pos.x, pos.y, rad * fin);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, p3);
|
||||
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case radius -> radius = (float)p1;
|
||||
case stroke -> stroke = (float)p1;
|
||||
case color -> color.fromDouble(p1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
@@ -745,31 +884,58 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public ShapeMarker(){}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
public void draw(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(pos.x, pos.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(pos.x, pos.y, sides, (radius + 1f) * scaleFactor, rotation);
|
||||
}else{
|
||||
Draw.color(color);
|
||||
Fill.poly(pos.x, pos.y, sides, radius, rotation);
|
||||
Fill.poly(pos.x, pos.y, sides, radius * scaleFactor, rotation);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, 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.fromDouble(p1);
|
||||
case shape -> sides = (int)p1;
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case shape -> fill = !Mathf.equal((float)p2, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p3)){
|
||||
if(type == LMarkerControl.shape){
|
||||
outline = !Mathf.equal((float)p3, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
@@ -790,12 +956,304 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
public TextMarker(){}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
public void draw(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, pos.x, pos.y, drawLayer, flags, fontSize * scaleFactor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case labelFlags -> {
|
||||
if(!Mathf.equal((float)p2, 0f)){
|
||||
flags |= WorldLabel.flagOutline;
|
||||
}else{
|
||||
flags &= ~WorldLabel.flagOutline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 color1 = Color.valueOf("ffd37f");
|
||||
public Color color2 = 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(){}
|
||||
|
||||
@Override
|
||||
public void draw(float scaleFactor){
|
||||
Draw.z(drawLayer);
|
||||
if(outline){
|
||||
Lines.stroke((stroke + 2f) * scaleFactor, Pal.gray);
|
||||
Lines.line(pos.x, pos.y, endPos.x, endPos.y);
|
||||
}
|
||||
|
||||
Lines.stroke(stroke * scaleFactor, Color.white);
|
||||
Lines.line(pos.x, pos.y, color1, endPos.x, endPos.y, color2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, p3);
|
||||
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case endPos -> endPos.x = (float)p1 * tilesize;
|
||||
case stroke -> stroke = (float)p1;
|
||||
case color -> color1.set(color2.fromDouble(p1));
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case endPos -> endPos.y = (float)p2 * tilesize;
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p1) && !Double.isNaN(p2)){
|
||||
switch (type){
|
||||
case posi -> ((int)p1 == 0 ? pos : (int)p1 == 1 ? endPos : Tmp.v1).x = (float)p2 * tilesize;
|
||||
case colori -> ((int)p1 == 0 ? color1 : (int)p1 == 1 ? color2 : Tmp.c1).fromDouble(p2);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p1) && !Double.isNaN(p3)){
|
||||
switch(type){
|
||||
case posi -> ((int)p1 == 0 ? pos : (int)p1 == 1 ? endPos : Tmp.v1).y = (float)p3 * tilesize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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){
|
||||
super.control(type, p1, p2, p3);
|
||||
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case rotation -> rotation = (float)p1;
|
||||
case textureSize -> width = (float)p1 * tilesize;
|
||||
case color -> color.fromDouble(p1);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case textureSize -> height = (float)p2 * tilesize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(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, pos.x, pos.y, width * scaleFactor, height * scaleFactor, rotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexture(String textureName){
|
||||
this.textureName = textureName;
|
||||
|
||||
if(fetchedRegion == null) fetchedRegion = new TextureRegion();
|
||||
lookupRegion(textureName, fetchedRegion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class QuadMarker extends ObjectiveMarker{
|
||||
public String textureName = "white";
|
||||
public @Vertices float[] vertices = new float[24];
|
||||
private boolean mapRegion = true;
|
||||
|
||||
private transient TextureRegion fetchedRegion;
|
||||
|
||||
public QuadMarker() {
|
||||
for(int i = 0; i < 4; i++){
|
||||
vertices[i * 6 + 2] = Color.white.toFloatBits();
|
||||
vertices[i * 6 + 5] = Color.clearFloatBits;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(float scaleFactor){
|
||||
if(fetchedRegion == null) setTexture(textureName);
|
||||
|
||||
Draw.z(drawLayer);
|
||||
Draw.vert(fetchedRegion.texture, vertices, 0, vertices.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void control(LMarkerControl type, double p1, double p2, double p3){
|
||||
super.control(type, p1, p2, p3);
|
||||
|
||||
if(!Double.isNaN(p1)){
|
||||
switch(type){
|
||||
case color -> {
|
||||
float col = Tmp.c1.fromDouble(p1).toFloatBits();
|
||||
for(int i = 0; i < 4; i++) vertices[i * 6 + 2] = col;
|
||||
}
|
||||
case pos -> vertices[0] = (float)p1 * tilesize;
|
||||
case posi -> setPos((int)p1, p2, p3);
|
||||
case uvi -> setUv((int)p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case pos -> vertices[1] = (float)p1 * tilesize;
|
||||
}
|
||||
}
|
||||
|
||||
if(!Double.isNaN(p1) && !Double.isNaN(p2)){
|
||||
switch(type){
|
||||
case colori -> setColor((int)p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexture(String textureName){
|
||||
this.textureName = textureName;
|
||||
|
||||
boolean firstUpdate = fetchedRegion == null;
|
||||
|
||||
if(fetchedRegion == null) fetchedRegion = new TextureRegion();
|
||||
Tmp.tr1.set(fetchedRegion);
|
||||
|
||||
lookupRegion(textureName, fetchedRegion);
|
||||
|
||||
if(firstUpdate){
|
||||
if(mapRegion){
|
||||
mapRegion = false;
|
||||
|
||||
// possibly from the editor, we need to clamp the values
|
||||
for(int i = 0; i < 4; i++){
|
||||
vertices[i * 6 + 3] = Mathf.map(Mathf.clamp(vertices[i * 6 + 3]), fetchedRegion.u, fetchedRegion.u2);
|
||||
vertices[i * 6 + 4] = Mathf.map(1 - Mathf.clamp(vertices[i * 6 + 4]), fetchedRegion.v, fetchedRegion.v2);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for(int i = 0; i < 4; i++){
|
||||
vertices[i * 6 + 3] = Mathf.map(vertices[i * 6 + 3], Tmp.tr1.u, Tmp.tr1.u2, fetchedRegion.u, fetchedRegion.u2);
|
||||
vertices[i * 6 + 4] = Mathf.map(vertices[i * 6 + 4], Tmp.tr1.v, Tmp.tr1.v2, fetchedRegion.v, fetchedRegion.v2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPos(int i, double x, double y){
|
||||
if(i >= 0 && i < 4){
|
||||
if(!Double.isNaN(x)) vertices[i * 6] = (float)x * tilesize;
|
||||
if(!Double.isNaN(y)) vertices[i * 6 + 1] = (float)y * tilesize;
|
||||
}
|
||||
}
|
||||
|
||||
private void setColor(int i, double c){
|
||||
if(i >= 0 && i < 4){
|
||||
vertices[i * 6 + 2] = Tmp.c1.fromDouble(c).toFloatBits();
|
||||
}
|
||||
}
|
||||
|
||||
private void setUv(int i, double u, double v){
|
||||
if(i >= 0 && i < 4){
|
||||
if(fetchedRegion == null) setTexture(textureName);
|
||||
|
||||
if(!Double.isNaN(u)) vertices[i * 6 + 3] = Mathf.map(Mathf.clamp((float)u), fetchedRegion.u, fetchedRegion.u2);
|
||||
if(!Double.isNaN(v)) vertices[i * 6 + 4] = Mathf.map(1 - Mathf.clamp((float)v), fetchedRegion.v, fetchedRegion.v2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void lookupRegion(String name, TextureRegion out){
|
||||
TextureRegion region = Core.atlas.find(name);
|
||||
if(region.found()){
|
||||
out.set(region);
|
||||
}else{
|
||||
if(Core.assets.isLoaded(name, Texture.class)){
|
||||
out.set(Core.assets.get(name, Texture.class));
|
||||
}else{
|
||||
out.set(Core.atlas.find("error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,6 +1262,16 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
|
||||
@Retention(RUNTIME)
|
||||
public @interface Unordered{}
|
||||
|
||||
/** For arrays or {@link Seq}s; does not add the new and delete buttons */
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Immutable{}
|
||||
|
||||
/** For {@code float[]}; treats it as an array of vertices. */
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface Vertices{}
|
||||
|
||||
/** For {@code byte}; treats it as a world label flag. */
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -31,7 +31,7 @@ public class Schematic implements Publishable, Comparable<Schematic>{
|
||||
}
|
||||
|
||||
public float powerProduction(){
|
||||
return tiles.sumf(s -> s.block instanceof PowerGenerator p ? p.powerProduction : 0f);
|
||||
return tiles.sumf(s -> s.block instanceof PowerGenerator p ? p.getDisplayedPowerProduction() : 0f);
|
||||
}
|
||||
|
||||
public float powerConsumption(){
|
||||
|
||||
@@ -128,7 +128,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
|
||||
String tname = data.getString("type", "dagger");
|
||||
|
||||
type = content.unit(LegacyIO.unitMap.get(tname, tname));
|
||||
if(type == null) type = UnitTypes.dagger;
|
||||
if(type == null || type.internal) type = UnitTypes.dagger;
|
||||
begin = data.getInt("begin", 0);
|
||||
end = data.getInt("end", never);
|
||||
spacing = data.getInt("spacing", 1);
|
||||
|
||||
@@ -31,7 +31,7 @@ public class Team implements Comparable<Team>{
|
||||
derelict = new Team(0, "derelict", Color.valueOf("4d4e58")),
|
||||
sharded = new Team(1, "sharded", Pal.accent.cpy(), Color.valueOf("ffd37f"), Color.valueOf("eab678"), Color.valueOf("d4816b")),
|
||||
crux = new Team(2, "crux", Color.valueOf("f25555"), Color.valueOf("fc8e6c"), Color.valueOf("f25555"), Color.valueOf("a04553")),
|
||||
malis = new Team(3, "malis", Color.valueOf("a27ce5"), Color.valueOf("c195fb"), Color.valueOf("665c9f"), Color.valueOf("484988")),
|
||||
malis = new Team(3, "malis", Color.valueOf("a27ce5"), Color.valueOf("c7a4f5"), Color.valueOf("896fd6"), Color.valueOf("504cba")),
|
||||
|
||||
//TODO temporarily no palettes for these teams.
|
||||
green = new Team(4, "green", Color.valueOf("54d67d")),//Color.valueOf("96f58c"), Color.valueOf("54d67d"), Color.valueOf("28785c")),
|
||||
|
||||
@@ -307,6 +307,8 @@ public class Teams{
|
||||
|
||||
//convert all team tiles to neutral, randomly killing them
|
||||
for(var b : builds){
|
||||
if(b.block.privileged) continue;
|
||||
|
||||
if(b instanceof CoreBuild){
|
||||
b.kill();
|
||||
}else{
|
||||
@@ -331,7 +333,7 @@ public class Teams{
|
||||
}
|
||||
|
||||
for(var build : builds){
|
||||
if(build.within(x, y, range)){
|
||||
if(build.within(x, y, range) && !build.block.privileged){
|
||||
scheduleDerelict(build);
|
||||
}
|
||||
}
|
||||
@@ -345,7 +347,7 @@ public class Teams{
|
||||
}
|
||||
|
||||
for(var build : builds){
|
||||
if(build.within(x, y, range) && !cores.contains(c -> c.within(x, y, range))){
|
||||
if(build.within(x, y, range) && !cores.contains(c -> c.within(build, range))){
|
||||
//TODO GPU driver bugs?
|
||||
build.kill();
|
||||
//Time.run(Mathf.random(0f, 60f * 6f), build::kill);
|
||||
|
||||
@@ -223,7 +223,7 @@ public class Drawf{
|
||||
/** Sets Draw.z to the text layer, and returns the previous layer. */
|
||||
public static float text(){
|
||||
float z = Draw.z();
|
||||
if(renderer.pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
Draw.z(Layer.endPixeled);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,12 +111,12 @@ public final class FogRenderer{
|
||||
dynamicFog.getTexture().setFilter(TextureFilter.linear);
|
||||
|
||||
Draw.shader(Shaders.fog);
|
||||
Draw.color(state.rules.dynamicColor);
|
||||
Draw.color(state.rules.dynamicColor, 0.5f);
|
||||
Draw.fbo(dynamicFog.getTexture(), world.width(), world.height(), tilesize);
|
||||
//TODO ai check?
|
||||
if(state.rules.staticFog){
|
||||
//TODO why does this require a half-tile offset while dynamic does not
|
||||
Draw.color(state.rules.staticColor);
|
||||
Draw.color(state.rules.staticColor, 1f);
|
||||
Draw.fbo(staticFog.getTexture(), world.width(), world.height(), tilesize, tilesize/2f);
|
||||
}
|
||||
Draw.shader();
|
||||
|
||||
@@ -146,16 +146,27 @@ public class MinimapRenderer{
|
||||
|
||||
rect.set((dx - sz) * tilesize, (dy - sz) * tilesize, sz * 2 * tilesize, sz * 2 * tilesize);
|
||||
|
||||
Tmp.m2.set(Draw.trans());
|
||||
|
||||
var trans = Tmp.m1.idt();
|
||||
trans.translate(lastX, lastY);
|
||||
if(!worldSpace){
|
||||
trans.scl(Tmp.v1.set(lastW / rect.width, lastH / rect.height));
|
||||
trans.translate(-rect.x, -rect.y);
|
||||
}else{
|
||||
trans.scl(Tmp.v1.set(lastW / world.unitWidth(), lastH / world.unitHeight()));
|
||||
}
|
||||
trans.translate(tilesize / 2f, tilesize / 2f);
|
||||
Draw.trans(trans);
|
||||
|
||||
for(Unit unit : units){
|
||||
if(unit.inFogTo(player.team()) || !unit.type.drawMinimap) continue;
|
||||
|
||||
float rx = !fullView ? (unit.x - rect.x) / rect.width * w : unit.x / (world.width() * tilesize) * w;
|
||||
float ry = !fullView ? (unit.y - rect.y) / rect.width * h : unit.y / (world.height() * tilesize) * h;
|
||||
float scale = Scl.scl(1f) * tilesize * 3;
|
||||
var region = unit.icon();
|
||||
|
||||
Draw.mixcol(unit.team.color, 1f);
|
||||
float scale = Scl.scl(1f) / 2f * scaling * 32f;
|
||||
var region = unit.icon();
|
||||
Draw.rect(region, x + rx, y + ry, scale, scale * (float)region.height / region.width, unit.rotation() - 90);
|
||||
Draw.rect(region, unit.x, unit.y, scale, scale * (float)region.height / region.width, unit.rotation() - 90);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@@ -186,23 +197,22 @@ public class MinimapRenderer{
|
||||
//crisp pixels
|
||||
dynamicTex.setFilter(TextureFilter.nearest);
|
||||
|
||||
if(worldSpace){
|
||||
region.set(0f, 0f, 1f, 1f);
|
||||
}
|
||||
|
||||
Tmp.tr1.set(dynamicTex);
|
||||
Tmp.tr1.set(region.u, 1f - region.v, region.u2, 1f - region.v2);
|
||||
Tmp.tr1.set(0f, 0f, 1f, 1f);
|
||||
|
||||
Draw.color(state.rules.dynamicColor);
|
||||
Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h);
|
||||
float wf = world.width() * tilesize;
|
||||
float hf = world.height() * tilesize;
|
||||
|
||||
Draw.color(state.rules.dynamicColor, 0.5f);
|
||||
Draw.rect(Tmp.tr1, wf / 2, hf / 2, wf, hf);
|
||||
|
||||
if(state.rules.staticFog){
|
||||
staticTex.setFilter(TextureFilter.nearest);
|
||||
|
||||
Tmp.tr1.texture = staticTex;
|
||||
//must be black to fit with borders
|
||||
Draw.color(0f, 0f, 0f, state.rules.staticColor.a);
|
||||
Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h);
|
||||
Draw.color(0f, 0f, 0f, 1f);
|
||||
Draw.rect(Tmp.tr1, wf / 2, hf / 2, wf, hf);
|
||||
}
|
||||
|
||||
Draw.color();
|
||||
@@ -211,23 +221,21 @@ public class MinimapRenderer{
|
||||
|
||||
//TODO might be useful in the standard minimap too
|
||||
if(fullView){
|
||||
drawSpawns(x, y, w, h, scaling);
|
||||
drawSpawns();
|
||||
|
||||
if(!mobile){
|
||||
//draw bounds for camera - not drawn on mobile because you can't shift it by tapping anyway
|
||||
Rect r = Core.camera.bounds(Tmp.r1);
|
||||
Vec2 bot = transform(Tmp.v1.set(r.x, r.y));
|
||||
Vec2 top = transform(Tmp.v2.set(r.x + r.width, r.y + r.height));
|
||||
Lines.stroke(Scl.scl(3f));
|
||||
Draw.color(Pal.accent);
|
||||
Lines.rect(bot.x,bot.y, top.x - bot.x, top.y - bot.y);
|
||||
Lines.rect(r.x, r.y, r.width, r.height);
|
||||
Draw.reset();
|
||||
}
|
||||
}
|
||||
|
||||
LongSeq indicators = control.indicators.list();
|
||||
float fin = ((Time.globalTime / 30f) % 1f);
|
||||
float rad = scale(fin * 5f + tilesize - 2f);
|
||||
float rad = fin * 5f + tilesize - 2f;
|
||||
Lines.stroke(Scl.scl((1f - fin) * 4f + 0.5f));
|
||||
|
||||
for(int i = 0; i < indicators.size; i++){
|
||||
@@ -244,23 +252,32 @@ public class MinimapRenderer{
|
||||
offset = build.block.offset / tilesize;
|
||||
}
|
||||
|
||||
Vec2 v = transform(Tmp.v1.set((ix + 0.5f + offset) * tilesize, (iy + 0.5f + offset) * tilesize));
|
||||
|
||||
Draw.color(Color.orange, Color.scarlet, Mathf.clamp(time / 70f));
|
||||
|
||||
Lines.square(v.x, v.y, rad);
|
||||
Lines.square((ix + 0.5f + offset) * tilesize, (iy + 0.5f + offset) * tilesize, rad);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
|
||||
//TODO autoscale markers
|
||||
state.rules.objectives.eachRunning(obj -> {
|
||||
for(var marker : obj.markers){
|
||||
marker.drawMinimap(this);
|
||||
if(marker.minimap){
|
||||
marker.draw(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for(var marker : state.markers){
|
||||
if(marker.minimap){
|
||||
marker.draw(1);
|
||||
}
|
||||
}
|
||||
|
||||
Draw.trans(Tmp.m2);
|
||||
}
|
||||
|
||||
public void drawSpawns(float x, float y, float w, float h, float scaling){
|
||||
public void drawSpawns(){
|
||||
if(!state.rules.showSpawns || !state.hasSpawns() || !state.rules.waves) return;
|
||||
|
||||
TextureRegion icon = Icon.units.getRegion();
|
||||
@@ -269,36 +286,21 @@ public class MinimapRenderer{
|
||||
|
||||
Draw.color(state.rules.waveTeam.color, Tmp.c2.set(state.rules.waveTeam.color).value(1.2f), Mathf.absin(Time.time, 16f, 1f));
|
||||
|
||||
float rad = scale(state.rules.dropZoneRadius);
|
||||
float rad = state.rules.dropZoneRadius;
|
||||
float curve = Mathf.curve(Time.time % 240f, 120f, 240f);
|
||||
|
||||
for(Tile tile : spawner.getSpawns()){
|
||||
float tx = ((tile.x + 0.5f) / world.width()) * w;
|
||||
float ty = ((tile.y + 0.5f) / world.height()) * h;
|
||||
float tx = tile.worldx();
|
||||
float ty = tile.worldy();
|
||||
|
||||
Draw.rect(icon, x + tx, y + ty, icon.width, icon.height);
|
||||
Lines.circle(x + tx, y + ty, rad);
|
||||
if(curve > 0f) Lines.circle(x + tx, y + ty, rad * Interp.pow3Out.apply(curve));
|
||||
Draw.rect(icon, tx, ty, icon.width, icon.height);
|
||||
Lines.circle(tx, ty, rad);
|
||||
if(curve > 0f) Lines.circle(tx, ty, rad * Interp.pow3Out.apply(curve));
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
//TODO horrible code, everywhere.
|
||||
public Vec2 transform(Vec2 position){
|
||||
if(!worldSpace){
|
||||
position.sub(rect.x, rect.y).scl(lastW / rect.width, lastH / rect.height);
|
||||
}else{
|
||||
position.scl(lastW / world.unitWidth(), lastH / world.unitHeight());
|
||||
}
|
||||
|
||||
return position.add(lastX, lastY);
|
||||
}
|
||||
|
||||
public float scale(float radius){
|
||||
return worldSpace ? (radius / (baseSize / 2f)) * 5f * lastScl : lastW / rect.width * radius;
|
||||
}
|
||||
|
||||
public @Nullable TextureRegion getRegion(){
|
||||
if(texture == null) return null;
|
||||
|
||||
|
||||
@@ -113,11 +113,6 @@ public class OverlayRenderer{
|
||||
}
|
||||
}
|
||||
|
||||
//draw objective markers
|
||||
state.rules.objectives.eachRunning(obj -> {
|
||||
for(var marker : obj.markers) marker.draw();
|
||||
});
|
||||
|
||||
if(player.dead()) return; //dead players don't draw
|
||||
|
||||
InputHandler input = control.input;
|
||||
|
||||
@@ -58,7 +58,7 @@ public class Pixelator implements Disposable{
|
||||
}
|
||||
|
||||
public boolean enabled(){
|
||||
return Core.settings.getBool("pixelate");
|
||||
return renderer.pixelate;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -242,10 +242,17 @@ public class Shaders{
|
||||
@Override
|
||||
public void apply(){
|
||||
setUniformf("u_progress", progress);
|
||||
setUniformf("u_uv", region.u, region.v);
|
||||
setUniformf("u_uv2", region.u2, region.v2);
|
||||
setUniformf("u_time", time);
|
||||
setUniformf("u_texsize", region.texture.width, region.texture.height);
|
||||
|
||||
if(region.texture == null){
|
||||
setUniformf("u_uv", 0f, 0f);
|
||||
setUniformf("u_uv2", 1f, 1f);
|
||||
setUniformf("u_texsize", 1, 1);
|
||||
}else{
|
||||
setUniformf("u_uv", region.u, region.v);
|
||||
setUniformf("u_uv2", region.u2, region.v2);
|
||||
setUniformf("u_texsize", region.texture.width, region.texture.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package mindustry.input;
|
||||
|
||||
import arc.*;
|
||||
import arc.KeyBinds.*;
|
||||
import arc.input.InputDevice.*;
|
||||
import arc.input.*;
|
||||
import mindustry.*;
|
||||
|
||||
public enum Binding implements KeyBind{
|
||||
move_x(new Axis(KeyCode.a, KeyCode.d), "general"),
|
||||
@@ -12,16 +12,12 @@ public enum Binding implements KeyBind{
|
||||
pan(KeyCode.mouseForward),
|
||||
|
||||
boost(KeyCode.shiftLeft),
|
||||
command_mode(KeyCode.shiftLeft),
|
||||
control(KeyCode.controlLeft),
|
||||
respawn(KeyCode.v),
|
||||
control(KeyCode.controlLeft),
|
||||
select(KeyCode.mouseLeft),
|
||||
deselect(KeyCode.mouseRight),
|
||||
break_block(KeyCode.mouseRight),
|
||||
|
||||
select_all_units(KeyCode.g),
|
||||
select_all_unit_factories(KeyCode.h),
|
||||
|
||||
pickupCargo(KeyCode.leftBracket),
|
||||
dropCargo(KeyCode.rightBracket),
|
||||
|
||||
@@ -38,6 +34,33 @@ public enum Binding implements KeyBind{
|
||||
schematic_flip_y(KeyCode.x),
|
||||
schematic_menu(KeyCode.t),
|
||||
|
||||
|
||||
command_mode(KeyCode.shiftLeft, "command"),
|
||||
command_queue(KeyCode.mouseMiddle),
|
||||
create_control_group(KeyCode.controlLeft),
|
||||
|
||||
select_all_units(KeyCode.g),
|
||||
select_all_unit_factories(KeyCode.h),
|
||||
|
||||
cancel_orders(KeyCode.unset),
|
||||
|
||||
unit_stance_shoot(KeyCode.unset),
|
||||
unit_stance_hold_fire(KeyCode.unset),
|
||||
unit_stance_pursue_target(KeyCode.unset),
|
||||
unit_stance_patrol(KeyCode.unset),
|
||||
unit_stance_ram(KeyCode.unset),
|
||||
|
||||
unit_command_move(KeyCode.unset),
|
||||
unit_command_repair(KeyCode.unset),
|
||||
unit_command_rebuild(KeyCode.unset),
|
||||
unit_command_assist(KeyCode.unset),
|
||||
unit_command_mine(KeyCode.unset),
|
||||
unit_command_boost(KeyCode.unset),
|
||||
unit_command_enter_payload(KeyCode.unset),
|
||||
unit_command_load_units(KeyCode.unset),
|
||||
unit_command_load_blocks(KeyCode.unset),
|
||||
unit_command_unload_payload(KeyCode.unset),
|
||||
|
||||
category_prev(KeyCode.comma, "blocks"),
|
||||
category_next(KeyCode.period),
|
||||
|
||||
@@ -57,7 +80,7 @@ public enum Binding implements KeyBind{
|
||||
block_select_10(KeyCode.num0),
|
||||
|
||||
zoom(new Axis(KeyCode.scroll), "view"),
|
||||
menu(Core.app.isAndroid() ? KeyCode.back : KeyCode.escape),
|
||||
menu(Vars.android ? KeyCode.back : KeyCode.escape),
|
||||
fullscreen(KeyCode.f11),
|
||||
pause(KeyCode.space),
|
||||
minimap(KeyCode.m),
|
||||
|
||||
@@ -6,10 +6,12 @@ import arc.Graphics.Cursor.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.input.KeyCode.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.core.*;
|
||||
@@ -21,6 +23,7 @@ import mindustry.graphics.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static arc.Core.camera;
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.input.PlaceMode.*;
|
||||
@@ -48,6 +51,11 @@ public class DesktopInput extends InputHandler{
|
||||
/** Previously selected tile. */
|
||||
public Tile prevSelected;
|
||||
|
||||
/** Most recently selected control group by index */
|
||||
public int lastCtrlGroup;
|
||||
/** Time of most recent control group selection */
|
||||
public long lastCtrlGroupSelectMillis;
|
||||
|
||||
boolean showHint(){
|
||||
return ui.hudfrag.shown && Core.settings.getBool("hints") && selectPlans.isEmpty() &&
|
||||
(!isBuilding && !Core.settings.getBool("buildautopause") || player.unit().isBuilding() || !player.dead() && !player.unit().spawnedByCore());
|
||||
@@ -106,7 +114,7 @@ public class DesktopInput extends InputHandler{
|
||||
|
||||
//draw break selection
|
||||
if(mode == breaking){
|
||||
drawBreakSelection(selectX, selectY, cursorX, cursorY, !Core.input.keyDown(Binding.schematic_select) ? maxLength : Vars.maxSchematicSize);
|
||||
drawBreakSelection(selectX, selectY, cursorX, cursorY, !Core.input.keyDown(Binding.schematic_select) ? maxLength : Vars.maxSchematicSize, false);
|
||||
}
|
||||
|
||||
if(!Core.scene.hasKeyboard() && mode != breaking){
|
||||
@@ -261,22 +269,86 @@ public class DesktopInput extends InputHandler{
|
||||
//validate commanding units
|
||||
selectedUnits.removeAll(u -> !u.isCommandable() || !u.isValid());
|
||||
|
||||
if(commandMode && input.keyTap(Binding.select_all_units) && !scene.hasField() && !scene.hasDialog()){
|
||||
selectedUnits.clear();
|
||||
commandBuildings.clear();
|
||||
for(var unit : player.team().data().units){
|
||||
if(unit.isCommandable()){
|
||||
selectedUnits.add(unit);
|
||||
if(commandMode && !scene.hasField() && !scene.hasDialog()){
|
||||
if(input.keyTap(Binding.select_all_units)){
|
||||
selectedUnits.clear();
|
||||
commandBuildings.clear();
|
||||
for(var unit : player.team().data().units){
|
||||
if(unit.isCommandable()){
|
||||
selectedUnits.add(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(commandMode && input.keyTap(Binding.select_all_unit_factories) && !scene.hasField() && !scene.hasDialog()){
|
||||
selectedUnits.clear();
|
||||
commandBuildings.clear();
|
||||
for(var build : player.team().data().buildings){
|
||||
if(build.block.commandable){
|
||||
commandBuildings.add(build);
|
||||
if(input.keyTap(Binding.select_all_unit_factories)){
|
||||
selectedUnits.clear();
|
||||
commandBuildings.clear();
|
||||
for(var build : player.team().data().buildings){
|
||||
if(build.block.commandable){
|
||||
commandBuildings.add(build);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < controlGroupBindings.length; i++){
|
||||
if(input.keyTap(controlGroupBindings[i])){
|
||||
|
||||
//create control group if it doesn't exist yet
|
||||
if(controlGroups[i] == null) controlGroups[i] = new IntSeq();
|
||||
|
||||
IntSeq group = controlGroups[i];
|
||||
boolean creating = input.keyDown(Binding.create_control_group);
|
||||
|
||||
//clear existing if making a new control group
|
||||
//if any of the control group edit buttons are pressed take the current selection
|
||||
if(creating){
|
||||
group.clear();
|
||||
|
||||
IntSeq selectedUnitIds = selectedUnits.mapInt(u -> u.id);
|
||||
if(Core.settings.getBool("distinctcontrolgroups", true)){
|
||||
for(IntSeq cg : controlGroups){
|
||||
if(cg != null){
|
||||
cg.removeAll(selectedUnitIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
group.addAll(selectedUnitIds);
|
||||
}
|
||||
|
||||
//remove invalid units
|
||||
for(int j = 0; j < group.size; j++){
|
||||
Unit u = Groups.unit.getByID(group.get(j));
|
||||
if(u == null || !u.isCommandable() || !u.isValid()){
|
||||
group.removeIndex(j);
|
||||
j --;
|
||||
}
|
||||
}
|
||||
|
||||
//replace the selected units with the current control group
|
||||
if(!group.isEmpty() && !creating){
|
||||
selectedUnits.clear();
|
||||
commandBuildings.clear();
|
||||
|
||||
group.each(id -> {
|
||||
var unit = Groups.unit.getByID(id);
|
||||
if(unit != null){
|
||||
selectedUnits.addAll(unit);
|
||||
}
|
||||
});
|
||||
|
||||
//double tap to center camera
|
||||
if(lastCtrlGroup == i && Time.timeSinceMillis(lastCtrlGroupSelectMillis) < 400){
|
||||
float totalX = 0, totalY = 0;
|
||||
for(Unit unit : selectedUnits){
|
||||
totalX += unit.x;
|
||||
totalY += unit.y;
|
||||
}
|
||||
panning = true;
|
||||
Core.camera.position.set(totalX / selectedUnits.size, totalY / selectedUnits.size);
|
||||
}
|
||||
lastCtrlGroup = i;
|
||||
lastCtrlGroupSelectMillis = Time.millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,10 +447,14 @@ public class DesktopInput extends InputHandler{
|
||||
Tile cursor = tileAt(Core.input.mouseX(), Core.input.mouseY());
|
||||
|
||||
if(cursor != null){
|
||||
if(cursor.build != null){
|
||||
if(cursor.build != null && cursor.build.interactable(player.team())){
|
||||
cursorType = cursor.build.getCursor();
|
||||
}
|
||||
|
||||
if(cursor.build != null && cursor.build.team == Team.derelict && Build.validPlace(cursor.block(), player.team(), cursor.build.tileX(), cursor.build.tileY(), cursor.build.rotation)){
|
||||
cursorType = ui.repairCursor;
|
||||
}
|
||||
|
||||
if((isPlacing() && player.isBuilder()) || !selectPlans.isEmpty()){
|
||||
cursorType = SystemCursor.hand;
|
||||
}
|
||||
@@ -387,8 +463,23 @@ public class DesktopInput extends InputHandler{
|
||||
cursorType = ui.drillCursor;
|
||||
}
|
||||
|
||||
if(commandMode && selectedUnits.any() && ((cursor.build != null && !cursor.build.inFogTo(player.team()) && cursor.build.team != player.team()) || (selectedEnemyUnit(input.mouseWorldX(), input.mouseWorldY()) != null))){
|
||||
cursorType = ui.targetCursor;
|
||||
if(commandMode && selectedUnits.any()){
|
||||
boolean canAttack = (cursor.build != null && !cursor.build.inFogTo(player.team()) && cursor.build.team != player.team());
|
||||
|
||||
if(!canAttack){
|
||||
var unit = selectedEnemyUnit(input.mouseWorldX(), input.mouseWorldY());
|
||||
if(unit != null){
|
||||
canAttack = selectedUnits.contains(u -> u.canTarget(unit));
|
||||
}
|
||||
}
|
||||
|
||||
if(canAttack){
|
||||
cursorType = ui.targetCursor;
|
||||
}
|
||||
|
||||
if(input.keyTap(Binding.command_queue) && keybinds.get(Binding.command_queue).key.type != KeyType.mouse){
|
||||
commandTap(input.mouseX(), input.mouseY(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if(getPlan(cursor.x, cursor.y) != null && mode == none){
|
||||
@@ -585,7 +676,7 @@ public class DesktopInput extends InputHandler{
|
||||
commandRect = true;
|
||||
commandRectX = input.mouseWorldX();
|
||||
commandRectY = input.mouseWorldY();
|
||||
}else if(!checkConfigTap() && selected != null){
|
||||
}else if(!checkConfigTap() && selected != null && !tryRepairDerelict(selected)){
|
||||
//only begin shooting if there's no cursor event
|
||||
if(!tryTapPlayer(Core.input.mouseWorld().x, Core.input.mouseWorld().y) && !tileTapped(selected.build) && !player.unit().activelyBuilding() && !droppingItem
|
||||
&& !(tryStopMine(selected) || (!settings.getBool("doubletapmine") || selected == prevSelected && Time.timeSinceMillis(selectMillis) < 500) && tryBeginMine(selected)) && !Core.scene.hasKeyboard()){
|
||||
@@ -710,6 +801,10 @@ public class DesktopInput extends InputHandler{
|
||||
commandTap(x, y);
|
||||
}
|
||||
|
||||
if(button == keybinds.get(Binding.command_queue).key){
|
||||
commandTap(x, y, true);
|
||||
}
|
||||
|
||||
return super.touchDown(x, y, pointer, button);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
@@ -46,6 +47,9 @@ import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
//not sure where else to put this - maps unique commands based on position to a list of units that will be turned into a unit group
|
||||
static ObjectMap<Vec2, Seq<Unit>> queuedCommands = new ObjectMap<>();
|
||||
|
||||
/** Used for dropping items. */
|
||||
final static float playerSelectRange = mobile ? 17f : 11f;
|
||||
final static IntSeq removed = new IntSeq();
|
||||
@@ -53,6 +57,18 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
final static int maxLength = 100;
|
||||
final static Rect r1 = new Rect(), r2 = new Rect();
|
||||
final static Seq<Unit> tmpUnits = new Seq<>(false);
|
||||
final static Binding[] controlGroupBindings = {
|
||||
Binding.block_select_01,
|
||||
Binding.block_select_02,
|
||||
Binding.block_select_03,
|
||||
Binding.block_select_04,
|
||||
Binding.block_select_05,
|
||||
Binding.block_select_06,
|
||||
Binding.block_select_07,
|
||||
Binding.block_select_08,
|
||||
Binding.block_select_09,
|
||||
Binding.block_select_10
|
||||
};
|
||||
|
||||
/** If true, there is a cutscene currently occurring in logic. */
|
||||
public boolean logicCutscene;
|
||||
@@ -87,6 +103,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
public boolean commandRect = false;
|
||||
public boolean tappedOne = false;
|
||||
public float commandRectX, commandRectY;
|
||||
/** Groups of units saved to different hotkeys */
|
||||
public IntSeq[] controlGroups = new IntSeq[controlGroupBindings.length];
|
||||
|
||||
private Seq<BuildPlan> plansOut = new Seq<>(BuildPlan.class);
|
||||
private QuadTree<BuildPlan> playerPlanTree = new QuadTree<>(new Rect());
|
||||
@@ -124,6 +142,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
|
||||
Events.on(ResetEvent.class, e -> {
|
||||
logicCutscene = false;
|
||||
Arrays.fill(controlGroups, null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -212,7 +231,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget){
|
||||
public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget, boolean queueCommand, boolean finalBatch){
|
||||
if(player == null || unitIds == null) return;
|
||||
|
||||
//why did I ever think this was a good idea
|
||||
@@ -225,6 +244,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
Teamc teamTarget = buildTarget == null ? unitTarget : buildTarget;
|
||||
Vec2 targetAsVec = new Vec2().set(teamTarget != null ? teamTarget : posTarget);
|
||||
Seq<Unit> toAdd = queuedCommands.get(targetAsVec, Seq::new);
|
||||
boolean anyCommandedTarget = false;
|
||||
|
||||
for(int id : unitIds){
|
||||
Unit unit = Groups.unit.getByID(id);
|
||||
@@ -235,23 +257,78 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
ai.command(UnitCommand.moveCommand);
|
||||
}
|
||||
|
||||
if(teamTarget != null && teamTarget.team() != player.team()){
|
||||
ai.commandTarget(teamTarget);
|
||||
if(teamTarget != null && teamTarget.team() != player.team() &&
|
||||
!(teamTarget instanceof Unit u && !unit.canTarget(u)) && !(teamTarget instanceof Building && !unit.type.targetGround)){
|
||||
|
||||
anyCommandedTarget = true;
|
||||
if(queueCommand){
|
||||
ai.commandQueue(teamTarget);
|
||||
}else{
|
||||
ai.commandQueue.clear();
|
||||
ai.commandTarget(teamTarget);
|
||||
}
|
||||
}else if(posTarget != null){
|
||||
ai.commandPosition(posTarget);
|
||||
if(queueCommand){
|
||||
ai.commandQueue(posTarget);
|
||||
}else{
|
||||
ai.commandQueue.clear();
|
||||
ai.commandPosition(posTarget);
|
||||
}
|
||||
}
|
||||
|
||||
unit.lastCommanded = player.coloredName();
|
||||
if(ai.commandQueue.size <= 0){
|
||||
ai.group = null;
|
||||
}
|
||||
|
||||
//remove when other player command
|
||||
if(!headless && player != Vars.player){
|
||||
control.input.selectedUnits.remove(unit);
|
||||
}
|
||||
|
||||
toAdd.add(unit);
|
||||
}
|
||||
}
|
||||
|
||||
//in the "final batch" of commands, assign formations based on EVERYTHING that was commanded.
|
||||
if(finalBatch){
|
||||
//each physics layer has its own group
|
||||
UnitGroup[] groups = new UnitGroup[PhysicsProcess.layers];
|
||||
var units = queuedCommands.remove(targetAsVec);
|
||||
|
||||
for(Unit unit : units){
|
||||
if(unit.controller() instanceof CommandAI ai){
|
||||
//only assign a group when this is not a queued command
|
||||
if(ai.commandQueue.size == 0 && unitIds.length > 1){
|
||||
int layer = unit.collisionLayer();
|
||||
if(groups[layer] == null){
|
||||
groups[layer] = new UnitGroup();
|
||||
}
|
||||
|
||||
groups[layer].units.add(unit);
|
||||
ai.group = groups[layer];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float minSpeed = 100000000f;
|
||||
for(int i = 0; i < groups.length; i ++){
|
||||
var group = groups[i];
|
||||
if(group != null && group.units.size > 0){
|
||||
group.calculateFormation(targetAsVec, i);
|
||||
minSpeed = Math.min(group.minSpeed, minSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
for(var group : groups){
|
||||
if(group != null){
|
||||
group.minSpeed = minSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(unitIds.length > 0 && player == Vars.player && !state.isPaused()){
|
||||
if(teamTarget != null){
|
||||
if(anyCommandedTarget){
|
||||
Fx.attackCommand.at(teamTarget);
|
||||
}else{
|
||||
Fx.moveCommand.at(posTarget);
|
||||
@@ -283,6 +360,29 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void setUnitStance(Player player, int[] unitIds, UnitStance stance){
|
||||
if(player == null || unitIds == null || stance == null) return;
|
||||
|
||||
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
|
||||
event.unitIDs = unitIds;
|
||||
})){
|
||||
throw new ValidateException(player, "Player cannot command units.");
|
||||
}
|
||||
|
||||
for(int id : unitIds){
|
||||
Unit unit = Groups.unit.getByID(id);
|
||||
if(unit != null && unit.team == player.team() && unit.controller() instanceof CommandAI ai){
|
||||
if(stance == UnitStance.stop){ //not a real stance, just cancels orders
|
||||
ai.clearCommands();
|
||||
}else{
|
||||
ai.stance = stance;
|
||||
}
|
||||
unit.lastCommanded = player.coloredName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void commandBuilding(Player player, int[] buildings, Vec2 target){
|
||||
if(player == null || target == null) return;
|
||||
@@ -457,6 +557,31 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void unitEnteredPayload(Unit unit, Building build){
|
||||
if(unit == null || build == null || unit.team != build.team) return;
|
||||
|
||||
unit.remove();
|
||||
|
||||
//reset the enter command
|
||||
if(unit.controller() instanceof CommandAI ai && ai.command == UnitCommand.enterPayloadCommand){
|
||||
ai.clearCommands();
|
||||
ai.command = UnitCommand.moveCommand;
|
||||
}
|
||||
|
||||
//clear removed state of unit so it can be synced
|
||||
if(Vars.net.client()){
|
||||
Vars.netClient.clearRemovedEntity(unit.id);
|
||||
}
|
||||
|
||||
UnitPayload unitPay = new UnitPayload(unit);
|
||||
|
||||
if(build.acceptPayload(build, unitPay)){
|
||||
Fx.unitDrop.at(build);
|
||||
build.handlePayload(build, unitPay);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, called = Loc.server)
|
||||
public static void dropItem(Player player, float angle){
|
||||
if(player == null) return;
|
||||
@@ -490,7 +615,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
|
||||
@Remote(targets = Loc.both, called = Loc.both, forward = true)
|
||||
public static void tileConfig(@Nullable Player player, Building build, @Nullable Object value){
|
||||
if(build == null && net.server()) throw new ValidateException(player, "building is null");
|
||||
if(build == null) return;
|
||||
|
||||
if(net.server() && (!Units.canInteract(player, build) ||
|
||||
!netServer.admins.allowAction(player, ActionType.configure, build.tile, action -> action.config = value))){
|
||||
|
||||
@@ -527,7 +654,13 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
if(player.team() == build.team && build.canControlSelect(player.unit())){
|
||||
var before = player.unit();
|
||||
|
||||
build.onControlSelect(player.unit());
|
||||
|
||||
if(!before.dead && before.spawnedByCore && !before.isPlayer()){
|
||||
Call.unitDespawn(before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,6 +705,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
//direct dock transfer???
|
||||
unit.dockedType = before.dockedType;
|
||||
}
|
||||
|
||||
if(before.spawnedByCore && !before.isPlayer()){
|
||||
Call.unitDespawn(before);
|
||||
}
|
||||
}
|
||||
|
||||
Time.run(Fx.unitSpirit.lifetime, () -> Fx.unitControl.at(unit.x, unit.y, 0f, unit));
|
||||
@@ -850,6 +987,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
public void commandTap(float screenX, float screenY){
|
||||
commandTap(screenX, screenY, false);
|
||||
}
|
||||
|
||||
public void commandTap(float screenX, float screenY, boolean queue){
|
||||
if(commandMode){
|
||||
//right click: move to position
|
||||
|
||||
@@ -878,10 +1019,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
if(ids.length > maxChunkSize){
|
||||
for(int i = 0; i < ids.length; i += maxChunkSize){
|
||||
int[] data = Arrays.copyOfRange(ids, i, Math.min(i + maxChunkSize, ids.length));
|
||||
Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target);
|
||||
Call.commandUnits(player, data, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, i + maxChunkSize >= ids.length);
|
||||
}
|
||||
}else{
|
||||
Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target);
|
||||
Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target, queue, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,6 +1044,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
//draw command overlay UI
|
||||
for(Unit unit : selectedUnits){
|
||||
CommandAI ai = unit.command();
|
||||
Position lastPos = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
|
||||
|
||||
//draw target line
|
||||
if(ai.targetPos != null && ai.currentCommand().drawTarget){
|
||||
Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
|
||||
@@ -918,6 +1061,24 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
if(ai.attackTarget != null && ai.currentCommand().drawTarget){
|
||||
Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove);
|
||||
}
|
||||
|
||||
if(lastPos == null){
|
||||
lastPos = unit;
|
||||
}
|
||||
|
||||
//draw command queue
|
||||
if(ai.currentCommand().drawTarget && ai.commandQueue.size > 0){
|
||||
for(var next : ai.commandQueue){
|
||||
Drawf.limitLine(lastPos, next, 3.5f, 3.5f);
|
||||
lastPos = next;
|
||||
|
||||
if(next instanceof Vec2 vec){
|
||||
Drawf.square(vec.x, vec.y, 3.5f);
|
||||
}else{
|
||||
Drawf.target(next.getX(), next.getY(), 6f, Pal.remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(var commandBuild : commandBuildings){
|
||||
@@ -1149,6 +1310,10 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
|
||||
protected void drawBreakSelection(int x1, int y1, int x2, int y2, int maxLength){
|
||||
drawBreakSelection(x1, y1, x2, y2, maxLength, true);
|
||||
}
|
||||
|
||||
protected void drawBreakSelection(int x1, int y1, int x2, int y2, int maxLength, boolean useSelectPlans){
|
||||
NormalizeDrawResult result = Placement.normalizeDrawArea(Blocks.air, x1, y1, x2, y2, false, maxLength, 1f);
|
||||
NormalizeResult dresult = Placement.normalizeArea(x1, y1, x2, y2, rotation, false, maxLength);
|
||||
|
||||
@@ -1167,16 +1332,16 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
Lines.stroke(1f);
|
||||
|
||||
for(var plan : player.unit().plans()){
|
||||
if(plan.breaking) continue;
|
||||
if(plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
drawBreaking(plan);
|
||||
}
|
||||
}
|
||||
|
||||
for(var plan : selectPlans){
|
||||
if(plan.breaking) continue;
|
||||
if(plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
drawBreaking(plan);
|
||||
if(useSelectPlans){
|
||||
for(var plan : selectPlans){
|
||||
if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
drawBreaking(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1338,11 +1503,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
}
|
||||
|
||||
it = selectPlans.iterator();
|
||||
while(it.hasNext()){
|
||||
var plan = it.next();
|
||||
if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
it.remove();
|
||||
//don't remove plans on desktop, where flushing is false
|
||||
if(flush){
|
||||
it = selectPlans.iterator();
|
||||
while(it.hasNext()){
|
||||
var plan = it.next();
|
||||
if(!plan.breaking && plan.bounds(Tmp.r2).overlaps(Tmp.r1)){
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1413,7 +1581,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
consumed = true;
|
||||
if((!config.isShown() && build.shouldShowConfigure(player)) //if the config fragment is hidden, show
|
||||
//alternatively, the current selected block can 'agree' to switch config tiles
|
||||
|| (config.isShown() && config.getSelected().onConfigureBuildTapped(build))){
|
||||
|| (config.isShown() && config.getSelected().onConfigureBuildTapped(build) && build.shouldShowConfigure(player))){
|
||||
Sounds.click.at(build);
|
||||
config.showConfig(build);
|
||||
}
|
||||
@@ -1492,6 +1660,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean tryRepairDerelict(Tile selected){
|
||||
if(selected != null && selected.build != null && selected.build.block.unlockedNow() && selected.build.team == Team.derelict && Build.validPlace(selected.block(), player.team(), selected.build.tileX(), selected.build.tileY(), selected.build.rotation)){
|
||||
player.unit().addBuild(new BuildPlan(selected.build.tileX(), selected.build.tileY(), selected.build.rotation, selected.block(), selected.build.config()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canMine(Tile tile){
|
||||
return !Core.scene.hasMouse()
|
||||
&& player.unit().validMine(tile)
|
||||
@@ -1797,11 +1973,13 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
var end = world.build(endX, endY);
|
||||
if(diagonal && (block == null || block.allowDiagonal)){
|
||||
if(block != null && start instanceof ChainedBuilding && end instanceof ChainedBuilding
|
||||
&& block.canReplace(end.block) && block.canReplace(start.block)){
|
||||
&& block.canReplace(end.block) && block.canReplace(start.block)){
|
||||
points = Placement.upgradeLine(startX, startY, endX, endY);
|
||||
}else{
|
||||
points = Placement.pathfindLine(block != null && block.conveyorPlacement, startX, startY, endX, endY);
|
||||
}
|
||||
}else if(block != null && block.allowRectanglePlacement){
|
||||
points = Placement.normalizeRectangle(startX, startY, endX, endY, block.size);
|
||||
}else{
|
||||
points = Placement.normalizeLine(startX, startY, endX, endY);
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
public Seq<BuildPlan> removals = new Seq<>();
|
||||
/** Whether the player is currently shifting all placed tiles. */
|
||||
public boolean selecting;
|
||||
/** Whether the player is currently in line-place mode. */
|
||||
public boolean lineMode, schematicMode, rebuildMode;
|
||||
/** Various modes that aren't enums for some reason. This should be cleaned up. */
|
||||
public boolean lineMode, schematicMode, rebuildMode, queueCommandMode;
|
||||
/** Current place mode. */
|
||||
public PlaceMode mode = none;
|
||||
/** Whether no recipe was available when switching to break mode. */
|
||||
@@ -287,9 +287,14 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
group.fill(t -> {
|
||||
t.visible(() -> !showCancel() && block == null && !hasSchem());
|
||||
t.bottom().left();
|
||||
t.button("@command", Icon.units, Styles.squareTogglet, () -> {
|
||||
|
||||
t.button("@command.queue", Icon.rightOpen, Styles.clearTogglet, () -> {
|
||||
queueCommandMode = !queueCommandMode;
|
||||
}).width(155f).height(48f).margin(12f).checked(b -> queueCommandMode).visible(() -> commandMode).row();
|
||||
|
||||
t.button("@command", Icon.units, Styles.clearTogglet, () -> {
|
||||
commandMode = !commandMode;
|
||||
}).width(155f).height(50f).margin(12f).checked(b -> commandMode).row();
|
||||
}).width(155f).height(48f).margin(12f).checked(b -> commandMode);
|
||||
|
||||
//for better looking insets
|
||||
t.rect((x, y, w, h) -> {
|
||||
@@ -681,7 +686,7 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
selectPlans.add(new BuildPlan(linked.x, linked.y));
|
||||
}else if((commandMode && selectedUnits.size > 0) || commandBuildings.size > 0){
|
||||
//handle selecting units with command mode
|
||||
commandTap(x, y);
|
||||
commandTap(x, y, queueCommandMode);
|
||||
}else if(commandMode){
|
||||
tapCommandUnit();
|
||||
}else{
|
||||
@@ -707,7 +712,7 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
buildingTapped = selectedControlBuild();
|
||||
|
||||
//prevent mining if placing/breaking blocks
|
||||
if(!tryStopMine() && !canTapPlayer(worldx, worldy) && !checkConfigTap() && !tileTapped(linked.build) && mode == none && !Core.settings.getBool("doubletapmine")){
|
||||
if(!tryRepairDerelict(cursor) && !tryStopMine() && !canTapPlayer(worldx, worldy) && !checkConfigTap() && !tileTapped(linked.build) && mode == none && !Core.settings.getBool("doubletapmine")){
|
||||
tryBeginMine(cursor);
|
||||
}
|
||||
}
|
||||
@@ -734,6 +739,15 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
|
||||
boolean locked = locked();
|
||||
|
||||
if(!commandMode){
|
||||
queueCommandMode = false;
|
||||
}
|
||||
|
||||
//cannot rebuild and place at the same time
|
||||
if(block != null){
|
||||
rebuildMode = false;
|
||||
}
|
||||
|
||||
if(player.dead()){
|
||||
mode = none;
|
||||
manualShooting = false;
|
||||
@@ -757,7 +771,7 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
renderer.scaleCamera(Core.input.axisTap(Binding.zoom));
|
||||
}
|
||||
|
||||
if(!Core.settings.getBool("keyboard") && !locked){
|
||||
if(!Core.settings.getBool("keyboard") && !locked && !scene.hasKeyboard()){
|
||||
//move camera around
|
||||
float camSpeed = 6f;
|
||||
Core.camera.position.add(Tmp.v1.setZero().add(Core.input.axis(Binding.move_x), Core.input.axis(Binding.move_y)).nor().scl(Time.delta * camSpeed));
|
||||
@@ -923,6 +937,8 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
Core.camera.position.y -= deltaY;
|
||||
}
|
||||
|
||||
camera.position.clamp(-camera.width/4f, -camera.height/4f, world.unitWidth() + camera.width/4f, world.unitHeight() + camera.height/4f);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -956,7 +972,6 @@ public class MobileInput extends InputHandler implements GestureListener{
|
||||
boolean allowHealing = type.canHeal;
|
||||
boolean validHealTarget = allowHealing && target instanceof Building b && b.isValid() && target.team() == unit.team && b.damaged() && target.within(unit, type.range);
|
||||
boolean boosted = (unit instanceof Mechc && unit.isFlying());
|
||||
|
||||
//reset target if:
|
||||
// - in the editor, or...
|
||||
// - it's both an invalid standard target and an invalid heal target
|
||||
|
||||
@@ -58,6 +58,22 @@ public class Placement{
|
||||
return points;
|
||||
}
|
||||
|
||||
/** Normalize two points into a rectangle. */
|
||||
public static Seq<Point2> normalizeRectangle(int startX, int startY, int endX, int endY, int blockSize){
|
||||
Pools.freeAll(points);
|
||||
points.clear();
|
||||
|
||||
int minX = Math.min(startX, endX), minY = Math.min(startY, endY), maxX = Math.max(startX, endX), maxY = Math.max(startY, endY);
|
||||
|
||||
for(int y = 0; y <= maxY - minY; y += blockSize){
|
||||
for(int x = 0; x <= maxX - minX; x += blockSize){
|
||||
points.add(Pools.obtain(Point2.class, Point2::new).set(startX + x * Mathf.sign(endX - startX), startY + y * Mathf.sign(endY - startY)));
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
public static Seq<Point2> upgradeLine(int startX, int startY, int endX, int endY){
|
||||
closed.clear();
|
||||
Pools.freeAll(points);
|
||||
@@ -113,18 +129,22 @@ public class Placement{
|
||||
}
|
||||
|
||||
public static void calculateBridges(Seq<BuildPlan> plans, ItemBridge bridge){
|
||||
if(isSidePlace(plans)) return;
|
||||
calculateBridges(plans, bridge, t -> false);
|
||||
}
|
||||
|
||||
public static void calculateBridges(Seq<BuildPlan> plans, ItemBridge bridge, Boolf<Block> avoid){
|
||||
if(isSidePlace(plans) || plans.size == 0) return;
|
||||
|
||||
//check for orthogonal placement + unlocked state
|
||||
if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){
|
||||
return;
|
||||
}
|
||||
|
||||
Boolf<BuildPlan> placeable = plan -> (plan.placeable(player.team())) ||
|
||||
(plan.tile() != null && plan.tile().block() == plan.block); //don't count the same block as inaccessible
|
||||
Boolf<BuildPlan> placeable = plan ->
|
||||
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
|
||||
!(plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
|
||||
|
||||
var result = plans1.clear();
|
||||
var team = player.team();
|
||||
var rotated = plans.first().tile() != null && plans.first().tile().absoluteRelativeTo(plans.peek().x, plans.peek().y) == Mathf.mod(plans.first().rotation + 2, 4);
|
||||
|
||||
outer:
|
||||
@@ -134,6 +154,7 @@ public class Placement{
|
||||
|
||||
//gap found
|
||||
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
|
||||
boolean wereSame = true;
|
||||
|
||||
//find the closest valid position within range
|
||||
for(int j = i + 1; j < plans.size; j++){
|
||||
@@ -147,18 +168,29 @@ public class Placement{
|
||||
}
|
||||
i = j;
|
||||
continue outer;
|
||||
}else if(other.placeable(team)){
|
||||
//found a link, assign bridges
|
||||
cur.block = bridge;
|
||||
other.block = bridge;
|
||||
if(rotated){
|
||||
other.config = new Point2(cur.x - other.x, cur.y - other.y);
|
||||
}else{
|
||||
cur.config = new Point2(other.x - cur.x, other.y - cur.y);
|
||||
}
|
||||
}else if(placeable.get(other)){
|
||||
|
||||
i = j;
|
||||
continue outer;
|
||||
if(wereSame){
|
||||
//the gap is fake, it's just conveyors that can be replaced with junctions
|
||||
i ++;
|
||||
continue outer;
|
||||
}else{
|
||||
//found a link, assign bridges
|
||||
cur.block = bridge;
|
||||
other.block = bridge;
|
||||
if(rotated){
|
||||
other.config = new Point2(cur.x - other.x, cur.y - other.y);
|
||||
}else{
|
||||
cur.config = new Point2(other.x - cur.x, other.y - cur.y);
|
||||
}
|
||||
|
||||
i = j;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if(other.tile() != null && !avoid.get(other.tile().block())){
|
||||
wereSame = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,21 +207,17 @@ public class Placement{
|
||||
plans.set(result);
|
||||
}
|
||||
|
||||
public static void calculateBridges(Seq<BuildPlan> plans, DirectionBridge bridge, boolean hasJunction, Boolf<Block> same){
|
||||
if(isSidePlace(plans)) return;
|
||||
public static void calculateBridges(Seq<BuildPlan> plans, DirectionBridge bridge, boolean hasJunction, Boolf<Block> avoid){
|
||||
if(isSidePlace(plans) || plans.size == 0) return;
|
||||
|
||||
//check for orthogonal placement + unlocked state
|
||||
if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){
|
||||
return;
|
||||
}
|
||||
|
||||
Boolf<BuildPlan> rotated = plan -> plan.build() != null && same.get(plan.build().block) && plan.rotation != plan.build().rotation;
|
||||
|
||||
//TODO for chains of ducts, do not count consecutives in a different rotation as 'placeable'
|
||||
Boolf<BuildPlan> placeable = plan ->
|
||||
!(!hasJunction && rotated.get(plan)) &&
|
||||
(plan.placeable(player.team()) ||
|
||||
(plan.tile() != null && same.get(plan.tile().block()))); //don't count the same block as inaccessible
|
||||
(plan.placeable(player.team()) || (plan.tile() != null && plan.tile().block() == plan.block)) && //don't count the same block as inaccessible
|
||||
!(plan.build() != null && plan.build().rotation != plan.rotation && avoid.get(plan.tile().block()));
|
||||
|
||||
var result = plans1.clear();
|
||||
|
||||
@@ -199,10 +227,11 @@ public class Placement{
|
||||
result.add(cur);
|
||||
|
||||
//gap found
|
||||
if(i < plans.size - 1 && placeable.get(cur) && (!placeable.get(plans.get(i + 1)) || (hasJunction && rotated.get(plans.get(i + 1)) && i < plans.size - 2 && !placeable.get(plans.get(i + 2))))){
|
||||
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
|
||||
boolean wereSame = true;
|
||||
|
||||
//find the closest valid position within range
|
||||
for(int j = i + 2; j < plans.size; j++){
|
||||
for(int j = i + 1; j < plans.size; j++){
|
||||
var other = plans.get(j);
|
||||
|
||||
//out of range now, set to current position and keep scanning forward for next occurrence
|
||||
@@ -214,12 +243,22 @@ public class Placement{
|
||||
i = j;
|
||||
continue outer;
|
||||
}else if(placeable.get(other)){
|
||||
//found a link, assign bridges
|
||||
cur.block = bridge;
|
||||
other.block = bridge;
|
||||
|
||||
i = j;
|
||||
continue outer;
|
||||
if(wereSame && hasJunction){
|
||||
//the gap is fake, it's just conveyors that can be replaced with junctions
|
||||
i ++;
|
||||
continue outer;
|
||||
}else{
|
||||
//found a link, assign bridges
|
||||
cur.block = bridge;
|
||||
other.block = bridge;
|
||||
i = j;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if(other.tile() != null && !avoid.get(other.tile().block())){
|
||||
wereSame = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,15 @@ public class JsonIO{
|
||||
}
|
||||
};
|
||||
|
||||
public static void writeBytes(Object value, Class<?> elementType, DataOutputStream output){
|
||||
json.setWriter(new UBJsonWriter(output));
|
||||
json.writeValue(value, value == null ? null : value.getClass(), elementType);
|
||||
}
|
||||
|
||||
public static <T> T readBytes(Class<T> type, Class<?> elementType, DataInputStream input) throws IOException{
|
||||
return json.readValue(type, elementType, new UBJsonReader().parseWihoutClosing(input));
|
||||
}
|
||||
|
||||
public static String write(Object object){
|
||||
return json.toJson(object, object.getClass());
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class SaveIO{
|
||||
/** Save format header. */
|
||||
public static final byte[] header = {'M', 'S', 'A', 'V'};
|
||||
public static final IntMap<SaveVersion> versions = new IntMap<>();
|
||||
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7());
|
||||
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8());
|
||||
|
||||
static{
|
||||
for(SaveVersion version : versionArray){
|
||||
@@ -56,8 +56,13 @@ public class SaveIO{
|
||||
}
|
||||
|
||||
public static boolean isSaveValid(Fi file){
|
||||
return isSaveFileValid(file) || isSaveFileValid(backupFileFor(file));
|
||||
}
|
||||
|
||||
private static boolean isSaveFileValid(Fi file){
|
||||
try(DataInputStream stream = new DataInputStream(new InflaterInputStream(file.read(bufferSize)))){
|
||||
return isSaveValid(stream);
|
||||
getMeta(stream);
|
||||
return true;
|
||||
}catch(Throwable e){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.maps.Map;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
@@ -74,6 +75,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
try{
|
||||
region("map", stream, counter, in -> readMap(in, context));
|
||||
region("entities", stream, counter, this::readEntities);
|
||||
if(version >= 8) region("markers", stream, counter, this::readMarkers);
|
||||
region("custom", stream, counter, this::readCustomChunks);
|
||||
}finally{
|
||||
content.setTemporaryMapper(null);
|
||||
@@ -85,6 +87,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
region("content", stream, this::writeContentHeader);
|
||||
region("map", stream, this::writeMap);
|
||||
region("entities", stream, this::writeEntities);
|
||||
region("markers", stream, this::writeMarkers);
|
||||
region("custom", stream, s -> writeCustomChunks(s, false));
|
||||
}
|
||||
|
||||
@@ -124,7 +127,10 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
node.save();
|
||||
}
|
||||
|
||||
writeStringMap(stream, StringMap.of(
|
||||
StringMap result = new StringMap();
|
||||
result.putAll(tags);
|
||||
|
||||
writeStringMap(stream, result.merge(StringMap.of(
|
||||
"saved", Time.millis(),
|
||||
"playtime", headless ? 0 : control.saves.getTotalPlaytime(),
|
||||
"build", Version.build,
|
||||
@@ -134,14 +140,16 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
"wavetime", state.wavetime,
|
||||
"stats", JsonIO.write(state.stats),
|
||||
"rules", JsonIO.write(state.rules),
|
||||
"locales", JsonIO.write(state.mapLocales),
|
||||
"mods", JsonIO.write(mods.getModStrings().toArray(String.class)),
|
||||
"controlGroups", headless || control == null ? "null" : JsonIO.write(control.input.controlGroups),
|
||||
"width", world.width(),
|
||||
"height", world.height(),
|
||||
"viewpos", Tmp.v1.set(player == null ? Vec2.ZERO : player).toString(),
|
||||
"controlledType", headless || control.input.controlledType == null ? "null" : control.input.controlledType.name,
|
||||
"nocores", state.rules.defaultTeam.cores().isEmpty(),
|
||||
"playerteam", player == null ? state.rules.defaultTeam.id : player.team().id
|
||||
).merge(tags));
|
||||
)));
|
||||
}
|
||||
|
||||
public void readMeta(DataInput stream, WorldContext context) throws IOException{
|
||||
@@ -152,6 +160,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
state.tick = map.getFloat("tick");
|
||||
state.stats = JsonIO.read(GameStats.class, map.get("stats", "{}"));
|
||||
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
|
||||
state.mapLocales = JsonIO.read(MapLocales.class, map.get("locales", "{}"));
|
||||
if(state.rules.spawns.isEmpty()) state.rules.spawns = waves.get();
|
||||
lastReadBuild = map.getInt("build", -1);
|
||||
|
||||
@@ -177,6 +186,11 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
if(!net.client() && team != Team.derelict){
|
||||
player.team(team);
|
||||
}
|
||||
|
||||
var groups = JsonIO.read(IntSeq[].class, map.get("controlGroups", "null"));
|
||||
if(groups != null && groups.length == control.input.controlGroups.length){
|
||||
control.input.controlGroups = groups;
|
||||
}
|
||||
}
|
||||
|
||||
Map worldmap = maps.byName(map.get("mapname", "\\\\\\"));
|
||||
@@ -386,6 +400,14 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
writeWorldEntities(stream);
|
||||
}
|
||||
|
||||
public void writeMarkers(DataOutput stream) throws IOException{
|
||||
state.markers.write(stream);
|
||||
}
|
||||
|
||||
public void readMarkers(DataInput stream) throws IOException{
|
||||
state.markers.read(stream);
|
||||
}
|
||||
|
||||
public void readTeamBlocks(DataInput stream) throws IOException{
|
||||
int teamc = stream.readInt();
|
||||
|
||||
@@ -413,6 +435,8 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
//entityMapping is null in older save versions, so use the default
|
||||
var mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping;
|
||||
|
||||
Seq<Entityc> entities = new Seq<>();
|
||||
|
||||
int amount = stream.readInt();
|
||||
for(int j = 0; j < amount; j++){
|
||||
readChunk(stream, true, in -> {
|
||||
@@ -425,12 +449,17 @@ public abstract class SaveVersion extends SaveFileReader{
|
||||
int id = in.readInt();
|
||||
|
||||
Entityc entity = (Entityc)mapping[typeid].get();
|
||||
entities.add(entity);
|
||||
EntityGroup.checkNextId(id);
|
||||
entity.id(id);
|
||||
entity.read(Reads.get(in));
|
||||
entity.add();
|
||||
});
|
||||
}
|
||||
|
||||
for(var e : entities){
|
||||
e.afterAllRead();
|
||||
}
|
||||
}
|
||||
|
||||
public void readEntityMapping(DataInput stream) throws IOException{
|
||||
|
||||
@@ -16,6 +16,7 @@ import mindustry.entities.abilities.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.net.Administration.*;
|
||||
@@ -203,7 +204,7 @@ public class TypeIO{
|
||||
for(int i = 0; i < objlen; i++) objs[i] = readObjectBoxed(read, box);
|
||||
yield objs;
|
||||
}
|
||||
case 23 -> UnitCommand.all.get(read.us());
|
||||
case 23 -> content.unitCommand(read.us());
|
||||
default -> throw new IllegalArgumentException("Unknown object type: " + type);
|
||||
};
|
||||
}
|
||||
@@ -311,7 +312,17 @@ public class TypeIO{
|
||||
|
||||
public static @Nullable UnitCommand readCommand(Reads read){
|
||||
int val = read.ub();
|
||||
return val == 255 ? null : UnitCommand.all.get(val);
|
||||
return val == 255 ? null : content.unitCommand(val);
|
||||
}
|
||||
|
||||
public static void writeStance(Writes write, @Nullable UnitStance stance){
|
||||
write.b(stance == null ? 255 : stance.id);
|
||||
}
|
||||
|
||||
public static UnitStance readStance(Reads read){
|
||||
int val = read.ub();
|
||||
//never returns null
|
||||
return val == 255 || val >= content.unitStances().size ? UnitStance.shoot : content.unitStance(val);
|
||||
}
|
||||
|
||||
public static void writeEntity(Writes write, Entityc entity){
|
||||
@@ -472,7 +483,7 @@ public class TypeIO{
|
||||
write.b(3);
|
||||
write.i(logic.controller.pos());
|
||||
}else if(control instanceof CommandAI ai){
|
||||
write.b(6);
|
||||
write.b(8);
|
||||
write.bool(ai.attackTarget != null);
|
||||
write.bool(ai.targetPos != null);
|
||||
|
||||
@@ -489,6 +500,26 @@ public class TypeIO{
|
||||
}
|
||||
}
|
||||
write.b(ai.command == null ? -1 : ai.command.id);
|
||||
|
||||
write.b(ai.commandQueue.size);
|
||||
for(var pos : ai.commandQueue){
|
||||
if(pos instanceof Building b){
|
||||
write.b(0);
|
||||
write.i(b.pos());
|
||||
}else if(pos instanceof Unit u){
|
||||
write.b(1);
|
||||
write.i(u.id);
|
||||
}else if(pos instanceof Vec2 v){
|
||||
write.b(2);
|
||||
write.f(v.x);
|
||||
write.f(v.y);
|
||||
}else{
|
||||
//who put garbage in the command queue??
|
||||
write.b(3);
|
||||
}
|
||||
}
|
||||
|
||||
writeStance(write, ai.stance);
|
||||
}else if(control instanceof AssemblerAI){ //hate
|
||||
write.b(5);
|
||||
}else{
|
||||
@@ -520,8 +551,8 @@ public class TypeIO{
|
||||
out.controller = world.build(pos);
|
||||
return out;
|
||||
}
|
||||
//type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte.
|
||||
}else if(type == 4 || type == 6){
|
||||
//type 4 is the old CommandAI with no commandIndex, type 6 is the new one with the index as a single byte, type 7 is the one with the command queue, 8 adds a stance
|
||||
}else if(type == 4 || type == 6 || type == 7 || type == 8){
|
||||
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
|
||||
|
||||
boolean hasAttack = read.bool(), hasPos = read.bool();
|
||||
@@ -532,21 +563,50 @@ public class TypeIO{
|
||||
ai.targetPos = null;
|
||||
}
|
||||
ai.setupLastPos();
|
||||
ai.readAttackTarget = -1;
|
||||
|
||||
if(hasAttack){
|
||||
byte entityType = read.b();
|
||||
if(entityType == 1){
|
||||
ai.attackTarget = world.build(read.i());
|
||||
}else{
|
||||
ai.attackTarget = Groups.unit.getByID(read.i());
|
||||
ai.attackTarget = Groups.unit.getByID(ai.readAttackTarget = read.i());
|
||||
}
|
||||
}else{
|
||||
ai.attackTarget = null;
|
||||
}
|
||||
|
||||
if(type == 6){
|
||||
if(type == 6 || type == 7 || type == 8){
|
||||
byte id = read.b();
|
||||
ai.command = id < 0 ? null : UnitCommand.all.get(id);
|
||||
ai.command = id < 0 ? null : content.unitCommand(id);
|
||||
if(ai.command == null) ai.command = UnitCommand.moveCommand;
|
||||
}
|
||||
|
||||
//command queue only in type 7
|
||||
if(type == 7 || type == 8){
|
||||
ai.commandQueue.clear();
|
||||
int length = read.ub();
|
||||
for(int i = 0; i < length; i++){
|
||||
int commandType = read.b();
|
||||
switch(commandType){
|
||||
case 0 -> {
|
||||
var build = world.build(read.i());
|
||||
if(build != null) ai.commandQueue.add(build);
|
||||
}
|
||||
case 1 -> {
|
||||
var unit = Groups.unit.getByID(read.i());
|
||||
if(unit != null) ai.commandQueue.add(unit);
|
||||
}
|
||||
case 2 -> {
|
||||
ai.commandQueue.add(new Vec2(read.f(), read.f()));
|
||||
}
|
||||
//otherwise disregard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(type == 8){
|
||||
ai.stance = readStance(read);
|
||||
}
|
||||
|
||||
return ai;
|
||||
@@ -571,6 +631,14 @@ public class TypeIO{
|
||||
return KickReason.values()[read.b()];
|
||||
}
|
||||
|
||||
public static void writeMarkerControl(Writes write, LMarkerControl reason){
|
||||
write.b((byte)reason.ordinal());
|
||||
}
|
||||
|
||||
public static LMarkerControl readMarkerControl(Reads read){
|
||||
return LMarkerControl.all[read.ub()];
|
||||
}
|
||||
|
||||
public static void writeRules(Writes write, Rules rules){
|
||||
String string = JsonIO.write(rules);
|
||||
byte[] bytes = string.getBytes(charset);
|
||||
@@ -597,6 +665,19 @@ public class TypeIO{
|
||||
return JsonIO.read(MapObjectives.class, string);
|
||||
}
|
||||
|
||||
public static void writeObjectiveMarker(Writes write, ObjectiveMarker marker){
|
||||
String string = JsonIO.json.toJson(marker, MapObjectives.ObjectiveMarker.class);
|
||||
byte[] bytes = string.getBytes(charset);
|
||||
write.i(bytes.length);
|
||||
write.b(bytes);
|
||||
}
|
||||
|
||||
public static ObjectiveMarker readObjectiveMarker(Reads read){
|
||||
int length = read.i();
|
||||
String string = new String(read.b(new byte[length]), charset);
|
||||
return JsonIO.read(MapObjectives.ObjectiveMarker.class, string);
|
||||
}
|
||||
|
||||
public static void writeVecNullable(Writes write, @Nullable Vec2 v){
|
||||
if(v == null){
|
||||
write.f(Float.NaN);
|
||||
@@ -633,10 +714,50 @@ public class TypeIO{
|
||||
public static void writeStatus(Writes write, StatusEntry entry){
|
||||
write.s(entry.effect.id);
|
||||
write.f(entry.time);
|
||||
|
||||
//write dynamic fields
|
||||
if(entry.effect.dynamic){
|
||||
//write a byte with bits set based on which field is actually used
|
||||
write.b(
|
||||
(entry.damageMultiplier != 1f ? (1 << 0) : 0) |
|
||||
(entry.healthMultiplier != 1f ? (1 << 1) : 0) |
|
||||
(entry.speedMultiplier != 1f ? (1 << 2) : 0) |
|
||||
(entry.reloadMultiplier != 1f ? (1 << 3) : 0) |
|
||||
(entry.buildSpeedMultiplier != 1f ? (1 << 4) : 0) |
|
||||
(entry.dragMultiplier != 1f ? (1 << 5) : 0) |
|
||||
(entry.armorOverride >= 0f ? (1 << 6) : 0)
|
||||
);
|
||||
|
||||
if(entry.damageMultiplier != 1f) write.f(entry.damageMultiplier);
|
||||
if(entry.healthMultiplier != 1f) write.f(entry.healthMultiplier);
|
||||
if(entry.speedMultiplier != 1f) write.f(entry.speedMultiplier);
|
||||
if(entry.reloadMultiplier != 1f) write.f(entry.reloadMultiplier);
|
||||
if(entry.buildSpeedMultiplier != 1f) write.f(entry.buildSpeedMultiplier);
|
||||
if(entry.dragMultiplier != 1f) write.f(entry.dragMultiplier);
|
||||
if(entry.armorOverride >= 0f) write.f(entry.armorOverride);
|
||||
}
|
||||
}
|
||||
|
||||
public static StatusEntry readStatus(Reads read){
|
||||
return new StatusEntry().set(content.getByID(ContentType.status, read.s()), read.f());
|
||||
short id = read.s();
|
||||
float time = read.f();
|
||||
|
||||
StatusEntry result = new StatusEntry().set(content.getByID(ContentType.status, id), time);
|
||||
|
||||
if(result.effect.dynamic){
|
||||
//read flags that store which fields are set
|
||||
int flags = read.ub();
|
||||
|
||||
if((flags & (1 << 0)) != 0) result.damageMultiplier = read.f();
|
||||
if((flags & (1 << 1)) != 0) result.healthMultiplier = read.f();
|
||||
if((flags & (1 << 2)) != 0) result.speedMultiplier = read.f();
|
||||
if((flags & (1 << 3)) != 0) result.reloadMultiplier = read.f();
|
||||
if((flags & (1 << 4)) != 0) result.buildSpeedMultiplier = read.f();
|
||||
if((flags & (1 << 5)) != 0) result.dragMultiplier = read.f();
|
||||
if((flags & (1 << 6)) != 0) result.armorOverride = read.f();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void writeItems(Writes write, ItemStack stack){
|
||||
|
||||
10
core/src/mindustry/io/versions/Save8.java
Normal file
10
core/src/mindustry/io/versions/Save8.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package mindustry.io.versions;
|
||||
|
||||
import mindustry.io.*;
|
||||
|
||||
public class Save8 extends SaveVersion{
|
||||
|
||||
public Save8(){
|
||||
super(8);
|
||||
}
|
||||
}
|
||||
@@ -27,34 +27,63 @@ public class GlobalVars{
|
||||
public static final Rand rand = new Rand();
|
||||
|
||||
//non-constants that depend on state
|
||||
private static int varTime, varTick, varSecond, varMinute, varWave, varWaveTime;
|
||||
private static int varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varMapW, varMapH, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
|
||||
|
||||
private ObjectIntMap<String> namesToIds = new ObjectIntMap<>();
|
||||
private Seq<Var> vars = new Seq<>(Var.class);
|
||||
private Seq<VarEntry> varEntries = new Seq<>();
|
||||
private IntSet privilegedIds = new IntSet();
|
||||
private UnlockableContent[][] logicIdToContent;
|
||||
private int[][] contentIdToLogicId;
|
||||
|
||||
public void init(){
|
||||
put("the end", null);
|
||||
putEntryOnly("sectionProcessor");
|
||||
|
||||
putEntryOnly("@this");
|
||||
putEntryOnly("@thisx");
|
||||
putEntryOnly("@thisy");
|
||||
putEntryOnly("@links");
|
||||
putEntryOnly("@ipt");
|
||||
|
||||
putEntryOnly("sectionGeneral");
|
||||
|
||||
put("the end", null, false, true);
|
||||
//add default constants
|
||||
put("false", 0);
|
||||
put("true", 1);
|
||||
put("null", null);
|
||||
putEntry("false", 0);
|
||||
putEntry("true", 1);
|
||||
put("null", null, false, true);
|
||||
|
||||
//math
|
||||
put("@pi", Mathf.PI);
|
||||
put("π", Mathf.PI); //for the "cool" kids
|
||||
put("@e", Mathf.E);
|
||||
put("@degToRad", Mathf.degRad);
|
||||
put("@radToDeg", Mathf.radDeg);
|
||||
putEntry("@pi", Mathf.PI);
|
||||
put("π", Mathf.PI, false, true); //for the "cool" kids
|
||||
putEntry("@e", Mathf.E);
|
||||
putEntry("@degToRad", Mathf.degRad);
|
||||
putEntry("@radToDeg", Mathf.radDeg);
|
||||
|
||||
putEntryOnly("sectionMap");
|
||||
|
||||
//time
|
||||
varTime = put("@time", 0);
|
||||
varTick = put("@tick", 0);
|
||||
varSecond = put("@second", 0);
|
||||
varMinute = put("@minute", 0);
|
||||
varWave = put("@waveNumber", 0);
|
||||
varWaveTime = put("@waveTime", 0);
|
||||
varTime = putEntry("@time", 0);
|
||||
varTick = putEntry("@tick", 0);
|
||||
varSecond = putEntry("@second", 0);
|
||||
varMinute = putEntry("@minute", 0);
|
||||
varWave = putEntry("@waveNumber", 0);
|
||||
varWaveTime = putEntry("@waveTime", 0);
|
||||
|
||||
varMapW = putEntry("@mapw", 0);
|
||||
varMapH = putEntry("@maph", 0);
|
||||
|
||||
putEntryOnly("sectionNetwork");
|
||||
|
||||
varServer = putEntry("@server", 0, true);
|
||||
varClient = putEntry("@client", 0, true);
|
||||
|
||||
//privileged desynced client variables
|
||||
varClientLocale = putEntry("@clientLocale", null, true);
|
||||
varClientUnit = putEntry("@clientUnit", null, true);
|
||||
varClientName = putEntry("@clientName", null, true);
|
||||
varClientTeam = putEntry("@clientTeam", 0, true);
|
||||
varClientMobile = putEntry("@clientMobile", 0, true);
|
||||
|
||||
//special enums
|
||||
put("@ctrlProcessor", ctrlProcessor);
|
||||
@@ -104,6 +133,8 @@ public class GlobalVars{
|
||||
logicIdToContent = new UnlockableContent[ContentType.all.length][];
|
||||
contentIdToLogicId = new int[ContentType.all.length][];
|
||||
|
||||
putEntryOnly("sectionLookup");
|
||||
|
||||
Fi ids = Core.files.internal("logicids.dat");
|
||||
if(ids.exists()){
|
||||
//read logic ID mapping data (generated in ImagePacker)
|
||||
@@ -114,7 +145,7 @@ public class GlobalVars{
|
||||
contentIdToLogicId[ctype.ordinal()] = new int[Vars.content.getBy(ctype).size];
|
||||
|
||||
//store count constants
|
||||
put("@" + ctype.name() + "Count", amount);
|
||||
putEntry("@" + ctype.name() + "Count", amount);
|
||||
|
||||
for(int i = 0; i < amount; i++){
|
||||
String name = in.readUTF();
|
||||
@@ -147,6 +178,26 @@ public class GlobalVars{
|
||||
//wave state
|
||||
vars.items[varWave].numval = state.wave;
|
||||
vars.items[varWaveTime].numval = state.wavetime / 60f;
|
||||
|
||||
vars.items[varMapW].numval = world.width();
|
||||
vars.items[varMapH].numval = world.height();
|
||||
|
||||
//network
|
||||
vars.items[varServer].numval = (net.server() || !net.active()) ? 1 : 0;
|
||||
vars.items[varClient].numval = net.client() ? 1 : 0;
|
||||
|
||||
//client
|
||||
if(!net.server() && player != null){
|
||||
vars.items[varClientLocale].objval = player.locale();
|
||||
vars.items[varClientUnit].objval = player.unit();
|
||||
vars.items[varClientName].objval = player.name();
|
||||
vars.items[varClientTeam].numval = player.team().id;
|
||||
vars.items[varClientMobile].numval = mobile ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Seq<VarEntry> getEntries(){
|
||||
return varEntries;
|
||||
}
|
||||
|
||||
/** @return a piece of content based on its logic ID. This is not equivalent to content ID. */
|
||||
@@ -161,23 +212,35 @@ public class GlobalVars{
|
||||
return arr != null && content.id >= 0 && content.id < arr.length ? arr[content.id] : -1;
|
||||
}
|
||||
|
||||
/** @return a constant ID > 0 if there is a constant with this name, otherwise -1. */
|
||||
/**
|
||||
* @return a constant ID > 0 if there is a constant with this name, otherwise -1.
|
||||
* Attempt to get privileged variable id from non-privileged logic executor returns null constant id.
|
||||
*/
|
||||
public int get(String name){
|
||||
return namesToIds.get(name, -1);
|
||||
}
|
||||
|
||||
/** @return a constant variable by ID. ID is not bound checked and must be positive. */
|
||||
public Var get(int id){
|
||||
/**
|
||||
* @return a constant variable by ID. ID is not bound checked and must be positive.
|
||||
* Attempt to get privileged variable from non-privileged logic executor returns null constant
|
||||
*/
|
||||
public Var get(int id, boolean privileged){
|
||||
if(!privileged && privilegedIds.contains(id)) return vars.get(namesToIds.get("null"));
|
||||
return vars.items[id];
|
||||
}
|
||||
|
||||
/** Sets a global variable by an ID returned from put(). */
|
||||
public void set(int id, double value){
|
||||
get(id).numval = value;
|
||||
get(id, true).numval = value;
|
||||
}
|
||||
|
||||
/** Adds a constant value by name. */
|
||||
public int put(String name, Object value){
|
||||
public int put(String name, Object value, boolean privileged){
|
||||
return put(name, value, privileged, true);
|
||||
}
|
||||
|
||||
/** Adds a constant value by name. */
|
||||
public int put(String name, Object value, boolean privileged, boolean hidden){
|
||||
int existingIdx = namesToIds.get(name, -1);
|
||||
if(existingIdx != -1){ //don't overwrite existing vars (see #6910)
|
||||
Log.debug("Failed to add global logic variable '@', as it already exists.", name);
|
||||
@@ -195,7 +258,46 @@ public class GlobalVars{
|
||||
|
||||
int index = vars.size;
|
||||
namesToIds.put(name, index);
|
||||
if(privileged) privilegedIds.add(index);
|
||||
vars.add(var);
|
||||
|
||||
if(!hidden){
|
||||
varEntries.add(new VarEntry(index, name, "", "", privileged));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public int put(String name, Object value){
|
||||
return put(name, value, false);
|
||||
}
|
||||
|
||||
public int putEntry(String name, Object value){
|
||||
return put(name, value, false, false);
|
||||
}
|
||||
|
||||
public int putEntry(String name, Object value, boolean privileged){
|
||||
return put(name, value, privileged, false);
|
||||
}
|
||||
|
||||
public void putEntryOnly(String name){
|
||||
varEntries.add(new VarEntry(0, name, "", "", false));
|
||||
}
|
||||
|
||||
/** An entry that describes a variable for documentation purposes. This is *only* used inside UI for global variables. */
|
||||
public static class VarEntry{
|
||||
public int id;
|
||||
public String name, description, icon;
|
||||
public boolean privileged;
|
||||
|
||||
public VarEntry(int id, String name, String description, String icon, boolean privileged){
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.icon = icon;
|
||||
this.privileged = privileged;
|
||||
}
|
||||
|
||||
public VarEntry(){
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
core/src/mindustry/logic/GlobalVarsDialog.java
Normal file
61
core/src/mindustry/logic/GlobalVarsDialog.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package mindustry.logic;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
public class GlobalVarsDialog extends BaseDialog{
|
||||
|
||||
public GlobalVarsDialog(){
|
||||
super("@logic.globals");
|
||||
|
||||
addCloseButton();
|
||||
shown(this::setup);
|
||||
onResize(this::setup);
|
||||
}
|
||||
|
||||
void setup(){
|
||||
float prefWidth = Math.min(Core.graphics.getWidth() * 0.9f / Scl.scl(1f) - 240f, 600f);
|
||||
cont.clearChildren();
|
||||
|
||||
cont.pane(t -> {
|
||||
t.margin(10f).marginRight(16f);
|
||||
t.defaults().fillX().fillY();
|
||||
for(var entry : Vars.logicVars.getEntries()){
|
||||
|
||||
if(entry.name.startsWith("section")){
|
||||
Color color = Pal.accent;
|
||||
t.add("@lglobal." + entry.name).fillX().center().labelAlign(Align.center).colspan(4).color(color).padTop(4f).padBottom(2f).row();
|
||||
t.image(Tex.whiteui).height(4f).color(color).colspan(4).padBottom(8f).row();
|
||||
}else{
|
||||
Color varColor = Pal.gray;
|
||||
float stub = 8f, mul = 0.5f, pad = 4;
|
||||
|
||||
String desc = entry.description;
|
||||
if(desc == null || desc.isEmpty()){
|
||||
desc = Core.bundle.get("lglobal." + entry.name, "");
|
||||
}
|
||||
|
||||
String fdesc = desc;
|
||||
|
||||
t.add(new Image(Tex.whiteui, varColor.cpy().mul(mul))).width(stub);
|
||||
t.stack(new Image(Tex.whiteui, varColor), new Label(" " + entry.name + " ", Styles.outlineLabel)).padRight(pad);
|
||||
|
||||
t.add(new Image(Tex.whiteui, Pal.gray.cpy().mul(mul))).width(stub);
|
||||
t.table(Tex.pane, out -> out.add(fdesc).style(Styles.outlineLabel).width(prefWidth).padLeft(2).padRight(2).wrap()).padRight(pad);
|
||||
|
||||
t.row();
|
||||
|
||||
t.add().fillX().colspan(4).height(4).row();
|
||||
}
|
||||
}
|
||||
}).grow();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public enum LAccess{
|
||||
maxHealth,
|
||||
heat,
|
||||
shield,
|
||||
armor,
|
||||
efficiency,
|
||||
progress,
|
||||
timescale,
|
||||
@@ -29,6 +30,10 @@ public enum LAccess{
|
||||
y,
|
||||
shootX,
|
||||
shootY,
|
||||
cameraX,
|
||||
cameraY,
|
||||
cameraWidth,
|
||||
cameraHeight,
|
||||
size,
|
||||
dead,
|
||||
range,
|
||||
@@ -62,7 +67,7 @@ public enum LAccess{
|
||||
all = values(),
|
||||
senseable = Seq.select(all, t -> t.params.length <= 1).toArray(LAccess.class),
|
||||
controls = Seq.select(all, t -> t.params.length > 0).toArray(LAccess.class),
|
||||
settable = {x, y, rotation, team, flag, health, totalPower, payloadType};
|
||||
settable = {x, y, rotation, speed, armor, health, team, flag, totalPower, payloadType};
|
||||
|
||||
LAccess(String... params){
|
||||
this.params = params;
|
||||
|
||||
@@ -15,6 +15,7 @@ public class LAssembler{
|
||||
private static final int invalidNum = Integer.MIN_VALUE;
|
||||
|
||||
private int lastVar;
|
||||
private boolean privileged;
|
||||
/** Maps names to variable IDs. */
|
||||
public ObjectMap<String, BVar> vars = new ObjectMap<>();
|
||||
/** All instructions to be executed. */
|
||||
@@ -35,6 +36,7 @@ public class LAssembler{
|
||||
Seq<LStatement> st = read(data, privileged);
|
||||
|
||||
asm.instructions = st.map(l -> l.build(asm)).retainAll(l -> l != null).toArray(LInstruction.class);
|
||||
asm.privileged = privileged;
|
||||
return asm;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,13 @@ import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.logic.LogicFx.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
import mindustry.world.blocks.logic.*;
|
||||
@@ -48,6 +51,7 @@ public class LExecutor{
|
||||
public Var[] vars = {};
|
||||
public Var counter;
|
||||
public int[] binds;
|
||||
public boolean yield;
|
||||
|
||||
public int iptIndex = -1;
|
||||
public LongSeq graphicsBuffer = new LongSeq();
|
||||
@@ -59,10 +63,14 @@ public class LExecutor{
|
||||
public boolean privileged = false;
|
||||
|
||||
//yes, this is a minor memory leak, but it's probably not significant enough to matter
|
||||
protected IntFloatMap unitTimeouts = new IntFloatMap();
|
||||
protected static IntFloatMap unitTimeouts = new IntFloatMap();
|
||||
//lookup variable by name, lazy init.
|
||||
protected ObjectIntMap<String> nameMap;
|
||||
|
||||
static{
|
||||
Events.on(ResetEvent.class, e -> unitTimeouts.clear());
|
||||
}
|
||||
|
||||
boolean timeoutDone(Unit unit, float delay){
|
||||
return Time.time >= unitTimeouts.get(unit.id) + delay;
|
||||
}
|
||||
@@ -122,7 +130,7 @@ public class LExecutor{
|
||||
|
||||
public Var var(int index){
|
||||
//global constants have variable IDs < 0, and they are fetched from the global constants object after being negated
|
||||
return index < 0 ? logicVars.get(-index) : vars[index];
|
||||
return index < 0 ? logicVars.get(-index, privileged) : vars[index];
|
||||
}
|
||||
|
||||
public @Nullable Var optionalVar(String name){
|
||||
@@ -178,11 +186,23 @@ public class LExecutor{
|
||||
return v.isobj ? v.objval != null ? 1 : 0 : invalid(v.numval) ? 0 : v.numval;
|
||||
}
|
||||
|
||||
/** Get num value from variable, convert null to NaN to handle it differently in some instructions */
|
||||
public double numOrNan(int index){
|
||||
Var v = var(index);
|
||||
return v.isobj ? v.objval != null ? 1 : Double.NaN : invalid(v.numval) ? 0 : v.numval;
|
||||
}
|
||||
|
||||
public float numf(int index){
|
||||
Var v = var(index);
|
||||
return v.isobj ? v.objval != null ? 1 : 0 : invalid(v.numval) ? 0 : (float)v.numval;
|
||||
}
|
||||
|
||||
/** Get float value from variable, convert null to NaN to handle it differently in some instructions */
|
||||
public float numfOrNan(int index){
|
||||
Var v = var(index);
|
||||
return v.isobj ? v.objval != null ? 1 : Float.NaN : invalid(v.numval) ? 0 : (float)v.numval;
|
||||
}
|
||||
|
||||
public int numi(int index){
|
||||
return (int)num(index);
|
||||
}
|
||||
@@ -446,7 +466,6 @@ public class LExecutor{
|
||||
case unbind -> {
|
||||
//TODO is this a good idea? will allocate
|
||||
unit.resetController();
|
||||
exec.setobj(varUnit, null);
|
||||
}
|
||||
case within -> {
|
||||
exec.setnum(p4, unit.within(x1, y1, d1) ? 1 : 0);
|
||||
@@ -893,8 +912,10 @@ public class LExecutor{
|
||||
|
||||
if(!v.constant){
|
||||
if(f.isobj){
|
||||
v.objval = f.objval;
|
||||
v.isobj = true;
|
||||
if(to != varCounter){
|
||||
v.objval = f.objval;
|
||||
v.isobj = true;
|
||||
}
|
||||
}else{
|
||||
v.numval = invalid(f.numval) ? 0 : f.numval;
|
||||
v.isobj = false;
|
||||
@@ -976,12 +997,6 @@ public class LExecutor{
|
||||
//graphics on headless servers are useless.
|
||||
if(Vars.headless || exec.graphicsBuffer.size >= maxGraphicsBuffer) return;
|
||||
|
||||
int num1 = exec.numi(p1);
|
||||
|
||||
if(type == LogicDisplay.commandImage){
|
||||
num1 = exec.obj(p1) instanceof UnlockableContent u ? u.iconId : 0;
|
||||
}
|
||||
|
||||
//explicitly unpack colorPack, it's pre-processed here
|
||||
if(type == LogicDisplay.commandColorPack){
|
||||
double packed = exec.num(x);
|
||||
@@ -993,7 +1008,63 @@ public class LExecutor{
|
||||
a = ((value & 0x000000ff));
|
||||
|
||||
exec.graphicsBuffer.add(DisplayCmd.get(LogicDisplay.commandColor, pack(r), pack(g), pack(b), pack(a), 0, 0));
|
||||
}else if(type == LogicDisplay.commandPrint){
|
||||
CharSequence str = exec.textBuffer;
|
||||
|
||||
if(str.length() > 0){
|
||||
var data = Fonts.logic.getData();
|
||||
int advance = (int)data.spaceXadvance, lineHeight = (int)data.lineHeight;
|
||||
|
||||
int xOffset, yOffset;
|
||||
int align = p1; //p1 is not a variable, it's a raw align value. what a massive hack
|
||||
|
||||
int maxWidth = 0, lines = 1, lineWidth = 0;
|
||||
for(int i = 0; i < str.length(); i++){
|
||||
char next = str.charAt(i);
|
||||
if(next == '\n'){
|
||||
maxWidth = Math.max(maxWidth, lineWidth);
|
||||
lineWidth = 0;
|
||||
lines ++;
|
||||
}else{
|
||||
lineWidth ++;
|
||||
}
|
||||
}
|
||||
maxWidth = Math.max(maxWidth, lineWidth);
|
||||
|
||||
float
|
||||
width = maxWidth * advance,
|
||||
height = lines * lineHeight,
|
||||
ha = ((Align.isLeft(align) ? -1f : 0f) + 1f + (Align.isRight(align) ? 1f : 0f))/2f,
|
||||
va = ((Align.isBottom(align) ? -1f : 0f) + 1f + (Align.isTop(align) ? 1f : 0f))/2f;
|
||||
|
||||
xOffset = -(int)(width * ha);
|
||||
yOffset = -(int)(height * va) + (lines - 1) * lineHeight;
|
||||
|
||||
|
||||
int curX = exec.numi(x), curY = exec.numi(y);
|
||||
for(int i = 0; i < str.length(); i++){
|
||||
char next = str.charAt(i);
|
||||
if(next == '\n'){
|
||||
//move Y down when newline is encountered
|
||||
curY -= lineHeight;
|
||||
curX = exec.numi(x); //reset
|
||||
continue;
|
||||
}
|
||||
if(Fonts.logic.getData().hasGlyph(next)){
|
||||
exec.graphicsBuffer.add(DisplayCmd.get(LogicDisplay.commandPrint, packSign(curX + xOffset), packSign(curY + yOffset), next, 0, 0, 0));
|
||||
}
|
||||
curX += advance;
|
||||
}
|
||||
|
||||
exec.textBuffer.setLength(0);
|
||||
}
|
||||
}else{
|
||||
int num1 = exec.numi(p1);
|
||||
|
||||
if(type == LogicDisplay.commandImage){
|
||||
num1 = exec.obj(p1) instanceof UnlockableContent u ? u.iconId : 0;
|
||||
}
|
||||
|
||||
//add graphics calls, cap graphics buffer size
|
||||
exec.graphicsBuffer.add(DisplayCmd.get(type, packSign(exec.numi(x)), packSign(exec.numi(y)), packSign(num1), packSign(exec.numi(p2)), packSign(exec.numi(p3)), packSign(exec.numi(p4))));
|
||||
}
|
||||
@@ -1079,6 +1150,55 @@ public class LExecutor{
|
||||
}
|
||||
}
|
||||
|
||||
public static class FormatI implements LInstruction{
|
||||
public int value;
|
||||
|
||||
public FormatI(int value){
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
FormatI(){}
|
||||
|
||||
@Override
|
||||
public void run(LExecutor exec){
|
||||
|
||||
if(exec.textBuffer.length() >= maxTextBuffer) return;
|
||||
|
||||
int placeholderIndex = -1;
|
||||
int placeholderNumber = 10;
|
||||
|
||||
for(int i = 0; i < exec.textBuffer.length(); i++){
|
||||
if(exec.textBuffer.charAt(i) == '{' && exec.textBuffer.length() - i > 2){
|
||||
char numChar = exec.textBuffer.charAt(i + 1);
|
||||
|
||||
if(numChar >= '0' && numChar <= '9' && exec.textBuffer.charAt(i + 2) == '}'){
|
||||
if(numChar - '0' < placeholderNumber){
|
||||
placeholderNumber = numChar - '0';
|
||||
placeholderIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(placeholderIndex == -1) return;
|
||||
|
||||
//this should avoid any garbage allocation
|
||||
Var v = exec.var(value);
|
||||
if(v.isobj && value != 0){
|
||||
String strValue = PrintI.toString(v.objval);
|
||||
|
||||
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, strValue);
|
||||
}else{
|
||||
//display integer version when possible
|
||||
if(Math.abs(v.numval - (long)v.numval) < 0.00001){
|
||||
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, (long)v.numval + "");
|
||||
}else{
|
||||
exec.textBuffer.replace(placeholderIndex, placeholderIndex + 3, v.numval + "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PrintFlushI implements LInstruction{
|
||||
public int target;
|
||||
|
||||
@@ -1144,7 +1264,6 @@ public class LExecutor{
|
||||
public int value;
|
||||
|
||||
public float curTime;
|
||||
public long frameId;
|
||||
|
||||
public WaitI(int value){
|
||||
this.value = value;
|
||||
@@ -1160,11 +1279,8 @@ public class LExecutor{
|
||||
}else{
|
||||
//skip back to self.
|
||||
exec.var(varCounter).numval --;
|
||||
}
|
||||
|
||||
if(state.updateId != frameId){
|
||||
exec.yield = true;
|
||||
curTime += Time.delta / 60f;
|
||||
frameId = state.updateId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1175,6 +1291,7 @@ public class LExecutor{
|
||||
public void run(LExecutor exec){
|
||||
//skip back to self.
|
||||
exec.var(varCounter).numval --;
|
||||
exec.yield = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1289,13 +1406,20 @@ public class LExecutor{
|
||||
exec.setobj(result, i < 0 || i >= builds.size ? null : builds.get(i));
|
||||
}
|
||||
}
|
||||
case unitCount -> exec.setnum(result, data.units.size);
|
||||
case unitCount -> {
|
||||
UnitType type = exec.obj(extra) instanceof UnitType u ? u : null;
|
||||
if(type == null){
|
||||
exec.setnum(result, data.units.size);
|
||||
}else{
|
||||
exec.setnum(result, data.unitsByType[type.id].size);
|
||||
}
|
||||
}
|
||||
case coreCount -> exec.setnum(result, data.cores.size);
|
||||
case playerCount -> exec.setnum(result, data.players.size);
|
||||
case buildCount -> {
|
||||
Block block = exec.obj(extra) instanceof Block b ? b : null;
|
||||
if(block == null){
|
||||
exec.setobj(result, null);
|
||||
exec.setnum(result, data.buildings.size);
|
||||
}else{
|
||||
exec.setnum(result, data.getBuildings(block).size);
|
||||
}
|
||||
@@ -1485,6 +1609,23 @@ public class LExecutor{
|
||||
}
|
||||
case ambientLight -> state.rules.ambientLight.fromDouble(exec.num(value));
|
||||
case solarMultiplier -> state.rules.solarMultiplier = Math.max(exec.numf(value), 0f);
|
||||
case ban -> {
|
||||
Object cont = exec.obj(value);
|
||||
if(cont instanceof Block b){
|
||||
// Rebuild PlacementFragment if anything has changed
|
||||
if(state.rules.bannedBlocks.add(b) && !headless) ui.hudfrag.blockfrag.rebuild();
|
||||
}else if(cont instanceof UnitType u){
|
||||
state.rules.bannedUnits.add(u);
|
||||
}
|
||||
}
|
||||
case unban -> {
|
||||
Object cont = exec.obj(value);
|
||||
if(cont instanceof Block b){
|
||||
if(state.rules.bannedBlocks.remove(b) && !headless) ui.hudfrag.blockfrag.rebuild();
|
||||
}else if(cont instanceof UnitType u){
|
||||
state.rules.bannedUnits.remove(u);
|
||||
}
|
||||
}
|
||||
case unitHealth, unitBuildSpeed, unitCost, unitDamage, blockHealth, blockDamage, buildSpeed, rtsMinSquad, rtsMinWeight -> {
|
||||
Team team = exec.team(p1);
|
||||
if(team != null){
|
||||
@@ -1555,11 +1696,12 @@ public class LExecutor{
|
||||
|
||||
public static class FlushMessageI implements LInstruction{
|
||||
public MessageType type = MessageType.announce;
|
||||
public int duration;
|
||||
public int duration, outSuccess;
|
||||
|
||||
public FlushMessageI(MessageType type, int duration){
|
||||
public FlushMessageI(MessageType type, int duration, int outSuccess){
|
||||
this.type = type;
|
||||
this.duration = duration;
|
||||
this.outSuccess = outSuccess;
|
||||
}
|
||||
|
||||
public FlushMessageI(){
|
||||
@@ -1567,16 +1709,20 @@ public class LExecutor{
|
||||
|
||||
@Override
|
||||
public void run(LExecutor exec){
|
||||
if(headless && type != MessageType.mission) return;
|
||||
//set default to success
|
||||
exec.setnum(outSuccess, 1);
|
||||
if(headless && type != MessageType.mission) {
|
||||
exec.textBuffer.setLength(0);
|
||||
return;
|
||||
}
|
||||
|
||||
//skip back to self until possible
|
||||
//TODO this is guaranteed desync on servers - I don't see a good solution
|
||||
if(
|
||||
type == MessageType.announce && ui.hasAnnouncement() ||
|
||||
type == MessageType.notify && ui.hudfrag.hasToast() ||
|
||||
type == MessageType.toast && ui.hasAnnouncement()
|
||||
){
|
||||
exec.var(varCounter).numval --;
|
||||
//set outSuccess=false to let user retry.
|
||||
exec.setnum(outSuccess, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1630,9 +1776,9 @@ public class LExecutor{
|
||||
}
|
||||
|
||||
public static class ExplosionI implements LInstruction{
|
||||
public int team, x, y, radius, damage, air, ground, pierce;
|
||||
public int team, x, y, radius, damage, air, ground, pierce, effect;
|
||||
|
||||
public ExplosionI(int team, int x, int y, int radius, int damage, int air, int ground, int pierce){
|
||||
public ExplosionI(int team, int x, int y, int radius, int damage, int air, int ground, int pierce, int effect){
|
||||
this.team = team;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
@@ -1641,6 +1787,7 @@ public class LExecutor{
|
||||
this.air = air;
|
||||
this.ground = ground;
|
||||
this.pierce = pierce;
|
||||
this.effect = effect;
|
||||
}
|
||||
|
||||
public ExplosionI(){
|
||||
@@ -1652,19 +1799,21 @@ public class LExecutor{
|
||||
|
||||
Team t = exec.team(team);
|
||||
//note that there is a radius cap
|
||||
Call.logicExplosion(t, World.unconv(exec.numf(x)), World.unconv(exec.numf(y)), World.unconv(Math.min(exec.numf(radius), 100)), exec.numf(damage), exec.bool(air), exec.bool(ground), exec.bool(pierce));
|
||||
Call.logicExplosion(t, World.unconv(exec.numf(x)), World.unconv(exec.numf(y)), World.unconv(Math.min(exec.numf(radius), 100)), exec.numf(damage), exec.bool(air), exec.bool(ground), exec.bool(pierce), exec.bool(effect));
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void logicExplosion(Team team, float x, float y, float radius, float damage, boolean air, boolean ground, boolean pierce){
|
||||
public static void logicExplosion(Team team, float x, float y, float radius, float damage, boolean air, boolean ground, boolean pierce, boolean effect){
|
||||
if(damage < 0f) return;
|
||||
|
||||
Damage.damage(team, x, y, radius, damage, pierce, air, ground);
|
||||
if(pierce){
|
||||
Fx.spawnShockwave.at(x, y, World.conv(radius));
|
||||
}else{
|
||||
Fx.dynamicExplosion.at(x, y, World.conv(radius) / 8f);
|
||||
if(effect){
|
||||
if(pierce){
|
||||
Fx.spawnShockwave.at(x, y, World.conv(radius));
|
||||
}else{
|
||||
Fx.dynamicExplosion.at(x, y, World.conv(radius) / 8f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1856,5 +2005,148 @@ public class LExecutor{
|
||||
}
|
||||
}
|
||||
|
||||
public static class SetMarkerI implements LInstruction{
|
||||
public LMarkerControl type = LMarkerControl.pos;
|
||||
public int id, p1, p2, p3;
|
||||
|
||||
public SetMarkerI(LMarkerControl type, int id, int p1, int p2, int p3){
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.p3 = p3;
|
||||
}
|
||||
|
||||
public SetMarkerI(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(LExecutor exec){
|
||||
if(type == LMarkerControl.remove){
|
||||
state.markers.remove(exec.numi(id));
|
||||
}else{
|
||||
var marker = state.markers.get(exec.numi(id));
|
||||
if(marker == null) return;
|
||||
|
||||
if(type == LMarkerControl.flushText){
|
||||
marker.setText(exec.textBuffer.toString(), exec.bool(p1));
|
||||
exec.textBuffer.setLength(0);
|
||||
}else if(type == LMarkerControl.texture){
|
||||
if(exec.bool(p1)){
|
||||
marker.setTexture(exec.textBuffer.toString());
|
||||
exec.textBuffer.setLength(0);
|
||||
}else{
|
||||
marker.setTexture(PrintI.toString(exec.obj(p2)));
|
||||
}
|
||||
}else{
|
||||
marker.control(type, exec.numOrNan(p1), exec.numOrNan(p2), exec.numOrNan(p3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MakeMarkerI implements LInstruction{
|
||||
//TODO arbitrary number
|
||||
public static final int maxMarkers = 20000;
|
||||
|
||||
public String type = "shape";
|
||||
public int id, x, y, replace;
|
||||
|
||||
public MakeMarkerI(String type, int id, int x, int y, int replace){
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.replace = replace;
|
||||
}
|
||||
|
||||
public MakeMarkerI(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(LExecutor exec){
|
||||
var cons = MapObjectives.markerNameToType.get(type);
|
||||
|
||||
if(cons != null && state.markers.size() < maxMarkers){
|
||||
int mid = exec.numi(id);
|
||||
if(exec.bool(replace) || !state.markers.has(mid)){
|
||||
var marker = cons.get();
|
||||
marker.control(LMarkerControl.pos, exec.num(x), exec.num(y), 0);
|
||||
state.markers.add(mid, marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void createMarker(int id, ObjectiveMarker marker){
|
||||
state.markers.add(id, marker);
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void removeMarker(int id){
|
||||
state.markers.remove(id);
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void updateMarker(int id, LMarkerControl control, double p1, double p2, double p3){
|
||||
var marker = state.markers.get(id);
|
||||
if(marker != null){
|
||||
marker.control(control, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void updateMarkerText(int id, LMarkerControl type, boolean fetch, String text){
|
||||
var marker = state.markers.get(id);
|
||||
if(marker != null){
|
||||
if(type == LMarkerControl.flushText){
|
||||
marker.setText(text, fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void updateMarkerTexture(int id, String textureName){
|
||||
var marker = state.markers.get(id);
|
||||
if(marker != null){
|
||||
marker.setTexture(textureName);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalePrintI implements LInstruction{
|
||||
public int name;
|
||||
|
||||
public LocalePrintI(int name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public LocalePrintI(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(LExecutor exec){
|
||||
if(exec.textBuffer.length() >= maxTextBuffer) return;
|
||||
|
||||
//this should avoid any garbage allocation
|
||||
Var v = exec.var(name);
|
||||
if(v.isobj){
|
||||
String name = PrintI.toString(v.objval);
|
||||
|
||||
String strValue;
|
||||
|
||||
if(mobile){
|
||||
strValue = state.mapLocales.containsProperty(name + ".mobile") ?
|
||||
state.mapLocales.getProperty(name + ".mobile") :
|
||||
state.mapLocales.getProperty(name);
|
||||
}else{
|
||||
strValue = state.mapLocales.getProperty(name);
|
||||
}
|
||||
|
||||
exec.textBuffer.append(strValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
||||
33
core/src/mindustry/logic/LMarkerControl.java
Normal file
33
core/src/mindustry/logic/LMarkerControl.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package mindustry.logic;
|
||||
|
||||
public enum LMarkerControl{
|
||||
remove,
|
||||
world("true/false"),
|
||||
minimap("true/false"),
|
||||
autoscale("true/false"),
|
||||
pos("x", "y"),
|
||||
endPos("x", "y"),
|
||||
drawLayer("layer"),
|
||||
color("color"),
|
||||
radius("radius"),
|
||||
stroke("stroke"),
|
||||
rotation("rotation"),
|
||||
shape("sides", "fill", "outline"),
|
||||
flushText("fetch"),
|
||||
fontSize("size"),
|
||||
textHeight("height"),
|
||||
labelFlags("background", "outline"),
|
||||
texture("printFlush", "name"),
|
||||
textureSize("width", "height"),
|
||||
posi("index", "x", "y"),
|
||||
uvi("index", "x", "y"),
|
||||
colori("index", "color");
|
||||
|
||||
public final String[] params;
|
||||
|
||||
public static final LMarkerControl[] all = values();
|
||||
|
||||
LMarkerControl(String... params){
|
||||
this.params = params;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package mindustry.logic;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.actions.*;
|
||||
@@ -10,10 +11,12 @@ import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.LCanvas.*;
|
||||
import mindustry.logic.LExecutor.*;
|
||||
import mindustry.ui.*;
|
||||
|
||||
import static mindustry.Vars.ui;
|
||||
import static mindustry.logic.LCanvas.*;
|
||||
|
||||
/**
|
||||
@@ -108,6 +111,35 @@ public abstract class LStatement{
|
||||
return field(table, value, setter).width(85f).padRight(10).left();
|
||||
}
|
||||
|
||||
/** Puts the text and field in one table, taking up one cell. */
|
||||
protected Cell<TextField> fieldst(Table table, String desc, String value, Cons<String> setter){
|
||||
Cell[] result = {null};
|
||||
table.table(t -> {
|
||||
t.setColor(table.color);
|
||||
t.add(desc).padLeft(10).left().self(this::param);
|
||||
result[0] = field(t, value, setter).width(85f).padRight(10).left();
|
||||
});
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/** Adds color edit button */
|
||||
protected Cell<Button> col(Table table, String value, Cons<Color> setter){
|
||||
return table.button(b -> {
|
||||
b.image(Icon.pencilSmall);
|
||||
b.clicked(() -> {
|
||||
Color current = Pal.accent.cpy();
|
||||
if(value.startsWith("%")){
|
||||
try{
|
||||
current = Color.valueOf(value.substring(1));
|
||||
}catch(Exception ignored){}
|
||||
}
|
||||
|
||||
ui.picker.show(current, setter);
|
||||
});
|
||||
}, Styles.logict, () -> {}).size(40f).padLeft(-11).color(table.color);
|
||||
}
|
||||
|
||||
protected Cell<TextField> fields(Table table, String value, Cons<String> setter){
|
||||
return field(table, value, setter).width(85f);
|
||||
}
|
||||
@@ -132,7 +164,7 @@ public abstract class LStatement{
|
||||
if(p instanceof Enum e){
|
||||
tooltip(c, e);
|
||||
}
|
||||
}).checked(current == p).group(group));
|
||||
}).checked(current.equals(p)).group(group));
|
||||
|
||||
if(++i % cols == 0) t.row();
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import arc.graphics.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.LCanvas.*;
|
||||
@@ -120,6 +122,20 @@ public class LStatements{
|
||||
|
||||
@RegisterStatement("draw")
|
||||
public static class DrawStatement extends LStatement{
|
||||
static final String[] aligns = {"center", "top", "bottom", "left", "right", "topLeft", "topRight", "bottomLeft", "bottomRight"};
|
||||
//yes, boxing Integer is gross but this is easier to construct and Integers <128 don't allocate anyway
|
||||
static final ObjectMap<String, Integer> nameToAlign = ObjectMap.of(
|
||||
"center", Align.center,
|
||||
"top", Align.top,
|
||||
"bottom", Align.bottom,
|
||||
"left", Align.left,
|
||||
"right", Align.right,
|
||||
"topLeft", Align.topLeft,
|
||||
"topRight", Align.topRight,
|
||||
"bottomLeft", Align.bottomLeft,
|
||||
"bottomRight", Align.bottomRight
|
||||
);
|
||||
|
||||
public GraphicsType type = GraphicsType.clear;
|
||||
public String x = "0", y = "0", p1 = "0", p2 = "0", p3 = "0", p4 = "0";
|
||||
|
||||
@@ -146,6 +162,11 @@ public class LStatements{
|
||||
p2 = "32";
|
||||
p3 = "0";
|
||||
}
|
||||
|
||||
if(type == GraphicsType.print){
|
||||
p1 = "bottomLeft";
|
||||
}
|
||||
|
||||
rebuild(table);
|
||||
}, 2, cell -> cell.size(100, 50)));
|
||||
}, Styles.logict, () -> {}).size(90, 40).color(table.color).left().padLeft(2);
|
||||
@@ -173,6 +194,10 @@ public class LStatements{
|
||||
}
|
||||
case col -> {
|
||||
fields(s, "color", x, v -> x = v).width(144f);
|
||||
col(s, x, res -> {
|
||||
x = "%" + res.toString().substring(0, res.a >= 1f ? 6 : 8);
|
||||
build(table);
|
||||
});
|
||||
}
|
||||
case stroke -> {
|
||||
s.add().width(4);
|
||||
@@ -220,14 +245,21 @@ public class LStatements{
|
||||
row(s);
|
||||
fields(s, "rotation", p3, v -> p3 = v);
|
||||
}
|
||||
//TODO
|
||||
/*
|
||||
case character -> {
|
||||
case print -> {
|
||||
fields(s, "x", x, v -> x = v);
|
||||
fields(s, "y", y, v -> y = v);
|
||||
|
||||
row(s);
|
||||
fields(s, "char", p1, v -> p1 = v);
|
||||
}*/
|
||||
|
||||
s.add("align ");
|
||||
|
||||
s.button(b -> {
|
||||
b.label(() -> nameToAlign.containsKey(p1) ? p1 : "bottomLeft");
|
||||
b.clicked(() -> showSelect(b, aligns, p1, t -> {
|
||||
p1 = t;
|
||||
}, 2, cell -> cell.size(165, 50)));
|
||||
}, Styles.logict, () -> {}).size(165, 40).color(s.color).left().padLeft(2);
|
||||
}
|
||||
}
|
||||
}).expand().left();
|
||||
}
|
||||
@@ -242,7 +274,8 @@ public class LStatements{
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new DrawI((byte)type.ordinal(), 0, builder.var(x), builder.var(y), builder.var(p1), builder.var(p2), builder.var(p3), builder.var(p4));
|
||||
return new DrawI((byte)type.ordinal(), 0, builder.var(x), builder.var(y),
|
||||
type == GraphicsType.print ? nameToAlign.get(p1, Align.bottomLeft) : builder.var(p1), builder.var(p2), builder.var(p3), builder.var(p4));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -272,6 +305,27 @@ public class LStatements{
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterStatement("format")
|
||||
public static class FormatStatement extends LStatement{
|
||||
public String value = "\"frog\"";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
field(table, value, str -> value = str).width(0f).growX().padRight(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new FormatI(builder.var(value));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LCategory category(){
|
||||
return LCategory.io;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterStatement("drawflush")
|
||||
public static class DrawFlushStatement extends LStatement{
|
||||
public String target = "display1";
|
||||
@@ -1392,6 +1446,11 @@ public class LStatements{
|
||||
row(table);
|
||||
field(table, value, s -> value = s);
|
||||
}
|
||||
case ban, unban -> {
|
||||
table.add(" block/unit ");
|
||||
|
||||
field(table, value, s -> value = s);
|
||||
}
|
||||
default -> {
|
||||
table.add(" = ");
|
||||
|
||||
@@ -1419,7 +1478,7 @@ public class LStatements{
|
||||
@RegisterStatement("message")
|
||||
public static class FlushMessageStatement extends LStatement{
|
||||
public MessageType type = MessageType.announce;
|
||||
public String duration = "3";
|
||||
public String duration = "3", outSuccess = "success";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
@@ -1438,12 +1497,14 @@ public class LStatements{
|
||||
}, Styles.logict, () -> {}).size(160f, 40f).padLeft(2).color(table.color);
|
||||
|
||||
switch(type){
|
||||
case announce, toast -> {
|
||||
case announce, toast -> {
|
||||
table.add(" for ");
|
||||
fields(table, duration, str -> duration = str);
|
||||
table.add(" secs ");
|
||||
}
|
||||
}
|
||||
table.add(" success ");
|
||||
fields(table, outSuccess, str -> outSuccess = str);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1453,7 +1514,7 @@ public class LStatements{
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new FlushMessageI(type, builder.var(duration));
|
||||
return new FlushMessageI(type, builder.var(duration), builder.var(outSuccess));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1546,22 +1607,10 @@ public class LStatements{
|
||||
if(entry.color){
|
||||
fields(table, "color", color, str -> color = str).width(120f);
|
||||
|
||||
table.button(b -> {
|
||||
b.image(Icon.pencilSmall);
|
||||
b.clicked(() -> {
|
||||
Color current = Pal.accent.cpy();
|
||||
if(color.startsWith("%")){
|
||||
try{
|
||||
current = Color.valueOf(color.substring(1));
|
||||
}catch(Exception ignored){}
|
||||
}
|
||||
|
||||
ui.picker.show(current, result -> {
|
||||
color = "%" + result.toString().substring(0, result.a >= 1f ? 6 : 8);
|
||||
build(table);
|
||||
});
|
||||
});
|
||||
}, Styles.logict, () -> {}).size(40f).padLeft(-11).color(table.color);
|
||||
col(table, color, res -> {
|
||||
color = "%" + res.toString().substring(0, res.a >= 1f ? 6 : 8);
|
||||
build(table);
|
||||
});
|
||||
}
|
||||
|
||||
row(table);
|
||||
@@ -1594,7 +1643,7 @@ public class LStatements{
|
||||
|
||||
@RegisterStatement("explosion")
|
||||
public static class ExplosionStatement extends LStatement{
|
||||
public String team = "@crux", x = "0", y = "0", radius = "5", damage = "50", air = "true", ground = "true", pierce = "false";
|
||||
public String team = "@crux", x = "0", y = "0", radius = "5", damage = "50", air = "true", ground = "true", pierce = "false", effect = "true";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
@@ -1609,6 +1658,8 @@ public class LStatements{
|
||||
row(table);
|
||||
fields(table, "ground", ground, str -> ground = str);
|
||||
fields(table, "pierce", pierce, str -> pierce = str);
|
||||
table.row();
|
||||
fields(table, "effect", effect, str -> effect = str);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1618,7 +1669,7 @@ public class LStatements{
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler b){
|
||||
return new ExplosionI(b.var(team), b.var(x), b.var(y), b.var(radius), b.var(damage), b.var(air), b.var(ground), b.var(pierce));
|
||||
return new ExplosionI(b.var(team), b.var(x), b.var(y), b.var(radius), b.var(damage), b.var(air), b.var(ground), b.var(pierce), b.var(effect));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1689,7 +1740,7 @@ public class LStatements{
|
||||
fields(table, index, i -> index = i);
|
||||
}
|
||||
|
||||
if(type == FetchType.buildCount || type == FetchType.build){
|
||||
if(type == FetchType.buildCount || type == FetchType.build || type == FetchType.unitCount){
|
||||
row(table);
|
||||
|
||||
fields(table, "block", extra, i -> extra = i);
|
||||
@@ -1912,4 +1963,162 @@ public class LStatements{
|
||||
return LCategory.world;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterStatement("setmarker")
|
||||
public static class SetMarkerStatement extends LStatement{
|
||||
public LMarkerControl type = LMarkerControl.pos;
|
||||
public String id = "0", p1 = "0", p2 = "0", p3 = "0";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
rebuild(table);
|
||||
}
|
||||
|
||||
void rebuild(Table table){
|
||||
table.clearChildren();
|
||||
|
||||
table.add("set");
|
||||
|
||||
table.button(b -> {
|
||||
b.label(() -> type.name());
|
||||
b.clicked(() -> showSelect(b, LMarkerControl.all, type, t -> {
|
||||
type = t;
|
||||
rebuild(table);
|
||||
}, 3, cell -> cell.size(140, 50)));
|
||||
}, Styles.logict, () -> {}).size(190, 40).color(table.color).left().padLeft(2);
|
||||
|
||||
row(table);
|
||||
|
||||
fieldst(table, "of id#", id, str -> id = str);
|
||||
|
||||
//Q: why don't you just use arrays for this?
|
||||
//A: arrays aren't as easy to serialize so the code generator doesn't handle them
|
||||
for(int f = 0; f < type.params.length; f++){
|
||||
int i = f;
|
||||
|
||||
table.table(t -> {
|
||||
t.setColor(table.color);
|
||||
|
||||
String value = i == 0 ? p1 : i == 1 ? p2 : p3;
|
||||
Cons<String> setter = i == 0 ? v -> p1 = v : i == 1 ? v -> p2 = v : v -> p3 = v;
|
||||
|
||||
fields(t, type.params[i], value, setter).width(100f);
|
||||
|
||||
if(type == LMarkerControl.color || (type == LMarkerControl.colori && i == 1)){
|
||||
col(t, value, res -> {
|
||||
setter.get("%" + res.toString().substring(0, res.a >= 1f ? 6 : 8));
|
||||
build(table);
|
||||
});
|
||||
}else if(type == LMarkerControl.drawLayer){
|
||||
t.button(b -> {
|
||||
b.image(Icon.pencilSmall);
|
||||
b.clicked(() -> showSelectTable(b, (o, hide) -> {
|
||||
o.row();
|
||||
o.table(s -> {
|
||||
s.left();
|
||||
for(var field : Layer.class.getFields()){
|
||||
float layer = Reflect.get(field);
|
||||
s.button(field.getName() + " = " + layer, Styles.logicTogglet, () -> {
|
||||
p1 = Float.toString(layer);
|
||||
rebuild(table);
|
||||
hide.run();
|
||||
}).size(240f, 40f).row();
|
||||
}
|
||||
}).width(240f).left();
|
||||
}));
|
||||
}, Styles.logict, () -> {}).size(40f).padLeft(-11).color(table.color);
|
||||
}
|
||||
});
|
||||
|
||||
if(i == 0) row(table);
|
||||
if(i == 2) table.row();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean privileged(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new SetMarkerI(type, builder.var(id), builder.var(p1), builder.var(p2), builder.var(p3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LCategory category(){
|
||||
return LCategory.world;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterStatement("makemarker")
|
||||
public static class MakeMarkerStatement extends LStatement{
|
||||
public String type = "shape", id = "0", x = "0", y = "0", replace = "true";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
table.clearChildren();
|
||||
|
||||
table.button(b -> {
|
||||
b.label(() -> type);
|
||||
|
||||
b.clicked(() -> showSelect(b, MapObjectives.allMarkerTypeNames.toArray(String.class), type, t -> {
|
||||
type = t;
|
||||
build(table);
|
||||
}, 2, cell -> cell.size(160, 50)));
|
||||
}, Styles.logict, () -> {}).size(190, 40).color(table.color).left().padLeft(2);
|
||||
|
||||
fieldst(table, "id", id, str -> id = str);
|
||||
|
||||
row(table);
|
||||
|
||||
fieldst(table, "x", x, v -> x = v);
|
||||
|
||||
fieldst(table, "y", y, v -> y = v);
|
||||
|
||||
row(table);
|
||||
|
||||
fieldst(table, "replace", replace, v -> replace = v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean privileged(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new MakeMarkerI(type, builder.var(id), builder.var(x), builder.var(y), builder.var(replace));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LCategory category(){
|
||||
return LCategory.world;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterStatement("localeprint")
|
||||
public static class LocalePrintStatement extends LStatement{
|
||||
public String value = "\"name\"";
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
field(table, value, str -> value = str).width(0f).growX().padRight(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean privileged(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LInstruction build(LAssembler builder){
|
||||
return new LocalePrintI(builder.var(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LCategory category(){
|
||||
return LCategory.world;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public class LogicDialog extends BaseDialog{
|
||||
Cons<String> consumer = s -> {};
|
||||
boolean privileged;
|
||||
@Nullable LExecutor executor;
|
||||
GlobalVarsDialog globalsDialog = new GlobalVarsDialog();
|
||||
|
||||
public LogicDialog(){
|
||||
super("logic");
|
||||
@@ -51,7 +52,7 @@ public class LogicDialog extends BaseDialog{
|
||||
add(buttons).growX().name("canvas");
|
||||
}
|
||||
|
||||
private Color typeColor(Var s, Color color){
|
||||
public static Color typeColor(Var s, Color color){
|
||||
return color.set(
|
||||
!s.isobj ? Pal.place :
|
||||
s.objval == null ? Color.darkGray :
|
||||
@@ -65,7 +66,7 @@ public class LogicDialog extends BaseDialog{
|
||||
);
|
||||
}
|
||||
|
||||
private String typeName(Var s){
|
||||
public static String typeName(Var s){
|
||||
return
|
||||
!s.isobj ? "number" :
|
||||
s.objval == null ? "null" :
|
||||
@@ -178,6 +179,8 @@ public class LogicDialog extends BaseDialog{
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.buttons.button("@logic.globals", Icon.list, () -> globalsDialog.show()).size(210f, 64f);
|
||||
|
||||
dialog.show();
|
||||
}).name("variables").disabled(b -> executor == null || executor.vars.length == 0);
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ public class LogicFx{
|
||||
return map.get(name);
|
||||
}
|
||||
|
||||
/** Adds an effect entry to the map. */
|
||||
public static void add(String name, EffectEntry entry){
|
||||
entry.name = name;
|
||||
map.put(name, entry);
|
||||
}
|
||||
|
||||
public static String[] all(){
|
||||
return map.orderedKeys().toArray(String.class);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public enum LogicRule{
|
||||
lighting,
|
||||
ambientLight,
|
||||
solarMultiplier,
|
||||
ban,
|
||||
unban,
|
||||
|
||||
//team specific
|
||||
buildSpeed,
|
||||
|
||||
@@ -335,7 +335,7 @@ public abstract class BasicGenerator implements WorldGenerator{
|
||||
ore = tile.overlay();
|
||||
r.get(tile.x, tile.y);
|
||||
tile.setFloor(floor.asFloor());
|
||||
tile.setBlock(block);
|
||||
if(block != tile.block()) tile.setBlock(block);
|
||||
tile.setOverlay(ore);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public class ClassMap{
|
||||
classes.put("ParticleEffect", mindustry.entities.effect.ParticleEffect.class);
|
||||
classes.put("RadialEffect", mindustry.entities.effect.RadialEffect.class);
|
||||
classes.put("SeqEffect", mindustry.entities.effect.SeqEffect.class);
|
||||
classes.put("SoundEffect", mindustry.entities.effect.SoundEffect.class);
|
||||
classes.put("WaveEffect", mindustry.entities.effect.WaveEffect.class);
|
||||
classes.put("WrapEffect", mindustry.entities.effect.WrapEffect.class);
|
||||
classes.put("DrawPart", mindustry.entities.part.DrawPart.class);
|
||||
|
||||
@@ -2,6 +2,7 @@ package mindustry.mod;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.assets.loaders.MusicLoader.*;
|
||||
import arc.assets.loaders.SoundLoader.*;
|
||||
import arc.audio.*;
|
||||
import arc.files.*;
|
||||
@@ -56,9 +57,10 @@ import static mindustry.Vars.*;
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ContentParser{
|
||||
private static final boolean ignoreUnknownFields = true;
|
||||
private static final ContentType[] typesToSearch = {ContentType.block, ContentType.item, ContentType.unit, ContentType.liquid, ContentType.planet};
|
||||
|
||||
ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
|
||||
ObjectSet<Class<?>> implicitNullable = ObjectSet.with(TextureRegion.class, TextureRegion[].class, TextureRegion[][].class, TextureRegion[][][].class);
|
||||
ObjectMap<String, AssetDescriptor<?>> sounds = new ObjectMap<>();
|
||||
Seq<ParseListener> listeners = new Seq<>();
|
||||
|
||||
ObjectMap<Class<?>, FieldParser> classParsers = new ObjectMap<>(){{
|
||||
@@ -112,7 +114,7 @@ public class ContentParser{
|
||||
});
|
||||
put(UnitCommand.class, (type, data) -> {
|
||||
if(data.isString()){
|
||||
var cmd = UnitCommand.all.find(u -> u.name.equals(data.asString()));
|
||||
var cmd = content.unitCommand(data.asString());
|
||||
if(cmd != null){
|
||||
return cmd;
|
||||
}else{
|
||||
@@ -122,6 +124,18 @@ public class ContentParser{
|
||||
throw new IllegalArgumentException("Unit commands must be strings.");
|
||||
}
|
||||
});
|
||||
put(UnitStance.class, (type, data) -> {
|
||||
if(data.isString()){
|
||||
var cmd = content.unitStance(data.asString());
|
||||
if(cmd != null){
|
||||
return cmd;
|
||||
}else{
|
||||
throw new IllegalArgumentException("Unknown unit stance name: " + data.asString());
|
||||
}
|
||||
}else{
|
||||
throw new IllegalArgumentException("Unit stances must be strings.");
|
||||
}
|
||||
});
|
||||
put(BulletType.class, (type, data) -> {
|
||||
if(data.isString()){
|
||||
return field(Bullets.class, data);
|
||||
@@ -257,18 +271,15 @@ public class ContentParser{
|
||||
return new Vec3(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
|
||||
});
|
||||
put(Sound.class, (type, data) -> {
|
||||
if(fieldOpt(Sounds.class, data) != null) return fieldOpt(Sounds.class, data);
|
||||
if(Vars.headless) return new Sound();
|
||||
if(data.isArray()) return new RandomSound(parser.readValue(Sound[].class, data));
|
||||
|
||||
String name = "sounds/" + data.asString();
|
||||
String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
|
||||
var field = fieldOpt(Sounds.class, data);
|
||||
return field != null ? field : Vars.tree.loadSound(data.asString());
|
||||
});
|
||||
put(Music.class, (type, data) -> {
|
||||
var field = fieldOpt(Musics.class, data);
|
||||
|
||||
if(sounds.containsKey(path)) return ((SoundParameter)sounds.get(path).params).sound;
|
||||
var sound = new Sound();
|
||||
AssetDescriptor<?> desc = Core.assets.load(path, Sound.class, new SoundParameter(sound));
|
||||
desc.errored = Throwable::printStackTrace;
|
||||
sounds.put(path, desc);
|
||||
return sound;
|
||||
return field != null ? field : Vars.tree.loadMusic(data.asString());
|
||||
});
|
||||
put(Objectives.Objective.class, (type, data) -> {
|
||||
if(data.isString()){
|
||||
@@ -385,6 +396,17 @@ public class ContentParser{
|
||||
return (T)new Rect(jsonData.get(0).asFloat(), jsonData.get(1).asFloat(), jsonData.get(2).asFloat(), jsonData.get(3).asFloat());
|
||||
}
|
||||
|
||||
//search across different content types to find one by name
|
||||
if(type == UnlockableContent.class){
|
||||
for(ContentType c : typesToSearch){
|
||||
T found = (T)locate(c, jsonData.asString());
|
||||
if(found != null){
|
||||
return found;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("\"" + jsonData.name + "\": No content found with name '" + jsonData.asString() + "'.");
|
||||
}
|
||||
|
||||
if(Content.class.isAssignableFrom(type)){
|
||||
ContentType ctype = contentTypes.getThrow(type, () -> new IllegalArgumentException("No content type for class: " + type.getSimpleName()));
|
||||
String prefix = currentMod != null ? currentMod.name + "-" : "";
|
||||
@@ -424,6 +446,17 @@ public class ContentParser{
|
||||
if(value.has("consumes") && value.get("consumes").isObject()){
|
||||
for(JsonValue child : value.get("consumes")){
|
||||
switch(child.name){
|
||||
case "remove" -> {
|
||||
String[] values = child.isString() ? new String[]{child.asString()} : child.asStringArray();
|
||||
for(String type : values){
|
||||
Class<?> consumeType = resolve("Consume" + Strings.capitalize(type), Consume.class);
|
||||
if(consumeType != Consume.class){
|
||||
block.removeConsumers(b -> consumeType.isAssignableFrom(b.getClass()));
|
||||
}else{
|
||||
Log.warn("Unknown consumer type '@' (Class: @) in consume: remove.", type, "Consume" + Strings.capitalize(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
case "item" -> block.consumeItem(find(ContentType.item, child.asString()));
|
||||
case "itemCharged" -> block.consume((Consume)parser.readValue(ConsumeItemCharged.class, child));
|
||||
case "itemFlammable" -> block.consume((Consume)parser.readValue(ConsumeItemFlammable.class, child));
|
||||
@@ -596,7 +629,7 @@ public class ContentParser{
|
||||
ContentType.planet, (TypeParser<Planet>)(mod, name, value) -> {
|
||||
if(value.isString()) return locate(ContentType.planet, name);
|
||||
|
||||
Planet parent = locate(ContentType.planet, value.getString("parent"));
|
||||
Planet parent = locate(ContentType.planet, value.getString("parent", ""));
|
||||
Planet planet = new Planet(mod + "-" + name, parent, value.getFloat("radius", 1f), value.getInt("sectorSize", 0));
|
||||
|
||||
if(value.has("mesh")){
|
||||
@@ -970,7 +1003,6 @@ public class ContentParser{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Object fieldOpt(Class<?> type, JsonValue value){
|
||||
try{
|
||||
return type.getField(value.asString()).get(null);
|
||||
|
||||
@@ -38,7 +38,7 @@ public class Mods implements Loadable{
|
||||
private @Nullable Scripts scripts;
|
||||
private ContentParser parser = new ContentParser();
|
||||
private ObjectMap<String, Seq<Fi>> bundles = new ObjectMap<>();
|
||||
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites", "sprites-override");
|
||||
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites", "sprites-override", ".git");
|
||||
|
||||
private int totalSprites;
|
||||
private ObjectFloatMap<String> textureResize = new ObjectFloatMap<>();
|
||||
@@ -1180,8 +1180,6 @@ public class Mods implements Loadable{
|
||||
public boolean hidden;
|
||||
/** If true, this mod should be loaded as a Java class mod. This is technically optional, but highly recommended. */
|
||||
public boolean java;
|
||||
/** If true, -outline regions for units are kept when packing. Only use if you know exactly what you are doing. */
|
||||
public boolean keepOutlines;
|
||||
/** To rescale textures with a different size. Represents the size in pixels of the sprite of a 1x1 block. */
|
||||
public float texturescale = 1.0f;
|
||||
/** If true, bleeding is skipped and no content icons are generated. */
|
||||
@@ -1229,7 +1227,6 @@ public class Mods implements Loadable{
|
||||
", softDependencies=" + softDependencies +
|
||||
", hidden=" + hidden +
|
||||
", java=" + java +
|
||||
", keepOutlines=" + keepOutlines +
|
||||
", texturescale=" + texturescale +
|
||||
", pregenerated=" + pregenerated +
|
||||
'}';
|
||||
|
||||
@@ -577,6 +577,10 @@ public class Administration{
|
||||
changed.run();
|
||||
}
|
||||
|
||||
public boolean isDefault(){
|
||||
return Structs.eq(get(), defaultValue);
|
||||
}
|
||||
|
||||
private static boolean debug(){
|
||||
return Config.debug.bool();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.net.*;
|
||||
import arc.net.FrameworkMessage.*;
|
||||
import arc.net.Server.*;
|
||||
import arc.net.dns.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
@@ -93,7 +94,9 @@ public class ArcNetProvider implements NetProvider{
|
||||
server.setMulticast(multicastGroup, multicastPort);
|
||||
server.setDiscoveryHandler((address, handler) -> {
|
||||
ByteBuffer buffer = NetworkIO.writeServerData();
|
||||
int length = buffer.position();
|
||||
buffer.position(0);
|
||||
buffer.limit(length);
|
||||
handler.respond(buffer);
|
||||
});
|
||||
|
||||
@@ -161,6 +164,11 @@ public class ArcNetProvider implements NetProvider{
|
||||
server.setConnectFilter(connectFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ServerConnectFilter getConnectFilter(){
|
||||
return server.getConnectFilter();
|
||||
}
|
||||
|
||||
private static boolean isLocal(InetAddress addr){
|
||||
if(addr.isAnyLocalAddress() || addr.isLoopbackAddress()) return true;
|
||||
|
||||
@@ -327,7 +335,7 @@ public class ArcNetProvider implements NetProvider{
|
||||
|
||||
@Override
|
||||
public void sendStream(Streamable stream){
|
||||
connection.addListener(new InputStreamSender(stream.stream, 512){
|
||||
connection.addListener(new InputStreamSender(stream.stream, 1024){
|
||||
int id;
|
||||
|
||||
@Override
|
||||
@@ -446,7 +454,7 @@ public class ArcNetProvider implements NetProvider{
|
||||
byteBuffer.put((byte)-2); //code for framework message
|
||||
writeFramework(byteBuffer, msg);
|
||||
}else{
|
||||
if(!(o instanceof Packet pack)) throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass());
|
||||
if(!(o instanceof Packet pack)) throw new RuntimeException("All sent objects must extend Packet! Class: " + o.getClass());
|
||||
byte id = Net.getPacketId(pack);
|
||||
byteBuffer.put(id);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user