Merge branch 'master' into mod-dependencies

This commit is contained in:
MEEPofFaith
2024-08-19 02:09:41 -07:00
195 changed files with 6964 additions and 4228 deletions

View File

@@ -66,11 +66,11 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Time.setDeltaProvider(() -> {
float result = Core.graphics.getDeltaTime() * 60f;
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, maxDeltaClient);
});
UI.loadColors();
batch = new SortedSpriteBatch();
batch = new SpriteBatch();
assets = new AssetManager();
assets.setLoader(Texture.class, "." + mapExtension, new MapPreviewLoader());

View File

@@ -137,6 +137,13 @@ public class Vars implements Loadable{
Color.valueOf("4b5ef1"),
Color.valueOf("2cabfe"),
};
/** Icons available to the user for customization in certain dialogs. */
public static final String[] accessibleIcons = {
"effect", "power", "logic", "units", "liquid", "production", "defense", "turret", "distribution", "crafting",
"settings", "cancel", "zoom", "ok", "star", "home", "pencil", "up", "down", "left", "right",
"hammer", "warning", "tree", "admin", "map", "modePvp", "terrain",
"modeSurvival", "commandRally", "commandAttack",
};
/** maximum TCP packet size */
public static final int maxTcpSize = 900;
/** default server port */
@@ -147,6 +154,8 @@ public class Vars implements Loadable{
public static final int maxModSubtitleLength = 40;
/** multicast group for discovery.*/
public static final String multicastGroup = "227.2.7.7";
/** Maximum delta time. If the actual delta time (*60) between frames is higher than this number, the game will start to slow down. */
public static float maxDeltaClient = 6f, maxDeltaServer = 10f;
/** whether the graphical game client has loaded */
public static boolean clientLoaded = false;
/** max GL texture size */

View File

@@ -32,7 +32,6 @@ public class BaseBuilderAI{
private static int correct = 0, incorrect = 0;
private int lastX, lastY, lastW, lastH;
private boolean foundPath;
final TeamData data;
@@ -262,11 +261,6 @@ public class BaseBuilderAI{
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block.id, tile.config));
}
lastX = cx - 1;
lastY = cy - 1;
lastW = result.width + 2;
lastH = result.height + 2;
return true;
}
}

View File

