Merge branch 'master' into port-field

This commit is contained in:
Antsiferov Andrew
2021-01-01 21:55:19 +03:00
committed by GitHub
1246 changed files with 30340 additions and 60705 deletions

View File

@@ -9,6 +9,7 @@ import arc.graphics.g2d.*;
import arc.math.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.ai.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
@@ -74,7 +75,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Fonts.loadDefaultFont();
//load fallback atlas if max texture size is below 4096
assets.load(new AssetDescriptor<>(Gl.getInt(Gl.maxTextureSize) >= 4096 ? "sprites/sprites.atlas" : "sprites/fallback/sprites.atlas", TextureAtlas.class)).loaded = t -> {
assets.load(new AssetDescriptor<>(Gl.getInt(Gl.maxTextureSize) >= 4096 ? "sprites/sprites.atlas" : "sprites/fallback/sprites.atlas", TextureAtlas.class)).loaded = t -> {
atlas = (TextureAtlas)t;
Fonts.mergeFontAtlas(atlas);
};
@@ -103,6 +104,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
assets.load(schematics);
assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> content.load());
assets.loadRun("baseparts", BaseRegistry.class, () -> {}, () -> bases.load());
}
@Override
@@ -110,8 +112,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
super.add(module);
//autoload modules when necessary
if(module instanceof Loadable){
assets.load((Loadable)module);
if(module instanceof Loadable l){
assets.load(l);
}
}

View File

@@ -10,14 +10,14 @@ import arc.util.*;
import arc.util.Log.*;
import mindustry.ai.*;
import mindustry.async.*;
import mindustry.audio.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.input.*;
import mindustry.io.*;
import mindustry.logic.*;
import mindustry.maps.Map;
import mindustry.maps.*;
import mindustry.mod.*;
@@ -36,6 +36,8 @@ public class Vars implements Loadable{
public static boolean loadLocales = true;
/** Whether the logger is loaded. */
public static boolean loadedLogger = false, loadedFileLogger = false;
/** Whether to enable various experimental features (e.g. cliffs) */
public static boolean experimental = false;
/** Maximum extra padding around deployment schematics. */
public static final int maxLoadoutSchematicPad = 5;
/** Maximum schematic size.*/
@@ -55,7 +57,7 @@ public class Vars implements Loadable{
/** URL for sending crash reports to */
public static final String crashReportURL = "http://192.99.169.18/report";
/** URL the links to the wiki's modding guide.*/
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/";
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/1-modding/";
/** URL to the JSON file containing all the global, public servers. Not queried in BE. */
public static final String serverJsonURL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers.json";
/** URL to the JSON file containing all the BE servers. Only queried in BE. */
@@ -63,17 +65,17 @@ public class Vars implements Loadable{
/** URL to the JSON file containing all the BE servers. Only queried in the V6 alpha (will be removed once it's out). */
public static final String serverJsonV6URL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers_v6.json";
/** URL of the github issue report template.*/
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?template=bug_report.md";
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?labels=bug&template=bug_report.md";
/** list of built-in servers.*/
public static final Seq<String> defaultServers = Seq.with();
public static final Seq<ServerGroup> defaultServers = Seq.with();
/** maximum size of any block, do not change unless you know what you're doing */
public static final int maxBlockSize = 16;
/** maximum distance between mine and core that supports automatic transferring */
public static final float mineTransferRange = 220f;
/** max chat message length */
public static final int maxTextLength = 150;
/** max player name length in bytes */
public static final int maxNameLength = 40;
/** shadow color for turrets */
public static final float turretShadowColor = Color.toFloatBits(0, 0, 0, 0.22f);
/** displayed item size when ingame. */
public static final float itemSize = 5f;
/** units outside of this bound will die instantly */
@@ -84,10 +86,14 @@ public class Vars implements Loadable{
public static final float buildingRange = 220f;
/** range for moving items */
public static final float itemTransferRange = 220f;
/** range for moving items for logic units */
public static final float logicItemTransferRange = 45f;
/** duration of time between turns in ticks */
public static final float turnDuration = 20 * Time.toMinutes;
/** turns needed to destroy a sector completely */
public static final float sectorDestructionTurns = 3f;
public static final float turnDuration = 2 * Time.toMinutes;
/** chance of an invasion per turn, 1 = 100% */
public static final float baseInvasionChance = 1f / 100f;
/** how many turns have to pass before invasions start */
public static final int invasionGracePeriod = 20;
/** min armor fraction damage; e.g. 0.05 = at least 5% damage */
public static final float minArmorDamage = 0.1f;
/** launch animation duration */
@@ -183,15 +189,14 @@ public class Vars implements Loadable{
public static ContentLoader content;
public static GameState state;
public static EntityCollisions collisions;
public static DefaultWaves defaultWaves;
public static mindustry.audio.LoopControl loops;
public static Waves waves;
public static Platform platform = new Platform(){};
public static Mods mods;
public static Schematics schematics;
public static BeControl becontrol;
public static AsyncCore asyncCore;
public static TeamIndexProcess teamIndex;
public static BaseRegistry bases;
public static GlobalConstants constants;
public static Universe universe;
public static World world;
@@ -232,6 +237,7 @@ public class Vars implements Loadable{
}
Arrays.sort(locales, Structs.comparing(l -> l.getDisplayName(l), String.CASE_INSENSITIVE_ORDER));
locales = Seq.with(locales).and(new Locale("router")).toArray(Locale.class);
}
Version.init();
@@ -252,8 +258,7 @@ public class Vars implements Loadable{
if(mods == null) mods = new Mods();
content = new ContentLoader();
loops = new LoopControl();
defaultWaves = new DefaultWaves();
waves = new Waves();
collisions = new EntityCollisions();
world = new World();
universe = new Universe();
@@ -265,6 +270,7 @@ public class Vars implements Loadable{
indexer = new BlockIndexer();
pathfinder = new Pathfinder();
bases = new BaseRegistry();
constants = new GlobalConstants();
state = new GameState();
@@ -282,10 +288,10 @@ public class Vars implements Loadable{
if(loadedLogger) return;
String[] tags = {"[green][D][]", "[royal][I][]", "[yellow][W][]", "[scarlet][E][]", ""};
String[] stags = {"&lc&fb[D]", "&lg&fb[I]", "&ly&fb[W]", "&lr&fb[E]", ""};
String[] stags = {"&lc&fb[D]", "&lb&fb[I]", "&ly&fb[W]", "&lr&fb[E]", ""};
Seq<String> logBuffer = new Seq<>();
Log.setLogger((level, text) -> {
Log.logger = (level, text) -> {
String result = text;
String rawText = Log.format(stags[level.ordinal()] + "&fr " + text);
System.out.println(rawText);
@@ -301,9 +307,9 @@ public class Vars implements Loadable{
}
}
ui.scriptfrag.addMessage(Log.removeCodes(result));
ui.scriptfrag.addMessage(Log.removeColors(result));
}
});
};
Events.on(ClientLoadEvent.class, e -> logBuffer.each(ui.scriptfrag::addMessage));
@@ -315,19 +321,25 @@ public class Vars implements Loadable{
settings.setAppName(appName);
Writer writer = settings.getDataDirectory().child("last_log.txt").writer(false);
LogHandler log = Log.getLogger();
Log.setLogger((level, text) -> {
log.log(level, text);
try{
Writer writer = settings.getDataDirectory().child("last_log.txt").writer(false);
LogHandler log = Log.logger;
//ignore it
Log.logger = (level, text) -> {
log.log(level, text);
try{
writer.write("[" + Character.toUpperCase(level.name().charAt(0)) +"] " + Log.removeCodes(text) + "\n");
writer.flush();
}catch(IOException e){
e.printStackTrace();
//ignore it
}
});
try{
writer.write("[" + Character.toUpperCase(level.name().charAt(0)) +"] " + Log.removeColors(text) + "\n");
writer.flush();
}catch(IOException e){
e.printStackTrace();
//ignore it
}
};
}catch(Exception e){
//handle log file not being found
Log.err(e);
}
loadedFileLogger = true;
}
@@ -383,6 +395,11 @@ public class Vars implements Loadable{
Locale.setDefault(locale);
Core.bundle = I18NBundle.createBundle(handle, locale);
//router
if(locale.getDisplayName().equals("router")){
bundle.debug("router");
}
}
}
}

View File

@@ -6,7 +6,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.world.*;
import static mindustry.Vars.world;
import static mindustry.Vars.*;
public class Astar{
public static final DistanceHeuristic manhattan = (x1, y1, x2, y2) -> Math.abs(x1 - x2) + Math.abs(y1 - y2);

View File

@@ -7,6 +7,7 @@ import arc.util.*;
import mindustry.*;
import mindustry.ai.BaseRegistry.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.*;
import mindustry.game.Schematic.*;
import mindustry.game.Teams.*;
@@ -14,46 +15,129 @@ import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.*;
import mindustry.world.blocks.distribution.*;
import mindustry.world.blocks.production.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import static mindustry.Vars.*;
public class BaseAI{
private static final Vec2 axis = new Vec2(), rotator = new Vec2();
private static final float correctPercent = 0.5f;
private static final float step = 5;
private static final int attempts = 5;
private static final int attempts = 4;
private static final float emptyChance = 0.01f;
private static final int timerStep = 0, timerSpawn = 1;
private static final int timerStep = 0, timerSpawn = 1, timerRefreshPath = 2;
private static final int pathStep = 50;
private static final Seq<Tile> tmpTiles = new Seq<>();
private static int correct = 0, incorrect = 0;
private int lastX, lastY, lastW, lastH;
private boolean triedWalls;
private boolean triedWalls, foundPath;
TeamData data;
Interval timer = new Interval(4);
IntSet path = new IntSet();
IntSet calcPath = new IntSet();
@Nullable Tile calcTile;
boolean calculating, startedCalculating;
int calcCount = 0;
int totalCalcs = 0;
public BaseAI(TeamData data){
this.data = data;
}
public void update(){
if(timer.get(timerSpawn, 60) && data.hasCore()){
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 2.5f) && data.hasCore()){
CoreBlock block = (CoreBlock)data.core().block;
int coreUnits = Groups.unit.count(u -> u.team == data.team && u.type == block.unitType);
//create AI core unit
if(!Groups.unit.contains(u -> u.team() == data.team && u.type() == block.unitType)){
//create AI core unit(s)
if(!state.isEditor() && coreUnits < data.cores.size){
Unit unit = block.unitType.create(data.team);
unit.set(data.core());
unit.set(data.cores.random());
unit.add();
Fx.spawn.at(unit);
}
}
//refresh path
if(!calculating && (timer.get(timerRefreshPath, 3f * Time.toMinutes) || !startedCalculating) && data.hasCore()){
calculating = true;
startedCalculating = true;
calcPath.clear();
}
//didn't find tile in time
if(calculating && calcCount >= world.width() * world.height()){
calculating = false;
calcCount = 0;
calcPath.clear();
totalCalcs ++;
}
//calculate path for units so schematics are not placed on it
if(calculating){
if(calcTile == null){
Vars.spawner.eachGroundSpawn((x, y) -> calcTile = world.tile(x, y));
if(calcTile == null){
calculating = false;
}
}else{
var field = pathfinder.getField(state.rules.waveTeam, Pathfinder.costGround, Pathfinder.fieldCore);
int[][] weights = field.weights;
for(int i = 0; i < pathStep; i++){
int minCost = Integer.MAX_VALUE;
int cx = calcTile.x, cy = calcTile.y;
boolean foundAny = false;
for(Point2 p : Geometry.d4){
int nx = cx + p.x, ny = cy + p.y;
Tile other = world.tile(nx, ny);
if(other != null && weights[nx][ny] < minCost && weights[nx][ny] != -1){
minCost = weights[nx][ny];
calcTile = other;
foundAny = true;
}
}
//didn't find anything, break out of loop, this will trigger a clear later
if(!foundAny){
calcCount = Integer.MAX_VALUE;
break;
}
calcPath.add(calcTile.pos());
for(Point2 p : Geometry.d8){
calcPath.add(Point2.pack(p.x + calcTile.x, p.y + calcTile.y));
}
//found the end.
if(calcTile.build instanceof CoreBuild b && b.team == state.rules.defaultTeam){
//clean up calculations and flush results
calculating = false;
calcCount = 0;
path.clear();
path.addAll(calcPath);
calcPath.clear();
calcTile = null;
totalCalcs ++;
foundPath = true;
break;
}
calcCount ++;
}
}
}
//only schedule when there's something to build.
if(data.blocks.isEmpty() && timer.get(timerStep, step)){
if(foundPath && data.blocks.isEmpty() && timer.get(timerStep, Mathf.lerp(20f, 4f, data.team.rules().aiTier))){
if(!triedWalls){
tryWalls();
triedWalls = true;
@@ -68,9 +152,14 @@ public class BaseAI{
if(pos == null) return;
Tmp.v1.rnd(Mathf.random(range));
int wx = (int)(world.toTile(pos.getX()) + Tmp.v1.x), wy = (int)(world.toTile(pos.getY()) + Tmp.v1.y);
int wx = (int)(World.toTile(pos.getX()) + Tmp.v1.x), wy = (int)(World.toTile(pos.getY()) + Tmp.v1.y);
Tile tile = world.tiles.getc(wx, wy);
//try not to block the spawn point
if(spawner.getSpawns().contains(t -> t.within(tile, tilesize * 40f))){
continue;
}
Seq<BasePart> parts = null;
//pick a completely random base part, and place it a random location
@@ -117,6 +206,13 @@ public class BaseAI{
if(!Build.validPlace(tile.block, data.team, realX, realY, tile.rotation)){
return false;
}
Tile wtile = world.tile(realX, realY);
//may intersect AI path
tmpTiles.clear();
if(tile.block.solid && wtile != null && wtile.getLinkedTilesAs(tile.block, tmpTiles).contains(t -> path.contains(t.pos()))){
return false;
}
}
//make sure at least X% of resource requirements are met
@@ -179,13 +275,18 @@ public class BaseAI{
}
Tile o = world.tile(tile.x + p.x, tile.y + p.y);
if(o != null && (o.block() instanceof PayloadAcceptor || o.block() instanceof PayloadConveyor)){
break;
}
if(o != null && o.team() == data.team && !(o.block() instanceof Wall)){
any = true;
break;
}
}
if(any && Build.validPlace(wall, data.team, tile.x, tile.y, 0)){
tmpTiles.clear();
if(any && Build.validPlace(wall, data.team, tile.x, tile.y, 0) && !tile.getLinkedTilesAs(wall, tmpTiles).contains(t -> path.contains(t.pos()))){
data.blocks.add(new BlockPlan(tile.x, tile.y, (short)0, wall.id, null));
}
}

View File

@@ -3,13 +3,13 @@ package mindustry.ai;
import arc.*;
import arc.math.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.game.Schematic.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.production.*;
import mindustry.world.blocks.sandbox.*;
import mindustry.world.blocks.storage.*;
@@ -17,12 +17,16 @@ import mindustry.world.meta.*;
import java.io.*;
import static mindustry.Vars.tilesize;
import static mindustry.Vars.*;
public class BaseRegistry{
/** cores, sorted by tier */
public Seq<BasePart> cores = new Seq<>();
/** parts with no requirement */
public Seq<BasePart> parts = new Seq<>();
public ObjectMap<Content, Seq<BasePart>> reqParts = new ObjectMap<>();
public ObjectMap<Item, OreBlock> ores = new ObjectMap<>();
public ObjectMap<Item, Floor> oreFloors = new ObjectMap<>();
public Seq<BasePart> forResource(Content item){
return reqParts.get(item, Seq::new);
@@ -33,6 +37,15 @@ public class BaseRegistry{
parts.clear();
reqParts.clear();
//load ore types and corresponding items
for(Block block : content.blocks()){
if(block instanceof OreBlock && block.asFloor().itemDrop != null){
ores.put(block.asFloor().itemDrop, (OreBlock)block);
}else if(block.isFloor() && block.asFloor().itemDrop != null && !oreFloors.containsKey(block.asFloor().itemDrop)){
oreFloors.put(block.asFloor().itemDrop, block.asFloor());
}
}
String[] names = Core.files.internal("basepartnames").readString().split("\n");
for(String name : names){
@@ -69,7 +82,7 @@ public class BaseRegistry{
}
schem.tiles.removeAll(s -> s.block.buildVisibility == BuildVisibility.sandboxOnly);
part.tier = schem.tiles.sumf(s -> Mathf.pow(s.block.buildCost / s.block.buildCostMultiplier, 1.2f));
part.tier = schem.tiles.sumf(s -> Mathf.pow(s.block.buildCost / s.block.buildCostMultiplier, 1.4f));
if(part.core != null){
cores.add(part);
@@ -86,7 +99,9 @@ public class BaseRegistry{
part.centerY = part.schematic.height/2;
}
if(part.required != null) reqParts.get(part.required, Seq::new).add(part);
if(part.required != null && part.core == null){
reqParts.get(part.required, Seq::new).add(part);
}
}catch(IOException e){
throw new RuntimeException(e);

View File

@@ -6,10 +6,12 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.EnumSet;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
@@ -54,18 +56,7 @@ public class BlockIndexer{
public BlockIndexer(){
Events.on(TileChangeEvent.class, event -> {
if(typeMap.get(event.tile.pos()) != null){
TileIndex index = typeMap.get(event.tile.pos());
for(BlockFlag flag : index.flags){
getFlagged(index.team)[flag.ordinal()].remove(event.tile);
}
if(index.flags.contains(BlockFlag.unitModifier)){
updateCap(index.team);
}
}
process(event.tile);
updateQuadrant(event.tile);
updateIndices(event.tile);
});
Events.on(WorldLoadEvent.class, event -> {
@@ -109,6 +100,21 @@ public class BlockIndexer{
});
}
public void updateIndices(Tile tile){
if(typeMap.get(tile.pos()) != null){
TileIndex index = typeMap.get(tile.pos());
for(BlockFlag flag : index.flags){
getFlagged(index.team)[flag.ordinal()].remove(tile);
}
if(index.flags.contains(BlockFlag.unitModifier)){
updateCap(index.team);
}
}
process(tile);
updateQuadrant(tile);
}
private TileArray[] getFlagged(Team team){
return flagMap[team.id];
}
@@ -178,8 +184,8 @@ public class BlockIndexer{
public boolean eachBlock(Team team, float wx, float wy, float range, Boolf<Building> pred, Cons<Building> cons){
intSet.clear();
int tx = world.toTile(wx);
int ty = world.toTile(wy);
int tx = World.toTile(wx);
int ty = World.toTile(wy);
int tileRange = (int)(range / tilesize + 1);
boolean any = false;
@@ -192,7 +198,7 @@ public class BlockIndexer{
if(other == null) continue;
if(other.team == team && pred.get(other) && intSet.add(other.pos())){
if((team == null || other.team == team) && pred.get(other) && intSet.add(other.pos())){
cons.get(other);
any = true;
}
@@ -205,8 +211,22 @@ public class BlockIndexer{
/** Get all enemy blocks with a flag. */
public Seq<Tile> getEnemy(Team team, BlockFlag type){
returnArray.clear();
for(Team enemy : team.enemies()){
if(state.teams.isActive(enemy)){
Seq<TeamData> data = state.teams.present;
//when team data is not initialized, scan through every team. this is terrible
if(data.isEmpty()){
for(Team enemy : Team.all){
if(enemy == team) continue;
TileArray set = getFlagged(enemy)[type.ordinal()];
if(set != null){
for(Tile tile : set){
returnArray.add(tile);
}
}
}
}else{
for(int i = 0; i < data.size; i++){
Team enemy = data.items[i].team;
if(enemy == team) continue;
TileArray set = getFlagged(enemy)[type.ordinal()];
if(set != null){
for(Tile tile : set){
@@ -215,12 +235,13 @@ public class BlockIndexer{
}
}
}
return returnArray;
}
public void notifyTileDamaged(Building entity){
if(damagedTiles[entity.team.id] == null){
damagedTiles[entity.team.id] = new ObjectSet<Building>();
damagedTiles[entity.team.id] = new ObjectSet<>();
}
damagedTiles[entity.team.id].add(entity);
@@ -230,7 +251,7 @@ public class BlockIndexer{
for(int i = 0; i < activeTeams.size; i++){
Team enemy = activeTeams.items[i];
if(enemy == team) continue;
if(enemy == team || team == Team.derelict) continue;
Building entity = indexer.findTile(enemy, x, y, range, pred, true);
if(entity != null){
@@ -259,10 +280,7 @@ public class BlockIndexer{
for(int ty = ry * quadrantSize; ty < (ry + 1) * quadrantSize && ty < world.height(); ty++){
Building e = world.build(tx, ty);
if(e == null) continue;
if(e.team != team || !pred.get(e) || !e.block.targetable)
continue;
if(e == null || e.team != team || !pred.get(e) || !e.block.targetable || e.team == Team.derelict) continue;
float ndst = e.dst2(x, y);
if(ndst < range2 && (closest == null ||
@@ -309,6 +327,11 @@ public class BlockIndexer{
return null;
}
/** Find the closest ore block relative to a position. */
public Tile findClosestOre(Unit unit, Item item){
return findClosestOre(unit.x, unit.y, item);
}
/** @return extra unit cap of a team. This is added onto the base value. */
public int getExtraUnits(Team team){
return unitCaps[team.id];
@@ -454,8 +477,8 @@ public class BlockIndexer{
}
public static class TileArray implements Iterable<Tile>{
private Seq<Tile> tiles = new Seq<>(false, 16);
private IntSet contained = new IntSet();
Seq<Tile> tiles = new Seq<>(false, 16);
IntSet contained = new IntSet();
public void add(Tile tile){
if(contained.add(tile.pos())){

View File

@@ -4,20 +4,22 @@ import arc.*;
import arc.func.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(6);
private static final long maxUpdate = Time.millisToNanos(7);
private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS;
private static final int impassable = -1;
@@ -35,7 +37,7 @@ public class Pathfinder implements Runnable{
public static final int
costGround = 0,
costLegs = 1,
costWater = 2;
costNaval = 2;
public static final Seq<PathCost> costTypes = Seq.with(
//ground
@@ -43,7 +45,7 @@ public class Pathfinder implements Runnable{
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 70 : 0) +
(PathTile.deep(tile) ? 6000 : 0) +
(PathTile.damages(tile) ? 30 : 0),
//legs
@@ -51,7 +53,7 @@ public class Pathfinder implements Runnable{
(PathTile.solid(tile) ? 5 : 0),
//water
(team, tile) -> PathTile.solid(tile) || !PathTile.liquid(tile) ? 200 : 2 + //TODO cannot go through blocks - pathfinding isn't great
(team, tile) -> PathTile.solid(tile) || !PathTile.liquid(tile) ? 200 : 2 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? -1 : 0) +
(PathTile.damages(tile) ? 35 : 0)
@@ -86,9 +88,13 @@ public class Pathfinder implements Runnable{
tiles[tile.x][tile.y] = packTile(tile);
}
//special preset which may help speed things up; this is optional
preloadPath(getField(state.rules.waveTeam, costGround, fieldCore));
//preload water on naval maps
if(spawner.getSpawns().contains(t -> t.floor().isLiquid)){
preloadPath(getField(state.rules.waveTeam, costNaval, fieldCore));
}
start();
});
@@ -103,11 +109,10 @@ public class Pathfinder implements Runnable{
/** Packs a tile into its internal representation. */
private int packTile(Tile tile){
//TODO nearGround is just the inverse of nearLiquid?
boolean nearLiquid = false, nearSolid = false, nearGround = false;
for(int i = 0; i < 4; i++){
Tile other = tile.getNearby(i);
Tile other = tile.nearby(i);
if(other != null){
if(other.floor().isLiquid) nearLiquid = true;
if(other.solid()) nearSolid = true;
@@ -116,11 +121,11 @@ public class Pathfinder implements Runnable{
}
return PathTile.get(
tile.build == null ? 0 : Math.min((int)(tile.build.health / 40), 127),
tile.build == null || !tile.solid() || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tile.getTeamID(),
tile.solid(),
tile.floor().isLiquid,
tile.staticDarkness() >= 2,
tile.staticDarkness() >= 2 || (tile.floor().solid && tile.block() == Blocks.air),
nearLiquid,
nearGround,
nearSolid,
@@ -188,6 +193,8 @@ public class Pathfinder implements Runnable{
for(Flowfield data : threadList){
updateFrontier(data, maxUpdate / threadList.size);
//TODO implement timeouts... or don't
/*
//remove flowfields that have 'timed out' so they can be garbage collected and no longer waste space
if(data.refreshRate > 0 && Time.timeSinceMillis(data.lastUpdateTime) > fieldTimeout){
//make sure it doesn't get removed twice
@@ -196,12 +203,11 @@ public class Pathfinder implements Runnable{
Team team = data.team;
Core.app.post(() -> {
//TODO ?????
//remove its used state
//if(fieldMap[team.id] != null){
// fieldMap[team.id].remove(data.target);
// fieldMapUsed[team.id].remove(data.target);
//}
if(fieldMap[team.id] != null){
fieldMap[team.id].remove(data.target);
fieldMapUsed[team.id].remove(data.target);
}
//remove from main thread list
mainList.remove(data);
});
@@ -210,7 +216,7 @@ public class Pathfinder implements Runnable{
//remove from this thread list with a delay
threadList.remove(data);
});
}
}*/
}
}
@@ -353,13 +359,7 @@ public class Pathfinder implements Runnable{
threadList.add(path);
//add to main thread's list of paths
Core.app.post(() -> {
mainList.add(path);
//TODO
//if(fieldMap[team.id] != null){
// fieldMap[team.id].put(target, path);
//}
});
Core.app.post(() -> mainList.add(path));
//fill with impassables by default
for(int x = 0; x < world.width(); x++){
@@ -445,16 +445,15 @@ public class Pathfinder implements Runnable{
@Override
public void getPositions(IntSeq out){
out.add(Point2.pack(world.toTile(position.getX()), world.toTile(position.getY())));
out.add(Point2.pack(World.toTile(position.getX()), World.toTile(position.getY())));
}
}
/**
* Data for a flow field to some set of destinations.
* Concrete subclasses must specify a way to fetch costs and destinations.
* */
static abstract class Flowfield{
*/
public static abstract class Flowfield{
/** Refresh rate in milliseconds. Return any number <= 0 to disable. */
protected int refreshRate;
/** Team this path is for. Set before using. */
@@ -463,13 +462,13 @@ public class Pathfinder implements Runnable{
protected PathCost cost = costTypes.get(costGround);
/** costs of getting to a specific tile */
int[][] weights;
public int[][] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
int[][] searches;
public int[][] searches;
/** search frontier, these are Pos objects */
IntQueue frontier = new IntQueue();
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
IntSeq targets = new IntSeq();
final IntSeq targets = new IntSeq();
/** current search ID */
int search = 1;
/** last updated time */

View File

@@ -8,6 +8,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
@@ -23,11 +24,21 @@ public class WaveSpawner{
private Seq<Tile> spawns = new Seq<>();
private boolean spawning = false;
private boolean any = false;
private Tile firstSpawn = null;
public WaveSpawner(){
Events.on(WorldLoadEvent.class, e -> reset());
}
@Nullable
public Tile getFirstSpawn(){
firstSpawn = null;
eachGroundSpawn((cx, cy) -> {
firstSpawn = world.tile(cx, cy);
});
return firstSpawn;
}
public int countSpawns(){
return spawns.size;
}
@@ -38,16 +49,22 @@ public class WaveSpawner{
/** @return true if the player is near a ground spawn point. */
public boolean playerNear(){
return !player.dead() && spawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius && player.team() != state.rules.waveTeam);
return state.hasSpawns() && !player.dead() && spawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius && player.team() != state.rules.waveTeam);
}
public void spawnEnemies(){
spawning = true;
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
if(doShockwave){
doShockwave(spawnX, spawnY);
}
});
for(SpawnGroup group : state.rules.spawns){
if(group.type == null) continue;
int spawned = group.getUnitsSpawned(state.wave - 1);
int spawned = group.getSpawned(state.wave - 1);
if(group.type.flying){
float spread = margin / 1.5f;
@@ -69,25 +86,29 @@ public class WaveSpawner{
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
Time.run(Math.min(i * 5, 60 * 2), () -> spawnEffect(unit));
spawnEffect(unit);
}
});
}
}
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
if(doShockwave){
Time.run(20f, () -> Fx.spawnShockwave.at(spawnX, spawnY, state.rules.dropZoneRadius));
Time.run(40f, () -> Damage.damage(state.rules.waveTeam, spawnX, spawnY, state.rules.dropZoneRadius, 99999999f, true));
}
});
Time.runTask(121f, () -> spawning = false);
}
public void doShockwave(float x, float y){
Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius);
Damage.damage(state.rules.waveTeam, x, y, state.rules.dropZoneRadius, 99999999f, true);
}
public void eachGroundSpawn(Intc2 cons){
eachGroundSpawn((x, y, shock) -> cons.get(World.toTile(x), World.toTile(y)));
}
private void eachGroundSpawn(SpawnConsumer cons){
for(Tile spawn : spawns){
cons.accept(spawn.worldx(), spawn.worldy(), true);
if(state.hasSpawns()){
for(Tile spawn : spawns){
cons.accept(spawn.worldx(), spawn.worldy(), true);
}
}
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam) && !state.teams.playerCores().isEmpty()){
@@ -100,7 +121,7 @@ public class WaveSpawner{
//keep moving forward until the max step amount is reached
while(steps++ < maxSteps){
int tx = world.toTile(core.x + Tmp.v1.x), ty = world.toTile(core.y + Tmp.v1.y);
int tx = World.toTile(core.x + Tmp.v1.x), ty = World.toTile(core.y + Tmp.v1.y);
any = false;
Geometry.circle(tx, ty, world.width(), world.height(), 3, (x, y) -> {
if(world.solid(x, y)){
@@ -157,7 +178,7 @@ public class WaveSpawner{
}
private void spawnEffect(Unit unit){
Call.spawnEffect(unit.x, unit.y, unit.type());
Call.spawnEffect(unit.x, unit.y, unit.type);
Time.run(30f, unit::add);
}

View File

@@ -17,10 +17,8 @@ import arc.struct.*;
* @author davebaol
*/
public class Formation{
/** A list of slots assignments. */
public Seq<SlotAssignment> slotAssignments;
/** The anchor point of this formation. */
public Vec3 anchor;
/** The formation pattern */
@@ -138,7 +136,6 @@ public class Formation{
* @return {@code false} if no more slots are available; {@code true} otherwise.
*/
public boolean addMember(FormationMember member){
// Check if the pattern supports one more slot
if(pattern.supportsSlots(slotAssignments.size + 1)){
// Add a new slot assignment

View File

@@ -16,8 +16,9 @@ public class FreeSlotAssignmentStrategy implements SlotAssignmentStrategy{
public void updateSlotAssignments(Seq<SlotAssignment> assignments){
// A very simple assignment algorithm: we simply go through
// each assignment in the list and assign sequential slot numbers
for(int i = 0; i < assignments.size; i++)
for(int i = 0; i < assignments.size; i++){
assignments.get(i).slotNumber = i;
}
}
@Override

View File

@@ -51,7 +51,6 @@ public class SoftRoleSlotAssignmentStrategy extends BoundedSlotAssignmentStrateg
@Override
public void updateSlotAssignments(Seq<SlotAssignment> assignments){
// Holds a list of member and slot data for each member.
Seq<MemberAndSlots> memberData = new Seq<>();
@@ -125,7 +124,6 @@ public class SoftRoleSlotAssignmentStrategy extends BoundedSlotAssignmentStrateg
// Some sensible action should be taken, such as reporting to the player.
throw new ArcRuntimeException("SoftRoleSlotAssignmentStrategy cannot find valid slot assignment for member " + memberDatum.member);
}
}
static class CostAndSlot implements Comparable<CostAndSlot>{

View File

@@ -1,26 +0,0 @@
package mindustry.ai.formations.patterns;
import arc.math.geom.*;
import mindustry.ai.formations.*;
public class ArrowFormation extends FormationPattern{
//total triangular numbers
private static final int totalTris = 30;
//triangular number table
private static final int[] triTable = new int[totalTris];
//calculat triangular numbers
static{
int sum = 0;
for(int i = 0; i < totalTris; i++){
triTable[i] = sum;
sum += (i + 1);
}
}
@Override
public Vec3 calculateSlotLocation(Vec3 out, int slot){
//TODO
return out;
}
}

View File

@@ -15,7 +15,7 @@ public class CircleFormation extends FormationPattern{
float radius = spacing / (float)Math.sin(180f / slots * Mathf.degRad);
outLocation.set(Angles.trnsx(angle, radius), Angles.trnsy(angle, radius), angle);
}else{
outLocation.set(0, 0, 360f * slotNumber);
outLocation.set(0, spacing * 1.1f, 360f * slotNumber);
}
outLocation.z += angleOffset;

View File

@@ -1,7 +1,8 @@
package mindustry.ai.types;
import arc.math.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.game.Teams.*;
@@ -14,17 +15,16 @@ import static mindustry.Vars.*;
public class BuilderAI extends AIController{
float buildRadius = 1500;
boolean found = false;
@Nullable Builderc following;
@Nullable Unit following;
@Override
public void updateUnit(){
Builderc builder = (Builderc)unit;
public void updateMovement(){
if(builder.moving()){
builder.lookAt(builder.vel().angle());
if(target != null && shouldShoot()){
unit.lookAt(target);
}
builder.updateBuilding(true);
unit.updateBuilding = true;
if(following != null){
//try to follow and mimic someone
@@ -32,21 +32,31 @@ public class BuilderAI extends AIController{
//validate follower
if(!following.isValid() || !following.activelyBuilding()){
following = null;
builder.plans().clear();
unit.plans.clear();
return;
}
//set to follower's first build plan, whatever that is
builder.plans().clear();
builder.plans().addFirst(following.buildPlan());
unit.plans.clear();
unit.plans.addFirst(following.buildPlan());
}
if(builder.buildPlan() != null){
if(unit.buildPlan() != null){
//approach request if building
BuildPlan req = builder.buildPlan();
BuildPlan req = unit.buildPlan();
//clear break plan if another player is breaking something.
if(!req.breaking && timer.get(timerTarget2, 40f)){
for(Player player : Groups.player){
if(player.isBuilder() && player.unit().activelyBuilding() && player.unit().buildPlan().samePos(req) && player.unit().buildPlan().breaking){
unit.plans.removeFirst();
return;
}
}
}
boolean valid =
(req.tile().build instanceof ConstructBuild && req.tile().<ConstructBuild>bc().cblock == req.block) ||
(req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.cblock == req.block) ||
(req.breaking ?
Build.validBreak(unit.team(), req.x, req.y) :
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation));
@@ -56,7 +66,7 @@ public class BuilderAI extends AIController{
moveTo(req.tile(), buildingRange - 20f);
}else{
//discard invalid request
builder.plans().removeFirst();
unit.plans.removeFirst();
}
}else{
@@ -67,18 +77,16 @@ public class BuilderAI extends AIController{
Units.nearby(unit.team, unit.x, unit.y, buildRadius, u -> {
if(found) return;
if(u instanceof Builderc && u != unit && ((Builderc)u).activelyBuilding()){
Builderc b = (Builderc)u;
BuildPlan plan = b.buildPlan();
if(u.canBuild() && u != unit && u.activelyBuilding()){
BuildPlan plan = u.buildPlan();
Building build = world.build(plan.x, plan.y);
if(build instanceof ConstructBuild){
ConstructBuild cons = (ConstructBuild)build;
if(build instanceof ConstructBuild cons){
float dist = Math.min(cons.dst(unit) - buildingRange, 0);
//make sure you can reach the request in time
if(dist / unit.type().speed < cons.buildCost * 0.9f){
following = b;
if(dist / unit.speed() < cons.buildCost * 0.9f){
following = u;
found = true;
}
}
@@ -86,9 +94,11 @@ public class BuilderAI extends AIController{
});
}
float rebuildTime = (unit.team.rules().ai ? Mathf.lerp(15f, 2f, unit.team.rules().aiTier) : 2f) * 60f;
//find new request
if(!unit.team().data().blocks.isEmpty() && following == null && timer.get(timerTarget3, 60 * 2f)){
Queue<BlockPlan> blocks = unit.team().data().blocks;
if(!unit.team.data().blocks.isEmpty() && following == null && timer.get(timerTarget3, rebuildTime)){
Queue<BlockPlan> blocks = unit.team.data().blocks;
BlockPlan block = blocks.first();
//check if it's already been placed
@@ -96,14 +106,30 @@ public class BuilderAI extends AIController{
blocks.removeFirst();
}else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation)){ //it's valid.
//add build request.
builder.addBuild(new BuildPlan(block.x, block.y, block.rotation, content.block(block.block), block.config));
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, content.block(block.block), block.config));
//shift build plan to tail so next unit builds something else.
blocks.addLast(blocks.removeFirst());
}else{
//shift head of queue to tail, try something else next time
blocks.removeFirst();
blocks.addLast(block);
}
}
}
}
@Override
public AIController fallback(){
return unit.type.flying ? new FlyingAI() : new GroundAI();
}
@Override
public boolean useFallback(){
return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().ai;
}
@Override
public boolean shouldShoot(){
return !unit.isBuilding();
}
}

View File

@@ -12,11 +12,11 @@ public class FlyingAI extends AIController{
@Override
public void updateMovement(){
if(target != null && unit.hasWeapons() && command() == UnitCommand.attack){
if(unit.type().weapons.first().rotate){
if(!unit.type.circleTarget){
moveTo(target, unit.range() * 0.8f);
unit.lookAt(target);
}else{
attack(80f);
attack(120f);
}
}
@@ -34,17 +34,15 @@ public class FlyingAI extends AIController{
Teamc result = target(x, y, range, air, ground);
if(result != null) return result;
if(ground) result = targetFlag(x, y, BlockFlag.producer, true);
if(ground) result = targetFlag(x, y, BlockFlag.generator, true);
if(result != null) return result;
if(ground) result = targetFlag(x, y, BlockFlag.turret, true);
if(ground) result = targetFlag(x, y, BlockFlag.core, true);
if(result != null) return result;
return null;
}
//TODO clean up
protected void attack(float circleLength){
vec.set(target).sub(unit);
@@ -57,7 +55,7 @@ public class FlyingAI extends AIController{
vec.setAngle(Mathf.slerpDelta(unit.vel().angle(), vec.angle(), 0.6f));
}
vec.setLength(unit.type().speed);
vec.setLength(unit.speed());
unit.moveAt(vec);
}

View File

@@ -2,11 +2,11 @@ package mindustry.ai.types;
import arc.math.*;
import arc.math.geom.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.ai.formations.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.storage.CoreBlock.*;
public class FormationAI extends AIController implements FormationMember{
public Unit leader;
@@ -26,36 +26,52 @@ public class FormationAI extends AIController implements FormationMember{
@Override
public void updateUnit(){
UnitType type = unit.type();
if(leader.dead){
if(leader == null || leader.dead){
unit.resetController();
return;
}
if(unit.type().canBoost && unit.canPassOn()){
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, 0.08f);
if(unit.type.canBoost){
unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() ? 1f : leader.type.canBoost ? leader.elevation : 0f, 0.08f);
}
unit.controlWeapons(true, leader.isShooting);
// unit.moveAt(Tmp.v1.set(deltaX, deltaY).limit(unit.type().speed));
unit.aim(leader.aimX(), leader.aimY());
if(unit.type().rotateShooting){
if(unit.type.rotateShooting){
unit.lookAt(leader.aimX(), leader.aimY());
}else if(unit.moving()){
unit.lookAt(unit.vel.angle());
}
Vec2 realtarget = vec.set(target);
Vec2 realtarget = vec.set(target).add(leader.vel.x, leader.vel.y);
float margin = 3f;
float speed = unit.realSpeed() * unit.floorSpeedMultiplier() * Time.delta;
unit.approach(Mathf.arrive(unit.x, unit.y, realtarget.x, realtarget.y, unit.vel, speed, 0f, speed, 1f).scl(1f / Time.delta));
if(unit.dst(realtarget) <= margin){
unit.vel.approachDelta(Vec2.ZERO, type.speed * type.accel / 2f);
}else{
unit.moveAt(realtarget.sub(unit).limit(type.speed));
if(unit.canMine() && leader.canMine()){
if(leader.mineTile != null && unit.validMine(leader.mineTile)){
unit.mineTile(leader.mineTile);
CoreBuild core = unit.team.core();
if(core != null && leader.mineTile.drop() != null && unit.within(core, unit.type.range) && !unit.acceptsItem(leader.mineTile.drop())){
if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){
Call.transferItemTo(unit, unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
unit.clearItem();
}
}
}else{
unit.mineTile(null);
}
}
if(unit.canBuild() && leader.canBuild() && leader.activelyBuilding()){
unit.clearBuilding();
unit.addBuild(leader.buildPlan());
}
}
@@ -69,11 +85,7 @@ public class FormationAI extends AIController implements FormationMember{
@Override
public float formationSize(){
if(unit instanceof Commanderc && ((Commanderc)unit).isCommanding()){
//TODO return formation size
//eturn ((Commanderc)unit).formation().
}
return unit.hitSize * 1f;
return unit.hitSize * 1.1f;
}
@Override

View File

@@ -13,8 +13,6 @@ import java.util.*;
import static mindustry.Vars.*;
public class GroundAI extends AIController{
//static final float commandCooldown = 60f * 10;
//float commandTimer = 60*3;
@Override
public void updateMovement(){
@@ -34,55 +32,28 @@ public class GroundAI extends AIController{
if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)) move = false;
}
if(move) moveTo(Pathfinder.fieldCore);
if(move) pathfind(Pathfinder.fieldCore);
}
if(command() == UnitCommand.rally){
Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false);
if(target != null && !unit.within(target, 70f)){
moveTo(Pathfinder.fieldRally);
pathfind(Pathfinder.fieldRally);
}
}
if(unit.type().canBoost && unit.canPassOn()){
if(unit.type.canBoost && !unit.onSolid()){
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, 0.08f);
}
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type().rotateShooting){
if(unit.type().hasWeapons()){
//TODO certain units should not look at the target, e.g. ships
unit.lookAt(Predict.intercept(unit, target, unit.type().weapons.first().bullet.speed));
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type.rotateShooting){
if(unit.type.hasWeapons()){
unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
}
}else if(unit.moving()){
unit.lookAt(unit.vel().angle());
}
//auto-command works but it's very buggy
/*
if(unit instanceof Commanderc){
Commanderc c = (Commanderc)unit;
//try to command when missing members
if(c.controlling().size <= unit.type().commandLimit/2){
commandTimer -= Time.delta;
if(commandTimer <= 0){
c.commandNearby(new SquareFormation(), u -> !(u.controller() instanceof FormationAI) && !(u instanceof Commanderc));
commandTimer = commandCooldown;
}
}
}*/
}
protected void moveTo(int pathTarget){
int costType = unit.pathType();
Tile tile = unit.tileOn();
if(tile == null) return;
Tile targetTile = pathfinder.getTargetTile(tile, pathfinder.getField(unit.team, costType, pathTarget));
if(tile == targetTile || (costType == Pathfinder.costWater && !targetTile.floor().isLiquid)) return;
unit.moveAt(vec.trns(unit.angleTo(targetTile), unit.type().speed));
}
}

View File

@@ -0,0 +1,159 @@
package mindustry.ai.types;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.world.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class LogicAI extends AIController{
/** Minimum delay between item transfers. */
public static final float transferDelay = 60f * 2f;
/** Time after which the unit resets its controlled and reverts to a normal unit. */
public static final float logicControlTimeout = 10f * 60f;
public LUnitControl control = LUnitControl.stop;
public float moveX, moveY, moveRad;
public float itemTimer, payTimer, controlTimer = logicControlTimeout, targetTimer;
@Nullable
public Building controller;
public BuildPlan plan = new BuildPlan();
//special cache for instruction to store data
public ObjectMap<Object, Object> execCache = new ObjectMap<>();
//type of aiming to use
public LUnitControl aimControl = LUnitControl.stop;
//whether to use the boost (certain units only)
public boolean boost;
//main target set for shootP
public Teamc mainTarget;
//whether to shoot at all
public boolean shoot;
//target shoot positions for manual aiming
public PosTeam posTarget = PosTeam.create();
private ObjectSet<Object> radars = new ObjectSet<>();
@Override
protected void updateMovement(){
if(itemTimer >= 0) itemTimer -= Time.delta;
if(payTimer >= 0) payTimer -= Time.delta;
if(targetTimer > 0f){
targetTimer -= Time.delta;
}else{
radars.clear();
targetTimer = 40f;
}
//timeout when not controlled by logic for a while
if(controlTimer > 0 && controller != null && controller.isValid()){
controlTimer -= Time.delta;
}else{
unit.resetController();
return;
}
switch(control){
case move -> {
moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f);
}
case approach -> {
moveTo(Tmp.v1.set(moveX, moveY), moveRad - 7f, 7);
}
case pathfind -> {
Building core = unit.closestEnemyCore();
if((core == null || !unit.within(core, unit.range() * 0.5f)) && command() == UnitCommand.attack){
boolean move = true;
if(state.rules.waves && unit.team == state.rules.defaultTeam){
Tile spawner = getClosestSpawner();
if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)) move = false;
}
if(move) pathfind(Pathfinder.fieldCore);
}
if(command() == UnitCommand.rally){
Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false);
if(target != null && !unit.within(target, 70f)){
pathfind(Pathfinder.fieldRally);
}
}
}
case stop -> {
unit.clearBuilding();
}
}
if(unit.type.canBoost && !unit.type.flying){
unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid()), 0.08f);
}
//look where moving if there's nothing to aim at
if(!shoot){
unit.lookAt(unit.prefRotation());
}else if(unit.hasWeapons() && unit.mounts.length > 0){ //if there is, look at the object
unit.lookAt(unit.mounts[0].aimX, unit.mounts[0].aimY);
}
}
public boolean checkTargetTimer(Object radar){
return radars.add(radar);
}
@Override
protected void moveTo(Position target, float circleLength, float smooth){
if(target == null) return;
vec.set(target).sub(unit);
float length = circleLength <= 0.001f ? 1f : Mathf.clamp((unit.dst(target) - circleLength) / smooth, -1f, 1f);
vec.setLength(unit.realSpeed() * length);
if(length < -0.5f){
vec.rotate(180f);
}else if(length < 0){
vec.setZero();
}
unit.approach(vec);
}
//always retarget
@Override
protected boolean retarget(){
return true;
}
@Override
protected boolean invalid(Teamc target){
return false;
}
@Override
protected boolean shouldShoot(){
return shoot && !(unit.type.canBoost && boost);
}
//always aim for the main target
@Override
protected Teamc target(float x, float y, float range, boolean air, boolean ground){
return switch(aimControl){
case target -> posTarget;
case targetp -> mainTarget;
default -> null;
};
}
}

View File

@@ -17,37 +17,37 @@ public class MinerAI extends AIController{
protected void updateMovement(){
Building core = unit.closestCore();
if(!(unit instanceof Minerc) || core == null) return;
if(!(unit.canMine()) || core == null) return;
Minerc miner = (Minerc)unit;
if(miner.mineTile() != null && !miner.mineTile().within(unit, unit.type().range)){
miner.mineTile(null);
if(unit.mineTile != null && !unit.mineTile.within(unit, unit.type.range)){
unit.mineTile(null);
}
if(mining){
targetItem = unit.team.data().mineItems.min(i -> indexer.hasOre(i) && miner.canMine(i), i -> core.items.get(i));
if(timer.get(timerTarget2, 60 * 4) || targetItem == null){
targetItem = unit.team.data().mineItems.min(i -> indexer.hasOre(i) && unit.canMine(i), i -> core.items.get(i));
}
//core full of the target item, do nothing
if(targetItem != null && core.acceptStack(targetItem, 1, unit) == 0){
unit.clearItem();
miner.mineTile(null);
unit.mineTile(null);
return;
}
//if inventory is full, drop it off.
if(unit.stack.amount >= unit.type().itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
if(unit.stack.amount >= unit.type.itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
mining = false;
}else{
if(retarget() && targetItem != null){
ore = indexer.findClosestOre(unit.x, unit.y, targetItem);
if(timer.get(timerTarget, 60) && targetItem != null){
ore = indexer.findClosestOre(unit, targetItem);
}
if(ore != null){
moveTo(ore, unit.type().range / 2f);
moveTo(ore, unit.type.range / 2f, 20f);
if(unit.within(ore, unit.type().range)){
miner.mineTile(ore);
if(unit.within(ore, unit.type.range)){
unit.mineTile = ore;
}
if(ore.block() != Blocks.air){
@@ -56,28 +56,27 @@ public class MinerAI extends AIController{
}
}
}else{
miner.mineTile(null);
unit.mineTile = null;
if(unit.stack.amount == 0){
mining = true;
return;
}
if(unit.within(core, unit.type().range)){
if(unit.within(core, unit.type.range)){
if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){
Call.transferItemTo(unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
Call.transferItemTo(unit, unit.stack.item, unit.stack.amount, unit.x, unit.y, core);
}
unit.clearItem();
mining = true;
}
circle(core, unit.type().range / 1.8f);
circle(core, unit.type.range / 1.8f);
}
}
@Override
protected void updateTargeting(){
}
}

View File

@@ -2,33 +2,45 @@ package mindustry.ai.types;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.world.blocks.ConstructBlock.*;
//note that repair AI doesn't attack anything even if it theoretically can
public class RepairAI extends AIController{
@Override
protected void updateMovement(){
boolean shoot = false;
if(target instanceof Building){
boolean shoot = false;
if(target != null){
if(!target.within(unit, unit.type().range * 0.8f)){
moveTo(target, unit.type().range * 0.8f);
}
if(target.within(unit, unit.type().range)){
if(target.within(unit, unit.type.range)){
unit.aim(target);
shoot = true;
}
unit.controlWeapons(shoot);
}else if(target == null){
unit.controlWeapons(false);
}
unit.controlWeapons(shoot);
if(target != null){
if(!target.within(unit, unit.type.range * 0.65f) && target instanceof Building b && b.team == unit.team){
moveTo(target, unit.type.range * 0.65f);
}
unit.lookAt(target);
}
}
@Override
protected void updateTargeting(){
target = Units.findDamagedTile(unit.team, unit.x, unit.y);
Building target = Units.findDamagedTile(unit.team, unit.x, unit.y);
if(target instanceof ConstructBuild) target = null;
if(target == null){
super.updateTargeting();
}else{
this.target = target;
}
}
}

View File

@@ -6,6 +6,8 @@ import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.blocks.distribution.*;
import mindustry.world.blocks.liquid.*;
import mindustry.world.meta.*;
public class SuicideAI extends GroundAI{
@@ -14,52 +16,58 @@ public class SuicideAI extends GroundAI{
@Override
public void updateUnit(){
if(Units.invalidateTarget(target, unit.team(), unit.x(), unit.y(), Float.MAX_VALUE)){
if(Units.invalidateTarget(target, unit.team, unit.x, unit.y, Float.MAX_VALUE)){
target = null;
}
if(retarget()){
target = target(unit.x, unit.y, unit.range(), unit.type().targetAir, unit.type().targetGround);
target = target(unit.x, unit.y, unit.range(), unit.type.targetAir, unit.type.targetGround);
}
Building core = unit.closestEnemyCore();
boolean rotate = false, shoot = false, moveToTarget = false;
if(!Units.invalidateTarget(target, unit, unit.range())){
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.hasWeapons()){
rotate = true;
shoot = unit.within(target, unit.type().weapons.first().bullet.range() +
(target instanceof Building ? ((Building)target).block.size * Vars.tilesize / 2f : ((Hitboxc)target).hitSize() / 2f));
shoot = unit.within(target, unit.type.weapons.first().bullet.range() +
(target instanceof Building b ? b.block.size * Vars.tilesize / 2f : ((Hitboxc)target).hitSize() / 2f));
if(unit.type().hasWeapons()){
unit.aimLook(Predict.intercept(unit, target, unit.type().weapons.first().bullet.speed));
if(unit.type.hasWeapons()){
unit.aimLook(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
}
blockedByBlock = false;
//do not move toward walls or transport blocks
if(!(target instanceof Building build && (
build.block.group == BlockGroup.walls ||
build.block.group == BlockGroup.liquids ||
build.block.group == BlockGroup.transportation
))){
blockedByBlock = false;
//raycast for target
boolean blocked = Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
Tile tile = Vars.world.tile(x, y);
if(tile != null && tile.build == target) return false;
if(tile != null && tile.build != null && tile.build.team != unit.team()){
blockedByBlock = true;
return true;
}else{
return tile == null || tile.solid();
//raycast for target
boolean blocked = Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
Tile tile = Vars.world.tile(x, y);
if(tile != null && tile.build == target) return false;
if(tile != null && tile.build != null && tile.build.team != unit.team()){
blockedByBlock = true;
return true;
}else{
return tile == null || tile.solid();
}
});
//shoot when there's an enemy block in the way
if(blockedByBlock){
shoot = true;
}
});
//shoot when there's an enemy block in the way
if(blockedByBlock){
shoot = true;
if(!blocked){
moveToTarget = true;
//move towards target directly
unit.moveAt(vec.set(target).sub(unit).limit(unit.speed()));
}
}
if(!blocked){
moveToTarget = true;
//move towards target directly
unit.moveAt(vec.set(target).sub(unit).limit(unit.type().speed));
}
}
if(!moveToTarget){
@@ -67,10 +75,10 @@ public class SuicideAI extends GroundAI{
Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false);
if(target != null && !unit.within(target, 70f)){
moveTo(Pathfinder.fieldRally);
pathfind(Pathfinder.fieldRally);
}
}else if(command() == UnitCommand.attack && core != null){
moveTo(Pathfinder.fieldCore);
pathfind(Pathfinder.fieldCore);
}
if(unit.moving()) unit.lookAt(unit.vel().angle());
@@ -78,4 +86,10 @@ public class SuicideAI extends GroundAI{
unit.controlWeapons(rotate, shoot);
}
@Override
protected Teamc target(float x, float y, float range, boolean air, boolean ground){
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground &&
!(t.block instanceof Conveyor || t.block instanceof Conduit)); //do not target conveyors/conduits
}
}

View File

@@ -2,18 +2,16 @@ package mindustry.async;
import arc.*;
import arc.struct.*;
import mindustry.*;
import mindustry.game.EventType.*;
import java.util.concurrent.*;
import static mindustry.Vars.state;
import static mindustry.Vars.*;
public class AsyncCore{
//all processes to be executed each frame
private final Seq<AsyncProcess> processes = Seq.with(
new PhysicsProcess(),
Vars.teamIndex = new TeamIndexProcess()
new PhysicsProcess()
);
//futures to be awaited

View File

@@ -1,31 +1,25 @@
package mindustry.async;
import arc.box2d.*;
import arc.box2d.BodyDef.*;
import arc.math.*;
import arc.math.geom.*;
import arc.math.geom.QuadTree.*;
import arc.struct.*;
import mindustry.*;
import mindustry.async.PhysicsProcess.PhysicsWorld.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class PhysicsProcess implements AsyncProcess{
private Physics physics;
private static final int
layers = 3,
layerGround = 0,
layerLegs = 1,
layerFlying = 2;
private PhysicsWorld physics;
private Seq<PhysicRef> refs = new Seq<>(false);
private BodyDef def;
private EntityGroup<? extends Physicsc> group;
private Filter flying = new Filter(){{
maskBits = categoryBits = 2;
}}, ground = new Filter(){{
maskBits = categoryBits = 1;
}};
public PhysicsProcess(){
def = new BodyDef();
def.type = BodyType.dynamicBody;
//currently only enabled for units
group = Groups.unit;
}
//currently only enabled for units
private EntityGroup<Unit> group = Groups.unit;
@Override
public void begin(){
@@ -34,47 +28,39 @@ public class PhysicsProcess implements AsyncProcess{
//remove stale entities
refs.removeAll(ref -> {
if(!ref.entity.isAdded()){
physics.destroyBody(ref.body);
physics.remove(ref.body);
ref.entity.physref(null);
return true;
}
return false;
});
//find entities without bodies and assign them
for(Physicsc entity : group){
boolean grounded = entity.isGrounded();
int bits = grounded ? ground.maskBits : flying.maskBits;
//find Unit without bodies and assign them
for(Unit entity : group){
if(entity.physref() == null){
//add bodies to entities that have none
FixtureDef fd = new FixtureDef();
fd.shape = new CircleShape(entity.hitSize() / 2f);
fd.density = 5f;
fd.restitution = 0.0f;
fd.filter.maskBits = fd.filter.categoryBits = (grounded ? ground : flying).maskBits;
def.position.set(entity);
Body body = physics.createBody(def);
body.createFixture(fd);
PhysicsBody body = new PhysicsBody();
body.x = entity.x();
body.y = entity.y();
body.mass = entity.mass();
body.radius = entity.hitSize() / 2f;
PhysicRef ref = new PhysicRef(entity, body);
refs.add(ref);
entity.physref(ref);
physics.add(body);
}
//save last position
PhysicRef ref = entity.physref();
if(ref.body.getFixtureList().any() && ref.body.getFixtureList().first().getFilterData().categoryBits != bits){
//set correct filter
ref.body.getFixtureList().first().setFilterData(grounded ? ground : flying);
}
ref.velocity.set(entity.deltaX(), entity.deltaY());
ref.position.set(entity);
ref.body.layer =
entity.type.allowLegStep ? layerLegs :
entity.isGrounded() ? layerGround : layerFlying;
ref.x = entity.x();
ref.y = entity.y();
}
}
@@ -85,26 +71,11 @@ public class PhysicsProcess implements AsyncProcess{
//get last position vectors before step
for(PhysicRef ref : refs){
//force set target position
ref.body.setPosition(ref.position.x, ref.position.y);
//save last position for delta
ref.lastPosition.set(ref.body.getPosition());
//write velocity
ref.body.setLinearVelocity(ref.velocity);
ref.lastVelocity.set(ref.velocity);
ref.body.x = ref.x;
ref.body.y = ref.y;
}
physics.step(1f/45f, 5, 8);
//get delta vectors
for(PhysicRef ref : refs){
//get delta vector
ref.delta.set(ref.body.getPosition()).sub(ref.lastPosition);
ref.velocity.set(ref.body.getLinearVelocity());
}
physics.update();
}
@Override
@@ -115,13 +86,8 @@ public class PhysicsProcess implements AsyncProcess{
for(PhysicRef ref : refs){
Physicsc entity = ref.entity;
entity.move(ref.delta.x, ref.delta.y);
//save last position
ref.position.set(entity);
//add delta velocity - this doesn't work very well yet
//entity.vel().add(ref.velocity).sub(ref.lastVelocity);
//move by delta
entity.move(ref.body.x - ref.x, ref.body.y - ref.y);
}
}
@@ -129,7 +95,6 @@ public class PhysicsProcess implements AsyncProcess{
public void reset(){
if(physics != null){
refs.clear();
physics.dispose();
physics = null;
}
}
@@ -138,17 +103,95 @@ public class PhysicsProcess implements AsyncProcess{
public void init(){
reset();
physics = new Physics(new Vec2(), true);
physics = new PhysicsWorld(Vars.world.getQuadBounds(new Rect()));
}
public static class PhysicRef{
public Physicsc entity;
public Body body;
public Vec2 lastPosition = new Vec2(), delta = new Vec2(), velocity = new Vec2(), lastVelocity = new Vec2(), position = new Vec2();
public PhysicsBody body;
public float x, y;
public PhysicRef(Physicsc entity, Body body){
public PhysicRef(Physicsc entity, PhysicsBody body){
this.entity = entity;
this.body = body;
}
}
//world for simulating physics in a different thread
public static class PhysicsWorld{
//how much to soften movement by
private static final float scl = 1.25f;
private final QuadTree<PhysicsBody>[] trees = new QuadTree[layers];
private final Seq<PhysicsBody> bodies = new Seq<>(false, 16, PhysicsBody.class);
private final Seq<PhysicsBody> seq = new Seq<>(PhysicsBody.class);
private final Rect rect = new Rect();
private final Vec2 vec = new Vec2();
public PhysicsWorld(Rect bounds){
for(int i = 0; i < layers; i++){
trees[i] = new QuadTree<>(new Rect(bounds));
}
}
public void add(PhysicsBody body){
bodies.add(body);
}
public void remove(PhysicsBody body){
bodies.remove(body);
}
public void update(){
for(int i = 0; i < layers; i++){
trees[i].clear();
}
for(int i = 0; i < bodies.size; i++){
PhysicsBody body = bodies.items[i];
body.collided = false;
trees[body.layer].insert(body);
}
for(int i = 0; i < bodies.size; i++){
PhysicsBody body = bodies.items[i];
body.hitbox(rect);
seq.size = 0;
trees[body.layer].intersect(rect, seq);
for(int j = 0; j < seq.size; j++){
PhysicsBody other = seq.items[j];
if(other == body || other.collided) continue;
float rs = body.radius + other.radius;
float dst = Mathf.dst(body.x, body.y, other.x, other.y);
if(dst < rs){
vec.set(body.x - other.x, body.y - other.y).setLength(rs - dst);
float ms = body.mass + other.mass;
float m1 = other.mass / ms, m2 = body.mass / ms;
body.x += vec.x * m1 / scl;
body.y += vec.y * m1 / scl;
other.x -= vec.x * m2 / scl;
other.y -= vec.y * m2 / scl;
}
}
body.collided = true;
}
}
public static class PhysicsBody implements QuadTreeObject{
public float x, y, radius, mass;
public int layer = 0;
public boolean collided = false;
@Override
public void hitbox(Rect out){
out.setCentered(x, y, radius * 2, radius * 2);
}
}
}
}

View File

@@ -1,82 +0,0 @@
package mindustry.async;
import arc.math.geom.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.payloads.*;
import java.util.*;
/** Creates quadtrees per unit team. */
public class TeamIndexProcess implements AsyncProcess{
private QuadTree<Unit>[] trees = new QuadTree[Team.all.length];
private int[] counts = new int[Team.all.length];
private int[][] typeCounts = new int[Team.all.length][0];
public QuadTree<Unit> tree(Team team){
if(trees[team.id] == null) trees[team.id] = new QuadTree<>(Vars.world.getQuadBounds(new Rect()));
return trees[team.id];
}
public int count(Team team){
return counts[team.id];
}
public int countType(Team team, UnitType type){
return typeCounts[team.id].length <= type.id ? 0 : typeCounts[team.id][type.id];
}
public void updateCount(Team team, UnitType type, int amount){
counts[team.id] += amount;
if(typeCounts[team.id].length <= type.id){
typeCounts[team.id] = new int[Vars.content.units().size];
}
typeCounts[team.id][type.id] += amount;
}
private void count(Unit unit){
updateCount(unit.team, unit.type(), 1);
if(unit instanceof Payloadc){
((Payloadc)unit).payloads().each(p -> {
if(p instanceof UnitPayload){
count(((UnitPayload)p).unit);
}
});
}
}
@Override
public void reset(){
counts = new int[Team.all.length];
trees = new QuadTree[Team.all.length];
}
@Override
public void begin(){
for(Team team : Team.all){
if(trees[team.id] != null){
trees[team.id].clear();
}
Arrays.fill(typeCounts[team.id], 0);
}
Arrays.fill(counts, 0);
for(Unit unit : Groups.unit){
tree(unit.team).insert(unit);
count(unit);
}
}
@Override
public boolean shouldProcess(){
return false;
}
}

View File

@@ -1,61 +0,0 @@
package mindustry.audio;
import arc.*;
import arc.audio.*;
import arc.struct.*;
import arc.math.*;
import arc.math.geom.*;
import mindustry.*;
public class LoopControl{
private ObjectMap<Sound, SoundData> sounds = new ObjectMap<>();
public void play(Sound sound, Position pos, float volume){
if(Vars.headless) return;
float baseVol = sound.calcFalloff(pos.getX(), pos.getY());
float vol = baseVol * volume;
SoundData data = sounds.get(sound, SoundData::new);
data.volume += vol;
data.volume = Mathf.clamp(data.volume, 0f, 1f);
data.total += baseVol;
data.sum.add(pos.getX() * baseVol, pos.getY() * baseVol);
}
public void update(){
float avol = Core.settings.getInt("ambientvol", 100) / 100f;
sounds.each((sound, data) -> {
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.2f);
boolean play = data.curVolume > 0.01f;
float pan = Mathf.zero(data.total, 0.0001f) ? 0f : sound.calcPan(data.sum.x / data.total, data.sum.y / data.total);
if(data.soundID <= 0){
if(play){
data.soundID = sound.loop(data.curVolume, 1f, pan);
}
}else{
if(data.curVolume <= 0.01f){
sound.stop();
data.soundID = -1;
return;
}
sound.setPan(data.soundID, pan, data.curVolume);
}
data.volume = 0f;
data.total = 0f;
data.sum.setZero();
});
}
private static class SoundData{
float volume;
float total;
Vec2 sum = new Vec2();
int soundID;
float curVolume;
}
}

View File

@@ -2,46 +2,99 @@ package mindustry.audio;
import arc.*;
import arc.audio.*;
import arc.audio.Filters.*;
import arc.files.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
/** Controls playback of multiple music tracks.*/
public class MusicControl{
protected static final float finTime = 120f, foutTime = 120f, musicInterval = 60 * 60 * 3f, musicChance = 0.6f, musicWaveChance = 0.5f;
/** Controls playback of multiple audio tracks.*/
public class SoundControl{
protected static final float finTime = 120f, foutTime = 120f, musicInterval = 60 * 60 * 3f, musicChance = 0.6f, musicWaveChance = 0.46f;
/** normal, ambient music, plays at any time */
public Seq<Music> ambientMusic = Seq.with();
/** darker music, used in times of conflict */
public Seq<Music> darkMusic = Seq.with();
/** music used explicitly after boss spawns */
public Seq<Music> bossMusic = Seq.with();
protected Music lastRandomPlayed;
protected Interval timer = new Interval();
protected Interval timer = new Interval(4);
protected @Nullable Music current;
protected float fade;
protected boolean silenced;
public MusicControl(){
protected AudioBus uiBus = new AudioBus();
protected boolean wasPlaying;
protected AudioFilter filter = new BiquadFilter(){{
set(0, 500, 1);
}};
protected ObjectMap<Sound, SoundData> sounds = new ObjectMap<>();
public SoundControl(){
Events.on(ClientLoadEvent.class, e -> reload());
//only run music 10 seconds after a wave spawns
Events.on(WaveEvent.class, e -> Time.run(60f * 10f, () -> {
if(Mathf.chance(musicWaveChance)){
Events.on(WaveEvent.class, e -> Time.run(Mathf.random(8f, 15f) * 60f, () -> {
boolean boss = state.rules.spawns.contains(group -> group.getSpawned(state.wave - 2) > 0 && group.effect == StatusEffects.boss);
if(boss){
playOnce(bossMusic.random(lastRandomPlayed));
}else if(Mathf.chance(musicWaveChance)){
playRandom();
}
}));
setupFilters();
}
protected void setupFilters(){
Core.audio.soundBus.setFilter(0, filter);
Core.audio.soundBus.setFilterParam(0, Filters.paramWet, 0f);
}
protected void reload(){
current = null;
fade = 0f;
ambientMusic = Seq.with(Musics.game1, Musics.game3, Musics.game4, Musics.game6);
darkMusic = Seq.with(Musics.game2, Musics.game5, Musics.game7);
ambientMusic = Seq.with(Musics.game1, Musics.game3, Musics.game6, Musics.game8, Musics.game9);
darkMusic = Seq.with(Musics.game2, Musics.game5, Musics.game7, Musics.game4);
bossMusic = Seq.with(Musics.boss1, Musics.boss2, Musics.game2, Musics.game5);
//setup UI bus for all sounds that are in the UI folder
for(var sound : Core.assets.getAll(Sound.class, new Seq<>())){
var file = Fi.get(Core.assets.getAssetFileName(sound));
if(file.parent().name().equals("ui")){
sound.setBus(uiBus);
}
}
}
public void loop(Sound sound, float volume){
if(Vars.headless) return;
loop(sound, Core.camera.position, volume);
}
public void loop(Sound sound, Position pos, float volume){
if(Vars.headless) return;
float baseVol = sound.calcFalloff(pos.getX(), pos.getY());
float vol = baseVol * volume;
SoundData data = sounds.get(sound, SoundData::new);
data.volume += vol;
data.volume = Mathf.clamp(data.volume, 0f, 1f);
data.total += baseVol;
data.sum.add(pos.getX() * baseVol, pos.getY() * baseVol);
}
public void stop(){
@@ -55,6 +108,32 @@ public class MusicControl{
/** Update and play the right music track.*/
public void update(){
boolean paused = state.isGame() && Core.scene.hasDialog();
boolean playing = state.isGame();
//check if current track is finished
if(current != null && !current.isPlaying()){
current = null;
fade = 0f;
}
//fade the lowpass filter in/out, poll every 30 ticks just in case performance is an issue
if(timer.get(1, 30f)){
Core.audio.soundBus.fadeFilterParam(0, Filters.paramWet, paused ? 1f : 0f, 0.4f);
}
//play/stop ordinary effects
if(playing != wasPlaying){
wasPlaying = playing;
if(playing){
Core.audio.soundBus.play();
setupFilters();
}else{
Core.audio.soundBus.stop();
}
}
if(state.isMenu()){
silenced = false;
if(ui.planet.isShown()){
@@ -79,10 +158,46 @@ public class MusicControl{
}
}
}
updateLoops();
}
protected void updateLoops(){
//clear loops when in menu
if(!state.isGame()){
sounds.clear();
return;
}
float avol = Core.settings.getInt("ambientvol", 100) / 100f;
sounds.each((sound, data) -> {
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.2f);
boolean play = data.curVolume > 0.01f;
float pan = Mathf.zero(data.total, 0.0001f) ? 0f : sound.calcPan(data.sum.x / data.total, data.sum.y / data.total);
if(data.soundID <= 0 || !Core.audio.isPlaying(data.soundID)){
if(play){
data.soundID = sound.loop(data.curVolume, 1f, pan);
Core.audio.protect(data.soundID, true);
}
}else{
if(data.curVolume <= 0.001f){
sound.stop();
data.soundID = -1;
return;
}
Core.audio.set(data.soundID, pan, data.curVolume);
}
data.volume = 0f;
data.total = 0f;
data.sum.setZero();
});
}
/** Plays a random track.*/
protected void playRandom(){
public void playRandom(){
if(isDark()){
playOnce(darkMusic.random(lastRandomPlayed));
}else{
@@ -171,12 +286,6 @@ public class MusicControl{
current = music;
current.setVolume(1f);
current.setLooping(false);
current.setCompletionListener(m -> {
if(current == m){
current = null;
fade = 0f;
}
});
current.play();
}
@@ -188,4 +297,13 @@ public class MusicControl{
protected void silence(){
play(null);
}
protected static class SoundData{
float volume;
float total;
Vec2 sum = new Vec2();
int soundID;
float curVolume;
}
}

View File

@@ -1,5 +1,6 @@
package mindustry.audio;
import arc.*;
import arc.audio.*;
import arc.math.*;
import arc.util.*;
@@ -18,7 +19,7 @@ public class SoundLoop{
}
public void update(float x, float y, boolean play){
if(baseVolume < 0) return;
if(baseVolume <= 0) return;
if(id < 0){
if(play){
@@ -31,18 +32,19 @@ public class SoundLoop{
}else{
volume = Mathf.clamp(volume - fadeSpeed * Time.delta);
if(volume <= 0.001f){
sound.stop(id);
Core.audio.stop(id);
id = -1;
return;
}
}
sound.setPan(id, sound.calcPan(x, y), sound.calcVolume(x, y) * volume * baseVolume);
Core.audio.set(id, sound.calcPan(x, y), sound.calcVolume(x, y) * volume * baseVolume);
}
}
public void stop(){
if(id != -1){
sound.stop(id);
Core.audio.stop(id);
id = -1;
volume = baseVolume = -1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -30,18 +30,33 @@ public class Bullets implements ContentList{
missileExplosive, missileIncendiary, missileSurge,
//standard
standardCopper, standardDense, standardThorium, standardHoming, standardIncendiary, standardMechSmall,
standardGlaive, standardDenseBig, standardThoriumBig, standardIncendiaryBig,
standardCopper, standardDense, standardThorium, standardHoming, standardIncendiary,
standardDenseBig, standardThoriumBig, standardIncendiaryBig,
//liquid
waterShot, cryoShot, slagShot, oilShot,
waterShot, cryoShot, slagShot, oilShot, heavyWaterShot, heavyCryoShot, heavySlagShot, heavyOilShot,
//environment, misc.
damageLightning, damageLightningGround, fireball, basicFlame, pyraFlame, driverBolt, healBullet, healBulletBig, frag;
damageLightning, damageLightningGround, fireball, basicFlame, pyraFlame, driverBolt;
@Override
public void load(){
//lightning bullets need to be initialized first.
damageLightning = new BulletType(0.0001f, 0f){{
lifetime = Fx.lightning.lifetime;
hitEffect = Fx.hitLancer;
despawnEffect = Fx.none;
status = StatusEffects.shocked;
statusDuration = 10f;
hittable = false;
}};
//this is just a copy of the damage lightning bullet that doesn't damage air units
damageLightningGround = new BulletType(0.0001f, 0f){};
JsonIO.copy(damageLightning, damageLightningGround);
damageLightningGround.collidesAir = false;
artilleryDense = new ArtilleryBulletType(3f, 20, "shell"){{
hitEffect = Fx.flakExplosion;
knockback = 0.8f;
@@ -102,6 +117,7 @@ public class Bullets implements ContentList{
status = StatusEffects.burning;
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
makeFire = true;
trailEffect = Fx.incendTrail;
}};
@@ -156,7 +172,7 @@ public class Bullets implements ContentList{
}};
flakGlass = new FlakBulletType(4f, 3){{
lifetime = 70f;
lifetime = 60f;
ammoMultiplier = 5f;
shootEffect = Fx.shootSmall;
reloadMultiplier = 0.8f;
@@ -190,7 +206,6 @@ public class Bullets implements ContentList{
}};
fragGlass = new FlakBulletType(4f, 3){{
lifetime = 70f;
ammoMultiplier = 3f;
shootEffect = Fx.shootSmall;
reloadMultiplier = 0.8f;
@@ -265,6 +280,7 @@ public class Bullets implements ContentList{
homingPower = 0.08f;
splashDamageRadius = 20f;
splashDamage = 20f;
makeFire = true;
hitEffect = Fx.blastExplosion;
status = StatusEffects.burning;
}};
@@ -278,6 +294,7 @@ public class Bullets implements ContentList{
splashDamage = 25f;
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
lightningDamage = 10;
lightning = 2;
lightningLength = 10;
}};
@@ -323,27 +340,11 @@ public class Bullets implements ContentList{
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
status = StatusEffects.burning;
makeFire = true;
inaccuracy = 3f;
lifetime = 60f;
}};
standardGlaive = new BasicBulletType(4f, 7.5f, "bullet"){{
width = 10f;
height = 12f;
frontColor = Color.valueOf("feb380");
backColor = Color.valueOf("ea8878");
status = StatusEffects.burning;
lifetime = 60f;
}};
standardMechSmall = new BasicBulletType(4f, 9, "bullet"){{
width = 11f;
height = 14f;
lifetime = 40f;
inaccuracy = 5f;
despawnEffect = Fx.hitBulletSmall;
}};
standardDenseBig = new BasicBulletType(7f, 55, "bullet"){{
width = 15f;
height = 21f;
@@ -354,6 +355,9 @@ public class Bullets implements ContentList{
width = 16f;
height = 23f;
shootEffect = Fx.shootBig;
pierceCap = 2;
pierceBuilding = true;
knockback = 0.7f;
}};
standardIncendiaryBig = new BasicBulletType(7f, 60, "bullet"){{
@@ -363,29 +367,10 @@ public class Bullets implements ContentList{
backColor = Pal.lightOrange;
status = StatusEffects.burning;
shootEffect = Fx.shootBig;
}};
damageLightning = new BulletType(0.0001f, 0f){{
lifetime = Fx.lightning.lifetime;
hitEffect = Fx.hitLancer;
despawnEffect = Fx.none;
status = StatusEffects.shocked;
statusDuration = 10f;
hittable = false;
}};
//this is just a copy of the damage lightning bullet that doesn't damage air units
damageLightningGround = new BulletType(0.0001f, 0f){{
collidesAir = false;
}};
JsonIO.copy(damageLightning, damageLightningGround);
healBullet = new HealBulletType(5.2f, 13){{
healPercent = 3f;
}};
healBulletBig = new HealBulletType(5.2f, 15){{
healPercent = 5.5f;
makeFire = true;
pierceCap = 2;
pierceBuilding = true;
knockback = 0.7f;
}};
fireball = new BulletType(1f, 4){
@@ -428,7 +413,7 @@ public class Bullets implements ContentList{
}
};
basicFlame = new BulletType(3.35f, 15f){{
basicFlame = new BulletType(3.35f, 16f){{
ammoMultiplier = 3f;
hitSize = 7f;
lifetime = 18f;
@@ -443,7 +428,7 @@ public class Bullets implements ContentList{
hittable = false;
}};
pyraFlame = new BulletType(3.35f, 22f){{
pyraFlame = new BulletType(3.35f, 25f){{
ammoMultiplier = 4f;
hitSize = 7f;
lifetime = 18f;
@@ -459,29 +444,70 @@ public class Bullets implements ContentList{
waterShot = new LiquidBulletType(Liquids.water){{
knockback = 0.7f;
drag = 0.01f;
}};
cryoShot = new LiquidBulletType(Liquids.cryofluid){{
drag = 0.01f;
}};
slagShot = new LiquidBulletType(Liquids.slag){{
damage = 4;
drag = 0.03f;
drag = 0.01f;
}};
oilShot = new LiquidBulletType(Liquids.oil){{
drag = 0.03f;
drag = 0.01f;
}};
heavyWaterShot = new LiquidBulletType(Liquids.water){{
lifetime = 49f;
speed = 4f;
knockback = 1.7f;
puddleSize = 8f;
orbSize = 4f;
drag = 0.001f;
ammoMultiplier = 0.4f;
statusDuration = 60f * 4f;
damage = 0.2f;
}};
heavyCryoShot = new LiquidBulletType(Liquids.cryofluid){{
lifetime = 49f;
speed = 4f;
knockback = 1.3f;
puddleSize = 8f;
orbSize = 4f;
drag = 0.001f;
ammoMultiplier = 0.4f;
statusDuration = 60f * 4f;
damage = 0.2f;
}};
heavySlagShot = new LiquidBulletType(Liquids.slag){{
lifetime = 49f;
speed = 4f;
knockback = 1.3f;
puddleSize = 8f;
orbSize = 4f;
damage = 4.75f;
drag = 0.001f;
ammoMultiplier = 0.4f;
statusDuration = 60f * 4f;
}};
heavyOilShot = new LiquidBulletType(Liquids.oil){{
lifetime = 49f;
speed = 4f;
knockback = 1.3f;
puddleSize = 8f;
orbSize = 4f;
drag = 0.001f;
ammoMultiplier = 0.4f;
statusDuration = 60f * 4f;
damage = 0.2f;
}};
driverBolt = new MassDriverBolt();
frag = new BasicBulletType(5f, 8, "bullet"){{
width = 8f;
height = 9f;
shrinkY = 0.5f;
lifetime = 50f;
drag = 0.04f;
}};
}
}

View File

@@ -18,7 +18,7 @@ import static arc.graphics.g2d.Draw.rect;
import static arc.graphics.g2d.Draw.*;
import static arc.graphics.g2d.Lines.*;
import static arc.math.Angles.*;
import static mindustry.Vars.tilesize;
import static mindustry.Vars.*;
public class Fx{
public static final Effect
@@ -56,9 +56,9 @@ public class Fx{
mixcol(Pal.accent, 1f);
alpha(e.fout());
rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type().icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f);
rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type.icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f);
alpha(1f);
Lines.stroke(e.fslope() * 1f);
Lines.stroke(e.fslope());
Lines.square(select.x, select.y, e.fout() * select.hitSize * 2f, 45);
Lines.stroke(e.fslope() * 2f);
Lines.square(select.x, select.y, e.fout() * select.hitSize * 3f, 45f);
@@ -66,7 +66,7 @@ public class Fx{
}),
unitDespawn = new Effect(100f, e -> {
if(!(e.data instanceof Unit) || e.<Unit>data().type() == null) return;
if(!(e.data instanceof Unit) || e.<Unit>data().type == null) return;
Unit select = e.data();
float scl = e.fout(Interp.pow2Out);
@@ -74,7 +74,7 @@ public class Fx{
Draw.scl *= scl;
mixcol(Pal.accent, 1f);
rect(select.type().icon(Cicon.full), select.x, select.y, select.rotation - 90f);
rect(select.type.icon(Cicon.full), select.x, select.y, select.rotation - 90f);
reset();
Draw.scl = p;
@@ -96,7 +96,7 @@ public class Fx{
x = Tmp.v1.x;
y = Tmp.v1.y;
Fill.square(x, y, 1f * size, 45f);
Fill.square(x, y, size, 45f);
}),
itemTransfer = new Effect(12f, e -> {
@@ -127,7 +127,7 @@ public class Fx{
pointHit = new Effect(8f, e -> {
color(Color.white, e.color, e.fin());
stroke(e.fout() * 1f + 0.2f);
stroke(e.fout() + 0.2f);
Lines.circle(e.x, e.y, e.fin() * 6f);
}),
@@ -138,12 +138,13 @@ public class Fx{
stroke(3f * e.fout());
color(e.color, Color.white, e.fin());
beginLine();
lines.each(Lines::linePoint);
linePoint(e.x, e.y);
endLine();
for(int i = 0; i < lines.size - 1; i++){
Vec2 cur = lines.get(i);
Vec2 next = lines.get(i + 1);
Lines.line(cur.x, cur.y, next.x, next.y, false);
}
int i = 0;
for(Vec2 p : lines){
Fill.circle(p.x, p.y, Lines.getStroke() / 2f);
}
@@ -256,33 +257,33 @@ public class Fx{
randLenVectors(e.id, 9, 3 + 20f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 4f + 0.4f);
});
}).ground(),
}).layer(Layer.debris),
unitLand = new Effect(30, e -> {
color(Tmp.c1.set(e.color).mul(1.1f));
randLenVectors(e.id, 6, 17f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 4f + 0.3f);
});
}).ground(),
}).layer(Layer.debris),
unitLandSmall = new Effect(30, e -> {
color(Tmp.c1.set(e.color).mul(1.1f));
randLenVectors(e.id, (int)(6 * e.rotation), 12f * e.finpow() * e.rotation, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.1f);
});
}).ground(),
}).layer(Layer.debris),
unitPickup = new Effect(18, e -> {
color(Pal.lightishGray);
stroke(e.fin() * 2f);
Lines.poly(e.x, e.y, 4, 13f * e.fout());
}).ground(),
}).layer(Layer.debris),
landShock = new Effect(12, e -> {
color(Pal.lancerLaser);
stroke(e.fout() * 3f);
Lines.poly(e.x, e.y, 12, 20f * e.fout());
}).ground(),
}).layer(Layer.debris),
pickup = new Effect(18, e -> {
color(Pal.lightishGray);
@@ -388,7 +389,6 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f);
});
}),
hitBulletBig = new Effect(13, e -> {
@@ -399,7 +399,6 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1.5f);
});
}),
hitFlameSmall = new Effect(14, e -> {
@@ -410,7 +409,6 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f);
});
}),
hitLiquid = new Effect(16, e -> {
@@ -419,7 +417,6 @@ public class Fx{
randLenVectors(e.id, 5, e.fin() * 15f, e.rotation, 60f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2f);
});
}),
hitLancer = new Effect(12, e -> {
@@ -430,7 +427,6 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1f);
});
}),
hitMeltdown = new Effect(12, e -> {
@@ -441,7 +437,6 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1f);
});
}),
hitMeltHeal = new Effect(12, e -> {
@@ -452,7 +447,79 @@ public class Fx{
float ang = Mathf.angle(x, y);
lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1f);
});
}),
instBomb = new Effect(15f, 100f, e -> {
color(Pal.bulletYellowBack);
stroke(e.fout() * 4f);
Lines.circle(e.x, e.y, 4f + e.finpow() * 20f);
for(int i = 0; i < 4; i++){
Drawf.tri(e.x, e.y, 6f, 80f * e.fout(), i*90 + 45);
}
color();
for(int i = 0; i < 4; i++){
Drawf.tri(e.x, e.y, 3f, 30f * e.fout(), i*90 + 45);
}
}),
instTrail = new Effect(30, e -> {
for(int i = 0; i < 2; i++){
color(i == 0 ? Pal.bulletYellowBack : Pal.bulletYellow);
float m = i == 0 ? 1f : 0.5f;
float rot = e.rotation + 180f;
float w = 15f * e.fout() * m;
Drawf.tri(e.x, e.y, w, (30f + Mathf.randomSeedRange(e.id, 15f)) * m, rot);
Drawf.tri(e.x, e.y, w, 10f * m, rot + 180f);
}
}),
instShoot = new Effect(24f, e -> {
e.scaled(10f, b -> {
color(Color.white, Pal.bulletYellowBack, b.fin());
stroke(b.fout() * 3f + 0.2f);
Lines.circle(b.x, b.y, b.fin() * 50f);
});
color(Pal.bulletYellowBack);
for(int i : Mathf.signs){
Drawf.tri(e.x, e.y, 13f * e.fout(), 85f, e.rotation + 90f * i);
Drawf.tri(e.x, e.y, 13f * e.fout(), 50f, e.rotation + 20f * i);
}
}),
instHit = new Effect(20f, 200f, e -> {
color(Pal.bulletYellowBack);
for(int i = 0; i < 2; i++){
color(i == 0 ? Pal.bulletYellowBack : Pal.bulletYellow);
float m = i == 0 ? 1f : 0.5f;
for(int j = 0; j < 5; j++){
float rot = e.rotation + Mathf.randomSeedRange(e.id + j, 50f);
float w = 23f * e.fout() * m;
Drawf.tri(e.x, e.y, w, (80f + Mathf.randomSeedRange(e.id + j, 40f)) * m, rot);
Drawf.tri(e.x, e.y, w, 20f * m, rot + 180f);
}
}
e.scaled(10f, c -> {
color(Pal.bulletYellow);
stroke(c.fout() * 2f + 0.2f);
Lines.circle(e.x, e.y, c.fin() * 30f);
});
e.scaled(12f, c -> {
color(Pal.bulletYellowBack);
randLenVectors(e.id, 25, 5f + e.fin() * 80f, e.rotation, 60f, (x, y) -> {
Fill.square(e.x + x, e.y + y, c.fout() * 3f, 45f);
});
});
}),
hitLaser = new Effect(8, e -> {
@@ -479,8 +546,8 @@ public class Fx{
}),
flakExplosion = new Effect(20, e -> {
color(Pal.bulletYellow);
e.scaled(6, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 10f);
@@ -493,17 +560,16 @@ public class Fx{
});
color(Pal.lighterOrange);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
plasticExplosion = new Effect(24, e -> {
color(Pal.plastaniumFront);
e.scaled(7, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 24f);
@@ -516,17 +582,16 @@ public class Fx{
});
color(Pal.plastaniumBack);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 4, 1f + 25f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
plasticExplosionFlak = new Effect(28, e -> {
color(Pal.plastaniumFront);
e.scaled(7, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 34f);
@@ -539,17 +604,16 @@ public class Fx{
});
color(Pal.plastaniumBack);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 4, 1f + 30f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
blastExplosion = new Effect(22, e -> {
color(Pal.missileYellow);
e.scaled(6, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 15f);
@@ -562,17 +626,16 @@ public class Fx{
});
color(Pal.missileYellowBack);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
sapExplosion = new Effect(25, e -> {
color(Pal.sapBullet);
e.scaled(6, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 80f);
@@ -585,17 +648,16 @@ public class Fx{
});
color(Pal.sapBulletBack);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 8, 1f + 60f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
massiveExplosion = new Effect(30, e -> {
color(Pal.missileYellow);
e.scaled(7, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 4f + i.fin() * 30f);
@@ -608,12 +670,11 @@ public class Fx{
});
color(Pal.missileYellowBack);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 6, 1f + 29f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 4f);
});
}),
artilleryTrail = new Effect(50, e -> {
@@ -638,8 +699,8 @@ public class Fx{
}),
flakExplosionBig = new Effect(30, e -> {
color(Pal.bulletYellowBack);
e.scaled(6, i -> {
stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 25f);
@@ -652,12 +713,11 @@ public class Fx{
});
color(Pal.bulletYellow);
stroke(1f * e.fout());
stroke(e.fout());
randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
burning = new Effect(35f, e -> {
@@ -666,7 +726,6 @@ public class Fx{
randLenVectors(e.id, 3, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.1f + e.fout() * 1.4f);
});
}),
fire = new Effect(50f, e -> {
@@ -687,7 +746,6 @@ public class Fx{
randLenVectors(e.id, 1, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fslope() * 1.5f);
});
}),
steam = new Effect(35f, e -> {
@@ -696,7 +754,6 @@ public class Fx{
randLenVectors(e.id, 2, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fslope() * 1.5f);
});
}),
fireballsmoke = new Effect(25f, e -> {
@@ -705,7 +762,6 @@ public class Fx{
randLenVectors(e.id, 1, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fout() * 1.5f);
});
}),
ballfire = new Effect(25f, e -> {
@@ -714,7 +770,6 @@ public class Fx{
randLenVectors(e.id, 2, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fout() * 1.5f);
});
}),
freezing = new Effect(40f, e -> {
@@ -723,7 +778,6 @@ public class Fx{
randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1.2f);
});
}),
melting = new Effect(40f, e -> {
@@ -732,21 +786,20 @@ public class Fx{
randLenVectors(e.id, 2, 1f + e.fin() * 3f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, .2f + e.fout() * 1.2f);
});
}),
wet = new Effect(80f, e -> {
color(Liquids.water.color);
alpha(Mathf.clamp(e.fin() * 2f));
Fill.circle(e.x, e.y, e.fout() * 1f);
Fill.circle(e.x, e.y, e.fout());
}),
muddy = new Effect(80f, e -> {
color(Color.valueOf("432722"));
alpha(Mathf.clamp(e.fin() * 2f));
Fill.circle(e.x, e.y, e.fout() * 1f);
Fill.circle(e.x, e.y, e.fout());
}),
sapped = new Effect(40f, e -> {
@@ -755,7 +808,6 @@ public class Fx{
randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.square(e.x + x, e.y + y, e.fslope() * 1.1f, 45f);
});
}),
sporeSlowed = new Effect(40f, e -> {
@@ -768,9 +820,8 @@ public class Fx{
color(Liquids.oil.color);
randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1f);
Fill.circle(e.x + x, e.y + y, e.fout());
});
}),
overdriven = new Effect(20f, e -> {
@@ -779,7 +830,6 @@ public class Fx{
randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.square(e.x + x, e.y + y, e.fout() * 2.3f + 0.5f);
});
}),
overclocked = new Effect(50f, e -> {
@@ -835,7 +885,7 @@ public class Fx{
randLenVectors(e.id, 6, 2f + 19f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.5f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout());
});
color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin());
@@ -879,7 +929,7 @@ public class Fx{
randLenVectors(e.id, 6, 2f + 19f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.5f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout());
});
color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin());
@@ -888,7 +938,6 @@ public class Fx{
randLenVectors(e.id + 1, 9, 1f + 23f * e.finpow(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f);
});
}),
blockExplosionSmoke = new Effect(30, e -> {
@@ -896,9 +945,8 @@ public class Fx{
randLenVectors(e.id, 6, 4f + 30f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout());
});
}),
shootSmall = new Effect(8, e -> {
@@ -928,7 +976,6 @@ public class Fx{
randLenVectors(e.id, 5, e.finpow() * 6f, e.rotation, 20f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1.5f);
});
}),
shootBig = new Effect(9, e -> {
@@ -951,7 +998,6 @@ public class Fx{
randLenVectors(e.id, 8, e.finpow() * 19f, e.rotation, 10f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2f + 0.2f);
});
}),
shootBigSmoke2 = new Effect(18f, e -> {
@@ -960,7 +1006,6 @@ public class Fx{
randLenVectors(e.id, 9, e.finpow() * 23f, e.rotation, 20f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2.4f + 0.2f);
});
}),
shootSmallFlame = new Effect(32f, 80f, e -> {
@@ -969,7 +1014,6 @@ public class Fx{
randLenVectors(e.id, 8, e.finpow() * 60f, e.rotation, 10f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.65f + e.fout() * 1.5f);
});
}),
shootPyraFlame = new Effect(33f, 80f, e -> {
@@ -978,7 +1022,6 @@ public class Fx{
randLenVectors(e.id, 10, e.finpow() * 70f, e.rotation, 10f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.65f + e.fout() * 1.6f);
});
}),
shootLiquid = new Effect(40f, 80f, e -> {
@@ -987,69 +1030,104 @@ public class Fx{
randLenVectors(e.id, 6, e.finpow() * 60f, e.rotation, 11f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.5f + e.fout() * 2.5f);
});
}),
shellEjectSmall = new Effect(30f, e -> {
casing1 = new Effect(30f, e -> {
color(Pal.lightOrange, Color.lightGray, Pal.lightishGray, e.fin());
alpha(e.fout(0.3f));
float rot = Math.abs(e.rotation) + 90f;
int i = Mathf.sign(e.rotation);
int i = -Mathf.sign(e.rotation);
float len = (2f + e.finpow() * 6f) * i;
float lr = rot + e.fin() * 30f * i;
Fill.rect(e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
1f, 2f, rot + e.fin() * 50f * i);
Fill.rect(
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
1f, 2f, rot + e.fin() * 50f * i
);
}).ground(400f),
}).layer(Layer.bullet),
shellEjectMedium = new Effect(34f, e -> {
casing2 = new Effect(34f, e -> {
color(Pal.lightOrange, Color.lightGray, Pal.lightishGray, e.fin());
float rot = e.rotation + 90f;
alpha(e.fout(0.5f));
float rot = Math.abs(e.rotation) + 90f;
int i = -Mathf.sign(e.rotation);
float len = (2f + e.finpow() * 10f) * i;
float lr = rot + e.fin() * 20f * i;
rect(Core.atlas.find("casing"),
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2f, 3f, rot + e.fin() * 50f * i
);
}).layer(Layer.bullet),
casing3 = new Effect(40f, e -> {
color(Pal.lightOrange, Pal.lightishGray, Pal.lightishGray, e.fin());
alpha(e.fout(0.5f));
float rot = Math.abs(e.rotation) + 90f;
int i = -Mathf.sign(e.rotation);
float len = (4f + e.finpow() * 9f) * i;
float lr = rot + Mathf.randomSeedRange(e.id + i + 6, 20f * e.fin()) * i;
rect(Core.atlas.find("casing"),
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2.5f, 4f,
rot + e.fin() * 50f * i
);
}).layer(Layer.bullet),
casing4 = new Effect(45f, e -> {
color(Pal.lightOrange, Pal.lightishGray, Pal.lightishGray, e.fin());
alpha(e.fout(0.5f));
float rot = Math.abs(e.rotation) + 90f;
int i = -Mathf.sign(e.rotation);
float len = (4f + e.finpow() * 9f) * i;
float lr = rot + Mathf.randomSeedRange(e.id + i + 6, 20f * e.fin()) * i;
rect(Core.atlas.find("casing"),
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
3f, 6f,
rot + e.fin() * 50f * i
);
}).layer(Layer.bullet),
casing2Double = new Effect(34f, e -> {
color(Pal.lightOrange, Color.lightGray, Pal.lightishGray, e.fin());
alpha(e.fout(0.5f));
float rot = Math.abs(e.rotation) + 90f;
for(int i : Mathf.signs){
float len = (2f + e.finpow() * 10f) * i;
float lr = rot + e.fin() * 20f * i;
rect(Core.atlas.find("casing"),
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2f, 3f, rot);
2f, 3f, rot + e.fin() * 50f * i
);
}
color(Color.lightGray, Color.gray, e.fin());
}).layer(Layer.bullet),
casing3Double = new Effect(40f, e -> {
color(Pal.lightOrange, Pal.lightishGray, Pal.lightishGray, e.fin());
alpha(e.fout(0.5f));
float rot = Math.abs(e.rotation) + 90f;
for(int i : Mathf.signs){
float ex = e.x, ey = e.y, fout = e.fout();
randLenVectors(e.id, 4, 1f + e.finpow() * 11f, e.rotation + 90f * i, 20f, (x, y) -> {
Fill.circle(ex + x, ey + y, fout * 1.5f);
});
}
}).ground(400f),
shellEjectBig = new Effect(22f, e -> {
color(Pal.lightOrange, Color.lightGray, Pal.lightishGray, e.fin());
float rot = e.rotation + 90f;
for(int i : Mathf.signs){
float len = (4f + e.finpow() * 8f) * i;
float len = (4f + e.finpow() * 9f) * i;
float lr = rot + Mathf.randomSeedRange(e.id + i + 6, 20f * e.fin()) * i;
rect(Core.atlas.find("casing"),
e.x + trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2.5f, 4f,
rot + e.fin() * 30f * i + Mathf.randomSeedRange(e.id + i + 9, 40f * e.fin()));
rot + e.fin() * 50f * i
);
}
color(Color.lightGray);
for(int i : Mathf.signs){
float ex = e.x, ey = e.y, fout = e.fout();
randLenVectors(e.id, 4, -e.finpow() * 15f, e.rotation + 90f * i, 25f, (x, y) -> {
Fill.circle(ex + x, ey + y, fout * 2f);
});
}
}).ground(400f),
}).layer(Layer.bullet),
railShoot = new Effect(24f, e -> {
e.scaled(10f, b -> {
@@ -1074,28 +1152,11 @@ public class Fx{
}),
railHit = new Effect(18f, 200f, e -> {
if(true){
color(Pal.orangeSpark);
color(Pal.orangeSpark);
for(int i : Mathf.signs){
Drawf.tri(e.x, e.y, 10f * e.fout(), 60f, e.rotation + 140f * i);
}
}else{
e.scaled(7f, b -> {
color(Color.white, Color.lightGray, b.fin());
stroke(b.fout() * 2f + 0.2f);
Lines.circle(b.x, b.y, b.fin() * 28f);
});
color(Pal.orangeSpark);
float rot = e.rotation + Mathf.randomSeedRange(e.id, 20f);
float w = 9f * e.fout();
Drawf.tri(e.x, e.y, w, 100f, rot);
Drawf.tri(e.x, e.y, w, 10f, rot + 180f);
for(int i : Mathf.signs){
Drawf.tri(e.x, e.y, 10f * e.fout(), 60f, e.rotation + 140f * i);
}
}),
lancerLaserShoot = new Effect(21f, e -> {
@@ -1104,17 +1165,15 @@ public class Fx{
for(int i : Mathf.signs){
Drawf.tri(e.x, e.y, 4f * e.fout(), 29f, e.rotation + 90f * i);
}
}),
lancerLaserShootSmoke = new Effect(26f, e -> {
color(Color.white);
float length = e.data == null || !(e.data instanceof Float) ? 70f : (Float)e.data;
float length = !(e.data instanceof Float) ? 70f : (Float)e.data;
randLenVectors(e.id, 7, length, e.rotation, 0f, (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fout() * 9f);
});
}),
lancerLaserCharge = new Effect(38f, e -> {
@@ -1123,7 +1182,6 @@ public class Fx{
randLenVectors(e.id, 2, 1f + 20f * e.fout(), e.rotation, 120f, (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fslope() * 3f + 1f);
});
}),
lancerLaserChargeBegin = new Effect(60f, e -> {
@@ -1140,7 +1198,6 @@ public class Fx{
randLenVectors(e.id, 2, 1f + 20f * e.fout(), e.rotation, 120f, (x, y) -> {
Drawf.tri(e.x + x, e.y + y, e.fslope() * 3f + 1, e.fslope() * 3f + 1, Mathf.angle(x, y));
});
}),
sparkShoot = new Effect(12f, e -> {
@@ -1150,7 +1207,6 @@ public class Fx{
randLenVectors(e.id, 7, 25f * e.finpow(), e.rotation, 3f, (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fslope() * 5f + 0.5f);
});
}),
lightningShoot = new Effect(12f, e -> {
@@ -1160,7 +1216,15 @@ public class Fx{
randLenVectors(e.id, 7, 25f * e.finpow(), e.rotation, 50f, (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fin() * 5f + 2f);
});
}),
thoriumShoot = new Effect(12f, e -> {
color(Color.white, Pal.thoriumPink, e.fin());
stroke(e.fout() * 1.2f + 0.5f);
randLenVectors(e.id, 7, 25f * e.finpow(), e.rotation, 50f, (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fin() * 5f + 2f);
});
}),
reactorsmoke = new Effect(17, e -> {
@@ -1236,10 +1300,18 @@ public class Fx{
});
}),
coreBurn = new Effect(23, e -> {
randLenVectors(e.id, 5, e.fin() * 9f, (x, y) -> {
float len = e.fout() * 4f;
color(Pal.accent, Color.gray, e.fin());
Fill.circle(e.x + x, e.y + y, len/2f);
});
}),
plasticburn = new Effect(40, e -> {
randLenVectors(e.id, 5, 3f + e.fin() * 5f, (x, y) -> {
color(Color.valueOf("e9ead3"), Color.gray, e.fin());
Fill.circle(e.x + x, e.y + y, e.fout() * 1f);
Fill.circle(e.x + x, e.y + y, e.fout());
});
}),
@@ -1263,21 +1335,21 @@ public class Fx{
Fill.square(e.x + x, e.y + y, e.fout() * 2.5f + 0.5f, 45);
});
}),
pulverizeSmall = new Effect(30, e -> {
randLenVectors(e.id, 3, e.fin() * 5f, (x, y) -> {
color(Pal.stoneGray);
Fill.square(e.x + x, e.y + y, e.fout() * 1f + 0.5f, 45);
Fill.square(e.x + x, e.y + y, e.fout() + 0.5f, 45);
});
}),
pulverizeMedium = new Effect(30, e -> {
randLenVectors(e.id, 5, 3f + e.fin() * 8f, (x, y) -> {
color(Pal.stoneGray);
Fill.square(e.x + x, e.y + y, e.fout() * 1f + 0.5f, 45);
Fill.square(e.x + x, e.y + y, e.fout() + 0.5f, 45);
});
}),
producesmoke = new Effect(12, e -> {
randLenVectors(e.id, 8, 4f + e.fin() * 18f, (x, y) -> {
color(Color.white, Pal.accent, e.fin());
@@ -1292,21 +1364,21 @@ public class Fx{
Fill.circle(e.x + x, e.y + y, 0.5f + fout * 4f);
});
}),
smeltsmoke = new Effect(15, e -> {
randLenVectors(e.id, 6, 4f + e.fin() * 5f, (x, y) -> {
color(Color.white, e.color, e.fin());
Fill.square(e.x + x, e.y + y, 0.5f + e.fout() * 2f, 45);
});
}),
formsmoke = new Effect(40, e -> {
randLenVectors(e.id, 6, 5f + e.fin() * 8f, (x, y) -> {
color(Pal.plasticSmoke, Color.lightGray, e.fin());
Fill.square(e.x + x, e.y + y, 0.2f + e.fout() * 2f, 45);
});
}),
blastsmoke = new Effect(26, e -> {
randLenVectors(e.id, 12, 1f + e.fin() * 23f, (x, y) -> {
float size = 2f + e.fout() * 6f;
@@ -1314,7 +1386,7 @@ public class Fx{
Fill.circle(e.x + x, e.y + y, size/2f);
});
}),
lava = new Effect(18, e -> {
randLenVectors(e.id, 3, 1f + e.fin() * 10f, (x, y) -> {
float size = e.fslope() * 4f;
@@ -1322,50 +1394,58 @@ public class Fx{
Fill.circle(e.x + x, e.y + y, size/2f);
});
}),
dooropen = new Effect(10, e -> {
stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize / 2f + e.fin() * 2f);
}),
doorclose = new Effect(10, e -> {
stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize / 2f + e.fout() * 2f);
}),
dooropenlarge = new Effect(10, e -> {
stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize + e.fin() * 2f);
}),
doorcloselarge = new Effect(10, e -> {
stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize + e.fout() * 2f);
}),
purify = new Effect(10, e -> {
color(Color.royal, Color.gray, e.fin());
stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
}),
purifyoil = new Effect(10, e -> {
color(Color.black, Color.gray, e.fin());
stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
}),
purifystone = new Effect(10, e -> {
color(Color.orange, Color.gray, e.fin());
stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
}),
generate = new Effect(11, e -> {
color(Color.orange, Color.yellow, e.fin());
stroke(1f);
Lines.spikes(e.x, e.y, e.fin() * 5f, 2, 8);
}),
mine = new Effect(20, e -> {
randLenVectors(e.id, 6, 3f + e.fin() * 6f, (x, y) -> {
color(e.color, Color.lightGray, e.fin());
Fill.square(e.x + x, e.y + y, e.fout() * 2f, 45);
});
}),
mineBig = new Effect(30, e -> {
randLenVectors(e.id, 6, 4f + e.fin() * 8f, (x, y) -> {
color(e.color, Color.lightGray, e.fin());
@@ -1379,12 +1459,14 @@ public class Fx{
Fill.square(e.x + x, e.y + y, e.fout() * 2f + 0.5f, 45);
});
}),
smelt = new Effect(20, e -> {
randLenVectors(e.id, 6, 2f + e.fin() * 5f, (x, y) -> {
color(Color.white, e.color, e.fin());
Fill.square(e.x + x, e.y + y, 0.5f + e.fout() * 2f, 45);
});
}),
teleportActivate = new Effect(50, e -> {
color(e.color);
@@ -1398,8 +1480,8 @@ public class Fx{
randLenVectors(e.id, 30, 4f + 40f * e.fin(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fin() * 4f + 1f);
});
}),
teleport = new Effect(60, e -> {
color(e.color);
stroke(e.fin() * 2f);
@@ -1408,8 +1490,8 @@ public class Fx{
randLenVectors(e.id, 20, 6f + 20f * e.fout(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fin() * 4f + 1f);
});
}),
teleportOut = new Effect(20, e -> {
color(e.color);
stroke(e.fout() * 2f);
@@ -1418,7 +1500,6 @@ public class Fx{
randLenVectors(e.id, 20, 4f + 20f * e.fin(), (x, y) -> {
lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fslope() * 4f + 1f);
});
}),
ripple = new Effect(30, e -> {
@@ -1427,7 +1508,7 @@ public class Fx{
color(Tmp.c1.set(e.color).mul(1.5f));
stroke(e.fout() * 1.4f);
Lines.circle(e.x, e.y, (2f + e.fin() * 4f) * e.rotation);
}).ground(),
}).layer(Layer.debris),
bubble = new Effect(20, e -> {
color(Tmp.c1.set(e.color).shiftValue(0.1f));
@@ -1467,7 +1548,7 @@ public class Fx{
overdriveWave = new Effect(50, e -> {
color(e.color);
stroke(e.fout() * 1f);
stroke(e.fout());
Lines.circle(e.x, e.y, e.finpow() * e.rotation);
}),
@@ -1502,7 +1583,6 @@ public class Fx{
float radius = unit.hitSize() * 1.3f;
e.scaled(16f, c -> {
color(Pal.shield);
stroke(c.fout() * 2f + 0.1f);
@@ -1513,7 +1593,7 @@ public class Fx{
});
color(Pal.shield, e.fout());
stroke(1f * e.fout());
stroke(e.fout());
Lines.circle(e.x, e.y, radius);
}),

View File

@@ -5,7 +5,7 @@ import mindustry.ctype.*;
import mindustry.type.*;
public class Items implements ContentList{
public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phasefabric, surgealloy,
public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phaseFabric, surgeAlloy,
sporePod, sand, blastCompound, pyratite, metaglass;
@Override
@@ -32,6 +32,7 @@ public class Items implements ContentList{
sand = new Item("sand", Color.valueOf("f7cba4")){{
alwaysUnlocked = true;
lowPriority = true;
}};
coal = new Item("coal", Color.valueOf("272727")){{
@@ -66,12 +67,13 @@ public class Items implements ContentList{
cost = 1.3f;
}};
phasefabric = new Item("phase-fabric", Color.valueOf("f4ba6e")){{
phaseFabric = new Item("phase-fabric", Color.valueOf("f4ba6e")){{
cost = 1.3f;
radioactivity = 0.6f;
}};
surgealloy = new Item("surge-alloy", Color.valueOf("f3e979")){{
surgeAlloy = new Item("surge-alloy", Color.valueOf("f3e979")){{
cost = 1.2f;
}};
sporePod = new Item("spore-pod", Color.valueOf("7457ce")){{

View File

@@ -1,8 +1,8 @@
package mindustry.content;
import arc.graphics.Color;
import mindustry.ctype.ContentList;
import mindustry.type.Liquid;
import arc.graphics.*;
import mindustry.ctype.*;
import mindustry.type.*;
public class Liquids implements ContentList{
public static Liquid water, slag, oil, cryofluid;
@@ -12,13 +12,13 @@ public class Liquids implements ContentList{
water = new Liquid("water", Color.valueOf("596ab8")){{
heatCapacity = 0.4f;
effect = StatusEffects.wet;
alwaysUnlocked = true;
effect = StatusEffects.wet;
}};
slag = new Liquid("slag", Color.valueOf("ffa166")){{
temperature = 1f;
viscosity = 0.8f;
viscosity = 0.7f;
effect = StatusEffects.melting;
lightColor = Color.valueOf("f0511d").a(0.4f);
}};

View File

@@ -9,12 +9,14 @@ import mindustry.type.*;
public class Planets implements ContentList{
public static Planet
sun,
//tantros,
serpulo;
@Override
public void load(){
sun = new Planet("sun", null, 0, 2){{
bloom = true;
accessible = false;
//lightColor = Color.valueOf("f4ee8e");
@@ -31,10 +33,21 @@ public class Planets implements ContentList{
);
}};
/*tantros = new Planet("tantros", sun, 2, 0.8f){{
generator = new TantrosPlanetGenerator();
meshLoader = () -> new HexMesh(this, 4);
atmosphereColor = Color.valueOf("3db899");
startSector = 10;
atmosphereRadIn = -0.01f;
atmosphereRadOut = 0.3f;
}};*/
serpulo = new Planet("serpulo", sun, 3, 1){{
generator = new SerpuloPlanetGenerator();
meshLoader = () -> new HexMesh(this, 6);
atmosphereColor = Color.valueOf("3c1b8f");
atmosphereRadIn = 0.02f;
atmosphereRadOut = 0.3f;
startSector = 15;
}};
}

View File

@@ -8,57 +8,92 @@ import static mindustry.content.Planets.*;
public class SectorPresets implements ContentList{
public static SectorPreset
groundZero,
craters, frozenForest, ruinousShores, stainedMountains, tarFields, fungalPass,
saltFlats, overgrowth,
desolateRift, nuclearComplex;
craters, biomassFacility, frozenForest, ruinousShores, windsweptIslands, stainedMountains, tarFields,
fungalPass, extractionOutpost, saltFlats, overgrowth,
impact0078, desolateRift, nuclearComplex, planetaryTerminal;
@Override
public void load(){
groundZero = new SectorPreset("groundZero", serpulo, 15){{
alwaysUnlocked = true;
addStartingItems = true;
captureWave = 10;
difficulty = 1;
}};
saltFlats = new SectorPreset("saltFlats", serpulo, 101){{
difficulty = 5;
useAI = false;
}};
frozenForest = new SectorPreset("frozenForest", serpulo, 86){{
captureWave = 40;
captureWave = 15;
difficulty = 2;
}};
biomassFacility = new SectorPreset("biomassFacility", serpulo, 81){{
captureWave = 20;
difficulty = 3;
}};
craters = new SectorPreset("craters", serpulo, 18){{
captureWave = 40;
captureWave = 20;
difficulty = 2;
}};
ruinousShores = new SectorPreset("ruinousShores", serpulo, 19){{
captureWave = 40;
ruinousShores = new SectorPreset("ruinousShores", serpulo, 213){{
captureWave = 30;
difficulty = 3;
}};
windsweptIslands = new SectorPreset("windsweptIslands", serpulo, 246){{
captureWave = 30;
difficulty = 4;
}};
stainedMountains = new SectorPreset("stainedMountains", serpulo, 20){{
captureWave = 30;
difficulty = 3;
}};
extractionOutpost = new SectorPreset("extractionOutpost", serpulo, 165){{
difficulty = 5;
useAI = false;
}};
fungalPass = new SectorPreset("fungalPass", serpulo, 21){{
difficulty = 4;
useAI = false;
}};
overgrowth = new SectorPreset("overgrowth", serpulo, 22){{
overgrowth = new SectorPreset("overgrowth", serpulo, 134){{
difficulty = 5;
useAI = false;
}};
tarFields = new SectorPreset("tarFields", serpulo, 23){{
captureWave = 40;
difficulty = 5;
}};
impact0078 = new SectorPreset("impact0078", serpulo, 227){{
captureWave = 45;
difficulty = 7;
}};
desolateRift = new SectorPreset("desolateRift", serpulo, 123){{
captureWave = 40;
captureWave = 18;
difficulty = 8;
}};
nuclearComplex = new SectorPreset("nuclearComplex", serpulo, 130){{
captureWave = 60;
captureWave = 50;
difficulty = 7;
}};
planetaryTerminal = new SectorPreset("planetaryTerminal", serpulo, 93){{
difficulty = 10;
}};
}
}

View File

@@ -2,10 +2,13 @@ package mindustry.content;
import arc.*;
import arc.graphics.*;
import arc.math.Mathf;
import mindustry.ctype.ContentList;
import arc.math.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.type.StatusEffect;
import mindustry.type.*;
import mindustry.graphics.*;
import static mindustry.Vars.*;
public class StatusEffects implements ContentList{
@@ -17,22 +20,24 @@ public class StatusEffects implements ContentList{
none = new StatusEffect("none");
burning = new StatusEffect("burning"){{
color = Pal.lightFlame;
damage = 0.12f; //over 8 seconds, this would be 60 damage
effect = Fx.burning;
init(() -> {
opposite(wet,freezing);
opposite(wet, freezing);
trans(tarred, ((unit, time, newTime, result) -> {
unit.damagePierce(8f);
Fx.burning.at(unit.x() + Mathf.range(unit.bounds() / 2f), unit.y() + Mathf.range(unit.bounds() / 2f));
Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f));
result.set(this, Math.min(time + newTime, 300f));
}));
});
}};
freezing = new StatusEffect("freezing"){{
color = Color.valueOf("6ecdec");
speedMultiplier = 0.6f;
armorMultiplier = 0.8f;
healthMultiplier = 0.8f;
effect = Fx.freezing;
init(() -> {
@@ -46,10 +51,12 @@ public class StatusEffects implements ContentList{
}};
unmoving = new StatusEffect("unmoving"){{
color = Pal.gray;
speedMultiplier = 0.001f;
}};
slow = new StatusEffect("slow"){{
color = Pal.lightishGray;
speedMultiplier = 0.4f;
}};
@@ -62,7 +69,7 @@ public class StatusEffects implements ContentList{
init(() -> {
trans(shocked, ((unit, time, newTime, result) -> {
unit.damagePierce(14f);
if(unit.team() == state.rules.waveTeam){
if(unit.team == state.rules.waveTeam){
Events.fire(Trigger.shock);
}
result.set(this, time);
@@ -79,42 +86,51 @@ public class StatusEffects implements ContentList{
}};
melting = new StatusEffect("melting"){{
color = Color.valueOf("ffa166");
speedMultiplier = 0.8f;
armorMultiplier = 0.8f;
healthMultiplier = 0.8f;
damage = 0.3f;
effect = Fx.melting;
init(() -> {
trans(tarred, ((unit, time, newTime, result) -> result.set(this, Math.min(time + newTime / 2f, 140f))));
opposite(wet, freezing);
trans(tarred, ((unit, time, newTime, result) -> {
unit.damagePierce(8f);
Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f));
result.set(this, Math.min(time + newTime, 200f));
}));
});
}};
sapped = new StatusEffect("sapped"){{
color = Pal.sap;
speedMultiplier = 0.7f;
armorMultiplier = 0.8f;
healthMultiplier = 0.8f;
effect = Fx.sapped;
effectChance = 0.1f;
}};
sporeSlowed = new StatusEffect("spore-slowed"){{
color = Pal.spore;
speedMultiplier = 0.8f;
effect = Fx.sapped;
effectChance = 0.04f;
}};
tarred = new StatusEffect("tarred"){{
color = Color.valueOf("313131");
speedMultiplier = 0.6f;
effect = Fx.oily;
init(() -> {
trans(melting, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
trans(melting, ((unit, time, newTime, result) -> result.set(melting, newTime + time)));
trans(burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
});
}};
overdrive = new StatusEffect("overdrive"){{
armorMultiplier = 0.95f;
color = Pal.accent;
healthMultiplier = 0.95f;
speedMultiplier = 1.15f;
damageMultiplier = 1.4f;
damage = -0.01f;
@@ -123,6 +139,7 @@ public class StatusEffects implements ContentList{
}};
overclock = new StatusEffect("overclock"){{
color = Pal.accent;
speedMultiplier = 1.15f;
damageMultiplier = 1.15f;
reloadMultiplier = 1.25f;
@@ -131,20 +148,27 @@ public class StatusEffects implements ContentList{
}};
shielded = new StatusEffect("shielded"){{
armorMultiplier = 3f;
color = Pal.accent;
healthMultiplier = 3f;
}};
boss = new StatusEffect("boss"){{
color = Pal.health;
permanent = true;
damageMultiplier = 1.5f;
armorMultiplier = 1.5f;
damageMultiplier = 1.3f;
healthMultiplier = 1.5f;
}};
shocked = new StatusEffect("shocked");
shocked = new StatusEffect("shocked"){{
color = Pal.lancerLaser;
}};
blasted = new StatusEffect("blasted");
blasted = new StatusEffect("blasted"){{
color = Color.valueOf("ff795e");
}};
corroded = new StatusEffect("corroded"){{
color = Pal.plastanium;
damage = 0.1f;
}};
}

View File

@@ -2,7 +2,7 @@ package mindustry.content;
import arc.*;
import arc.struct.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.game.Objectives.*;
import mindustry.type.*;
@@ -11,10 +11,10 @@ import static mindustry.content.Blocks.*;
import static mindustry.content.SectorPresets.craters;
import static mindustry.content.SectorPresets.*;
import static mindustry.content.UnitTypes.*;
import static mindustry.type.ItemStack.*;
public class TechTree implements ContentList{
static ObjectMap<UnlockableContent, TechNode> map = new ObjectMap<>();
static TechNode context = null;
public static Seq<TechNode> all;
public static TechNode root;
@@ -29,7 +29,10 @@ public class TechTree implements ContentList{
node(junction, () -> {
node(router, () -> {
node(launchPad, () -> {
node(launchPad, Seq.with(new SectorComplete(extractionOutpost)), () -> {
node(interplanetaryAccelerator, Seq.with(new SectorComplete(planetaryTerminal)), () -> {
});
});
node(distributor);
@@ -39,15 +42,15 @@ public class TechTree implements ContentList{
node(underflowGate);
});
});
node(container, () -> {
node(container, Seq.with(new SectorComplete(biomassFacility)), () -> {
node(unloader);
node(vault, () -> {
node(vault, Seq.with(new SectorComplete(stainedMountains)), () -> {
});
});
node(itemBridge, () -> {
node(titaniumConveyor, () -> {
node(titaniumConveyor, Seq.with(new SectorComplete(craters)), () -> {
node(phaseConveyor, () -> {
node(massDriver, () -> {
@@ -87,7 +90,7 @@ public class TechTree implements ContentList{
node(bridgeConduit);
node(pulseConduit, () -> {
node(pulseConduit, Seq.with(new SectorComplete(windsweptIslands)), () -> {
node(phaseConduit, () -> {
});
@@ -95,11 +98,11 @@ public class TechTree implements ContentList{
node(platedConduit, () -> {
});
});
node(rotaryPump, () -> {
node(thermalPump, () -> {
node(rotaryPump, () -> {
node(thermalPump, () -> {
});
});
});
});
@@ -107,183 +110,155 @@ public class TechTree implements ContentList{
});
});
node(Items.coal, with(Items.lead, 3000), () -> {
node(Items.graphite, with(Items.coal, 1000), () -> {
node(graphitePress, () -> {
node(Items.titanium, with(Items.graphite, 6000, Items.copper, 10000, Items.lead, 10000), () -> {
node(pneumaticDrill, () -> {
node(Items.sporePod, with(Items.coal, 5000, Items.graphite, 5000, Items.lead, 5000), () -> {
node(cultivator, () -> {
node(graphitePress, () -> {
node(pneumaticDrill, Seq.with(new SectorComplete(frozenForest)), () -> {
node(cultivator, Seq.with(new SectorComplete(biomassFacility)), () -> {
});
});
});
node(Items.thorium, with(Items.titanium, 10000, Items.lead, 15000, Items.copper, 30000), () -> {
node(laserDrill, () -> {
node(blastDrill, () -> {
node(laserDrill, () -> {
node(blastDrill, Seq.with(new SectorComplete(nuclearComplex)), () -> {
});
node(waterExtractor, () -> {
node(oilExtractor, () -> {
});
});
});
});
});
});
node(Items.pyratite, with(Items.coal, 6000, Items.lead, 10000, Items.sand, 5000), () -> {
node(pyratiteMixer, () -> {
node(Items.blastCompound, with(Items.pyratite, 3000, Items.sporePod, 3000), () -> {
node(blastMixer, () -> {
node(waterExtractor, Seq.with(new SectorComplete(saltFlats)), () -> {
node(oilExtractor, () -> {
});
});
});
});
node(Items.silicon, with(Items.coal, 4000, Items.sand, 4000), () -> {
node(siliconSmelter, () -> {
node(Liquids.oil, with(Items.coal, 8000, Items.pyratite, 6000, Items.sand, 20000), () -> {
node(sporePress, () -> {
node(coalCentrifuge, () -> {
node(multiPress, () -> {
node(siliconCrucible, () -> {
});
});
});
node(Items.plastanium, with(Items.titanium, 10000, Items.silicon, 10000), () -> {
node(plastaniumCompressor, () -> {
node(Items.phasefabric, with(Items.thorium, 15000, Items.sand, 30000, Items.silicon, 5000), () -> {
node(phaseWeaver, () -> {
});
});
});
});
});
});
node(Items.metaglass, with(Items.sand, 6000, Items.lead, 10000), () -> {
node(kiln, () -> {
node(incinerator, () -> {
node(Items.scrap, with(Items.copper, 20000, Items.sand, 10000), () -> {
node(Liquids.slag, with(Items.scrap, 4000), () -> {
node(melter, () -> {
node(Items.surgealloy, with(Items.thorium, 20000, Items.silicon, 30000, Items.lead, 40000), () -> {
node(surgeSmelter, () -> {
});
});
node(separator, () -> {
node(pulverizer, () -> {
node(disassembler, () -> {
});
});
});
node(Liquids.cryofluid, with(Items.titanium, 8000, Items.metaglass, 5000), () -> {
node(cryofluidMixer, () -> {
});
});
});
});
});
});
});
});
node(microProcessor, () -> {
node(switchBlock, () -> {
node(message, () -> {
node(logicDisplay, () -> {
node(largeLogicDisplay, () -> {
});
});
node(memoryCell, () -> {
node(memoryBank, () -> {
});
});
});
node(logicProcessor, () -> {
node(hyperProcessor, () -> {
});
});
});
});
});
});
});
});
node(pyratiteMixer, () -> {
node(blastMixer, () -> {
node(combustionGenerator, () -> {
node(powerNode, () -> {
node(powerNodeLarge, () -> {
node(diode, () -> {
node(surgeTower, () -> {
});
});
node(siliconSmelter, () -> {
node(sporePress, () -> {
node(coalCentrifuge, () -> {
node(multiPress, () -> {
node(siliconCrucible, () -> {
});
});
});
node(battery, () -> {
node(batteryLarge, () -> {
node(plastaniumCompressor, Seq.with(new SectorComplete(windsweptIslands)), () -> {
node(phaseWeaver, Seq.with(new SectorComplete(tarFields)), () -> {
});
});
});
node(mender, () -> {
node(mendProjector, () -> {
node(forceProjector, () -> {
node(overdriveProjector, () -> {
node(overdriveDome, () -> {
node(kiln, Seq.with(new SectorComplete(craters)), () -> {
node(pulverizer, () -> {
node(incinerator, () -> {
node(melter, () -> {
node(surgeSmelter, () -> {
});
node(separator, () -> {
node(disassembler, () -> {
});
});
});
node(repairPoint, () -> {
node(cryofluidMixer, () -> {
});
});
});
node(steamGenerator, () -> {
node(thermalGenerator, () -> {
node(differentialGenerator, () -> {
node(thoriumReactor, Seq.with(new Research(Liquids.cryofluid)), () -> {
node(impactReactor, () -> {
});
node(rtgGenerator, () -> {
});
});
});
});
});
});
node(solarPanel, () -> {
node(largeSolarPanel, () -> {
node(microProcessor, () -> {
node(switchBlock, () -> {
node(message, () -> {
node(logicDisplay, () -> {
node(largeLogicDisplay, () -> {
});
});
node(memoryCell, () -> {
node(memoryBank, () -> {
});
});
});
node(logicProcessor, () -> {
node(hyperProcessor, () -> {
});
});
});
});
node(illuminator, () -> {
});
});
});
node(combustionGenerator, Seq.with(new Research(Items.coal)), () -> {
node(powerNode, () -> {
node(powerNodeLarge, () -> {
node(diode, () -> {
node(surgeTower, () -> {
});
});
});
node(battery, () -> {
node(batteryLarge, () -> {
});
});
node(mender, () -> {
node(mendProjector, () -> {
node(forceProjector, Seq.with(new SectorComplete(impact0078)), () -> {
node(overdriveProjector, Seq.with(new SectorComplete(impact0078)), () -> {
node(overdriveDome, Seq.with(new SectorComplete(impact0078)), () -> {
});
});
});
node(repairPoint, () -> {
});
});
});
node(steamGenerator, Seq.with(new SectorComplete(craters)), () -> {
node(thermalGenerator, () -> {
node(differentialGenerator, () -> {
node(thoriumReactor, Seq.with(new Research(Liquids.cryofluid)), () -> {
node(impactReactor, () -> {
});
node(rtgGenerator, () -> {
});
});
});
});
});
node(solarPanel, () -> {
node(largeSolarPanel, () -> {
});
});
});
});
});
@@ -316,12 +291,11 @@ public class TechTree implements ContentList{
});
node(scatter, () -> {
node(hail, () -> {
node(hail, Seq.with(new SectorComplete(craters)), () -> {
node(salvo, () -> {
node(swarmer, () -> {
node(cyclone, () -> {
node(spectre, () -> {
node(spectre, Seq.with(new SectorComplete(nuclearComplex)), () -> {
});
});
@@ -344,11 +318,17 @@ public class TechTree implements ContentList{
});
});
node(tsunami, () -> {
});
});
node(lancer, () -> {
node(meltdown, () -> {
node(foreshadow, () -> {
});
});
node(shockMine, () -> {
@@ -425,7 +405,7 @@ public class TechTree implements ContentList{
});
});
node(navalFactory, () -> {
node(navalFactory, Seq.with(new SectorComplete(ruinousShores)), () -> {
node(risso, () -> {
node(minke, () -> {
node(bryde, () -> {
@@ -440,10 +420,11 @@ public class TechTree implements ContentList{
});
});
node(additiveReconstructor, () -> {
node(additiveReconstructor, Seq.with(new SectorComplete(biomassFacility)), () -> {
node(multiplicativeReconstructor, () -> {
node(exponentialReconstructor, () -> {
node(exponentialReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
node(tetrativeReconstructor, () -> {
});
});
});
@@ -468,30 +449,75 @@ public class TechTree implements ContentList{
new Research(kiln),
new Research(mechanicalPump)
), () -> {
node(tarFields, Seq.with(
node(windsweptIslands, Seq.with(
new SectorComplete(ruinousShores),
new Research(coalCentrifuge),
new Research(conduit),
new Research(wave)
new Research(pneumaticDrill),
new Research(hail),
new Research(siliconSmelter),
new Research(steamGenerator)
), () -> {
node(desolateRift, Seq.with(
new SectorComplete(tarFields),
new Research(thermalGenerator),
new Research(thoriumReactor)
node(tarFields, Seq.with(
new SectorComplete(windsweptIslands),
new Research(coalCentrifuge),
new Research(conduit),
new Research(wave)
), () -> {
node(impact0078, Seq.with(
new SectorComplete(tarFields),
new Research(Items.thorium),
new Research(lancer),
new Research(salvo),
new Research(coreFoundation)
), () -> {
node(desolateRift, Seq.with(
new SectorComplete(impact0078),
new Research(thermalGenerator),
new Research(thoriumReactor),
new Research(coreNucleus)
), () -> {
node(planetaryTerminal, Seq.with(
new SectorComplete(desolateRift),
new SectorComplete(nuclearComplex),
new SectorComplete(overgrowth),
new SectorComplete(extractionOutpost),
new SectorComplete(saltFlats),
new Research(risso),
new Research(minke),
new Research(bryde),
new Research(spectre),
new Research(launchPad),
new Research(massDriver),
new Research(impactReactor),
new Research(additiveReconstructor),
new Research(exponentialReconstructor)
), () -> {
});
});
});
});
node(extractionOutpost, Seq.with(
new SectorComplete(stainedMountains),
new SectorComplete(windsweptIslands),
new Research(groundFactory),
new Research(nova),
new Research(airFactory),
new Research(mono)
), () -> {
});
});
node(saltFlats, Seq.with(
new SectorComplete(ruinousShores),
new Research(groundFactory),
new Research(airFactory),
new Research(door),
new Research(waterExtractor)
), () -> {
node(saltFlats, Seq.with(
new SectorComplete(windsweptIslands),
new Research(commandCenter),
new Research(groundFactory),
new Research(additiveReconstructor),
new Research(airFactory),
new Research(door)
), () -> {
});
});
});
@@ -500,6 +526,7 @@ public class TechTree implements ContentList{
new SectorComplete(fungalPass),
new Research(cultivator),
new Research(sporePress),
new Research(additiveReconstructor),
new Research(UnitTypes.mace),
new Research(UnitTypes.flare)
), () -> {
@@ -507,23 +534,92 @@ public class TechTree implements ContentList{
});
});
node(stainedMountains, Seq.with(
node(biomassFacility, Seq.with(
new SectorComplete(frozenForest),
new Research(pneumaticDrill),
new Research(powerNode),
new Research(steamGenerator)
new Research(steamGenerator),
new Research(scatter),
new Research(graphitePress)
), () -> {
node(fungalPass, Seq.with(
new SectorComplete(stainedMountains),
new Research(groundFactory),
new Research(door),
node(stainedMountains, Seq.with(
new SectorComplete(biomassFacility),
new Research(pneumaticDrill),
new Research(siliconSmelter)
), () -> {
node(nuclearComplex, Seq.with(
new SectorComplete(fungalPass),
new Research(thermalGenerator),
new Research(laserDrill)
node(fungalPass, Seq.with(
new SectorComplete(stainedMountains),
new Research(groundFactory),
new Research(door),
new Research(siliconSmelter)
), () -> {
node(nuclearComplex, Seq.with(
new SectorComplete(fungalPass),
new Research(thermalGenerator),
new Research(laserDrill),
new Research(Items.plastanium),
new Research(swarmer)
), () -> {
});
});
});
});
});
});
nodeProduce(Items.copper, () -> {
nodeProduce(Liquids.water, () -> {
});
nodeProduce(Items.lead, () -> {
nodeProduce(Items.titanium, () -> {
nodeProduce(Liquids.cryofluid, () -> {
});
nodeProduce(Items.thorium, () -> {
nodeProduce(Items.surgeAlloy, () -> {
});
nodeProduce(Items.phaseFabric, () -> {
});
});
});
nodeProduce(Items.metaglass, () -> {
});
});
nodeProduce(Items.sand, () -> {
nodeProduce(Items.scrap, () -> {
nodeProduce(Liquids.slag, () -> {
});
});
nodeProduce(Items.coal, () -> {
nodeProduce(Items.graphite, () -> {
nodeProduce(Items.silicon, () -> {
});
});
nodeProduce(Items.pyratite, () -> {
nodeProduce(Items.blastCompound, () -> {
});
});
nodeProduce(Items.sporePod, () -> {
});
nodeProduce(Liquids.oil, () -> {
nodeProduce(Items.plastanium, () -> {
});
});
@@ -534,35 +630,54 @@ public class TechTree implements ContentList{
}
public static void setup(){
TechNode.context = null;
context = null;
map = new ObjectMap<>();
all = new Seq<>();
}
public static TechNode node(UnlockableContent content, Runnable children){
//all the "node" methods are hidden, because they are for internal context-dependent use only
//for custom research, just use the TechNode constructor
static TechNode node(UnlockableContent content, Runnable children){
return node(content, content.researchRequirements(), children);
}
public static TechNode node(UnlockableContent content, ItemStack[] requirements, Runnable children){
return new TechNode(content, requirements, children);
static TechNode node(UnlockableContent content, ItemStack[] requirements, Runnable children){
return node(content, requirements, null, children);
}
public static TechNode node(UnlockableContent content, Seq<Objective> objectives, Runnable children){
TechNode node = new TechNode(content, content.researchRequirements(), children);
node.objectives = objectives;
static TechNode node(UnlockableContent content, ItemStack[] requirements, Seq<Objective> objectives, Runnable children){
TechNode node = new TechNode(context, content, requirements);
if(objectives != null){
node.objectives.addAll(objectives);
}
TechNode prev = context;
context = node;
children.run();
context = prev;
return node;
}
public static TechNode node(UnlockableContent block){
static TechNode node(UnlockableContent content, Seq<Objective> objectives, Runnable children){
return node(content, content.researchRequirements(), objectives, children);
}
static TechNode node(UnlockableContent block){
return node(block, () -> {});
}
public static TechNode create(UnlockableContent parent, UnlockableContent block){
TechNode.context = all.find(t -> t.content == parent);
return node(block, () -> {});
static TechNode nodeProduce(UnlockableContent content, Seq<Objective> objectives, Runnable children){
return node(content, content.researchRequirements(), objectives.and(new Produce(content)), children);
}
public static @Nullable TechNode get(UnlockableContent content){
static TechNode nodeProduce(UnlockableContent content, Runnable children){
return nodeProduce(content, new Seq<>(), children);
}
@Nullable
public static TechNode get(UnlockableContent content){
return map.get(content);
}
@@ -571,8 +686,6 @@ public class TechTree implements ContentList{
}
public static class TechNode{
static TechNode context;
/** Depth in tech tree. */
public int depth;
/** Requirement node. */
@@ -585,19 +698,16 @@ public class TechTree implements ContentList{
public final ItemStack[] finishedRequirements;
/** Extra objectives needed to research this. */
public Seq<Objective> objectives = new Seq<>();
/** Time required to research this content, in seconds. */
public float time;
/** Nodes that depend on this node. */
public final Seq<TechNode> children = new Seq<>();
TechNode(@Nullable TechNode ccontext, UnlockableContent content, ItemStack[] requirements, Runnable children){
if(ccontext != null) ccontext.children.add(this);
public TechNode(@Nullable TechNode parent, UnlockableContent content, ItemStack[] requirements){
if(parent != null) parent.children.add(this);
this.parent = ccontext;
this.parent = parent;
this.content = content;
this.requirements = requirements;
this.depth = parent == null ? 0 : parent.depth + 1;
this.time = Seq.with(requirements).mapFloat(i -> i.item.cost * i.amount).sum() * 10;
this.finishedRequirements = new ItemStack[requirements.length];
//load up the requirements that have been finished if settings are available
@@ -605,18 +715,33 @@ public class TechTree implements ContentList{
finishedRequirements[i] = new ItemStack(requirements[i].item, Core.settings == null ? 0 : Core.settings.getInt("req-" + content.name + "-" + requirements[i].item.name));
}
var used = new ObjectSet<Content>();
//add dependencies as objectives.
content.getDependencies(d -> objectives.add(new Research(d)));
content.getDependencies(d -> {
if(used.add(d)){
objectives.add(new Research(d));
}
});
map.put(content, this);
context = this;
children.run();
context = ccontext;
all.add(this);
}
TechNode(UnlockableContent content, ItemStack[] requirements, Runnable children){
this(context, content, requirements, children);
/** Resets finished requirements and saves. */
public void reset(){
for(ItemStack stack : finishedRequirements){
stack.amount = 0;
}
save();
}
/** Removes this node from the tech tree. */
public void remove(){
all.remove(this);
if(parent != null){
parent.children.remove(this);
}
}
/** Flushes research progress to settings. */

File diff suppressed because it is too large Load Diff

View File

@@ -1,337 +1,106 @@
package mindustry.content;
import arc.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.type.weather.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class Weathers implements ContentList{
public static Weather
rain,
snow,
sandstorm,
sporestorm;
sporestorm,
fog;
@Override
public void load(){
snow = new Weather("snow"){
TextureRegion region;
float yspeed = 2f, xspeed = 0.25f, padding = 16f, size = 12f, density = 1200f;
snow = new ParticleWeather("snow"){{
particleRegion = "particle";
sizeMax = 13f;
sizeMin = 2.6f;
density = 1200f;
attrs.set(Attribute.light, -0.15f);
{
attrs.set(Attribute.light, -0.15f);
}
sound = Sounds.windhowl;
soundVol = 0f;
soundVolOscMag = 1.5f;
soundVolOscScl = 1100f;
soundVolMin = 0.02f;
}};
@Override
public void load(){
super.load();
rain = new RainWeather("rain"){{
attrs.set(Attribute.light, -0.2f);
attrs.set(Attribute.water, 0.2f);
status = StatusEffects.wet;
sound = Sounds.rain;
soundVol = 0.25f;
}};
region = Core.atlas.find("circle-shadow");
}
sandstorm = new ParticleWeather("sandstorm"){{
color = noiseColor = Color.valueOf("f7cba4");
particleRegion = "particle";
drawNoise = true;
useWindVector = true;
sizeMax = 140f;
sizeMin = 70f;
minAlpha = 0f;
maxAlpha = 0.2f;
density = 1500f;
baseSpeed = 5.4f;
attrs.set(Attribute.light, -0.1f);
attrs.set(Attribute.water, -0.1f);
opacityMultiplier = 0.35f;
force = 0.1f;
sound = Sounds.wind;
soundVol = 0.8f;
duration = 7f * Time.toMinutes;
}};
@Override
public void drawOver(WeatherState state){
rand.setSeed(0);
Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale());
Tmp.r1.grow(padding);
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / density * state.intensity());
sporestorm = new ParticleWeather("sporestorm"){{
color = noiseColor = Color.valueOf("7457ce");
particleRegion = "circle-small";
drawNoise = true;
statusGround = false;
useWindVector = true;
sizeMax = 5f;
sizeMin = 2.5f;
minAlpha = 0.1f;
maxAlpha = 0.8f;
density = 2000f;
baseSpeed = 4.3f;
attrs.set(Attribute.spores, 1f);
attrs.set(Attribute.light, -0.15f);
status = StatusEffects.sporeSlowed;
opacityMultiplier = 0.5f;
force = 0.1f;
sound = Sounds.wind;
soundVol = 0.7f;
duration = 7f * Time.toMinutes;
}};
for(int i = 0; i < total; i++){
float scl = rand.random(0.5f, 1f);
float scl2 = rand.random(0.5f, 1f);
float sscl = rand.random(0.2f, 1f);
float x = (rand.random(0f, world.unitWidth()) + Time.time() * xspeed * scl2);
float y = (rand.random(0f, world.unitHeight()) - Time.time() * yspeed * scl);
x += Mathf.sin(y, rand.random(30f, 80f), rand.random(1f, 7f));
x -= Tmp.r1.x;
y -= Tmp.r1.y;
x = Mathf.mod(x, Tmp.r1.width);
y = Mathf.mod(y, Tmp.r1.height);
x += Tmp.r1.x;
y += Tmp.r1.y;
if(Tmp.r3.setCentered(x, y, size * sscl).overlaps(Tmp.r2)){
Draw.rect(region, x, y, size * sscl, size * sscl);
}
}
}
};
rain = new Weather("rain"){
float yspeed = 5f, xspeed = 1.5f, padding = 16f, size = 40f, density = 1200f;
TextureRegion[] splashes = new TextureRegion[12];
{
attrs.set(Attribute.light, -0.2f);
attrs.set(Attribute.water, 0.2f);
status = StatusEffects.wet;
}
@Override
public void load(){
super.load();
for(int i = 0; i < splashes.length; i++){
splashes[i] = Core.atlas.find("splash-" + i);
}
}
@Override
public void drawOver(WeatherState state){
Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale());
Tmp.r1.grow(padding);
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / density * state.intensity());
Lines.stroke(0.75f);
float alpha = Draw.getColor().a;
Draw.color(Color.royal, Color.white, 0.3f);
for(int i = 0; i < total; i++){
float scl = rand.random(0.5f, 1f);
float scl2 = rand.random(0.5f, 1f);
float sscl = rand.random(0.2f, 1f);
float x = (rand.random(0f, world.unitWidth()) + Time.time() * xspeed * scl2);
float y = (rand.random(0f, world.unitHeight()) - Time.time() * yspeed * scl);
float tint = rand.random(1f) * alpha;
x -= Tmp.r1.x;
y -= Tmp.r1.y;
x = Mathf.mod(x, Tmp.r1.width);
y = Mathf.mod(y, Tmp.r1.height);
x += Tmp.r1.x;
y += Tmp.r1.y;
if(Tmp.r3.setCentered(x, y, size * sscl).overlaps(Tmp.r2)){
Draw.alpha(tint);
Lines.lineAngle(x, y, Angles.angle(xspeed * scl2, - yspeed * scl), size*sscl/2f);
}
}
}
@Override
public void drawUnder(WeatherState state){
Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale());
Tmp.r1.grow(padding);
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / density * state.intensity()) / 2;
Lines.stroke(0.75f);
float t = Time.time() / 22f;
for(int i = 0; i < total; i++){
float offset = rand.random(0f, 1f);
float time = t + offset;
int pos = (int)((time));
float life = time % 1f;
float x = (rand.random(0f, world.unitWidth()) + pos*953);
float y = (rand.random(0f, world.unitHeight()) - pos*453);
x -= Tmp.r1.x;
y -= Tmp.r1.y;
x = Mathf.mod(x, Tmp.r1.width);
y = Mathf.mod(y, Tmp.r1.height);
x += Tmp.r1.x;
y += Tmp.r1.y;
if(Tmp.r3.setCentered(x, y, life * 4f).overlaps(Tmp.r2)){
Tile tile = world.tileWorld(x, y);
if(tile != null && tile.floor().liquidDrop == Liquids.water){
Draw.color(Tmp.c1.set(tile.floor().mapColor).mul(1.5f).a(state.opacity()));
Draw.rect(splashes[(int)(life * (splashes.length - 1))], x, y);
}else{
Draw.color(Color.royal, Color.white, 0.3f);
Draw.alpha(Mathf.slope(life) * state.opacity());
float space = 45f;
for(int j : new int[]{-1, 1}){
Tmp.v1.trns(90f + j*space, 1f + 5f * life);
Lines.lineAngle(x + Tmp.v1.x, y + Tmp.v1.y, 90f + j*space, 3f * (1f - life));
}
}
}
}
}
};
sandstorm = new Weather("sandstorm"){
TextureRegion region;
float size = 140f, padding = size, invDensity = 1500f, baseSpeed = 6.1f;
float force = 0.45f;
Color color = Color.valueOf("f7cba4");
Texture noise;
{
attrs.set(Attribute.light, -0.1f);
}
@Override
public void load(){
region = Core.atlas.find("circle-shadow");
noise = new Texture("sprites/noiseAlpha.png");
noise.setWrap(TextureWrap.repeat);
noise.setFilter(TextureFilter.linear);
}
@Override
public void dispose(){
noise.dispose();
}
@Override
public void update(WeatherState state){
float speed = force * state.intensity;
float windx = state.windVector.x * speed, windy = state.windVector.y * speed;
for(Unit unit : Groups.unit){
unit.impulse(windx, windy);
}
}
@Override
public void drawOver(WeatherState state){
Draw.tint(color);
float speed = baseSpeed * state.intensity;
float windx = state.windVector.x * speed, windy = state.windVector.y * speed;
float scale = 1f / 2000f;
float scroll = Time.time() * scale;
Tmp.tr1.texture = noise;
Core.camera.bounds(Tmp.r1);
Tmp.tr1.set(Tmp.r1.x*scale, Tmp.r1.y*scale, (Tmp.r1.x + Tmp.r1.width)*scale, (Tmp.r1.y + Tmp.r1.height)*scale);
Tmp.tr1.scroll(-windx * scroll, windy * scroll);
Draw.rect(Tmp.tr1, Core.camera.position.x, Core.camera.position.y, Core.camera.width, -Core.camera.height);
rand.setSeed(0);
Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale());
Tmp.r1.grow(padding);
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / invDensity * state.intensity());
Draw.tint(color);
float baseAlpha = Draw.getColor().a;
for(int i = 0; i < total; i++){
float scl = rand.random(0.5f, 1f);
float scl2 = rand.random(0.5f, 1f);
float sscl = rand.random(0.5f, 1f);
float x = (rand.random(0f, world.unitWidth()) + Time.time() * windx * scl2);
float y = (rand.random(0f, world.unitHeight()) + Time.time() * windy * scl);
float alpha = rand.random(0.2f);
x += Mathf.sin(y, rand.random(30f, 80f), rand.random(1f, 7f));
x -= Tmp.r1.x;
y -= Tmp.r1.y;
x = Mathf.mod(x, Tmp.r1.width);
y = Mathf.mod(y, Tmp.r1.height);
x += Tmp.r1.x;
y += Tmp.r1.y;
if(Tmp.r3.setCentered(x, y, size * sscl).overlaps(Tmp.r2)){
Draw.alpha(alpha * baseAlpha);
Draw.rect(region, x, y, size * sscl, size * sscl);
}
}
}
};
sporestorm = new Weather("sporestorm"){
TextureRegion region;
float size = 5f, padding = size, invDensity = 2000f, baseSpeed = 4.3f, force = 0.28f;
Color color = Color.valueOf("7457ce");
Texture noise;
{
attrs.set(Attribute.spores, 1f);
attrs.set(Attribute.light, -0.15f);
status = StatusEffects.sporeSlowed;
statusGround = false;
}
@Override
public void load(){
region = Core.atlas.find("circle-shadow");
noise = new Texture("sprites/noiseAlpha.png");
noise.setWrap(TextureWrap.repeat);
noise.setFilter(TextureFilter.linear);
}
@Override
public void update(WeatherState state){
float speed = force * state.intensity;
float windx = state.windVector.x * speed, windy = state.windVector.y * speed;
for(Unit unit : Groups.unit){
unit.impulse(windx, windy);
}
}
@Override
public void dispose(){
noise.dispose();
}
@Override
public void drawOver(WeatherState state){
Draw.alpha(state.opacity * 0.8f);
Draw.tint(color);
float speed = baseSpeed * state.intensity;
float windx = state.windVector.x * speed, windy = state.windVector.y * speed;
float scale = 1f / 2000f;
float scroll = Time.time() * scale;
Tmp.tr1.texture = noise;
Core.camera.bounds(Tmp.r1);
Tmp.tr1.set(Tmp.r1.x*scale, Tmp.r1.y*scale, (Tmp.r1.x + Tmp.r1.width)*scale, (Tmp.r1.y + Tmp.r1.height)*scale);
Tmp.tr1.scroll(-windx * scroll, windy * scroll);
Draw.rect(Tmp.tr1, Core.camera.position.x, Core.camera.position.y, Core.camera.width, -Core.camera.height);
rand.setSeed(0);
Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale());
Tmp.r1.grow(padding);
Core.camera.bounds(Tmp.r2);
int total = (int)(Tmp.r1.area() / invDensity * state.intensity());
Draw.tint(color);
float baseAlpha = state.opacity;
Draw.alpha(baseAlpha);
for(int i = 0; i < total; i++){
float scl = rand.random(0.5f, 1f);
float scl2 = rand.random(0.5f, 1f);
float sscl = rand.random(0.5f, 1f);
float x = (rand.random(0f, world.unitWidth()) + Time.time() * windx * scl2);
float y = (rand.random(0f, world.unitHeight()) + Time.time() * windy * scl);
float alpha = rand.random(0.1f, 0.8f);
x += Mathf.sin(y, rand.random(30f, 80f), rand.random(1f, 7f));
x -= Tmp.r1.x;
y -= Tmp.r1.y;
x = Mathf.mod(x, Tmp.r1.width);
y = Mathf.mod(y, Tmp.r1.height);
x += Tmp.r1.x;
y += Tmp.r1.y;
if(Tmp.r3.setCentered(x, y, size * sscl).overlaps(Tmp.r2)){
Draw.alpha(alpha * baseAlpha);
Fill.circle(x, y, size * sscl / 2f);
}
}
}
};
fog = new ParticleWeather("fog"){{
duration = 15f * Time.toMinutes;
noiseLayers = 3;
noiseLayerSclM = 0.8f;
noiseLayerAlphaM = 0.7f;
noiseLayerSpeedM = 2f;
noiseLayerSclM = 0.6f;
baseSpeed = 0.05f;
color = noiseColor = Color.grays(0.4f);
noiseScale = 1100f;
noisePath = "fog";
drawParticles = false;
drawNoise = true;
useWindVector = false;
xspeed = 1f;
yspeed = 0.01f;
attrs.set(Attribute.light, -0.3f);
attrs.set(Attribute.water, 0.05f);
opacityMultiplier = 0.47f;
}};
}
}

View File

@@ -1,20 +1,21 @@
package mindustry.core;
import arc.*;
import arc.files.*;
import arc.struct.*;
import arc.func.*;
import arc.graphics.*;
import arc.util.ArcAnnotate.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.entities.bullet.*;
import mindustry.mod.Mods.*;
import mindustry.type.*;
import mindustry.world.*;
import static arc.Core.files;
import static mindustry.Vars.mods;
import static arc.Core.*;
import static mindustry.Vars.*;
/**
* Loads all game content.
@@ -97,6 +98,8 @@ public class ContentLoader{
/** Calls Content#init() on everything. Use only after all modules have been created.*/
public void init(){
initialize(Content::init);
if(constants != null) constants.init();
Events.fire(new ContentInitEvent());
}
/** Calls Content#load() on everything. Use only after all modules have been created on the client.*/
@@ -161,8 +164,8 @@ public class ContentLoader{
public void removeLast(){
if(lastAdded != null && contentMap[lastAdded.getContentType().ordinal()].peek() == lastAdded){
contentMap[lastAdded.getContentType().ordinal()].pop();
if(lastAdded instanceof MappableContent){
contentNameMap[lastAdded.getContentType().ordinal()].remove(((MappableContent)lastAdded).name);
if(lastAdded instanceof MappableContent c){
contentNameMap[lastAdded.getContentType().ordinal()].remove(c.name);
}
}
}

View File

@@ -6,16 +6,18 @@ import arc.audio.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.*;
import mindustry.audio.*;
import mindustry.content.*;
import mindustry.content.TechTree.*;
import mindustry.core.GameState.*;
import mindustry.entities.*;
import mindustry.game.EventType.*;
import mindustry.game.Objectives.*;
import mindustry.game.*;
import mindustry.game.Saves.*;
import mindustry.gen.*;
@@ -23,10 +25,12 @@ import mindustry.input.*;
import mindustry.io.*;
import mindustry.io.SaveIO.*;
import mindustry.maps.Map;
import mindustry.maps.*;
import mindustry.net.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import java.io.*;
import java.text.*;
@@ -38,14 +42,13 @@ import static mindustry.Vars.*;
/**
* Control module.
* Handles all input, saving, keybinds and keybinds.
* Handles all input, saving and keybinds.
* Should <i>not</i> handle any logic-critical state.
* This class is not created in the headless server.
*/
public class Control implements ApplicationListener, Loadable{
public Saves saves;
public MusicControl music;
public Tutorial tutorial;
public SoundControl sound;
public InputHandler input;
private Interval timer = new Interval(2);
@@ -54,15 +57,11 @@ public class Control implements ApplicationListener, Loadable{
public Control(){
saves = new Saves();
tutorial = new Tutorial();
music = new MusicControl();
sound = new SoundControl();
Events.on(StateChangeEvent.class, event -> {
if((event.from == State.playing && event.to == State.menu) || (event.from == State.menu && event.to != State.menu)){
Time.runTask(5f, platform::updateRPC);
for(Sound sound : assets.getAll(Sound.class, new Seq<>())){
sound.stop();
}
}
});
@@ -91,7 +90,6 @@ public class Control implements ApplicationListener, Loadable{
Events.on(ResetEvent.class, event -> {
player.reset();
tutorial.reset();
hiscore = false;
saves.resetSave();
@@ -131,7 +129,23 @@ public class Control implements ApplicationListener, Loadable{
}
}));
Events.on(UnlockEvent.class, e -> ui.hudfrag.showUnlock(e.content));
Events.on(UnlockEvent.class, e -> {
ui.hudfrag.showUnlock(e.content);
checkAutoUnlocks();
if(e.content instanceof SectorPreset){
for(TechNode node : TechTree.all){
if(!node.content.unlocked() && node.objectives.contains(o -> o instanceof SectorComplete sec && sec.preset == e.content) && !node.objectives.contains(o -> !o.complete())){
ui.hudfrag.showToast(new TextureRegionDrawable(node.content.icon(Cicon.large)), bundle.get("available"));
}
}
}
});
Events.on(SectorCaptureEvent.class, e -> {
checkAutoUnlocks();
});
Events.on(BlockBuildEndEvent.class, e -> {
if(e.team == player.team()){
@@ -159,11 +173,9 @@ public class Control implements ApplicationListener, Loadable{
Events.on(GameOverEvent.class, e -> {
if(state.isCampaign() && !net.client() && !headless){
//delete the save, it is gone.
if(saves.getCurrent() != null && !state.rules.tutorial){
Sector sector = state.getSector();
sector.save = null;
saves.getCurrent().delete();
//save gameover sate immediately
if(saves.getCurrent() != null){
saves.getCurrent().save();
}
}
});
@@ -183,18 +195,23 @@ public class Control implements ApplicationListener, Loadable{
app.post(() -> ui.hudfrag.showLand());
renderer.zoomIn(Fx.coreLand.lifetime);
app.post(() -> Fx.coreLand.at(core.getX(), core.getY(), 0, core.block));
camera.position.set(core);
player.set(core);
Time.run(Fx.coreLand.lifetime, () -> {
Fx.launch.at(core);
Effect.shake(5f, 5f, core);
if(state.isCampaign()){
ui.announce("[accent]" + state.rules.sector.name() + "\n" +
(state.rules.sector.info.resources.any() ? "[lightgray]" + bundle.get("sectors.resources") + "[white] " +
state.rules.sector.info.resources.toString(" ", u -> u.emoji()) : ""), 5);
}
});
});
}
void resetCamera(){
}
@Override
public void loadAsync(){
Draw.scl = 1f / Core.atlas.find("scale_marker").width;
@@ -213,6 +230,17 @@ public class Control implements ApplicationListener, Loadable{
saves.load();
}
/** Automatically unlocks things with no requirements. */
void checkAutoUnlocks(){
if(net.client()) return;
for(TechNode node : TechTree.all){
if(!node.content.unlocked() && node.requirements.length == 0 && !node.objectives.contains(o -> !o.complete())){
node.content.unlock();
}
}
}
void createPlayer(){
player = Player.create();
player.name = Core.settings.getString("name");
@@ -257,59 +285,96 @@ public class Control implements ApplicationListener, Loadable{
});
}
//TODO move
public void handleLaunch(CoreBuild tile){
LaunchCorec ent = LaunchCore.create();
ent.set(tile);
ent.block(Blocks.coreShard);
ent.lifetime(Vars.launchDuration);
ent.add();
//remove schematic requirements from core
tile.items.remove(universe.getLastLoadout().requirements());
tile.items.remove(universe.getLaunchResources());
}
public void playSector(Sector sector){
playSector(sector, sector);
}
public void playSector(@Nullable Sector origin, Sector sector){
playSector(origin, sector, new WorldReloader());
}
void playSector(@Nullable Sector origin, Sector sector, WorldReloader reloader){
ui.loadAnd(() -> {
if(saves.getCurrent() != null && state.isGame()){
control.saves.getCurrent().save();
control.saves.resetSave();
}
ui.planet.hide();
SaveSlot slot = sector.save;
sector.planet.setLastSector(sector);
if(slot != null && !clearSectors){
try{
net.reset();
reloader.begin();
slot.load();
slot.setAutosave(true);
state.rules.sector = sector;
//if there is no base, simulate a new game and place the right loadout at the spawn position
//TODO this is broken?
if(state.rules.defaultTeam.cores().isEmpty()){
//kill all friendly units, since they should be dead anwyay
for(Unit unit : Groups.unit){
if(unit.team() == state.rules.defaultTeam){
unit.remove();
}
//no spawn set -> delete the sector save
if(sector.info.spawnPosition == 0){
//delete old save
sector.save = null;
slot.delete();
//play again
playSector(origin, sector, reloader);
return;
}
Tile spawn = world.tile(sector.getSpawnPosition());
//TODO PLACE CORRECT LOADOUT
Schematics.placeLoadout(universe.getLastLoadout(), spawn.x, spawn.y);
//set spawn for sector damage to use
Tile spawn = world.tile(sector.info.spawnPosition);
spawn.setBlock(Blocks.coreShard, state.rules.defaultTeam);
//add extra damage.
SectorDamage.apply(1f);
//reset wave so things are more fair
state.wave = 1;
//set up default wave time
state.wavetime = state.rules.waveSpacing * 2f;
//reset captured state
sector.info.wasCaptured = false;
//re-enable waves
state.rules.waves = true;
//reset win wave??
state.rules.winWave = state.rules.attackMode ? -1 : sector.preset != null && sector.preset.captureWave > 0 ? sector.preset.captureWave : state.rules.winWave > state.wave ? state.rules.winWave : 30;
//if there's still an enemy base left, fix it
if(state.rules.attackMode){
//replace all broken blocks
for(var plan : state.rules.waveTeam.data().blocks){
Tile tile = world.tile(plan.x, plan.y);
if(tile != null){
tile.setBlock(content.block(plan.block), state.rules.waveTeam, plan.rotation);
if(plan.config != null && tile.build != null){
tile.build.configureAny(plan.config);
}
}
}
state.rules.waveTeam.data().blocks.clear();
}
//kill all units, since they should be dead anyway
Groups.unit.clear();
Groups.fire.clear();
Groups.puddle.clear();
Schematics.placeLaunchLoadout(spawn.x, spawn.y);
//set up camera/player locations
player.set(spawn.x * tilesize, spawn.y * tilesize);
camera.position.set(player);
Events.fire(new SectorLaunchEvent(sector));
Events.fire(Trigger.newGame);
}
state.set(State.playing);
reloader.end();
}catch(SaveException e){
Log.err(e);
@@ -320,78 +385,21 @@ public class Control implements ApplicationListener, Loadable{
}
ui.planet.hide();
}else{
net.reset();
logic.reset();
reloader.begin();
world.loadSector(sector);
state.rules.sector = sector;
//assign origin when launching
state.secinfo.origin = origin;
state.secinfo.destination = origin;
sector.info.origin = origin;
sector.info.destination = origin;
logic.play();
control.saves.saveSector(sector);
Events.fire(new SectorLaunchEvent(sector));
Events.fire(Trigger.newGame);
reloader.end();
}
});
}
public void playTutorial(){
ui.showInfo("@indev.notready");
//TODO implement
//ui.showInfo("death");
/*
Zone zone = Zones.groundZero;
ui.loadAnd(() -> {
logic.reset();
net.reset();
world.beginMapLoad();
world.resize(zone.generator.width, zone.generator.height);
zone.generator.generate(world.tiles);
Tile coreb = null;
out:
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
if(world.rawTile(x, y).block() instanceof CoreBlock){
coreb = world.rawTile(x, y);
break out;
}
}
}
Geometry.circle(coreb.x, coreb.y, 10, (cx, cy) -> {
Tile tile = world.ltile(cx, cy);
if(tile != null && tile.team() == state.rules.defaultTeam && !(tile.block() instanceof CoreBlock)){
tile.remove();
}
});
Geometry.circle(coreb.x, coreb.y, 5, (cx, cy) -> world.tile(cx, cy).clearOverlay());
world.endMapLoad();
zone.rules.get(state.rules);
//TODO assign zone!!
//state.rules.zone = zone;
for(Building core : state.teams.playerCores()){
for(ItemStack stack : zone.getStartingItems()){
core.items.add(stack.item, stack.amount);
}
}
Building core = state.teams.playerCores().first();
core.items.clear();
logic.play();
state.rules.waveTimer = false;
state.rules.waveSpacing = 60f * 30;
state.rules.buildCostMultiplier = 0.3f;
state.rules.tutorial = true;
Events.fire(Trigger.newGame);
});*/
}
public boolean isHighScore(){
return hiscore;
}
@@ -403,27 +411,33 @@ public class Control implements ApplicationListener, Loadable{
try{
SaveIO.save(control.saves.getCurrent().file);
Log.info("Saved on exit.");
}catch(Throwable e){
e.printStackTrace();
}catch(Throwable t){
Log.err(t);
}
}
for(Music music : assets.getAll(Music.class, new Seq<>())){
music.stop();
}
content.dispose();
net.dispose();
Musics.dispose();
Sounds.dispose();
ui.editor.dispose();
if(ui != null && ui.editor != null) ui.editor.dispose();
}
@Override
public void pause(){
wasPaused = state.is(State.paused);
if(state.is(State.playing)) state.set(State.paused);
if(settings.getBool("backgroundpause", true)){
wasPaused = state.is(State.paused);
if(state.is(State.playing)) state.set(State.paused);
}
}
@Override
public void resume(){
if(state.is(State.paused) && !wasPaused){
if(state.is(State.paused) && !wasPaused && settings.getBool("backgroundpause", true)){
state.set(State.playing);
}
}
@@ -432,19 +446,6 @@ public class Control implements ApplicationListener, Loadable{
public void init(){
platform.updateRPC();
//just a regular reminder
if(!OS.prop("user.name").equals("anuke") && !OS.hasEnv("iknowwhatimdoing")){
app.post(() -> app.post(() -> {
ui.showStartupInfo("@indev.popup");
}));
}
//play tutorial on start
//TODO no tutorial right now
if(!settings.getBool("playedtutorial", false)){
//Core.app.post(() -> Core.app.post(this::playTutorial));
}
//display UI scale changed dialog
if(Core.settings.getBool("uiscalechanged", false)){
Core.app.post(() -> Core.app.post(() -> {
@@ -477,15 +478,11 @@ public class Control implements ApplicationListener, Loadable{
dialog.show();
}));
}
if(android){
Sounds.empty.loop(0f, 1f, 0f);
}
}
@Override
public void update(){
//TODO find out why this happens on Android
//this happens on Android and nobody knows why
if(assets == null) return;
saves.update();
@@ -498,8 +495,7 @@ public class Control implements ApplicationListener, Loadable{
input.updateState();
music.update();
loops.update();
sound.update();
if(Core.input.keyTap(Binding.fullscreen)){
boolean full = settings.getBool("fullscreen");
@@ -511,19 +507,28 @@ public class Control implements ApplicationListener, Loadable{
settings.put("fullscreen", !full);
}
if(Float.isNaN(Vars.player.x) || Float.isNaN(Vars.player.y)){
player.set(0, 0);
if(!player.dead()) player.unit().kill();
}
if(Float.isNaN(camera.position.x)) camera.position.x = world.unitWidth()/2f;
if(Float.isNaN(camera.position.y)) camera.position.y = world.unitHeight()/2f;
if(state.isGame()){
input.update();
if(state.rules.tutorial){
tutorial.update();
}
//auto-update rpc every 5 seconds
if(timer.get(0, 60 * 5)){
platform.updateRPC();
}
if(Core.input.keyTap(Binding.pause) && !state.isOutOfTime() && !scene.hasDialog() && !scene.hasKeyboard() && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
//unlock core items
var core = state.rules.defaultTeam.core();
if(!net.client() && core != null && state.isCampaign()){
core.items.each((i, a) -> i.unlock());
}
if(Core.input.keyTap(Binding.pause) && !scene.hasDialog() && !scene.hasKeyboard() && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
state.set(state.is(State.playing) ? State.paused : State.playing);
}

View File

@@ -2,8 +2,8 @@ package mindustry.core;
import arc.*;
import arc.assets.loaders.*;
import arc.struct.*;
import arc.files.*;
import arc.struct.*;
/** Handles files in a modded context. */
public class FileTree implements FileHandleResolver{

View File

@@ -1,7 +1,7 @@
package mindustry.core;
import arc.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -17,17 +17,15 @@ public class GameState{
/** Wave countdown in ticks. */
public float wavetime;
/** Whether the game is in game over state. */
public boolean gameOver = false, launched = false, serverPaused = false, wasTimeout;
public boolean gameOver = false, serverPaused = false, wasTimeout;
/** Map that is currently being played on. */
public @NonNull Map map = emptyMap;
public Map map = emptyMap;
/** The current game rules. */
public Rules rules = new Rules();
/** Statistics for this save/game. Displayed after game over. */
public Stats stats = new Stats();
public GameStats stats = new GameStats();
/** Global attributes of the environment, calculated by weather. */
public Attributes envAttrs = new Attributes();
/** Sector information. Only valid in the campaign. */
public SectorInfo secinfo = new SectorInfo();
/** Team data. Gets reset every new game. */
public Teams teams = new Teams();
/** Number of enemies in the game; only used clientside in servers. */
@@ -35,26 +33,27 @@ public class GameState{
/** Current game state. */
private State state = State.menu;
//TODO optimize
public Unit boss(){
return Groups.unit.find(u -> u.isBoss() && u.team == rules.waveTeam);
return teams.boss;
}
public void set(State astate){
//cannot pause when in multiplayer
if(astate == State.paused && net.active()) return;
Events.fire(new StateChangeEvent(state, astate));
state = astate;
}
public boolean hasSpawns(){
return rules.waves && !(isCampaign() && rules.attackMode);
}
/** Note that being in a campaign does not necessarily mean having a sector. */
public boolean isCampaign(){
return rules.sector != null;
}
/** @return whether the player is in a campaign and they are out of sector time */
public boolean isOutOfTime(){
return isCampaign() && isGame() && getSector().getTimeSpent() >= turnDuration;
}
public boolean hasSector(){
return rules.sector != null;
}
@@ -69,11 +68,11 @@ public class GameState{
}
public boolean isPaused(){
return (is(State.paused) && !net.active()) || (gameOver && !net.active()) || (serverPaused && !isMenu());
return (is(State.paused) && !net.active()) || (gameOver && (!net.active() || isCampaign())) || (serverPaused && !isMenu());
}
public boolean isPlaying(){
return state == State.playing;
return (state == State.playing) || (state == State.paused && !isPaused());
}
/** @return whether the current state is *not* the menu. */

View File

@@ -4,8 +4,8 @@ import arc.*;
import arc.math.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.GameState.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
@@ -14,9 +14,6 @@ import mindustry.maps.*;
import mindustry.type.*;
import mindustry.type.Weather.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.ConstructBlock.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import java.util.*;
@@ -37,36 +34,10 @@ public class Logic implements ApplicationListener{
Events.on(BlockDestroyEvent.class, event -> {
//blocks that get broken are appended to the team's broken block queue
Tile tile = event.tile;
Block block = tile.block();
//skip null entities or un-rebuildables, for obvious reasons; also skip client since they can't modify these requests
if(tile.build == null || !tile.block().rebuildable || net.client()) return;
if(block instanceof ConstructBlock){
ConstructBuild entity = tile.bc();
//update block to reflect the fact that something was being constructed
if(entity.cblock != null && entity.cblock.synthetic()){
block = entity.cblock;
}else{
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
return;
}
}
TeamData data = state.teams.get(tile.team());
//remove existing blocks that have been placed here.
//painful O(n) iteration + copy
for(int i = 0; i < data.blocks.size; i++){
BlockPlan b = data.blocks.get(i);
if(b.x == tile.x && b.y == tile.y){
data.blocks.removeIndex(i);
break;
}
}
data.blocks.addFirst(new BlockPlan(tile.x, tile.y, (short)tile.build.rotation, block.id, tile.build.config()));
tile.build.addPlan(true);
});
Events.on(BlockBuildEndEvent.class, event -> {
@@ -83,51 +54,117 @@ public class Logic implements ApplicationListener{
}
});
Events.on(LaunchItemEvent.class, e -> state.secinfo.handleItemExport(e.stack));
//when loading a 'damaged' sector, propagate the damage
Events.on(WorldLoadEvent.class, e -> {
Events.on(SaveLoadEvent.class, e -> {
if(state.isCampaign()){
long seconds = state.rules.sector.getSecondsPassed();
CoreBuild core = state.rules.defaultTeam.core();
SectorInfo info = state.rules.sector.info;
info.write();
//apply fractional damage based on how many turns have passed for this sector
float turnsPassed = seconds / (turnDuration / 60f);
//how much wave time has passed
int wavesPassed = info.wavesPassed;
if(state.rules.sector.hasWaves() && turnsPassed > 0 && state.rules.sector.hasBase()){
SectorDamage.apply(turnsPassed / sectorDestructionTurns);
//wave has passed, remove all enemies, they are assumed to be dead
if(wavesPassed > 0){
Groups.unit.each(u -> {
if(u.team == state.rules.waveTeam){
u.remove();
}
});
}
//add resources based on turns passed
if(state.rules.sector.save != null && core != null){
//update correct storage capacity
state.rules.sector.save.meta.secinfo.storageCapacity = core.storageCapacity;
//simulate passing of waves
if(wavesPassed > 0){
//simulate wave counter moving forward
state.wave += wavesPassed;
state.wavetime = state.rules.waveSpacing;
//add new items received
state.rules.sector.calculateReceivedItems().each((item, amount) -> core.items.add(item, amount));
SectorDamage.applyCalculatedDamage();
//clear received items
state.rules.sector.setExtraItems(new ItemSeq());
//validation
for(Item item : content.items()){
//ensure positive items
if(core.items.get(item) < 0) core.items.set(item, 0);
//cap the items
if(core.items.get(item) > core.storageCapacity) core.items.set(item, core.storageCapacity);
//make sure damaged buildings are counted
for(Tile tile : world.tiles){
if(tile.build != null && tile.build.damaged()){
indexer.notifyTileDamaged(tile.build);
}
}
}
state.rules.sector.setSecondsPassed(0);
}
//reset values
info.damage = 0f;
info.wavesPassed = 0;
info.hasCore = true;
info.secondsPassed = 0;
state.rules.sector.saveInfo();
}
});
Events.on(WorldLoadEvent.class, e -> {
//enable infinite ammo for wave team by default
state.rules.waveTeam.rules().infiniteAmmo = true;
if(state.isCampaign()){
//enable building AI on campaign unless the preset disables it
if(!(state.getSector().preset != null && !state.getSector().preset.useAI)){
state.rules.waveTeam.rules().ai = true;
}
state.rules.waveTeam.rules().aiTier = state.getSector().threat * 0.8f;
state.rules.waveTeam.rules().infiniteResources = true;
//fill enemy cores by default.
for(var core : state.rules.waveTeam.cores()){
for(Item item : content.items()){
core.items.set(item, core.block.itemCapacity);
}
}
}
//save settings
Core.settings.manualSave();
});
//sync research
Events.on(UnlockEvent.class, e -> {
if(net.server()){
Call.researched(e.content);
}
});
Events.on(SectorCaptureEvent.class, e -> {
if(!net.client() && e.sector == state.getSector() && e.sector.isBeingPlayed()){
for(Tile tile : world.tiles){
//convert all blocks to neutral, randomly killing them
if(tile.isCenter() && tile.build != null && tile.build.team == state.rules.waveTeam){
Building b = tile.build;
Call.setTeam(b, Team.derelict);
Time.run(Mathf.random(0f, 60f * 6f), () -> {
if(Mathf.chance(0.25)){
b.kill();
}
});
}
}
//kill all units
Groups.unit.each(u -> {
if(u.team == state.rules.waveTeam){
Time.run(Mathf.random(0f, 60f * 5f), u::kill);
}
});
}
});
//send out items to each client
Events.on(TurnEvent.class, e -> {
if(net.server() && state.isCampaign()){
int[] out = new int[content.items().size];
state.getSector().info.production.each((item, stat) -> {
out[item.id] = Math.max(0, (int)(stat.mean * turnDuration / 60));
});
Call.sectorProduced(out);
}
});
}
/** Adds starting items, resets wave time, and sets state to playing. */
@@ -167,18 +204,13 @@ public class Logic implements ApplicationListener{
}
public void skipWave(){
if(state.isCampaign()){
//warp time spent forward because the wave was just skipped.
state.secinfo.internalTimeSpent += state.wavetime;
}
state.wavetime = 0;
}
public void runWave(){
spawner.spawnEnemies();
state.wave++;
state.wavetime = state.hasSector() && state.getSector().isLaunchWave(state.wave) ? state.rules.waveSpacing * state.rules.launchWaveMultiplier : state.rules.waveSpacing;
state.wavetime = state.rules.waveSpacing;
Events.fire(new WaveEvent());
}
@@ -199,36 +231,23 @@ public class Logic implements ApplicationListener{
}
//if there's a "win" wave and no enemies are present, win automatically
if(state.rules.waves && state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()){
//the sector has been conquered - waves get disabled
state.rules.waves = false;
if(state.rules.waves && (state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()) ||
(state.rules.attackMode && state.rules.waveTeam.cores().isEmpty())){
//fire capture event
Events.fire(new SectorCaptureEvent(state.rules.sector));
//save, just in case
if(!headless){
control.saves.saveSector(state.rules.sector);
}
Call.sectorCapture();
}
}else{
if(!state.rules.attackMode && state.teams.playerCores().size == 0 && !state.gameOver){
state.gameOver = true;
Events.fire(new GameOverEvent(state.rules.waveTeam));
}else if(state.rules.attackMode){
Team alive = null;
//count # of teams alive
int countAlive = state.teams.getActive().count(TeamData::hasCore);
for(TeamData team : state.teams.getActive()){
if(team.hasCore()){
if(alive != null){
return;
}
alive = team.team;
}
}
if(alive != null && !state.gameOver){
Events.fire(new GameOverEvent(alive));
if((countAlive <= 1 || (!state.rules.pvp && state.rules.defaultTeam.core() == null)) && !state.gameOver){
//find team that won
TeamData left = state.teams.getActive().find(TeamData::hasCore);
Events.fire(new GameOverEvent(left == null ? Team.derelict : left.team));
state.gameOver = true;
}
}
@@ -236,14 +255,15 @@ public class Logic implements ApplicationListener{
}
private void updateWeather(){
state.rules.weather.removeAll(w -> w.weather == null);
for(WeatherEntry entry : state.rules.weather){
//update cooldown
entry.cooldown -= Time.delta;
//create new event when not active
if(entry.cooldown < 0 && !entry.weather.isActive()){
float duration = Mathf.random(entry.minDuration, entry.maxDuration);
if((entry.cooldown < 0 || entry.always) && !entry.weather.isActive()){
float duration = entry.always ? Float.POSITIVE_INFINITY : Mathf.random(entry.minDuration, entry.maxDuration);
entry.cooldown = duration + Mathf.random(entry.minFrequency, entry.maxFrequency);
Tmp.v1.setToRandomDirection();
Call.createWeather(entry.weather, entry.intensity, duration, Tmp.v1.x, Tmp.v1.y);
@@ -251,56 +271,29 @@ public class Logic implements ApplicationListener{
}
}
@Remote(called = Loc.both)
public static void launchZone(){
if(!state.isCampaign()) return;
@Remote(called = Loc.server)
public static void sectorCapture(){
//the sector has been conquered - waves get disabled
state.rules.waves = false;
if(!headless){
ui.hudfrag.showLaunch();
if(state.rules.sector == null){
//disable attack mode
state.rules.attackMode = false;
return;
}
//TODO better core launch effect
for(Building tile : state.teams.playerCores()){
Fx.launch.at(tile);
state.rules.sector.info.wasCaptured = true;
//fire capture event
Events.fire(new SectorCaptureEvent(state.rules.sector));
//disable attack mode
state.rules.attackMode = false;
//save, just in case
if(!headless && !net.client()){
control.saves.saveSector(state.rules.sector);
}
Sector sector = state.rules.sector;
//TODO containers must be launched too
Time.runTask(30f, () -> {
Sector origin = sector.save.meta.secinfo.origin;
if(origin != null){
ItemSeq stacks = origin.getExtraItems();
//add up all items into list
for(Building entity : state.teams.playerCores()){
entity.items.each(stacks::add);
}
//save received items
origin.setExtraItems(stacks);
}
//remove all the cores
state.teams.playerCores().each(b -> b.tile.remove());
state.launched = true;
state.gameOver = true;
//save over the data w/o the cores
sector.save.save();
//run a turn, since launching takes up a turn
universe.runTurn();
//TODO apply extra damage to sector
//sector.setTurnsPassed(sector.getTurnsPassed() + 3);
//TODO load the sector that was launched from
Events.fire(new LaunchEvent());
//manually fire game over event now
Events.fire(new GameOverEvent(state.rules.defaultTeam));
});
}
@Remote(called = Loc.both)
@@ -315,6 +308,53 @@ public class Logic implements ApplicationListener{
netClient.setQuiet();
}
//called when the remote server researches something
@Remote
public static void researched(Content content){
if(!(content instanceof UnlockableContent u)) return;
var node = u.node();
//unlock all direct dependencies on client, permanently
while(node != null){
node.content.unlock();
node = node.parent;
}
state.rules.researched.add(u.name);
}
//called when the remote server runs a turn and produces something
@Remote
public static void sectorProduced(int[] amounts){
if(!state.isCampaign()) return;
Planet planet = state.rules.sector.planet;
boolean any = false;
for(Item item : content.items()){
int am = amounts[item.id];
if(am > 0){
int sumMissing = planet.sectors.sum(s -> s.hasBase() ? s.info.storageCapacity - s.info.items.get(item) : 0);
if(sumMissing == 0) continue;
//how much % to add
double percent = Math.min((double)am / sumMissing, 1);
for(Sector sec : planet.sectors){
if(sec.hasBase()){
int added = (int)Math.ceil(((sec.info.storageCapacity - sec.info.items.get(item)) * percent));
sec.info.items.add(item, added);
any = true;
}
}
}
}
if(any){
for(Sector sec : planet.sectors){
sec.saveInfo();
}
}
}
@Override
public void dispose(){
//save the settings before quitting
@@ -332,24 +372,14 @@ public class Logic implements ApplicationListener{
if(state.isGame()){
if(!net.client()){
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.type().isCounted);
}
//force pausing when the player is out of sector time
if(state.isOutOfTime()){
if(!state.wasTimeout){
universe.displayTimeEnd();
state.wasTimeout = true;
}
//if no turn was run.
if(state.isOutOfTime()){
state.set(State.paused);
}
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.type.isCounted);
}
if(!state.isPaused()){
state.teams.updateTeamStats();
if(state.isCampaign()){
state.secinfo.update();
state.rules.sector.info.update();
}
if(state.isCampaign()){
@@ -358,7 +388,7 @@ public class Logic implements ApplicationListener{
Time.update();
//weather is serverside
if(!net.client()){
if(!net.client() && !state.isEditor()){
updateWeather();
for(TeamData data : state.teams.getActive()){
@@ -395,5 +425,4 @@ public class Logic implements ApplicationListener{
public boolean isWaitingWave(){
return (state.rules.waitEnemies || (state.wave >= state.rules.winWave && state.rules.winWave > 0)) && state.enemies > 0;
}
}

View File

@@ -21,6 +21,7 @@ import mindustry.net.Administration.*;
import mindustry.net.Net.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.modules.*;
@@ -32,7 +33,7 @@ import static mindustry.Vars.*;
public class NetClient implements ApplicationListener{
private static final float dataTimeout = 60 * 18;
private static final float playerSyncTime = 2;
public final static float viewScale = 2f;
public static final float viewScale = 2f;
private long ping;
private Interval timer = new Interval(5);
@@ -40,7 +41,7 @@ public class NetClient implements ApplicationListener{
private boolean connecting = false;
/** If true, no message will be shown on disconnect. */
private boolean quiet = false;
/** Whether to supress disconnect events completely.*/
/** Whether to suppress disconnect events completely.*/
private boolean quietReset = false;
/** Counter for data timeout. */
private float timeoutTime = 0f;
@@ -105,12 +106,10 @@ public class NetClient implements ApplicationListener{
Time.runTask(3f, ui.loadfrag::hide);
if(packet.reason != null){
if(packet.reason.equals("closed")){
ui.showSmall("@disconnect", "@disconnect.closed");
}else if(packet.reason.equals("timeout")){
ui.showSmall("@disconnect", "@disconnect.timeout");
}else if(packet.reason.equals("error")){
ui.showSmall("@disconnect", "@disconnect.error");
switch(packet.reason){
case "closed" -> ui.showSmall("@disconnect", "@disconnect.closed");
case "timeout" -> ui.showSmall("@disconnect", "@disconnect.timeout");
case "error" -> ui.showSmall("@disconnect", "@disconnect.error");
}
}else{
ui.showErrorMessage("@disconnect");
@@ -196,14 +195,14 @@ public class NetClient implements ApplicationListener{
}
//server console logging
Log.info("&y@: &lb@", player.name, message);
Log.info("&fi@: @", "&lc" + player.name, "&lw" + message);
//invoke event for all clients but also locally
//this is required so other clients get the correct name even if they don't know who's sending it yet
Call.sendMessage(message, colorizeName(player.id(), player.name), player);
}else{
//log command to console but with brackets
Log.info("<&y@: &lm@&lg>", player.name, message);
Log.info("<&fi@: @&fr>", "&lk" + player.name, "&lw" + message);
//a command was sent, now get the output
if(response.type != ResponseType.valid){
@@ -236,7 +235,7 @@ public class NetClient implements ApplicationListener{
ui.join.connect(ip, port);
}
@Remote(targets = Loc.client)
public static void ping(Player player, long time){
Call.pingResponse(player.con, time);
@@ -258,6 +257,11 @@ public class NetClient implements ApplicationListener{
public static void kick(KickReason reason){
netClient.disconnectQuietly();
logic.reset();
if(reason == KickReason.serverRestarting){
ui.join.reconnect();
return;
}
if(!reason.quiet){
if(reason.extraText() != null){
@@ -295,6 +299,13 @@ public class NetClient implements ApplicationListener{
setHudText(message);
}
@Remote(variants = Variant.both)
public static void announce(String message){
if(message == null) return;
ui.announce(message);
}
@Remote(variants = Variant.both)
public static void infoMessage(String message){
if(message == null) return;
@@ -317,15 +328,15 @@ public class NetClient implements ApplicationListener{
}
@Remote(variants = Variant.both, unreliable = true)
public static void onEffect(Effect effect, float x, float y, float rotation, Color color){
public static void effect(Effect effect, float x, float y, float rotation, Color color){
if(effect == null) return;
effect.at(x, y, rotation, color);
}
@Remote(variants = Variant.both)
public static void onEffectReliable(Effect effect, float x, float y, float rotation, Color color){
onEffect(effect, x, y, rotation, color);
public static void effectReliable(Effect effect, float x, float y, float rotation, Color color){
effect(effect, x, y, rotation, color);
}
@Remote(variants = Variant.both)
@@ -335,6 +346,13 @@ public class NetClient implements ApplicationListener{
ui.showInfoToast(message, duration);
}
@Remote(variants = Variant.both)
public static void warningToast(int unicode, String text){
if(text == null || Fonts.icon.getData().getGlyph((char)unicode) == null) return;
ui.hudfrag.showToast(Fonts.getGlyph(Fonts.icon, (char)unicode), text);
}
@Remote(variants = Variant.both)
public static void setRules(Rules rules){
state.rules = rules;
@@ -435,7 +453,7 @@ public class NetClient implements ApplicationListener{
tile.build.readAll(Reads.get(input), tile.build.version());
}
}catch(Exception e){
e.printStackTrace();
Log.err(e);
}
}
@@ -566,19 +584,19 @@ public class NetClient implements ApplicationListener{
BuildPlan[] requests = null;
if(player.isBuilder()){
//limit to 10 to prevent buffer overflows
int usedRequests = Math.min(player.builder().plans().size, 10);
int usedRequests = Math.min(player.unit().plans().size, 10);
int totalLength = 0;
//prevent buffer overflow by checking config length
for(int i = 0; i < usedRequests; i++){
BuildPlan plan = player.builder().plans().get(i);
if(plan.config instanceof byte[]){
int length = ((byte[])plan.config).length;
BuildPlan plan = player.unit().plans().get(i);
if(plan.config instanceof byte[] b){
int length = b.length;
totalLength += length;
}
if(totalLength > 2048){
if(totalLength > 1024){
usedRequests = i + 1;
break;
}
@@ -586,7 +604,7 @@ public class NetClient implements ApplicationListener{
requests = new BuildPlan[usedRequests];
for(int i = 0; i < usedRequests; i++){
requests[i] = player.builder().plans().get(i);
requests[i] = player.unit().plans().get(i);
}
}
@@ -600,9 +618,9 @@ public class NetClient implements ApplicationListener{
unit.x, unit.y,
player.unit().aimX(), player.unit().aimY(),
unit.rotation,
unit instanceof Mechc ? ((Mechc)unit).baseRotation() : 0,
unit instanceof Mechc m ? m.baseRotation() : 0,
unit.vel.x, unit.vel.y,
player.miner().mineTile(),
player.unit().mineTile,
player.boosting, player.shooting, ui.chatfrag.shown(), control.input.isBuilding,
requests,
Core.camera.position.x, Core.camera.position.y,

View File

@@ -7,7 +7,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import arc.util.CommandHandler.*;
import arc.util.io.*;
import arc.util.serialization.*;
@@ -19,6 +18,7 @@ import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.net.*;
import mindustry.net.Administration.*;
import mindustry.net.Packets.*;
@@ -41,7 +41,7 @@ public class NetServer implements ApplicationListener{
private static final Vec2 vector = new Vec2();
private static final Rect viewport = new Rect();
/** If a player goes away of their server-side coordinates by this distance, they get teleported back. */
private static final float correctDist = 16f;
private static final float correctDist = tilesize * 12f;
public final Administration admins = new Administration();
public final CommandHandler clientCommands = new CommandHandler("/");
@@ -164,7 +164,7 @@ public class NetServer implements ApplicationListener{
info.id = packet.uuid;
admins.save();
Call.infoMessage(con, "You are not whitelisted here.");
Log.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;
}
@@ -226,8 +226,8 @@ public class NetServer implements ApplicationListener{
writeBuffer.reset();
player.write(outputBuffer);
}catch(Throwable t){
t.printStackTrace();
con.kick(KickReason.nameEmpty);
err(t);
return;
}
@@ -248,11 +248,10 @@ public class NetServer implements ApplicationListener{
try{
RemoteReadServer.readPacket(packet.reader(), packet.type, con.player);
}catch(ValidateException e){
Log.debug("Validation failed for '@': @", e.player, e.getMessage());
debug("Validation failed for '@': @", e.player, e.getMessage());
}catch(RuntimeException e){
if(e.getCause() instanceof ValidateException){
ValidateException v = (ValidateException)e.getCause();
Log.debug("Validation failed for '@': @", v.player, v.getMessage());
if(e.getCause() instanceof ValidateException v){
debug("Validation failed for '@': @", v.player, v.getMessage());
}else{
throw e;
}
@@ -285,7 +284,7 @@ public class NetServer implements ApplicationListener{
}
StringBuilder result = new StringBuilder();
result.append(Strings.format("[orange]-- Commands Page[lightgray] @[gray]/[lightgray]@[orange] --\n\n", (page+1), pages));
result.append(Strings.format("[orange]-- Commands Page[lightgray] @[gray]/[lightgray]@[orange] --\n\n", (page + 1), pages));
for(int i = commandsPerPage * page; i < Math.min(commandsPerPage * (page + 1), clientCommands.getCommandList().size); i++){
Command command = clientCommands.getCommandList().get(i);
@@ -301,6 +300,15 @@ public class NetServer implements ApplicationListener{
}
});
clientCommands.<Player>register("a", "<message...>", "Send a message only to admins.", (args, player) -> {
if(!player.admin){
player.sendMessage("[scarlet]You must be admin to use this command.");
return;
}
Groups.player.each(Player::admin, a -> a.sendMessage(args[0], player, "[#" + Pal.adminChat.toString() + "]<A>" + NetClient.colorizeName(player.id, player.name)));
});
//duration of a a kick in seconds
int kickDuration = 60 * 60;
//voting round duration in seconds
@@ -331,16 +339,16 @@ public class NetServer implements ApplicationListener{
votes += d;
voted.addAll(player.uuid(), admins.getInfo(player.uuid()).lastIP);
Call.sendMessage(Strings.format("[lightgray]A player has voted on kicking[orange] @[].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
target.name, votes, votesRequired()));
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
player.name, target.name, votes, votesRequired()));
checkPass();
}
boolean checkPass(){
if(votes >= votesRequired()){
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] @[orange] will be banned from the server for @ minutes.", target.name, (kickDuration/60)));
target.getInfo().lastKicked = Time.millis() + kickDuration*1000;
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] @[orange] will be banned from the server for @ minutes.", target.name, (kickDuration / 60)));
target.getInfo().lastKicked = Time.millis() + kickDuration * 1000;
Groups.player.each(p -> p.uuid().equals(target.uuid()), p -> p.kick(KickReason.vote));
map[0] = null;
task.cancel();
@@ -410,11 +418,11 @@ public class NetServer implements ApplicationListener{
VoteSession session = new VoteSession(currentlyKicking, found);
session.vote(player, 1);
vtime.reset();
vtime.reset();
currentlyKicking[0] = session;
}
}else{
player.sendMessage("[scarlet]No player[orange]'" + args[0] + "'[scarlet] found.");
player.sendMessage("[scarlet]No player [orange]'" + args[0] + "'[scarlet] found.");
}
}
});
@@ -439,12 +447,17 @@ public class NetServer implements ApplicationListener{
return;
}
if(!arg[0].toLowerCase().equals("y") && !arg[0].toLowerCase().equals("n")){
if(currentlyKicking[0].target.team() != player.team()){
player.sendMessage("[scarlet]You can't vote for other teams.");
return;
}
if(!arg[0].equalsIgnoreCase("y") && !arg[0].equalsIgnoreCase("n")){
player.sendMessage("[scarlet]Vote either 'y' (yes) or 'n' (no).");
return;
}
int sign = arg[0].toLowerCase().equals("y") ? 1 : -1;
int sign = arg[0].equalsIgnoreCase("y") ? 1 : -1;
currentlyKicking[0].vote(player, sign);
}
});
@@ -485,7 +498,7 @@ public class NetServer implements ApplicationListener{
data.stream = new ByteArrayInputStream(stream.toByteArray());
player.con.sendStream(data);
Log.debug("Packed @ bytes of world data.", stream.size());
debug("Packed @ bytes of world data.", stream.size());
}
public void addPacketHandler(String type, Cons2<Player, String> handler){
@@ -497,7 +510,7 @@ public class NetServer implements ApplicationListener{
}
public static void onDisconnect(Player player, String reason){
//singleplayer multiplayer wierdness
//singleplayer multiplayer weirdness
if(player.con == null){
player.remove();
return;
@@ -510,7 +523,8 @@ public class NetServer implements ApplicationListener{
Call.playerDisconnect(player.id());
}
if(Config.showConnectMessages.bool()) Log.info("&lm[@] &lc@ has disconnected. &lg&fi(@)", player.uuid(), player.name, reason);
String message = Strings.format("&lb@&fi&lk has disconnected. &fi&lk[&lb@&fi&lk] (@)", player.name, player.uuid(), reason);
if(Config.showConnectMessages.bool()) info(message);
}
player.remove();
@@ -530,7 +544,7 @@ public class NetServer implements ApplicationListener{
public static void serverPacketUnreliable(Player player, String type, String contents){
serverPacketReliable(player, type, contents);
}
private static boolean invalid(float f){
return Float.isInfinite(f) || Float.isNaN(f);
}
@@ -577,7 +591,7 @@ public class NetServer implements ApplicationListener{
shooting = false;
}
if(!player.dead() && (player.unit().type().flying || !player.unit().type().canBoost)){
if(!player.dead() && (player.unit().type.flying || !player.unit().type.canBoost)){
boosting = false;
}
@@ -591,8 +605,8 @@ public class NetServer implements ApplicationListener{
player.unit().aim(pointerX, pointerY);
if(player.isBuilder()){
player.builder().clearBuilding();
player.builder().updateBuilding(building);
player.unit().clearBuilding();
player.unit().updateBuilding(building);
if(requests != null){
for(BuildPlan req : requests){
@@ -616,14 +630,12 @@ public class NetServer implements ApplicationListener{
con.rejectedRequests.add(req);
continue;
}
player.builder().plans().addLast(req);
player.unit().plans().addLast(req);
}
}
}
if(player.isMiner()){
player.miner().mineTile(mining);
}
player.unit().mineTile = mining;
con.rejectedRequests.clear();
@@ -631,12 +643,12 @@ public class NetServer implements ApplicationListener{
Unit unit = player.unit();
long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime);
float maxSpeed = ((player.unit().type().canBoost && player.unit().isFlying()) ? player.unit().type().boostMultiplier : 1f) * player.unit().type().speed;
float maxSpeed = unit.realSpeed();
if(unit.isGrounded()){
maxSpeed *= unit.floorSpeedMultiplier();
}
float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.1f;
float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.2f;
//ignore the position if the player thinks they're dead, or the unit is wrong
boolean ignorePosition = dead || unit.id != unitID;
@@ -693,15 +705,14 @@ public class NetServer implements ApplicationListener{
@Remote(targets = Loc.client, called = Loc.server)
public static void adminRequest(Player player, Player other, AdminAction action){
if(!player.admin){
Log.warn("ACCESS DENIED: Player @ / @ attempted to perform admin action '@' on '@' without proper security access.",
player.name, player.con.address, action.name(), other == null ? null : other.name);
if(!player.admin && !player.isLocal()){
warn("ACCESS DENIED: Player @ / @ attempted to perform admin action '@' on '@' without proper security access.",
player.name, player.con == null ? "null" : player.con.address, action.name(), other == null ? null : other.name);
return;
}
if(other == null || ((other.admin && !player.isLocal()) && other != player)){
Log.warn("@ attempted to perform admin action on nonexistant or admin player.", player.name);
warn("@ attempted to perform admin action on nonexistant or admin player.", player.name);
return;
}
@@ -713,10 +724,10 @@ public class NetServer implements ApplicationListener{
netServer.admins.banPlayerIP(other.con.address);
netServer.admins.banPlayerID(other.con.uuid);
other.kick(KickReason.banned);
Log.info("&lc@ has banned @.", player.name, other.name);
info("&lc@ has banned @.", player.name, other.name);
}else if(action == AdminAction.kick){
other.kick(KickReason.kick);
Log.info("&lc@ has kicked @.", player.name, other.name);
info("&lc@ has kicked @.", player.name, other.name);
}else if(action == AdminAction.trace){
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.con.modclient, other.con.mobile);
if(player.con != null){
@@ -724,7 +735,7 @@ public class NetServer implements ApplicationListener{
}else{
NetClient.traceInfo(other, info);
}
Log.info("&lc@ has requested trace info of @.", player.name, other.name);
info("&lc@ has requested trace info of @.", player.name, other.name);
}
}
@@ -738,7 +749,8 @@ public class NetServer implements ApplicationListener{
if(Config.showConnectMessages.bool()){
Call.sendMessage("[accent]" + player.name + "[accent] has connected.");
Log.info("&lm[@] &y@ has connected.", player.uuid(), player.name);
String message = Strings.format("&lb@&fi&lk has connected. &fi&lk[&lb@&fi&lk]", player.name, player.uuid());
info(message);
}
if(!Config.motd.string().equalsIgnoreCase("off")){
@@ -763,7 +775,6 @@ public class NetServer implements ApplicationListener{
@Override
public void update(){
if(!headless && !closing && net.server() && state.isMenu()){
closing = true;
ui.loadfrag.show("@server.closing");
@@ -787,9 +798,9 @@ public class NetServer implements ApplicationListener{
public void openServer(){
try{
net.host(Config.port.num());
info("&lcOpened a server on port @.", Config.port.num());
info("Opened a server on port @.", Config.port.num());
}catch(BindException e){
Log.err("Unable to host: Port already in use! Make sure no other servers are running on the same port in your network.");
err("Unable to host: Port already in use! Make sure no other servers are running on the same port in your network.");
state.set(State.menu);
}catch(IOException e){
err(e);
@@ -905,7 +916,6 @@ public class NetServer implements ApplicationListener{
}
String checkColor(String str){
for(int i = 1; i < str.length(); i++){
if(str.charAt(i) == ']'){
String color = str.substring(1, i);
@@ -931,7 +941,6 @@ public class NetServer implements ApplicationListener{
}
void sync(){
try{
Groups.player.each(p -> !p.isLocal(), player -> {
if(player.con == null || !player.con.isConnected()){
@@ -955,7 +964,7 @@ public class NetServer implements ApplicationListener{
}
}catch(IOException e){
e.printStackTrace();
Log.err(e);
}
}

View File

@@ -14,10 +14,18 @@ import mindustry.type.*;
import mindustry.ui.dialogs.*;
import rhino.*;
import java.net.*;
import static mindustry.Vars.*;
public interface Platform{
/** Dynamically loads a jar file. */
default Class<?> loadJar(Fi jar, String mainClass) throws Exception{
URLClassLoader classLoader = new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, getClass().getClassLoader());
return Class.forName(mainClass, true, classLoader);
}
/** Steam: Update lobby visibility.*/
default void updateLobby(){}
@@ -109,9 +117,10 @@ public interface Platform{
* @param cons Selection listener
* @param open Whether to open or save files
* @param extension File extension to filter
* @param title The title of the native dialog
*/
default void showFileChooser(boolean open, String extension, Cons<Fi> cons){
new FileChooser(open ? "@open" : "@save", file -> file.extEquals(extension), open, file -> {
default void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){
new FileChooser(title, file -> file.extEquals(extension), open, file -> {
if(!open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
}else{
@@ -120,8 +129,12 @@ public interface Platform{
}).show();
}
default void showFileChooser(boolean open, String extension, Cons<Fi> cons){
showFileChooser(open, open ? "@open": "@save", extension, cons);
}
/**
* Show a file chooser for multiple file types. Only supported on desktop.
* Show a file chooser for multiple file types.
* @param cons Selection listener
* @param extensions File extensions to filter
*/

View File

@@ -27,10 +27,14 @@ public class Renderer implements ApplicationListener{
public final Pixelator pixelator = new Pixelator();
public PlanetRenderer planets;
public @Nullable Bloom bloom;
public FrameBuffer effectBuffer = new FrameBuffer();
public float laserOpacity = 1f;
public boolean animateShields, drawWeather = true;
/** minZoom = zooming out, maxZoom = zooming in */
public float minZoom = 1.5f, maxZoom = 6f;
private Bloom bloom;
//TODO unused
private FxProcessor fx = new FxProcessor();
private Color clearColor = new Color(0f, 0f, 0f, 1f);
private float targetscale = Scl.scl(4);
@@ -53,7 +57,7 @@ public class Renderer implements ApplicationListener{
public void init(){
planets = new PlanetRenderer();
if(settings.getBool("bloom")){
if(settings.getBool("bloom", !ios)){
setupBloom();
}
}
@@ -63,8 +67,11 @@ public class Renderer implements ApplicationListener{
Color.white.set(1f, 1f, 1f, 1f);
Gl.clear(Gl.stencilBufferBit);
camerascale = Mathf.lerpDelta(camerascale, targetscale, 0.1f);
laserOpacity = Core.settings.getInt("lasersopacity") / 100f;
float dest = Mathf.round(targetscale, 0.5f);
camerascale = Mathf.lerpDelta(camerascale, dest, 0.1f);
if(Mathf.equal(camerascale, dest, 0.001f)) camerascale = dest;
laserOpacity = settings.getInt("lasersopacity") / 100f;
animateShields = settings.getBool("animatedshields");
if(landTime > 0){
landTime -= Time.delta;
@@ -108,7 +115,10 @@ public class Renderer implements ApplicationListener{
minimap.dispose();
effectBuffer.dispose();
blocks.dispose();
planets.dispose();
if(planets != null){
planets.dispose();
planets = null;
}
if(bloom != null){
bloom.dispose();
bloom = null;
@@ -118,10 +128,6 @@ public class Renderer implements ApplicationListener{
@Override
public void resize(int width, int height){
if(settings.getBool("bloom")){
setupBloom();
}
fx.resize(width, height);
}
@@ -140,9 +146,9 @@ public class Renderer implements ApplicationListener{
}
bloom = new Bloom(true);
}catch(Throwable e){
e.printStackTrace();
settings.put("bloom", false);
ui.showErrorMessage("@error.bloom");
Log.err(e);
}
}
@@ -200,7 +206,7 @@ public class Renderer implements ApplicationListener{
graphics.clear(clearColor);
Draw.reset();
if(Core.settings.getBool("animatedwater") || Core.settings.getBool("animatedshields")){
if(Core.settings.getBool("animatedwater") || animateShields){
effectBuffer.resize(graphics.getWidth(), graphics.getHeight());
}
@@ -217,8 +223,6 @@ public class Renderer implements ApplicationListener{
pixelator.register();
}
//TODO fx
Draw.draw(Layer.background, this::drawBackground);
Draw.draw(Layer.floor, blocks.floor::drawFloor);
Draw.draw(Layer.block - 1, blocks::drawShadows);
@@ -239,17 +243,23 @@ public class Renderer implements ApplicationListener{
}
if(bloom != null){
bloom.resize(graphics.getWidth() / 4, graphics.getHeight() / 4);
Draw.draw(Layer.bullet - 0.01f, bloom::capture);
Draw.draw(Layer.effect + 0.01f, bloom::render);
}
Draw.draw(Layer.plans, overlays::drawBottom);
if(settings.getBool("animatedshields") && Shaders.shield != null){
if(animateShields && Shaders.shield != null){
Draw.drawRange(Layer.shields, 1f, () -> effectBuffer.begin(Color.clear), () -> {
effectBuffer.end();
effectBuffer.blit(Shaders.shield);
});
Draw.drawRange(Layer.buildBeam, 1f, () -> effectBuffer.begin(Color.clear), () -> {
effectBuffer.end();
effectBuffer.blit(Shaders.buildBeam);
});
}
Draw.draw(Layer.overlayUI, overlays::drawTop);
@@ -301,12 +311,19 @@ public class Renderer implements ApplicationListener{
}
public void clampScale(){
float s = Scl.scl(1f);
targetscale = Mathf.clamp(targetscale, minScale(), Math.round(s * 6));
targetscale = Mathf.clamp(targetscale, minScale(), maxScale());
}
public float getDisplayScale(){
return camerascale;
}
public float minScale(){
return Scl.scl(1.5f);
return Scl.scl(minZoom);
}
public float maxScale(){
return Mathf.round(Scl.scl(maxZoom));
}
public float getScale(){
@@ -334,6 +351,7 @@ public class Renderer implements ApplicationListener{
FrameBuffer buffer = new FrameBuffer(w, h);
drawWeather = false;
float vpW = camera.width, vpH = camera.height, px = camera.position.x, py = camera.position.y;
disableUI = true;
camera.width = w;
@@ -359,8 +377,8 @@ public class Renderer implements ApplicationListener{
PixmapIO.writePNG(file, fullPixmap);
fullPixmap.dispose();
ui.showInfoFade(Core.bundle.format("screenshot", file.toString()));
drawWeather = true;
buffer.dispose();
}
}

View File

@@ -24,7 +24,7 @@ import mindustry.editor.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.LogicDialog;
import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.ui.fragments.*;
@@ -42,6 +42,7 @@ public class UI implements ApplicationListener, Loadable{
public MinimapFragment minimapfrag;
public PlayerListFragment listfrag;
public LoadingFragment loadfrag;
public HintsFragment hints;
public WidgetGroup menuGroup, hudGroup;
@@ -91,6 +92,12 @@ public class UI implements ApplicationListener, Loadable{
Core.scene = new Scene();
Core.input.addProcessor(Core.scene);
int[] insets = Core.graphics.getSafeInsets();
Core.scene.marginLeft = insets[0];
Core.scene.marginRight = insets[1];
Core.scene.marginTop = insets[2];
Core.scene.marginBottom = insets[3];
Tex.load();
Icon.load();
Styles.load();
@@ -104,7 +111,7 @@ public class UI implements ApplicationListener, Loadable{
Tooltips.getInstance().textProvider = text -> new Tooltip(t -> t.background(Styles.black5).margin(4f).add(text));
Core.settings.setErrorHandler(e -> {
e.printStackTrace();
Log.err(e);
Core.app.post(() -> showErrorMessage("Failed to access local storage.\nSettings will not be saved."));
});
@@ -140,12 +147,6 @@ public class UI implements ApplicationListener, Loadable{
}
}
//draw overlay for buttons
if(state.rules.tutorial){
control.tutorial.draw();
Draw.flush();
}
Events.fire(Trigger.uiDrawEnd);
}
@@ -156,6 +157,7 @@ public class UI implements ApplicationListener, Loadable{
menufrag = new MenuFragment();
hudfrag = new HudFragment();
hints = new HintsFragment();
chatfrag = new ChatFragment();
minimapfrag = new MinimapFragment();
listfrag = new PlayerListFragment();
@@ -262,11 +264,11 @@ public class UI implements ApplicationListener, Loadable{
TextField field = cont.field(def, t -> {}).size(330f, 50f).get();
field.setFilter((f, c) -> field.getText().length() < textLength && filter.acceptChar(f, c));
buttons.defaults().size(120, 54).pad(4);
buttons.button("@cancel", this::hide);
buttons.button("@ok", () -> {
confirmed.get(field.getText());
hide();
}).disabled(b -> field.getText().isEmpty());
buttons.button("@cancel", this::hide);
keyDown(KeyCode.enter, () -> {
String text = field.getText();
if(!text.isEmpty()){
@@ -328,20 +330,21 @@ public class UI implements ApplicationListener, Loadable{
/** Shows a label in the world. This label is behind everything. Does not fade. */
public void showLabel(String info, float duration, float worldx, float worldy){
Table table = new Table();
table.setFillParent(true);
var table = new Table(Styles.black3).margin(4);
table.touchable = Touchable.disabled;
table.update(() -> {
if(state.isMenu()) table.remove();
Vec2 v = Core.camera.project(worldx, worldy);
table.setPosition(v.x, v.y, Align.center);
});
table.actions(Actions.delay(duration), Actions.remove());
table.align(Align.center).table(Styles.black3, t -> t.margin(4).add(info).style(Styles.outlineLabel)).update(t -> {
Vec2 v = Core.camera.project(worldx, worldy);
t.setPosition(v.x, v.y, Align.center);
});
table.add(info).style(Styles.outlineLabel);
table.pack();
table.act(0f);
//make sure it's at the back
Core.scene.root.addChildAt(0, table);
table.getChildren().first().act(0f);
}
public void showInfo(String info){
@@ -356,6 +359,7 @@ public class UI implements ApplicationListener, Loadable{
hide();
listener.run();
}).size(110, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -364,6 +368,7 @@ public class UI implements ApplicationListener, Loadable{
getCell(cont).growX();
cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.left);
buttons.button("@ok", this::hide).size(110, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -378,6 +383,7 @@ public class UI implements ApplicationListener, Loadable{
cont.add(text).pad(2f).growX().wrap().get().setAlignment(Align.center);
cont.row();
cont.button("@ok", this::hide).size(120, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -388,7 +394,7 @@ public class UI implements ApplicationListener, Loadable{
public void showException(String text, Throwable exc){
loadfrag.hide();
new Dialog(""){{
String message = Strings.getFinalMesage(exc);
String message = Strings.getFinalMessage(exc);
setFillParent(true);
cont.margin(15);
@@ -405,6 +411,7 @@ public class UI implements ApplicationListener, Loadable{
cont.button("@ok", this::hide).size(110, 50).fillX().left();
cont.row();
cont.add(col).colspan(2).pad(2);
closeOnBack();
}}.show();
}
@@ -420,6 +427,7 @@ public class UI implements ApplicationListener, Loadable{
cont.add(text).width(400f).wrap().get().setAlignment(align, align);
cont.row();
buttons.button("@ok", this::hide).size(110, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -427,6 +435,7 @@ public class UI implements ApplicationListener, Loadable{
new Dialog(titleText){{
cont.margin(15).add(text).width(400f).wrap().left().get().setAlignment(Align.left, Align.left);
buttons.button("@ok", this::hide).size(110, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -436,6 +445,7 @@ public class UI implements ApplicationListener, Loadable{
titleTable.row();
titleTable.image().color(Pal.accent).height(3f).growX().pad(2f);
buttons.button("@ok", this::hide).size(110, 50).pad(4);
closeOnBack();
}}.show();
}
@@ -487,13 +497,20 @@ public class UI implements ApplicationListener, Loadable{
dialog.show();
}
/** Display text in the middle of the screen, then fade out. */
public void announce(String text){
Table t = new Table();
announce(text, 3);
}
/** Display text in the middle of the screen, then fade out. */
public void announce(String text, float duration){
Table t = new Table(Styles.black3);
t.touchable = Touchable.disabled;
t.background(Styles.black3).margin(8f)
.add(text).style(Styles.outlineLabel).labelAlign(Align.center);
t.margin(8f).add(text).style(Styles.outlineLabel).labelAlign(Align.center);
t.update(() -> t.setPosition(Core.graphics.getWidth()/2f, Core.graphics.getHeight()/2f, Align.center));
t.actions(Actions.fadeOut(3, Interp.pow4In), Actions.remove());
t.actions(Actions.fadeOut(duration, Interp.pow4In), Actions.remove());
t.pack();
t.act(0.1f);
Core.scene.add(t);
}
@@ -511,7 +528,7 @@ public class UI implements ApplicationListener, Loadable{
//TODO move?
public static String formatAmount(long number){
public static String formatAmount(int number){
if(number >= 1_000_000_000){
return Strings.fixed(number / 1_000_000_000f, 1) + "[gray]" + Core.bundle.get("unit.billions") + "[]";
}else if(number >= 1_000_000){

View File

@@ -2,8 +2,8 @@ package mindustry.core;
import arc.*;
import arc.Files.*;
import arc.struct.*;
import arc.files.*;
import arc.struct.*;
import arc.util.*;
import arc.util.io.*;
@@ -46,6 +46,23 @@ public class Version{
}
}
/** @return whether the version is greater than the specified version string, e.g. "120.1"*/
public static boolean isAtLeast(String str){
if(build <= 0 || str == null || str.isEmpty()) return true;
int dot = str.indexOf('.');
if(dot != -1){
int major = Strings.parseInt(str.substring(0, dot), 0), minor = Strings.parseInt(str.substring(dot + 1), 0);
return build > major || (build == major && revision >= minor);
}else{
return build >= Strings.parseInt(str, 0);
}
}
public static String buildString(){
return build < 0 ? "custom" : build + (revision == 0 ? "" : "." + revision);
}
/** get menu version without colors */
public static String combined(){
if(build == -1){

View File

@@ -6,7 +6,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.struct.ObjectIntMap.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import arc.util.noise.*;
import mindustry.content.*;
@@ -31,7 +30,7 @@ import static mindustry.Vars.*;
public class World{
public final Context context = new Context();
public @NonNull Tiles tiles = new Tiles(0, 0);
public Tiles tiles = new Tiles(0, 0);
private boolean generating, invalidMap;
private ObjectMap<Map, Runnable> customMapLoaders = new ObjectMap<>();
@@ -66,6 +65,11 @@ public class World{
return tile == null || tile.block().solid;
}
public boolean wallSolidFull(int x, int y){
Tile tile = tile(x, y);
return tile == null || (tile.block().solid && tile.block().fillsTile);
}
public boolean isAccessible(int x, int y){
return !wallSolid(x, y - 1) || !wallSolid(x, y + 1) || !wallSolid(x - 1, y) || !wallSolid(x + 1, y);
}
@@ -86,13 +90,11 @@ public class World{
return height()*tilesize;
}
@NonNull
public Floor floor(int x, int y){
Tile tile = tile(x, y);
return tile == null ? Blocks.air.asFloor() : tile.floor();
}
@NonNull
public Floor floorWorld(float x, float y){
Tile tile = tileWorld(x, y);
return tile == null ? Blocks.air.asFloor() : tile.floor();
@@ -132,7 +134,6 @@ public class World{
return tile.build;
}
@NonNull
public Tile rawTile(int x, int y){
return tiles.getn(x, y);
}
@@ -147,7 +148,17 @@ public class World{
return build(Math.round(x / tilesize), Math.round(y / tilesize));
}
public int toTile(float coord){
/** Convert from world to logic tile coordinates. Whole numbers are at centers of tiles. */
public static float conv(float coord){
return coord / tilesize;
}
/** Convert from tile to world coordinates. */
public static float unconv(float coord){
return coord * tilesize;
}
public static int toTile(float coord){
return Math.round(coord / tilesize);
}
@@ -175,22 +186,22 @@ public class World{
/**
* Call to signify the beginning of map loading.
* BuildinghangeEvents will not be fired until endMapLoad().
* TileEvents will not be fired until endMapLoad().
*/
public void beginMapLoad(){
generating = true;
}
/**
* Call to signify the end of map loading. Updates tile occlusions and sets up physics for the world.
* Call to signify the end of map loading. Updates tile proximities and sets up physics for the world.
* A WorldLoadEvent will be fire.
*/
public void endMapLoad(){
for(Tile tile : tiles){
//remove legacy blocks; they need to stop existing
if(tile.block() instanceof LegacyBlock){
tile.remove();
if(tile.block() instanceof LegacyBlock l){
l.removeSelf(tile);
continue;
}
@@ -252,12 +263,12 @@ public class World{
setSectorRules(sector);
if(state.rules.defaultTeam.core() != null){
sector.setSpawnPosition(state.rules.defaultTeam.core().pos());
sector.info.spawnPosition = state.rules.defaultTeam.core().pos();
}
}
private void setSectorRules(Sector sector){
state.map = new Map(StringMap.of("name", sector.planet.localizedName + "; Sector " + sector.id));
state.map = new Map(StringMap.of("name", sector.preset == null ? sector.planet.localizedName + "; Sector " + sector.id : sector.preset.localizedName));
state.rules.sector = sector;
state.rules.weather.clear();
@@ -266,8 +277,6 @@ public class World{
ObjectIntMap<Block> floorc = new ObjectIntMap<>();
ObjectSet<UnlockableContent> content = new ObjectSet<>();
float waterFloors = 0, totalFloors = 0;
for(Tile tile : world.tiles){
if(world.getDarkness(tile.x, tile.y) >= 3){
continue;
@@ -279,10 +288,6 @@ public class World{
if(liquid != null) content.add(liquid);
if(!tile.block().isStatic()){
totalFloors ++;
if(liquid == Liquids.water){
waterFloors += tile.floor().isDeep() ? 1f : 0.7f;
}
floorc.increment(tile.floor());
if(tile.overlay() != Blocks.air){
floorc.increment(tile.overlay());
@@ -297,16 +302,14 @@ public class World{
entries.removeAll(e -> e.value < 30);
Block[] floors = new Block[entries.size];
int[] floorCounts = new int[entries.size];
for(int i = 0; i < entries.size; i++){
floorCounts[i] = entries.get(i).value;
floors[i] = entries.get(i).key;
}
//TODO bad code
boolean hasSnow = floors[0].name.contains("ice") || floors[0].name.contains("snow");
boolean hasRain = !hasSnow && floors[0].name.contains("water");
boolean hasDesert = !hasSnow && !hasRain && floors[0].name.contains("sand");
boolean hasRain = !hasSnow && content.contains(Liquids.water) && !floors[0].name.contains("sand");
boolean hasDesert = !hasSnow && !hasRain && floors[0] == Blocks.sand;
boolean hasSpores = floors[0].name.contains("spore") || floors[0].name.contains("moss") || floors[0].name.contains("tainted");
if(hasSnow){
@@ -315,6 +318,7 @@ public class World{
if(hasRain){
state.rules.weather.add(new WeatherEntry(Weathers.rain));
state.rules.weather.add(new WeatherEntry(Weathers.fog));
}
if(hasDesert){
@@ -325,9 +329,9 @@ public class World{
state.rules.weather.add(new WeatherEntry(Weathers.sporestorm));
}
state.secinfo.resources = content.asArray();
state.secinfo.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
sector.info.resources = content.asArray();
sector.info.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
sector.saveInfo();
}
public Context filterContext(Map map){
@@ -439,7 +443,6 @@ public class World{
int err = dx - dy;
int e2;
while(true){
if(cons.accept(x0, y0)) return true;
if(x0 == x1 && y0 == y1) return false;
@@ -539,7 +542,7 @@ public class World{
int circleDst = (int)(rawDst - (length - circleBlend));
if(circleDst > 0){
dark = Math.max(circleDst / 1f, dark);
dark = Math.max(circleDst, dark);
}
}
@@ -610,6 +613,7 @@ public class World{
GenerateInput input = new GenerateInput();
for(GenerateFilter filter : filters){
filter.randomize();
input.begin(filter, width(), height(), (x, y) -> tiles.getn(x, y));
filter.apply(tiles, input);
}

View File

@@ -2,7 +2,6 @@ package mindustry.ctype;
import arc.files.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.*;
import mindustry.mod.Mods.*;
@@ -10,8 +9,7 @@ import mindustry.mod.Mods.*;
public abstract class Content implements Comparable<Content>, Disposable{
public final short id;
/** Info on which mod this content was loaded from. */
public @NonNull ModContentInfo minfo = new ModContentInfo();
public ModContentInfo minfo = new ModContentInfo();
public Content(){
this.id = (short)Vars.content.getBy(getContentType()).size;
@@ -33,7 +31,7 @@ public abstract class Content implements Comparable<Content>, Disposable{
*/
public void load(){}
/** @return whether an error ocurred during mod loading. */
/** @return whether an error occurred during mod loading. */
public boolean hasErrored(){
return minfo.error != null;
}

View File

@@ -4,25 +4,34 @@ import arc.*;
import arc.func.*;
import arc.graphics.g2d.*;
import arc.scene.ui.layout.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.content.TechTree.*;
import mindustry.game.EventType.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
/** Base interface for an unlockable content type. */
public abstract class UnlockableContent extends MappableContent{
/** Stat storage for this content. Initialized on demand. */
public Stats stats = new Stats();
/** Localized, formal name. Never null. Set to internal name if not found in bundle. */
public String localizedName;
/** Localized description. May be null. */
public @Nullable String description;
/** Localized description & details. May be null. */
public @Nullable String description, details;
/** Whether this content is always unlocked in the tech tree. */
public boolean alwaysUnlocked = false;
/** Whether to show the description in the research dialog preview. */
public boolean inlineDescription = true;
/** Special logic icon ID. */
public int iconId = 0;
/** Icons by Cicon ID.*/
protected TextureRegion[] cicons = new TextureRegion[mindustry.ui.Cicon.all.length];
protected TextureRegion[] cicons = new TextureRegion[Cicon.all.length];
/** Unlock state. Loaded from settings. Do not modify outside of the constructor. */
protected boolean unlocked;
@@ -31,13 +40,31 @@ public abstract class UnlockableContent extends MappableContent{
this.localizedName = Core.bundle.get(getContentType() + "." + this.name + ".name", this.name);
this.description = Core.bundle.getOrNull(getContentType() + "." + this.name + ".description");
this.details = Core.bundle.getOrNull(getContentType() + "." + this.name + ".details");
this.unlocked = Core.settings != null && Core.settings.getBool(this.name + "-unlocked", false);
}
/** @return the tech node for this content. may be null. */
public @Nullable TechNode node(){
return TechTree.get(this);
}
public String displayDescription(){
return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName());
}
/** Checks stat initialization state. Call before displaying stats. */
public void checkStats(){
if(!stats.intialized){
setStats();
stats.intialized = true;
}
}
/** Initializes stats on demand. Should only be called once. Only called before something is displayed. */
public void setStats(){
}
/** Generate any special icons for this content. Called asynchronously.*/
@CallSuper
public void createIcons(MultiPacker packer){
@@ -59,9 +86,11 @@ public abstract class UnlockableContent extends MappableContent{
cicons[icon.ordinal()] =
Core.atlas.find(getContentType().name() + "-" + name + "-" + icon.name(),
Core.atlas.find(getContentType().name() + "-" + name + "-full",
Core.atlas.find(name + "-" + icon.name(),
Core.atlas.find(name + "-full",
Core.atlas.find(name,
Core.atlas.find(getContentType().name() + "-" + name,
Core.atlas.find(name + "1")))));
Core.atlas.find(name + "1")))))));
}
return cicons[icon.ordinal()];
}
@@ -73,7 +102,9 @@ public abstract class UnlockableContent extends MappableContent{
}
/** This should show all necessary info about this content in the specified table. */
public abstract void displayInfo(Table table);
public void display(Table table){
}
/** Called when this content is unlocked. Use this to unlock other related content. */
public void onUnlock(){
@@ -86,7 +117,7 @@ public abstract class UnlockableContent extends MappableContent{
/** Makes this piece of content unlocked; if it already unlocked, nothing happens. */
public void unlock(){
if(!unlocked()){
if(!unlocked && !alwaysUnlocked){
unlocked = true;
Core.settings.put(name + "-unlocked", true);
@@ -95,16 +126,33 @@ public abstract class UnlockableContent extends MappableContent{
}
}
public final boolean unlocked(){
/** Unlocks this content, but does not fire any events. */
public void quietUnlock(){
if(!unlocked()){
unlocked = true;
Core.settings.put(name + "-unlocked", true);
}
}
public boolean unlocked(){
if(net != null && net.client()) return unlocked || alwaysUnlocked || state.rules.researched.contains(name);
return unlocked || alwaysUnlocked;
}
/** @return whether this content is unlocked, or the player is in a custom (non-campaign) game. */
public final boolean unlockedNow(){
return unlocked || alwaysUnlocked || !state.isCampaign();
/** Locks this content again. */
public void clearUnlock(){
if(unlocked){
unlocked = false;
Core.settings.put(name + "-unlocked", false);
}
}
public final boolean locked(){
/** @return whether this content is unlocked, or the player is in a custom (non-campaign) game. */
public boolean unlockedNow(){
return unlocked() || !state.isCampaign();
}
public boolean locked(){
return !unlocked();
}
}

View File

@@ -1,20 +1,19 @@
package mindustry.editor;
import arc.struct.*;
import mindustry.annotations.Annotations.*;
import arc.struct.LongSeq;
import mindustry.game.Team;
import mindustry.gen.TileOp;
import mindustry.world.Block;
import mindustry.world.Tile;
import mindustry.world.blocks.environment.Floor;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.content;
import static mindustry.Vars.*;
public class DrawOperation{
private MapEditor editor;
private LongSeq array = new LongSeq();
public DrawOperation(MapEditor editor) {
public DrawOperation(MapEditor editor){
this.editor = editor;
}
@@ -38,7 +37,7 @@ public class DrawOperation{
}
}
private void updateTile(int i) {
private void updateTile(int i){
long l = array.get(i);
array.set(i, TileOp.get(TileOp.x(l), TileOp.y(l), TileOp.type(l), getTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l))));
setTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l), TileOp.value(l));

View File

@@ -1,7 +1,6 @@
package mindustry.editor;
import arc.func.*;
import arc.util.ArcAnnotate.*;
import mindustry.content.*;
import mindustry.editor.DrawOperation.*;
import mindustry.game.*;
@@ -19,7 +18,7 @@ public class EditorTile extends Tile{
}
@Override
public void setFloor(@NonNull Floor type){
public void setFloor(Floor type){
if(skip()){
super.setFloor(type);
return;
@@ -27,7 +26,7 @@ public class EditorTile extends Tile{
if(type instanceof OverlayFloor){
//don't place on liquids
if(!floor.isLiquid){
if(floor.hasSurface() || !type.needsSurface){
setOverlayID(type.id);
}
return;
@@ -51,9 +50,19 @@ public class EditorTile extends Tile{
return;
}
op(OpType.block, block.id);
if(rotation != 0) op(OpType.rotation, (byte)rotation);
if(team != Team.derelict) op(OpType.team, (byte)team.id);
if(!isCenter()){
EditorTile cen = (EditorTile)build.tile;
cen.op(OpType.rotation, (byte)build.rotation);
cen.op(OpType.team, (byte)build.team.id);
cen.op(OpType.block, block.id);
update();
}else{
if(build != null) op(OpType.rotation, (byte)build.rotation);
if(build != null) op(OpType.team, (byte)build.team.id);
op(OpType.block, block.id);
}
super.setBlock(type, team, rotation);
}
@@ -67,6 +76,8 @@ public class EditorTile extends Tile{
if(getTeamID() == team.id) return;
op(OpType.team, (byte)getTeamID());
super.setTeam(team);
getLinkedTiles(t -> ui.editor.editor.renderer.updatePoint(t.x, t.y));
}
@Override
@@ -76,7 +87,7 @@ public class EditorTile extends Tile{
return;
}
if(floor.isLiquid) return;
if(!floor.hasSurface() && overlay.asFloor().needsSurface) return;
if(overlay() == overlay) return;
op(OpType.overlay, this.overlay.id);
super.setOverlay(overlay);
@@ -97,11 +108,18 @@ public class EditorTile extends Tile{
super.recache();
}
}
@Override
protected void changeEntity(Team team, Prov<Building> entityprov, int rotation){
protected void changed(){
if(state.isGame()){
super.changed();
}
}
@Override
protected void changeBuild(Team team, Prov<Building> entityprov, int rotation){
if(skip()){
super.changeEntity(team, entityprov, rotation);
super.changeBuild(team, entityprov, rotation);
return;
}
@@ -112,7 +130,7 @@ public class EditorTile extends Tile{
Block block = block();
if(block.hasEntity()){
if(block.hasBuilding()){
build = entityprov.get().init(this, team, false, rotation);
build.cons = new ConsumeModule(build);
if(block.hasItems) build.items = new ItemModule();

View File

@@ -118,7 +118,7 @@ public enum EditorTool{
if(editor.drawBlock.isOverlay()){
Block dest = tile.overlay();
if(dest == editor.drawBlock) return;
tester = t -> t.overlay() == dest && !t.floor().isLiquid;
tester = t -> t.overlay() == dest && (t.floor().hasSurface() || !t.floor().needsSurface);
setter = t -> t.setOverlay(editor.drawBlock);
}else if(editor.drawBlock.isFloor()){
Block dest = tile.floor();

View File

@@ -4,6 +4,7 @@ import arc.files.*;
import arc.func.*;
import arc.graphics.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.content.*;
import mindustry.editor.DrawOperation.*;
@@ -136,7 +137,7 @@ public class MapEditor{
if(isFloor){
tile.setFloor(drawBlock.asFloor());
}else{
}else if(!(tile.block().isMultiblock() && !drawBlock.isMultiblock())){
if(drawBlock.rotate && tile.build != null && tile.build.rotation != rotation){
addTileOp(TileOp.get(tile.x, tile.y, (byte)OpType.rotation.ordinal(), (byte)rotation));
}
@@ -156,7 +157,7 @@ public class MapEditor{
boolean hasOverlap(int x, int y){
Tile tile = world.tile(x, y);
//allow direct replacement of blocks of the same size
if(tile != null && tile.isCenter() && tile.block() != drawBlock && tile.block().size == drawBlock.size){
if(tile != null && tile.isCenter() && tile.block() != drawBlock && tile.block().size == drawBlock.size && tile.x == x && tile.y == y){
return false;
}
@@ -167,12 +168,10 @@ public class MapEditor{
for(int dy = 0; dy < drawBlock.size; dy++){
int worldx = dx + offsetx + x;
int worldy = dy + offsety + y;
if(!(worldx == x && worldy == y)){
Tile other = world.tile(worldx, worldy);
Tile other = world.tile(worldx, worldy);
if(other != null && other.block().isMultiblock()){
return true;
}
if(other != null && other.block().isMultiblock()){
return true;
}
}
}
@@ -180,6 +179,52 @@ public class MapEditor{
return false;
}
public void addCliffs(){
for(Tile tile : world.tiles){
if(!tile.block().isStatic() || tile.block() == Blocks.cliff) continue;
int rotation = 0;
for(int i = 0; i < 8; i++){
Tile other = world.tiles.get(tile.x + Geometry.d8[i].x, tile.y + Geometry.d8[i].y);
if(other != null && !other.block().isStatic()){
rotation |= (1 << i);
}
}
if(rotation != 0){
tile.setBlock(Blocks.cliff);
}
tile.data = (byte)rotation;
}
for(Tile tile : world.tiles){
if(tile.block() != Blocks.cliff && tile.block().isStatic()){
tile.setBlock(Blocks.air);
}
}
}
public void addFloorCliffs(){
for(Tile tile : world.tiles){
if(!tile.floor().hasSurface() || tile.block() == Blocks.cliff) continue;
int rotation = 0;
for(int i = 0; i < 8; i++){
Tile other = world.tiles.get(tile.x + Geometry.d8[i].x, tile.y + Geometry.d8[i].y);
if(other != null && !other.floor().hasSurface()){
rotation |= (1 << i);
}
}
if(rotation != 0){
tile.setBlock(Blocks.cliff);
}
tile.data = (byte)rotation;
}
}
public void drawCircle(int x, int y, Cons<Tile> drawer){
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){

View File

@@ -1,7 +1,6 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.files.*;
import arc.func.*;
import arc.graphics.*;
@@ -14,8 +13,8 @@ import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.core.GameState.*;
@@ -43,6 +42,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
private MapGenerateDialog generateDialog;
private ScrollPane pane;
private BaseDialog menu;
private Table blockSelection;
private Rules lastSavedRules;
private boolean saved = false;
private boolean shownWithMap = false;
@@ -257,7 +257,14 @@ public class MapEditorDialog extends Dialog implements Disposable{
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
player.clearUnit();
Groups.unit.clear();
Groups.build.clear();
Groups.weather.clear();
logic.play();
if(player.team().core() == null){
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
player.unit(UnitTypes.alpha.spawn(player.team(), player.x, player.y));
}
});
}
@@ -385,7 +392,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
}
public void build(){
float size = 60f;
float size = 58f;
clearChildren();
table(cont -> {
@@ -557,6 +564,21 @@ public class MapEditorDialog extends Dialog implements Disposable{
t.add(slider).width(size * 3f - 20).padTop(4f);
}).padTop(5).growX().top();
mid.row();
if(!mobile){
mid.table(t -> {
t.button("@editor.center", Icon.move, Styles.cleart, view::center).growX().margin(9f);
}).growX().top();
}
if(experimental){
mid.row();
mid.table(t -> {
t.button("Cliffs", Icon.terrain, Styles.cleart, editor::addCliffs).growX().margin(9f);
}).growX().top();
}
}).margin(0).left().growY();
@@ -645,9 +667,9 @@ public class MapEditorDialog extends Dialog implements Disposable{
ui.showConfirm("@confirm", "@editor.unsaved", this::hide);
}
private void addBlockSelection(Table table){
Table content = new Table();
pane = new ScrollPane(content);
private void addBlockSelection(Table cont){
blockSelection = new Table();
pane = new ScrollPane(blockSelection);
pane.setFadeScrollBars(false);
pane.setOverscroll(true, false);
pane.exited(() -> {
@@ -655,9 +677,22 @@ public class MapEditorDialog extends Dialog implements Disposable{
Core.scene.setScrollFocus(view);
}
});
ButtonGroup<ImageButton> group = new ButtonGroup<>();
int i = 0;
cont.table(search -> {
search.image(Icon.zoom).padRight(8);
search.field("", this::rebuildBlockSelection)
.name("editor/search").maxTextLength(maxNameLength).get().setMessageText("@players.search");
}).pad(-2);
cont.row();
cont.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
cont.row();
cont.add(pane).expandY().top().left();
rebuildBlockSelection("");
}
private void rebuildBlockSelection(String searchText){
blockSelection.clear();
blocksOut.clear();
blocksOut.addAll(Vars.content.blocks());
@@ -671,28 +706,32 @@ public class MapEditorDialog extends Dialog implements Disposable{
return Integer.compare(b1.id, b2.id);
});
int i = 0;
for(Block block : blocksOut){
TextureRegion region = block.icon(Cicon.medium);
if(!Core.atlas.isFound(region) || !block.inEditor || (block.buildVisibility == BuildVisibility.debugOnly)) continue;
if(!Core.atlas.isFound(region) || !block.inEditor
|| block.buildVisibility == BuildVisibility.debugOnly
|| (!searchText.isEmpty() && !block.localizedName.toLowerCase().contains(searchText.toLowerCase()))
) continue;
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglei);
button.getStyle().imageUp = new TextureRegionDrawable(region);
button.clicked(() -> editor.drawBlock = block);
button.resizeImage(8 * 4f);
button.update(() -> button.setChecked(editor.drawBlock == block));
group.add(button);
content.add(button).size(50f);
blockSelection.add(button).size(50f).tooltip(block.localizedName);
if(i == 0) editor.drawBlock = block;
if(++i % 4 == 0){
content.row();
blockSelection.row();
}
}
group.getButtons().get(2).setChecked(true);
table.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
table.row();
table.add(pane).growY().fillX();
if(i == 0){
blockSelection.add("@none").color(Color.lightGray).padLeft(80f).padTop(10f);
}
}
}

View File

@@ -29,7 +29,8 @@ public class MapGenerateDialog extends BaseDialog{
private final Prov<GenerateFilter>[] filterTypes = new Prov[]{
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
BlendFilter::new, MirrorFilter::new, ClearFilter::new, CoreSpawnFilter::new,
EnemySpawnFilter::new, SpawnPathFilter::new
};
private final MapEditor editor;
private final boolean applied;
@@ -44,16 +45,20 @@ public class MapGenerateDialog extends BaseDialog{
private AsyncExecutor executor = new AsyncExecutor(1);
private AsyncResult<Void> result;
boolean generating;
private GenTile returnTile = new GenTile();
private GenTile[][] buffer1, buffer2;
private long[] buffer1, buffer2;
private Cons<Seq<GenerateFilter>> applier;
CachedTile ctile = new CachedTile(){
//nothing.
@Override
protected void changeEntity(Team team, Prov<Building> entityprov, int rotation){
protected void changeBuild(Team team, Prov<Building> entityprov, int rotation){
}
@Override
public void setBlock(Block type, Team team, int rotation, Prov<Building> entityprov){
this.block = type;
}
};
/** @param applied whether or not to use the applied in-game mode. */
@@ -65,7 +70,7 @@ public class MapGenerateDialog extends BaseDialog{
shown(this::setup);
addCloseButton();
if(applied){
buttons.button("@editor.apply", () -> {
buttons.button("@editor.apply", Icon.ok, () -> {
ui.loadAnd(() -> {
apply();
hide();
@@ -78,14 +83,14 @@ public class MapGenerateDialog extends BaseDialog{
update();
}).size(160f, 64f);
}
buttons.button("@editor.randomize", () -> {
buttons.button("@editor.randomize", Icon.refresh, () -> {
for(GenerateFilter filter : filters){
filter.randomize();
}
update();
}).size(160f, 64f);
buttons.button("@add", Icon.add, this::showAdd).height(64f).width(140f);
buttons.button("@add", Icon.add, this::showAdd).height(64f).width(150f);
if(!applied){
hidden(this::apply);
@@ -107,38 +112,36 @@ public class MapGenerateDialog extends BaseDialog{
/** Applies the specified filters to the editor. */
public void applyToEditor(Seq<GenerateFilter> filters){
//writeback buffer
GenTile[][] writeTiles = new GenTile[editor.width()][editor.height()];
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
writeTiles[x][y] = new GenTile();
}
}
long[] writeTiles = new long[editor.width() * editor.height()];
for(GenerateFilter filter : filters){
input.begin(filter, editor.width(), editor.height(), editor::tile);
//write to buffer
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
input.apply(x, y, tile.floor(), tile.block(), tile.overlay());
input.apply(x, y, tile.block(), tile.floor(), tile.overlay());
filter.apply(input);
writeTiles[x][y].set(input.floor, input.block, input.ore, tile.team());
writeTiles[x + y*world.width()] = PackTile.get(input.block.id, input.floor.id, input.overlay.id);
}
}
editor.load(() -> {
//read from buffer back into tiles
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
GenTile write = writeTiles[x][y];
for(int i = 0; i < editor.width() * editor.height(); i++){
Tile tile = world.tiles.geti(i);
long write = writeTiles[i];
tile.setFloor((Floor)content.block(write.floor));
tile.setBlock(content.block(write.block));
tile.setTeam(Team.get(write.team));
tile.setOverlay(content.block(write.ore));
Block block = content.block(PackTile.block(write)), floor = content.block(PackTile.floor(write)), overlay = content.block(PackTile.overlay(write));
//don't mess up synthetic stuff.
if(!tile.synthetic() && !block.synthetic()){
tile.setBlock(block);
}
tile.setFloor((Floor)floor);
tile.setOverlay(overlay);
}
});
}
@@ -201,15 +204,8 @@ public class MapGenerateDialog extends BaseDialog{
rebuildFilters();
}
GenTile[][] create(){
GenTile[][] out = new GenTile[editor.width() / scaling][editor.height() / scaling];
for(int x = 0; x < out.length; x++){
for(int y = 0; y < out[0].length; y++){
out[x][y] = new GenTile();
}
}
return out;
long[] create(){
return new long[(editor.width() / scaling) * (editor.height() / scaling)];
}
void rebuildFilters(){
@@ -295,7 +291,7 @@ public class MapGenerateDialog extends BaseDialog{
for(Prov<GenerateFilter> gen : filterTypes){
GenerateFilter filter = gen.get();
if((!applied && filter.isBuffered()) || (filter.isPost() && applied)) continue;
if((filter.isPost() && applied)) continue;
selection.cont.button(filter.name(), () -> {
filters.add(filter);
@@ -317,9 +313,15 @@ public class MapGenerateDialog extends BaseDialog{
selection.show();
}
GenTile dset(Tile tile){
returnTile.set(tile);
return returnTile;
long pack(Tile tile){
return PackTile.get(tile.blockID(), tile.floorID(), tile.overlayID());
}
Tile unpack(long tile){
ctile.setFloor((Floor)content.block(PackTile.floor(tile)));
ctile.setBlock(content.block(PackTile.block(tile)));
ctile.setOverlay(content.block(PackTile.overlay(tile)));
return ctile;
}
void apply(){
@@ -350,6 +352,7 @@ public class MapGenerateDialog extends BaseDialog{
result = executor.submit(() -> {
try{
int w = pixmap.getWidth();
world.setGenerating(true);
generating = true;
@@ -357,24 +360,24 @@ public class MapGenerateDialog extends BaseDialog{
//write to buffer1 for reading
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
buffer1[px + py*w] = pack(editor.tile(px * scaling, py * scaling));
}
}
}
for(GenerateFilter filter : copy){
input.begin(filter, editor.width(), editor.height(), (x, y) -> buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1)][Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)].tile());
input.begin(filter, editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1) + w* Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)]));
//read from buffer1 and write to buffer2
pixmap.each((px, py) -> {
int x = px * scaling, y = py * scaling;
GenTile tile = buffer1[px][py];
input.apply(x, y, content.block(tile.floor), content.block(tile.block), content.block(tile.ore));
long tile = buffer1[px + py * w];
input.apply(x, y, content.block(PackTile.block(tile)), content.block(PackTile.floor(tile)), content.block(PackTile.overlay(tile)));
filter.apply(input);
buffer2[px][py].set(input.floor, input.block, input.ore, Team.get(tile.team));
buffer2[px + py * w] = PackTile.get(input.block.id, input.floor.id, input.overlay.id);
});
pixmap.each((px, py) -> buffer1[px][py].set(buffer2[px][py]));
pixmap.each((px, py) -> buffer1[px + py*w] = buffer2[px + py*w]);
}
for(int px = 0; px < pixmap.getWidth(); px++){
@@ -383,10 +386,10 @@ public class MapGenerateDialog extends BaseDialog{
//get result from buffer1 if there's filters left, otherwise get from editor directly
if(filters.isEmpty()){
Tile tile = editor.tile(px * scaling, py * scaling);
color = MapIO.colorFor(tile.floor(), tile.block(), tile.overlay(), Team.derelict);
color = MapIO.colorFor(tile.block(), tile.floor(), tile.overlay(), Team.derelict);
}else{
GenTile tile = buffer1[px][py];
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.derelict);
long tile = buffer1[px + py*w];
color = MapIO.colorFor(content.block(PackTile.block(tile)), content.block(PackTile.floor(tile)), content.block(PackTile.overlay(tile)), Team.derelict);
}
pixmap.draw(px, pixmap.getHeight() - 1 - py, color);
}
@@ -401,44 +404,9 @@ public class MapGenerateDialog extends BaseDialog{
});
}catch(Exception e){
generating = false;
e.printStackTrace();
Log.err(e);
}
world.setGenerating(false);
});
}
private class GenTile{
public byte team;
public short block, floor, ore;
GenTile(){
}
public void set(Block floor, Block wall, Block ore, Team team){
this.floor = floor.id;
this.block = wall.id;
this.ore = ore.id;
this.team = (byte)team.id;
}
public void set(GenTile other){
this.floor = other.floor;
this.block = other.block;
this.ore = other.ore;
this.team = other.team;
}
public GenTile set(Tile other){
set(other.floor(), other.block(), other.overlay(), other.team());
return this;
}
Tile tile(){
ctile.setFloor((Floor)content.block(floor));
ctile.setBlock(content.block(block));
ctile.setOverlay(content.block(ore));
ctile.setTeam(Team.get(team));
return ctile;
}
}
}

View File

@@ -1,8 +1,8 @@
package mindustry.editor;
import arc.*;
import arc.struct.*;
import arc.scene.ui.*;
import arc.struct.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.io.*;

View File

@@ -8,7 +8,7 @@ import mindustry.maps.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.maps;
import static mindustry.Vars.*;
public class MapLoadDialog extends BaseDialog{
private Map selected = null;

View File

@@ -8,7 +8,7 @@ import arc.util.*;
import mindustry.ui.dialogs.*;
public class MapResizeDialog extends BaseDialog{
private static final int minSize = 50, maxSize = 500, increment = 50;
public static int minSize = 50, maxSize = 500, increment = 50;
int width, height;
public MapResizeDialog(MapEditor editor, Intc2 cons){

View File

@@ -1,24 +1,22 @@
package mindustry.editor;
import arc.Core;
import arc.graphics.Color;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.GestureDetector;
import arc.input.GestureDetector.GestureListener;
import arc.input.KeyCode;
import arc.math.Mathf;
import arc.input.*;
import arc.input.GestureDetector.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.Element;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.TextField;
import arc.scene.ui.layout.Scl;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.graphics.Pal;
import mindustry.input.Binding;
import mindustry.ui.GridImage;
import mindustry.graphics.*;
import mindustry.input.*;
import mindustry.ui.*;
import static mindustry.Vars.mobile;
import static mindustry.Vars.ui;
import static mindustry.Vars.*;
public class MapView extends Element implements GestureListener{
private MapEditor editor;
@@ -173,6 +171,10 @@ public class MapView extends Element implements GestureListener{
this.grid = grid;
}
public void center(){
offsetx = offsety = 0;
}
@Override
public void act(float delta){
super.act(delta);
@@ -241,14 +243,14 @@ public class MapView extends Element implements GestureListener{
image.setImageSize(editor.width(), editor.height());
if(!ScissorStack.push(rect.set(x, y, width, height))){
if(!ScissorStack.push(rect.set(x, y + Core.scene.marginBottom, width, height))){
return;
}
Draw.color(Pal.remove);
Lines.stroke(2f);
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
editor.renderer.draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
editor.renderer.draw(centerx - sclwidth / 2, centery - sclheight / 2 + Core.scene.marginBottom, sclwidth, sclheight);
Draw.reset();
if(grid){
@@ -319,7 +321,7 @@ public class MapView extends Element implements GestureListener{
}
private boolean active(){
return Core.scene.getKeyboardFocus() != null
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;

View File

@@ -1,6 +1,6 @@
package mindustry.editor;
import arc.struct.Seq;
import arc.struct.*;
public class OperationStack{
private static final int maxSize = 10;

View File

@@ -17,7 +17,7 @@ import mindustry.ui.*;
public class WaveGraph extends Table{
public Seq<SpawnGroup> groups = new Seq<>();
public int from, to = 20;
public int from = 0, to = 20;
private Mode mode = Mode.counts;
private int[][] values;
@@ -81,7 +81,7 @@ public class WaveGraph extends Table{
for(int i = 0; i < values.length; i++){
float sum = 0;
for(UnitType type : used.orderedItems()){
sum += type.health * values[i][type.id];
sum += (type.health) * values[i][type.id];
}
float cx = graphX + i*spacing, cy = 2f + graphY + sum * (graphH - 4f) / maxHealth;
@@ -114,7 +114,7 @@ public class WaveGraph extends Table{
Lines.line(cx, cy, cx, cy + len);
if(i == values.length/2){
font.draw("" + (i + from), cx, cy - 2f, Align.center);
font.draw("" + (i + from + 1), cx, cy - 2f, Align.center);
}
}
font.setColor(Color.white);
@@ -154,13 +154,13 @@ public class WaveGraph extends Table{
int sum = 0;
for(SpawnGroup spawn : groups){
int spawned = spawn.getUnitsSpawned(i);
int spawned = spawn.getSpawned(i);
values[index][spawn.type.id] += spawned;
if(spawned > 0){
used.add(spawn.type);
}
max = Math.max(max, values[index][spawn.type.id]);
healthsum += spawned * spawn.type.health;
healthsum += spawned * (spawn.type.health);
sum += spawned;
}
maxTotal = Math.max(maxTotal, sum);

View File

@@ -1,7 +1,6 @@
package mindustry.editor;
import arc.*;
import arc.input.*;
import arc.math.*;
import arc.scene.event.*;
import arc.scene.ui.*;
@@ -35,15 +34,9 @@ public class WaveInfoDialog extends BaseDialog{
super("@waves.title");
shown(this::setup);
hidden(() -> {
state.rules.spawns = groups;
});
hidden(() -> state.rules.spawns = groups);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back){
Core.app.post(this::hide);
}
});
addCloseListener();
onResize(this::setup);
addCloseButton();
@@ -71,7 +64,7 @@ public class WaveInfoDialog extends BaseDialog{
}).disabled(b -> Core.app.getClipboardText() == null || Core.app.getClipboardText().isEmpty());
dialog.cont.row();
dialog.cont.button("@settings.reset", () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
groups = JsonIO.copy(defaultWaves.get());
groups = JsonIO.copy(waves.get());
buildGroups();
dialog.hide();
}));
@@ -101,6 +94,14 @@ public class WaveInfoDialog extends BaseDialog{
view(1);
}
});
if(experimental){
buttons.button("Random", Icon.refresh, () -> {
groups.clear();
groups = Waves.generate(1f / 10f);
updateWaves();
}).width(200f);
}
}
void view(int amount){
@@ -124,7 +125,7 @@ public class WaveInfoDialog extends BaseDialog{
}
void setup(){
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? defaultWaves.get() : state.rules.spawns);
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? waves.get() : state.rules.spawns);
cont.clear();
cont.stack(new Table(Tex.clear, main -> {
@@ -159,7 +160,7 @@ public class WaveInfoDialog extends BaseDialog{
t.margin(0).defaults().pad(3).padLeft(5f).growX().left();
t.button(b -> {
b.left();
b.image(group.type.icon(mindustry.ui.Cicon.medium)).size(32f).padRight(3);
b.image(group.type.icon(Cicon.medium)).size(32f).padRight(3).scaling(Scaling.fit);
b.add(group.type.localizedName).color(Pal.accent);
b.add().growX();
@@ -262,7 +263,7 @@ public class WaveInfoDialog extends BaseDialog{
if(type.isHidden()) continue;
p.button(t -> {
t.left();
t.image(type.icon(Cicon.medium)).size(40f).padRight(2f);
t.image(type.icon(Cicon.medium)).size(8 * 4).scaling(Scaling.fit).padRight(2f);
t.add(type.localizedName);
}, () -> {
lastType = type;

View File

@@ -2,13 +2,13 @@ package mindustry.entities;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -23,7 +23,8 @@ public class Damage{
private static Tile furthest;
private static Rect rect = new Rect();
private static Rect hitrect = new Rect();
private static Vec2 tr = new Vec2();
private static Vec2 tr = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2();
private static Seq<Unit> units = new Seq<>();
private static GridBits bits = new GridBits(30, 30);
private static IntQueue propagation = new IntQueue();
private static IntSet collidedBlocks = new IntSet();
@@ -31,15 +32,22 @@ public class Damage{
private static Unit tmpUnit;
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, Color color, boolean damage){
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null);
}
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam){
if(damage){
for(int i = 0; i < Mathf.clamp(power / 20, 0, 6); i++){
int branches = 5 + Mathf.clamp((int)(power / 30), 1, 20);
Time.run(i * 2f + Mathf.random(4f), () -> Lightning.create(Team.derelict, Pal.power, 3, x, y, Mathf.random(360f), branches + Mathf.range(2)));
}
for(int i = 0; i < Mathf.clamp(flammability / 4, 0, 30); i++){
Time.run(i / 2f, () -> Call.createBullet(Bullets.fireball, Team.derelict, x, y, Mathf.random(360f), Bullets.fireball.damage, 1, 1));
if(fire){
for(int i = 0; i < Mathf.clamp(flammability / 4, 0, 30); i++){
Time.run(i / 2f, () -> Call.createBullet(Bullets.fireball, Team.derelict, x, y, Mathf.random(360f), Bullets.fireball.damage, 1, 1));
}
}
int waves = Mathf.clamp((int)(explosiveness / 4), 0, 30);
@@ -47,7 +55,7 @@ public class Damage{
for(int i = 0; i < waves; i++){
int f = i;
Time.run(i * 2f, () -> {
Damage.damage(x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f);
Damage.damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f, false);
Fx.blockExplosionSmoke.at(x + Mathf.range(radius), y + Mathf.range(radius));
});
}
@@ -82,7 +90,7 @@ public class Damage{
furthest = null;
boolean found = world.raycast(b.tileX(), b.tileY(), world.toTile(b.x + Tmp.v1.x), world.toTile(b.y + Tmp.v1.y),
boolean found = world.raycast(b.tileX(), b.tileY(), World.toTile(b.x + Tmp.v1.x), World.toTile(b.y + Tmp.v1.y),
(x, y) -> (furthest = world.tile(x, y)) != null && furthest.team() != b.team && furthest.block().absorbLasers);
return found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length;
@@ -112,20 +120,35 @@ public class Damage{
collidedBlocks.clear();
tr.trns(angle, length);
Intc2 collider = (cx, cy) -> {
Building tile = world.build(cx, cy);
if(tile != null && !collidedBlocks.contains(tile.pos()) && tile.team != team && tile.collide(hitter)){
tile.collision(hitter);
collidedBlocks.add(tile.pos());
hitter.type.hit(hitter, tile.x, tile.y);
boolean collide = tile != null && collidedBlocks.add(tile.pos());
if(hitter.damage > 0){
float health = !collide ? 0 : tile.health;
if(collide && tile.team != team && tile.collide(hitter)){
tile.collision(hitter);
hitter.type.hit(hitter, tile.x, tile.y);
}
//try to heal the tile
if(collide && hitter.type.testCollision(hitter, tile)){
hitter.type.hitTile(hitter, tile, health, false);
}
}
};
if(hitter.type.collidesGround){
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
seg1.set(x, y);
seg2.set(seg1).add(tr);
world.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {
collider.get(cx, cy);
if(large){
for(Point2 p : Geometry.d4){
for(Point2 p : Geometry.d4){
Tile other = world.tile(p.x + cx, p.y + cy);
if(other != null && (large || Intersector.intersectSegmentRectangle(seg1, seg2, other.getBounds(Tmp.r1)))){
collider.get(cx + p.x, cy + p.y);
}
}
@@ -154,20 +177,27 @@ public class Damage{
rect.height += expand * 2;
Cons<Unit> cons = e -> {
if(!e.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround)) return;
e.hitbox(hitrect);
Vec2 vec = Geometry.raycastRect(x, y, x2, y2, hitrect.grow(expand * 2));
if(vec != null){
if(vec != null && hitter.damage > 0){
effect.at(vec.x, vec.y);
e.collision(hitter, vec.x, vec.y);
hitter.collision(e, vec.x, vec.y);
}
};
Units.nearbyEnemies(team, rect, cons);
units.clear();
Units.nearbyEnemies(team, rect, u -> {
if(u.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround)){
units.add(u);
}
});
units.sort(u -> u.dst2(hitter));
units.each(cons);
}
/**
@@ -184,7 +214,6 @@ public class Damage{
Building tile = world.build(cx, cy);
if(tile != null && tile.team != hitter.team){
tmpBuilding = tile;
//TODO return tile
return true;
}
return false;
@@ -362,9 +391,9 @@ public class Damage{
if(scaledDamage <= 0 || tile == null) continue;
//apply damage to entity if needed
if(tile.build != null && tile.team() != team){
int health = (int)tile.build.health();
if(tile.build.health() > 0){
if(tile.build != null && tile.build.team != team){
int health = (int)(tile.build.health / (tile.block().size * tile.block().size));
if(tile.build.health > 0){
tile.build.damage(scaledDamage);
scaledDamage -= health;
@@ -402,8 +431,7 @@ public class Damage{
}
@Struct
static
class PropCellStruct{
static class PropCellStruct{
byte x;
byte y;
short damage;

View File

@@ -8,7 +8,6 @@ import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.gen.*;
@@ -22,35 +21,46 @@ public class Effect{
private static final EffectContainer container = new EffectContainer();
private static final Seq<Effect> all = new Seq<>();
public final int id;
public final Cons<EffectContainer> renderer;
public final float lifetime;
/** Clip size. */
public float size;
private boolean initialized;
public boolean ground;
public float groundDuration;
public final int id;
public Cons<EffectContainer> renderer = e -> {};
public float lifetime = 50f;
/** Clip size. */
public float clip;
public float layer = Layer.effect;
public float layerDuration;
public Effect(float life, float clipsize, Cons<EffectContainer> renderer){
this.id = all.size;
this.lifetime = life;
this.renderer = renderer;
this.size = clipsize;
this.clip = clipsize;
all.add(this);
}
public Effect(float life, Cons<EffectContainer> renderer){
this(life,50f, renderer);
this(life, 50f, renderer);
}
public Effect ground(){
ground = true;
//for custom implementations
public Effect(){
this.id = all.size;
all.add(this);
}
public void init(){}
public Effect layer(float l){
layer = l;
return this;
}
public Effect ground(float duration){
ground = true;
this.groundDuration = duration;
public Effect layer(float l, float duration){
layer = l;
this.layerDuration = duration;
return this;
}
@@ -88,14 +98,18 @@ public class Effect{
public float render(int id, Color color, float life, float lifetime, float rotation, float x, float y, Object data){
container.set(id, color, life, lifetime, rotation, x, y, data);
Draw.z(ground ? Layer.debris : Layer.effect);
Draw.z(layer);
Draw.reset();
renderer.get(container);
render(container);
Draw.reset();
return container.lifetime;
}
public void render(EffectContainer e){
renderer.get(e);
}
public static @Nullable Effect get(int id){
return id >= all.size || id < 0 ? null : all.get(id);
}
@@ -123,9 +137,14 @@ public class Effect{
if(headless || effect == Fx.none) return;
if(Core.settings.getBool("effects")){
Rect view = Core.camera.bounds(Tmp.r1);
Rect pos = Tmp.r2.setSize(effect.size).setCenter(x, y);
Rect pos = Tmp.r2.setSize(effect.clip).setCenter(x, y);
if(view.overlaps(pos)){
if(!effect.initialized){
effect.initialized = true;
effect.init();
}
EffectState entity = EffectState.create();
entity.effect = effect;
entity.rotation = rotation;
@@ -147,7 +166,7 @@ public class Effect{
if(headless || region == null || !Core.atlas.isFound(region)) return;
Tile tile = world.tileWorld(x, y);
if(tile == null || tile.floor().isLiquid) return;
if(tile == null || !tile.floor().hasSurface()) return;
Decal decal = Decal.create();
decal.set(x, y);
@@ -210,4 +229,4 @@ public class Effect{
}
}
}
}

View File

@@ -115,7 +115,6 @@ public class EntityCollisions{
@SuppressWarnings("unchecked")
public <T extends Hitboxc> void updatePhysics(EntityGroup<T> group){
QuadTree tree = group.tree();
tree.clear();
@@ -127,7 +126,7 @@ public class EntityCollisions{
public static boolean legsSolid(int x, int y){
Tile tile = world.tile(x, y);
return tile == null || tile.staticDarkness() >= 2;
return tile == null || tile.staticDarkness() >= 2 || tile.floor().solid;
}
public static boolean waterSolid(int x, int y){
@@ -141,7 +140,6 @@ public class EntityCollisions{
}
private void checkCollide(Hitboxc a, Hitboxc b){
a.hitbox(this.r1);
b.hitbox(this.r2);
@@ -218,7 +216,6 @@ public class EntityCollisions{
@SuppressWarnings("unchecked")
public <T extends Hitboxc> void collide(EntityGroup<T> groupa){
groupa.each(solid -> {
solid.hitbox(r1);
r1.x += (solid.lastX() - solid.getX());

View File

@@ -4,11 +4,12 @@ import arc.*;
import arc.func.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.gen.*;
import java.util.*;
import static mindustry.Vars.collisions;
import static mindustry.Vars.*;
/** Represents a group of a certain type of entity.*/
@SuppressWarnings("unchecked")
@@ -57,8 +58,9 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
each(Entityc::update);
}
public void copy(Seq<T> arr){
public Seq<T> copy(Seq<T> arr){
arr.addAll(array);
return arr;
}
public void each(Cons<T> cons){
@@ -92,6 +94,7 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
return map != null;
}
@Nullable
public T getByID(int id){
if(map == null) throw new RuntimeException("Mapping is not enabled for group " + id + "!");
return map.get(id);
@@ -182,16 +185,21 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
array.each(Entityc::remove);
array.clear();
if(map != null)
map.clear();
if(map != null) map.clear();
clearing = false;
}
@Nullable
public T find(Boolf<T> pred){
return array.find(pred);
}
@Nullable
public T first(){
return array.first();
}
@Override
public Iterator<T> iterator(){
return array.iterator();

View File

@@ -15,7 +15,7 @@ public class Fires{
private static final float baseLifetime = 1000f;
private static final IntMap<Fire> map = new IntMap<>();
/** Start a fire on the tile. If there already is a file there, refreshes its lifetime. */
/** Start a fire on the tile. If there already is a fire there, refreshes its lifetime. */
public static void create(Tile tile){
if(net.client() || tile == null || !state.rules.fire) return; //not clientside.
@@ -23,14 +23,14 @@ public class Fires{
if(fire == null){
fire = Fire.create();
fire.tile(tile);
fire.lifetime(baseLifetime);
fire.tile = tile;
fire.lifetime = baseLifetime;
fire.set(tile.worldx(), tile.worldy());
fire.add();
map.put(tile.pos(), fire);
}else{
fire.lifetime(baseLifetime);
fire.time(0f);
fire.lifetime = baseLifetime;
fire.time = 0f;
}
}

View File

@@ -11,5 +11,7 @@ class GroupDefs<G>{
@GroupDef(value = Buildingc.class) G build;
@GroupDef(value = Syncc.class, mapping = true) G sync;
@GroupDef(value = Drawc.class) G draw;
@GroupDef(value = Firec.class) G fire;
@GroupDef(value = Puddlec.class) G puddle;
@GroupDef(value = WeatherStatec.class) G weather;
}

View File

@@ -4,7 +4,9 @@ import arc.graphics.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.bullet.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -24,36 +26,34 @@ public class Lightning{
/** Create a lighting branch at a location. Use Team.derelict to damage everyone. */
public static void create(Team team, Color color, float damage, float x, float y, float targetAngle, int length){
createLightingInternal(null, lastSeed++, team, color, damage, x, y, targetAngle, length);
createLightningInternal(null, lastSeed++, team, color, damage, x, y, targetAngle, length);
}
/** Create a lighting branch at a location. Uses bullet parameters. */
public static void create(Bullet bullet, Color color, float damage, float x, float y, float targetAngle, int length){
createLightingInternal(bullet, lastSeed++, bullet.team, color, damage, x, y, targetAngle, length);
createLightningInternal(bullet, lastSeed++, bullet.team, color, damage, x, y, targetAngle, length);
}
//TODO remote method
//@Remote(called = Loc.server, unreliable = true)
private static void createLightingInternal(Bullet hitter, int seed, Team team, Color color, float damage, float x, float y, float rotation, int length){
private static void createLightningInternal(@Nullable Bullet hitter, int seed, Team team, Color color, float damage, float x, float y, float rotation, int length){
random.setSeed(seed);
hit.clear();
BulletType bulletType = hitter != null && !hitter.type.collidesAir ? Bullets.damageLightningGround : Bullets.damageLightning;
BulletType hitCreate = hitter == null || hitter.type.lightningType == null ? Bullets.damageLightning : hitter.type.lightningType;
Seq<Vec2> lines = new Seq<>();
bhit = false;
for(int i = 0; i < length / 2; i++){
bulletType.create(null, team, x, y, 0f, damage, 1f, 1f, hitter);
hitCreate.create(null, team, x, y, 0f, damage, 1f, 1f, hitter);
lines.add(new Vec2(x + Mathf.range(3f), y + Mathf.range(3f)));
if(lines.size > 1){
bhit = false;
Vec2 from = lines.get(lines.size - 2);
Vec2 to = lines.get(lines.size - 1);
world.raycastEach(world.toTile(from.getX()), world.toTile(from.getY()), world.toTile(to.getX()), world.toTile(to.getY()), (wx, wy) -> {
world.raycastEach(World.toTile(from.getX()), World.toTile(from.getY()), World.toTile(to.getX()), World.toTile(to.getY()), (wx, wy) -> {
Tile tile = world.tile(wx, wy);
if(tile != null && tile.block().insulated){
if(tile != null && tile.block().insulated && tile.team() != team){
bhit = true;
//snap it instead of removing
lines.get(lines.size -1).set(wx * tilesize, wy * tilesize);

View File

@@ -53,13 +53,13 @@ public class Predict{
public static Vec2 intercept(Position src, Position dst, float v){
float ddx = 0, ddy = 0;
if(dst instanceof Hitboxc){
ddx += ((Hitboxc)dst).deltaX();
ddy += ((Hitboxc)dst).deltaY();
if(dst instanceof Hitboxc h){
ddx += h.deltaX();
ddy += h.deltaY();
}
if(src instanceof Hitboxc){
ddx -= ((Hitboxc)src).deltaX()/(Time.delta);
ddy -= ((Hitboxc)src).deltaY()/(Time.delta);
if(src instanceof Hitboxc h){
ddx -= h.deltaX();
ddy -= h.deltaY();
}
return intercept(src.getX(), src.getY(), dst.getX(), dst.getY(), ddx, ddy, v);
}

View File

@@ -14,12 +14,12 @@ public class Puddles{
public static final float maxLiquid = 70f;
/** Deposists a Puddle between tile and source. */
/** Deposits a Puddle between tile and source. */
public static void deposit(Tile tile, Tile source, Liquid liquid, float amount){
deposit(tile, source, liquid, amount, 0);
}
/** Deposists a Puddle at a tile. */
/** Deposits a Puddle at a tile. */
public static void deposit(Tile tile, Liquid liquid, float amount){
deposit(tile, tile, liquid, amount, 0);
}
@@ -38,13 +38,17 @@ public class Puddles{
Puddle p = map.get(tile.pos());
if(generation == 0 && p != null && p.lastRipple() <= Time.time() - 40f){
if(generation == 0 && p != null && p.lastRipple <= Time.time - 40f){
Fx.ripple.at((tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f, 1f, tile.floor().liquidDrop.color);
p.lastRipple(Time.time());
p.lastRipple = Time.time;
}
return;
}
if(tile.floor().solid){
return;
}
Puddle p = map.get(tile.pos());
if(p == null){
Puddle puddle = Puddle.create();
@@ -58,9 +62,9 @@ public class Puddles{
}else if(p.liquid() == liquid){
p.accepting(Math.max(amount, p.accepting()));
if(generation == 0 && p.lastRipple() <= Time.time() - 40f && p.amount() >= maxLiquid / 2f){
if(generation == 0 && p.lastRipple <= Time.time - 40f && p.amount() >= maxLiquid / 2f){
Fx.ripple.at((tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f, 1f, p.liquid().color);
p.lastRipple(Time.time());
p.lastRipple = Time.time;
}
}else{
p.amount(p.amount() + reactPuddle(p.liquid(), liquid, amount, p.tile(), (p.x() + source.worldx())/2f, (p.y() + source.worldy())/2f));

View File

@@ -3,9 +3,11 @@ package mindustry.entities;
import arc.*;
import arc.func.*;
import arc.math.geom.*;
import arc.struct.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
@@ -65,7 +67,7 @@ public class Units{
/** @return whether a new instance of a unit of this team can be created. */
public static boolean canCreate(Team team, UnitType type){
return teamIndex.countType(team, type) < getCap(team);
return team.data().countType(type) < getCap(team);
}
public static int getCap(Team team){
@@ -124,7 +126,7 @@ public class Units{
nearby(x, y, width, height, unit -> {
if(boolResult) return;
if((unit.isGrounded() && !unit.type().hovering) == ground){
if((unit.isGrounded() && !unit.type.hovering) == ground){
unit.hitbox(hitrect);
if(hitrect.overlaps(x, y, width, height)){
@@ -175,6 +177,18 @@ public class Units{
}
}
/** Returns the closest target enemy. First, units are checked, then tile entities. */
public static Teamc bestTarget(Team team, float x, float y, float range, Boolf<Unit> unitPred, Boolf<Building> tilePred, Sortf sort){
if(team == Team.derelict) return null;
Unit unit = bestEnemy(team, x, y, range, unitPred, sort);
if(unit != null){
return unit;
}else{
return findEnemyTile(team, x, y, range, tilePred);
}
}
/** Returns the closest enemy of this team. Filter by predicate. */
public static Unit closestEnemy(Team team, float x, float y, float range, Boolf<Unit> predicate){
if(team == Team.derelict) return null;
@@ -183,7 +197,7 @@ public class Units{
cdist = 0f;
nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> {
if(e.dead() || !predicate.get(e)) return;
if(e.dead() || !predicate.get(e) || e.team == Team.derelict) return;
float dst2 = e.dst2(x, y);
if(dst2 < range*range && (result == null || dst2 < cdist)){
@@ -195,6 +209,26 @@ public class Units{
return result;
}
/** Returns the closest enemy of this team using a custom comparison function. Filter by predicate. */
public static Unit bestEnemy(Team team, float x, float y, float range, Boolf<Unit> predicate, Sortf sort){
if(team == Team.derelict) return null;
result = null;
cdist = 0f;
nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> {
if(e.dead() || !predicate.get(e) || !e.within(x, y, range)) return;
float cost = sort.cost(e, x, y);
if(result == null || cost < cdist){
result = e;
cdist = cost;
}
});
return result;
}
/** Returns the closest ally of this team. Filter by predicate. No range. */
public static Unit closest(Team team, float x, float y, Boolf<Unit> predicate){
result = null;
@@ -252,7 +286,7 @@ public class Units{
/** Iterates over all units in a rectangle. */
public static void nearby(Team team, float x, float y, float width, float height, Cons<Unit> cons){
teamIndex.tree(team).intersect(x, y, width, height, cons);
team.data().tree().intersect(x, y, width, height, cons);
}
/** Iterates over all units in a circle around this position. */
@@ -276,20 +310,12 @@ public class Units{
/** Iterates over all units that are enemies of this team. */
public static void nearbyEnemies(Team team, float x, float y, float width, float height, Cons<Unit> cons){
if(team.active()){
for(Team enemy : state.teams.enemiesOf(team)){
nearby(enemy, x, y, width, height, cons);
}
}else{
//inactive teams have no cache, check everything
//TODO cache all teams with units OR blocks
for(Team other : Team.all){
if(other != team && teamIndex.count(other) > 0){
nearby(other, x, y, width, height, cons);
}
Seq<TeamData> data = state.teams.present;
for(int i = 0; i < data.size; i++){
if(data.items[i].team != team){
nearby(data.items[i].team, x, y, width, height, cons);
}
}
}
/** Iterates over all units that are enemies of this team. */
@@ -297,4 +323,7 @@ public class Units{
nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons);
}
public interface Sortf{
float cost(Unit unit, float x, float y);
}
}

View File

@@ -1,5 +1,7 @@
package mindustry.entities.abilities;
import arc.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
public abstract class Ability implements Cloneable{
@@ -14,4 +16,13 @@ public abstract class Ability implements Cloneable{
throw new RuntimeException("java sucks", e);
}
}
public void displayBars(Unit unit, Table bars){
}
/** @return localized ability name; mods should override this. */
public String localized(){
return Core.bundle.get("ability." + getClass().getSimpleName().replace("Ability", "").toLowerCase());
}
}

View File

@@ -6,10 +6,12 @@ import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
public class ForceFieldAbility extends Ability{
/** Shield radius. */
@@ -27,8 +29,8 @@ public class ForceFieldAbility extends Ability{
private static float realRad;
private static Unit paramUnit;
private static ForceFieldAbility paramField;
private static final Cons<Shielderc> shieldConsumer = trait -> {
if(trait.team() != paramUnit.team && Intersector.isInsideHexagon(paramUnit.x, paramUnit.y, realRad * 2f, trait.x(), trait.y()) && paramUnit.shield > 0){
private static final Cons<Bullet> shieldConsumer = trait -> {
if(trait.team != paramUnit.team && trait.type.absorbable && Intersector.isInsideHexagon(paramUnit.x, paramUnit.y, realRad * 2f, trait.x(), trait.y()) && paramUnit.shield > 0){
trait.absorb();
Fx.absorb.at(trait);
@@ -94,7 +96,12 @@ public class ForceFieldAbility extends Ability{
}
}
private void checkRadius(Unit unit){
@Override
public void displayBars(Unit unit, Table bars){
bars.add(new Bar("stat.shieldhealth", Pal.accent, () -> unit.shield / max)).row();
}
public void checkRadius(Unit unit){
//timer2 is used to store radius scale as an effect
realRad = radiusScale * radius;
}

View File

@@ -0,0 +1,46 @@
package mindustry.entities.abilities;
import arc.graphics.*;
import arc.math.*;
import arc.util.*;
import arc.audio.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class MoveLightningAbility extends Ability{
//Lightning damage
public float damage = 35f;
//Chance of firing every tick. Set >= 1 to always fire lightning every tick at max speed.
public float chance = 0.15f;
//Length of the lightning
public int length = 12;
//Speeds for when to start lightninging and when to stop getting faster
public float minSpeed = 0.8f, maxSpeed = 1.2f;
//Lightning color
public Color color = Color.valueOf("a9d8ff");
public Effect shootEffect = Fx.sparkShoot;
public Sound shootSound = Sounds.spark;
MoveLightningAbility(){}
public MoveLightningAbility(float damage, int length, float chance, float minSpeed, float maxSpeed, Color color){
this.damage = damage;
this.length = length;
this.chance = chance;
this.minSpeed = minSpeed;
this.maxSpeed = maxSpeed;
this.color = color;
}
@Override
public void update(Unit unit){
float scl = Mathf.clamp((unit.vel().len() - minSpeed) / (maxSpeed - minSpeed));
if(Mathf.chance(Time.delta * chance * scl)){
shootEffect.at(unit.x, unit.y, unit.rotation, color);
Lightning.create(unit.team, color, damage, unit.x + unit.vel.x, unit.y + unit.vel.y, unit.rotation, length);
shootSound.at(unit);
}
}
}

View File

@@ -5,7 +5,7 @@ import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class HealFieldAbility extends Ability{
public class RepairFieldAbility extends Ability{
public float amount = 1, reload = 100, range = 60;
public Effect healEffect = Fx.heal;
public Effect activeEffect = Fx.healWaveDynamic;
@@ -13,9 +13,9 @@ public class HealFieldAbility extends Ability{
protected float timer;
protected boolean wasHealed = false;
HealFieldAbility(){}
RepairFieldAbility(){}
public HealFieldAbility(float amount, float reload, float range){
public RepairFieldAbility(float amount, float reload, float range){
this.amount = amount;
this.reload = reload;
this.range = range;
@@ -30,7 +30,7 @@ public class HealFieldAbility extends Ability{
Units.nearby(unit.team, unit.x, unit.y, range, other -> {
if(other.damaged()){
healEffect.at(unit);
healEffect.at(other);
wasHealed = true;
}
other.heal(amount);

View File

@@ -5,7 +5,7 @@ import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class ShieldFieldAbility extends Ability{
public class ShieldRegenFieldAbility extends Ability{
public float amount = 1, max = 100f, reload = 100, range = 60;
public Effect applyEffect = Fx.shieldApply;
public Effect activeEffect = Fx.shieldWave;
@@ -13,9 +13,9 @@ public class ShieldFieldAbility extends Ability{
protected float timer;
protected boolean applied = false;
ShieldFieldAbility(){}
ShieldRegenFieldAbility(){}
public ShieldFieldAbility(float amount, float max, float reload, float range){
public ShieldRegenFieldAbility(float amount, float max, float reload, float range){
this.amount = amount;
this.max = max;
this.reload = reload;

View File

@@ -1,6 +1,5 @@
package mindustry.entities.abilities;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
@@ -8,7 +7,7 @@ import mindustry.gen.*;
import mindustry.type.*;
public class StatusFieldAbility extends Ability{
public @NonNull StatusEffect effect;
public StatusEffect effect;
public float duration = 60, reload = 100, range = 20;
public Effect applyEffect = Fx.heal;
public Effect activeEffect = Fx.overdriveWave;
@@ -17,7 +16,7 @@ public class StatusFieldAbility extends Ability{
StatusFieldAbility(){}
public StatusFieldAbility(@NonNull StatusEffect effect, float duration, float reload, float range){
public StatusFieldAbility(StatusEffect effect, float duration, float reload, float range){
this.duration = duration;
this.reload = reload;
this.range = range;
@@ -29,7 +28,6 @@ public class StatusFieldAbility extends Ability{
timer += Time.delta;
if(timer >= reload){
Units.nearby(unit.team, unit.x, unit.y, range, other -> {
other.apply(effect, duration);
});

View File

@@ -1,8 +1,8 @@
package mindustry.entities.abilities;
import arc.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
@@ -12,14 +12,16 @@ import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class UnitSpawnAbility extends Ability{
public @NonNull UnitType type;
public UnitType type;
public float spawnTime = 60f, spawnX, spawnY;
public Effect spawnEffect = Fx.spawn;
protected float timer;
public UnitSpawnAbility(@NonNull UnitType type, float spawnTime, float spawnX, float spawnY){
public UnitSpawnAbility(UnitType type, float spawnTime, float spawnX, float spawnY){
this.type = type;
this.spawnTime = spawnTime;
this.spawnX = spawnX;
@@ -31,10 +33,9 @@ public class UnitSpawnAbility extends Ability{
@Override
public void update(Unit unit){
timer += Time.delta;
timer += Time.delta * state.rules.unitBuildSpeedMultiplier;
if(timer >= spawnTime && Units.canCreate(unit.team, type)){
float x = unit.x + Angles.trnsx(unit.rotation, spawnY, spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, spawnX);
spawnEffect.at(x, y);
Unit u = type.create(unit.team);
@@ -57,4 +58,9 @@ public class UnitSpawnAbility extends Ability{
});
}
}
@Override
public String localized(){
return Core.bundle.format("ability.unitspawn", type.localizedName);
}
}

View File

@@ -3,7 +3,6 @@ package mindustry.entities.bullet;
import arc.audio.*;
import arc.graphics.*;
import arc.math.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
@@ -14,6 +13,7 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
@@ -25,6 +25,7 @@ public abstract class BulletType extends Content{
public float drawSize = 40f;
public float drag = 0f;
public boolean pierce, pierceBuilding;
public int pierceCap = -1;
public Effect hitEffect, despawnEffect;
/** Effect created when shooting. */
@@ -40,7 +41,7 @@ public abstract class BulletType extends Content{
/** Multiplied by turret reload speed to get final shoot speed. */
public float reloadMultiplier = 1f;
/** Multiplier of how much base damage is done to tiles. */
public float tileDamageMultiplier = 1f;
public float buildingDamageMultiplier = 1f;
/** Recoil from shooter entities. */
public float recoil;
/** Whether to kill the shooter when this is shot. For suicide bombers. */
@@ -71,15 +72,25 @@ public abstract class BulletType extends Content{
public boolean hittable = true;
/** Whether this bullet can be reflected. */
public boolean reflectable = true;
/** Whether this projectile can be absorbed by shields. */
public boolean absorbable = true;
/** Whether to move the bullet back depending on delta to fix some delta-time realted issues.
* Do not change unless you know what you're doing. */
public boolean backMove = true;
/** Bullet range override. */
public float range = -1f;
public float maxRange = -1f;
/** % of block health healed **/
public float healPercent = 0f;
/** whether to make fire on impact */
public boolean makeFire = false;
//additional effects
public float fragCone = 360f;
public float fragAngle = 0f;
public int fragBullets = 9;
public float fragVelocityMin = 0.2f, fragVelocityMax = 1f, fragLifeMin = 1f, fragLifeMax = 1f;
public BulletType fragBullet = null;
public @Nullable BulletType fragBullet = null;
public Color hitColor = Color.white;
public Color trailColor = Pal.missileYellowBack;
@@ -95,12 +106,18 @@ public abstract class BulletType extends Content{
public float incendChance = 1f;
public float homingPower = 0f;
public float homingRange = 50f;
/** Use a negative value to disable homing delay. */
public float homingDelay = -1f;
public Color lightningColor = Pal.surge;
public int lightning;
public int lightningLength = 5, lightningLengthRand = 0;
/** Use a negative value to use default bullet damage. */
public float lightningDamage = -1;
public float lightningCone = 360f;
public float lightningAngle = 0f;
/** The bullet created at lightning points. */
public @Nullable BulletType lightningType = null;
public float weaveScale = 1f;
public float weaveMag = -1f;
@@ -126,17 +143,37 @@ public abstract class BulletType extends Content{
this(1f, 1f);
}
/** @return estimated damage per shot. this can be very inaccurate. */
public float estimateDPS(){
float sum = damage + splashDamage*0.75f;
if(fragBullet != null && fragBullet != this){
sum += fragBullet.estimateDPS() * fragBullets / 2f;
}
return sum;
}
/** Returns maximum distance the bullet this bullet type has can travel. */
public float range(){
return Math.max(speed * lifetime * (1f - drag), range);
return Math.max(speed * lifetime * (1f - drag), maxRange);
}
public boolean collides(Bullet bullet, Building tile){
return true;
public boolean testCollision(Bullet bullet, Building tile){
return healPercent <= 0.001f || tile.team != bullet.team || tile.healthf() < 1f;
}
public void hitTile(Bullet b, Building tile, float initialHealth){
hit(b);
/** If direct is false, this is an indirect hit and the tile was already damaged.
* TODO this is a mess. */
public void hitTile(Bullet b, Building build, float initialHealth, boolean direct){
if(makeFire && build.team != b.team){
Fires.create(build.tile);
}
if(healPercent > 0f && build.team == b.team && !(build.block instanceof ConstructBlock)){
Fx.healBlockFull.at(build.x, build.y, build.block.size, Pal.heal);
build.heal(healPercent / 100f * build.maxHealth());
}else if(build.team != b.team && direct){
hit(b);
}
}
public void hitEntity(Bullet b, Hitboxc other, float initialHealth){
@@ -156,7 +193,7 @@ public abstract class BulletType extends Content{
if(fragBullet != null){
for(int i = 0; i < fragBullets; i++){
float len = Mathf.random(1f, 7f);
float a = b.rotation() + Mathf.range(fragCone/2);
float a = b.rotation() + Mathf.range(fragCone/2) + fragAngle;
fragBullet.create(b, x + Angles.trnsx(a, len), y + Angles.trnsy(a, len), a, Mathf.random(fragVelocityMin, fragVelocityMax), Mathf.random(fragLifeMin, fragLifeMax));
}
}
@@ -172,16 +209,29 @@ public abstract class BulletType extends Content{
Damage.createIncend(x, y, incendSpread, incendAmount);
}
if(splashDamageRadius > 0){
if(splashDamageRadius > 0 && !b.absorbed){
Damage.damage(b.team, x, y, splashDamageRadius, splashDamage * b.damageMultiplier(), collidesAir, collidesGround);
if(status != StatusEffects.none){
Damage.status(b.team, x, y, splashDamageRadius, status, statusDuration, collidesAir, collidesGround);
}
if(healPercent > 0f){
indexer.eachBlock(b.team, x, y, splashDamageRadius, Building::damaged, other -> {
Fx.healBlockFull.at(other.x, other.y, other.block.size, Pal.heal);
other.heal(healPercent / 100f * other.maxHealth());
});
}
if(makeFire){
indexer.eachBlock(null, x, y, splashDamageRadius, other -> other.team != b.team, other -> {
Fires.create(other.tile);
});
}
}
for(int i = 0; i < lightning; i++){
Lightning.create(b, lightningColor, lightningDamage < 0 ? damage : lightningDamage, b.x, b.y, Mathf.random(360f), lightningLength + Mathf.random(lightningLengthRand));
Lightning.create(b, lightningColor, lightningDamage < 0 ? damage : lightningDamage, b.x, b.y, b.rotation() + Mathf.range(lightningCone/2) + lightningAngle, lightningLength + Mathf.random(lightningLengthRand));
}
}
@@ -204,17 +254,18 @@ public abstract class BulletType extends Content{
}
public void init(Bullet b){
if(killShooter && b.owner() instanceof Healthc){
((Healthc)b.owner()).kill();
if(killShooter && b.owner() instanceof Healthc h){
h.kill();
}
if(instantDisappear){
b.time(lifetime);
b.time = lifetime;
}
}
public void update(Bullet b){
if(homingPower > 0.0001f){
if(homingPower > 0.0001f && b.time >= homingDelay){
Teamc target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> (e.isGrounded() && collidesGround) || (e.isFlying() && collidesAir), t -> collidesGround);
if(target != null){
b.vel.setAngle(Mathf.slerpDelta(b.rotation(), b.angleTo(target), homingPower));
@@ -222,7 +273,8 @@ public abstract class BulletType extends Content{
}
if(weaveMag > 0){
b.vel.rotate(Mathf.sin(Mathf.randomSeed(b.id, 10f) + b.time, weaveScale, weaveMag) * Time.delta);
float scl = Mathf.randomSeed(id, 0.9f, 1.1f);
b.vel.rotate(Mathf.sin(b.time + Mathf.PI * weaveScale/2f * scl, weaveScale * scl, weaveMag) * Time.delta);
}
if(trailChance > 0){
@@ -232,6 +284,18 @@ public abstract class BulletType extends Content{
}
}
@Override
public void init(){
if(pierceCap >= 1){
pierce = true;
//pierceBuilding is not enabled by default, because a bullet may want to *not* pierce buildings
}
if(lightningType == null){
lightningType = !collidesAir ? Bullets.damageLightningGround : Bullets.damageLightning;
}
}
@Override
public ContentType getContentType(){
return ContentType.bullet;
@@ -254,11 +318,11 @@ public abstract class BulletType extends Content{
}
public Bullet create(Bullet parent, float x, float y, float angle){
return create(parent.owner(), parent.team, x, y, angle);
return create(parent.owner, parent.team, x, y, angle);
}
public Bullet create(Bullet parent, float x, float y, float angle, float velocityScl, float lifeScale){
return create(parent.owner(), parent.team, x, y, angle, velocityScl, lifeScale);
return create(parent.owner, parent.team, x, y, angle, velocityScl, lifeScale);
}
public Bullet create(Bullet parent, float x, float y, float angle, float velocityScl){
@@ -271,15 +335,19 @@ public abstract class BulletType extends Content{
bullet.owner = owner;
bullet.team = team;
bullet.vel.trns(angle, speed * velocityScl);
bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta);
if(backMove){
bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta);
}else{
bullet.set(x, y);
}
bullet.lifetime = lifetime * lifetimeScl;
bullet.data = data;
bullet.drag = drag;
bullet.hitSize = hitSize;
bullet.damage = damage < 0 ? this.damage : damage;
bullet.damage = (damage < 0 ? this.damage : damage) * bullet.damageMultiplier();
bullet.add();
if(keepVelocity && owner instanceof Velc) bullet.vel.add(((Velc)owner).vel().x, ((Velc)owner).vel().y);
if(keepVelocity && owner instanceof Velc v) bullet.vel.add(v.vel().x, v.vel().y);
return bullet;
}
@@ -289,6 +357,7 @@ public abstract class BulletType extends Content{
@Remote(called = Loc.server, unreliable = true)
public static void createBullet(BulletType type, Team team, float x, float y, float angle, float damage, float velocityScl, float lifetimeScl){
if(type == null) return;
type.create(null, team, x, y, angle, damage, velocityScl, lifetimeScl, null);
}
}

View File

@@ -13,6 +13,8 @@ public class ContinuousLaserBulletType extends BulletType{
public float length = 220f;
public float shake = 1f;
public float fadeTime = 16f;
public float lightStroke = 40f;
public float spaceMag = 35f;
public Color[] colors = {Color.valueOf("ec745855"), Color.valueOf("ec7458aa"), Color.valueOf("ff9c5a"), Color.white};
public float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
public float[] strokes = {2f, 1.5f, 1f, 0.3f};
@@ -28,21 +30,29 @@ public class ContinuousLaserBulletType extends BulletType{
hitSize = 4;
drawSize = 420f;
lifetime = 16f;
keepVelocity = false;
pierce = true;
hittable = false;
hitColor = colors[2];
collidesTiles = false;
incendAmount = 1;
incendSpread = 5;
incendChance = 0.4f;
lightColor = Color.orange;
keepVelocity = false;
collides = false;
pierce = true;
hittable = false;
absorbable = false;
}
protected ContinuousLaserBulletType(){
this(0);
}
@Override
public float estimateDPS(){
//assume firing duration is about 100 by default, may not be accurate there's no way of knowing in this method
//assume it pierces 3 blocks/units
return damage * 100f / 5f * 3f;
}
@Override
public float range(){
return length;
@@ -57,7 +67,6 @@ public class ContinuousLaserBulletType extends BulletType{
@Override
public void update(Bullet b){
//TODO possible laser absorption from blocks
//damage every 5 ticks
if(b.timer(1, 5f)){
@@ -77,17 +86,17 @@ public class ContinuousLaserBulletType extends BulletType{
Lines.lineAngle(b.x, b.y, b.rotation(), baseLen);
for(int s = 0; s < colors.length; s++){
Draw.color(Tmp.c1.set(colors[s]).mul(1f + Mathf.absin(Time.time(), 1f, 0.1f)));
Draw.color(Tmp.c1.set(colors[s]).mul(1f + Mathf.absin(Time.time, 1f, 0.1f)));
for(int i = 0; i < tscales.length; i++){
Tmp.v1.trns(b.rotation() + 180f, (lenscales[i] - 1f) * 35f);
Lines.stroke((width + Mathf.absin(Time.time(), oscScl, oscMag)) * fout * strokes[s] * tscales[i]);
Tmp.v1.trns(b.rotation() + 180f, (lenscales[i] - 1f) * spaceMag);
Lines.stroke((width + Mathf.absin(Time.time, oscScl, oscMag)) * fout * strokes[s] * tscales[i]);
Lines.lineAngle(b.x + Tmp.v1.x, b.y + Tmp.v1.y, b.rotation(), baseLen * lenscales[i], false);
}
}
Tmp.v1.trns(b.rotation(), baseLen * 1.1f);
Drawf.light(b.team, b.x, b.y, b.x + Tmp.v1.x, b.y + Tmp.v1.y, 40, lightColor, 0.7f);
Drawf.light(b.team, b.x, b.y, b.x + Tmp.v1.x, b.y + Tmp.v1.y, lightStroke, lightColor, 0.7f);
Draw.reset();
}

View File

@@ -1,54 +0,0 @@
package mindustry.entities.bullet;
import arc.graphics.*;
import arc.graphics.g2d.*;
import mindustry.content.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.world.blocks.*;
public class HealBulletType extends BulletType{
protected float healPercent = 3f;
protected float height = 7f, width = 2f;
protected Color backColor = Pal.heal, frontColor = Color.white;
public HealBulletType(float speed, float damage){
super(speed, damage);
shootEffect = Fx.shootHeal;
smokeEffect = Fx.hitLaser;
hitEffect = Fx.hitLaser;
despawnEffect = Fx.hitLaser;
collidesTeam = true;
hittable = false;
}
public HealBulletType(){
this(1f, 1f);
}
@Override
public boolean collides(Bullet b, Building tile){
return tile.team != b.team || tile.healthf() < 1f;
}
@Override
public void draw(Bullet b){
Draw.color(backColor);
Lines.stroke(width);
Lines.lineAngleCenter(b.x, b.y, b.rotation(), height);
Draw.color(frontColor);
Lines.lineAngleCenter(b.x, b.y, b.rotation(), height / 2f);
Draw.reset();
}
@Override
public void hitTile(Bullet b, Building tile, float initialHealth){
super.hit(b);
if(tile.team == b.team && !(tile.block instanceof ConstructBlock)){
Fx.healBlockFull.at(tile.x, tile.y, tile.block.size, Pal.heal);
tile.heal(healPercent / 100f * tile.maxHealth());
}
}
}

View File

@@ -0,0 +1,33 @@
package mindustry.entities.bullet;
import arc.graphics.g2d.*;
import mindustry.gen.*;
import mindustry.content.*;
public class LaserBoltBulletType extends BasicBulletType{
public float width = 2f, height = 7f;
public LaserBoltBulletType(float speed, float damage){
super(speed, damage);
smokeEffect = Fx.hitLaser;
hitEffect = Fx.hitLaser;
despawnEffect = Fx.hitLaser;
hittable = false;
reflectable = false;
}
public LaserBoltBulletType(){
this(1f, 1f);
}
@Override
public void draw(Bullet b){
Draw.color(backColor);
Lines.stroke(width);
Lines.lineAngleCenter(b.x, b.y, b.rotation(), height);
Draw.color(frontColor);
Lines.lineAngleCenter(b.x, b.y, b.rotation(), height / 2f);
Draw.reset();
}
}

View File

@@ -23,22 +23,29 @@ public class LaserBulletType extends BulletType{
public LaserBulletType(float damage){
super(0.01f, damage);
keepVelocity = false;
hitEffect = Fx.hitLancer;
despawnEffect = Fx.none;
shootEffect = Fx.hitLancer;
smokeEffect = Fx.none;
collides = false;
hitSize = 4;
lifetime = 16f;
keepVelocity = false;
collides = false;
pierce = true;
hittable = false;
absorbable = false;
}
public LaserBulletType(){
this(1f);
}
//assume it pierces at least 3 blocks
@Override
public float estimateDPS(){
return super.estimateDPS() * 3f;
}
@Override
public void init(){
super.init();

View File

@@ -26,6 +26,11 @@ public class LightningBulletType extends BulletType{
return (lightningLength + lightningLengthRand/2f) * 6f;
}
@Override
public float estimateDPS(){
return super.estimateDPS() * Math.max(lightningLength / 10f, 1);
}
@Override
public void draw(Bullet b){
}

View File

@@ -3,7 +3,7 @@ package mindustry.entities.bullet;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.geom.*;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
@@ -13,8 +13,9 @@ import mindustry.world.*;
import static mindustry.Vars.*;
public class LiquidBulletType extends BulletType{
public @NonNull Liquid liquid;
public Liquid liquid;
public float puddleSize = 6f;
public float orbSize = 3f;
public LiquidBulletType(@Nullable Liquid liquid){
super(3.5f, 0);
@@ -22,10 +23,12 @@ public class LiquidBulletType extends BulletType{
if(liquid != null){
this.liquid = liquid;
this.status = liquid.effect;
lightColor = liquid.lightColor;
lightOpacity = liquid.lightColor.a;
}
ammoMultiplier = 1f;
lifetime = 74f;
lifetime = 34f;
statusDuration = 60f * 2f;
despawnEffect = Fx.none;
hitEffect = Fx.hitLiquid;
@@ -62,7 +65,7 @@ public class LiquidBulletType extends BulletType{
public void draw(Bullet b){
Draw.color(liquid.color, Color.white, b.fout() / 100f);
Fill.circle(b.x, b.y, 3f);
Fill.circle(b.x, b.y, orbSize);
}
@Override
@@ -79,7 +82,7 @@ public class LiquidBulletType extends BulletType{
Puddles.deposit(world.tileWorld(hitx, hity), liquid, puddleSize);
if(liquid.temperature <= 0.5f && liquid.flammability < 0.3f){
float intensity = 400f;
float intensity = 400f * puddleSize/6f;
Fires.extinguish(world.tileWorld(hitx, hity), intensity);
for(Point2 p : Geometry.d4){
Fires.extinguish(world.tileWorld(hitx + p.x * tilesize, hity + p.y * tilesize), intensity);

View File

@@ -1,15 +1,14 @@
package mindustry.entities.bullet;
import arc.graphics.Color;
import arc.graphics.g2d.Draw;
import arc.math.Angles;
import arc.math.Mathf;
import mindustry.content.Fx;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import mindustry.content.*;
import mindustry.gen.*;
import mindustry.graphics.Pal;
import mindustry.world.blocks.distribution.MassDriver.DriverBulletData;
import mindustry.graphics.*;
import mindustry.world.blocks.distribution.MassDriver.*;
import static mindustry.Vars.content;
import static mindustry.Vars.*;
public class MassDriverBolt extends BulletType{
@@ -37,15 +36,13 @@ public class MassDriverBolt extends BulletType{
@Override
public void update(Bullet b){
//data MUST be an instance of DriverBulletData
if(!(b.data() instanceof DriverBulletData)){
if(!(b.data() instanceof DriverBulletData data)){
hit(b);
return;
}
float hitDst = 7f;
DriverBulletData data = (DriverBulletData)b.data();
//if the target is dead, just keep flying until the bullet explodes
if(data.to.dead()){
return;
@@ -84,9 +81,7 @@ public class MassDriverBolt extends BulletType{
public void despawned(Bullet b){
super.despawned(b);
if(!(b.data() instanceof DriverBulletData)) return;
DriverBulletData data = (DriverBulletData)b.data();
if(!(b.data() instanceof DriverBulletData data)) return;
for(int i = 0; i < data.items.length; i++){
int amountDropped = Mathf.random(0, data.items[i]);

View File

@@ -0,0 +1,70 @@
package mindustry.entities.bullet;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class PointBulletType extends BulletType{
private static float cdist = 0f;
private static Unit result;
public float trailSpacing = 10f;
public PointBulletType(){
scaleVelocity = true;
lifetime = 100f;
collides = false;
keepVelocity = false;
backMove = false;
}
@Override
public void init(Bullet b){
super.init(b);
float px = b.x + b.lifetime * b.vel.x,
py = b.y + b.lifetime * b.vel.y,
rot = b.rotation();
Geometry.iterateLine(0f, b.x, b.y, px, py, trailSpacing, (x, y) -> {
trailEffect.at(x, y, rot);
});
b.time = b.lifetime;
b.set(px, py);
//calculate hit entity
cdist = 0f;
result = null;
float range = 1f;
Units.nearbyEnemies(b.team, px - range, py - range, range*2f, range*2f, e -> {
if(e.dead()) return;
e.hitbox(Tmp.r1);
if(!Tmp.r1.contains(px, py)) return;
float dst = e.dst(px, py) - e.hitSize;
if((result == null || dst < cdist)){
result = e;
cdist = dst;
}
});
if(result != null){
b.collision(result, px, py);
}else{
Building build = Vars.world.buildWorld(px, py);
if(build != null && build.team != b.team){
build.collision(b);
}
}
b.remove();
b.vel.setZero();
}
}

View File

@@ -1,60 +1,80 @@
package mindustry.entities.bullet;
import arc.math.geom.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
//TODO this class is bad for multiple reasons, remove/replace it.
//- effects unreliable
//- not really hitscan but works like it
//- buggy trails
//- looks bad
//- generally unreliable
public class RailBulletType extends BulletType{
public Effect pierceEffect = Fx.hitBulletSmall, updateEffect = Fx.none;
/** Multiplier of damage decreased per health pierced. */
public float pierceDamageFactor = 1f;
public float length = 100f;
public float updateEffectSeg = 20f;
public RailBulletType(){
pierceBuilding = true;
pierce = true;
reflectable = false;
hitEffect = Fx.none;
despawnEffect = Fx.none;
collides = false;
lifetime = 1f;
speed = 0.01f;
}
@Override
public float range(){
return length;
}
void handle(Bullet b, Posc pos, float initialHealth){
float sub = initialHealth*pierceDamageFactor;
float sub = Math.max(initialHealth*pierceDamageFactor, 0);
if(sub >= b.damage){
//cause a despawn
b.remove();
if(b.damage <= 0){
b.fdata = Math.min(b.fdata, b.dst(pos));
return;
}
if(b.damage > 0){
pierceEffect.at(pos.getX(), pos.getY(), b.rotation());
hitEffect.at(pos.getX(), pos.getY());
}
//subtract health from each consecutive pierce
b.damage -= Math.min(b.damage, sub);
if(b.damage > 0){
pierceEffect.at(pos.getX(), pos.getY(), b.rotation());
}
hitEffect.at(pos.getX(), pos.getY());
}
@Override
public void update(Bullet b){
if(b.timer(1, 0.9f)){
updateEffect.at(b.x, b.y, b.rotation());
public void init(Bullet b){
super.init(b);
b.fdata = length;
Damage.collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), length, false);
float resultLen = b.fdata;
Vec2 nor = Tmp.v1.set(b.vel).nor();
for(float i = 0; i <= resultLen; i += updateEffectSeg){
updateEffect.at(b.x + nor.x * i, b.y + nor.y * i, b.rotation());
}
}
@Override
public boolean testCollision(Bullet bullet, Building tile){
return bullet.team != tile.team;
}
@Override
public void hitEntity(Bullet b, Hitboxc entity, float initialHealth){
handle(b, entity, initialHealth);
}
@Override
public void hitTile(Bullet b, Building tile, float initialHealth){
handle(b, tile, initialHealth);
public void hitTile(Bullet b, Building build, float initialHealth, boolean direct){
handle(b, build, initialHealth);
}
}

View File

@@ -19,7 +19,8 @@ public class SapBulletType extends BulletType{
public SapBulletType(){
speed = 0.0001f;
despawnEffect = Fx.none;
pierce = true;
pierce = false;
collides = false;
hitSize = 0f;
hittable = false;
hitEffect = Fx.hitLiquid;
@@ -29,8 +30,7 @@ public class SapBulletType extends BulletType{
@Override
public void draw(Bullet b){
if(b.data instanceof Position){
Position data = (Position)b.data;
if(b.data instanceof Position data){
Tmp.v1.set(data).lerp(b, b.fin());
Draw.color(color);
@@ -61,21 +61,17 @@ public class SapBulletType extends BulletType{
b.data = target;
if(target != null){
float result = Math.min(target.health(), damage);
float result = Math.max(Math.min(target.health(), damage), 0);
if(b.owner instanceof Healthc){
((Healthc)b.owner).heal(result * sapStrength);
if(b.owner instanceof Healthc h){
h.heal(result * sapStrength);
}
}
if(target instanceof Hitboxc){
Hitboxc hit = (Hitboxc)target;
if(target instanceof Hitboxc hit){
hit.collision(b, hit.x(), hit.y());
b.collision(hit, hit.x(), hit.y());
}else if(target instanceof Building){
Building tile = (Building)target;
}else if(target instanceof Building tile){
if(tile.collide(b)){
tile.collision(b);
hit(b, tile.x, tile.y);

View File

@@ -24,9 +24,11 @@ public class ShrapnelBulletType extends BulletType{
shootEffect = smokeEffect = Fx.lightningShoot;
lifetime = 10f;
despawnEffect = Fx.none;
pierce = true;
keepVelocity = false;
collides = false;
pierce = true;
hittable = false;
absorbable = false;
}
@Override

View File

@@ -0,0 +1,28 @@
package mindustry.entities.comp;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.units.*;
@Component
abstract class AmmoDistributeComp implements Unitc{
@Import float x, y;
@Import UnitType type;
@Import Team team;
@Import float ammo;
private transient float ammoCooldown;
@Override
public void update(){
if(ammoCooldown > 0f) ammoCooldown -= Time.delta;
if(ammo > 0 && ammoCooldown <= 0f && ResupplyPoint.resupply(team, x, y, type.ammoResupplyRange, Math.min(type.ammoResupplyAmount, ammo), type.ammoType.color, u -> u != self())){
ammo -= Math.min(type.ammoResupplyAmount, ammo);
ammoCooldown = 5f;
}
}
}

View File

@@ -16,11 +16,13 @@ abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{
@Override
public void update(){
//repel unit out of bounds
if(x < 0) vel.x += (-x/warpDst);
if(y < 0) vel.y += (-y/warpDst);
if(x > world.unitWidth()) vel.x -= (x - world.unitWidth())/warpDst;
if(y > world.unitHeight()) vel.y -= (y - world.unitHeight())/warpDst;
if(!net.client() || isLocal()){
//repel unit out of bounds
if(x < 0) vel.x += (-x/warpDst);
if(y < 0) vel.y += (-y/warpDst);
if(x > world.unitWidth()) vel.x -= (x - world.unitWidth())/warpDst;
if(y > world.unitHeight()) vel.y -= (y - world.unitHeight())/warpDst;
}
//clamp position if not flying
if(isGrounded()){

View File

@@ -1,19 +1,21 @@
package mindustry.entities.comp;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.Queue;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.ConstructBlock.*;
@@ -23,20 +25,38 @@ import java.util.*;
import static mindustry.Vars.*;
@Component
abstract class BuilderComp implements Unitc{
abstract class BuilderComp implements Posc, Teamc, Rotc{
static final Vec2[] vecs = new Vec2[]{new Vec2(), new Vec2(), new Vec2(), new Vec2()};
@Import float x, y, rotation;
@Import UnitType type;
@Import Team team;
@SyncLocal Queue<BuildPlan> plans = new Queue<>();
@SyncLocal transient boolean updateBuilding = true;
@SyncLocal Queue<BuildPlan> plans = new Queue<>(1);
@SyncLocal boolean updateBuilding = true;
private transient BuildPlan lastActive;
private transient int lastSize;
private transient float buildAlpha = 0f;
public boolean canBuild(){
return type.buildSpeed > 0;
}
@Override
public void update(){
if(!updateBuilding) return;
if(!headless){
//visual activity update
if(lastActive != null && buildAlpha <= 0.01f){
lastActive = null;
}
buildAlpha = Mathf.lerpDelta(buildAlpha, activelyBuilding() ? 1f : 0f, 0.15f);
}
if(!updateBuilding || !canBuild()) return;
float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : buildingRange;
boolean infinite = state.rules.infiniteResources || team().rules().infiniteResources;
Iterator<BuildPlan> it = plans.iterator();
@@ -57,7 +77,7 @@ abstract class BuilderComp implements Unitc{
if(plans.size > 1){
int total = 0;
BuildPlan req;
while((dst((req = buildPlan()).tile()) > finalPlaceDst || shouldSkip(req, core)) && total < plans.size){
while((!within((req = buildPlan()).tile(), finalPlaceDst) || shouldSkip(req, core)) && total < plans.size){
plans.removeFirst();
plans.addLast(req);
total++;
@@ -65,82 +85,88 @@ abstract class BuilderComp implements Unitc{
}
BuildPlan current = buildPlan();
Tile tile = current.tile();
if(!within(current.tile(), finalPlaceDst)) return;
lastActive = current;
buildAlpha = 1f;
if(current.breaking) lastSize = tile.block().size;
Tile tile = world.tile(current.x, current.y);
if(!within(tile, finalPlaceDst)) return;
if(within(tile, finalPlaceDst)){
lookAt(angleTo(tile));
}
if(!(tile.block() instanceof ConstructBlock)){
if(!current.initialized && !current.breaking && Build.validPlace(current.block, team(), current.x, current.y, current.rotation)){
boolean hasAll = infinite || !Structs.contains(current.block.requirements, i -> core != null && !core.items.has(i.item));
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));
if(hasAll){
Call.beginPlace(current.block, team(), current.x, current.y, current.rotation);
Call.beginPlace(self(), current.block, team, current.x, current.y, current.rotation);
}else{
current.stuck = true;
}
}else if(!current.initialized && current.breaking && Build.validBreak(team(), current.x, current.y)){
Call.beginBreak(team(), current.x, current.y);
}else if(!current.initialized && current.breaking && Build.validBreak(team, current.x, current.y)){
Call.beginBreak(self(), team, current.x, current.y);
}else{
plans.removeFirst();
return;
}
}else if(tile.team() != team()){
}else if((tile.team() != team && tile.team() != Team.derelict) || (!current.breaking && cb.cblock != current.block)){
plans.removeFirst();
return;
}
if(tile.build instanceof ConstructBuild && !current.initialized){
Core.app.post(() -> Events.fire(new BuildSelectEvent(tile, team(), (Builderc)this, current.breaking)));
Core.app.post(() -> Events.fire(new BuildSelectEvent(tile, team, self(), current.breaking)));
current.initialized = true;
}
//if there is no core to build with or no build entity, stop building!
if((core == null && !infinite) || !(tile.build instanceof ConstructBuild)){
if((core == null && !infinite) || !(tile.build instanceof ConstructBuild entity)){
return;
}
//otherwise, update it.
ConstructBuild entity = tile.bc();
if(current.breaking){
entity.deconstruct(self(), core, 1f / entity.buildCost * Time.delta * type().buildSpeed * state.rules.buildSpeedMultiplier);
entity.deconstruct(self(), core, 1f / entity.buildCost * Time.delta * type.buildSpeed * state.rules.buildSpeedMultiplier);
}else{
entity.construct(self(), core, 1f / entity.buildCost * Time.delta * type().buildSpeed * state.rules.buildSpeedMultiplier, current.config);
entity.construct(self(), core, 1f / entity.buildCost * Time.delta * type.buildSpeed * state.rules.buildSpeedMultiplier, current.config);
}
current.stuck = Mathf.equal(current.progress, entity.progress);
current.progress = entity.progress;
}
/** Draw all current build plans. Does not draw the beam effect, only the positions. */
void drawBuildPlans(){
/** Draw all current build requests. Does not draw the beam effect, only the positions. */
void drawBuildRequests(){
for(BuildPlan request : plans){
if(request.progress > 0.01f || (buildPlan() == request && request.initialized && (dst(request.x * tilesize, request.y * tilesize) <= buildingRange || state.isEditor()))) continue;
request.animScale = 1f;
if(request.breaking){
control.input.drawBreaking(request);
}else{
request.block.drawRequest(request, control.input.allRequests(),
Build.validPlace(request.block, team(), request.x, request.y, request.rotation) || control.input.requestMatches(request));
}
for(BuildPlan plan : plans){
if(plan.progress > 0.01f || (buildPlan() == plan && plan.initialized && (within(plan.x * tilesize, plan.y * tilesize, buildingRange) || state.isEditor()))) continue;
drawPlan(plan, 1f);
}
Draw.reset();
}
void drawPlan(BuildPlan request, float alpha){
request.animScale = 1f;
if(request.breaking){
control.input.drawBreaking(request);
}else{
request.block.drawPlan(request, control.input.allRequests(),
Build.validPlace(request.block, team, request.x, request.y, request.rotation) || control.input.requestMatches(request),
alpha);
Draw.reset();
Draw.mixcol(Color.white, 0.24f + Mathf.absin(Time.globalTime, 6f, 0.28f));
Draw.alpha(alpha);
request.block.drawRequestConfigTop(request, plans);
}
}
/** @return whether this request should be skipped, in favor of the next one. */
boolean shouldSkip(BuildPlan request, @Nullable Building core){
//requests that you have at least *started* are considered
if(state.rules.infiniteResources || team().rules().infiniteResources || request.breaking || core == null) return false;
return (request.stuck && !core.items.has(request.block.requirements)) || (Structs.contains(request.block.requirements, i -> !core.items.has(i.item)) && !request.initialized);
if(state.rules.infiniteResources || team.rules().infiniteResources || request.breaking || core == null || request.isRotation(team)) return false;
return (request.stuck && !core.items.has(request.block.requirements)) || (Structs.contains(request.block.requirements, i -> !core.items.has(i.item) && Mathf.round(i.amount * state.rules.buildCostMultiplier) > 0) && !request.initialized);
}
void removeBuild(int x, int y, boolean breaking){
@@ -168,6 +194,8 @@ abstract class BuilderComp implements Unitc{
/** Add another build requests to the queue, if it doesn't exist there yet. */
void addBuild(BuildPlan place, boolean tail){
if(!canBuild()) return;
BuildPlan replace = null;
for(BuildPlan request : plans){
if(request.x == place.x && request.y == place.y){
@@ -179,8 +207,8 @@ abstract class BuilderComp implements Unitc{
plans.remove(replace);
}
Tile tile = world.tile(place.x, place.y);
if(tile != null && tile.build instanceof ConstructBuild){
place.progress = tile.<ConstructBuild>bc().progress;
if(tile != null && tile.build instanceof ConstructBuild cons){
place.progress = cons.progress;
}
if(tail){
plans.addLast(place);
@@ -190,6 +218,12 @@ abstract class BuilderComp implements Unitc{
}
boolean activelyBuilding(){
//not actively building when not near the build plan
if(isBuilding()){
if(!state.isEditor() && !within(buildPlan(), state.rules.infiniteResources ? Float.MAX_VALUE : buildingRange)){
return false;
}
}
return isBuilding() && updateBuilding;
}
@@ -199,25 +233,32 @@ abstract class BuilderComp implements Unitc{
return plans.size == 0 ? null : plans.first();
}
@Override
public void draw(){
if(!isBuilding() || !updateBuilding) return;
boolean active = activelyBuilding();
if(!active && lastActive == null) return;
//TODO check correctness
Draw.z(Layer.flyingUnit);
BuildPlan plan = buildPlan();
BuildPlan plan = active ? buildPlan() : lastActive;
Tile tile = world.tile(plan.x, plan.y);
var core = team.core();
if(dst(tile) > buildingRange && !state.isEditor()){
if(tile == null || !within(plan, state.rules.infiniteResources ? Float.MAX_VALUE : buildingRange)){
return;
}
int size = plan.breaking ? tile.block().size : plan.block.size;
//draw remote plans.
if(core != null && active && !isLocal() && !(tile.block() instanceof ConstructBlock)){
Draw.z(Layer.plans - 1f);
drawPlan(plan, 0.5f);
Draw.z(Layer.flyingUnit);
}
int size = plan.breaking ? active ? tile.block().size : lastSize : plan.block.size;
float tx = plan.drawx(), ty = plan.drawy();
Lines.stroke(1f, Pal.accent);
float focusLen = 3.8f + Mathf.absin(Time.time(), 1.1f, 0.6f);
Lines.stroke(1f, plan.breaking ? Pal.remove : Pal.accent);
float focusLen = type.buildBeamOffset + Mathf.absin(Time.time, 3f, 0.6f);
float px = x + Angles.trnsx(rotation, focusLen);
float py = y + Angles.trnsy(rotation, focusLen);
@@ -231,16 +272,35 @@ abstract class BuilderComp implements Unitc{
Arrays.sort(vecs, Structs.comparingFloat(vec -> -Angles.angleDist(angleTo(vec), ang)));
Vec2 close = Geometry.findClosest(x, y, vecs);
float x1 = vecs[0].x, y1 = vecs[0].y,
x2 = close.x, y2 = close.y,
x3 = vecs[1].x, y3 = vecs[1].y;
Draw.alpha(1f);
Draw.z(Layer.buildBeam);
Lines.line(px, py, x1, y1);
Lines.line(px, py, x3, y3);
Draw.alpha(buildAlpha);
Fill.circle(px, py, 1.6f + Mathf.absin(Time.time(), 0.8f, 1.5f));
if(!active && !(tile.build instanceof ConstructBuild)){
Fill.square(plan.drawx(), plan.drawy(), size * tilesize/2f);
}
Draw.color();
if(renderer.animateShields){
if(close != vecs[0] && close != vecs[1]){
Fill.tri(px, py, x1, y1, x2, y2);
Fill.tri(px, py, x3, y3, x2, y2);
}else{
Fill.tri(px, py, x1, y1, x3, y3);
}
}else{
Lines.line(px, py, x1, y1);
Lines.line(px, py, x3, y3);
}
Fill.square(px, py, 1.8f + Mathf.absin(Time.time, 2.2f, 1.1f), rotation + 45);
Draw.reset();
Draw.z(Layer.flyingUnit);
}
}

View File

@@ -13,21 +13,25 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import arc.util.io.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.audio.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.entities.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.ConstructBlock.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.payloads.*;
import mindustry.world.blocks.power.*;
@@ -47,17 +51,18 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
static final Seq<Tile> tempTiles = new Seq<>();
static int sleepingEntities = 0;
@Import float x, y, health;
@Import float x, y, health, maxHealth;
@Import Team team;
transient Tile tile;
transient Block block;
transient Seq<Building> proximity = new Seq<>(8);
transient boolean updateFlow;
transient byte dump;
transient byte cdump;
transient int rotation;
transient boolean enabled = true;
transient float enabledControlTime;
transient String lastAccessed;
PowerModule power;
ItemModule items;
@@ -79,8 +84,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}else{
if(block.hasPower){
//reinit power graph
power.graph = new PowerGraph();
power.graph.add(self());
new PowerGraph().add(self());
}
}
this.rotation = rotation;
@@ -103,8 +107,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
this.block = block;
this.team = team;
if(block.activeSound != Sounds.none){
sound = new SoundLoop(block.activeSound, block.activeSoundVolume);
if(block.loopSound != Sounds.none){
sound = new SoundLoop(block.loopSound, block.loopSoundVolume);
}
health = block.health;
@@ -191,6 +195,39 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
//endregion
//region utility methods
public void addPlan(boolean checkPrevious){
if(!block.rebuildable || (team == state.rules.defaultTeam && state.isCampaign() && !block.isVisible())) return;
Object overrideConfig = null;
if(self() instanceof ConstructBuild entity){
//update block to reflect the fact that something was being constructed
if(entity.cblock != null && entity.cblock.synthetic() && entity.wasConstructing){
block = entity.cblock;
overrideConfig = entity.lastConfig;
}else{
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
return;
}
}
TeamData data = state.teams.get(team);
if(checkPrevious){
//remove existing blocks that have been placed here.
//painful O(n) iteration + copy
for(int i = 0; i < data.blocks.size; i++){
BlockPlan b = data.blocks.get(i);
if(b.x == tile.x && b.y == tile.y){
data.blocks.removeIndex(i);
break;
}
}
}
data.blocks.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, block.id, overrideConfig == null ? config() : overrideConfig));
}
/** Configure with the current, local player. */
public void configure(Object value){
//save last used config
@@ -216,9 +253,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return true;
}
public void applyBoost(float intensity, float duration){
public void applyBoost(float intensity, float duration){
//do not refresh time scale when getting a weaker intensity
if(intensity >= this.timeScale){
timeScaleDuration = Math.max(timeScaleDuration, duration);
}
timeScale = Math.max(timeScale, intensity);
timeScaleDuration = Math.max(timeScaleDuration, duration);
}
public Building nearby(int dx, int dy){
@@ -318,6 +358,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return power != null && (block.consumes.has(ConsumeType.power) && !block.consumes.getPower().buffered) ? power.status : 1f;
}
public BlockStatus status(){
return cons.status();
}
/** Call when nothing is happening to the entity. This increments the internal sleep timer. */
public void sleep(){
sleepTime += Time.delta;
@@ -346,6 +390,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
//endregion
//region handler methods
public boolean canUnload(){
return block.unloadable;
}
/** Called when an unloader takes an item. */
public void itemTaken(Item item){
}
/** Called when this block is dropped as a payload. */
public void dropped(){
@@ -393,7 +446,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
/** Handle a stack input. */
public void handleStack(Item item, int amount, Teamc source){
public void handleStack(Item item, int amount, @Nullable Teamc source){
noSleep();
items.add(item, amount);
}
@@ -421,9 +474,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
* @param todump payload to dump.
* @return whether the payload was moved successfully
*/
public boolean movePayload(@NonNull Payload todump){
public boolean movePayload(Payload todump){
int trns = block.size/2 + 1;
Tile next = tile.getNearby(Geometry.d4(rotation).x * trns, Geometry.d4(rotation).y * trns);
Tile next = tile.nearby(Geometry.d4(rotation).x * trns, Geometry.d4(rotation).y * trns);
if(next != null && next.build != null && next.build.team == team && next.build.acceptPayload(self(), todump)){
next.build.handlePayload(self(), todump);
@@ -438,10 +491,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
* @param todump payload to dump.
* @return whether the payload was moved successfully
*/
public boolean dumpPayload(@NonNull Payload todump){
public boolean dumpPayload(Payload todump){
if(proximity.size == 0) return false;
int dump = this.dump;
int dump = this.cdump;
for(int i = 0; i < proximity.size; i++){
Building other = proximity.get((i + dump) % proximity.size);
@@ -466,8 +519,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return block.consumes.itemFilters.get(item.id) && items.get(item) < getMaximumAccepted(item);
}
public boolean acceptLiquid(Building source, Liquid liquid, float amount){
return block.hasLiquids && liquids.get(liquid) + amount < block.liquidCapacity && block.consumes.liquidfilters.get(liquid.id);
public boolean acceptLiquid(Building source, Liquid liquid){
return block.hasLiquids && block.consumes.liquidfilters.get(liquid.id);
}
public void handleLiquid(Building source, Liquid liquid, float amount){
@@ -475,7 +528,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
public void dumpLiquid(Liquid liquid){
int dump = this.dump;
int dump = this.cdump;
if(liquids.get(liquid) <= 0.0001f) return;
if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) liquid.unlock();
for(int i = 0; i < proximity.size; i++){
incrementDump(proximity.size);
@@ -497,23 +554,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
public void transferLiquid(Building next, float amount, Liquid liquid){
float flow = Math.min(next.block.liquidCapacity - next.liquids.get(liquid) - 0.001f, amount);
float flow = Math.min(next.block.liquidCapacity - next.liquids.get(liquid), amount);
if(next.acceptLiquid(self(), liquid, flow)){
if(next.acceptLiquid(self(), liquid)){
next.handleLiquid(self(), liquid, flow);
liquids.remove(liquid, flow);
}
}
public float moveLiquidForward(float leakResistance, Liquid liquid){
Tile next = tile.getNearby(rotation);
public float moveLiquidForward(boolean leaks, Liquid liquid){
Tile next = tile.nearby(rotation);
if(next == null) return 0;
if(next.build != null){
return moveLiquid(next.build, liquid);
}else if(leakResistance != 100f && !next.block().solid && !next.block().hasLiquids){
float leakAmount = liquids.get(liquid) / leakResistance;
}else if(leaks && !next.block().solid && !next.block().hasLiquids){
float leakAmount = liquids.get(liquid) / 1.5f;
Puddles.deposit(next, tile, liquid, leakAmount);
liquids.remove(liquid, leakAmount);
}
@@ -528,10 +585,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(next.team == team && next.block.hasLiquids && liquids.get(liquid) > 0f){
float ofract = next.liquids.get(liquid) / next.block.liquidCapacity;
float fract = liquids.get(liquid) / block.liquidCapacity * block.liquidPressure;
float flow = Math.min(Mathf.clamp((fract - ofract) * (1f)) * (block.liquidCapacity), liquids.get(liquid));
flow = Math.min(flow, next.block.liquidCapacity - next.liquids.get(liquid) - 0.001f);
float flow = Math.min(Mathf.clamp((fract - ofract)) * (block.liquidCapacity), liquids.get(liquid));
flow = Math.min(flow, next.block.liquidCapacity - next.liquids.get(liquid));
if(flow > 0f && ofract <= fract && next.acceptLiquid(self(), liquid, flow)){
if(flow > 0f && ofract <= fract && next.acceptLiquid(self(), liquid)){
next.handleLiquid(self(), liquid, flow);
liquids.remove(liquid, flow);
return flow;
@@ -575,7 +632,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
* containers, it gets added to the block's inventory.
*/
public void offload(Item item){
int dump = this.dump;
produced(item, 1);
int dump = this.cdump;
if(!net.client() && state.isCampaign() && team == state.rules.defaultTeam) item.unlock();
for(int i = 0; i < proximity.size; i++){
incrementDump(proximity.size);
@@ -593,7 +652,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
* Tries to put this item into a nearby container. Returns success. Unlike #offload(), this method does not change the block inventory.
*/
public boolean put(Item item){
int dump = this.dump;
int dump = this.cdump;
for(int i = 0; i < proximity.size; i++){
incrementDump(proximity.size);
@@ -607,6 +666,14 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return false;
}
public void produced(Item item){
produced(item, 1);
}
public void produced(Item item, int amount){
if(Vars.state.rules.sector != null && team == state.rules.defaultTeam) Vars.state.rules.sector.info.handleProduction(item, amount);
}
/** Try dumping any item near the */
public boolean dump(){
return dump(null);
@@ -619,7 +686,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public boolean dump(Item todump){
if(!block.hasItems || items.total() == 0 || (todump != null && !items.has(todump))) return false;
int dump = this.dump;
int dump = this.cdump;
if(proximity.size == 0) return false;
@@ -654,7 +721,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
public void incrementDump(int prox){
dump = (byte)((dump + 1) % prox);
cdump = (byte)((cdump + 1) % prox);
}
/** Used for dumping items. */
@@ -692,9 +759,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
public void powerGraphRemoved(){
if(power == null){
return;
}
if(power == null) return;
power.graph.remove(self());
for(int i = 0; i < power.links.size; i++){
@@ -739,7 +804,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
/** @return whether this block should play its idle sound.*/
public boolean shouldIdleSound(){
public boolean shouldAmbientSound(){
return shouldConsume();
}
@@ -751,16 +816,16 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
Draw.z(Layer.power + 1);
Draw.color(Pal.gray);
Fill.square(brcx, brcy, 2.5f, 45);
Draw.color(cons.status().color);
Draw.color(status().color);
Fill.square(brcx, brcy, 1.5f, 45);
Draw.color();
}
}
public void drawCracks(){
if(!damaged() || block.size > Block.maxCrackSize) return;
if(!damaged() || block.size > BlockRenderer.maxCrackSize) return;
int id = pos();
TextureRegion region = Block.cracks[block.size - 1][Mathf.clamp((int)((1f - healthf()) * Block.crackRegions), 0, Block.crackRegions-1)];
TextureRegion region = renderer.blocks.cracks[block.size - 1][Mathf.clamp((int)((1f - healthf()) * BlockRenderer.crackRegions), 0, BlockRenderer.crackRegions-1)];
Draw.colorl(0.2f, 0.1f + (1f - healthf())* 0.6f);
Draw.rect(region, x, y, (id%4)*90);
Draw.color();
@@ -828,7 +893,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void placed(){
if(net.client()) return;
if((block.consumesPower && !block.outputsPower) || (!block.consumesPower && block.outputsPower)){
if(block.consumesPower || block.outputsPower){
int range = 10;
tempTiles.clear();
Geometry.circle(tileX(), tileY(), range, (x, y) -> {
@@ -849,6 +914,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
/**
* Called when a block is placed over some other blocks. This seq will always have at least one item.
* Should load some previous state, if necessary. */
public void overwrote(Seq<Building> previous){
}
public void onRemoved(){
}
@@ -863,7 +935,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Called when arbitrary configuration is applied to a tile. */
public void configured(@Nullable Unit builder, @Nullable Object value){
//null is of type void.class; anonymous classes use their superclass.
Class<?> type = value == null ? void.class : value.getClass().isAnonymousClass() ? value.getClass().getSuperclass() : value.getClass();
Class<?> type = value == null ? void.class : value.getClass().isAnonymousClass() || value.getClass().getSimpleName().startsWith("adapter") ? value.getClass().getSuperclass() : value.getClass();
if(value instanceof Item) type = Item.class;
if(value instanceof Block) type = Block.class;
if(value instanceof Liquid) type = Liquid.class;
if(builder != null && builder.isPlayer()){
lastAccessed = builder.getPlayer().name;
}
if(block.configurations.containsKey(type)){
block.configurations.get(type).get(this, value);
@@ -914,34 +994,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
});
}
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, Pal.darkFlame, state.rules.damageExplosions);
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions);
if(!floor().solid && !floor().isLiquid){
Effect.rubble(x, y, block.size);
}
}
/**
* Returns the flammability of the Used for fire calculations.
* Takes flammability of floor liquid into account.
*/
public float getFlammability(){
if(!block.hasItems){
if(floor().isLiquid && !block.solid){
return floor().liquidDrop.flammability;
}
return 0;
}else{
float result = items.sum((item, amount) -> item.flammability * amount);
if(block.hasLiquids){
result += liquids.sum((liquid, amount) -> liquid.flammability * amount / 3f);
}
return result;
}
}
public String getDisplayName(){
return block.localizedName;
}
@@ -1029,6 +1088,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
if(net.active() && lastAccessed != null){
table.row();
table.add(Core.bundle.format("lastaccessed", lastAccessed)).growX().wrap().left();
}
table.marginBottom(-5);
}
}
@@ -1043,8 +1107,13 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void displayBars(Table table){
for(Func<Building, Bar> bar : block.bars.list()){
table.add(bar.get(self())).growX();
table.row();
//TODO fix conclusively
try{
table.add(bar.get(self())).growX();
table.row();
}catch(ClassCastException e){
break;
}
}
}
@@ -1061,7 +1130,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Returns whether or not a hand cursor should be shown over this block. */
public Cursor getCursor(){
return block.configurable ? SystemCursor.hand : SystemCursor.arrow;
return block.configurable && team == player.team() ? SystemCursor.hand : SystemCursor.arrow;
}
/**
@@ -1093,7 +1162,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return false;
}
public float handleDamage(float amount){
return amount;
}
@@ -1105,7 +1173,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Handle a bullet collision.
* @return whether the bullet should be removed. */
public boolean collision(Bullet other){
damage(other.damage() * other.type().tileDamageMultiplier);
damage(other.damage() * other.type().buildingDamageMultiplier);
return true;
}
@@ -1114,6 +1182,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return true;
}
public void pickedUp(){
}
public void removeFromProximity(){
onProximityRemoved();
tmpTiles.clear();
@@ -1156,10 +1228,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
proximity.add(tile);
}
for(Building other : tmpTiles){
other.onProximityUpdate();
}
onProximityAdded();
onProximityUpdate();
@@ -1172,6 +1240,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
/** @return ambient sound volume scale. */
public float ambientVolume(){
return efficiency();
}
//endregion
//region overrides
@@ -1213,31 +1286,41 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
@Override
public double sense(LAccess sensor){
if(sensor == LAccess.x) return x;
if(sensor == LAccess.y) return y;
if(sensor == LAccess.team) return team.id;
if(sensor == LAccess.health) return health;
if(sensor == LAccess.maxHealth) return maxHealth();
if(sensor == LAccess.efficiency) return efficiency();
if(sensor == LAccess.rotation) return rotation;
if(sensor == LAccess.totalItems && items != null) return items.total();
if(sensor == LAccess.totalLiquids && liquids != null) return liquids.total();
if(sensor == LAccess.totalPower && power != null && block.consumes.hasPower()) return power.status * (block.consumes.getPower().buffered ? block.consumes.getPower().capacity : 1f);
if(sensor == LAccess.itemCapacity) return block.itemCapacity;
if(sensor == LAccess.liquidCapacity) return block.liquidCapacity;
if(sensor == LAccess.powerCapacity) return block.consumes.hasPower() ? block.consumes.getPower().capacity : 0f;
if(sensor == LAccess.powerNetIn && power != null) return power.graph.getLastScaledPowerIn() * 60;
if(sensor == LAccess.powerNetOut && power != null) return power.graph.getLastScaledPowerOut() * 60;
if(sensor == LAccess.powerNetStored && power != null) return power.graph.getLastPowerStored();
if(sensor == LAccess.powerNetCapacity && power != null) return power.graph.getLastCapacity();
return 0;
return switch(sensor){
case x -> World.conv(x);
case y -> World.conv(y);
case team -> team.id;
case health -> health;
case maxHealth -> maxHealth;
case efficiency -> efficiency();
case rotation -> rotation;
case totalItems -> items == null ? 0 : items.total();
case totalLiquids -> liquids == null ? 0 : liquids.total();
case totalPower -> power == null || !block.consumes.hasPower() ? 0 : power.status * (block.consumes.getPower().buffered ? block.consumes.getPower().capacity : 1f);
case itemCapacity -> block.hasItems ? block.itemCapacity : 0;
case liquidCapacity -> block.hasLiquids ? block.liquidCapacity : 0;
case powerCapacity -> block.consumes.hasPower() ? block.consumes.getPower().capacity : 0f;
case powerNetIn -> power == null ? 0 : power.graph.getLastScaledPowerIn() * 60;
case powerNetOut -> power == null ? 0 : power.graph.getLastScaledPowerOut() * 60;
case powerNetStored -> power == null ? 0 : power.graph.getLastPowerStored();
case powerNetCapacity -> power == null ? 0 : power.graph.getLastCapacity();
case enabled -> enabled ? 1 : 0;
case controlled -> this instanceof ControlBlock c ? c.isControlled() ? 1 : 0 : 0;
case payloadCount -> getPayload() != null ? 1 : 0;
default -> 0;
};
}
@Override
public Object senseObject(LAccess sensor){
if(sensor == LAccess.type) return block;
return switch(sensor){
case type -> block;
case firstItem -> items == null ? null : items.first();
case config -> block.configurations.containsKey(Item.class) || block.configurations.containsKey(Liquid.class) ? config() : null;
case payloadType -> getPayload() instanceof UnitPayload p1 ? p1.unit.type : getPayload() instanceof BuildPayload p2 ? p2.block() : null;
default -> noSensed;
};
return noSensed;
}
@Override
@@ -1255,6 +1338,18 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
@Override
public void control(LAccess type, Object p1, double p2, double p3, double p4){
//don't execute configure instructions as the client
if(type == LAccess.configure && block.logicConfigurable && !net.client()){
//change config only if it's new
Object prev = senseObject(LAccess.config);
if(prev != p1){
configureAny(p1);
}
}
}
@Override
public void remove(){
if(sound != null){
@@ -1274,6 +1369,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
@Final
@Override
public void update(){
if(state.isEditor()) return;
timeScaleDuration -= Time.delta;
if(timeScaleDuration <= 0f || !block.canOverdrive){
timeScale = 1f;
@@ -1287,12 +1384,18 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
}
}
if(sound != null){
sound.update(x, y, shouldActiveSound());
if(team == Team.derelict){
enabled = false;
}
if(block.idleSound != Sounds.none && shouldIdleSound()){
loops.play(block.idleSound, self(), block.idleSoundVolume);
if(!headless){
if(sound != null){
sound.update(x, y, shouldActiveSound());
}
if(block.ambientSound != Sounds.none && shouldAmbientSound()){
control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
}
}
if(enabled || !block.noUpdateDisabled){

View File

@@ -1,5 +1,6 @@
package mindustry.entities.comp;
import arc.*;
import arc.func.*;
import arc.graphics.g2d.*;
import arc.math.*;
@@ -7,10 +8,14 @@ import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.core.*;
import mindustry.entities.bullet.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.world.blocks.defense.Wall.*;
import static mindustry.Vars.*;
@@ -19,25 +24,20 @@ import static mindustry.Vars.*;
abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Drawc, Shielderc, Ownerc, Velc, Bulletc, Timerc{
@Import Team team;
@Import Entityc owner;
@Import float x,y;
@Import float x, y, damage;
IntSeq collided = new IntSeq(6);
Object data;
BulletType type;
float damage;
float fdata;
transient boolean absorbed;
@Override
public void getCollisions(Cons<QuadTree> consumer){
if(team.active()){
for(Team team : team.enemies()){
consumer.get(teamIndex.tree(team));
}
}else{
for(Team other : Team.all){
if(other != team && teamIndex.count(other) > 0){
consumer.get(teamIndex.tree(other));
}
Seq<TeamData> data = state.teams.present;
for(int i = 0; i < data.size; i++){
if(data.items[i].team != team){
consumer.get(data.items[i].tree());
}
}
}
@@ -68,6 +68,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
@Override
public void absorb(){
absorbed = true;
remove();
}
@@ -76,11 +77,6 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
return type.drawSize;
}
@Override
public float damage(){
return damage * damageMultiplier();
}
@Replace
@Override
public boolean collides(Hitboxc other){
@@ -95,14 +91,12 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
type.hit(self(), x, y);
float health = 0f;
if(other instanceof Healthc){
Healthc h = (Healthc)other;
if(other instanceof Healthc h){
health = h.health();
h.damage(damage);
}
if(other instanceof Unit){
Unit unit = (Unit)other;
if(other instanceof Unit unit){
unit.impulse(Tmp.v3.set(unit).sub(this.x, this.y).nor().scl(type.knockback * 80f));
unit.apply(type.status, type.statusDuration);
}
@@ -115,6 +109,10 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
}
type.hitEntity(self(), other, health);
if(owner instanceof WallBuild && player != null && team == player.team() && other instanceof Unit unit && unit.dead){
Events.fire(Trigger.phaseDeflectHit);
}
}
@Override
@@ -122,12 +120,12 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
type.update(self());
if(type.collidesTiles && type.collides && type.collidesGround){
world.raycastEach(world.toTile(lastX()), world.toTile(lastY()), tileX(), tileY(), (x, y) -> {
world.raycastEach(World.toTile(lastX()), World.toTile(lastY()), tileX(), tileY(), (x, y) -> {
Building tile = world.build(x, y);
if(tile == null || !isAdded()) return false;
if(tile.collide(self()) && type.collides(self(), tile) && !tile.dead() && (type.collidesTeam || tile.team != team) && !(type.pierceBuilding && collided.contains(tile.id))){
if(tile.collide(self()) && type.testCollision(self(), tile) && !tile.dead() && (type.collidesTeam || tile.team != team) && !(type.pierceBuilding && collided.contains(tile.id))){
boolean remove = false;
float health = tile.health;
@@ -144,7 +142,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
}
}
type.hitTile(self(), tile, health);
type.hitTile(self(), tile, health, true);
return !type.pierceBuilding;
}
@@ -152,6 +150,10 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
return false;
});
}
if(type.pierceCap != -1 && collided.size >= type.pierceCap){
remove();
}
}
@Override

View File

@@ -1,6 +1,6 @@
package mindustry.entities.comp;
import arc.util.ArcAnnotate.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.gen.*;

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