@@ -130,13 +130,13 @@ public class BlockIndexer{
data.turretTree.remove(build);
}
//is no longer registered
build.wasDamaged = false;
//unregister damaged buildings
if(build.damaged() && damagedTiles[team.id] != null){
if(build.wasDamaged && damagedTiles[team.id] != null){
damagedTiles[team.id].remove(build);
}
//is no longer registered
build.wasDamaged = false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.turrets.BaseTurret.*;
import mindustry.world.blocks.defense.turrets.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.meta.*;
@@ -77,6 +78,7 @@ public class RtsAI{
}
public void update(){
if(timer.get(timeUpdate, 60f * 2f)){
assignSquads();
checkBuilding();
@@ -110,7 +112,8 @@ public class RtsAI{
for(var unit : data.units){
if(used.add(unit.id) && unit.isCommandable() && !unit.command().hasCommand() && !unit.command().isAttacking()){
squad.clear();
data.tree().intersect(unit.x - squadRadius/2f, unit.y - squadRadius/2f, squadRadius, squadRadius, squad);
float rad = squadRadius + unit.hitSize*1.5f;
data.tree().intersect(unit.x - rad/2f, unit.y - rad/2f, rad, rad, squad);
squad.truncate(data.team.rules().rtsMaxSquad);
@@ -244,7 +247,7 @@ public class RtsAI{
}
}
var build = anyDefend ? null : findTarget(ax, ay, units.size, dps, health, units.first().flag == 0);
var build = anyDefend ? null : findTarget(ax, ay, units.size, dps, health, units.first().flag == 0, units.first().isFlying());
if(build != null || anyDefend){
for(var unit : units){
@@ -267,7 +270,7 @@ public class RtsAI{
return anyDefend;
}
@Nullable Building findTarget(float x, float y, int total, float dps, float health, boolean checkWeight){
@Nullable Building findTarget(float x, float y, int total, float dps, float health, boolean checkWeight, boolean air){
if(total < data.team.rules().rtsMinSquad) return null;
//flag priority?
@@ -289,7 +292,7 @@ public class RtsAI{
targets.truncate(maxTargetsChecked);
for(var target : targets){
weights.put(target, estimateStats(x, y, target.x, target.y, dps, health));
weights.put(target, estimateStats(x, y, target.x, target.y, dps, health, air));
}
var result = targets.min(
@@ -311,12 +314,12 @@ public class RtsAI{
}
//TODO extremely slow especially with many squads.
float estimateStats(float fromX, float fromY, float x, float y, float selfDps, float selfHealth){
float estimateStats(float fromX, float fromY, float x, float y, float selfDps, float selfHealth, boolean air){
float[] health = {0f}, dps = {0f};
float extraRadius = 50f;
for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){
if(turret instanceof BaseTurretBuild t && Intersector.distanceSegmentPoint(fromX, fromY, x, y, t.x, t.y) <= t.range() + extraRadius){
if(turret instanceof BaseTurretBuild t && turret.block instanceof Turret tb && ((tb.targetAir && air) || (tb.targetGround && !air)) && Intersector.distanceSegmentPoint(fromX, fromY, x, y, t.x, t.y) <= t.range() + extraRadius){
health[0] += t.health;
dps[0] += t.estimateDps();
}

View File

@@ -168,9 +168,9 @@ public class UnitGroup{
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));
int res = ControlPathfinder.raycastFastAvoid(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
//collision found, make the 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));

View File

@@ -152,14 +152,21 @@ public class WaveSpawner{
}
private void eachFlyerSpawn(int filterPos, Floatc2 cons){
boolean airUseSpawns = state.rules.airUseSpawns;
for(Tile tile : spawns){
if(filterPos != -1 && filterPos != tile.pos()) continue;
float angle = Angles.angle(world.width() / 2f, world.height() / 2f, tile.x, tile.y);
float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize;
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin);
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(angle, trns), -margin, world.height() * tilesize + margin);
cons.get(spawnX, spawnY);
if(!airUseSpawns){
float angle = Angles.angle(world.width() / 2f, world.height() / 2f, tile.x, tile.y);
float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize;
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin);
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(angle, trns), -margin, world.height() * tilesize + margin);
cons.get(spawnX, spawnY);
}else{
cons.get(tile.worldx(), tile.worldy());
}
}
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam)){

View File

@@ -47,6 +47,8 @@ public class BuilderAI extends AIController{
if(target != null && shouldShoot()){
unit.lookAt(target);
}else if(!unit.type.flying){
unit.lookAt(unit.prefRotation());
}
unit.updateBuilding = true;
@@ -55,6 +57,8 @@ public class BuilderAI extends AIController{
following = assistFollowing;
}
boolean moving = false;
if(following != null){
retreatTimer = 0f;
//try to follow and mimic someone
@@ -83,6 +87,7 @@ public class BuilderAI extends AIController{
var core = unit.closestCore();
if(core != null && !unit.within(core, retreatDst)){
moveTo(core, retreatDst);
moving = true;
}
}
}
@@ -114,7 +119,8 @@ public class BuilderAI extends AIController{
if(valid){
//move toward the plan
moveTo(req.tile(), unit.type.buildRange - 20f);
moveTo(req.tile(), unit.type.buildRange - 20f, 20f);
moving = !unit.within(req.tile(), unit.type.buildRange - 10f);
}else{
//discard invalid plan
unit.plans.removeFirst();
@@ -124,6 +130,7 @@ public class BuilderAI extends AIController{
if(assistFollowing != null){
moveTo(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 60f);
moving = !unit.within(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 65f);
}
//follow someone and help them build
@@ -153,7 +160,7 @@ public class BuilderAI extends AIController{
float minDst = Float.MAX_VALUE;
Player closest = null;
for(var player : Groups.player){
if(player.unit().canBuild() && !player.dead() && player.team() == unit.team){
if(!player.dead() && player.isBuilder() && player.team() == unit.team){
float dst = player.dst2(unit);
if(dst < minDst){
closest = player;
@@ -186,6 +193,10 @@ public class BuilderAI extends AIController{
}
}
}
if(!unit.type.flying){
unit.updateBoosting(moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f);
}
}
protected boolean nearEnemy(int x, int y){

View File

@@ -4,7 +4,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.core.*;
import mindustry.entities.*;
@@ -35,7 +34,6 @@ public class CommandAI extends AIController{
protected boolean stopAtTarget, stopWhenInRange;
protected Vec2 lastTargetPos;
protected int pathId = -1;
protected boolean blockingUnit;
protected float timeSpentBlocked;
@@ -205,6 +203,11 @@ public class CommandAI extends AIController{
}
}
boolean alwaysArrive = false;
float engageRange = unit.type.range - 10f;
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram;
if(targetPos != null){
boolean move = true, isFinalPoint = commandQueue.size == 0;
vecOut.set(targetPos);
@@ -221,6 +224,7 @@ public class CommandAI extends AIController{
}
if(unit.isGrounded() && stance != UnitStance.ram){
//TODO: blocking enable or disable?
if(timer.get(timerTarget3, avoidInterval)){
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
float max = unit.hitSize/2f;
@@ -248,8 +252,18 @@ public class CommandAI extends AIController{
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);
//if the unit is next to the target, stop asking the pathfinder how to get there, it's a waste of CPU
//TODO maybe stop moving too?
if(withinAttackRange){
move = true;
noFound[0] = false;
vecOut.set(vecMovePos);
}else{
move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
}
//rare case where unit must be perfectly aligned (happens with 1-tile gaps)
alwaysArrive = vecOut.epsilonEquals(unit.tileX() * tilesize, unit.tileY() * tilesize);
//we've reached the final point if the returned coordinate is equal to the supplied input
isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f);
@@ -266,18 +280,16 @@ public class CommandAI extends AIController{
vecOut.set(vecMovePos);
}
float engageRange = unit.type.range - 10f;
if(move){
if(unit.type.circleTarget && attackTarget != null){
target = attackTarget;
circleAttack(80f);
}else{
moveTo(vecOut,
attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram ? engageRange :
withinAttackRange ? engageRange :
unit.isGrounded() ? 0f :
attackTarget != null && stance != UnitStance.ram ? engageRange :
0f, unit.isFlying() ? 40f : 100f, false, null, isFinalPoint);
attackTarget != null && stance != UnitStance.ram ? engageRange : 0f,
unit.isFlying() ? 40f : 100f, false, null, isFinalPoint || alwaysArrive);
}
}
@@ -417,7 +429,6 @@ public class CommandAI extends AIController{
//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;
}
@@ -432,7 +443,6 @@ public class CommandAI extends AIController{
public void commandTarget(Teamc moveTo, boolean stopAtTarget){
attackTarget = moveTo;
this.stopAtTarget = stopAtTarget;
pathId = Vars.controlPath.nextTargetId();
}
/*

View File

@@ -39,7 +39,7 @@ public class FlyingFollowAI extends FlyingAI{
@Override
public void updateVisuals(){
if(unit.isFlying()){
unit.wobble();
if(unit.type.wobble) unit.wobble();
if(!shouldFaceTarget()){
unit.lookAt(unit.prefRotation());

View File

@@ -3,7 +3,6 @@ package mindustry.ai.types;
import arc.math.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
@@ -41,8 +40,6 @@ public class LogicAI extends AIController{
public PosTeam posTarget = PosTeam.create();
private ObjectSet<Object> radars = new ObjectSet<>();
private float lastMoveX, lastMoveY;
private int lastPathId = 0;
// LogicAI state should not be reset after reading.
@Override
@@ -52,14 +49,6 @@ public class LogicAI extends AIController{
@Override
public void updateMovement(){
if(control == LUnitControl.pathfind){
if(!Mathf.equal(moveX, lastMoveX, 0.1f) || !Mathf.equal(moveY, lastMoveY, 0.1f)){
lastPathId ++;
lastMoveX = moveX;
lastMoveY = moveY;
}
}
if(targetTimer > 0f){
targetTimer -= Time.delta;
}else{
@@ -86,7 +75,7 @@ public class LogicAI extends AIController{
if(unit.isFlying()){
moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f);
}else{
if(Vars.controlPath.getPathPosition(unit, lastPathId, Tmp.v2.set(moveX, moveY), Tmp.v1, null)){
if(controlPath.getPathPosition(unit, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){
moveTo(Tmp.v1, 1f, Tmp.v2.epsilonEquals(Tmp.v1, 4.1f) ? 30f : 0f);
}
}

View File

@@ -164,8 +164,11 @@ public class SoundControl{
//this just fades out the last track to make way for ingame music
silence();
//play music at intervals
if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
if(Core.settings.getBool("alwaysmusic")){
if(current == null){
playRandom();
}
}else if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
//chance to play it per interval
if(Mathf.chance(musicChance)){
lastPlayed = Time.millis();
@@ -213,7 +216,9 @@ public class SoundControl{
/** Plays a random track.*/
public void playRandom(){
if(isDark()){
if(state.boss() != null){
playOnce(bossMusic.random(lastRandomPlayed));
}else if(isDark()){
playOnce(darkMusic.random(lastRandomPlayed));
}else{
playOnce(ambientMusic.random(lastRandomPlayed));

View File

@@ -2556,8 +2556,8 @@ public class Blocks{
fluxReactor = new VariableReactor("flux-reactor"){{
requirements(Category.power, with(Items.graphite, 300, Items.carbide, 200, Items.oxide, 100, Items.silicon, 600, Items.surgeAlloy, 300));
powerProduction = 120f;
maxHeat = 140f;
powerProduction = 240f;
maxHeat = 150f;
consumeLiquid(Liquids.cyanogen, 9f / 60f);
liquidCapacity = 30f;
@@ -4108,7 +4108,7 @@ public class Blocks{
rotateSpeed = 3f;
coolant = consume(new ConsumeLiquid(Liquids.water, 15f / 60f));
limitRange(16f);
limitRange(25f);
}};
sublimate = new ContinuousLiquidTurret("sublimate"){{
@@ -4152,6 +4152,7 @@ public class Blocks{
liquidConsumed = 10f / 60f;
targetInterval = 5f;
newTargetInterval = 30f;
targetUnderBlocks = false;
float r = range = 130f;
@@ -4242,6 +4243,8 @@ public class Blocks{
shootY = 7f;
rotateSpeed = 1.4f;
minWarmup = 0.85f;
newTargetInterval = 40f;
shootWarmupSpeed = 0.07f;
coolant = consume(new ConsumeLiquid(Liquids.water, 30f / 60f));
@@ -4379,6 +4382,7 @@ public class Blocks{
trailInterval = 3f;
trailParam = 4f;
pierceCap = 2;
buildingDamageMultiplier = 0.5f;
fragOnHit = false;
speed = 5f;
damage = 180f;
@@ -4462,6 +4466,8 @@ public class Blocks{
heatRequirement = 10f;
maxHeatEfficiency = 2f;
newTargetInterval = 40f;
inaccuracy = 1f;
shake = 2f;
shootY = 4;
@@ -4684,6 +4690,8 @@ public class Blocks{
shootSound = Sounds.missileLaunch;
minWarmup = 0.94f;
newTargetInterval = 40f;
unitSort = UnitSorts.strongest;
shootWarmupSpeed = 0.03f;
targetAir = false;
targetUnderBlocks = false;

View File

@@ -37,7 +37,9 @@ public class Bullets{
damageLightningGround = damageLightning.copy();
damageLightningGround.collidesAir = false;
fireball = new FireBulletType(1f, 4);
fireball = new FireBulletType(1f, 4){{
hittable = false;
}};
spaceLiquid = new SpaceLiquidBulletType(){{
knockback = 0.7f;

View File

@@ -2434,12 +2434,9 @@ public class Fx{
shieldBreak = new Effect(40, e -> {
color(e.color);
stroke(3f * e.fout());
if(e.data instanceof Unit u){
var ab = (ForceFieldAbility)Structs.find(u.abilities, a -> a instanceof ForceFieldAbility);
if(ab != null){
Lines.poly(e.x, e.y, ab.sides, e.rotation + e.fin(), ab.rotation);
return;
}
if(e.data instanceof ForceFieldAbility ab){
Lines.poly(e.x, e.y, ab.sides, e.rotation + e.fin(), ab.rotation);
return;
}
Lines.poly(e.x, e.y, 6, e.rotation + e.fin());
@@ -2584,7 +2581,7 @@ public class Fx{
if(!(e.data instanceof Vec2[] vec)) return;
Draw.color(e.color);
Lines.stroke(1f);
Lines.stroke(2f);
if(vec.length == 2){
Lines.line(vec[0].x, vec[0].y, vec[1].x, vec[1].y);
@@ -2596,5 +2593,15 @@ public class Fx{
}
Draw.reset();
}),
debugRect = new Effect(90f, 1000000000000f, e -> {
if(!(e.data instanceof Rect rect)) return;
Draw.color(e.color);
Lines.stroke(2f);
Lines.rect(rect);
Draw.reset();
});
}

View File

@@ -322,7 +322,7 @@ public class UnitTypes{
speed = 0.55f;
hitSize = 8f;
health = 120f;
buildSpeed = 0.8f;
buildSpeed = 0.35f;
armor = 1f;
abilities.add(new RepairFieldAbility(10f, 60f * 4, 60f));
@@ -354,7 +354,7 @@ public class UnitTypes{
speed = 0.7f;
hitSize = 11f;
health = 320f;
buildSpeed = 0.9f;
buildSpeed = 0.5f;
armor = 4f;
riseSpeed = 0.07f;
@@ -408,7 +408,7 @@ public class UnitTypes{
mineTier = 3;
boostMultiplier = 2f;
health = 640f;
buildSpeed = 1.7f;
buildSpeed = 1.1f;
canBoost = true;
armor = 9f;
mechLandShake = 2f;

View File

@@ -171,6 +171,13 @@ public class ContentLoader{
public void handleMappableContent(MappableContent content){
if(contentNameMap[content.getContentType().ordinal()].containsKey(content.name)){
var list = contentMap[content.getContentType().ordinal()];
//this method is only called when registering content, and after handleContent.
//If this is the last registered content, and it is invalid, make sure to remove it from the list to prevent invalid stuff from being registered
if(list.size > 0 && list.peek() == content){
list.pop();
}
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + content.name + "')");
}
if(currentMod != null){

View File

@@ -47,17 +47,7 @@ public class Logic implements ApplicationListener{
Events.on(BlockBuildEndEvent.class, event -> {
if(!event.breaking){
TeamData data = event.team.data();
Iterator<BlockPlan> it = data.plans.iterator();
var bounds = event.tile.block().bounds(event.tile.x, event.tile.y, Tmp.r1);
while(it.hasNext()){
BlockPlan b = it.next();
Block block = content.block(b.block);
if(bounds.overlaps(block.bounds(b.x, b.y, Tmp.r2))){
b.removed = true;
it.remove();
}
}
checkOverlappingPlans(event.team, event.tile);
if(event.team == state.rules.defaultTeam){
state.stats.placedBlockCount.increment(event.tile.block());
@@ -65,6 +55,12 @@ public class Logic implements ApplicationListener{
}
});
Events.on(PayloadDropEvent.class, e -> {
if(e.build != null){
checkOverlappingPlans(e.build.team, e.build.tile);
}
});
//when loading a 'damaged' sector, propagate the damage
Events.on(SaveLoadEvent.class, e -> {
if(state.isCampaign()){
@@ -211,6 +207,20 @@ public class Logic implements ApplicationListener{
});
}
private void checkOverlappingPlans(Team team, Tile tile){
TeamData data = team.data();
Iterator<BlockPlan> it = data.plans.iterator();
var bounds = tile.block().bounds(tile.x, tile.y, Tmp.r1);
while(it.hasNext()){
BlockPlan b = it.next();
Block block = content.block(b.block);
if(bounds.overlaps(block.bounds(b.x, b.y, Tmp.r2))){
b.removed = true;
it.remove();
}
}
}
/** Adds starting items, resets wave time, and sets state to playing. */
public void play(){
state.set(State.playing);
@@ -251,6 +261,7 @@ public class Logic implements ApplicationListener{
Groups.clear();
Time.clear();
Events.fire(new ResetEvent());
world.tiles = new Tiles(0, 0);
//save settings on reset
Core.settings.manualSave();

View File

@@ -478,7 +478,7 @@ public class NetClient implements ApplicationListener{
Log.warn("Block ID mismatch at @: @ != @. Skipping block snapshot.", tile, tile.build.block.id, block);
break;
}
tile.build.readAll(Reads.get(input), tile.build.version());
tile.build.readSync(Reads.get(input), tile.build.version());
}
}catch(Exception e){
Log.err(e);
@@ -622,21 +622,22 @@ public class NetClient implements ApplicationListener{
void sync(){
if(timer.get(0, playerSyncTime)){
Unit unit = player.dead() ? Nulls.unit : player.unit();
int uid = player.dead() ? -1 : unit.id;
boolean dead = player.dead();
Unit unit = dead ? null : player.unit();
int uid = dead || unit == null ? -1 : unit.id;
Call.clientSnapshot(
lastSent++,
uid,
player.dead(),
player.dead() ? player.x : unit.x, player.dead() ? player.y : unit.y,
player.unit().aimX(), player.unit().aimY(),
unit.rotation,
dead,
dead ? player.x : unit.x, dead ? player.y : unit.y,
dead ? 0f : unit.aimX(), dead ? 0f : unit.aimY(),
unit == null ? 0f : unit.rotation,
unit instanceof Mechc m ? m.baseRotation() : 0,
unit.vel.x, unit.vel.y,
player.unit().mineTile,
unit == null ? 0f : unit.vel.x, unit == null ? 0f : unit.vel.y,
dead ? null : unit.mineTile,
player.boosting, player.shooting, ui.chatfrag.shown(), control.input.isBuilding,
player.isBuilder() ? player.unit().plans : null,
player.isBuilder() && unit != null ? unit.plans : null,
Core.camera.position.x, Core.camera.position.y,
Core.camera.width, Core.camera.height
);

View File

@@ -59,7 +59,7 @@ public class NetServer implements ApplicationListener{
count++;
}
}
return count;
return (float)count + Mathf.random(-0.1f, 0.1f); //if several have the same playercount pick random
});
return re == null ? null : re.team;
}
@@ -98,7 +98,7 @@ public class NetServer implements ApplicationListener{
private boolean closing = false, pvpAutoPaused = true;
private Interval timer = new Interval(10);
private IntSet buildHealthChanged = new IntSet();
/** Current kick session. */
public @Nullable VoteSession currentlyKicking = null;
/** Duration of a kick in seconds. */
@@ -117,6 +117,8 @@ public class NetServer implements ApplicationListener{
private DataOutputStream dataStream = new DataOutputStream(syncStream);
/** Packet handlers for custom types of messages. */
private ObjectMap<String, Seq<Cons2<Player, String>>> customPacketHandlers = new ObjectMap<>();
/** Packet handlers for logic client data */
private ObjectMap<String, Seq<Cons2<Player, Object>>> logicClientDataHandlers = new ObjectMap<>();
public NetServer(){
@@ -203,7 +205,7 @@ public class NetServer implements ApplicationListener{
info.id = packet.uuid;
admins.save();
Call.infoMessage(con, "You are not whitelisted here.");
info("&lcDo &lywhitelist-add @&lc to whitelist the player &lb'@'", packet.uuid, packet.name);
info("&lcDo &lywhitelist add @&lc to whitelist the player &lb'@'", packet.uuid, packet.name);
con.kick(KickReason.whitelist);
return;
}
@@ -515,6 +517,10 @@ public class NetServer implements ApplicationListener{
return customPacketHandlers.get(type, Seq::new);
}
public void addLogicDataHandler(String type, Cons2<Player, Object> handler){
logicClientDataHandlers.get(type, Seq::new).add(handler);
}
public static void onDisconnect(Player player, String reason){
//singleplayer multiplayer weirdness
if(player.con == null){
@@ -542,10 +548,10 @@ public class NetServer implements ApplicationListener{
@Remote(targets = Loc.client, variants = Variant.one)
public static void requestDebugStatus(Player player){
int flags =
(player.con.hasDisconnected ? 1 : 0) |
(player.con.hasConnected ? 2 : 0) |
(player.isAdded() ? 4 : 0) |
(player.con.hasBegunConnecting ? 8 : 0);
(player.con.hasDisconnected ? 1 : 0) |
(player.con.hasConnected ? 2 : 0) |
(player.isAdded() ? 4 : 0) |
(player.con.hasBegunConnecting ? 8 : 0);
Call.debugStatusClient(player.con, flags, player.con.lastReceivedClientSnapshot, player.con.snapshotsSent);
Call.debugStatusClientUnreliable(player.con, flags, player.con.lastReceivedClientSnapshot, player.con.snapshotsSent);
@@ -583,24 +589,39 @@ public class NetServer implements ApplicationListener{
serverPacketReliable(player, type, contents);
}
@Remote(targets = Loc.client)
public static void clientLogicDataReliable(Player player, String channel, Object value){
Seq<Cons2<Player, Object>> handlers = netServer.logicClientDataHandlers.get(channel);
if(handlers != null){
for(Cons2<Player, Object> handler : handlers){
handler.get(player, value);
}
}
}
@Remote(targets = Loc.client, unreliable = true)
public static void clientLogicDataUnreliable(Player player, String channel, Object value){
clientLogicDataReliable(player, channel, value);
}
private static boolean invalid(float f){
return Float.isInfinite(f) || Float.isNaN(f);
}
@Remote(targets = Loc.client, unreliable = true)
public static void clientSnapshot(
Player player,
int snapshotID,
int unitID,
boolean dead,
float x, float y,
float pointerX, float pointerY,
float rotation, float baseRotation,
float xVelocity, float yVelocity,
Tile mining,
boolean boosting, boolean shooting, boolean chatting, boolean building,
@Nullable Queue<BuildPlan> plans,
float viewX, float viewY, float viewWidth, float viewHeight
Player player,
int snapshotID,
int unitID,
boolean dead,
float x, float y,
float pointerX, float pointerY,
float rotation, float baseRotation,
float xVelocity, float yVelocity,
Tile mining,
boolean boosting, boolean shooting, boolean chatting, boolean building,
@Nullable Queue<BuildPlan> plans,
float viewX, float viewY, float viewWidth, float viewHeight
){
NetConnection con = player.con;
if(con == null || snapshotID < con.lastReceivedClientSnapshot) return;
@@ -639,12 +660,11 @@ public class NetServer implements ApplicationListener{
player.shooting = shooting;
player.boosting = boosting;
player.unit().controlWeapons(shooting, shooting);
player.unit().aim(pointerX, pointerY);
@Nullable var unit = player.unit();
if(player.isBuilder()){
player.unit().clearBuilding();
player.unit().updateBuilding(building);
unit.clearBuilding();
unit.updateBuilding(building);
if(plans != null){
for(BuildPlan req : plans){
@@ -673,12 +693,12 @@ public class NetServer implements ApplicationListener{
}
}
player.unit().mineTile = mining;
con.rejectedRequests.clear();
if(!player.dead()){
Unit unit = player.unit();
unit.controlWeapons(shooting, shooting);
unit.aim(pointerX, pointerY);
unit.mineTile = mining;
long elapsed = Math.min(Time.timeSinceMillis(con.lastReceivedClientTime), 1500);
float maxSpeed = unit.speed();
@@ -893,7 +913,7 @@ public class NetServer implements ApplicationListener{
dataStream.writeInt(entity.pos());
dataStream.writeShort(entity.block.id);
entity.writeAll(Writes.get(dataStream));
entity.writeSync(Writes.get(dataStream));
if(syncStream.size() > maxSnapshotSize){
dataStream.close();
@@ -1104,7 +1124,7 @@ public class NetServer implements ApplicationListener{
voted.put(admins.getInfo(player.uuid()).lastIP, d);
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[lightgray].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
player.name, target.name, votes, votesRequired()));
player.name, target.name, votes, votesRequired()));
checkPass();
}

View File

@@ -1,6 +1,7 @@
package mindustry.core;
import arc.*;
import arc.filedialogs.*;
import arc.files.*;
import arc.func.*;
import arc.math.*;
@@ -141,7 +142,9 @@ public interface Platform{
* @param title The title of the native dialog
*/
default void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){
if(OS.isLinux && !OS.isAndroid){
if(OS.isWindows || OS.isMac){
showNativeFileChooser(open, title, cons, extension);
}else if(OS.isLinux && !OS.isAndroid){
showZenity(open, title, new String[]{extension}, cons, () -> defaultFileDialog(open, title, extension, cons));
}else{
defaultFileDialog(open, title, extension, cons);
@@ -223,6 +226,8 @@ public interface Platform{
default void showMultiFileChooser(Cons<Fi> cons, String... extensions){
if(mobile){
showFileChooser(true, extensions[0], cons);
}else if(OS.isWindows || OS.isMac){
showNativeFileChooser(true, "@open", cons, extensions);
}else if(OS.isLinux && !OS.isAndroid){
showZenity(true, "@open", extensions, cons, () -> defaultMultiFileChooser(cons, extensions));
}else{
@@ -234,6 +239,68 @@ public interface Platform{
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
}
default void showNativeFileChooser(boolean open, String title, Cons<Fi> cons, String... shownExtensions){
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
//this should never happen unless someone is being dumb with the parameters
String[] ext = shownExtensions == null || shownExtensions.length == 0 ? new String[]{""} : shownExtensions;
//native file dialog
Threads.daemon(() -> {
try{
FileDialogs.loadNatives();
String result;
String[] patterns = new String[ext.length];
for(int i = 0; i < ext.length; i++){
patterns[i] = "*." + ext[i];
}
//on MacOS, .msav is not properly recognized until I put garbage into the array?
if(patterns.length == 1 && OS.isMac && open){
patterns = new String[]{"", "*." + ext[0]};
}
if(open){
result = FileDialogs.openFileDialog(formatted, FileChooser.getLastDirectory().absolutePath(), patterns, "." + ext[0] + " files", false);
}else{
result = FileDialogs.saveFileDialog(formatted, FileChooser.getLastDirectory().child("file." + ext[0]).absolutePath(), patterns, "." + ext[0] + " files");
}
if(result == null) return;
if(result.length() > 1 && result.contains("\n")){
result = result.split("\n")[0];
}
//cancelled selection, ignore result
if(result.isEmpty() || result.equals("\n")) return;
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
Fi file = Core.files.absolute(result);
Core.app.post(() -> {
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent());
if(!open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + ext[0]));
}else{
cons.get(file);
}
});
}catch(Throwable error){
Log.err("Failure to execute native file chooser", error);
Core.app.post(() -> {
if(ext.length > 1){
defaultMultiFileChooser(cons, ext);
}else{
defaultFileDialog(open, title, ext[0], cons);
}
});
}
});
}
/** Hide the app. Android only. */
default void hide(){
}

View File

@@ -2,6 +2,7 @@ package mindustry.core;
import arc.*;
import arc.assets.loaders.TextureLoader.*;
import arc.audio.*;
import arc.files.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
@@ -554,6 +555,11 @@ public class Renderer implements ApplicationListener{
landTime = landCore.landDuration();
launchCoreType = coreType;
Music music = landCore.launchMusic();
music.stop();
music.play();
music.setVolume(settings.getInt("musicvol") / 100f);
landCore.beginLaunch(coreType);
}

View File

@@ -158,7 +158,7 @@ public class UI implements ApplicationListener, Loadable{
Core.scene.draw();
if(Core.input.keyTap(KeyCode.mouseLeft) && Core.scene.hasField()){
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(!(e instanceof TextField)){
Core.scene.setKeyboardFocus(null);
}

View File

@@ -64,7 +64,7 @@ public abstract class UnlockableContent extends MappableContent{
@Override
public void loadIcon(){
fullIcon =
Core.atlas.find(fullOverride,
Core.atlas.find(fullOverride == null ? "" : fullOverride,
Core.atlas.find(getContentType().name() + "-" + name + "-full",
Core.atlas.find(name + "-full",
Core.atlas.find(name,
@@ -147,6 +147,11 @@ public abstract class UnlockableContent extends MappableContent{
return Fonts.getUnicodeStr(name);
}
public int emojiChar(){
return Fonts.getUnicode(name);
}
public boolean hasEmoji(){
return Fonts.hasUnicodeStr(name);
}

View File

@@ -24,7 +24,6 @@ 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.*;
@@ -212,11 +211,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
margin(0);
update(() -> {
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
return;
}
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
if(hasKeyboard()){
doInput();
}
});

View File

@@ -14,16 +14,15 @@ import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
public class MapInfoDialog extends BaseDialog{
private final WaveInfoDialog waveInfo;
private final MapGenerateDialog generate;
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
private final MapObjectivesDialog objectives = new MapObjectivesDialog();
private final MapLocalesDialog locales = new MapLocalesDialog();
private WaveInfoDialog waveInfo = new WaveInfoDialog();
private MapGenerateDialog generate = new MapGenerateDialog(false);
private CustomRulesDialog ruleInfo = new CustomRulesDialog();
private MapObjectivesDialog objectives = new MapObjectivesDialog();
private MapLocalesDialog locales = new MapLocalesDialog();
private MapProcessorsDialog processors = new MapProcessorsDialog();
public MapInfoDialog(){
super("@editor.mapinfo");
this.waveInfo = new WaveInfoDialog();
this.generate = new MapGenerateDialog(false);
addCloseButton();
@@ -108,7 +107,12 @@ public class MapInfoDialog extends BaseDialog{
ui.showException(e);
}
hide();
}).marginLeft(10f).width(0f).colspan(2).center().growX();
}).marginLeft(10f);
r.button("@editor.worldprocessors", Icon.logic, style, () -> {
hide();
processors.show();
}).marginLeft(10f);
}).colspan(2).center();
name.change();

View File

@@ -39,7 +39,7 @@ public class MapLoadDialog extends BaseDialog{
ButtonGroup<Button> group = new ButtonGroup<>();
int i = 0;
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(250f)), 1);
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(300f)), 1);
Table table = new Table();
table.defaults().size(250f, 90f).pad(4f);
@@ -53,7 +53,7 @@ public class MapLoadDialog extends BaseDialog{
table.button(b -> {
b.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).padLeft(5f).size(16 * 4f);
b.add(map.name()).wrap().grow().labelAlign(Align.center).padLeft(5f);
}, Styles.squareTogglet, () -> selected = map).group(group).checked(b -> selected == map);
}, Styles.squareTogglet, () -> selected = map).group(group).margin(8f).checked(b -> selected == map);
if(++i % cols == 0) table.row();
}

View File

@@ -0,0 +1,155 @@
package mindustry.editor;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.logic.*;
import mindustry.world.blocks.logic.LogicBlock.*;
import static mindustry.Vars.*;
public class MapProcessorsDialog extends BaseDialog{
private IconSelectDialog iconSelect = new IconSelectDialog();
private TextField search;
private Seq<Building> processors = new Seq<>();
private Table list;
public MapProcessorsDialog(){
super("@editor.worldprocessors");
shown(this::setup);
addCloseButton();
buttons.button("@add", Icon.add, () -> {
boolean foundAny = false;
outer:
for(int y = 0; y < Vars.world.height(); y++){
for(int x = 0; x < Vars.world.width(); x++){
Tile tile = Vars.world.rawTile(x, y);
if(!tile.synthetic()){
foundAny = true;
tile.setNet(Blocks.worldProcessor, Team.sharded, 0);
if(ui.editor.isShown()){
Vars.editor.renderer.updatePoint(x, y);
}
break outer;
}
}
}
if(!foundAny){
ui.showErrorMessage("@editor.worldprocessors.nospace");
}else{
setup();
}
}).size(210f, 64f);
cont.top();
getCell(cont).grow();
cont.table(s -> {
s.image(Icon.zoom).padRight(8);
search = s.field(null, text -> rebuild()).growX().get();
search.setMessageText("@players.search");
}).width(440f).fillX().padBottom(4).row();
cont.pane(t -> {
list = t;
});
}
private void rebuild(){
list.clearChildren();
if(processors.isEmpty()){
list.add("@editor.worldprocessors.none");
}else{
Table t = list;
var text = search.getText().toLowerCase();
t.defaults().pad(4f);
float h = 50f;
for(var build : processors){
if(build instanceof LogicBuild log && (text.isEmpty() || (log.tag != null && log.tag.toLowerCase().contains(text)))){
t.button(log.iconTag == 0 ? Styles.none : new TextureRegionDrawable(Fonts.getLargeIcon(Fonts.unicodeToName(log.iconTag))), Styles.graySquarei, iconMed, () -> {
iconSelect.show(ic -> {
log.iconTag = (char)ic;
rebuild();
});
}).size(h);
t.button((log.tag == null ? "<no name>\n" : "[accent]" + log.tag + "\n") + "[lightgray][[" + log.tile.x + ", " + log.tile.y + "]", Styles.grayt, () -> {
//TODO: bug: if you edit name inside of the edit dialog, it won't show up in the list properly
log.showEditDialog(true);
}).size(Vars.mobile ? 390f : 450f, h).margin(10f).with(b -> {
b.getLabel().setAlignment(Align.left, Align.left);
});
t.button(Icon.pencil, Styles.graySquarei, Vars.iconMed, () -> {
ui.showTextInput("", "@editor.name", LogicBlock.maxNameLength, log.tag == null ? "" : log.tag, tag -> {
//bypass configuration and set it directly in case privileged checks mess things up
log.tag = tag;
setup();
});
}).size(h);
if(Vars.state.isGame() && state.isEditor()){
t.button(Icon.eyeSmall, Styles.graySquarei, Vars.iconMed, () -> {
hide();
control.input.config.showConfig(build);
control.input.panCamera(Tmp.v1.set(build));
}).size(h);
}
t.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
ui.showConfirm("@editor.worldprocessors.delete.confirm", () -> {
boolean surrounded = true;
for(int i = 0; i < 4; i++){
Tile other = build.tile.nearby(i);
if(other != null && !(other.block().privileged || other.block().isStatic())){
surrounded = false;
break;
}
}
if(surrounded){
build.tile.setNet(build.tile.floor().wall instanceof StaticWall ? build.tile.floor().wall : Blocks.stoneWall);
}else{
build.tile.setNet(Blocks.air);
}
processors.remove(build);
rebuild();
});
}).size(h);
t.row();
}
}
}
}
private void setup(){
processors.clear();
//scan the entire world for processor (Groups.build can be empty, indexer is probably inaccurate)
Vars.world.tiles.eachTile(t -> {
if(t.isCenter() && t.block() == Blocks.worldProcessor){
processors.add(t.build);
}
});
rebuild();
}
}

View File

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

View File

@@ -25,7 +25,6 @@ public class Damage{
private static final Rect rect = new Rect();
private static final Rect hitrect = new Rect();
private static final Vec2 vec = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2();
private static final Seq<Unit> units = new Seq<>();
private static final IntSet collidedBlocks = new IntSet();
private static final IntFloatMap damages = new IntFloatMap();
private static final Seq<Collided> collided = new Seq<>();
@@ -41,6 +40,7 @@ public class Damage{
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 -> {
@@ -175,21 +175,23 @@ public class Damage{
distances.clear();
World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + vec.x), World.toTile(b.y + vec.y), (x, y) -> {
//add distance to list so it can be processed
var build = world.build(x, y);
if(b.type.collidesGround && b.type.collidesTiles){
World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + vec.x), World.toTile(b.y + vec.y), (x, y) -> {
//add distance to list so it can be processed
var build = world.build(x, y);
if(build != null && build.team != b.team && build.collide(b) && b.checkUnderBuild(build, x * tilesize, y * tilesize)){
distances.add(b.dst(build));
if(build != null && build.team != b.team && build.collide(b) && b.checkUnderBuild(build, x * tilesize, y * tilesize)){
distances.add(b.dst(build));
if(laser && build.absorbLasers()){
maxDst = Math.min(maxDst, b.dst(build));
return true;
if(laser && build.absorbLasers()){
maxDst = Math.min(maxDst, b.dst(build));
return true;
}
}
}
return false;
});
return false;
});
}
Units.nearbyEnemies(b.team, rect, u -> {
u.hitbox(hitrect);
@@ -243,11 +245,12 @@ public class Damage{
*/
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large, boolean laser, int pierceCap){
length = findLength(hitter, length, laser, pierceCap);
hitter.fdata = length;
collidedBlocks.clear();
vec.trnsExact(angle, length);
if(hitter.type.collidesGround){
if(hitter.type.collidesGround && hitter.type.collidesTiles){
seg1.set(x, y);
seg2.set(seg1).add(vec);
World.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {

View File

@@ -247,4 +247,4 @@ public class EntityCollisions{
public interface SolidPred{
boolean solid(int x, int y);
}
}
}

View File

@@ -32,6 +32,7 @@ public class ForceFieldAbility extends Ability{
/** State. */
protected float radiusScale, alpha;
protected boolean wasBroken = true;
private static float realRad;
private static Unit paramUnit;
@@ -41,13 +42,6 @@ public class ForceFieldAbility extends Ability{
trait.absorb();
Fx.absorb.at(trait);
//break shield
if(paramUnit.shield <= trait.damage()){
paramUnit.shield -= paramField.cooldown * paramField.regen;
Fx.shieldBreak.at(paramUnit.x, paramUnit.y, paramField.radius, paramUnit.team.color, paramUnit);
}
paramUnit.shield -= trait.damage();
paramField.alpha = 1f;
}
@@ -85,6 +79,14 @@ public class ForceFieldAbility extends Ability{
@Override
public void update(Unit unit){
if(unit.shield <= 0f && !wasBroken){
unit.shield -= cooldown * regen;
Fx.shieldBreak.at(unit.x, unit.y, radius, unit.type.shieldColor(unit), this);
}
wasBroken = unit.shield <= 0f;
if(unit.shield < max){
unit.shield += Time.delta * regen;
}
@@ -108,7 +110,7 @@ public class ForceFieldAbility extends Ability{
checkRadius(unit);
if(unit.shield > 0){
Draw.color(unit.team.color, Color.white, Mathf.clamp(alpha));
Draw.color(unit.type.shieldColor(unit), Color.white, Mathf.clamp(alpha));
if(Vars.renderer.animateShields){
Draw.z(Layer.shields + 0.001f * alpha);

View File

@@ -31,7 +31,7 @@ public class ShieldArcAbility extends Ability{
if(paramField.data <= b.damage()){
paramField.data -= paramField.cooldown * paramField.regen;
Fx.arcShieldBreak.at(paramPos.x, paramPos.y, 0, paramUnit.team.color, paramUnit);
Fx.arcShieldBreak.at(paramPos.x, paramPos.y, 0, paramField.color == null ? paramUnit.type.shieldColor(paramUnit) : paramField.color, paramUnit);
}
paramField.data -= b.damage();
@@ -60,6 +60,8 @@ public class ShieldArcAbility extends Ability{
public boolean drawArc = true;
/** If not null, will be drawn on top. */
public @Nullable String region;
/** Color override of the shield. Uses unit shield colour by default. */
public @Nullable Color color;
/** If true, sprite position will be influenced by x/y. */
public boolean offsetRegion = false;
@@ -109,7 +111,7 @@ public class ShieldArcAbility extends Ability{
if(widthScale > 0.001f){
Draw.z(Layer.shields);
Draw.color(unit.team.color, Color.white, Mathf.clamp(alpha));
Draw.color(color == null ? unit.type.shieldColor(unit) : color, Color.white, Mathf.clamp(alpha));
var pos = paramPos.set(x, y).rotate(unit.rotation - 90f).add(unit);
if(!Vars.renderer.animateShields){

View File

@@ -48,13 +48,13 @@ public class ShieldRegenFieldAbility extends Ability{
if(other.shield < max){
other.shield = Math.min(other.shield + amount, max);
other.shieldAlpha = 1f; //TODO may not be necessary
applyEffect.at(unit.x, unit.y, 0f, unit.team.color, parentizeEffects ? other : null);
applyEffect.at(other.x, other.y, 0f, other.type.shieldColor(other), parentizeEffects ? other : null);
applied = true;
}
});
if(applied){
activeEffect.at(unit.x, unit.y, unit.team.color);
activeEffect.at(unit.x, unit.y, unit.type.shieldColor(unit));
}
timer = 0f;

View File

@@ -309,6 +309,8 @@ public class BulletType extends Content implements Cloneable{
/** Color of light emitted by this bullet. */
public Color lightColor = Pal.powerLight;
protected float cachedDps = -1;
public BulletType(float speed, float damage){
this.speed = speed;
this.damage = damage;
@@ -338,15 +340,20 @@ public class BulletType extends Content implements Cloneable{
/** @return estimated damage per shot. this can be very inaccurate. */
public float estimateDPS(){
if(cachedDps >= 0f) return cachedDps;
if(spawnUnit != null){
return spawnUnit.estimateDps();
}
float sum = damage + splashDamage*0.75f;
float sum = damage * (pierce ? pierceCap == -1 ? 2 : Mathf.clamp(pierceCap, 1, 2) : 1f) * splashDamage*0.75f;
if(fragBullet != null && fragBullet != this){
sum += fragBullet.estimateDPS() * fragBullets / 2f;
}
return sum;
for(var other : spawnBullets){
sum += other.estimateDPS();
}
return cachedDps = sum;
}
/** @return maximum distance the bullet this bullet type has can travel. */

View File

@@ -10,31 +10,24 @@ import mindustry.world.blocks.distribution.MassDriver.*;
import static mindustry.Vars.*;
public class MassDriverBolt extends BulletType{
public class MassDriverBolt extends BasicBulletType{
public MassDriverBolt(){
super(1f, 75);
collidesTiles = false;
lifetime = 1f;
width = 11f;
height = 13f;
shrinkY = 0f;
sprite = "shell";
despawnEffect = Fx.smeltsmoke;
hitEffect = Fx.hitBulletBig;
}
@Override
public void draw(Bullet b){
float w = 11f, h = 13f;
Draw.color(Pal.bulletYellowBack);
Draw.rect("shell-back", b.x, b.y, w, h, b.rotation() + 90);
Draw.color(Pal.bulletYellow);
Draw.rect("shell", b.x, b.y, w, h, b.rotation() + 90);
Draw.reset();
}
@Override
public void update(Bullet b){
super.update(b);
//data MUST be an instance of DriverBulletData
if(!(b.data() instanceof DriverBulletData data)){
hit(b);

View File

@@ -118,6 +118,7 @@ public class PointLaserBulletType extends BulletType{
}
}
@Override
public void updateBulletInterval(Bullet b){
if(intervalBullet != null && b.time >= intervalDelay && b.timer.get(2, bulletInterval)){
float ang = b.rotation();

View File

@@ -40,7 +40,7 @@ abstract class BlockUnitComp implements Unitc{
@Replace
@Override
public TextureRegion icon(){
return tile.block.fullIcon;
return tile.block.uiIcon;
}
@Override

View File

@@ -138,7 +138,11 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
if(!(tile.build instanceof ConstructBuild cb)){
if(!current.initialized && !current.breaking && Build.validPlace(current.block, team, current.x, current.y, current.rotation)){
boolean hasAll = infinite || current.isRotation(team) || !Structs.contains(current.block.requirements, i -> core != null && !core.items.has(i.item, Math.min(Mathf.round(i.amount * state.rules.buildCostMultiplier), 1)));
boolean hasAll = infinite || current.isRotation(team) ||
//derelict repair
(tile.team() == Team.derelict && tile.block() == current.block && tile.build != null && tile.block().allowDerelictRepair && state.rules.derelictRepair) ||
//make sure there's at least 1 item of each type first
!Structs.contains(current.block.requirements, i -> core != null && !core.items.has(i.item, Math.min(Mathf.round(i.amount * state.rules.buildCostMultiplier), 1)));
if(hasAll){
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation);
@@ -290,10 +294,6 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
return plans.size == 0 ? null : plans.first();
}
public void draw(){
drawBuilding();
}
public void drawBuilding(){
//TODO make this more generic so it works with builder "weapons"
boolean active = activelyBuilding();

View File

@@ -59,6 +59,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
@Import float x, y, health, maxHealth;
@Import Team team;
@Import boolean dead;
transient Tile tile;
transient Block block;
@@ -87,6 +88,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
transient float optionalEfficiency;
/** The efficiency this block *would* have if shouldConsume() returned true. */
transient float potentialEfficiency;
/** Whether there are any consumers (aside from power) that have efficiency > 0. */
transient boolean shouldConsumePower;
transient float healSuppressionTime = -1f;
transient float lastHealTime = -120f * 10f;
@@ -259,6 +262,14 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
read(read, revision);
}
public void writeSync(Writes write){
writeAll(write);
}
public void readSync(Reads read, byte revision){
readAll(read, revision);
}
@CallSuper
public void write(Writes write){
//overriden by subclasses!
@@ -1359,6 +1370,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Called when the block is destroyed. The tile is still intact at this stage. */
public void onDestroyed(){
if(sound != null){
sound.stop();
}
float explosiveness = block.baseExplosiveness;
float flammability = 0f;
float power = 0f;
@@ -1773,6 +1788,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(!block.hasConsumers || cheating()){
potentialEfficiency = enabled && productionValid() ? 1f : 0f;
efficiency = optionalEfficiency = shouldConsume() ? potentialEfficiency : 0f;
shouldConsumePower = true;
updateEfficiencyMultiplier();
return;
}
@@ -1780,6 +1796,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
//disabled -> nothing works
if(!enabled){
potentialEfficiency = efficiency = optionalEfficiency = 0f;
shouldConsumePower = false;
return;
}
@@ -1789,10 +1806,17 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
//assume efficiency is 1 for the calculations below
efficiency = optionalEfficiency = 1f;
shouldConsumePower = true;
//first pass: get the minimum efficiency of any consumer
for(var cons : block.nonOptionalConsumers){
minEfficiency = Math.min(minEfficiency, cons.efficiency(self()));
float result = cons.efficiency(self());
if(cons != block.consPower && result <= 0.0000001f){
shouldConsumePower = false;
}
minEfficiency = Math.min(minEfficiency, result);
}
//same for optionals
@@ -1915,6 +1939,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
case y -> World.conv(y);
case color -> Color.toDoubleBits(team.color.r, team.color.g, team.color.b, 1f);
case dead -> !isValid() ? 1 : 0;
case solid -> block.solid || checkSolid() ? 1 : 0;
case team -> team.id;
case health -> health;
case maxHealth -> maxHealth;
@@ -1981,9 +2006,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
switch(prop){
case health -> {
health = (float)Mathf.clamp(value, 0, maxHealth);
healthChanged();
if(health <= 0f && !dead()){
Call.buildDestroyed(self());
}else{
healthChanged();
}
}
case team -> {
@@ -2060,6 +2086,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
@Override
public void killed(){
dead = true;
Events.fire(new BlockDestroyEvent(tile));
block.destroySound.at(tile);
onDestroyed();

View File

@@ -35,10 +35,6 @@ abstract class EntityComp{
return ((Object)this) instanceof Unitc u && u.isPlayer() && !isLocal();
}
boolean isNull(){
return false;
}
/** Replaced with `this` after code generation. */
<T extends Entityc> T self(){
return (T)this;

View File

@@ -68,7 +68,7 @@ abstract class HitboxComp implements Posc, Sized, QuadTreeObject{
public void hitboxTile(Rect rect){
//tile hitboxes are never bigger than a tile, otherwise units get stuck
float size = Math.min(hitSize * 0.66f, 7.9f);
float size = Math.min(hitSize * 0.66f, 7.8f);
//TODO: better / more accurate version is
//float size = hitSize * 0.85f;
//- for tanks?

View File

@@ -24,6 +24,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
@Import float x, y, rotation, speedMultiplier;
@Import UnitType type;
@Import Team team;
@Import boolean disarmed;
transient Leg[] legs = {};
transient float totalLength;
@@ -191,7 +192,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
}
}
if(type.legSplashDamage > 0){
if(type.legSplashDamage > 0 && !disarmed){
Damage.damage(team, l.base.x, l.base.y, type.legSplashRange, type.legSplashDamage * state.rules.unitDamage(team), false, true);
}
}

View File

@@ -123,31 +123,4 @@ abstract class MinerComp implements Itemsc, Posc, Teamc, Rotc, Drawc{
}
}
}
@Override
public void draw(){
if(!mining()) return;
float focusLen = hitSize / 2f + Mathf.absin(Time.time, 1.1f, 0.5f);
float swingScl = 12f, swingMag = tilesize / 8f;
float flashScl = 0.3f;
float px = x + Angles.trnsx(rotation, focusLen);
float py = y + Angles.trnsy(rotation, focusLen);
float ex = mineTile.worldx() + Mathf.sin(Time.time + 48, swingScl, swingMag);
float ey = mineTile.worldy() + Mathf.sin(Time.time + 48, swingScl + 2f, swingMag);
Draw.z(Layer.flyingUnit + 0.1f);
Draw.color(Color.lightGray, Color.white, 1f - flashScl + Mathf.absin(Time.time, 0.5f, flashScl));
Drawf.laser(Core.atlas.find("minelaser"), Core.atlas.find("minelaser-end"), px, py, ex, ey, 0.75f);
if(isLocal()){
Lines.stroke(1f, Pal.accent);
Lines.poly(mineTile.worldx(), mineTile.worldy(), 4, tilesize / 2f * Mathf.sqrt2, Time.time);
}
Draw.color();
}
}

View File

@@ -33,7 +33,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
@Import float x, y;
@ReadOnly Unit unit = Nulls.unit;
@ReadOnly @Nullable Unit unit;
transient @Nullable NetConnection con;
@ReadOnly Team team = Team.sharded;
@SyncLocal boolean typing, shooting, boosting;
@@ -47,13 +47,14 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
transient float deathTimer;
transient String lastText = "";
transient float textFadeTime;
transient Ratekeeper itemDepositRate = new Ratekeeper();
transient private Unit lastReadUnit = Nulls.unit;
transient private @Nullable Unit lastReadUnit;
transient private int wrongReadUnits;
transient @Nullable Unit justSwitchFrom, justSwitchTo;
public boolean isBuilder(){
return unit.canBuild();
return unit != null && unit.canBuild();
}
public @Nullable CoreBuild closestCore(){
@@ -72,7 +73,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
public TextureRegion icon(){
//display default icon for dead players
if(dead()) return core() == null ? UnitTypes.alpha.fullIcon : ((CoreBlock)bestCore().block).unitType.fullIcon;
if(dead()) return core() == null ? UnitTypes.alpha.uiIcon : ((CoreBlock)bestCore().block).unitType.uiIcon;
return unit.icon();
}
@@ -88,7 +89,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
x = y = 0f;
if(!dead()){
unit.resetController();
unit = Nulls.unit;
unit = null;
}
}
@@ -104,7 +105,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
@Replace
public float clipSize(){
return unit.isNull() ? 20 : unit.type.hitSize * 2f;
return unit == null ? 20 : unit.type.hitSize * 2f;
}
@Override
@@ -130,17 +131,18 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
unit = lastReadUnit;
unit(set);
lastReadUnit = unit;
unit.aim(mouseX, mouseY);
//this is only necessary when the thing being controlled isn't synced
unit.controlWeapons(shooting, shooting);
//extra precaution, necessary for non-synced things
unit.controller(this);
if(unit != null){
unit.aim(mouseX, mouseY);
//this is only necessary when the thing being controlled isn't synced
unit.controlWeapons(shooting, shooting);
//extra precaution, necessary for non-synced things
unit.controller(this);
}
}
@Override
public void update(){
if(!unit.isValid()){
if(unit != null && !unit.isValid()){
clearUnit();
}
@@ -180,42 +182,43 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
@Override
public void remove(){
//clear unit upon removal
if(!unit.isNull()){
if(unit != null){
clearUnit();
}
lastReadUnit = Nulls.unit;
lastReadUnit = null;
justSwitchTo = justSwitchFrom = null;
}
public void team(Team team){
this.team = team;
unit.team(team);
if(unit != null){
unit.team(team);
}
}
public void clearUnit(){
unit(Nulls.unit);
unit(null);
}
public Unit unit(){
public @Nullable Unit unit(){
return unit;
}
public void unit(Unit unit){
public void unit(@Nullable Unit unit){
//refuse to switch when the unit was just transitioned from
if(isLocal() && unit == justSwitchFrom && justSwitchFrom != null && justSwitchTo != null){
return;
}
if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead.");
if(this.unit == unit) return;
//save last command this unit had
if(unit.controller() instanceof CommandAI ai){
if(unit != null && unit.controller() instanceof CommandAI ai){
lastCommand = ai.command;
}
if(this.unit != Nulls.unit){
if(this.unit != null){
//un-control the old unit
this.unit.resetController();
//restore last command issued before it was controlled
@@ -224,7 +227,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
}
}
this.unit = unit;
if(unit != Nulls.unit){
if(unit != null){
unit.team(team);
unit.controller(this);
@@ -243,7 +246,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
}
boolean dead(){
return unit.isNull() || !unit.isValid();
return unit == null || !unit.isValid();
}
String ip(){
@@ -276,10 +279,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra
@Override
public void draw(){
if(unit != null && unit.inFogTo(Vars.player.team())) return;
//??????
if(name == null) return;
if(unit == null || name == null || unit.inFogTo(Vars.player.team())) return;
Draw.z(Layer.playerName);
float z = Drawf.text();

View File

@@ -61,7 +61,7 @@ abstract class ShieldComp implements Healthc, Posc{
}
if(hadShields && shield <= 0.0001f){
Fx.unitShieldBreak.at(x, y, 0, team.color, this);
Fx.unitShieldBreak.at(x, y, 0, type.shieldColor(self()), this);
}
}
}

View File

@@ -73,6 +73,7 @@ abstract class StatusComp implements Posc, Flyingc{
}
void clearStatuses(){
statuses.each(e -> e.effect.onRemoved(self()));
statuses.clear();
}
@@ -80,6 +81,7 @@ abstract class StatusComp implements Posc, Flyingc{
void unapply(StatusEffect effect){
statuses.remove(e -> {
if(e.effect == effect){
e.effect.onRemoved(self());
Pools.free(e);
return true;
}
@@ -189,6 +191,10 @@ abstract class StatusComp implements Posc, Flyingc{
entry.time = Math.max(entry.time - Time.delta, 0);
if(entry.effect == null || (entry.time <= 0 && !entry.effect.permanent)){
if(entry.effect != null){
entry.effect.onRemoved(self());
}
Pools.free(entry);
index --;
statuses.remove(index);

View File

@@ -18,7 +18,7 @@ import static mindustry.Vars.*;
@Component
abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec{
@Import float x, y, hitSize, rotation, speedMultiplier;
@Import boolean hovering;
@Import boolean hovering, disarmed;
@Import UnitType type;
@Import Team team;
@@ -51,7 +51,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
}
//calculate overlapping tiles so it slows down when going "over" walls
int r = Math.max(Math.round(hitSize * 0.6f / tilesize), 1);
int r = Math.max((int)(hitSize * 0.6f / tilesize), 0);
int solids = 0, total = (r*2+1)*(r*2+1);
for(int dx = -r; dx <= r; dx++){
@@ -62,7 +62,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
}
//TODO should this apply to the player team(s)? currently PvE due to balancing
if(type.crushDamage > 0 && (walked || deltaLen() >= 0.01f) && t != null && t.build != null && t.build.team != team
if(type.crushDamage > 0 && !disarmed && (walked || deltaLen() >= 0.01f) && t != null && t.build != null && t.build.team != team
//damage radius is 1 tile smaller to prevent it from just touching walls as it passes
&& Math.max(Math.abs(dx), Math.abs(dy)) <= r - 1){

View File

@@ -214,6 +214,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
case ammoCapacity -> type.ammoCapacity;
case x -> World.conv(x);
case y -> World.conv(y);
case velocityX -> vel.x * 60f / tilesize;
case velocityY -> vel.y * 60f / tilesize;
case dead -> dead || !isAdded() ? 1 : 0;
case team -> team.id;
case shooting -> isShooting() ? 1 : 0;
@@ -282,6 +284,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
y = World.unconv((float)value);
if(!isLocal()) snapInterpolation();
}
case velocityX -> vel.x = (float)(value * tilesize / 60d);
case velocityY -> vel.y = (float)(value * tilesize / 60d);
case rotation -> rotation = (float)value;
case team -> {
if(!net.client()){
@@ -402,7 +406,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
}
/** @return pathfinder path type for calculating costs */
/** @return pathfinder path type for calculating costs. This is used for wave AI only. (TODO: remove) */
public int pathType(){
return Pathfinder.costGround;
}
@@ -666,9 +670,9 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
}
}
/** @return a preview icon for this unit. */
/** @return a preview UI icon for this unit. */
public TextureRegion icon(){
return type.fullIcon;
return type.uiIcon;
}
/** Actually destroys the unit, removing it and creating explosions. **/
@@ -788,6 +792,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
@Override
@Replace
public String toString(){
return "Unit#" + id() + ":" + type;
return "Unit#" + id() + ":" + type + " (" + x + ", " + y + ")";
}
}

View File

@@ -79,7 +79,7 @@ public class ParticleEffect extends Effect{
float x = rv.x, y = rv.y;
Lines.lineAngle(ox + x, oy + y, Mathf.angle(x, y), len, cap);
Drawf.light(ox + x, oy + y, len * lightScl, lightColor, lightOpacity * Draw.getColor().a);
Drawf.light(ox + x, oy + y, len * lightScl, lightColor, lightOpacity * Draw.getColorAlpha());
}
}else{
rand.setSeed(e.id);
@@ -88,8 +88,8 @@ public class ParticleEffect extends Effect{
rv.trns(realRotation + rand.range(cone), !randLength ? l : rand.random(l));
float x = rv.x, y = rv.y;
Draw.rect(tex, ox + x, oy + y, rad, rad, realRotation + offset + e.time * spin);
Drawf.light(ox + x, oy + y, rad * lightScl, lightColor, lightOpacity * Draw.getColor().a);
Draw.rect(tex, ox + x, oy + y, rad, rad / tex.ratio(), realRotation + offset + e.time * spin);
Drawf.light(ox + x, oy + y, rad * lightScl, lightColor, lightOpacity * Draw.getColorAlpha());
}
}
}

View File

@@ -15,7 +15,7 @@ public class RegionPart extends DrawPart{
public String suffix = "";
/** Overrides suffix if set. */
public @Nullable String name;
public TextureRegion heat;
public TextureRegion heat, light;
public TextureRegion[] regions = {};
public TextureRegion[] outlines = {};
@@ -133,7 +133,7 @@ public class RegionPart extends DrawPart{
float hprog = heatProgress.getClamp(params);
heatColor.write(Tmp.c1).a(hprog * heatColor.a);
Drawf.additive(heat, Tmp.c1, rx, ry, rot, turretShading ? turretHeatLayer : Draw.z() + heatLayerOffset);
if(heatLight) Drawf.light(rx, ry, heat, rot, Tmp.c1, heatLightOpacity * hprog);
if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog);
}
Draw.xscl *= sign;
@@ -187,6 +187,7 @@ public class RegionPart extends DrawPart{
}
heat = Core.atlas.find(realName + "-heat");
light = Core.atlas.find(realName + "-light");
for(var child : children){
child.turretShading = turretShading;
child.load(name);

View File

@@ -83,7 +83,7 @@ public class AIController implements UnitController{
public void updateVisuals(){
if(unit.isFlying()){
unit.wobble();
if(unit.type.wobble) unit.wobble();
unit.lookAt(unit.prefRotation());
}
@@ -341,7 +341,7 @@ public class AIController implements UnitController{
vec.setLength(speed * length);
}
//do not move when infinite vectors are used or if its zero.
//ignore invalid movement values
if(vec.isNaN() || vec.isInfinite() || vec.isZero()) return;
if(!unit.type.omniMovement && unit.type.rotateMoveFirst){

View File

@@ -13,6 +13,7 @@ public enum Gamemode{
}, map -> map.spawns > 0),
sandbox(rules -> {
rules.infiniteResources = true;
rules.allowEditRules = true;
rules.waves = true;
rules.waveTimer = false;
}),

View File

@@ -19,6 +19,8 @@ import mindustry.world.blocks.*;
* Does not store game state, just configuration.
*/
public class Rules{
/** Allows editing the rules in-game. Essentially a cheat mode toggle. */
public boolean allowEditRules = false;
/** Sandbox mode: Enables infinite resources, build range and build speed. */
public boolean infiniteResources;
/** Team-specific rules. */
@@ -29,6 +31,8 @@ public class Rules{
public boolean waveSending = true;
/** Whether waves are spawnable at all. */
public boolean waves;
/** Whether air units spawn at spawns instead of the edge of the map */
public boolean airUseSpawns = false;
/** Whether the game objective is PvP. Note that this enables automatic hosting. */
public boolean pvp;
/** Whether is waiting for players enabled in PvP. */
@@ -103,6 +107,8 @@ public class Rules{
public boolean cleanupDeadTeams = true;
/** If true, items can only be deposited in the core. */
public boolean onlyDepositCore = false;
/** Cooldown, in seconds, of item depositing for players. */
public float itemDepositCooldown = 0.5f;
/** If true, every enemy block in the radius of the (enemy) core is destroyed upon death. Used for campaign maps. */
public boolean coreDestroyClear = false;
/** If true, banned blocks are hidden from the build menu. */
@@ -201,6 +207,8 @@ public class Rules{
public @Nullable PlanetParams planetBackground;
/** Rules from this planet are applied. If it's {@code sun}, mixed tech is enabled. */
public Planet planet = Planets.serpulo;
/** If the `data` instruction is allowed for world processors */
public boolean allowLogicData = false;
/** Copies this ruleset exactly. Not efficient at all, do not use often. */
public Rules copy(){

View File

@@ -480,7 +480,7 @@ public class Drawf{
/** Draws a sprite that should be light-wise correct, when rotated. Provided sprite must be symmetrical in shape. */
public static void spinSprite(TextureRegion region, float x, float y, float r){
float a = Draw.getColor().a;
float a = Draw.getColorAlpha();
r = Mathf.mod(r, 90f);
Draw.rect(region, x, y, r);
Draw.alpha(r / 90f*a);

View File

@@ -70,7 +70,7 @@ public class LightRenderer{
float rot = Mathf.angleExact(x2 - x, y2 - y);
TextureRegion ledge = Core.atlas.find("circle-end"), lmid = Core.atlas.find("circle-mid");
float color = Draw.getColor().toFloatBits();
float color = Draw.getColorPacked();
float u = lmid.u;
float v = lmid.v2;
float u2 = lmid.u2;

View File

@@ -129,9 +129,11 @@ public class OverlayRenderer{
Draw.mixcol(Pal.accent, 1f);
Draw.alpha(unitFade);
Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null);
TextureRegion region = build != null ? build.block.fullIcon : select instanceof Unit u ? u.icon() : Core.atlas.white();
TextureRegion region = build != null ? build.block.fullIcon : Core.atlas.white();
Draw.rect(region, select.getX(), select.getY(), select instanceof Unit u && !(select instanceof BlockUnitc) ? u.rotation - 90f : 0f);
if(!(select instanceof Unitc)){
Draw.rect(region, select.getX(), select.getY());
}
for(int i = 0; i < 4; i++){
float rot = i * 90f + 45f + (-Time.time) % 360f;
@@ -239,7 +241,9 @@ public class OverlayRenderer{
Draw.reset();
Building build = world.buildWorld(v.x, v.y);
if(input.canDropItem() && build != null && build.interactable(player.team()) && build.acceptStack(player.unit().item(), player.unit().stack.amount, player.unit()) > 0 && player.within(build, itemTransferRange)){
if(input.canDropItem() && build != null && build.interactable(player.team()) && build.acceptStack(player.unit().item(), player.unit().stack.amount, player.unit()) > 0 && player.within(build, itemTransferRange) &&
input.itemDepositCooldown <= 0f){
boolean invalid = (state.rules.onlyDepositCore && !(build instanceof CoreBuild));
Lines.stroke(3f, Pal.gray);
@@ -255,6 +259,13 @@ public class OverlayRenderer{
}
}
public void checkApplySelection(Unit u){
if(unitFade > 0.001f && lastSelect == u){
Color prev = Draw.getMixColor();
Draw.mixcol(prev.a > 0.001f ? prev.lerp(Pal.accent, unitFade) : Pal.accent, Math.max(unitFade, prev.a));
}
}
private static class CoreEdge{
float x1, y1, x2, y2;
Team t1, t2;

View File

@@ -106,9 +106,9 @@ public class Trail{
int count = (int)(counter += Time.delta);
counter -= count;
if(points.size + ((count - 1) * 3) > length * 3 && points.size > 0){
points.removeRange(0, Math.min(3 * count - 1, points.size - 1));
}
if(count > 0 && points.size > 0){
points.removeRange(0, Math.min(count * 3 - 1, points.size - 1));
}
}
/** Adds a new point to the trail at intervals. */

View File

@@ -57,7 +57,7 @@ public class DesktopInput extends InputHandler{
public long lastCtrlGroupSelectMillis;
boolean showHint(){
return ui.hudfrag.shown && Core.settings.getBool("hints") && selectPlans.isEmpty() &&
return ui.hudfrag.shown && Core.settings.getBool("hints") && selectPlans.isEmpty() && !player.dead() &&
(!isBuilding && !Core.settings.getBool("buildautopause") || player.unit().isBuilding() || !player.dead() && !player.unit().spawnedByCore());
}
@@ -108,18 +108,22 @@ public class DesktopInput extends InputHandler{
@Override
public void drawTop(){
if(cursorType != SystemCursor.arrow && scene.hasMouse()){
graphics.cursor(cursorType = SystemCursor.arrow);
}
Lines.stroke(1f);
int cursorX = tileX(Core.input.mouseX());
int cursorY = tileY(Core.input.mouseY());
//draw break selection
if(mode == breaking){
drawBreakSelection(selectX, selectY, cursorX, cursorY, !Core.input.keyDown(Binding.schematic_select) ? maxLength : Vars.maxSchematicSize, false);
drawBreakSelection(selectX, selectY, cursorX, cursorY, !(Core.input.keyDown(Binding.schematic_select) && schemX != -1 && schemY != -1) ? maxLength : Vars.maxSchematicSize, false);
}
if(!Core.scene.hasKeyboard() && mode != breaking){
if(Core.input.keyDown(Binding.schematic_select)){
if(Core.input.keyDown(Binding.schematic_select) && schemX != -1 && schemY != -1){
drawSelection(schemX, schemY, cursorX, cursorY, Vars.maxSchematicSize);
}else if(Core.input.keyDown(Binding.rebuild_select)){
drawRebuildSelection(schemX, schemY, cursorX, cursorY);
@@ -446,6 +450,8 @@ public class DesktopInput extends InputHandler{
Tile cursor = tileAt(Core.input.mouseX(), Core.input.mouseY());
cursorType = SystemCursor.arrow;
if(cursor != null){
if(cursor.build != null && cursor.build.interactable(player.team())){
cursorType = cursor.build.getCursor();
@@ -498,9 +504,9 @@ public class DesktopInput extends InputHandler{
if(!Core.scene.hasMouse()){
Core.graphics.cursor(cursorType);
}else{
cursorType = SystemCursor.arrow;
}
cursorType = SystemCursor.arrow;
}
@Override
@@ -572,7 +578,7 @@ public class DesktopInput extends InputHandler{
player.unit().mineTile = null;
}
if(Core.input.keyTap(Binding.clear_building)){
if(Core.input.keyTap(Binding.clear_building) && !player.dead()){
player.unit().clearBuilding();
}
@@ -594,7 +600,7 @@ public class DesktopInput extends InputHandler{
selectPlans.clear();
}
if( !Core.scene.hasKeyboard() && selectX == -1 && selectY == -1 && schemX != -1 && schemY != -1){
if(!Core.scene.hasKeyboard() && selectX == -1 && selectY == -1 && schemX != -1 && schemY != -1){
if(Core.input.keyRelease(Binding.schematic_select)){
lastSchematic = schematics.create(schemX, schemY, rawCursorX, rawCursorY);
useSchematic(lastSchematic);
@@ -668,7 +674,7 @@ public class DesktopInput extends InputHandler{
lastLineY = cursorY;
mode = placing;
updateLine(selectX, selectY);
}else if(plan != null && !plan.breaking && mode == none && !plan.initialized){
}else if(plan != null && !plan.breaking && mode == none && !plan.initialized && plan.progress <= 0f){
splan = plan;
}else if(plan != null && plan.breaking){
deleting = true;

View File

@@ -83,6 +83,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
public boolean overrideLineRotation;
public int rotation;
public boolean droppingItem;
public float itemDepositCooldown;
public Group uiGroup;
public boolean isBuilding = true, buildWasAutoPaused = false, wasShooting = false;
public @Nullable UnitType controlledType;
@@ -115,7 +116,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
private WidgetGroup group = new WidgetGroup();
private final Eachable<BuildPlan> allPlans = cons -> {
player.unit().plans().each(cons);
if(!player.dead()){
player.unit().plans().each(cons);
}
selectPlans.each(cons);
linePlans.each(cons);
};
@@ -142,6 +145,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
Events.on(ResetEvent.class, e -> {
logicCutscene = false;
itemDepositCooldown = 0f;
Arrays.fill(controlGroups, null);
});
}
@@ -234,9 +238,6 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
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
if(unitTarget != null && unitTarget.isNull()) unitTarget = null;
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
event.unitIDs = unitIds;
})){
@@ -258,7 +259,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
if(teamTarget != null && teamTarget.team() != player.team() &&
!(teamTarget instanceof Unit u && !unit.canTarget(u)) && !(teamTarget instanceof Building && !unit.type.targetGround)){
!(teamTarget instanceof Unit u && !unit.canTarget(u)) && !(teamTarget instanceof Building && !unit.type.targetGround)){
anyCommandedTarget = true;
if(queueCommand){
@@ -280,7 +281,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(ai.commandQueue.size <= 0){
ai.group = null;
}
//remove when other player command
if(!headless && player != Vars.player){
control.input.selectedUnits.remove(unit);
@@ -404,7 +405,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
@Remote(called = Loc.server, targets = Loc.both, forward = true)
public static void requestItem(Player player, Building build, Item item, int amount){
if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, itemTransferRange) || player.dead()) return;
if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, itemTransferRange) || player.dead() || amount <= 0) return;
if(net.server() && (!Units.canInteract(player, build) ||
!netServer.admins.allowAction(player, ActionType.withdrawItem, build.tile(), action -> {
@@ -423,6 +424,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(player == null || build == null || !player.within(build, itemTransferRange) || build.items == null || player.dead() || (state.rules.onlyDepositCore && !(build instanceof CoreBuild))) return;
if(net.server() && (player.unit().stack.amount <= 0 || !Units.canInteract(player, build) ||
//to avoid rejecting deposit packets that happen to overlap due to packet speed differences, the actual cap is double the cooldown with 2 deposits.
(!player.isLocal() && !player.itemDepositRate.allow((long)(state.rules.itemDepositCooldown * 1000 * 2), 2)) ||
!netServer.admins.allowAction(player, ActionType.depositItem, build.tile, action -> {
action.itemAmount = player.unit().stack.amount;
action.item = player.unit().item();
@@ -592,7 +596,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(build == null) return;
if(net.server() && (!Units.canInteract(player, build) ||
!netServer.admins.allowAction(player, ActionType.rotate, build.tile(), action -> action.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4)))){
!netServer.admins.allowAction(player, ActionType.rotate, build.tile(), action -> action.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4)))){
throw new ValidateException(player, "Player cannot rotate a block.");
}
@@ -611,7 +615,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(build == null) return;
if(net.server() && (!Units.canInteract(player, build) ||
!netServer.admins.allowAction(player, ActionType.configure, build.tile, action -> action.config = value))){
!netServer.admins.allowAction(player, ActionType.configure, build.tile, action -> action.config = value))){
if(player.con != null){
var packet = new TileConfigCallPacket(); //undo the config on the client
@@ -690,7 +694,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
player.unit(unit);
if(before != null && !before.isNull()){
if(before != null){
if(before.spawnedByCore){
unit.dockedType = before.type;
}else if(before.dockedType != null && before.dockedType.coreUnitDock){
@@ -796,6 +800,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
logicCutsceneZoom = -1f;
}
itemDepositCooldown -= Time.delta / 60f;
commandBuildings.removeAll(b -> !b.isValid());
if(!commandMode){
@@ -803,7 +809,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
}
playerPlanTree.clear();
player.unit().plans.each(playerPlanTree::insert);
if(!player.dead()){
player.unit().plans.each(playerPlanTree::insert);
}
player.typing = ui.chatfrag.shown();
@@ -815,7 +823,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
player.unit().updateBuilding(isBuilding);
}
if(player.shooting && !wasShooting && player.unit().hasWeapons() && state.rules.unitAmmo && !player.team().rules().infiniteAmmo && player.unit().ammo <= 0){
if(!player.dead() && player.shooting && !wasShooting && player.unit().hasWeapons() && state.rules.unitAmmo && !player.team().rules().infiniteAmmo && player.unit().ammo <= 0){
player.unit().type.weapons.first().noAmmoSound.at(player.unit());
}
@@ -1664,9 +1672,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
boolean canMine(Tile tile){
return !Core.scene.hasMouse()
&& player.unit().validMine(tile)
&& player.unit().acceptsItem(player.unit().getMineResult(tile))
&& !((!Core.settings.getBool("doubletapmine") && tile.floor().playerUnmineable) && tile.overlay().itemDrop == null);
&& player.unit().validMine(tile)
&& player.unit().acceptsItem(player.unit().getMineResult(tile))
&& !((!Core.settings.getBool("doubletapmine") && tile.floor().playerUnmineable) && tile.overlay().itemDrop == null);
}
/** Returns the tile at the specified MOUSE coordinates. */
@@ -1832,7 +1840,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
public boolean canShoot(){
return block == null && !onConfigurable() && !isDroppingItem() && !player.unit().activelyBuilding() &&
!(player.unit() instanceof Mechc && player.unit().isFlying()) && !player.unit().mining() && !commandMode;
!(player.unit() instanceof Mechc && player.unit().isFlying()) && !player.unit().mining() && !commandMode;
}
public boolean onConfigurable(){
@@ -1858,9 +1866,11 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
ItemStack stack = player.unit().stack;
if(build != null && build.acceptStack(stack.item, stack.amount, player.unit()) > 0 && build.interactable(player.team()) &&
build.block.hasItems && player.unit().stack().amount > 0 && build.interactable(player.team())){
if(!(state.rules.onlyDepositCore && !(build instanceof CoreBuild))){
build.block.hasItems && player.unit().stack().amount > 0 && build.interactable(player.team())){
if(!(state.rules.onlyDepositCore && !(build instanceof CoreBuild)) && itemDepositCooldown <= 0f){
Call.transferInventory(player, build);
itemDepositCooldown = state.rules.itemDepositCooldown;
}
}else{
Call.dropItem(player.angleTo(x, y));

View File

@@ -90,7 +90,7 @@ public class MobileInput extends InputHandler implements GestureListener{
void checkTargets(float x, float y){
Unit unit = Units.closestEnemy(player.team(), x, y, 20f, u -> !u.dead);
if(unit != null && player.unit().type.canAttack){
if(unit != null && !player.dead() && player.unit().type.canAttack){
player.unit().mineTile = null;
target = unit;
}else{
@@ -126,18 +126,21 @@ public class MobileInput extends InputHandler implements GestureListener{
}
}
for(var plan : player.unit().plans()){
Tile other = world.tile(plan.x, plan.y);
if(!player.dead()){
for(var plan : player.unit().plans()){
Tile other = world.tile(plan.x, plan.y);
if(other == null || plan.breaking) continue;
if(other == null || plan.breaking) continue;
r1.setSize(plan.block.size * tilesize);
r1.setCenter(other.worldx() + plan.block.offset, other.worldy() + plan.block.offset);
r1.setSize(plan.block.size * tilesize);
r1.setCenter(other.worldx() + plan.block.offset, other.worldy() + plan.block.offset);
if(r2.overlaps(r1)){
return true;
if(r2.overlaps(r1)){
return true;
}
}
}
return false;
}
@@ -263,7 +266,7 @@ public class MobileInput extends InputHandler implements GestureListener{
}
boolean showCancel(){
return (player.unit().isBuilding() || block != null || mode == breaking || !selectPlans.isEmpty()) && !hasSchem();
return !player.dead() && (player.unit().isBuilding() || block != null || mode == breaking || !selectPlans.isEmpty()) && !hasSchem();
}
boolean hasSchem(){
@@ -277,7 +280,9 @@ public class MobileInput extends InputHandler implements GestureListener{
t.visible(this::showCancel);
t.bottom().left();
t.button("@cancel", Icon.cancel, () -> {
player.unit().clearBuilding();
if(!player.dead()){
player.unit().clearBuilding();
}
selectPlans.clear();
mode = none;
block = null;
@@ -864,7 +869,7 @@ public class MobileInput extends InputHandler implements GestureListener{
}
}
if(player.shooting && (player.unit().activelyBuilding() || player.unit().mining())){
if(player.shooting && !player.dead() && (player.unit().activelyBuilding() || player.unit().mining())){
player.shooting = false;
}
}
@@ -1037,7 +1042,7 @@ public class MobileInput extends InputHandler implements GestureListener{
unit.movePref(movement);
//update shooting if not building + not mining
if(!player.unit().activelyBuilding() && player.unit().mineTile == null){
if(!unit.activelyBuilding() && unit.mineTile == null){
//autofire targeting
if(manualShooting){
@@ -1046,7 +1051,7 @@ public class MobileInput extends InputHandler implements GestureListener{
}else if(target == null){
player.shooting = false;
if(Core.settings.getBool("autotarget") && !(player.unit() instanceof BlockUnitUnit u && u.tile() instanceof ControlBlock c && !c.shouldAutoTarget())){
if(player.unit().type.canAttack){
if(unit.type.canAttack){
target = Units.closestTarget(unit.team, unit.x, unit.y, range, u -> u.checkTarget(type.targetAir, type.targetGround), u -> type.targetGround);
}

View File

@@ -279,7 +279,7 @@ public class TypeIO{
}
public static void writeUnit(Writes write, Unit unit){
write.b(unit == null || unit.isNull() ? 0 : unit instanceof BlockUnitc ? 1 : 2);
write.b(unit == null ? 0 : unit instanceof BlockUnitc ? 1 : 2);
//block units are special
if(unit instanceof BlockUnitc){
@@ -295,15 +295,14 @@ public class TypeIO{
byte type = read.b();
int id = read.i();
//nothing
if(type == 0) return Nulls.unit;
if(type == 0) return null;
if(type == 2){ //standard unit
Unit unit = Groups.unit.getByID(id);
return unit == null ? Nulls.unit : unit;
return Groups.unit.getByID(id);
}else if(type == 1){ //block
Building tile = world.build(id);
return tile instanceof ControlBlock cont ? cont.unit() : Nulls.unit;
return tile instanceof ControlBlock cont ? cont.unit() : null;
}
return Nulls.unit;
return null;
}
public static void writeCommand(Writes write, @Nullable UnitCommand command){
@@ -628,7 +627,7 @@ public class TypeIO{
}
public static KickReason readKick(Reads read){
return KickReason.values()[read.b()];
return KickReason.all[read.b()];
}
public static void writeMarkerControl(Writes write, LMarkerControl reason){
@@ -786,7 +785,7 @@ public class TypeIO{
}
public static AdminAction readAction(Reads read){
return AdminAction.values()[read.b()];
return AdminAction.all[read.b()];
}
public static void writeUnitType(Writes write, UnitType effect){
@@ -1038,14 +1037,19 @@ public class TypeIO{
}
}
public interface Boxed<T> {
T unbox();
}
/** Represents a building that has not been resolved yet. */
public static class BuildingBox{
public static class BuildingBox implements Boxed<Building>{
public int pos;
public BuildingBox(int pos){
this.pos = pos;
}
@Override
public Building unbox(){
return world.build(pos);
}
@@ -1059,13 +1063,14 @@ public class TypeIO{
}
/** Represents a unit that has not been resolved yet. TODO unimplemented / unused*/
public static class UnitBox{
public static class UnitBox implements Boxed<Unit>{
public int id;
public UnitBox(int id){
this.id = id;
}
@Override
public Unit unbox(){
return Groups.unit.getByID(id);
}

View File

@@ -1,16 +1,16 @@
package mindustry.logic;
import arc.*;
import arc.audio.*;
import arc.files.*;
import arc.graphics.*;
import arc.math.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.game.*;
import mindustry.logic.LExecutor.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.legacy.*;
@@ -27,15 +27,16 @@ 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, varMapW, varMapH, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
private static LVar varTime, varTick, varSecond, varMinute, varWave, varWaveTime, varMapW, varMapH, varWait, varServer, varClient, varClientLocale, varClientUnit, varClientName, varClientTeam, varClientMobile;
private ObjectIntMap<String> namesToIds = new ObjectIntMap<>();
private Seq<Var> vars = new Seq<>(Var.class);
private ObjectMap<String, LVar> vars = new ObjectMap<>();
private Seq<VarEntry> varEntries = new Seq<>();
private IntSet privilegedIds = new IntSet();
private ObjectSet<String> privilegedNames = new ObjectSet<>();
private UnlockableContent[][] logicIdToContent;
private int[][] contentIdToLogicId;
public static final Seq<String> soundNames = new Seq<>();
public void init(){
putEntryOnly("sectionProcessor");
@@ -72,6 +73,7 @@ public class GlobalVars{
varMapW = putEntry("@mapw", 0);
varMapH = putEntry("@maph", 0);
varWait = putEntry("@wait", null);
putEntryOnly("sectionNetwork");
@@ -89,6 +91,17 @@ public class GlobalVars{
put("@ctrlProcessor", ctrlProcessor);
put("@ctrlPlayer", ctrlPlayer);
put("@ctrlCommand", ctrlCommand);
//sounds
if(Core.assets != null){
for(Sound sound : Core.assets.getAll(Sound.class, new Seq<>(Sound.class))){
if(sound != Sounds.none && sound != Sounds.swish && sound.file != null){
String name = sound.file.nameWithoutExtension();
soundNames.add(name);
put("@sfx-" + name, Sounds.getSoundId(sound));
}
}
}
//store base content
@@ -118,9 +131,6 @@ public class GlobalVars{
put("@color" + Strings.capitalize(entry.key), entry.value.toDoubleBits());
}
//used as a special value for any environmental solid block
put("@solid", Blocks.stoneWall);
for(UnitType type : Vars.content.units()){
put("@" + type.name, type);
}
@@ -172,34 +182,38 @@ public class GlobalVars{
public void update(){
//set up time; note that @time is now only updated once every invocation and directly based off of @tick.
//having time be based off of user system time was a very bad idea.
vars.items[varTime].numval = state.tick / 60.0 * 1000.0;
vars.items[varTick].numval = state.tick;
varTime.numval = state.tick / 60.0 * 1000.0;
varTick.numval = state.tick;
//shorthands for seconds/minutes spent in save
vars.items[varSecond].numval = state.tick / 60f;
vars.items[varMinute].numval = state.tick / 60f / 60f;
varSecond.numval = state.tick / 60f;
varMinute.numval = state.tick / 60f / 60f;
//wave state
vars.items[varWave].numval = state.wave;
vars.items[varWaveTime].numval = state.wavetime / 60f;
varWave.numval = state.wave;
varWaveTime.numval = state.wavetime / 60f;
vars.items[varMapW].numval = world.width();
vars.items[varMapH].numval = world.height();
varMapW.numval = world.width();
varMapH.numval = world.height();
//network
vars.items[varServer].numval = (net.server() || !net.active()) ? 1 : 0;
vars.items[varClient].numval = net.client() ? 1 : 0;
varServer.numval = (net.server() || !net.active()) ? 1 : 0;
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;
varClientLocale.objval = player.locale();
varClientUnit.objval = player.unit();
varClientName.objval = player.name();
varClientTeam.numval = player.team().id;
varClientMobile.numval = mobile ? 1 : 0;
}
}
public LVar waitVar(){
return varWait;
}
public Seq<VarEntry> getEntries(){
return varEntries;
}
@@ -217,84 +231,81 @@ public class GlobalVars{
}
/**
* @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.
* @return a constant variable if there is a constant with this name, otherwise null.
* Attempt to get privileged variable from non-privileged logic executor returns null constant.
*/
public int get(String name){
return namesToIds.get(name, -1);
public LVar get(String name){
return vars.get(name);
}
/**
* @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
* @return a constant variable by name
* 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];
public LVar get(String name, boolean privileged){
if(!privileged && privilegedNames.contains(name)) return vars.get("null");
return vars.get(name);
}
/** Sets a global variable by an ID returned from put(). */
public void set(int id, double value){
get(id, true).numval = value;
/** Sets a global variable by name. */
public void set(String name, double value){
get(name, true).numval = value;
}
/** Adds a constant value by name. */
public int put(String name, Object value, boolean privileged){
public LVar 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)
public LVar put(String name, Object value, boolean privileged, boolean hidden){
LVar existingVar = vars.get(name);
if(existingVar != null){ //don't overwrite existing vars (see #6910)
Log.debug("Failed to add global logic variable '@', as it already exists.", name);
return existingIdx;
return existingVar;
}
Var var = new Var(name);
LVar var = new LVar(name);
var.constant = true;
if(value instanceof Number num){
var.isobj = false;
var.numval = num.doubleValue();
}else{
var.isobj = true;
var.objval = value;
}
int index = vars.size;
namesToIds.put(name, index);
if(privileged) privilegedIds.add(index);
vars.add(var);
vars.put(name, var);
if(privileged) privilegedNames.add(name);
if(!hidden){
varEntries.add(new VarEntry(index, name, "", "", privileged));
varEntries.add(new VarEntry(name, "", "", privileged));
}
return index;
return var;
}
public int put(String name, Object value){
public LVar put(String name, Object value){
return put(name, value, false);
}
public int putEntry(String name, Object value){
public LVar putEntry(String name, Object value){
return put(name, value, false, false);
}
public int putEntry(String name, Object value, boolean privileged){
public LVar 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));
varEntries.add(new VarEntry(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;
public VarEntry(String name, String description, String icon, boolean privileged){
this.name = name;
this.description = description;
this.icon = icon;

View File

@@ -5,12 +5,13 @@ 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.*;
import static mindustry.Vars.*;
public class GlobalVarsDialog extends BaseDialog{
public GlobalVarsDialog(){
@@ -28,8 +29,7 @@ public class GlobalVarsDialog extends BaseDialog{
cont.pane(t -> {
t.margin(10f).marginRight(16f);
t.defaults().fillX().fillY();
for(var entry : Vars.logicVars.getEntries()){
for(var entry : 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();

View File

@@ -17,6 +17,7 @@ public enum LAccess{
powerNetOut,
ammo,
ammoCapacity,
currentAmmoType,
health,
maxHealth,
heat,
@@ -28,6 +29,8 @@ public enum LAccess{
rotation,
x,
y,
velocityX,
velocityY,
shootX,
shootY,
cameraX,
@@ -35,6 +38,7 @@ public enum LAccess{
cameraWidth,
cameraHeight,
size,
solid,
dead,
range,
shooting,
@@ -67,7 +71,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, speed, armor, health, shield, team, flag, totalPower, payloadType};
settable = {x, y, velocityX, velocityY, rotation, speed, armor, health, shield, team, flag, totalPower, payloadType};
LAccess(String... params){
this.params = params;
@@ -78,5 +82,4 @@ public enum LAccess{
this.params = params;
isObj = obj;
}
}

View File

@@ -10,20 +10,18 @@ import mindustry.logic.LExecutor.*;
/** "Compiles" a sequence of statements into instructions. */
public class LAssembler{
public static ObjectMap<String, Func<String[], LStatement>> customParsers = new ObjectMap<>();
public static final int maxTokenLength = 36;
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<>();
/** Maps names to variable. */
public OrderedMap<String, LVar> vars = new OrderedMap<>();
/** All instructions to be executed. */
public LInstruction[] instructions;
public LAssembler(){
//instruction counter
putVar("@counter").value = 0;
putVar("@counter");
//currently controlled unit
putConst("@unit", null);
//reference to self
@@ -57,20 +55,17 @@ public class LAssembler{
return new LParser(text, privileged).parse();
}
/** @return a variable ID by name.
/** @return a variable by name.
* This may be a constant variable referring to a number or object. */
public int var(String symbol){
int constId = Vars.logicVars.get(symbol);
if(constId > 0){
//global constants are *negated* and stored separately
return -constId;
}
public LVar var(String symbol){
LVar constVar = Vars.logicVars.get(symbol, privileged);
if(constVar != null) return constVar;
symbol = symbol.trim();
//string case
if(!symbol.isEmpty() && symbol.charAt(0) == '\"' && symbol.charAt(symbol.length() - 1) == '\"'){
return putConst("___" + symbol, symbol.substring(1, symbol.length() - 1).replace("\\n", "\n")).id;
return putConst("___" + symbol, symbol.substring(1, symbol.length() - 1).replace("\\n", "\n"));
}
//remove spaces for non-strings
@@ -79,10 +74,10 @@ public class LAssembler{
double value = parseDouble(symbol);
if(value == invalidNum){
return putVar(symbol).id;
return putVar(symbol);
}else{
//this creates a hidden const variable with the specified value
return putConst("___" + value, value).id;
return putConst("___" + value, value);
}
}
@@ -106,48 +101,34 @@ public class LAssembler{
}
/** Adds a constant value by name. */
public BVar putConst(String name, Object value){
BVar var = putVar(name);
public LVar putConst(String name, Object value){
LVar var = putVar(name);
if(value instanceof Number number){
var.isobj = false;
var.numval = number.doubleValue();
var.objval = null;
}else{
var.isobj = true;
var.objval = value;
}
var.constant = true;
var.value = value;
return var;
}
/** Registers a variable name mapping. */
public BVar putVar(String name){
public LVar putVar(String name){
if(vars.containsKey(name)){
return vars.get(name);
}else{
BVar var = new BVar(lastVar++);
LVar var = new LVar(name);
vars.put(name, var);
return var;
}
}
@Nullable
public BVar getVar(String name){
public LVar getVar(String name){
return vars.get(name);
}
/** A variable "builder". */
public static class BVar{
public int id;
public boolean constant;
public Object value;
public BVar(int id){
this.id = id;
}
BVar(){}
@Override
public String toString(){
return "BVar{" +
"id=" + id +
", constant=" + constant +
", value=" + value +
'}';
}
}
}

View File

@@ -164,8 +164,14 @@ public class LCanvas extends Table{
this.statements.layout();
}
public void clearStatements(){
jumps.clear();
statements.clearChildren();
statements.layout();
}
StatementElem checkHovered(){
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e != null){
while(e != null && !(e instanceof StatementElem)){
e = e.parent;
@@ -251,7 +257,7 @@ public class LCanvas extends Table{
}
}
invalidateHierarchy();
if(parent != null) parent.invalidateHierarchy();//don't invalid self
if(parent != null && parent instanceof Table){
setCullingArea(parent.getCullingArea());

File diff suppressed because it is too large Load Diff

View File

@@ -103,7 +103,7 @@ public abstract class LStatement{
protected Cell<TextField> field(Table table, String value, Cons<String> setter){
return table.field(value, Styles.nodeField, s -> setter.get(sanitize(s)))
.size(144f, 40f).pad(2f).color(table.color).maxTextLength(LAssembler.maxTokenLength);
.size(144f, 40f).pad(2f).color(table.color);
}
protected Cell<TextField> fields(Table table, String desc, String value, Cons<String> setter){

View File

@@ -281,8 +281,8 @@ public class LStatements{
@Override
public LInstruction build(LAssembler builder){
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));
return new DrawI((byte)type.ordinal(), builder.var(x), builder.var(y),
type == GraphicsType.print ? new LVar(p1, nameToAlign.get(p1, Align.bottomLeft), true) : builder.var(p1), builder.var(p2), builder.var(p3), builder.var(p4));
}
@Override
@@ -1045,7 +1045,7 @@ public class LStatements{
@Override
public LInstruction build(LAssembler builder){
return new RadarI(target1, target2, target3, sort, LExecutor.varUnit, builder.var(sortOrder), builder.var(output));
return new RadarI(target1, target2, target3, sort, builder.var("@unit"), builder.var(sortOrder), builder.var(output));
}
@Override
@@ -1594,7 +1594,7 @@ public class LStatements{
@RegisterStatement("message")
public static class FlushMessageStatement extends LStatement{
public MessageType type = MessageType.announce;
public String duration = "3", outSuccess = "success";
public String duration = "3", outSuccess = "@wait";
@Override
public void build(Table table){
@@ -1616,9 +1616,11 @@ public class LStatements{
case announce, toast -> {
table.add(" for ");
fields(table, duration, str -> duration = str);
table.add(" secs ");
table.add(" sec ");
}
}
row(table);
table.add(" success ");
fields(table, outSuccess, str -> outSuccess = str);
}
@@ -1911,6 +1913,42 @@ public class LStatements{
}
}
@RegisterStatement("clientdata")
public static class ClientDataStatement extends LStatement{
public String channel = "\"frog\"", value = "\"bar\"", reliable = "0";
@Override
public void build(Table table){
table.add("send ");
fields(table, value, str -> value = str);
table.add(" on ");
fields(table, channel, str -> channel = str);
table.add(", reliable ");
fields(table, reliable, str -> reliable = str);
}
@Override
public boolean hidden(){
return true;
}
@Override
public boolean privileged(){
return true;
}
@Override
public LInstruction build(LAssembler builder){
if(!state.rules.allowLogicData) return null;
return new ClientDataI(builder.var(channel), builder.var(value), builder.var(reliable));
}
@Override
public LCategory category(){
return LCategory.world;
}
}
@RegisterStatement("getflag")
public static class GetFlagStatement extends LStatement{
public String result = "result", flag = "\"flag\"";
@@ -2085,6 +2123,74 @@ public class LStatements{
return LCategory.world;
}
}
@RegisterStatement("playsound")
public static class PlaySoundStatement extends LStatement{
public boolean positional;
public String id = "@sfx-pew", volume = "1", pitch = "1", pan = "0", x = "@thisx", y = "@thisy", limit = "true";
@Override
public void build(Table table){
rebuild(table);
}
void rebuild(Table table){
table.clearChildren();
table.button(positional ? "positional" : "global", Styles.logict, () -> {
positional = !positional;
rebuild(table);
}).size(160f, 40f).pad(4f).color(table.color);
row(table);
field(table, id, str -> id = str).padRight(0f).get();
table.button(b -> {
b.image(Icon.pencilSmall);
String soundName = id.startsWith("@sfx-") ? id.substring(5) : id;
b.clicked(() -> showSelect(b, GlobalVars.soundNames.toArray(String.class), soundName, t -> {
id = "@sfx-" + t;
rebuild(table);
}, 2, cell -> cell.size(160, 50)));
}, Styles.logict, () -> {}).size(40).color(table.color).left().padLeft(-1);
row(table);
fieldst(table, "volume", volume, str -> volume = str);
fieldst(table, "pitch", pitch, str -> pitch = str);
table.row();
if(positional){
fieldst(table, "x", x, str -> x = str);
fieldst(table, "y", y, str -> y = str);
}else{
fieldst(table, "pan", pan, str -> pan = str);
}
table.row();
fieldst(table, "limit", limit, str -> limit = str);
}
@Override
public boolean privileged(){
return true;
}
@Override
public LInstruction build(LAssembler builder){
return new PlaySoundI(positional, builder.var(id), builder.var(volume), builder.var(pitch), builder.var(pan), builder.var(x), builder.var(y), builder.var(limit));
}
@Override
public LCategory category(){
return LCategory.world;
}
}
@RegisterStatement("setmarker")
public static class SetMarkerStatement extends LStatement{

View File

@@ -0,0 +1,108 @@
package mindustry.logic;
import arc.util.*;
import mindustry.game.*;
import mindustry.gen.*;
public class LVar{
public final String name;
public int id;
public boolean isobj, constant;
public Object objval;
public double numval;
//ms timestamp for when this was last synced; used in the sync instruction
public long syncTime;
public LVar(String name){
this(name, -1);
}
public LVar(String name, int id){
this(name, id, false);
}
public LVar(String name, int id, boolean constant){
this.name = name;
this.id = id;
this.constant = constant;
}
public @Nullable Building building(){
return isobj && objval instanceof Building building ? building : null;
}
public @Nullable Object obj(){
return isobj ? objval : null;
}
public @Nullable Team team(){
if(isobj){
return objval instanceof Team t ? t : null;
}else{
int t = (int)numval;
if(t < 0 || t >= Team.all.length) return null;
return Team.all[t];
}
}
public boolean bool(){
return isobj ? objval != null : Math.abs(numval) >= 0.00001;
}
public double num(){
return isobj ? objval != null ? 1 : 0 : invalid(numval) ? 0 : numval;
}
/** Get num value from variable, convert null to NaN to handle it differently in some instructions */
public double numOrNan(){
return isobj ? objval != null ? 1 : Double.NaN : invalid(numval) ? 0 : numval;
}
public float numf(){
return isobj ? objval != null ? 1 : 0 : invalid(numval) ? 0 : (float)numval;
}
/** Get float value from variable, convert null to NaN to handle it differently in some instructions */
public float numfOrNan(){
return isobj ? objval != null ? 1 : Float.NaN : invalid(numval) ? 0 : (float)numval;
}
public int numi(){
return (int)num();
}
public void setbool(boolean value){
setnum(value ? 1 : 0);
}
public void setnum(double value){
if(constant) return;
if(invalid(value)){
objval = null;
isobj = true;
}else{
numval = value;
objval = null;
isobj = false;
}
}
public void setobj(Object value){
if(constant) return;
objval = value;
isobj = true;
}
public void setconst(Object value){
objval = value;
isobj = true;
}
public static boolean invalid(double d){
return Double.isNaN(d) || Double.isInfinite(d);
}
}

View File

@@ -17,6 +17,7 @@ import mindustry.logic.LExecutor.*;
import mindustry.logic.LStatements.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.blocks.logic.*;
import static mindustry.Vars.*;
import static mindustry.logic.LCanvas.*;
@@ -52,7 +53,7 @@ public class LogicDialog extends BaseDialog{
add(buttons).growX().name("canvas");
}
public static Color typeColor(Var s, Color color){
public static Color typeColor(LVar s, Color color){
return color.set(
!s.isobj ? Pal.place :
s.objval == null ? Color.darkGray :
@@ -66,7 +67,7 @@ public class LogicDialog extends BaseDialog{
);
}
public static String typeName(Var s){
public static String typeName(LVar s){
return
!s.isobj ? "number" :
s.objval == null ? "null" :
@@ -92,11 +93,29 @@ public class LogicDialog extends BaseDialog{
TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left();
if(privileged && executor != null && executor.build != null && !ui.editor.isShown()){
t.button("@editor.worldprocessors.editname", Icon.edit, style, () -> {
ui.showTextInput("", "@editor.name", LogicBlock.maxNameLength, executor.build.tag == null ? "" : executor.build.tag, tag -> {
if(privileged && executor != null && executor.build != null){
executor.build.configure(tag);
//just in case of privilege shenanigans...
executor.build.tag = tag;
}
});
dialog.hide();
}).marginLeft(12f).row();
}
t.button("@clear", Icon.cancel, style, () -> {
ui.showConfirm("@logic.clear.confirm", () -> canvas.clearStatements());
dialog.hide();
}).marginLeft(12f).row();
t.button("@schematic.copy", Icon.copy, style, () -> {
dialog.hide();
Core.app.setClipboardText(canvas.save());
}).marginLeft(12f);
t.row();
}).marginLeft(12f).row();
t.button("@schematic.copy.import", Icon.download, style, () -> {
dialog.hide();
try{

View File

@@ -15,6 +15,7 @@ public enum LogicRule{
lighting,
ambientLight,
solarMultiplier,
dragMultiplier,
ban,
unban,

View File

@@ -33,7 +33,7 @@ public class Maps{
NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new,
RiverNoiseFilter::new, OreFilter::new, OreMedianFilter::new, MedianFilter::new,
BlendFilter::new, MirrorFilter::new, ClearFilter::new, CoreSpawnFilter::new,
EnemySpawnFilter::new, SpawnPathFilter::new
EnemySpawnFilter::new, SpawnPathFilter::new, LogicFilter::new
};
/** List of all built-in maps. Filenames only. */

View File

@@ -27,7 +27,7 @@ public class BlendFilter extends GenerateFilter{
@Override
public char icon(){
return Iconc.blockSand;
return Iconc.blockSandFloor;
}
@Override

View File

@@ -0,0 +1,72 @@
package mindustry.maps.filters;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.maps.filters.FilterOption.*;
import mindustry.world.*;
import static mindustry.Vars.*;
public class LogicFilter extends GenerateFilter{
/** max available execution for logic filter */
public static int maxInstructionsExecution = 500 * 500 * 25;
public String code;
public boolean loop;
@Override
public FilterOption[] options(){
return new FilterOption[]{
new FilterOption(){
final String name;
{
name = "code";
}
@Override
public void build(Table table){
table.button(b -> b.image(Icon.pencil).size(iconSmall), () -> {
ui.logic.show(code, null, true, code -> LogicFilter.this.code = code);
}).pad(4).margin(12f);
table.add("@filter.option." + name);
}
},
new ToggleOption("loop", () -> loop, f -> loop = f)
};
}
@Override
public void apply(Tiles tiles, GenerateInput in){
LExecutor executor = new LExecutor();
executor.privileged = true;
try{
//assembler has no variables, all the standard ones are null
executor.load(LAssembler.assemble(code, true));
}catch(Throwable ignored){
//if loading code
return;
}
//this updates map width/height global variables
logicVars.update();
//NOTE: all tile operations will call setNet for tiles, but that should have no overhead during world loading
//executions are limited to prevent infinite generation
for(int i = 1; i < maxInstructionsExecution; i++){
if(!loop && (executor.counter.numval >= executor.instructions.length || executor.counter.numval < 0)) break;
executor.runOnce();
}
}
@Override
public char icon(){
return Iconc.blockMicroProcessor;
}
@Override
public boolean isPost(){
return true;
}
}

View File

@@ -28,7 +28,7 @@ public class SpawnPathFilter extends GenerateFilter{
@Override
public char icon(){
return Iconc.blockCommandCenter;
return Iconc.blockCoreZone;
}
@Override

View File

@@ -146,6 +146,11 @@ public class ContentParser{
readFields(result, data);
return result;
});
put(MassDriverBolt.class, (type, data) -> {
MassDriverBolt result = (MassDriverBolt)make(MassDriverBolt.class);
readFields(result, data);
return result;
});
put(AmmoType.class, (type, data) -> {
//string -> item
//if liquid ammo support is added, this should scan for liquids as well
@@ -322,6 +327,20 @@ public class ContentParser{
readFields(consume, data);
return consume;
});
put(Team.class, (type, data) -> {
if(data.isString()){
Team out = Structs.find(Team.baseTeams, t -> t.name.equals(data.asString()));
if(out == null) throw new IllegalArgumentException("Unknown team: " + data.asString());
return out;
}else if(data.isNumber()){
if(data.asInt() >= Team.all.length || data.asInt() < 0){
throw new IllegalArgumentException("Unknown team: " + data.asString());
}
return Team.get(data.asInt());
}else{
throw new IllegalArgumentException("Unknown team: " + data.asString() + ". Team must either be a string or a number.");
}
});
}};
/** Stores things that need to be parsed fully, e.g. reading fields of content.
* This is done to accommodate binding of content names first.*/
@@ -462,6 +481,7 @@ public class ContentParser{
case "itemFlammable" -> block.consume((Consume)parser.readValue(ConsumeItemFlammable.class, child));
case "itemRadioactive" -> block.consume((Consume)parser.readValue(ConsumeItemRadioactive.class, child));
case "itemExplosive" -> block.consume((Consume)parser.readValue(ConsumeItemExplosive.class, child));
case "itemList" -> block.consume((Consume)parser.readValue(ConsumeItemList.class, child));
case "itemExplode" -> block.consume((Consume)parser.readValue(ConsumeItemExplode.class, child));
case "items" -> block.consume(child.isArray() ?
new ConsumeItems(parser.readValue(ItemStack[].class, child)) :
@@ -668,6 +688,27 @@ public class ContentParser{
currentContent = planet;
read(() -> readFields(planet, value));
return planet;
},
ContentType.team, (TypeParser<TeamEntry>)(mod, name, value) -> {
TeamEntry entry;
Team team;
if(value.has("team")){
team = (Team)classParsers.get(Team.class).parse(Team.class, value.get("team"));
}else{
throw new RuntimeException("Team field missing.");
}
value.remove("team");
if(locate(ContentType.team, name) != null){
entry = locate(ContentType.team, name);
readBundle(ContentType.team, name, value);
}else{
readBundle(ContentType.team, name, value);
entry = new TeamEntry(mod + "-" + name, team);
}
currentContent = entry;
read(() -> readFields(entry, value));
return entry;
}
);
@@ -949,6 +990,8 @@ public class ContentParser{
case "min" -> base.min(parser.readValue(PartProgress.class, data.get("other")));
case "sin" -> base.sin(data.has("offset") ? data.getFloat("offset") : 0f, data.getFloat("scl"), data.getFloat("mag"));
case "absin" -> base.absin(data.getFloat("scl"), data.getFloat("mag"));
case "mod" -> base.mod(data.getFloat("amount"));
case "loop" -> base.loop(data.getFloat("time"));
case "curve" -> data.has("interp") ? base.curve(parser.readValue(Interp.class, data.get("interp"))) : base.curve(data.getFloat("offset"), data.getFloat("duration"));
default -> throw new RuntimeException("Unknown operation '" + op + "', check PartProgress class for a list of methods.");
};
@@ -1054,17 +1097,25 @@ public class ContentParser{
}
Field field = metadata.field;
try{
boolean mergeMap = ObjectMap.class.isAssignableFrom(field.getType()) && child.has("add") && child.get("add").isBoolean() && child.getBoolean("add", false);
boolean isMap = ObjectMap.class.isAssignableFrom(field.getType()) || ObjectIntMap.class.isAssignableFrom(field.getType()) || ObjectFloatMap.class.isAssignableFrom(field.getType());
boolean mergeMap = isMap && child.has("add") && child.get("add").isBoolean() && child.getBoolean("add", false);
if(mergeMap){
child.remove("add");
}
Object readField = parser.readValue(field.getType(), metadata.elementType, child, metadata.keyType);
Object fieldObj = field.get(object);
//if a map has add: true, add its contents to the map instead
if(mergeMap && field.get(object) instanceof ObjectMap<?,?> baseMap){
baseMap.putAll((ObjectMap)readField);
if(mergeMap && (fieldObj instanceof ObjectMap<?,?> || fieldObj instanceof ObjectIntMap<?> || fieldObj instanceof ObjectFloatMap<?>)){
if(field.get(object) instanceof ObjectMap<?,?> baseMap){
baseMap.putAll((ObjectMap)readField);
}else if(field.get(object) instanceof ObjectIntMap<?> baseMap){
baseMap.putAll((ObjectIntMap)readField);
}else if(field.get(object) instanceof ObjectFloatMap<?> baseMap){
baseMap.putAll((ObjectFloatMap)readField);
}
}else{
field.set(object, readField);
}

View File

@@ -871,6 +871,11 @@ public class Mods implements Loadable{
Seq<LoadRun> runs = new Seq<>();
for(LoadedMod mod : orderedMods()){
Seq<LoadRun> unorderedContent = new Seq<>();
ObjectMap<String, LoadRun> orderedContent = new ObjectMap<>();
String[] contentOrder = mod.meta.contentOrder;
ObjectSet<String> orderSet = contentOrder == null ? null : ObjectSet.with(contentOrder);
if(mod.root.child("content").exists()){
Fi contentRoot = mod.root.child("content");
for(ContentType type : ContentType.all){
@@ -878,15 +883,34 @@ public class Mods implements Loadable{
Fi folder = contentRoot.child(lower + (lower.endsWith("s") ? "" : "s"));
if(folder.exists()){
for(Fi file : folder.findAll(f -> f.extension().equals("json") || f.extension().equals("hjson"))){
runs.add(new LoadRun(type, file, mod));
//if this is part of the ordered content, put it aside to be dealt with later
if(orderSet != null && orderSet.contains(file.nameWithoutExtension())){
orderedContent.put(file.nameWithoutExtension(), new LoadRun(type, file, mod));
}else{
unorderedContent.add(new LoadRun(type, file, mod));
}
}
}
}
}
//ordered content will be loaded first, if it exists
if(contentOrder != null){
for(String contentName : contentOrder){
LoadRun run = orderedContent.get(contentName);
if(run != null){
runs.add(run);
}else{
Log.warn("Cannot find content defined in contentOrder: @", contentName);
}
}
}
//unordered content is sorted alphabetically per mod
runs.addAll(unorderedContent.sort());
}
//make sure mod content is in proper order
runs.sort();
for(LoadRun l : runs){
Content current = content.getLastAdded();
try{
@@ -1357,6 +1381,8 @@ public class Mods implements Loadable{
public float texturescale = 1.0f;
/** If true, bleeding is skipped and no content icons are generated. */
public boolean pregenerated;
/** If set, load the mod content in this order by content names */
public String[] contentOrder;
public String displayName(){
//useless, kept for legacy reasons

View File

@@ -17,6 +17,8 @@ public class Packets{
nameInUse, idInUse, nameEmpty, customClient, serverClose, vote, typeMismatch,
whitelist, playerLimit, serverRestarting;
public static final KickReason[] all = values();
public final boolean quiet;
KickReason(){
@@ -38,7 +40,9 @@ public class Packets{
}
public enum AdminAction{
kick, ban, trace, wave, switchTeam
kick, ban, trace, wave, switchTeam;
public static final AdminAction[] all = values();
}
/** Generic client connection event. */

View File

@@ -291,7 +291,7 @@ public class GameService{
});
Events.on(SectorLaunchLoadoutEvent.class, e -> {
if(!schematics.isDefaultLoadout(e.loadout)){
if(e.sector.planet == Planets.serpulo && !schematics.isDefaultLoadout(e.loadout)){
launchCoreSchematic.complete();
}
});

View File

@@ -99,7 +99,7 @@ public class StatusEffect extends UnlockableContent{
boolean reacts = false;
for(var e : opposites.toSeq().sort()){
stats.add(Stat.opposites, e.emoji() + "" + e);
stats.add(Stat.opposites, e.emoji() + e);
}
if(reactive){
@@ -142,6 +142,11 @@ public class StatusEffect extends UnlockableContent{
}
}
/** Called when status effect is removed. */
public void onRemoved(Unit unit){
}
protected void trans(StatusEffect effect, TransitionHandler handler){
transitions.put(effect, handler);
}

View File

@@ -9,11 +9,15 @@ import mindustry.game.*;
public class TeamEntry extends UnlockableContent{
public final Team team;
public TeamEntry(Team team){
super(team.name);
public TeamEntry(String name, Team team){
super(name);
this.team = team;
}
public TeamEntry(Team team){
this(team.name, team);
}
@Override
public void displayExtra(Table table){
table.add("@team." + name + ".log").pad(6).padTop(20).width(400f).wrap().fillX();

View File

@@ -141,8 +141,10 @@ public class UnitType extends UnlockableContent implements Senseable{
/** if true, this unit counts as an enemy in the wave counter (usually false for support-only units) */
public boolean isEnemy = true,
/** If true, the unit is always at elevation 1. */
/** if true, the unit is always at elevation 1 */
flying = false,
/** whether this flying unit should wobble around */
wobble = true,
/** whether this unit tries to attack air units */
targetAir = true,
/** whether this unit tries to attack ground units */
@@ -257,6 +259,8 @@ public class UnitType extends UnlockableContent implements Senseable{
public Color healColor = Pal.heal;
/** Color of light that this unit produces when lighting is enabled in the map. */
public Color lightColor = Pal.powerLight;
/** override for unit shield colour. */
public @Nullable Color shieldColor;
/** sound played when this unit explodes (*not* when it is shot down) */
public Sound deathSound = Sounds.bang;
/** sound played on loop when this unit is around. */
@@ -288,6 +292,8 @@ public class UnitType extends UnlockableContent implements Senseable{
/** Function used for calculating cost of moving with ControlPathfinder. Does not affect "normal" flow field pathfinding. */
public @Nullable PathCost pathCost;
/** ID for path cost, to be used in the control path finder. This is the value that actually matters; do not assign manually. Set in init(). */
public int pathCostId;
/** A sample of the unit that this type creates. Do not modify! */
public @Nullable Unit sample;
@@ -427,10 +433,13 @@ public class UnitType extends UnlockableContent implements Senseable{
//(undocumented, you shouldn't need to use these, and if you do just check how they're drawn and copy that)
public TextureRegion baseRegion, legRegion, region, previewRegion, shadowRegion, cellRegion, itemCircleRegion,
softShadowRegion, jointRegion, footRegion, legBaseRegion, baseJointRegion, outlineRegion, treadRegion;
softShadowRegion, jointRegion, footRegion, legBaseRegion, baseJointRegion, outlineRegion, treadRegion,
mineLaserRegion, mineLaserEndRegion;
public TextureRegion[] wreckRegions, segmentRegions, segmentOutlineRegions;
public TextureRegion[][] treadRegions;
//INTERNAL REQUIREMENTS
protected float buildTime = -1f;
protected @Nullable ItemStack[] totalRequirements, cachedRequirements, firstRequirements;
@@ -691,6 +700,9 @@ public class UnitType extends UnlockableContent implements Senseable{
ControlPathfinder.costGround;
}
pathCostId = ControlPathfinder.costTypes.indexOf(pathCost);
if(pathCostId == -1) pathCostId = 0;
if(flying){
envEnabled |= Env.space;
}
@@ -816,6 +828,10 @@ public class UnitType extends UnlockableContent implements Senseable{
if(canBoost){
cmds.add(UnitCommand.boostCommand);
if(buildSpeed > 0f){
cmds.add(UnitCommand.rebuildCommand, UnitCommand.assistCommand);
}
}
//healing, mining and building is only supported for flying units; pathfinding to ambiguously reachable locations is hard.
@@ -842,7 +858,7 @@ public class UnitType extends UnlockableContent implements Senseable{
if(stances.length == 0){
if(canAttack){
Seq<UnitStance> seq = Seq.with(UnitStance.stop, UnitStance.shoot, UnitStance.holdFire, UnitStance.pursueTarget, UnitStance.patrol);
if(crushDamage > 0){
if(!flying){
seq.add(UnitStance.ram);
}
stances = seq.toArray(UnitStance.class);
@@ -910,6 +926,9 @@ public class UnitType extends UnlockableContent implements Senseable{
legBaseRegion = Core.atlas.find(name + "-leg-base", name + "-leg");
baseRegion = Core.atlas.find(name + "-base");
cellRegion = Core.atlas.find(name + "-cell", Core.atlas.find("power-cell"));
mineLaserRegion = Core.atlas.find("minelaser");
mineLaserEndRegion = Core.atlas.find("minelaser-end");
//when linear filtering is on, it's acceptable to use the relatively low-res 'particle' region
softShadowRegion =
squareShape ? Core.atlas.find("square-shadow") :
@@ -1186,6 +1205,10 @@ public class UnitType extends UnlockableContent implements Senseable{
public void draw(Unit unit){
if(unit.inFogTo(Vars.player.team())) return;
unit.drawBuilding();
drawMining(unit);
boolean isPayload = !unit.isAdded();
Mechc mech = unit instanceof Mechc ? (Mechc)unit : null;
@@ -1259,9 +1282,9 @@ public class UnitType extends UnlockableContent implements Senseable{
for(int i = 0; i < parts.size; i++){
var part = parts.get(i);
WeaponMount first = unit.mounts.length > part.weaponIndex ? unit.mounts[part.weaponIndex] : null;
if(first != null){
DrawPart.params.set(first.warmup, first.reload / weapons.first().reload, first.smoothReload, first.heat, first.recoil, first.charge, unit.x, unit.y, unit.rotation);
WeaponMount mount = unit.mounts.length > part.weaponIndex ? unit.mounts[part.weaponIndex] : null;
if(mount != null){
DrawPart.params.set(mount.warmup, mount.reload / mount.weapon.reload, mount.smoothReload, mount.heat, mount.recoil, mount.charge, unit.x, unit.y, unit.rotation);
}else{
DrawPart.params.set(0f, 0f, 0f, 0f, 0f, 0f, unit.x, unit.y, unit.rotation);
}
@@ -1288,6 +1311,38 @@ public class UnitType extends UnlockableContent implements Senseable{
Draw.reset();
}
//...where do I put this
public Color shieldColor(Unit unit){
return shieldColor == null ? unit.team.color : shieldColor;
}
public void drawMining(Unit unit){
if(!unit.mining()) return;
float focusLen = unit.hitSize / 2f + Mathf.absin(Time.time, 1.1f, 0.5f);
float swingScl = 12f, swingMag = tilesize / 8f;
float flashScl = 0.3f;
float px = unit.x + Angles.trnsx(unit.rotation, focusLen);
float py = unit.y + Angles.trnsy(unit.rotation, focusLen);
float ex = unit.mineTile.worldx() + Mathf.sin(Time.time + 48, swingScl, swingMag);
float ey = unit.mineTile.worldy() + Mathf.sin(Time.time + 48, swingScl + 2f, swingMag);
Draw.z(Layer.flyingUnit + 0.1f);
Draw.color(Color.lightGray, Color.white, 1f - flashScl + Mathf.absin(Time.time, 0.5f, flashScl));
Drawf.laser(mineLaserRegion, mineLaserEndRegion, px, py, ex, ey, 0.75f);
if(unit.isLocal()){
Lines.stroke(1f, Pal.accent);
Lines.poly(unit.mineTile.worldx(), unit.mineTile.worldy(), 4, tilesize / 2f * Mathf.sqrt2, Time.time);
}
Draw.color();
}
public <T extends Unit & Payloadc> void drawPayload(T unit){
if(unit.hasPayload()){
@@ -1302,7 +1357,7 @@ public class UnitType extends UnlockableContent implements Senseable{
float radius = unit.hitSize() * 1.3f;
Fill.light(unit.x, unit.y, Lines.circleVertices(radius), radius,
Color.clear,
Tmp.c2.set(unit.team.color).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(0.7f * alpha)
Tmp.c2.set(unit.type.shieldColor(unit)).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(0.7f * alpha)
);
}
@@ -1465,6 +1520,7 @@ public class UnitType extends UnlockableContent implements Senseable{
}
public <T extends Unit & Tankc> void drawTank(T unit){
applyColor(unit);
Draw.rect(treadRegion, unit.x, unit.y, unit.rotation - 90);
if(treadRegion.found()){
@@ -1636,6 +1692,10 @@ public class UnitType extends UnlockableContent implements Senseable{
if(unit.drownTime > 0 && unit.lastDrownFloor != null){
Draw.mixcol(Tmp.c1.set(unit.lastDrownFloor.mapColor).mul(0.83f), unit.drownTime * 0.9f);
}
//this is horribly scuffed.
if(renderer != null && renderer.overlays != null){
renderer.overlays.checkApplySelection(unit);
}
}
//endregion

View File

@@ -161,7 +161,7 @@ public class Weather extends UnlockableContent{
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / density * intensity);
Lines.stroke(stroke);
float alpha = Draw.getColor().a;
float alpha = Draw.getColorAlpha();
Draw.color(color);
for(int i = 0; i < total; i++){

View File

@@ -20,27 +20,21 @@ import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import java.util.*;
import java.io.*;
public class Fonts{
private static final String mainFont = "fonts/font.woff";
private static final ObjectSet<String> unscaled = ObjectSet.with("iconLarge");
private static ObjectIntMap<String> unicodeIcons = new ObjectIntMap<>();
private static IntMap<String> unicodeToName = new IntMap<>();
private static ObjectMap<String, String> stringIcons = new ObjectMap<>();
private static ObjectMap<String, TextureRegion> largeIcons = new ObjectMap<>();
private static TextureRegion[] iconTable;
private static int lastCid;
public static Font def, outline, icon, iconLarge, tech, logic;
public static TextureRegion logicIcon(int id){
return iconTable[id];
}
public static int getUnicode(String content){
return unicodeIcons.get(content, 0);
}
@@ -95,12 +89,16 @@ public class Fonts{
}})).loaded = f -> Fonts.logic = f;
}
public static @Nullable String unicodeToName(int unicode){
return unicodeToName.get(unicode, () -> Iconc.codeToName.get(unicode));
}
public static TextureRegion getLargeIcon(String name){
return largeIcons.get(name, () -> {
var region = new TextureRegion();
int code = Iconc.codes.get(name, '\uF8D4');
var glyph = iconLarge.getData().getGlyph((char)code);
if(glyph == null) return Core.atlas.find("error");
if(glyph == null) return Core.atlas.find(name);
region.set(iconLarge.getRegion().texture);
region.set(glyph.u, glyph.v2, glyph.u2, glyph.v);
return region;
@@ -112,9 +110,9 @@ public class Fonts{
Texture uitex = Core.atlas.find("logo").texture;
int size = (int)(Fonts.def.getData().lineHeight/Fonts.def.getData().scaleY);
try(Scanner scan = new Scanner(Core.files.internal("icons/icons.properties").read(512))){
while(scan.hasNextLine()){
String line = scan.nextLine();
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0], texture = nametex[1];
@@ -127,6 +125,7 @@ public class Fonts{
unicodeIcons.put(nametex[0], ch);
stringIcons.put(nametex[0], ((char)ch) + "");
unicodeToName.put(ch, texture);
Vec2 out = Scaling.fit.apply(region.width, region.height, size, size);
@@ -148,32 +147,21 @@ public class Fonts{
glyph.page = 0;
fonts.each(f -> f.getData().setGlyph(ch, glyph));
}
}catch(IOException e){
throw new RuntimeException(e);
}
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));
iconTable = new TextureRegion[512];
iconTable[0] = Core.atlas.find("error");
lastCid = 1;
Vars.content.each(c -> {
if(c instanceof UnlockableContent u){
TextureRegion region = Core.atlas.find(u.name + "-icon-logic");
if(region.found()){
iconTable[u.iconId = lastCid++] = region;
}
}
});
for(Team team : Team.baseTeams){
team.emoji = stringIcons.get(team.name, "");
}
}
public static void loadContentIconsHeadless(){
try(Scanner scan = new Scanner(Core.files.internal("icons/icons.properties").read(512))){
while(scan.hasNextLine()){
String line = scan.nextLine();
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0];
@@ -182,6 +170,8 @@ public class Fonts{
unicodeIcons.put(nametex[0], ch);
stringIcons.put(nametex[0], ((char)ch) + "");
}
}catch(IOException e){
throw new RuntimeException(e);
}
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));

View File

@@ -3,9 +3,11 @@ package mindustry.ui;
import arc.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
@@ -20,6 +22,22 @@ public class Minimap extends Table{
add(new Element(){
{
setSize(Scl.scl(140f));
addListener(new ClickListener(KeyCode.mouseRight){
@Override
public void clicked(InputEvent event, float cx, float cy){
var region = renderer.minimap.getRegion();
if(region == null) return;
float
sx = (cx - x) / width,
sy = (cy - y) / height,
scaledX = Mathf.lerp(region.u, region.u2, sx) * world.width() * tilesize,
scaledY = Mathf.lerp(1f - region.v2, 1f - region.v, sy) * world.height() * tilesize;
control.input.panCamera(Tmp.v1.set(scaledX, scaledY));
}
});
}
@Override
@@ -93,7 +111,7 @@ public class Minimap extends Table{
update(() -> {
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e != null && e.isDescendantOf(this)){
requestScroll();
}else if(hasScroll()){

View File

@@ -73,6 +73,8 @@ public class Styles{
geni,
/** Gray, toggleable, no background. */
grayi,
/** Gray square background, standard behavior. Equivalent to grayt. */
graySquarei,
/** Flat, square, black background. */
flati,
/** Square border. */
@@ -288,6 +290,14 @@ public class Styles{
imageUpColor = Color.lightGray;
imageDownColor = Color.white;
}};
graySquarei = new ImageButtonStyle(){{
imageUpColor = Color.white;
imageDownColor = Color.lightGray;
over = flatOver;
down = flatOver;
up = grayPanel;
}};
flati = new ImageButtonStyle(){{
down = flatOver;
up = black;

View File

@@ -30,16 +30,24 @@ public class CustomRulesDialog extends BaseDialog{
private Table main;
private Prov<Rules> resetter;
private LoadoutDialog loadoutDialog;
public boolean showRuleEditRule;
public Seq<Table> categories;
public Table current;
public Seq<String> categoryNames;
public String currentName;
public String currentName = "";
public String ruleSearch = "";
public Seq<Runnable> additionalSetup; // for modding to easily add new rules
public CustomRulesDialog(){
this(false);
}
public CustomRulesDialog(boolean showRuleEditRule){
super("@mode.custom");
this.showRuleEditRule = showRuleEditRule;
loadoutDialog = new LoadoutDialog();
setFillParent(true);
@@ -49,8 +57,6 @@ public class CustomRulesDialog extends BaseDialog{
additionalSetup = new Seq<>();
categories = new Seq<>();
categoryNames = new Seq<>();
currentName = "";
ruleSearch = "";
buttons.button("@edit", Icon.pencil, () -> {
BaseDialog dialog = new BaseDialog("@waves.edit");
@@ -209,12 +215,12 @@ public class CustomRulesDialog extends BaseDialog{
main.left().defaults().fillX().left();
main.row();
category("waves");
check("@rules.waves", b -> rules.waves = b, () -> rules.waves);
check("@rules.wavesending", b -> rules.waveSending = b, () -> rules.waveSending, () -> rules.waves);
check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer, () -> rules.waves);
check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies, () -> rules.waves && rules.waveTimer);
check("@rules.airUseSpawns", b -> rules.airUseSpawns = b, () -> rules.airUseSpawns, () -> rules.waves);
numberi("@rules.wavelimit", f -> rules.winWave = f, () -> rules.winWave, () -> rules.waves, 0, Integer.MAX_VALUE);
number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> rules.waves && rules.waveTimer, 1, Float.MAX_VALUE);
//this is experimental, because it's not clear that 0 makes it default.
@@ -351,6 +357,10 @@ public class CustomRulesDialog extends BaseDialog{
category("teams");
//not sure where else to put this
if(showRuleEditRule){
check("@rules.allowedit", b -> rules.allowEditRules = b, () -> rules.allowEditRules);
}
team("@rules.playerteam", t -> rules.defaultTeam = t, () -> rules.defaultTeam);
team("@rules.enemyteam", t -> rules.waveTeam = t, () -> rules.waveTeam);

View File

@@ -45,7 +45,7 @@ public class DatabaseDialog extends BaseDialog{
void rebuild(){
all.clear();
var text = search.getText();
var text = search.getText().toLowerCase();
Seq<Content>[] allContent = Vars.content.getContentMap();
@@ -54,7 +54,7 @@ public class DatabaseDialog extends BaseDialog{
Seq<UnlockableContent> array = allContent[j]
.select(c -> c instanceof UnlockableContent u && !u.isHidden() &&
(text.isEmpty() || u.localizedName.toLowerCase().contains(text.toLowerCase()))).as();
(text.isEmpty() || u.localizedName.toLowerCase().contains(text))).as();
if(array.size == 0) continue;
all.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);

View File

@@ -0,0 +1,74 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class IconSelectDialog extends Dialog{
private Intc consumer = i -> Log.info("you have mere seconds");
public IconSelectDialog(){
closeOnBack();
setFillParent(true);
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
t.marginRight(19f);
t.defaults().size(48f);
t.button(Icon.none, Styles.flati, () -> {
hide();
consumer.get(0);
});
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 1;
for(var key : accessibleIcons){
var value = Icon.icons.get(key);
t.button(value, Styles.flati, () -> {
hide();
consumer.get(Iconc.codes.get(key));
});
if(++i % cols == 0) t.row();
}
for(ContentType ctype : defaultContentIcons){
t.row();
t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent);
t.row();
i = 0;
for(UnlockableContent u : content.getBy(ctype).<UnlockableContent>as()){
if(!u.isHidden() && u.unlocked()){
t.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
hide();
consumer.get(u.emojiChar());
});
if(++i % cols == 0) t.row();
}
}
}
});
});
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
public void show(Intc listener){
consumer = listener;
super.show();
}
}

View File

@@ -213,7 +213,6 @@ public class KeybindDialog extends Dialog{
@Override
public boolean keyDown(InputEvent event, KeyCode keycode){
rebindDialog.hide();
if(keycode == KeyCode.escape) return false;
rebind(section, name, keycode);
return false;
}

View File

@@ -14,7 +14,7 @@ import static mindustry.Vars.*;
public class MapPlayDialog extends BaseDialog{
public @Nullable Runnable playListener;
CustomRulesDialog dialog = new CustomRulesDialog();
CustomRulesDialog dialog = new CustomRulesDialog(true);
Rules rules;
Gamemode selectedGamemode = Gamemode.survival;
Map lastMap;
@@ -102,7 +102,7 @@ public class MapPlayDialog extends BaseDialog{
ScrollPane pane = new ScrollPane(table);
pane.setFadeScrollBars(false);
table.row();
for(Gamemode mode : Gamemode.values()){
for(Gamemode mode : Gamemode.all){
if(mode.hidden) continue;
table.labelWrap("[accent]" + mode + ":[] [lightgray]" + mode.description()).width(400f);
table.row();

View File

@@ -1,19 +1,40 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.editor.*;
import mindustry.game.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
public class PausedDialog extends BaseDialog{
private MapProcessorsDialog processors = new MapProcessorsDialog();
private SaveDialog save = new SaveDialog();
private LoadDialog load = new LoadDialog();
private boolean wasClient = false;
private CustomRulesDialog rulesDialog = new CustomRulesDialog();
public PausedDialog(){
super("@menu");
shouldPause = true;
clearChildren();
add(titleTable).growX().row();
stack(cont, new Table(t -> {
t.bottom().left();
t.button(Icon.book, () -> {
Rules toEdit = Vars.state.rules.copy();
rulesDialog.show(toEdit, () -> state.rules.copy());
rulesDialog.hidden(() -> {
//apply rule changes only once it is hidden
Vars.state.rules = toEdit;
Call.setRules(toEdit);
});
}).size(70f).tooltip("@customize").visible(() -> state.rules.allowEditRules && (net.server() || !net.active()));
})).grow().row();
shown(this::rebuild);
addCloseListener();
@@ -49,13 +70,22 @@ public class PausedDialog extends BaseDialog{
cont.row();
cont.button("@hostserver", Icon.host, () -> {
//the button runs out of space when the editor button is added, so use the mobile text
cont.button(state.isEditor() ? "@hostserver.mobile" : "@hostserver", Icon.host, () -> {
if(net.server() && steam){
platform.inviteFriends();
}else{
ui.host.show();
}
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(2).width(dw * 2 + 10f).update(e -> e.setText(net.server() && steam ? "@invitefriends" : "@hostserver"));
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(state.isEditor() ? 1 : 2).width(state.isEditor() ? dw : dw * 2 + 10f)
.update(e -> e.setText(net.server() && steam ? "@invitefriends" : state.isEditor() ? "@hostserver.mobile" : "@hostserver"));
if(state.isEditor()){
cont.button("@editor.worldprocessors", Icon.logic, () -> {
hide();
processors.show();
});
}
cont.row();
@@ -119,7 +149,7 @@ public class PausedDialog extends BaseDialog{
}
public void runExitSave(){
wasClient = net.client();
boolean wasClient = net.client();
if(net.client()) netClient.disconnectQuietly();
if(state.isEditor() && !wasClient){

View File

@@ -43,13 +43,6 @@ import static mindustry.graphics.g3d.PlanetRenderer.*;
import static mindustry.ui.dialogs.PlanetDialog.Mode.*;
public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
static final String[] defaultIcons = {
"effect", "power", "logic", "units", "liquid", "production", "defense", "turret", "distribution", "crafting",
"settings", "cancel", "zoom", "ok", "star", "home", "pencil", "up", "down", "left", "right",
"hammer", "warning", "tree", "admin", "map", "modePvp", "terrain",
"modeSurvival", "commandRally", "commandAttack",
};
//if true, enables launching anywhere for testing
public static boolean debugSelect = false;
public static float sectorShowDuration = 60f * 2.4f;
@@ -594,7 +587,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
@Override
public void act(float delta){
if(scene.getDialog() == PlanetDialog.this && !scene.hit(input.mouseX(), input.mouseY(), true).isDescendantOf(e -> e instanceof ScrollPane)){
if(scene.getDialog() == PlanetDialog.this && (scene.getHoverElement() == null || !scene.getHoverElement().isDescendantOf(e -> e instanceof ScrollPane))){
scene.setScrollFocus(PlanetDialog.this);
}
@@ -998,6 +991,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
sector.save = null;
}
updateSelected();
rebuildList();
});
}
@@ -1053,6 +1047,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
Icon.icons.get(sector.info.icon + "Small");
title.button(icon == null ? Icon.noneSmall : icon, Styles.clearNonei, iconSmall, () -> {
//TODO use IconSelectDialog
new Dialog(""){{
closeOnBack();
setFillParent(true);
@@ -1079,7 +1074,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 1;
for(var key : defaultIcons){
for(var key : accessibleIcons){
var value = Icon.icons.get(key);
t.button(value, Styles.squareTogglei, () -> {

View File

@@ -472,7 +472,7 @@ public class ResearchDialog extends BaseDialog{
if(mobile){
tapped(() -> {
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e == this){
hoverNode = null;
rebuild();

View File

@@ -398,6 +398,7 @@ public class SchematicsDialog extends BaseDialog{
closeOnBack();
setFillParent(true);
//TODO: use IconSelectDialog
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
@@ -407,7 +408,7 @@ public class SchematicsDialog extends BaseDialog{
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 0;
for(String icon : PlanetDialog.defaultIcons){
for(String icon : accessibleIcons){
String out = (char)Iconc.codes.get(icon) + "";
if(tags.contains(out)) continue;

View File

@@ -297,6 +297,7 @@ public class SettingsMenuDialog extends BaseDialog{
}
void addSettings(){
sound.checkPref("alwaysmusic", false);
sound.sliderPref("musicvol", 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("sfxvol", 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("ambientvol", 100, 0, 100, 1, i -> i + "%");

View File

@@ -67,7 +67,7 @@ public class BlockConfigFragment{
}
public boolean hasConfigMouse(){
Element e = Core.scene.hit(Core.input.mouseX(), Core.graphics.getHeight() - Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
return e != null && (e == table || e.isDescendantOf(table));
}

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