Merge branch 'master' into port-field
This commit is contained in:
@@ -4,11 +4,11 @@ import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.assets.loaders.*;
|
||||
import arc.audio.*;
|
||||
import arc.files.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -18,7 +18,7 @@ import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.mod.*;
|
||||
import mindustry.net.Net;
|
||||
import mindustry.net.*;
|
||||
import mindustry.ui.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
@@ -27,13 +27,20 @@ import static mindustry.Vars.*;
|
||||
public abstract class ClientLauncher extends ApplicationCore implements Platform{
|
||||
private static final int loadingFPS = 20;
|
||||
|
||||
private long lastTime;
|
||||
private long nextFrame;
|
||||
private long beginTime;
|
||||
private long lastTargetFps = -1;
|
||||
private boolean finished = false;
|
||||
private LoadRenderer loader;
|
||||
|
||||
@Override
|
||||
public void setup(){
|
||||
String dataDir = System.getProperty("mindustry.data.dir", OS.env("MINDUSTRY_DATA_DIR"));
|
||||
if(dataDir != null){
|
||||
Core.settings.setDataDirectory(files.absolute(dataDir));
|
||||
}
|
||||
|
||||
checkLaunch();
|
||||
loadLogger();
|
||||
|
||||
loader = new LoadRenderer();
|
||||
@@ -41,30 +48,101 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
loadFileLogger();
|
||||
platform = this;
|
||||
maxTextureSize = Gl.getInt(Gl.maxTextureSize);
|
||||
beginTime = Time.millis();
|
||||
|
||||
//debug GL information
|
||||
Log.info("[GL] Version: @", graphics.getGLVersion());
|
||||
Log.info("[GL] Max texture size: @", Gl.getInt(Gl.maxTextureSize));
|
||||
Log.info("[GL] Max texture size: @", maxTextureSize);
|
||||
Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2");
|
||||
Log.info("[JAVA] Version: @", System.getProperty("java.version"));
|
||||
if(NvGpuInfo.hasMemoryInfo()){
|
||||
Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024);
|
||||
}
|
||||
if(maxTextureSize < 4096) Log.warn("[GL] Your maximum texture size is below the recommended minimum of 4096. This will cause severe performance issues.");
|
||||
Log.info("[JAVA] Version: @", OS.javaVersion);
|
||||
if(Core.app.isAndroid()){
|
||||
Log.info("[ANDROID] API level: @", Core.app.getVersion());
|
||||
}
|
||||
long ram = Runtime.getRuntime().maxMemory();
|
||||
boolean gb = ram >= 1024 * 1024 * 1024;
|
||||
if(!OS.isIos){
|
||||
Log.info("[RAM] Available: @ @", Strings.fixed(gb ? ram / 1024f / 1024 / 1024f : ram / 1024f / 1024f, 1), gb ? "GB" : "MB");
|
||||
}
|
||||
|
||||
Time.setDeltaProvider(() -> {
|
||||
float result = Core.graphics.getDeltaTime() * 60f;
|
||||
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
|
||||
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, maxDeltaClient);
|
||||
});
|
||||
|
||||
batch = new SortedSpriteBatch();
|
||||
UI.loadColors();
|
||||
batch = new SpriteBatch();
|
||||
assets = new AssetManager();
|
||||
assets.setLoader(Texture.class, "." + mapExtension, new MapPreviewLoader());
|
||||
|
||||
tree = new FileTree();
|
||||
assets.setLoader(Sound.class, new SoundLoader(tree));
|
||||
assets.setLoader(Music.class, new MusicLoader(tree));
|
||||
assets.setLoader(Sound.class, new SoundLoader(tree){
|
||||
@Override
|
||||
public void loadAsync(AssetManager manager, String fileName, Fi file, SoundParameter parameter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sound loadSync(AssetManager manager, String fileName, Fi file, SoundParameter parameter){
|
||||
if(parameter != null && parameter.sound != null){
|
||||
mainExecutor.submit(() -> parameter.sound.load(file));
|
||||
|
||||
return parameter.sound;
|
||||
}else{
|
||||
Sound sound = new Sound();
|
||||
|
||||
mainExecutor.submit(() -> {
|
||||
try{
|
||||
sound.load(file);
|
||||
}catch(Throwable t){
|
||||
Log.err("Error loading sound: " + file, t);
|
||||
}
|
||||
});
|
||||
|
||||
return sound;
|
||||
}
|
||||
}
|
||||
});
|
||||
assets.setLoader(Music.class, new MusicLoader(tree){
|
||||
@Override
|
||||
public void loadAsync(AssetManager manager, String fileName, Fi file, MusicParameter parameter){}
|
||||
|
||||
@Override
|
||||
public Music loadSync(AssetManager manager, String fileName, Fi file, MusicParameter parameter){
|
||||
if(parameter != null && parameter.music != null){
|
||||
mainExecutor.submit(() -> {
|
||||
try{
|
||||
parameter.music.load(file);
|
||||
}catch(Throwable t){
|
||||
Log.err("Error loading music: " + file, t);
|
||||
}
|
||||
});
|
||||
|
||||
return parameter.music;
|
||||
}else{
|
||||
Music music = new Music();
|
||||
|
||||
mainExecutor.submit(() -> {
|
||||
try{
|
||||
music.load(file);
|
||||
}catch(Throwable t){
|
||||
Log.err("Error loading music: " + file, t);
|
||||
}
|
||||
});
|
||||
|
||||
return music;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assets.load("sprites/error.png", Texture.class);
|
||||
atlas = TextureAtlas.blankAtlas();
|
||||
Vars.net = new Net(platform.getNet());
|
||||
MapPreviewLoader.setupLoaders();
|
||||
mods = new Mods();
|
||||
schematics = new Schematics();
|
||||
|
||||
@@ -75,11 +153,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 -> {
|
||||
atlas = (TextureAtlas)t;
|
||||
Fonts.mergeFontAtlas(atlas);
|
||||
};
|
||||
|
||||
assets.load(new AssetDescriptor<>(maxTextureSize >= 4096 ? "sprites/sprites.aatls" : "sprites/fallback/sprites.aatls", TextureAtlas.class)).loaded = t -> atlas = t;
|
||||
assets.loadRun("maps", Map.class, () -> maps.loadPreviews());
|
||||
|
||||
Musics.load();
|
||||
@@ -93,6 +167,9 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
content.createModContent();
|
||||
});
|
||||
|
||||
assets.load(mods);
|
||||
assets.loadRun("mergeUI", PixmapPacker.class, () -> {}, () -> Fonts.mergeFontAtlas(atlas));
|
||||
|
||||
add(logic = new Logic());
|
||||
add(control = new Control());
|
||||
add(renderer = new Renderer());
|
||||
@@ -100,7 +177,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
add(netServer = new NetServer());
|
||||
add(netClient = new NetClient());
|
||||
|
||||
assets.load(mods);
|
||||
assets.load(schematics);
|
||||
|
||||
assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> content.load());
|
||||
@@ -130,6 +206,18 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
int targetfps = Core.settings.getInt("fpscap", 120);
|
||||
boolean changed = lastTargetFps != targetfps && lastTargetFps != -1;
|
||||
boolean limitFps = targetfps > 0 && targetfps <= 240;
|
||||
|
||||
lastTargetFps = targetfps;
|
||||
|
||||
if(limitFps && !changed){
|
||||
nextFrame += (1000 * 1000000) / targetfps;
|
||||
}else{
|
||||
nextFrame = Time.nanos();
|
||||
}
|
||||
|
||||
if(!finished){
|
||||
if(loader != null){
|
||||
loader.draw();
|
||||
@@ -137,15 +225,21 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
if(assets.update(1000 / loadingFPS)){
|
||||
loader.dispose();
|
||||
loader = null;
|
||||
Log.info("Total time to load: @", Time.timeSinceMillis(beginTime));
|
||||
Log.info("Total time to load: @ms", Time.timeSinceMillis(beginTime));
|
||||
for(ApplicationListener listener : modules){
|
||||
listener.init();
|
||||
}
|
||||
mods.eachClass(Mod::init);
|
||||
finished = true;
|
||||
Events.fire(new ClientLoadEvent());
|
||||
clientLoaded = true;
|
||||
super.resize(graphics.getWidth(), graphics.getHeight());
|
||||
app.post(() -> app.post(() -> app.post(() -> app.post(() -> super.resize(graphics.getWidth(), graphics.getHeight())))));
|
||||
app.post(() -> app.post(() -> app.post(() -> app.post(() -> {
|
||||
super.resize(graphics.getWidth(), graphics.getHeight());
|
||||
|
||||
//mark initialization as complete
|
||||
finishLaunch();
|
||||
}))));
|
||||
}
|
||||
}else{
|
||||
asyncCore.begin();
|
||||
@@ -155,21 +249,24 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
asyncCore.end();
|
||||
}
|
||||
|
||||
int targetfps = Core.settings.getInt("fpscap", 120);
|
||||
|
||||
if(targetfps > 0 && targetfps <= 240){
|
||||
long target = (1000 * 1000000) / targetfps; //target in nanos
|
||||
long elapsed = Time.timeSinceNanos(lastTime);
|
||||
if(elapsed < target){
|
||||
Threads.sleep((target - elapsed) / 1000000, (int)((target - elapsed) % 1000000));
|
||||
if(limitFps){
|
||||
long current = Time.nanos();
|
||||
if(nextFrame > current){
|
||||
long toSleep = nextFrame - current;
|
||||
Threads.sleep(toSleep / 1000000, (int)(toSleep % 1000000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = Time.nanos();
|
||||
@Override
|
||||
public void exit(){
|
||||
//on graceful exit, finish the launch normally.
|
||||
Vars.finishLaunch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
nextFrame = Time.nanos();
|
||||
setup();
|
||||
}
|
||||
|
||||
@@ -182,6 +279,11 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
|
||||
@Override
|
||||
public void pause(){
|
||||
//when the user tabs out on mobile, the exit() event doesn't fire reliably - in that case, just assume they're about to kill the app
|
||||
//this isn't 100% reliable but it should work for most cases
|
||||
if(mobile){
|
||||
Vars.finishLaunch();
|
||||
}
|
||||
if(finished){
|
||||
super.pause();
|
||||
}
|
||||
|
||||
+153
-53
@@ -11,37 +11,52 @@ import arc.util.Log.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.editor.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.input.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.maps.Map;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.mod.*;
|
||||
import mindustry.net.Net;
|
||||
import mindustry.net.*;
|
||||
import mindustry.service.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
|
||||
public class Vars implements Loadable{
|
||||
/** Whether the game failed to launch last time. */
|
||||
public static boolean failedToLaunch = false;
|
||||
/** Whether to load locales.*/
|
||||
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;
|
||||
/** Name of current Steam player. */
|
||||
public static String steamPlayerName = "";
|
||||
/** If true, the BE server list is always used. */
|
||||
public static boolean forceBeServers = false;
|
||||
/** Default accessible content types used for player-selectable icons. */
|
||||
public static final ContentType[] defaultContentIcons = {ContentType.item, ContentType.liquid, ContentType.block, ContentType.unit};
|
||||
/** Default rule environment. */
|
||||
public static final int defaultEnv = Env.terrestrial | Env.spores | Env.groundOil | Env.groundWater | Env.oxygen;
|
||||
/** Wall darkness radius. */
|
||||
public static final int darkRadius = 4;
|
||||
/** Maximum extra padding around deployment schematics. */
|
||||
public static final int maxLoadoutSchematicPad = 5;
|
||||
/** Maximum schematic size.*/
|
||||
public static final int maxSchematicSize = 32;
|
||||
/** All schematic base64 starts with this string.*/
|
||||
public static final String schematicBaseStart ="bXNjaA";
|
||||
/** IO buffer size. */
|
||||
@@ -50,20 +65,18 @@ public class Vars implements Loadable{
|
||||
public static final Charset charset = Charset.forName("UTF-8");
|
||||
/** main application name, capitalized */
|
||||
public static final String appName = "Mindustry";
|
||||
/** URL for itch.io donations. */
|
||||
public static final String donationURL = "https://anuke.itch.io/mindustry/purchase";
|
||||
/** Github API URL. */
|
||||
public static final String ghApi = "https://api.github.com";
|
||||
/** URL for discord invite. */
|
||||
public static final String discordURL = "https://discord.gg/mindustry";
|
||||
/** 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/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. */
|
||||
public static final String serverJsonBeURL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers_be.json";
|
||||
/** 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";
|
||||
/** URLs to the JSON file containing all the BE servers. Only queried in BE. */
|
||||
public static final String[] serverJsonBeURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_be.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_be.json"};
|
||||
/** URLs to the JSON file containing all the stable servers. */
|
||||
public static final String[] serverJsonURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_v8.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_v8.json"};
|
||||
/** URLs to the JSON files containing the list of mods. */
|
||||
public static final String[] modJsonURLs = {"https://raw.githubusercontent.com/Anuken/MindustryMods/master/mods.json", "https://cdn.jsdelivr.net/gh/anuken/mindustrymods/mods.json"};
|
||||
/** URL of the github issue report template.*/
|
||||
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?labels=bug&template=bug_report.md";
|
||||
/** list of built-in servers.*/
|
||||
@@ -78,12 +91,12 @@ public class Vars implements Loadable{
|
||||
public static final int maxNameLength = 40;
|
||||
/** displayed item size when ingame. */
|
||||
public static final float itemSize = 5f;
|
||||
/** units outside of this bound will die instantly */
|
||||
public static final float finalWorldBounds = 500;
|
||||
/** mining range for manual miners */
|
||||
public static final float miningRange = 70f;
|
||||
/** range for building */
|
||||
/** units outside this bound will die instantly */
|
||||
public static final float finalWorldBounds = 250;
|
||||
/** default range for building */
|
||||
public static final float buildingRange = 220f;
|
||||
/** scaling for unit circle collider radius, based on hitbox size */
|
||||
public static final float unitCollisionRadiusScale = 0.6f;
|
||||
/** range for moving items */
|
||||
public static final float itemTransferRange = 220f;
|
||||
/** range for moving items for logic units */
|
||||
@@ -92,18 +105,20 @@ public class Vars implements Loadable{
|
||||
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;
|
||||
/** how many minutes have to pass before invasions in a *captured* sector start */
|
||||
public static final float invasionGracePeriod = 20;
|
||||
/** min armor fraction damage; e.g. 0.05 = at least 5% damage */
|
||||
public static final float minArmorDamage = 0.1f;
|
||||
/** launch animation duration */
|
||||
public static final float launchDuration = 140f;
|
||||
/** @deprecated see {@link CoreBlock#landDuration} instead! */
|
||||
public static final @Deprecated float coreLandDuration = 160f;
|
||||
/** size of tiles in units */
|
||||
public static final int tilesize = 8;
|
||||
/** size of one tile payload (^2) */
|
||||
public static final float tilePayload = tilesize * tilesize;
|
||||
/** tile used in certain situations, instead of null */
|
||||
public static Tile emptyTile;
|
||||
/** icon sizes for UI */
|
||||
public static final float iconXLarge = 8*6f, iconLarge = 8*5f, iconMed = 8*4f, iconSmall = 8*3f;
|
||||
/** macbook screen notch height */
|
||||
public static float macNotchHeight = 32f;
|
||||
/** for map generator dialog */
|
||||
public static boolean updateEditorOnChange = false;
|
||||
/** all choosable player colors in join/host dialog */
|
||||
@@ -125,14 +140,41 @@ public class Vars implements Loadable{
|
||||
Color.valueOf("4b5ef1"),
|
||||
Color.valueOf("2cabfe"),
|
||||
};
|
||||
/** Icons available to the user for customization in certain dialogs. */
|
||||
public static final String[] accessibleIcons = {
|
||||
"effect", "power", "logic", "units", "liquid", "production", "defense", "turret", "distribution", "crafting",
|
||||
"settings", "cancel", "zoom", "ok", "star", "home", "pencil", "up", "down", "left", "right",
|
||||
"hammer", "warning", "tree", "admin", "map", "modePvp", "terrain",
|
||||
"modeSurvival", "commandRally", "commandAttack",
|
||||
};
|
||||
/** maximum TCP packet size */
|
||||
public static final int maxTcpSize = 1100;
|
||||
/** default server port */
|
||||
public static final int port = 6567;
|
||||
/** multicast discovery port.*/
|
||||
public static final int multicastPort = 20151;
|
||||
/** Maximum char length of mod subtitles in browser/viewer. */
|
||||
public static final int maxModSubtitleLength = 40;
|
||||
/** multicast group for discovery.*/
|
||||
public static final String multicastGroup = "227.2.7.7";
|
||||
/** Maximum delta time. If the actual delta time (*60) between frames is higher than this number, the game will start to slow down. */
|
||||
public static float maxDeltaClient = 6f, maxDeltaServer = 10f;
|
||||
/** whether the graphical game client has loaded */
|
||||
public static boolean clientLoaded = false;
|
||||
/** max GL texture size */
|
||||
public static int maxTextureSize = 2048;
|
||||
/** Maximum schematic size.*/
|
||||
public static int maxSchematicSize = 64;
|
||||
/** Whether to show sector info upon landing. */
|
||||
public static boolean showSectorLandInfo = true;
|
||||
/** Whether to check for memory use before taking screenshots. */
|
||||
public static boolean checkScreenshotMemory = true;
|
||||
/** Whether to prompt the user to confirm exiting. */
|
||||
public static boolean confirmExit = true;
|
||||
/** if true, UI is not drawn */
|
||||
public static boolean disableUI;
|
||||
/** if true, most autosaving is disabled. internal use only! */
|
||||
public static boolean disableSave;
|
||||
/** if true, game is set up in mobile mode, even on desktop. used for debugging */
|
||||
public static boolean testMobile;
|
||||
/** whether the game is running on a mobile device */
|
||||
@@ -145,8 +187,6 @@ public class Vars implements Loadable{
|
||||
public static boolean headless;
|
||||
/** whether steam is enabled for this game */
|
||||
public static boolean steam;
|
||||
/** whether typing into the console is enabled - developers only */
|
||||
public static boolean enableConsole = false;
|
||||
/** whether to clear sector saves when landing */
|
||||
public static boolean clearSectors = false;
|
||||
/** whether any light rendering is enabled */
|
||||
@@ -172,18 +212,27 @@ public class Vars implements Loadable{
|
||||
public static Fi schematicDirectory;
|
||||
/** data subdirectory used for bleeding edge build versions */
|
||||
public static Fi bebuildDirectory;
|
||||
/** file used to store launch ID */
|
||||
public static Fi launchIDFile;
|
||||
/** empty map, indicates no current map */
|
||||
public static Map emptyMap;
|
||||
/** empty tile for payloads */
|
||||
public static Tile emptyTile;
|
||||
/** map file extension */
|
||||
public static final String mapExtension = "msav";
|
||||
/** save file extension */
|
||||
public static final String saveExtension = "msav";
|
||||
/** schematic file extension */
|
||||
public static final String schematicExtension = "msch";
|
||||
/** path to the java executable */
|
||||
public static String javaPath;
|
||||
|
||||
/** list of all locales that can be switched to */
|
||||
public static Locale[] locales;
|
||||
|
||||
//the main executor will only have at most [cores] number of threads active
|
||||
public static ExecutorService mainExecutor = Threads.executor("Main Executor", OS.cores);
|
||||
|
||||
public static FileTree tree = new FileTree();
|
||||
public static Net net;
|
||||
public static ContentLoader content;
|
||||
@@ -196,7 +245,9 @@ public class Vars implements Loadable{
|
||||
public static BeControl becontrol;
|
||||
public static AsyncCore asyncCore;
|
||||
public static BaseRegistry bases;
|
||||
public static GlobalConstants constants;
|
||||
public static GlobalVars logicVars;
|
||||
public static MapEditor editor;
|
||||
public static GameService service = new GameService();
|
||||
|
||||
public static Universe universe;
|
||||
public static World world;
|
||||
@@ -204,6 +255,8 @@ public class Vars implements Loadable{
|
||||
public static WaveSpawner spawner;
|
||||
public static BlockIndexer indexer;
|
||||
public static Pathfinder pathfinder;
|
||||
public static ControlPathfinder controlPath;
|
||||
public static FogControl fogControl;
|
||||
|
||||
public static Control control;
|
||||
public static Logic logic;
|
||||
@@ -236,11 +289,16 @@ 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);
|
||||
Arrays.sort(locales, Structs.comparing(LanguageDialog::getDisplayName, String.CASE_INSENSITIVE_ORDER));
|
||||
locales = Seq.with(locales).add(new Locale("router")).toArray(Locale.class);
|
||||
}
|
||||
|
||||
Version.init();
|
||||
CacheLayer.init();
|
||||
|
||||
if(!headless){
|
||||
Log.info("[Mindustry] Version: @", Version.buildString());
|
||||
}
|
||||
|
||||
dataDirectory = settings.getDataDirectory();
|
||||
screenshotDirectory = dataDirectory.child("screenshots/");
|
||||
@@ -252,7 +310,6 @@ public class Vars implements Loadable{
|
||||
schematicDirectory = dataDirectory.child("schematics/");
|
||||
bebuildDirectory = dataDirectory.child("be_builds/");
|
||||
emptyMap = new Map(new StringMap());
|
||||
emptyTile = null;
|
||||
|
||||
if(tree == null) tree = new FileTree();
|
||||
if(mods == null) mods = new Mods();
|
||||
@@ -264,13 +321,21 @@ public class Vars implements Loadable{
|
||||
universe = new Universe();
|
||||
becontrol = new BeControl();
|
||||
asyncCore = new AsyncCore();
|
||||
if(!headless) editor = new MapEditor();
|
||||
|
||||
maps = new Maps();
|
||||
spawner = new WaveSpawner();
|
||||
indexer = new BlockIndexer();
|
||||
pathfinder = new Pathfinder();
|
||||
controlPath = new ControlPathfinder();
|
||||
fogControl = new FogControl();
|
||||
bases = new BaseRegistry();
|
||||
constants = new GlobalConstants();
|
||||
logicVars = new GlobalVars();
|
||||
javaPath =
|
||||
new Fi(OS.prop("java.home")).child("bin/java").exists() ? new Fi(OS.prop("java.home")).child("bin/java").absolutePath() :
|
||||
Core.files.local("jre/bin/java").exists() ? Core.files.local("jre/bin/java").absolutePath() : // Unix
|
||||
Core.files.local("jre/bin/java.exe").exists() ? Core.files.local("jre/bin/java.exe").absolutePath() : // Windows
|
||||
"java";
|
||||
|
||||
state = new GameState();
|
||||
|
||||
@@ -280,10 +345,35 @@ public class Vars implements Loadable{
|
||||
|
||||
modDirectory.mkdirs();
|
||||
|
||||
Events.on(ContentInitEvent.class, e -> {
|
||||
emptyTile = new Tile(Short.MAX_VALUE - 20, Short.MAX_VALUE - 20);
|
||||
});
|
||||
|
||||
mods.load();
|
||||
maps.load();
|
||||
}
|
||||
|
||||
/** Checks if a launch failure occurred.
|
||||
* If this is the case, failedToLaunch is set to true. */
|
||||
public static void checkLaunch(){
|
||||
settings.setAppName(appName);
|
||||
launchIDFile = settings.getDataDirectory().child("launchid.dat");
|
||||
|
||||
if(launchIDFile.exists()){
|
||||
failedToLaunch = true;
|
||||
}else{
|
||||
failedToLaunch = false;
|
||||
launchIDFile.writeString("go away");
|
||||
}
|
||||
}
|
||||
|
||||
/** Cleans up after a successful launch. */
|
||||
public static void finishLaunch(){
|
||||
if(launchIDFile != null){
|
||||
launchIDFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadLogger(){
|
||||
if(loadedLogger) return;
|
||||
|
||||
@@ -292,26 +382,28 @@ public class Vars implements Loadable{
|
||||
|
||||
Seq<String> logBuffer = new Seq<>();
|
||||
Log.logger = (level, text) -> {
|
||||
String result = text;
|
||||
String rawText = Log.format(stags[level.ordinal()] + "&fr " + text);
|
||||
System.out.println(rawText);
|
||||
synchronized(logBuffer){
|
||||
String result = text;
|
||||
String rawText = Log.format(stags[level.ordinal()] + "&fr " + text);
|
||||
System.out.println(rawText);
|
||||
|
||||
result = tags[level.ordinal()] + " " + result;
|
||||
result = tags[level.ordinal()] + " " + result;
|
||||
|
||||
if(!headless && (ui == null || ui.scriptfrag == null)){
|
||||
logBuffer.add(result);
|
||||
}else if(!headless){
|
||||
if(!OS.isWindows){
|
||||
for(String code : ColorCodes.values){
|
||||
result = result.replace(code, "");
|
||||
if(!headless && (ui == null || ui.consolefrag == null)){
|
||||
logBuffer.add(result);
|
||||
}else if(!headless){
|
||||
if(!OS.isWindows){
|
||||
for(String code : ColorCodes.values){
|
||||
result = result.replace(code, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.scriptfrag.addMessage(Log.removeColors(result));
|
||||
ui.consolefrag.addMessage(Log.removeColors(result));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Events.on(ClientLoadEvent.class, e -> logBuffer.each(ui.scriptfrag::addMessage));
|
||||
Events.on(ClientLoadEvent.class, e -> logBuffer.each(ui.consolefrag::addMessage));
|
||||
|
||||
loadedLogger = true;
|
||||
}
|
||||
@@ -324,12 +416,11 @@ public class Vars implements Loadable{
|
||||
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.removeColors(text) + "\n");
|
||||
writer.write("[" + Character.toUpperCase(level.name().charAt(0)) + "] " + Log.removeColors(text) + "\n");
|
||||
writer.flush();
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
@@ -345,7 +436,7 @@ public class Vars implements Loadable{
|
||||
}
|
||||
|
||||
public static void loadSettings(){
|
||||
settings.setJson(JsonIO.json());
|
||||
settings.setJson(JsonIO.json);
|
||||
settings.setAppName(appName);
|
||||
|
||||
if(steam || (Version.modifier != null && Version.modifier.contains("steam"))){
|
||||
@@ -357,7 +448,12 @@ public class Vars implements Loadable{
|
||||
settings.setAutosave(false);
|
||||
settings.load();
|
||||
|
||||
Scl.setProduct(settings.getInt("uiscale", 100) / 100f);
|
||||
//https://github.com/Anuken/Mindustry/issues/8483
|
||||
if(settings.getInt("uiscale") == 5){
|
||||
settings.put("uiscale", 100);
|
||||
}
|
||||
|
||||
Scl.setProduct(Math.max(settings.getInt("uiscale", 100), 25) / 100f);
|
||||
|
||||
if(!loadLocales) return;
|
||||
|
||||
@@ -371,7 +467,7 @@ public class Vars implements Loadable{
|
||||
Log.info("NOTE: external translation bundle has been loaded.");
|
||||
|
||||
if(!headless){
|
||||
Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle."));
|
||||
Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle.\n[accent]" + handle.absolutePath()));
|
||||
}
|
||||
}catch(Throwable e){
|
||||
//no external bundle found
|
||||
@@ -397,8 +493,12 @@ public class Vars implements Loadable{
|
||||
Core.bundle = I18NBundle.createBundle(handle, locale);
|
||||
|
||||
//router
|
||||
if(locale.getDisplayName().equals("router")){
|
||||
bundle.debug("router");
|
||||
if(locale.toString().equals("router")){
|
||||
I18NBundle defBundle = I18NBundle.createBundle(Core.files.internal("bundles/bundle"));
|
||||
String router = Character.toString(Iconc.blockRouter);
|
||||
for(String s : bundle.getKeys()){
|
||||
bundle.getProperties().put(s, Strings.stripColors(defBundle.get(s)).replaceAll("\\S", router));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Astar{
|
||||
@@ -13,18 +15,18 @@ public class Astar{
|
||||
|
||||
private static final Seq<Tile> out = new Seq<>();
|
||||
private static final PQueue<Tile> queue = new PQueue<>(200 * 200 / 4, (a, b) -> 0);
|
||||
private static final IntFloatMap costs = new IntFloatMap();
|
||||
private static float[] costs;
|
||||
private static byte[][] rotations;
|
||||
|
||||
public static Seq<Tile> pathfind(Tile from, Tile to, TileHueristic th, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(Tile from, Tile to, TileHeuristic th, Boolf<Tile> passable){
|
||||
return pathfind(from.x, from.y, to.x, to.y, th, manhattan, passable);
|
||||
}
|
||||
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHueristic th, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHeuristic th, Boolf<Tile> passable){
|
||||
return pathfind(startX, startY, endX, endY, th, manhattan, passable);
|
||||
}
|
||||
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHueristic th, DistanceHeuristic dh, Boolf<Tile> passable){
|
||||
public static Seq<Tile> pathfind(int startX, int startY, int endX, int endY, TileHeuristic th, DistanceHeuristic dh, Boolf<Tile> passable){
|
||||
Tiles tiles = world.tiles;
|
||||
|
||||
Tile start = tiles.getn(startX, startY);
|
||||
@@ -32,9 +34,14 @@ public class Astar{
|
||||
|
||||
GridBits closed = new GridBits(tiles.width, tiles.height);
|
||||
|
||||
costs.clear();
|
||||
if(costs == null || costs.length != tiles.width * tiles.height){
|
||||
costs = new float[tiles.width * tiles.height];
|
||||
}
|
||||
|
||||
Arrays.fill(costs, 0);
|
||||
|
||||
queue.clear();
|
||||
queue.comparator = Structs.comparingFloat(a -> costs.get(a.pos(), 0f) + dh.cost(a.x, a.y, end.x, end.y));
|
||||
queue.comparator = Structs.comparingFloat(a -> costs[a.array()] + dh.cost(a.x, a.y, end.x, end.y));
|
||||
queue.add(start);
|
||||
if(rotations == null || rotations.length != world.width() || rotations[0].length != world.height()){
|
||||
rotations = new byte[world.width()][world.height()];
|
||||
@@ -43,7 +50,7 @@ public class Astar{
|
||||
boolean found = false;
|
||||
while(!queue.empty()){
|
||||
Tile next = queue.poll();
|
||||
float baseCost = costs.get(next.pos(), 0f);
|
||||
float baseCost = costs[next.array()];
|
||||
if(next == end){
|
||||
found = true;
|
||||
break;
|
||||
@@ -58,7 +65,7 @@ public class Astar{
|
||||
if(!closed.get(child.x, child.y)){
|
||||
closed.set(child.x, child.y);
|
||||
rotations[child.x][child.y] = child.relativeTo(next.x, next.y);
|
||||
costs.put(child.pos(), newCost);
|
||||
costs[child.array()] = newCost;
|
||||
queue.add(child);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +94,7 @@ public class Astar{
|
||||
float cost(int x1, int y1, int x2, int y2);
|
||||
}
|
||||
|
||||
public interface TileHueristic{
|
||||
public interface TileHeuristic{
|
||||
float cost(Tile tile);
|
||||
|
||||
default float cost(Tile from, Tile tile){
|
||||
|
||||
@@ -14,31 +14,28 @@ import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.defense.*;
|
||||
import mindustry.world.blocks.distribution.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.blocks.production.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class BaseAI{
|
||||
public class BaseBuilderAI{
|
||||
private static final Vec2 axis = new Vec2(), rotator = new Vec2();
|
||||
private static final float correctPercent = 0.5f;
|
||||
private static final int attempts = 4;
|
||||
private static final int attempts = 6, coreUnitMultiplier = 2;
|
||||
private static final float emptyChance = 0.01f;
|
||||
private static final int timerStep = 0, timerSpawn = 1, timerRefreshPath = 2;
|
||||
private static final float placeIntervalMin = 12f, placeIntervalMax = 2f;
|
||||
private static final int pathStep = 50;
|
||||
private static final Seq<Tile> tmpTiles = new Seq<>();
|
||||
|
||||
private static int correct = 0, incorrect = 0;
|
||||
private static boolean anyDrills;
|
||||
|
||||
private int lastX, lastY, lastW, lastH;
|
||||
private boolean triedWalls, foundPath;
|
||||
private boolean foundPath;
|
||||
|
||||
TeamData data;
|
||||
Interval timer = new Interval(4);
|
||||
final TeamData data;
|
||||
final Interval timer = new Interval(4);
|
||||
|
||||
IntSet path = new IntSet();
|
||||
IntSet calcPath = new IntSet();
|
||||
@@ -47,17 +44,26 @@ public class BaseAI{
|
||||
int calcCount = 0;
|
||||
int totalCalcs = 0;
|
||||
|
||||
public BaseAI(TeamData data){
|
||||
public BaseBuilderAI(TeamData data){
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void update(){
|
||||
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 2.5f) && data.hasCore()){
|
||||
|
||||
//fill cores.
|
||||
if(data.team.cores().size > 0){
|
||||
var core = data.team.cores().first();
|
||||
for(Item item : content.items()){
|
||||
core.items.set(item, core.getMaximumAccepted(item));
|
||||
}
|
||||
}
|
||||
|
||||
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 6f) && data.hasCore()){
|
||||
CoreBlock block = (CoreBlock)data.core().block;
|
||||
int coreUnits = Groups.unit.count(u -> u.team == data.team && u.type == block.unitType);
|
||||
int coreUnits = data.countType(block.unitType);
|
||||
|
||||
//create AI core unit(s)
|
||||
if(!state.isEditor() && coreUnits < data.cores.size){
|
||||
if(!state.isEditor() && coreUnits < data.cores.size * coreUnitMultiplier){
|
||||
Unit unit = block.unitType.create(data.team);
|
||||
unit.set(data.cores.random());
|
||||
unit.add();
|
||||
@@ -88,61 +94,59 @@ public class BaseAI{
|
||||
calculating = false;
|
||||
}
|
||||
}else{
|
||||
var field = pathfinder.getField(state.rules.waveTeam, Pathfinder.costGround, Pathfinder.fieldCore);
|
||||
var field = pathfinder.getField(data.team, 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;
|
||||
if(field.hasCompleteWeights()){
|
||||
int[] weights = field.completeWeights;
|
||||
for(int i = 0; i < pathStep; i++){
|
||||
int minCost = Integer.MAX_VALUE;
|
||||
int cx = calcTile.x, cy = calcTile.y;
|
||||
boolean foundAny = false;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int nx = cx + p.x, ny = cy + p.y, packed = world.packArray(nx, ny);
|
||||
|
||||
Tile other = world.tile(nx, ny);
|
||||
if(other != null && weights[nx][ny] < minCost && weights[nx][ny] != -1){
|
||||
minCost = weights[nx][ny];
|
||||
calcTile = other;
|
||||
foundAny = true;
|
||||
Tile other = world.tile(nx, ny);
|
||||
if(other != null && weights[packed] < minCost && weights[packed] != -1){
|
||||
minCost = weights[packed];
|
||||
calcTile = other;
|
||||
foundAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
//didn't find anything, break out of loop, this will trigger a clear later
|
||||
if(!foundAny){
|
||||
calcCount = Integer.MAX_VALUE;
|
||||
break;
|
||||
}
|
||||
|
||||
calcPath.add(calcTile.pos());
|
||||
for(Point2 p : Geometry.d8){
|
||||
calcPath.add(Point2.pack(p.x + calcTile.x, p.y + calcTile.y));
|
||||
}
|
||||
|
||||
//found the end.
|
||||
if(calcTile.build instanceof CoreBuild b && b.team != data.team){
|
||||
//clean up calculations and flush results
|
||||
calculating = false;
|
||||
calcCount = 0;
|
||||
path.clear();
|
||||
path.addAll(calcPath);
|
||||
calcPath.clear();
|
||||
calcTile = null;
|
||||
totalCalcs ++;
|
||||
foundPath = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
calcCount ++;
|
||||
}
|
||||
|
||||
//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(foundPath && data.blocks.isEmpty() && timer.get(timerStep, Mathf.lerp(20f, 4f, data.team.rules().aiTier))){
|
||||
if(!triedWalls){
|
||||
tryWalls();
|
||||
triedWalls = true;
|
||||
}
|
||||
if((foundPath || !calculating) && data.plans.isEmpty() && timer.get(timerStep, Mathf.lerp(placeIntervalMin, placeIntervalMax, data.team.rules().buildAiTier))){
|
||||
|
||||
for(int i = 0; i < attempts; i++){
|
||||
int range = 150;
|
||||
@@ -209,6 +213,16 @@ public class BaseAI{
|
||||
}
|
||||
Tile wtile = world.tile(realX, realY);
|
||||
|
||||
if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadBlock){
|
||||
//near a building
|
||||
for(Point2 point : Edges.getEdges(tile.block.size)){
|
||||
var t = world.build(tile.x + point.x, tile.y + point.y);
|
||||
if(t != null){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//may intersect AI path
|
||||
tmpTiles.clear();
|
||||
if(tile.block.solid && wtile != null && wtile.getLinkedTilesAs(tile.block, tmpTiles).contains(t -> path.contains(t.pos()))){
|
||||
@@ -218,7 +232,7 @@ public class BaseAI{
|
||||
|
||||
//make sure at least X% of resource requirements are met
|
||||
correct = incorrect = 0;
|
||||
anyDrills = false;
|
||||
boolean anyDrills = false;
|
||||
|
||||
if(part.required instanceof Item){
|
||||
for(Stile tile : result.tiles){
|
||||
@@ -244,55 +258,9 @@ public class BaseAI{
|
||||
|
||||
//queue it
|
||||
for(Stile tile : result.tiles){
|
||||
data.blocks.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block.id, tile.config));
|
||||
data.plans.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block, tile.config));
|
||||
}
|
||||
|
||||
lastX = cx - 1;
|
||||
lastY = cy - 1;
|
||||
lastW = result.width + 2;
|
||||
lastH = result.height + 2;
|
||||
|
||||
triedWalls = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void tryWalls(){
|
||||
Block wall = Blocks.copperWall;
|
||||
Building spawnt = state.rules.defaultTeam.core() != null ? state.rules.defaultTeam.core() : data.team.core();
|
||||
Tile spawn = spawnt == null ? null : spawnt.tile;
|
||||
|
||||
if(spawn == null) return;
|
||||
|
||||
for(int wx = lastX; wx <= lastX + lastW; wx++){
|
||||
for(int wy = lastY; wy <= lastY + lastH; wy++){
|
||||
Tile tile = world.tile(wx, wy);
|
||||
|
||||
if(tile == null || !tile.block().alwaysReplace) continue;
|
||||
|
||||
boolean any = false;
|
||||
|
||||
for(Point2 p : Geometry.d8){
|
||||
if(Angles.angleDist(Angles.angle(p.x, p.y), spawn.angleTo(tile)) > 70){
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,8 @@ public class BaseRegistry{
|
||||
|
||||
//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);
|
||||
if(block instanceof OreBlock ore && ore.itemDrop != null && !ore.wallOre && !ores.containsKey(ore.itemDrop)){
|
||||
ores.put(ore.itemDrop, ore);
|
||||
}else if(block.isFloor() && block.asFloor().itemDrop != null && !oreFloors.containsKey(block.asFloor().itemDrop)){
|
||||
oreFloors.put(block.asFloor().itemDrop, block.asFloor());
|
||||
}
|
||||
|
||||
@@ -4,262 +4,374 @@ import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.EnumSet;
|
||||
import arc.struct.*;
|
||||
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.logic.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Class used for indexing special target blocks for AI. */
|
||||
public class BlockIndexer{
|
||||
/** Size of one quadrant. */
|
||||
private static final int quadrantSize = 16;
|
||||
private static final int quadrantSize = 20;
|
||||
private static final Rect rect = new Rect();
|
||||
private static boolean returnBool = false;
|
||||
|
||||
/** Set of all ores that are being scanned. */
|
||||
private final ObjectSet<Item> scanOres = new ObjectSet<>();
|
||||
private final IntSet intSet = new IntSet();
|
||||
private final ObjectSet<Item> itemSet = new ObjectSet<>();
|
||||
/** Stores all ore quadtrants on the map. */
|
||||
private ObjectMap<Item, TileArray> ores = new ObjectMap<>();
|
||||
/** Maps each team ID to a quarant. A quadrant is a grid of bits, where each bit is set if and only if there is a block of that team in that quadrant. */
|
||||
private GridBits[] structQuadrants;
|
||||
private int quadWidth, quadHeight;
|
||||
|
||||
/** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */
|
||||
private IntSeq[][][] ores;
|
||||
/** Stores all damaged tile entities by team. */
|
||||
private ObjectSet<Building>[] damagedTiles = new ObjectSet[Team.all.length];
|
||||
private Seq<Building>[] damagedTiles = new Seq[Team.all.length];
|
||||
/** All ores available on this map. */
|
||||
private ObjectSet<Item> allOres = new ObjectSet<>();
|
||||
private ObjectIntMap<Item> allOres = new ObjectIntMap<>();
|
||||
/** Stores teams that are present here as tiles. */
|
||||
private Seq<Team> activeTeams = new Seq<>(Team.class);
|
||||
/** Maps teams to a map of flagged tiles by flag. */
|
||||
private TileArray[][] flagMap = new TileArray[Team.all.length][BlockFlag.all.length];
|
||||
/** Max units by team. */
|
||||
private int[] unitCaps = new int[Team.all.length];
|
||||
/** Maps tile positions to their last known tile index data. */
|
||||
private IntMap<TileIndex> typeMap = new IntMap<>();
|
||||
/** Empty set used for returning. */
|
||||
private TileArray emptySet = new TileArray();
|
||||
private Seq<Building>[][] flagMap = new Seq[Team.all.length][BlockFlag.all.length];
|
||||
/** Counts whether a certain floor is present in the world upon load. */
|
||||
private boolean[] blocksPresent;
|
||||
/** Array used for returning and reusing. */
|
||||
private Seq<Tile> returnArray = new Seq<>();
|
||||
/** Array used for returning and reusing. */
|
||||
private Seq<Building> breturnArray = new Seq<>();
|
||||
private Seq<Building> breturnArray = new Seq<>(Building.class);
|
||||
|
||||
public BlockIndexer(){
|
||||
clearFlags();
|
||||
|
||||
Events.on(TilePreChangeEvent.class, event -> {
|
||||
removeIndex(event.tile);
|
||||
});
|
||||
|
||||
Events.on(TileChangeEvent.class, event -> {
|
||||
updateIndices(event.tile);
|
||||
addIndex(event.tile);
|
||||
});
|
||||
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
scanOres.clear();
|
||||
scanOres.addAll(Item.getAllOres());
|
||||
damagedTiles = new ObjectSet[Team.all.length];
|
||||
flagMap = new TileArray[Team.all.length][BlockFlag.all.length];
|
||||
unitCaps = new int[Team.all.length];
|
||||
damagedTiles = new Seq[Team.all.length];
|
||||
flagMap = new Seq[Team.all.length][BlockFlag.all.length];
|
||||
activeTeams = new Seq<>(Team.class);
|
||||
|
||||
for(int i = 0; i < flagMap.length; i++){
|
||||
for(int j = 0; j < BlockFlag.all.length; j++){
|
||||
flagMap[i][j] = new TileArray();
|
||||
clearFlags();
|
||||
|
||||
allOres.clear();
|
||||
ores = new IntSeq[content.items().size][][];
|
||||
quadWidth = Mathf.ceil(world.width() / (float)quadrantSize);
|
||||
quadHeight = Mathf.ceil(world.height() / (float)quadrantSize);
|
||||
blocksPresent = new boolean[content.blocks().size];
|
||||
|
||||
//so WorldLoadEvent gets called twice sometimes... ugh
|
||||
for(Team team : Team.all){
|
||||
var data = state.teams.get(team);
|
||||
if(data != null){
|
||||
if(data.buildingTree != null) data.buildingTree.clear();
|
||||
if(data.turretTree != null) data.turretTree.clear();
|
||||
}
|
||||
}
|
||||
|
||||
typeMap.clear();
|
||||
allOres.clear();
|
||||
ores = null;
|
||||
|
||||
//create bitset for each team type that contains each quadrant
|
||||
structQuadrants = new GridBits[Team.all.length];
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
process(tile);
|
||||
|
||||
if(tile.build != null && tile.build.damaged()){
|
||||
notifyTileDamaged(tile.build);
|
||||
}
|
||||
var drop = tile.drop();
|
||||
|
||||
if(tile.drop() != null) allOres.add(tile.drop());
|
||||
}
|
||||
if(drop != null){
|
||||
int qx = (tile.x / quadrantSize);
|
||||
int qy = (tile.y / quadrantSize);
|
||||
|
||||
for(int x = 0; x < quadWidth(); x++){
|
||||
for(int y = 0; y < quadHeight(); y++){
|
||||
updateQuadrant(world.tile(x * quadrantSize, y * quadrantSize));
|
||||
//add position of quadrant to list
|
||||
if(tile.block() == Blocks.air){
|
||||
if(ores[drop.id] == null){
|
||||
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
|
||||
}
|
||||
if(ores[drop.id][qx][qy] == null){
|
||||
ores[drop.id][qx][qy] = new IntSeq(false, 16);
|
||||
}
|
||||
ores[drop.id][qx][qy].add(tile.pos());
|
||||
allOres.increment(drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanOres();
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
public void removeIndex(Tile tile){
|
||||
var team = tile.team();
|
||||
if(tile.build != null && tile.isCenter()){
|
||||
var build = tile.build;
|
||||
var flags = tile.block().flags;
|
||||
var data = team.data();
|
||||
|
||||
if(flags.size > 0){
|
||||
for(BlockFlag flag : flags.array){
|
||||
getFlagged(team)[flag.ordinal()].remove(build);
|
||||
}
|
||||
}
|
||||
|
||||
if(index.flags.contains(BlockFlag.unitModifier)){
|
||||
updateCap(index.team);
|
||||
//no longer part of the building list
|
||||
data.buildings.remove(build);
|
||||
data.buildingTypes.get(build.block, () -> new Seq<>(false)).remove(build);
|
||||
|
||||
//update the unit cap when building is removed
|
||||
data.unitCap -= tile.block().unitCapModifier;
|
||||
|
||||
//unregister building from building quadtree
|
||||
if(data.buildingTree != null){
|
||||
data.buildingTree.remove(build);
|
||||
}
|
||||
|
||||
//remove indexed turret
|
||||
if(data.turretTree != null && build.block.attacks){
|
||||
data.turretTree.remove(build);
|
||||
}
|
||||
|
||||
//unregister damaged buildings
|
||||
if(build.wasDamaged && damagedTiles[team.id] != null){
|
||||
damagedTiles[team.id].remove(build);
|
||||
}
|
||||
|
||||
//is no longer registered
|
||||
build.wasDamaged = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void addIndex(Tile tile){
|
||||
process(tile);
|
||||
updateQuadrant(tile);
|
||||
}
|
||||
|
||||
private TileArray[] getFlagged(Team team){
|
||||
return flagMap[team.id];
|
||||
}
|
||||
var drop = tile.drop();
|
||||
if(drop != null && ores != null){
|
||||
int qx = tile.x / quadrantSize;
|
||||
int qy = tile.y / quadrantSize;
|
||||
|
||||
private GridBits structQuadrant(Team t){
|
||||
if(structQuadrants[t.id] == null){
|
||||
structQuadrants[t.id] = new GridBits(Mathf.ceil(world.width() / (float)quadrantSize), Mathf.ceil(world.height() / (float)quadrantSize));
|
||||
}
|
||||
return structQuadrants[t.id];
|
||||
}
|
||||
if(ores[drop.id] == null){
|
||||
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
|
||||
}
|
||||
if(ores[drop.id][qx][qy] == null){
|
||||
ores[drop.id][qx][qy] = new IntSeq(false, 16);
|
||||
}
|
||||
|
||||
/** Updates all the structure quadrants for a newly activated team. */
|
||||
public void updateTeamIndex(Team team){
|
||||
if(structQuadrants == null) return;
|
||||
int pos = tile.pos();
|
||||
var seq = ores[drop.id][qx][qy];
|
||||
|
||||
//go through every tile... ouch
|
||||
for(Tile tile : world.tiles){
|
||||
if(tile.team() == team){
|
||||
int quadrantX = tile.x / quadrantSize;
|
||||
int quadrantY = tile.y / quadrantSize;
|
||||
structQuadrant(team).set(quadrantX, quadrantY);
|
||||
if(tile.block() == Blocks.air){
|
||||
//add the index if it is a valid new spot to mine at
|
||||
if(!seq.contains(pos)){
|
||||
seq.add(pos);
|
||||
allOres.increment(drop);
|
||||
}
|
||||
}else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it
|
||||
seq.removeValue(pos);
|
||||
allOres.increment(drop, -1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @return whether a certain block is anywhere on this map. */
|
||||
public boolean isBlockPresent(Block block){
|
||||
return blocksPresent != null && blocksPresent[block.id];
|
||||
}
|
||||
|
||||
private void clearFlags(){
|
||||
for(int i = 0; i < flagMap.length; i++){
|
||||
for(int j = 0; j < BlockFlag.all.length; j++){
|
||||
flagMap[i][j] = new Seq();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Seq<Building>[] getFlagged(Team team){
|
||||
return flagMap[team.id];
|
||||
}
|
||||
|
||||
/** @return whether this item is present on this map. */
|
||||
public boolean hasOre(Item item){
|
||||
return allOres.contains(item);
|
||||
return allOres.get(item) > 0;
|
||||
}
|
||||
|
||||
/** Returns all damaged tiles by team. */
|
||||
public ObjectSet<Building> getDamaged(Team team){
|
||||
breturnArray.clear();
|
||||
|
||||
public Seq<Building> getDamaged(Team team){
|
||||
if(damagedTiles[team.id] == null){
|
||||
damagedTiles[team.id] = new ObjectSet<>();
|
||||
return damagedTiles[team.id] = new Seq<>(false);
|
||||
}
|
||||
|
||||
ObjectSet<Building> set = damagedTiles[team.id];
|
||||
for(Building build : set){
|
||||
if((!build.isValid() || build.team != team || !build.damaged()) || build.block instanceof ConstructBlock){
|
||||
breturnArray.add(build);
|
||||
}
|
||||
}
|
||||
var tiles = damagedTiles[team.id];
|
||||
tiles.removeAll(b -> !b.damaged());
|
||||
|
||||
for(Building tile : breturnArray){
|
||||
set.remove(tile);
|
||||
}
|
||||
|
||||
return set;
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/** Get all allied blocks with a flag. */
|
||||
public TileArray getAllied(Team team, BlockFlag type){
|
||||
public Seq<Building> getFlagged(Team team, BlockFlag type){
|
||||
return flagMap[team.id][type.ordinal()];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Tile findClosestFlag(float x, float y, Team team, BlockFlag flag){
|
||||
return Geometry.findClosest(x, y, getAllied(team, flag));
|
||||
public Building findClosestFlag(float x, float y, Team team, BlockFlag flag){
|
||||
return Geometry.findClosest(x, y, getFlagged(team, flag));
|
||||
}
|
||||
|
||||
public boolean eachBlock(Teamc team, float range, Boolf<Building> pred, Cons<Building> cons){
|
||||
return eachBlock(team.team(), team.getX(), team.getY(), range, pred, cons);
|
||||
}
|
||||
|
||||
public boolean eachBlock(Team team, float wx, float wy, float range, Boolf<Building> pred, Cons<Building> cons){
|
||||
intSet.clear();
|
||||
public boolean eachBlock(@Nullable Team team, float wx, float wy, float range, Boolf<Building> pred, Cons<Building> cons){
|
||||
|
||||
int tx = World.toTile(wx);
|
||||
int ty = World.toTile(wy);
|
||||
if(team == null){
|
||||
returnBool = false;
|
||||
|
||||
int tileRange = (int)(range / tilesize + 1);
|
||||
boolean any = false;
|
||||
|
||||
for(int x = -tileRange + tx; x <= tileRange + tx; x++){
|
||||
for(int y = -tileRange + ty; y <= tileRange + ty; y++){
|
||||
if(!Mathf.within(x * tilesize, y * tilesize, wx, wy, range)) continue;
|
||||
|
||||
Building other = world.build(x, y);
|
||||
|
||||
if(other == null) continue;
|
||||
|
||||
if((team == null || other.team == team) && pred.get(other) && intSet.add(other.pos())){
|
||||
cons.get(other);
|
||||
any = true;
|
||||
allBuildings(wx, wy, range, b -> {
|
||||
if(pred.get(b)){
|
||||
returnBool = true;
|
||||
cons.get(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
return returnBool;
|
||||
}else{
|
||||
breturnArray.clear();
|
||||
|
||||
var buildings = team.data().buildingTree;
|
||||
if(buildings == null) return false;
|
||||
buildings.intersect(wx - range, wy - range, range*2f, range*2f, b -> {
|
||||
if(b.within(wx, wy, range + b.hitSize() / 2f) && pred.get(b)){
|
||||
breturnArray.add(b);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return any;
|
||||
int size = breturnArray.size;
|
||||
var items = breturnArray.items;
|
||||
for(int i = 0; i < size; i++){
|
||||
cons.get(items[i]);
|
||||
items[i] = null;
|
||||
}
|
||||
breturnArray.size = 0;
|
||||
|
||||
return size > 0;
|
||||
}
|
||||
|
||||
/** Does not work with null teams. */
|
||||
public boolean eachBlock(Team team, Rect rect, Boolf<Building> pred, Cons<Building> cons){
|
||||
if(team == null) return false;
|
||||
|
||||
breturnArray.clear();
|
||||
|
||||
var buildings = team.data().buildingTree;
|
||||
if(buildings == null) return false;
|
||||
buildings.intersect(rect, b -> {
|
||||
if(pred.get(b)){
|
||||
breturnArray.add(b);
|
||||
}
|
||||
});
|
||||
|
||||
int size = breturnArray.size;
|
||||
var items = breturnArray.items;
|
||||
for(int i = 0; i < size; i++){
|
||||
cons.get(items[i]);
|
||||
items[i] = null;
|
||||
}
|
||||
breturnArray.size = 0;
|
||||
|
||||
return size > 0;
|
||||
}
|
||||
|
||||
/** Get all enemy blocks with a flag. */
|
||||
public Seq<Tile> getEnemy(Team team, BlockFlag type){
|
||||
returnArray.clear();
|
||||
public Seq<Building> getEnemy(Team team, BlockFlag type){
|
||||
breturnArray.clear();
|
||||
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(enemy == team || (enemy == Team.derelict && !state.rules.coreCapture)) continue;
|
||||
var set = getFlagged(enemy)[type.ordinal()];
|
||||
if(set != null){
|
||||
for(Tile tile : set){
|
||||
returnArray.add(tile);
|
||||
}
|
||||
breturnArray.addAll(set);
|
||||
}
|
||||
}
|
||||
}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(enemy == team || (enemy == Team.derelict && !state.rules.coreCapture)) continue;
|
||||
var set = getFlagged(enemy)[type.ordinal()];
|
||||
if(set != null){
|
||||
for(Tile tile : set){
|
||||
returnArray.add(tile);
|
||||
}
|
||||
breturnArray.addAll(set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnArray;
|
||||
return breturnArray;
|
||||
}
|
||||
|
||||
public void notifyTileDamaged(Building entity){
|
||||
if(damagedTiles[entity.team.id] == null){
|
||||
damagedTiles[entity.team.id] = new ObjectSet<>();
|
||||
public void notifyHealthChanged(Building build){
|
||||
boolean damaged = build.damaged();
|
||||
|
||||
if(build.wasDamaged != damaged){
|
||||
if(damagedTiles[build.team.id] == null){
|
||||
damagedTiles[build.team.id] = new Seq<>(false);
|
||||
}
|
||||
|
||||
if(damaged){
|
||||
//is now damaged, add to array
|
||||
damagedTiles[build.team.id].add(build);
|
||||
}else{
|
||||
//no longer damaged, remove
|
||||
damagedTiles[build.team.id].remove(build);
|
||||
}
|
||||
|
||||
build.wasDamaged = damaged;
|
||||
}
|
||||
}
|
||||
|
||||
public void allBuildings(float x, float y, float range, Cons<Building> cons){
|
||||
breturnArray.clear();
|
||||
for(int i = 0; i < activeTeams.size; i++){
|
||||
Team team = activeTeams.items[i];
|
||||
var buildings = team.data().buildingTree;
|
||||
if(buildings == null) continue;
|
||||
buildings.intersect(x - range, y - range, range*2f, range*2f, breturnArray);
|
||||
}
|
||||
|
||||
damagedTiles[entity.team.id].add(entity);
|
||||
var items = breturnArray.items;
|
||||
int size = breturnArray.size;
|
||||
for(int i = 0; i < size; i++){
|
||||
var b = items[i];
|
||||
if(b != null && b.within(x, y, range + b.hitSize()/2f)){
|
||||
cons.get(b);
|
||||
}
|
||||
items[i] = null;
|
||||
}
|
||||
breturnArray.size = 0;
|
||||
}
|
||||
|
||||
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
|
||||
Building target = null;
|
||||
float targetDist = 0;
|
||||
|
||||
for(int i = 0; i < activeTeams.size; i++){
|
||||
Team enemy = activeTeams.items[i];
|
||||
if(enemy == team || (enemy == Team.derelict && !state.rules.coreCapture)) continue;
|
||||
|
||||
if(enemy == team || team == Team.derelict) continue;
|
||||
Building candidate = indexer.findTile(enemy, x, y, range, b -> pred.get(b) && b.isDiscovered(team), true);
|
||||
if(candidate == null) continue;
|
||||
|
||||
Building entity = indexer.findTile(enemy, x, y, range, pred, true);
|
||||
if(entity != null){
|
||||
return entity;
|
||||
//if a block has the same priority, the closer one should be targeted
|
||||
float dist = candidate.dst(x, y) - candidate.hitSize() / 2f;
|
||||
if(target == null ||
|
||||
//if its closer and is at least equal priority
|
||||
(dist < targetDist && candidate.block.priority >= target.block.priority) ||
|
||||
// block has higher priority (so range doesnt matter)
|
||||
(candidate.block.priority > target.block.priority)){
|
||||
target = candidate;
|
||||
targetDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return target;
|
||||
}
|
||||
|
||||
public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred){
|
||||
@@ -269,59 +381,52 @@ public class BlockIndexer{
|
||||
public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred, boolean usePriority){
|
||||
Building closest = null;
|
||||
float dst = 0;
|
||||
float range2 = range * range;
|
||||
var buildings = team.data().buildingTree;
|
||||
if(buildings == null) return null;
|
||||
|
||||
for(int rx = Math.max((int)((x - range) / tilesize / quadrantSize), 0); rx <= (int)((x + range) / tilesize / quadrantSize) && rx < quadWidth(); rx++){
|
||||
for(int ry = Math.max((int)((y - range) / tilesize / quadrantSize), 0); ry <= (int)((y + range) / tilesize / quadrantSize) && ry < quadHeight(); ry++){
|
||||
breturnArray.clear();
|
||||
buildings.intersect(rect.setCentered(x, y, range * 2f), breturnArray);
|
||||
|
||||
if(!getQuad(team, rx, ry)) continue;
|
||||
for(int i = 0; i < breturnArray.size; i++){
|
||||
var next = breturnArray.items[i];
|
||||
|
||||
for(int tx = rx * quadrantSize; tx < (rx + 1) * quadrantSize && tx < world.width(); tx++){
|
||||
for(int ty = ry * quadrantSize; ty < (ry + 1) * quadrantSize && ty < world.height(); ty++){
|
||||
Building e = world.build(tx, ty);
|
||||
if(!pred.get(next) || !next.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 ||
|
||||
//this one is closer, and it is at least of equal priority
|
||||
(ndst < dst && (!usePriority || closest.block.priority.ordinal() <= e.block.priority.ordinal())) ||
|
||||
//priority is used, and new block has higher priority regardless of range
|
||||
(usePriority && closest.block.priority.ordinal() < e.block.priority.ordinal()))){
|
||||
dst = ndst;
|
||||
closest = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
float bdst = next.dst(x, y) - next.hitSize() / 2f;
|
||||
if(bdst < range && (closest == null ||
|
||||
//this one is closer, and it is at least of equal priority
|
||||
(bdst < dst && (!usePriority || closest.block.priority <= next.block.priority)) ||
|
||||
//priority is used, and new block has higher priority regardless of range
|
||||
(usePriority && closest.block.priority < next.block.priority))){
|
||||
dst = bdst;
|
||||
closest = next;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of tiles that have ores of the specified type nearby.
|
||||
* While each tile in the set is not guaranteed to have an ore directly on it,
|
||||
* each tile will at least have an ore within {@link #quadrantSize} / 2 blocks of it.
|
||||
* Only specific ore types are scanned. See {@link #scanOres}.
|
||||
*/
|
||||
public TileArray getOrePositions(Item item){
|
||||
return ores.get(item, emptySet);
|
||||
}
|
||||
|
||||
/** Find the closest ore block relative to a position. */
|
||||
public Tile findClosestOre(float xp, float yp, Item item){
|
||||
Tile tile = Geometry.findClosest(xp, yp, getOrePositions(item));
|
||||
|
||||
if(tile == null) return null;
|
||||
|
||||
for(int x = Math.max(0, tile.x - quadrantSize / 2); x < tile.x + quadrantSize / 2 && x < world.width(); x++){
|
||||
for(int y = Math.max(0, tile.y - quadrantSize / 2); y < tile.y + quadrantSize / 2 && y < world.height(); y++){
|
||||
Tile res = world.tile(x, y);
|
||||
if(res.block() == Blocks.air && res.drop() == item){
|
||||
return res;
|
||||
if(ores[item.id] != null){
|
||||
float minDst = 0f;
|
||||
Tile closest = null;
|
||||
for(int qx = 0; qx < quadWidth; qx++){
|
||||
for(int qy = 0; qy < quadHeight; qy++){
|
||||
var arr = ores[item.id][qx][qy];
|
||||
if(arr != null && arr.size > 0){
|
||||
Tile tile = world.tile(arr.first());
|
||||
if(tile.block() == Blocks.air){
|
||||
float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy());
|
||||
if(closest == null || dst < minDst){
|
||||
closest = tile;
|
||||
minDst = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -332,177 +437,73 @@ public class BlockIndexer{
|
||||
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];
|
||||
}
|
||||
|
||||
private void updateCap(Team team){
|
||||
TileArray capped = getFlagged(team)[BlockFlag.unitModifier.ordinal()];
|
||||
unitCaps[team.id] = 0;
|
||||
for(Tile capper : capped){
|
||||
unitCaps[team.id] += capper.block().unitCapModifier;
|
||||
}
|
||||
}
|
||||
|
||||
private void process(Tile tile){
|
||||
if(tile.block().flags.size() > 0 && tile.team() != Team.derelict && tile.isCenter()){
|
||||
TileArray[] map = getFlagged(tile.team());
|
||||
var team = tile.team();
|
||||
//only process entity changes with centered tiles
|
||||
if(tile.isCenter() && tile.build != null){
|
||||
var data = team.data();
|
||||
|
||||
for(BlockFlag flag : tile.block().flags){
|
||||
if(tile.block().flags.size > 0 && tile.isCenter()){
|
||||
var map = getFlagged(team);
|
||||
|
||||
TileArray arr = map[flag.ordinal()];
|
||||
|
||||
arr.add(tile);
|
||||
|
||||
map[flag.ordinal()] = arr;
|
||||
}
|
||||
|
||||
if(tile.block().flags.contains(BlockFlag.unitModifier)){
|
||||
updateCap(tile.team());
|
||||
}
|
||||
|
||||
typeMap.put(tile.pos(), new TileIndex(tile.block().flags, tile.team()));
|
||||
}
|
||||
|
||||
if(!activeTeams.contains(tile.team())){
|
||||
activeTeams.add(tile.team());
|
||||
}
|
||||
|
||||
if(ores == null) return;
|
||||
|
||||
int quadrantX = tile.x / quadrantSize;
|
||||
int quadrantY = tile.y / quadrantSize;
|
||||
itemSet.clear();
|
||||
|
||||
Tile rounded = world.rawTile(Mathf.clamp(quadrantX * quadrantSize + quadrantSize / 2, 0, world.width() - 1), Mathf.clamp(quadrantY * quadrantSize + quadrantSize / 2, 0, world.height() - 1));
|
||||
|
||||
//find all items that this quadrant contains
|
||||
for(int x = Math.max(0, rounded.x - quadrantSize / 2); x < rounded.x + quadrantSize / 2 && x < world.width(); x++){
|
||||
for(int y = Math.max(0, rounded.y - quadrantSize / 2); y < rounded.y + quadrantSize / 2 && y < world.height(); y++){
|
||||
Tile result = world.tile(x, y);
|
||||
if(result == null || result.drop() == null || !scanOres.contains(result.drop()) || result.block() != Blocks.air) continue;
|
||||
|
||||
itemSet.add(result.drop());
|
||||
}
|
||||
}
|
||||
|
||||
//update quadrant at this position
|
||||
for(Item item : scanOres){
|
||||
TileArray set = ores.get(item);
|
||||
|
||||
//update quadrant status depending on whether the item is in it
|
||||
if(!itemSet.contains(item)){
|
||||
set.remove(rounded);
|
||||
}else{
|
||||
set.add(rounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateQuadrant(Tile tile){
|
||||
if(structQuadrants == null) return;
|
||||
|
||||
//this quadrant is now 'dirty', re-scan the whole thing
|
||||
int quadrantX = tile.x / quadrantSize;
|
||||
int quadrantY = tile.y / quadrantSize;
|
||||
|
||||
for(Team team : activeTeams){
|
||||
GridBits bits = structQuadrant(team);
|
||||
|
||||
//fast-set this quadrant to 'occupied' if the tile just placed is already of this team
|
||||
if(tile.team() == team && tile.build != null && tile.block().targetable){
|
||||
bits.set(quadrantX, quadrantY);
|
||||
continue; //no need to process futher
|
||||
}
|
||||
|
||||
bits.set(quadrantX, quadrantY, false);
|
||||
|
||||
outer:
|
||||
for(int x = quadrantX * quadrantSize; x < world.width() && x < (quadrantX + 1) * quadrantSize; x++){
|
||||
for(int y = quadrantY * quadrantSize; y < world.height() && y < (quadrantY + 1) * quadrantSize; y++){
|
||||
Building result = world.build(x, y);
|
||||
//when a targetable block is found, mark this quadrant as occupied and stop searching
|
||||
if(result != null && result.team == team){
|
||||
bits.set(quadrantX, quadrantY);
|
||||
break outer;
|
||||
}
|
||||
for(BlockFlag flag : tile.block().flags.array){
|
||||
map[flag.ordinal()].add(tile.build);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getQuad(Team team, int quadrantX, int quadrantY){
|
||||
return structQuadrant(team).get(quadrantX, quadrantY);
|
||||
}
|
||||
//record in list of buildings
|
||||
data.buildings.add(tile.build);
|
||||
data.buildingTypes.get(tile.block(), () -> new Seq<>(false)).add(tile.build);
|
||||
|
||||
private int quadWidth(){
|
||||
return Mathf.ceil(world.width() / (float)quadrantSize);
|
||||
}
|
||||
//update the unit cap when new tile is registered
|
||||
data.unitCap += tile.block().unitCapModifier;
|
||||
|
||||
private int quadHeight(){
|
||||
return Mathf.ceil(world.height() / (float)quadrantSize);
|
||||
}
|
||||
|
||||
private void scanOres(){
|
||||
ores = new ObjectMap<>();
|
||||
|
||||
//initialize ore map with empty sets
|
||||
for(Item item : scanOres){
|
||||
ores.put(item, new TileArray());
|
||||
}
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
int qx = (tile.x / quadrantSize);
|
||||
int qy = (tile.y / quadrantSize);
|
||||
|
||||
//add position of quadrant to list when an ore is found
|
||||
if(tile.drop() != null && scanOres.contains(tile.drop()) && tile.block() == Blocks.air){
|
||||
ores.get(tile.drop()).add(world.tile(
|
||||
//make sure to clamp quadrant middle position, since it might go off bounds
|
||||
Mathf.clamp(qx * quadrantSize + quadrantSize / 2, 0, world.width() - 1),
|
||||
Mathf.clamp(qy * quadrantSize + quadrantSize / 2, 0, world.height() - 1)));
|
||||
if(!activeTeams.contains(team)){
|
||||
activeTeams.add(team);
|
||||
}
|
||||
|
||||
//insert the new tile into the quadtree for targeting
|
||||
if(data.buildingTree == null){
|
||||
data.buildingTree = new QuadTree<>(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
|
||||
}
|
||||
data.buildingTree.insert(tile.build);
|
||||
|
||||
if(tile.block().attacks && tile.build instanceof Ranged){
|
||||
if(data.turretTree == null){
|
||||
data.turretTree = new TurretQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
|
||||
}
|
||||
|
||||
data.turretTree.insert(tile.build);
|
||||
}
|
||||
|
||||
notifyHealthChanged(tile.build);
|
||||
}
|
||||
|
||||
if(blocksPresent != null){
|
||||
if(!tile.block().isStatic()){
|
||||
blocksPresent[tile.floorID()] = true;
|
||||
blocksPresent[tile.overlayID()] = true;
|
||||
}
|
||||
//bounds checks only needed in very specific scenarios
|
||||
if(tile.blockID() < blocksPresent.length) blocksPresent[tile.blockID()] = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TileIndex{
|
||||
public final EnumSet<BlockFlag> flags;
|
||||
public final Team team;
|
||||
static class TurretQuadtree extends QuadTree<Building>{
|
||||
|
||||
public TileIndex(EnumSet<BlockFlag> flags, Team team){
|
||||
this.flags = flags;
|
||||
this.team = team;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TileArray implements Iterable<Tile>{
|
||||
Seq<Tile> tiles = new Seq<>(false, 16);
|
||||
IntSet contained = new IntSet();
|
||||
|
||||
public void add(Tile tile){
|
||||
if(contained.add(tile.pos())){
|
||||
tiles.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(Tile tile){
|
||||
if(contained.remove(tile.pos())){
|
||||
tiles.remove(tile);
|
||||
}
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return tiles.size;
|
||||
}
|
||||
|
||||
public Tile first(){
|
||||
return tiles.first();
|
||||
public TurretQuadtree(Rect bounds){
|
||||
super(bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Tile> iterator(){
|
||||
return tiles.iterator();
|
||||
public void hitbox(Building build){
|
||||
tmp.setCentered(build.x, build.y, ((Ranged)build).range() * 2f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected QuadTree<Building> newChild(Rect rect){
|
||||
return new TurretQuadtree(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.util.*;
|
||||
|
||||
/** A priority queue. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public class PathfindQueue{
|
||||
private static final double CAPACITY_RATIO_LOW = 1.5f;
|
||||
private static final double CAPACITY_RATIO_HI = 2f;
|
||||
|
||||
/**
|
||||
* Priority queue represented as a balanced binary heap: the two children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
|
||||
* priority queue is ordered by the elements' natural ordering: For each node n in the heap and each descendant d of n, n <= d.
|
||||
* The element with the lowest value is in queue[0], assuming the queue is nonempty.
|
||||
*/
|
||||
public int[] queue;
|
||||
/** Weights of each object in the queue. */
|
||||
public float[] weights;
|
||||
/** The number of elements in the priority queue. */
|
||||
public int size = 0;
|
||||
|
||||
public PathfindQueue(){
|
||||
this(12);
|
||||
}
|
||||
|
||||
public PathfindQueue(int initialCapacity){
|
||||
this.queue = new int[initialCapacity];
|
||||
this.weights = new float[initialCapacity];
|
||||
}
|
||||
|
||||
public boolean empty(){
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the specified element into this priority queue. If {@code uniqueness} is enabled and this priority queue already
|
||||
* contains the element, the call leaves the queue unchanged and returns false.
|
||||
* @return true if the element was added to this queue, else false
|
||||
* @throws ClassCastException if the specified element cannot be compared with elements currently in this priority queue
|
||||
* according to the priority queue's ordering
|
||||
* @throws IllegalArgumentException if the specified element is null
|
||||
*/
|
||||
public boolean add(int e, float weight){
|
||||
int i = size;
|
||||
if(i >= queue.length) growToSize(i + 1);
|
||||
size = i + 1;
|
||||
if(i == 0){
|
||||
queue[0] = e;
|
||||
weights[0] = weight;
|
||||
}else{
|
||||
siftUp(i, e, weight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, but does not remove, the head of this queue. If this queue is empty, {@code 0} is returned.
|
||||
* @return the head of this queue
|
||||
*/
|
||||
public int peek(){
|
||||
return size == 0 ? 0 : queue[0];
|
||||
}
|
||||
|
||||
/** Removes all of the elements from this priority queue. The queue will be empty after this call returns. */
|
||||
public void clear(){
|
||||
size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and removes the head of this queue, or returns {@code null} if this queue is empty.
|
||||
* @return the head of this queue, or {@code null} if this queue is empty.
|
||||
*/
|
||||
public int poll(){
|
||||
if(size == 0) return 0;
|
||||
int s = --size;
|
||||
int result = queue[0];
|
||||
int x = queue[s];
|
||||
if(s != 0) siftDown(0, x, weights[s]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts item x at position k, maintaining heap invariant by promoting x up the tree until it is greater than or equal to its
|
||||
* parent, or is the root.
|
||||
* @param k the position to fill
|
||||
* @param x the item to insert
|
||||
*/
|
||||
private void siftUp(int k, int x, float weight){
|
||||
while(k > 0){
|
||||
int parent = (k - 1) >>> 1;
|
||||
int e = queue[parent];
|
||||
if(weight >= weights[parent]) break;
|
||||
queue[k] = e;
|
||||
weights[k] = weights[parent];
|
||||
k = parent;
|
||||
}
|
||||
queue[k] = x;
|
||||
weights[k] = weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts item x at position k, maintaining heap invariant by demoting x down the tree repeatedly until it is less than or
|
||||
* equal to its children or is a leaf.
|
||||
* @param k the position to fill
|
||||
* @param x the item to insert
|
||||
*/
|
||||
private void siftDown(int k, int x, float weight){
|
||||
int half = size >>> 1; // loop while a non-leaf
|
||||
while(k < half){
|
||||
int child = (k << 1) + 1; // assume left child is least
|
||||
int c = queue[child];
|
||||
int right = child + 1;
|
||||
if(right < size && weights[child] > weights[right]){
|
||||
c = queue[child = right];
|
||||
}
|
||||
if(weight <= weights[child]) break;
|
||||
queue[k] = c;
|
||||
weights[k] = weights[child];
|
||||
k = child;
|
||||
}
|
||||
queue[k] = x;
|
||||
weights[k] = weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the capacity of the array.
|
||||
* @param minCapacity the desired minimum capacity
|
||||
*/
|
||||
private void growToSize(int minCapacity){
|
||||
if(minCapacity < 0) // overflow
|
||||
throw new ArcRuntimeException("Capacity upper limit exceeded.");
|
||||
int oldCapacity = queue.length;
|
||||
// Double size if small; else grow by 50%
|
||||
int newCapacity = (int)((oldCapacity < 64) ? ((oldCapacity + 1) * CAPACITY_RATIO_HI) : (oldCapacity * CAPACITY_RATIO_LOW));
|
||||
if(newCapacity < 0) // overflow
|
||||
newCapacity = Integer.MAX_VALUE;
|
||||
if(newCapacity < minCapacity) newCapacity = minCapacity;
|
||||
|
||||
int[] newQueue = new int[newCapacity];
|
||||
float[] newWeights = new float[newCapacity];
|
||||
System.arraycopy(queue, 0, newQueue, 0, size);
|
||||
System.arraycopy(weights, 0, newWeights, 0, size);
|
||||
queue = newQueue;
|
||||
weights = newWeights;
|
||||
}
|
||||
}
|
||||
@@ -2,46 +2,50 @@ package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
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.environment.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.world.meta.BlockFlag.*;
|
||||
|
||||
public class Pathfinder implements Runnable{
|
||||
private static final long maxUpdate = Time.millisToNanos(7);
|
||||
private static final long maxUpdate = Time.millisToNanos(8);
|
||||
private static final int updateFPS = 60;
|
||||
private static final int updateInterval = 1000 / updateFPS;
|
||||
private static final int impassable = -1;
|
||||
private static final int fieldTimeout = 1000 * 60 * 2;
|
||||
|
||||
/** cached world size */
|
||||
static int wwidth, wheight;
|
||||
|
||||
static final int impassable = -1;
|
||||
|
||||
public static final int
|
||||
fieldCore = 0,
|
||||
fieldRally = 1;
|
||||
fieldCore = 0;
|
||||
|
||||
public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with(
|
||||
EnemyCoreField::new,
|
||||
RallyField::new
|
||||
EnemyCoreField::new
|
||||
);
|
||||
|
||||
public static final int
|
||||
costGround = 0,
|
||||
costLegs = 1,
|
||||
costNaval = 2;
|
||||
costNaval = 2,
|
||||
costHover = 3;
|
||||
|
||||
public static final Seq<PathCost> costTypes = Seq.with(
|
||||
//ground
|
||||
(team, tile) -> (PathTile.team(tile) == team.id || PathTile.team(tile) == 0) && PathTile.solid(tile) ? impassable : 1 +
|
||||
(team, tile) ->
|
||||
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
|
||||
PathTile.health(tile) * 5 +
|
||||
(PathTile.nearSolid(tile) ? 2 : 0) +
|
||||
(PathTile.nearLiquid(tile) ? 6 : 0) +
|
||||
@@ -49,21 +53,31 @@ public class Pathfinder implements Runnable{
|
||||
(PathTile.damages(tile) ? 30 : 0),
|
||||
|
||||
//legs
|
||||
(team, tile) -> PathTile.legSolid(tile) ? impassable : 1 +
|
||||
(team, tile) ->
|
||||
PathTile.legSolid(tile) ? impassable : 1 +
|
||||
(PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown
|
||||
(PathTile.solid(tile) ? 5 : 0),
|
||||
|
||||
//water
|
||||
(team, tile) -> PathTile.solid(tile) || !PathTile.liquid(tile) ? 200 : 2 +
|
||||
(team, tile) ->
|
||||
(!PathTile.liquid(tile) ? 6000 : 1) +
|
||||
PathTile.health(tile) * 5 +
|
||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
|
||||
(PathTile.deep(tile) ? -1 : 0) +
|
||||
(PathTile.damages(tile) ? 35 : 0)
|
||||
(PathTile.deep(tile) ? 0 : 1) +
|
||||
(PathTile.damages(tile) ? 35 : 0),
|
||||
|
||||
//hover
|
||||
(team, tile) ->
|
||||
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
|
||||
PathTile.health(tile) * 5 +
|
||||
(PathTile.nearSolid(tile) ? 2 : 0)
|
||||
);
|
||||
|
||||
//maps team, cost, type to flow field
|
||||
Flowfield[][][] cache;
|
||||
/** tile data, see PathTileStruct - kept as a separate array for threading reasons */
|
||||
int[] tiles = new int[0];
|
||||
|
||||
/** tile data, see PathTileStruct */
|
||||
int[][] tiles = new int[0][0];
|
||||
/** maps team, cost, type to flow field*/
|
||||
Flowfield[][][] cache;
|
||||
/** unordered array of path data for iteration only. DO NOT iterate or access this in the main thread. */
|
||||
Seq<Flowfield> threadList = new Seq<>(), mainList = new Seq<>();
|
||||
/** handles task scheduling on the update thread. */
|
||||
@@ -79,20 +93,29 @@ public class Pathfinder implements Runnable{
|
||||
stop();
|
||||
|
||||
//reset and update internal tile array
|
||||
tiles = new int[world.width()][world.height()];
|
||||
tiles = new int[world.width() * world.height()];
|
||||
wwidth = world.width();
|
||||
wheight = world.height();
|
||||
threadList = new Seq<>();
|
||||
mainList = new Seq<>();
|
||||
clearCache();
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
tiles[tile.x][tile.y] = packTile(tile);
|
||||
for(int i = 0; i < tiles.length; i++){
|
||||
Tile tile = world.tiles.geti(i);
|
||||
tiles[i] = packTile(tile);
|
||||
}
|
||||
|
||||
preloadPath(getField(state.rules.waveTeam, costGround, fieldCore));
|
||||
//don't bother setting up paths unless necessary
|
||||
if(state.rules.waveTeam.needsFlowField() && !net.client()){
|
||||
preloadPath(getField(state.rules.waveTeam, costGround, fieldCore));
|
||||
Log.debug("Preloading ground enemy flowfield.");
|
||||
|
||||
//preload water on naval maps
|
||||
if(spawner.getSpawns().contains(t -> t.floor().isLiquid)){
|
||||
preloadPath(getField(state.rules.waveTeam, costNaval, fieldCore));
|
||||
Log.debug("Preloading naval enemy flowfield.");
|
||||
}
|
||||
|
||||
//preload water on naval maps
|
||||
if(spawner.getSpawns().contains(t -> t.floor().isLiquid)){
|
||||
preloadPath(getField(state.rules.waveTeam, costNaval, fieldCore));
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -101,6 +124,35 @@ public class Pathfinder implements Runnable{
|
||||
Events.on(ResetEvent.class, event -> stop());
|
||||
|
||||
Events.on(TileChangeEvent.class, event -> updateTile(event.tile));
|
||||
|
||||
//remove nearSolid flag for tiles
|
||||
Events.on(TilePreChangeEvent.class, event -> {
|
||||
Tile tile = event.tile;
|
||||
|
||||
if(tile.solid()){
|
||||
for(int i = 0; i < 4; i++){
|
||||
Tile other = tile.nearby(i);
|
||||
if(other != null){
|
||||
//other tile needs to update its nearSolid to be false if it's not solid and this tile just got un-solidified
|
||||
if(!other.solid()){
|
||||
boolean otherNearSolid = false;
|
||||
for(int j = 0; j < 4; j++){
|
||||
Tile othernear = other.nearby(i);
|
||||
if(othernear != null && othernear.solid()){
|
||||
otherNearSolid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int arr = other.array();
|
||||
//the other tile is no longer near solid, remove the solid bit
|
||||
if(!otherNearSolid && tiles.length > arr){
|
||||
tiles[arr] &= ~(PathTile.bitMaskNearSolid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearCache(){
|
||||
@@ -108,36 +160,60 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
|
||||
/** Packs a tile into its internal representation. */
|
||||
private int packTile(Tile tile){
|
||||
boolean nearLiquid = false, nearSolid = false, nearGround = false;
|
||||
public int packTile(Tile tile){
|
||||
boolean nearLiquid = false, nearSolid = false, nearLegSolid = false, nearGround = false, solid = tile.solid(), allDeep = tile.floor().isDeep();
|
||||
|
||||
for(int i = 0; i < 4; i++){
|
||||
Tile other = tile.nearby(i);
|
||||
if(other != null){
|
||||
if(other.floor().isLiquid) nearLiquid = true;
|
||||
if(other.solid()) nearSolid = true;
|
||||
if(!other.floor().isLiquid) nearGround = true;
|
||||
Floor floor = other.floor();
|
||||
boolean osolid = other.solid();
|
||||
if(floor.isLiquid && floor.isDeep()) nearLiquid = true;
|
||||
//TODO potentially strange behavior when teamPassable is false for other teams?
|
||||
if(osolid && !other.block().teamPassable) nearSolid = true;
|
||||
if(!floor.isLiquid) nearGround = true;
|
||||
if(!floor.isDeep()) allDeep = false;
|
||||
if(other.legSolid()) nearLegSolid = true;
|
||||
|
||||
//other tile is now near solid
|
||||
if(solid && !tile.block().teamPassable){
|
||||
tiles[other.array()] |= PathTile.bitMaskNearSolid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int tid = tile.getTeamID();
|
||||
|
||||
return PathTile.get(
|
||||
tile.build == null || !tile.solid() || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
|
||||
tile.getTeamID(),
|
||||
tile.solid(),
|
||||
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
|
||||
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
|
||||
solid,
|
||||
tile.floor().isLiquid,
|
||||
tile.staticDarkness() >= 2 || (tile.floor().solid && tile.block() == Blocks.air),
|
||||
tile.legSolid(),
|
||||
nearLiquid,
|
||||
nearGround,
|
||||
nearSolid,
|
||||
nearLegSolid,
|
||||
tile.floor().isDeep(),
|
||||
tile.floor().damageTaken > 0.00001f
|
||||
tile.floor().damageTaken > 0.00001f,
|
||||
allDeep,
|
||||
tile.block().teamPassable
|
||||
);
|
||||
}
|
||||
|
||||
public int get(int x, int y){
|
||||
return tiles[x + y * wwidth];
|
||||
}
|
||||
|
||||
/** Starts or restarts the pathfinding thread. */
|
||||
private void start(){
|
||||
stop();
|
||||
thread = Threads.daemon(this);
|
||||
if(net.client()) return;
|
||||
|
||||
thread = new Thread(this, "Pathfinder");
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/** Stops the pathfinding thread. */
|
||||
@@ -150,15 +226,14 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
|
||||
/** Update a tile in the internal pathfinding grid.
|
||||
* Causes a complete pathfinding reclaculation. Main thread only. */
|
||||
* Causes a complete pathfinding recalculation. Main thread only. */
|
||||
public void updateTile(Tile tile){
|
||||
if(net.client()) return;
|
||||
|
||||
int x = tile.x, y = tile.y;
|
||||
|
||||
tile.getLinkedTiles(t -> {
|
||||
if(Structs.inBounds(t.x, t.y, tiles)){
|
||||
tiles[t.x][t.y] = packTile(t);
|
||||
int pos = t.array();
|
||||
if(pos < tiles.length){
|
||||
tiles[pos] = packTile(t);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,17 +241,19 @@ public class Pathfinder implements Runnable{
|
||||
for(Flowfield path : mainList){
|
||||
if(path != null){
|
||||
synchronized(path.targets){
|
||||
path.targets.clear();
|
||||
path.getPositions(path.targets);
|
||||
path.updateTargetPositions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//mark every flow field as dirty, so it updates when it's done
|
||||
queue.post(() -> {
|
||||
for(Flowfield data : threadList){
|
||||
updateTargets(data, x, y);
|
||||
data.dirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
controlPath.updateTile(tile);
|
||||
}
|
||||
|
||||
/** Thread implementation. */
|
||||
@@ -189,34 +266,16 @@ public class Pathfinder implements Runnable{
|
||||
if(state.isPlaying()){
|
||||
queue.run();
|
||||
|
||||
//total update time no longer than maxUpdate
|
||||
//each update time (not total!) no longer than maxUpdate
|
||||
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
|
||||
data.lastUpdateTime = Time.millis();
|
||||
//if it's dirty and there is nothing to update, begin updating once more
|
||||
if(data.dirty && data.frontier.size == 0){
|
||||
updateTargets(data);
|
||||
data.dirty = false;
|
||||
}
|
||||
|
||||
Team team = data.team;
|
||||
|
||||
Core.app.post(() -> {
|
||||
//remove its used state
|
||||
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);
|
||||
});
|
||||
|
||||
queue.post(() -> {
|
||||
//remove from this thread list with a delay
|
||||
threadList.remove(data);
|
||||
});
|
||||
}*/
|
||||
updateFrontier(data, maxUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +324,7 @@ public class Pathfinder implements Runnable{
|
||||
synchronized(path.targets){
|
||||
//make sure the position actually changed
|
||||
if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){
|
||||
path.targets.clear();
|
||||
path.getPositions(path.targets);
|
||||
path.updateTargetPositions();
|
||||
|
||||
//queue an update
|
||||
queue.post(() -> updateTargets(path));
|
||||
@@ -274,8 +332,10 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
}
|
||||
|
||||
int[][] values = path.weights;
|
||||
int value = values[tile.x][tile.y];
|
||||
//use complete weights if possible; these contain a complete flow field that is not being updated
|
||||
int[] values = path.hasComplete ? path.completeWeights : path.weights;
|
||||
int apos = tile.array();
|
||||
int value = values[apos];
|
||||
|
||||
Tile current = null;
|
||||
int tl = 0;
|
||||
@@ -285,42 +345,20 @@ public class Pathfinder implements Runnable{
|
||||
Tile other = world.tile(dx, dy);
|
||||
if(other == null) continue;
|
||||
|
||||
if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && path.passable(dx, dy) &&
|
||||
!(point.x != 0 && point.y != 0 && (!path.passable(tile.x + point.x, tile.y) || !path.passable(tile.x, tile.y + point.y)))){ //diagonal corner trap
|
||||
int packed = world.packArray(dx, dy);
|
||||
|
||||
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
|
||||
!(point.x != 0 && point.y != 0 && (!path.passable(world.packArray(tile.x + point.x, tile.y)) || !path.passable(world.packArray(tile.x, tile.y + point.y))))){ //diagonal corner trap
|
||||
current = other;
|
||||
tl = values[dx][dy];
|
||||
tl = values[packed];
|
||||
}
|
||||
}
|
||||
|
||||
if(current == null || tl == impassable) return tile;
|
||||
if(current == null || tl == impassable || (path.cost == costTypes.items[costGround] && current.dangerous() && !tile.dangerous())) return tile;
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the frontier, increments the search and sets up all flow sources.
|
||||
* This only occurs for active teams.
|
||||
*/
|
||||
private void updateTargets(Flowfield path, int x, int y){
|
||||
if(!Structs.inBounds(x, y, path.weights)) return;
|
||||
|
||||
if(path.weights[x][y] == 0){
|
||||
//this was a previous target
|
||||
path.frontier.clear();
|
||||
}else if(!path.frontier.isEmpty()){
|
||||
//skip if this path is processing
|
||||
return;
|
||||
}
|
||||
|
||||
//update cost of the tile TODO maybe only update the cost when it's not passable
|
||||
path.weights[x][y] = path.cost.getCost(path.team, tiles[x][y]);
|
||||
|
||||
//clear frontier to prevent contamination
|
||||
path.frontier.clear();
|
||||
|
||||
updateTargets(path);
|
||||
}
|
||||
|
||||
/** Increments the search and sets up flow sources. Does not change the frontier. */
|
||||
private void updateTargets(Flowfield path){
|
||||
|
||||
@@ -331,18 +369,16 @@ public class Pathfinder implements Runnable{
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
int tx = Point2.x(pos), ty = Point2.y(pos);
|
||||
|
||||
path.weights[tx][ty] = 0;
|
||||
path.searches[tx][ty] = path.search;
|
||||
path.weights[pos] = 0;
|
||||
path.searches[pos] = path.search;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void preloadPath(Flowfield path){
|
||||
path.targets.clear();
|
||||
path.getPositions(path.targets);
|
||||
path.updateTargetPositions();
|
||||
registerPath(path);
|
||||
updateFrontier(path, -1);
|
||||
}
|
||||
@@ -354,7 +390,7 @@ public class Pathfinder implements Runnable{
|
||||
*/
|
||||
private void registerPath(Flowfield path){
|
||||
path.lastUpdateTime = Time.millis();
|
||||
path.setup(tiles.length, tiles[0].length);
|
||||
path.setup(tiles.length);
|
||||
|
||||
threadList.add(path);
|
||||
|
||||
@@ -362,28 +398,29 @@ public class Pathfinder implements Runnable{
|
||||
Core.app.post(() -> mainList.add(path));
|
||||
|
||||
//fill with impassables by default
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
path.weights[x][y] = impassable;
|
||||
}
|
||||
for(int i = 0; i < tiles.length; i++){
|
||||
path.weights[i] = impassable;
|
||||
}
|
||||
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
path.weights[Point2.x(pos)][Point2.y(pos)] = 0;
|
||||
path.weights[pos] = 0;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the frontier for a path. Pathfinding thread only. */
|
||||
private void updateFrontier(Flowfield path, long nsToRun){
|
||||
boolean hadAny = path.frontier.size > 0;
|
||||
long start = Time.nanos();
|
||||
|
||||
while(path.frontier.size > 0 && (nsToRun < 0 || Time.timeSinceNanos(start) <= nsToRun)){
|
||||
Tile tile = world.tile(path.frontier.removeLast());
|
||||
if(tile == null || path.weights == null) return; //something went horribly wrong, bail
|
||||
int cost = path.weights[tile.x][tile.y];
|
||||
int counter = 0;
|
||||
|
||||
while(path.frontier.size > 0){
|
||||
int tile = path.frontier.removeLast();
|
||||
if(path.weights == null) return; //something went horribly wrong, bail
|
||||
int cost = path.weights[tile];
|
||||
|
||||
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
|
||||
if(path.frontier.size >= world.width() * world.height()){
|
||||
@@ -394,47 +431,79 @@ public class Pathfinder implements Runnable{
|
||||
if(cost != impassable){
|
||||
for(Point2 point : Geometry.d4){
|
||||
|
||||
int dx = tile.x + point.x, dy = tile.y + point.y;
|
||||
int dx = (tile % wwidth) + point.x, dy = (tile / wwidth) + point.y;
|
||||
|
||||
if(dx < 0 || dy < 0 || dx >= tiles.length || dy >= tiles[0].length) continue;
|
||||
if(dx < 0 || dy < 0 || dx >= wwidth || dy >= wheight) continue;
|
||||
|
||||
int otherCost = path.cost.getCost(path.team, tiles[dx][dy]);
|
||||
int newPos = tile + point.x + point.y * wwidth;
|
||||
int otherCost = path.cost.getCost(path.team.id, tiles[newPos]);
|
||||
|
||||
if((path.weights[dx][dy] > cost + otherCost || path.searches[dx][dy] < path.search) && otherCost != impassable){
|
||||
path.frontier.addFirst(Point2.pack(dx, dy));
|
||||
path.weights[dx][dy] = cost + otherCost;
|
||||
path.searches[dx][dy] = (short)path.search;
|
||||
if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){
|
||||
path.frontier.addFirst(newPos);
|
||||
path.weights[newPos] = cost + otherCost;
|
||||
path.searches[newPos] = (short)path.search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//every N iterations, check the time spent - this prevents extra calls to nano time, which itself is slow
|
||||
if(nsToRun >= 0 && (counter++) >= 200){
|
||||
counter = 0;
|
||||
if(Time.timeSinceNanos(start) >= nsToRun){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//there WERE some things in the frontier, but now they are gone, so the path is done; copy over latest data
|
||||
if(hadAny && path.frontier.size == 0){
|
||||
System.arraycopy(path.weights, 0, path.completeWeights, 0, path.weights.length);
|
||||
path.hasComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnemyCoreField extends Flowfield{
|
||||
private final static BlockFlag[] randomTargets = {storage, generator, launchPad, factory, repair, battery, reactor, drill};
|
||||
private Rand rand = new Rand();
|
||||
|
||||
@Override
|
||||
protected void getPositions(IntSeq out){
|
||||
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
|
||||
out.add(other.pos());
|
||||
if(state.rules.randomWaveAI && team == state.rules.waveTeam){
|
||||
rand.setSeed(state.rules.waves ? state.wave : (int)(state.tick / (5400)) + hashCode());
|
||||
|
||||
//maximum amount of different target flag types they will attack
|
||||
int max = 1;
|
||||
|
||||
for(int attempt = 0; attempt < 5 && max > 0; attempt++){
|
||||
var targets = indexer.getEnemy(team, randomTargets[rand.random(randomTargets.length - 1)]);
|
||||
if(!targets.isEmpty()){
|
||||
boolean any = false;
|
||||
for(Building other : targets){
|
||||
if((other.items != null && other.items.any()) || other.status() != BlockStatus.noInput){
|
||||
out.add(other.tile.array());
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if(any){
|
||||
max --;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Building other : indexer.getEnemy(team, BlockFlag.core)){
|
||||
out.add(other.tile.array());
|
||||
}
|
||||
|
||||
//spawn points are also enemies.
|
||||
if(state.rules.waves && team == state.rules.defaultTeam){
|
||||
for(Tile other : spawner.getSpawns()){
|
||||
out.add(other.pos());
|
||||
out.add(other.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RallyField extends Flowfield{
|
||||
@Override
|
||||
protected void getPositions(IntSeq out){
|
||||
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
|
||||
out.add(other.pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PositionTarget extends Flowfield{
|
||||
public final Position position;
|
||||
|
||||
@@ -445,7 +514,7 @@ 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(world.packArray(World.toTile(position.getX()), World.toTile(position.getY())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,11 +529,18 @@ public class Pathfinder implements Runnable{
|
||||
protected Team team = Team.derelict;
|
||||
/** Function for calculating path cost. Set before using. */
|
||||
protected PathCost cost = costTypes.get(costGround);
|
||||
/** Whether there are valid weights in the complete array. */
|
||||
protected volatile boolean hasComplete;
|
||||
/** If true, this flow field needs updating. This flag is only set to false once the flow field finishes and the weights are copied over. */
|
||||
protected boolean dirty = false;
|
||||
|
||||
/** costs of getting to a specific tile */
|
||||
public int[][] weights;
|
||||
public int[] weights;
|
||||
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
|
||||
public int[][] searches;
|
||||
public int[] searches;
|
||||
/** the last "complete" weights of this tilemap. */
|
||||
public int[] completeWeights;
|
||||
|
||||
/** 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! */
|
||||
@@ -476,23 +552,35 @@ public class Pathfinder implements Runnable{
|
||||
/** whether this flow field is ready to be used */
|
||||
boolean initialized;
|
||||
|
||||
void setup(int width, int height){
|
||||
this.weights = new int[width][height];
|
||||
this.searches = new int[width][height];
|
||||
this.frontier.ensureCapacity((width + height) * 3);
|
||||
void setup(int length){
|
||||
this.weights = new int[length];
|
||||
this.searches = new int[length];
|
||||
this.completeWeights = new int[length];
|
||||
this.frontier.ensureCapacity((length) / 4);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
protected boolean passable(int x, int y){
|
||||
return cost.getCost(team, pathfinder.tiles[x][y]) != impassable;
|
||||
public boolean hasCompleteWeights(){
|
||||
return hasComplete && completeWeights != null;
|
||||
}
|
||||
|
||||
public void updateTargetPositions(){
|
||||
targets.clear();
|
||||
getPositions(targets);
|
||||
}
|
||||
|
||||
protected boolean passable(int pos){
|
||||
int amount = cost.getCost(team.id, pathfinder.tiles[pos]);
|
||||
//edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable
|
||||
return amount != impassable && !(cost == costTypes.get(costNaval) && amount >= 6000);
|
||||
}
|
||||
|
||||
/** Gets targets to pathfind towards. This must run on the main thread. */
|
||||
protected abstract void getPositions(IntSeq out);
|
||||
}
|
||||
|
||||
interface PathCost{
|
||||
int getCost(Team traversing, int tile);
|
||||
public interface PathCost{
|
||||
int getCost(int team, int tile);
|
||||
}
|
||||
|
||||
/** Holds a copy of tile data for a specific tile position. */
|
||||
@@ -514,9 +602,15 @@ public class Pathfinder implements Runnable{
|
||||
boolean nearGround;
|
||||
//whether this block is near a solid object
|
||||
boolean nearSolid;
|
||||
//whether this block is near a block that is solid for legged units
|
||||
boolean nearLegSolid;
|
||||
//whether this block is deep / drownable
|
||||
boolean deep;
|
||||
//whether the floor damages
|
||||
boolean damages;
|
||||
//whether all tiles nearby are deep
|
||||
boolean allDeep;
|
||||
//block teamPassable is true
|
||||
boolean teamPassable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.defense.turrets.BaseTurret.*;
|
||||
import mindustry.world.blocks.defense.turrets.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class RtsAI{
|
||||
static final Seq<Building> targets = new Seq<>();
|
||||
static final Seq<Unit> squad = new Seq<>(false);
|
||||
static final IntSet used = new IntSet();
|
||||
static final IntSet assignedTargets = new IntSet(), invalidTarget = new IntSet();
|
||||
static final float squadRadius = 140f;
|
||||
static final int timeUpdate = 0, timerSpawn = 1, maxTargetsChecked = 15;
|
||||
|
||||
//in order of priority??
|
||||
static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill};
|
||||
static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>();
|
||||
static final boolean debug = OS.hasProp("mindustry.debug");
|
||||
|
||||
final Interval timer = new Interval(10);
|
||||
final TeamData data;
|
||||
final ObjectSet<Building> damagedSet = new ObjectSet<>();
|
||||
final Seq<Building> damaged = new Seq<>(false);
|
||||
|
||||
//must be static, as this class can get instantiated many times; event listeners are hard to clean up
|
||||
static{
|
||||
Events.on(BuildDamageEvent.class, e -> {
|
||||
if(e.build.team.rules().rtsAi){
|
||||
var ai = e.build.team.data().rtsAi;
|
||||
if(ai != null){
|
||||
ai.damagedSet.add(e.build);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public RtsAI(TeamData data){
|
||||
this.data = data;
|
||||
timer.reset(0, Mathf.random(60f * 2f));
|
||||
|
||||
//TODO remove: debugging!
|
||||
|
||||
if(debug){
|
||||
Events.run(Trigger.draw, () -> {
|
||||
|
||||
Draw.draw(Layer.overlayUI, () -> {
|
||||
|
||||
float s = Fonts.outline.getScaleX();
|
||||
Fonts.outline.getData().setScale(0.5f);
|
||||
for(var target : weights){
|
||||
Fonts.outline.draw("[sky]" + Strings.fixed(target.value, 2), target.key.x, target.key.y, Align.center);
|
||||
}
|
||||
Fonts.outline.getData().setScale(s);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void update(){
|
||||
|
||||
if(timer.get(timeUpdate, 60f * 2f)){
|
||||
assignSquads();
|
||||
checkBuilding();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO atrocious implementation
|
||||
void checkBuilding(){
|
||||
if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 7f) && data.hasCore()){
|
||||
CoreBlock block = (CoreBlock)data.core().block;
|
||||
int coreUnits = data.countType(block.unitType);
|
||||
|
||||
//create AI core unit(s) at random cores
|
||||
if(coreUnits < data.cores.size){
|
||||
Unit unit = block.unitType.create(data.team);
|
||||
unit.set(data.cores.random());
|
||||
unit.add();
|
||||
Fx.spawn.at(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assignSquads(){
|
||||
assignedTargets.clear();
|
||||
used.clear();
|
||||
damaged.addAll(damagedSet);
|
||||
damagedSet.clear();
|
||||
|
||||
boolean didDefend = false;
|
||||
|
||||
for(var unit : data.units){
|
||||
if(used.add(unit.id) && unit.isCommandable() && !unit.command().hasCommand() && !unit.command().isAttacking()){
|
||||
squad.clear();
|
||||
float rad = squadRadius + unit.hitSize*1.5f;
|
||||
data.tree().intersect(unit.x - rad/2f, unit.y - rad/2f, rad, rad, squad);
|
||||
|
||||
squad.truncate(data.team.rules().rtsMaxSquad);
|
||||
|
||||
//remove overlapping squads
|
||||
squad.removeAll(u -> (u != unit && used.contains(u.id)) || !u.isCommandable() || u.command().hasCommand() || ((u.flag == 0) != (unit.flag == 0)));
|
||||
//mark used so other squads can't steal them
|
||||
for(var item : squad){
|
||||
used.add(item.id);
|
||||
}
|
||||
|
||||
//TODO flawed, squads
|
||||
if(handleSquad(squad, !didDefend)){
|
||||
didDefend = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
damaged.clear();
|
||||
}
|
||||
|
||||
boolean handleSquad(Seq<Unit> units, boolean noDefenders){
|
||||
if(units.isEmpty()) return false;
|
||||
|
||||
float health = 0f, dps = 0f;
|
||||
float ax = 0f, ay = 0f;
|
||||
boolean targetAir = true, targetGround = true;
|
||||
|
||||
for(var unit : units){
|
||||
if(!unit.type.targetAir) targetAir = false;
|
||||
if(!unit.type.targetGround) targetGround = false;
|
||||
|
||||
ax += unit.x;
|
||||
ay += unit.y;
|
||||
health += unit.health;
|
||||
dps += unit.type.dpsEstimate;
|
||||
}
|
||||
ax /= units.size;
|
||||
ay /= units.size;
|
||||
|
||||
if(debug){
|
||||
Vars.ui.showLabel("Squad: " + units.size, 2f, ax, ay);
|
||||
}
|
||||
|
||||
Building defend = null;
|
||||
boolean defendingCore = false;
|
||||
|
||||
//there is something to defend, see if it's worth the time
|
||||
if(damaged.size > 0){
|
||||
//TODO do the weights matter at all?
|
||||
//for(var build : damaged){
|
||||
//float w = estimateStats(ax, ay, dps, health);
|
||||
//weights.put(build, w);
|
||||
//}
|
||||
|
||||
//screw you java
|
||||
float aax = ax, aay = ay;
|
||||
|
||||
Building best = damaged.min(b -> {
|
||||
//rush to core IMMEDIATELY
|
||||
if(b instanceof CoreBuild){
|
||||
return -999999f;
|
||||
}
|
||||
|
||||
return b.dst(aax, aay);
|
||||
});
|
||||
|
||||
//defend when close, or this is the only squad defending
|
||||
//TODO will always rush to defense no matter what
|
||||
if(best != null && (best instanceof CoreBuild || (units.size >= data.team.rules().rtsMinSquad || (units.size > 0 && units.first().flag != 0)) || best.within(ax, ay, 1000f))){
|
||||
defend = best;
|
||||
|
||||
if(debug){
|
||||
Vars.ui.showLabel("Defend, dst = " + (int)(best.dst(ax, ay)), 8f, best.x, best.y);
|
||||
}
|
||||
|
||||
if(best instanceof CoreBuild){
|
||||
defendingCore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean tair = targetAir, tground = targetGround;
|
||||
|
||||
//find aggressor, or else, the thing being attacked
|
||||
Vec2 defendPos = null;
|
||||
Teamc defendTarget = null;
|
||||
if(defend != null){
|
||||
float checkRange = 350f;
|
||||
|
||||
//TODO could be made faster by storing bullet shooter
|
||||
Unit aggressor = Units.closestEnemy(data.team, defend.x, defend.y, checkRange, u -> u.checkTarget(tair, tground));
|
||||
if(aggressor != null){
|
||||
//do not target it directly - target the position?
|
||||
//defendTarget = aggressor;
|
||||
defendPos = new Vec2(aggressor.x, aggressor.y);
|
||||
defendTarget = aggressor;
|
||||
}else if(false){ //TODO currently ignored, no use defending against nothing
|
||||
//should it even go there if there's no aggressor found?
|
||||
Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
|
||||
if(closest != null){
|
||||
defendPos = new Vec2(closest.worldx(), closest.worldy());
|
||||
}
|
||||
}else{
|
||||
float mindst = Float.MAX_VALUE;
|
||||
Building build = null;
|
||||
|
||||
//find closest turret to attack.
|
||||
for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){
|
||||
if(turret.within(defend, ((Ranged)turret).range())){
|
||||
float dst = turret.dst2(defend);
|
||||
if(dst < mindst){
|
||||
mindst = dst;
|
||||
build = turret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(build != null){
|
||||
defendTarget = build;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean anyDefend = defendPos != null || defendTarget != null;
|
||||
|
||||
invalidTarget.clear();
|
||||
|
||||
for(var unit : squad){
|
||||
if(unit.controller() instanceof CommandAI ai){
|
||||
invalidTarget.addAll(ai.unreachableBuildings);
|
||||
}
|
||||
}
|
||||
|
||||
var build = anyDefend ? null : findTarget(ax, ay, units.size, dps, health, units.first().flag == 0, units.first().isFlying());
|
||||
|
||||
if(build != null || anyDefend){
|
||||
for(var unit : units){
|
||||
if(unit.isCommandable() && !unit.command().hasCommand()){
|
||||
if(defendPos != null && !unit.isPathImpassable(World.toTile(defendPos.x), World.toTile(defendPos.y))){
|
||||
unit.command().commandPosition(defendPos, true);
|
||||
}else{
|
||||
//TODO stopAtTarget parameter could be false, could be tweaked
|
||||
unit.command().commandTarget(defendTarget == null ? build : defendTarget, defendTarget != null);
|
||||
}
|
||||
|
||||
//assign a flag, so it will be "mobilized" more easily later
|
||||
if(!defendingCore){
|
||||
unit.flag = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return anyDefend;
|
||||
}
|
||||
|
||||
@Nullable Building findTarget(float x, float y, int total, float dps, float health, boolean checkWeight, boolean air){
|
||||
if(total < data.team.rules().rtsMinSquad) return null;
|
||||
|
||||
//flag priority?
|
||||
//1. generator
|
||||
//2. factory
|
||||
//3. core
|
||||
targets.clear();
|
||||
for(var flag : flags){
|
||||
targets.addAll(Vars.indexer.getEnemy(data.team, flag));
|
||||
}
|
||||
targets.removeAll(b -> assignedTargets.contains(b.id) || invalidTarget.contains(b.pos()));
|
||||
|
||||
if(targets.size == 0) return null;
|
||||
|
||||
weights.clear();
|
||||
|
||||
//only check a maximum number of targets to prevent hammering the CPU with estimateStats calls
|
||||
targets.shuffle();
|
||||
targets.truncate(maxTargetsChecked);
|
||||
|
||||
for(var target : targets){
|
||||
weights.put(target, estimateStats(x, y, target.x, target.y, dps, health, air));
|
||||
}
|
||||
|
||||
var result = targets.min(
|
||||
Structs.comps(
|
||||
//weight is most important?
|
||||
Structs.comparingFloat(b -> (1f - weights.get(b, 0f)) + b.dst(x, y)/10000f),
|
||||
//then distance TODO why weight above
|
||||
Structs.comparingFloat(b -> b.dst2(x, y))
|
||||
)
|
||||
);
|
||||
|
||||
float weight = weights.get(result, 0f);
|
||||
if(checkWeight && weight < data.team.rules().rtsMinWeight && total < Units.getCap(data.team)){
|
||||
return null;
|
||||
}
|
||||
|
||||
assignedTargets.add(result.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
//TODO extremely slow especially with many squads.
|
||||
float estimateStats(float fromX, float fromY, float x, float y, float selfDps, float selfHealth, boolean air){
|
||||
float[] health = {0f}, dps = {0f};
|
||||
float extraRadius = 50f;
|
||||
|
||||
for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){
|
||||
if(turret instanceof BaseTurretBuild t && turret.block instanceof Turret tb && ((tb.targetAir && air) || (tb.targetGround && !air)) && Intersector.distanceSegmentPoint(fromX, fromY, x, y, t.x, t.y) <= t.range() + extraRadius){
|
||||
health[0] += t.health;
|
||||
dps[0] += t.estimateDps();
|
||||
}
|
||||
}
|
||||
|
||||
Tmp.r1.set(fromX, fromY, x - fromX, y - fromY).normalize().grow(140f * 2f);
|
||||
|
||||
//add on extra radius, assume unit range is below that...?
|
||||
Units.nearbyEnemies(data.team, Tmp.r1, other -> {
|
||||
if(Intersector.distanceSegmentPoint(fromX, fromY, x, y, other.x, other.y) <= other.range() + extraRadius){
|
||||
health[0] += other.health;
|
||||
dps[0] += other.type.dpsEstimate;
|
||||
}
|
||||
});
|
||||
|
||||
float hp = health[0], dp = dps[0];
|
||||
|
||||
float timeDestroyOther = Mathf.zero(selfDps, 0.001f) ? Float.POSITIVE_INFINITY : hp / selfDps;
|
||||
float timeDestroySelf = Mathf.zero(dp) ? Float.POSITIVE_INFINITY : selfHealth / dp;
|
||||
|
||||
//other can never be destroyed | other destroys self instantly
|
||||
if(Float.isInfinite(timeDestroyOther) || Mathf.zero(timeDestroySelf)) return 0f;
|
||||
//self can never be destroyed | self destroys other instantly
|
||||
if(Float.isInfinite(timeDestroySelf) || Mathf.zero(timeDestroyOther)) return 100000f;
|
||||
|
||||
//examples:
|
||||
// self 10 sec / other 10 sec -> can destroy target with 100 % losses -> returns 1
|
||||
// self 5 sec / other 10 sec -> can destroy about half of other -> returns 0.5 (needs to be 2x stronger to defeat)
|
||||
return timeDestroySelf / timeDestroyOther;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
|
||||
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
|
||||
public class UnitCommand extends MappableContent{
|
||||
/** @deprecated now a content type, use the methods in Vars.content instead */
|
||||
@Deprecated
|
||||
public static final Seq<UnitCommand> all = new Seq<>();
|
||||
|
||||
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand;
|
||||
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
/** Controller that this unit will use when this command is used. Return null for "default" behavior. */
|
||||
public final Func<Unit, AIController> controller;
|
||||
/** If true, this unit will automatically switch away to the move command when given a position. */
|
||||
public boolean switchToMove = true;
|
||||
/** Whether to draw the movement/attack target. */
|
||||
public boolean drawTarget = false;
|
||||
/** Whether to reset targets when switching to or from this command. */
|
||||
public boolean resetTarget = true;
|
||||
/** */
|
||||
public boolean exactArrival = false;
|
||||
/** Key to press for this command. */
|
||||
public @Nullable Binding keybind = null;
|
||||
|
||||
public UnitCommand(String name, String icon, Func<Unit, AIController> controller){
|
||||
super(name);
|
||||
|
||||
this.icon = icon;
|
||||
this.controller = controller == null ? u -> null : controller;
|
||||
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){
|
||||
this(name, icon, controller);
|
||||
this.keybind = keybind;
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("command." + name);
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(){
|
||||
return Icon.icons.get(icon, Icon.cancel);
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char)Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.unitCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "UnitCommand:" + name;
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
|
||||
moveCommand = new UnitCommand("move", "right", Binding.unit_command_move, null){{
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
repairCommand = new UnitCommand("repair", "modeSurvival", Binding.unit_command_repair, u -> new RepairAI());
|
||||
rebuildCommand = new UnitCommand("rebuild", "hammer", Binding.unit_command_rebuild, u -> new BuilderAI());
|
||||
assistCommand = new UnitCommand("assist", "players", Binding.unit_command_assist, u -> {
|
||||
var ai = new BuilderAI();
|
||||
ai.onlyAssist = true;
|
||||
return ai;
|
||||
});
|
||||
mineCommand = new UnitCommand("mine", "production", Binding.unit_command_mine, u -> new MinerAI());
|
||||
boostCommand = new UnitCommand("boost", "up", Binding.unit_command_boost, u -> new BoostAI()){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
enterPayloadCommand = new UnitCommand("enterPayload", "downOpen", Binding.unit_command_enter_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loadUnitsCommand = new UnitCommand("loadUnits", "upload", Binding.unit_command_load_units, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loadBlocksCommand = new UnitCommand("loadBlocks", "up", Binding.unit_command_load_blocks, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
exactArrival = true;
|
||||
}};
|
||||
unloadPayloadCommand = new UnitCommand("unloadPayload", "download", Binding.unit_command_unload_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
loopPayloadCommand = new UnitCommand("loopPayload", "resize", Binding.unit_command_loop_payload, null){{
|
||||
switchToMove = false;
|
||||
drawTarget = true;
|
||||
resetTarget = false;
|
||||
}};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.Pathfinder.*;
|
||||
import mindustry.async.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class UnitGroup{
|
||||
public Seq<Unit> units = new Seq<>();
|
||||
public int collisionLayer;
|
||||
public volatile float[] positions, originalPositions;
|
||||
public volatile boolean valid;
|
||||
|
||||
public void calculateFormation(Vec2 dest, int collisionLayer){
|
||||
this.collisionLayer = collisionLayer;
|
||||
|
||||
float cx = 0f, cy = 0f;
|
||||
for(Unit unit : units){
|
||||
cx += unit.x;
|
||||
cy += unit.y;
|
||||
}
|
||||
cx /= units.size;
|
||||
cy /= units.size;
|
||||
positions = new float[units.size * 2];
|
||||
|
||||
|
||||
//all positions are relative to the center
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
Unit unit = units.get(i);
|
||||
positions[i * 2] = unit.x - cx;
|
||||
positions[i * 2 + 1] = unit.y - cy;
|
||||
unit.command().groupIndex = i;
|
||||
}
|
||||
|
||||
//run on new thread to prevent stutter
|
||||
Vars.mainExecutor.submit(() -> {
|
||||
//unused space between circles that needs to be reached for compression to end
|
||||
float maxSpaceUsage = 0.7f;
|
||||
boolean compress = true;
|
||||
|
||||
int compressionIterations = 0;
|
||||
int physicsIterations = 0;
|
||||
int totalIterations = 0;
|
||||
int maxPhysicsIterations = Math.min(1 + (int)(Math.pow(units.size, 0.65) / 10), 6);
|
||||
|
||||
//yep, new allocations, because this is a new thread.
|
||||
IntQuadTree tree = new IntQuadTree(new Rect(0f, 0f, Vars.world.unitWidth(), Vars.world.unitHeight()),
|
||||
(index, hitbox) -> hitbox.setCentered(positions[index * 2], positions[index * 2 + 1], units.get(index).hitSize));
|
||||
IntSeq tmpseq = new IntSeq();
|
||||
Vec2 v1 = new Vec2();
|
||||
Vec2 v2 = new Vec2();
|
||||
|
||||
//this algorithm basically squeezes all the circle colliders together, then proceeds to simulate physics to push them apart across several iterations.
|
||||
//it's rather slow, but shouldn't be too much of an issue when run in a different thread
|
||||
while(totalIterations++ < 40 && physicsIterations < maxPhysicsIterations){
|
||||
float spaceUsed = 0f;
|
||||
|
||||
if(compress){
|
||||
compressionIterations ++;
|
||||
|
||||
float maxDst = 1f, totalArea = 0f;
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
v1.set(positions[a * 2], positions[a * 2 + 1]).lerp(v2.set(0f, 0f), 0.3f);
|
||||
positions[a * 2] = v1.x;
|
||||
positions[a * 2 + 1] = v1.y;
|
||||
|
||||
float rad = units.get(a).hitSize * Vars.unitCollisionRadiusScale;
|
||||
|
||||
maxDst = Math.max(maxDst, v1.dst(0f, 0f) + rad);
|
||||
totalArea += Mathf.PI * rad * rad;
|
||||
}
|
||||
|
||||
//total area of bounding circle
|
||||
float boundingArea = Mathf.PI * maxDst * maxDst;
|
||||
spaceUsed = totalArea / boundingArea;
|
||||
|
||||
//ex: 60% (0.6) of the total area is used, this will not be enough to satisfy a maxSpaceUsage of 70% (0.7)
|
||||
compress = spaceUsed <= maxSpaceUsage && compressionIterations < 20;
|
||||
}
|
||||
|
||||
//uncompress units
|
||||
if(!compress || spaceUsed > 0.5f){
|
||||
physicsIterations++;
|
||||
|
||||
tree.clear();
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
tree.insert(a);
|
||||
}
|
||||
|
||||
for(int a = 0; a < units.size; a++){
|
||||
Unit unit = units.get(a);
|
||||
float x = positions[a * 2], y = positions[a * 2 + 1], radius = unit.hitSize/2f;
|
||||
|
||||
tmpseq.clear();
|
||||
tree.intersect(x - radius, y - radius, radius * 2f, radius * 2f, tmpseq);
|
||||
for(int res = 0; res < tmpseq.size; res ++){
|
||||
int b = tmpseq.items[res];
|
||||
|
||||
//simulate collision physics
|
||||
if(a != b){
|
||||
float ox = positions[b * 2], oy = positions[b * 2 + 1];
|
||||
Unit other = units.get(b);
|
||||
|
||||
float rs = (radius + other.hitSize/2f) * 1.2f;
|
||||
float dst = Mathf.dst(x, y, ox, oy);
|
||||
|
||||
if(dst < rs){
|
||||
v2.set(x - ox, y - oy).setLength(rs - dst);
|
||||
float mass1 = unit.hitSize, mass2 = other.hitSize;
|
||||
float ms = mass1 + mass2;
|
||||
float m1 = mass2 / ms, m2 = mass1 / ms;
|
||||
float scl = 1f;
|
||||
|
||||
positions[a * 2] += v2.x * m1 * scl;
|
||||
positions[a * 2 + 1] += v2.y * m1 * scl;
|
||||
|
||||
positions[b * 2] -= v2.x * m2 * scl;
|
||||
positions[b * 2 + 1] -= v2.y * m2 * scl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalPositions = positions.clone();
|
||||
|
||||
//raycast from the destination to the offset to make sure it's reachable
|
||||
for(int a = 0; a < units.size; a ++){
|
||||
updateRaycast(a, dest, v1);
|
||||
}
|
||||
|
||||
valid = true;
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> {
|
||||
for(int i = 0; i < units.size; i ++){
|
||||
float x = positions[i * 2], y = positions[i * 2 + 1];
|
||||
|
||||
Fx.placeBlock.at(x + dest.x, y + dest.y, 1f, Color.green);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateRaycast(int index, Vec2 dest){
|
||||
updateRaycast(index, dest, Tmp.v1);
|
||||
}
|
||||
|
||||
private void updateRaycast(int index, Vec2 dest, Vec2 v1){
|
||||
if(collisionLayer != PhysicsProcess.layerFlying){
|
||||
|
||||
//coordinates in world space
|
||||
float
|
||||
x = originalPositions[index * 2] + dest.x,
|
||||
y = originalPositions[index * 2 + 1] + dest.y;
|
||||
|
||||
Unit unit = units.get(index);
|
||||
|
||||
PathCost cost = unit.type.pathCost;
|
||||
int res = ControlPathfinder.raycastFastAvoid(unit.team.id, cost, World.toTile(dest.x), World.toTile(dest.y), World.toTile(x), World.toTile(y));
|
||||
|
||||
//collision found, make the destination the point right before the collision
|
||||
if(res != 0){
|
||||
v1.set(Point2.x(res) * Vars.tilesize - dest.x, Point2.y(res) * Vars.tilesize - dest.y);
|
||||
v1.setLength(Math.max(v1.len() - Vars.tilesize - 4f, 0));
|
||||
positions[index * 2] = v1.x;
|
||||
positions[index * 2 + 1] = v1.y;
|
||||
}
|
||||
|
||||
if(ControlPathfinder.showDebug){
|
||||
Core.app.post(() -> Fx.debugLine.at(unit.x, unit.y, 0f, Color.green, new Vec2[]{new Vec2(dest.x, dest.y), new Vec2(x, y)}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
|
||||
public class UnitStance extends MappableContent{
|
||||
/** @deprecated now a content type, use the methods in Vars.content instead */
|
||||
@Deprecated
|
||||
public static final Seq<UnitStance> all = new Seq<>();
|
||||
|
||||
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
|
||||
|
||||
/** Name of UI icon (from Icon class). */
|
||||
public final String icon;
|
||||
/** Key to press for this stance. */
|
||||
public @Nullable Binding keybind = null;
|
||||
|
||||
public UnitStance(String name, String icon, Binding keybind){
|
||||
super(name);
|
||||
this.icon = icon;
|
||||
this.keybind = keybind;
|
||||
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("stance." + name);
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(){
|
||||
return Icon.icons.get(icon, Icon.cancel);
|
||||
}
|
||||
|
||||
public char getEmoji() {
|
||||
return (char) Iconc.codes.get(icon, Iconc.cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.unitStance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "UnitStance:" + name;
|
||||
}
|
||||
|
||||
public static void loadAll(){
|
||||
stop = new UnitStance("stop", "cancel", Binding.cancel_orders);
|
||||
shoot = new UnitStance("shoot", "commandAttack", Binding.unit_stance_shoot);
|
||||
holdFire = new UnitStance("holdfire", "none", Binding.unit_stance_hold_fire);
|
||||
pursueTarget = new UnitStance("pursuetarget", "right", Binding.unit_stance_pursue_target);
|
||||
patrol = new UnitStance("patrol", "refresh", Binding.unit_stance_patrol);
|
||||
ram = new UnitStance("ram", "rightOpen", Binding.unit_stance_ram);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,9 @@ import mindustry.world.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class WaveSpawner{
|
||||
private static final float margin = 40f, coreMargin = tilesize * 2f, maxSteps = 30;
|
||||
private static final float margin = 0f, coreMargin = tilesize * 2f, maxSteps = 30;
|
||||
|
||||
private int tmpCount;
|
||||
private Seq<Tile> spawns = new Seq<>();
|
||||
private boolean spawning = false;
|
||||
private boolean any = false;
|
||||
@@ -55,7 +56,7 @@ public class WaveSpawner{
|
||||
public void spawnEnemies(){
|
||||
spawning = true;
|
||||
|
||||
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
|
||||
eachGroundSpawn(-1, (spawnX, spawnY, doShockwave) -> {
|
||||
if(doShockwave){
|
||||
doShockwave(spawnX, spawnY);
|
||||
}
|
||||
@@ -65,23 +66,33 @@ public class WaveSpawner{
|
||||
if(group.type == null) continue;
|
||||
|
||||
int spawned = group.getSpawned(state.wave - 1);
|
||||
if(spawned == 0) continue;
|
||||
|
||||
if(state.isCampaign()){
|
||||
//when spawning a boss, round down, so 1.5x (hard) * 1 boss does not result in 2 bosses
|
||||
spawned = Math.max(1, group.effect == StatusEffects.boss ?
|
||||
(int)(spawned * state.getPlanet().campaignRules.difficulty.enemySpawnMultiplier) :
|
||||
Mathf.round(spawned * state.getPlanet().campaignRules.difficulty.enemySpawnMultiplier));
|
||||
}
|
||||
|
||||
int spawnedf = spawned;
|
||||
|
||||
if(group.type.flying){
|
||||
float spread = margin / 1.5f;
|
||||
|
||||
eachFlyerSpawn((spawnX, spawnY) -> {
|
||||
for(int i = 0; i < spawned; i++){
|
||||
eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> {
|
||||
for(int i = 0; i < spawnedf; i++){
|
||||
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
|
||||
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
|
||||
unit.add();
|
||||
spawnEffect(unit);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
float spread = tilesize * 2;
|
||||
|
||||
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
|
||||
eachGroundSpawn(group.spawn, (spawnX, spawnY, doShockwave) -> {
|
||||
|
||||
for(int i = 0; i < spawned; i++){
|
||||
for(int i = 0; i < spawnedf; i++){
|
||||
Tmp.v1.rnd(spread);
|
||||
|
||||
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
|
||||
@@ -92,7 +103,7 @@ public class WaveSpawner{
|
||||
}
|
||||
}
|
||||
|
||||
Time.runTask(121f, () -> spawning = false);
|
||||
Time.run(121f, () -> spawning = false);
|
||||
}
|
||||
|
||||
public void doShockwave(float x, float y){
|
||||
@@ -101,12 +112,14 @@ public class WaveSpawner{
|
||||
}
|
||||
|
||||
public void eachGroundSpawn(Intc2 cons){
|
||||
eachGroundSpawn((x, y, shock) -> cons.get(World.toTile(x), World.toTile(y)));
|
||||
eachGroundSpawn(-1, (x, y, shock) -> cons.get(World.toTile(x), World.toTile(y)));
|
||||
}
|
||||
|
||||
private void eachGroundSpawn(SpawnConsumer cons){
|
||||
private void eachGroundSpawn(int filterPos, SpawnConsumer cons){
|
||||
if(state.hasSpawns()){
|
||||
for(Tile spawn : spawns){
|
||||
if(filterPos != -1 && filterPos != spawn.pos()) continue;
|
||||
|
||||
cons.accept(spawn.worldx(), spawn.worldy(), true);
|
||||
}
|
||||
}
|
||||
@@ -114,6 +127,8 @@ public class WaveSpawner{
|
||||
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam) && !state.teams.playerCores().isEmpty()){
|
||||
Building firstCore = state.teams.playerCores().first();
|
||||
for(Building core : state.rules.waveTeam.cores()){
|
||||
if(filterPos != -1 && filterPos != core.pos()) continue;
|
||||
|
||||
Tmp.v1.set(firstCore).sub(core).limit(coreMargin + core.block.size * tilesize /2f * Mathf.sqrt2);
|
||||
|
||||
boolean valid = false;
|
||||
@@ -146,28 +161,51 @@ public class WaveSpawner{
|
||||
}
|
||||
}
|
||||
|
||||
private void eachFlyerSpawn(Floatc2 cons){
|
||||
for(Tile tile : spawns){
|
||||
float angle = Angles.angle(world.width() / 2, world.height() / 2, tile.x, tile.y);
|
||||
private void eachFlyerSpawn(int filterPos, Floatc2 cons){
|
||||
boolean airUseSpawns = state.rules.airUseSpawns;
|
||||
|
||||
float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize;
|
||||
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin);
|
||||
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(angle, trns), -margin, world.height() * tilesize + margin);
|
||||
cons.get(spawnX, spawnY);
|
||||
for(Tile tile : spawns){
|
||||
if(filterPos != -1 && filterPos != tile.pos()) continue;
|
||||
|
||||
if(!airUseSpawns){
|
||||
|
||||
float angle = Angles.angle(world.width() / 2f, world.height() / 2f, tile.x, tile.y);
|
||||
float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize;
|
||||
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin);
|
||||
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(angle, trns), -margin, world.height() * tilesize + margin);
|
||||
cons.get(spawnX, spawnY);
|
||||
}else{
|
||||
cons.get(tile.worldx(), tile.worldy());
|
||||
}
|
||||
}
|
||||
|
||||
if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam)){
|
||||
for(Building core : state.teams.get(state.rules.waveTeam).cores){
|
||||
for(Building core : state.rules.waveTeam.data().cores){
|
||||
if(filterPos != -1 && filterPos != core.pos()) continue;
|
||||
|
||||
cons.get(core.x, core.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int countGroundSpawns(){
|
||||
tmpCount = 0;
|
||||
eachGroundSpawn((x, y) -> tmpCount ++);
|
||||
return tmpCount;
|
||||
}
|
||||
|
||||
public int countFlyerSpawns(){
|
||||
tmpCount = 0;
|
||||
eachFlyerSpawn(-1, (x, y) -> tmpCount ++);
|
||||
return tmpCount;
|
||||
}
|
||||
|
||||
public boolean isSpawning(){
|
||||
return spawning && !net.client();
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
public void reset(){
|
||||
spawning = false;
|
||||
spawns.clear();
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
@@ -177,9 +215,21 @@ public class WaveSpawner{
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnEffect(Unit unit){
|
||||
Call.spawnEffect(unit.x, unit.y, unit.type);
|
||||
Time.run(30f, unit::add);
|
||||
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
|
||||
public void spawnEffect(Unit unit){
|
||||
spawnEffect(unit, unit.angleTo(world.width()/2f * tilesize, world.height()/2f * tilesize));
|
||||
}
|
||||
|
||||
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
|
||||
public void spawnEffect(Unit unit, float rotation){
|
||||
unit.rotation = rotation;
|
||||
unit.apply(StatusEffects.unmoving, 30f);
|
||||
unit.apply(StatusEffects.invincible, 60f);
|
||||
unit.add();
|
||||
unit.unloaded();
|
||||
|
||||
Events.fire(new UnitSpawnEvent(unit));
|
||||
Call.spawnEffect(unit.x, unit.y, unit.rotation, unit.type);
|
||||
}
|
||||
|
||||
private interface SpawnConsumer{
|
||||
@@ -187,8 +237,9 @@ public class WaveSpawner{
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void spawnEffect(float x, float y, UnitType type){
|
||||
Fx.unitSpawn.at(x, y, 0f, type);
|
||||
public static void spawnEffect(float x, float y, float rotation, UnitType u){
|
||||
|
||||
Fx.unitSpawn.at(x, y, rotation, u);
|
||||
|
||||
Time.run(30f, () -> Fx.spawn.at(x, y));
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
|
||||
import arc.struct.*;
|
||||
|
||||
/**
|
||||
* {@code BoundedSlotAssignmentStrategy} is an abstract implementation of {@link SlotAssignmentStrategy} that supports roles.
|
||||
* Generally speaking, there are hard and soft roles. Hard roles cannot be broken, soft roles can.
|
||||
* <p>
|
||||
* This abstract class provides an implementation of the {@link #calculateNumberOfSlots(Seq) calculateNumberOfSlots} method that
|
||||
* is more general (and costly) than the simplified implementation in {@link FreeSlotAssignmentStrategy}. It scans the assignment
|
||||
* list to find the number of filled slots, which is the highest slot number in the assignments.
|
||||
* @author davebaol
|
||||
*/
|
||||
public abstract class BoundedSlotAssignmentStrategy implements SlotAssignmentStrategy{
|
||||
|
||||
@Override
|
||||
public abstract void updateSlotAssignments(Seq<SlotAssignment> assignments);
|
||||
|
||||
@Override
|
||||
public int calculateNumberOfSlots(Seq<SlotAssignment> assignments){
|
||||
// Find the number of filled slots: it will be the
|
||||
// highest slot number in the assignments
|
||||
int filledSlots = -1;
|
||||
for(int i = 0; i < assignments.size; i++){
|
||||
SlotAssignment assignment = assignments.get(i);
|
||||
if(assignment.slotNumber >= filledSlots) filledSlots = assignment.slotNumber;
|
||||
}
|
||||
|
||||
// Add one to go from the index of the highest slot to the number of slots needed.
|
||||
return filledSlots + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSlotAssignment(Seq<SlotAssignment> assignments, int index){
|
||||
int sn = assignments.get(index).slotNumber;
|
||||
for(int i = 0; i < assignments.size; i++){
|
||||
SlotAssignment sa = assignments.get(i);
|
||||
if(sa.slotNumber >= sn) sa.slotNumber--;
|
||||
}
|
||||
assignments.remove(index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
|
||||
public class DistanceAssignmentStrategy implements SlotAssignmentStrategy{
|
||||
private final Vec3 vec = new Vec3();
|
||||
private final FormationPattern form;
|
||||
|
||||
public DistanceAssignmentStrategy(FormationPattern form){
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSlotAssignments(Seq<SlotAssignment> assignments){
|
||||
IntSeq slots = IntSeq.range(0, assignments.size);
|
||||
|
||||
for(SlotAssignment slot : assignments){
|
||||
int mindex = 0;
|
||||
float mcost = Float.MAX_VALUE;
|
||||
|
||||
for(int i = 0; i < slots.size; i++){
|
||||
float cost = cost(slot.member, slots.get(i));
|
||||
if(cost < mcost){
|
||||
mcost = cost;
|
||||
mindex = i;
|
||||
}
|
||||
}
|
||||
|
||||
slot.slotNumber = slots.get(mindex);
|
||||
slots.removeIndex(mindex);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculateNumberOfSlots(Seq<SlotAssignment> assignments){
|
||||
return assignments.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSlotAssignment(Seq<SlotAssignment> assignments, int index){
|
||||
assignments.remove(index);
|
||||
}
|
||||
|
||||
float cost(FormationMember member, int slot){
|
||||
form.calculateSlotLocation(vec, slot);
|
||||
return Mathf.dst2(member.formationPos().x, member.formationPos().y, vec.x, vec.y);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
|
||||
/**
|
||||
* A {@code Formation} coordinates the movement of a group of characters so that they retain some group organization. Characters
|
||||
* belonging to a formation must implement the {@link FormationMember} interface. At its simplest, a formation can consist of
|
||||
* moving in a fixed geometric pattern such as a V or line abreast, but it is not limited to that. Formations can also make use of
|
||||
* the environment. Squads of characters can move between cover points using formation steering with only minor modifications, for
|
||||
* example.
|
||||
* <p>
|
||||
* Formation motion is used in team sports games, squad-based games, real-time strategy games, and sometimes in first-person
|
||||
* shooters, driving games, and action adventures too. It is a simple and flexible technique that is much quicker to write and
|
||||
* execute and can produce much more stable behavior than collaborative tactical decision making.
|
||||
* @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 */
|
||||
public FormationPattern pattern;
|
||||
/** The strategy used to assign a member to his slot */
|
||||
public SlotAssignmentStrategy slotAssignmentStrategy;
|
||||
/** The formation motion moderator */
|
||||
public FormationMotionModerator motionModerator;
|
||||
|
||||
private final Vec2 positionOffset;
|
||||
private final Mat orientationMatrix = new Mat();
|
||||
|
||||
/** The location representing the drift offset for the currently filled slots. */
|
||||
private final Vec3 driftOffset;
|
||||
|
||||
/**
|
||||
* Creates a {@code Formation} for the specified {@code pattern} using a {@link FreeSlotAssignmentStrategy} and no motion
|
||||
* moderator.
|
||||
* @param anchor the anchor point of this formation, Cannot be {@code null}.
|
||||
* @param pattern the pattern of this formation
|
||||
* @throws IllegalArgumentException if the anchor point is {@code null}
|
||||
*/
|
||||
public Formation(Vec3 anchor, FormationPattern pattern){
|
||||
this(anchor, pattern, new FreeSlotAssignmentStrategy(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code Formation} for the specified {@code pattern} and {@code slotAssignmentStrategy} using no motion moderator.
|
||||
* @param anchor the anchor point of this formation, Cannot be {@code null}.
|
||||
* @param pattern the pattern of this formation
|
||||
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
|
||||
* @throws IllegalArgumentException if the anchor point is {@code null}
|
||||
*/
|
||||
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy){
|
||||
this(anchor, pattern, slotAssignmentStrategy, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code Formation} for the specified {@code pattern}, {@code slotAssignmentStrategy} and {@code moderator}.
|
||||
* @param anchor the anchor point of this formation, Cannot be {@code null}.
|
||||
* @param pattern the pattern of this formation
|
||||
* @param slotAssignmentStrategy the strategy used to assign a member to his slot
|
||||
* @param motionModerator the motion moderator. Can be {@code null} if moderation is not needed
|
||||
* @throws IllegalArgumentException if the anchor point is {@code null}
|
||||
*/
|
||||
public Formation(Vec3 anchor, FormationPattern pattern, SlotAssignmentStrategy slotAssignmentStrategy,
|
||||
FormationMotionModerator motionModerator){
|
||||
if(anchor == null) throw new IllegalArgumentException("The anchor point cannot be null");
|
||||
this.anchor = anchor;
|
||||
this.pattern = pattern;
|
||||
this.slotAssignmentStrategy = slotAssignmentStrategy;
|
||||
this.motionModerator = motionModerator;
|
||||
|
||||
this.slotAssignments = new Seq<>();
|
||||
this.driftOffset = new Vec3();
|
||||
this.positionOffset = new Vec2(anchor.x, anchor.y).cpy();
|
||||
}
|
||||
|
||||
/** Updates the assignment of members to slots */
|
||||
public void updateSlotAssignments(){
|
||||
pattern.slots = slotAssignments.size;
|
||||
|
||||
// Apply the strategy to update slot assignments
|
||||
slotAssignmentStrategy.updateSlotAssignments(slotAssignments);
|
||||
|
||||
// Set the newly calculated number of slots
|
||||
pattern.slots = slotAssignmentStrategy.calculateNumberOfSlots(slotAssignments);
|
||||
|
||||
// Update the drift offset if a motion moderator is set
|
||||
if(motionModerator != null) motionModerator.calculateDriftOffset(driftOffset, slotAssignments, pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the pattern of this formation and updates slot assignments if the number of member is supported by the given
|
||||
* pattern.
|
||||
* @param pattern the pattern to set
|
||||
* @return {@code true} if the pattern has effectively changed; {@code false} otherwise.
|
||||
*/
|
||||
public boolean changePattern(FormationPattern pattern){
|
||||
// Find out how many slots we have occupied
|
||||
int occupiedSlots = slotAssignments.size;
|
||||
|
||||
// Check if the pattern supports one more slot
|
||||
if(pattern.supportsSlots(occupiedSlots)){
|
||||
this.pattern = pattern;
|
||||
|
||||
// Update the slot assignments and return success
|
||||
updateSlotAssignments();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Much more efficient than adding a single member.
|
||||
* @return number of members added. */
|
||||
public int addMembers(Iterable<? extends FormationMember> members){
|
||||
int added = 0;
|
||||
for(FormationMember member : members){
|
||||
if(pattern.supportsSlots(slotAssignments.size + 1)){
|
||||
slotAssignments.add(new SlotAssignment(member, slotAssignments.size));
|
||||
added ++;
|
||||
}
|
||||
}
|
||||
|
||||
updateSlotAssignments();
|
||||
return added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new member to the first available slot and updates slot assignments if the number of member is supported by the
|
||||
* current pattern.
|
||||
* @param member the member to add
|
||||
* @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
|
||||
slotAssignments.add(new SlotAssignment(member, slotAssignments.size));
|
||||
|
||||
// Update the slot assignments and return success
|
||||
updateSlotAssignments();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a member from its slot and updates slot assignments.
|
||||
* @param member the member to remove
|
||||
*/
|
||||
public void removeMember(FormationMember member){
|
||||
// Find the member's slot
|
||||
int slot = findMemberSlot(member);
|
||||
|
||||
// Make sure we've found a valid result
|
||||
if(slot >= 0){
|
||||
// Remove the slot
|
||||
// slotAssignments.removeIndex(slot);
|
||||
slotAssignmentStrategy.removeSlotAssignment(slotAssignments, slot);
|
||||
|
||||
// Update the assignments
|
||||
updateSlotAssignments();
|
||||
}
|
||||
}
|
||||
|
||||
private int findMemberSlot(FormationMember member){
|
||||
for(int i = 0; i < slotAssignments.size; i++){
|
||||
if(slotAssignments.get(i).member == member) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// debug
|
||||
public SlotAssignment getSlotAssignmentAt(int index){
|
||||
return slotAssignments.get(index);
|
||||
}
|
||||
|
||||
// debug
|
||||
public int getSlotAssignmentCount(){
|
||||
return slotAssignments.size;
|
||||
}
|
||||
|
||||
/** Writes new slot locations to each member */
|
||||
public void updateSlots(){
|
||||
positionOffset.set(anchor);
|
||||
float orientationOffset = anchor.z;
|
||||
if(motionModerator != null){
|
||||
positionOffset.sub(driftOffset);
|
||||
orientationOffset -= driftOffset.z;
|
||||
}
|
||||
|
||||
// Get the orientation of the anchor point as a matrix
|
||||
orientationMatrix.idt().rotate(anchor.z);
|
||||
|
||||
// Go through each member in turn
|
||||
for(int i = 0; i < slotAssignments.size; i++){
|
||||
SlotAssignment slotAssignment = slotAssignments.get(i);
|
||||
|
||||
// Retrieve the location reference of the formation member to calculate the new value
|
||||
Vec3 relativeLoc = slotAssignment.member.formationPos();
|
||||
float z = relativeLoc.z;
|
||||
|
||||
// Ask for the location of the slot relative to the anchor point
|
||||
pattern.calculateSlotLocation(relativeLoc, slotAssignment.slotNumber);
|
||||
|
||||
// Transform it by the anchor point's position and orientation
|
||||
relativeLoc.mul(orientationMatrix);
|
||||
|
||||
// Add the anchor and drift components
|
||||
relativeLoc.add(positionOffset.x, positionOffset.y, 0);
|
||||
relativeLoc.z = z + orientationOffset;
|
||||
}
|
||||
|
||||
// Possibly reset the anchor point if a moderator is set
|
||||
if(motionModerator != null){
|
||||
motionModerator.updateAnchorPoint(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.math.geom.*;
|
||||
|
||||
/**
|
||||
* Game characters coordinated by a {@link Formation} must implement this interface. Any {@code FormationMember} has a target
|
||||
* location which is the place where it should be in order to stay in formation. This target location is calculated by the
|
||||
* formation itself.
|
||||
* @author davebaol
|
||||
*/
|
||||
public interface FormationMember{
|
||||
/** Returns the target location of this formation member. */
|
||||
Vec3 formationPos();
|
||||
|
||||
float formationSize();
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
|
||||
/**
|
||||
* A {@code FormationMotionModerator} moderates the movement of the formation based on the current positions of the members in its
|
||||
* slots: in effect to keep the anchor point on a leash. If the members in the slots are having trouble reaching their targets,
|
||||
* then the formation as a whole should be held back to give them a chance to catch up.
|
||||
* @author davebaol
|
||||
*/
|
||||
public abstract class FormationMotionModerator{
|
||||
private Vec3 tempLocation;
|
||||
|
||||
/**
|
||||
* Update the anchor point to moderate formation motion. This method is called at each frame.
|
||||
* @param anchor the anchor point
|
||||
*/
|
||||
public abstract void updateAnchorPoint(Vec3 anchor);
|
||||
|
||||
/**
|
||||
* Calculates the drift offset when members are in the given set of slots for the specified pattern.
|
||||
* @param centerOfMass the output location set to the calculated drift offset
|
||||
* @param slotAssignments the set of slots
|
||||
* @param pattern the pattern
|
||||
* @return the given location for chaining.
|
||||
*/
|
||||
public Vec3 calculateDriftOffset(Vec3 centerOfMass, Seq<SlotAssignment> slotAssignments, FormationPattern pattern){
|
||||
// Clear the center of mass
|
||||
centerOfMass.x = centerOfMass.y = 0;
|
||||
float centerOfMassOrientation = 0;
|
||||
|
||||
// Make sure tempLocation is instantiated
|
||||
if(tempLocation == null) tempLocation = new Vec3();
|
||||
|
||||
// Go through each assignment and add its contribution to the center
|
||||
float numberOfAssignments = slotAssignments.size;
|
||||
for(int i = 0; i < numberOfAssignments; i++){
|
||||
pattern.calculateSlotLocation(tempLocation, slotAssignments.get(i).slotNumber);
|
||||
centerOfMass.add(tempLocation);
|
||||
centerOfMassOrientation += tempLocation.z;
|
||||
}
|
||||
|
||||
// Divide through to get the drift offset.
|
||||
centerOfMass.scl(1f / numberOfAssignments);
|
||||
centerOfMassOrientation /= numberOfAssignments;
|
||||
centerOfMass.z = centerOfMassOrientation;
|
||||
|
||||
return centerOfMass;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.math.geom.*;
|
||||
|
||||
/**
|
||||
* The {@code FormationPattern} interface represents the shape of a formation and generates the slot offsets, relative to its
|
||||
* anchor point. Since formations can be scalable the pattern must be able to determine if a given number of slots is supported.
|
||||
* <p>
|
||||
* Each particular pattern (such as a V, wedge, circle) needs its own instance of a class that implements this
|
||||
* {@code FormationPattern} interface.
|
||||
* @author davebaol
|
||||
*/
|
||||
public abstract class FormationPattern{
|
||||
public int slots;
|
||||
/** Spacing between members. */
|
||||
public float spacing = 20f;
|
||||
|
||||
/** Returns the location of the given slot index. */
|
||||
public abstract Vec3 calculateSlotLocation(Vec3 out, int slot);
|
||||
|
||||
/**
|
||||
* Returns true if the pattern can support the given number of slots
|
||||
* @param slotCount the number of slots
|
||||
* @return {@code true} if this pattern can support the given number of slots; {@code false} othervwise.
|
||||
*/
|
||||
public boolean supportsSlots(int slotCount){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
|
||||
import arc.struct.*;
|
||||
|
||||
/**
|
||||
* {@code FreeSlotAssignmentStrategy} is the simplest implementation of {@link SlotAssignmentStrategy}. It simply go through
|
||||
* each assignment in the list and assign sequential slot numbers. The number of slots is just the length of the list.
|
||||
* <p>
|
||||
* Because each member can occupy any slot this implementation does not support roles.
|
||||
* @author davebaol
|
||||
*/
|
||||
public class FreeSlotAssignmentStrategy implements SlotAssignmentStrategy{
|
||||
|
||||
@Override
|
||||
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++){
|
||||
assignments.get(i).slotNumber = i;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculateNumberOfSlots(Seq<SlotAssignment> assignments){
|
||||
return assignments.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSlotAssignment(Seq<SlotAssignment> assignments, int index){
|
||||
assignments.remove(index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
|
||||
/**
|
||||
* A {@code SlotAssignment} instance represents the assignment of a single {@link FormationMember} to its slot in the
|
||||
* {@link Formation}.
|
||||
* @author davebaol
|
||||
*/
|
||||
public class SlotAssignment{
|
||||
public FormationMember member;
|
||||
public int slotNumber;
|
||||
|
||||
/**
|
||||
* Creates a {@code SlotAssignment} for the given {@code member}.
|
||||
* @param member the member of this slot assignment
|
||||
*/
|
||||
public SlotAssignment(FormationMember member){
|
||||
this(member, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code SlotAssignment} for the given {@code member} and {@code slotNumber}.
|
||||
* @param member the member of this slot assignment
|
||||
*/
|
||||
public SlotAssignment(FormationMember member, int slotNumber){
|
||||
this.member = member;
|
||||
this.slotNumber = slotNumber;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
import arc.struct.*;
|
||||
|
||||
/**
|
||||
* This interface defines how each {@link FormationMember} is assigned to a slot in the {@link Formation}.
|
||||
* @author davebaol
|
||||
*/
|
||||
public interface SlotAssignmentStrategy{
|
||||
|
||||
/** Updates the assignment of members to slots */
|
||||
void updateSlotAssignments(Seq<SlotAssignment> assignments);
|
||||
|
||||
/** Calculates the number of slots from the assignment data. */
|
||||
int calculateNumberOfSlots(Seq<SlotAssignment> assignments);
|
||||
|
||||
/** Removes the slot assignment at the specified index. */
|
||||
void removeSlotAssignment(Seq<SlotAssignment> assignments, int index);
|
||||
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package mindustry.ai.formations;
|
||||
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
|
||||
/**
|
||||
* {@code SoftRoleSlotAssignmentStrategy} is a concrete implementation of {@link BoundedSlotAssignmentStrategy} that supports soft
|
||||
* roles, i.e. roles that can be broken. Rather than a member having a list of roles it can fulfill, it has a set of values
|
||||
* representing how difficult it would find it to fulfill every role. The value is known as the slot cost. To make a slot
|
||||
* impossible for a member to fill, its slot cost should be infinite (you can even set a threshold to ignore all slots whose cost
|
||||
* is too high; this will reduce computation time when several costs are exceeding). To make a slot ideal for a member, its slot
|
||||
* cost should be zero. We can have different levels of unsuitable assignment for one member.
|
||||
* <p>
|
||||
* Slot costs do not necessarily have to depend only on the member and the slot roles. They can be generalized to include any
|
||||
* difficulty a member might have in taking up a slot. If a formation is spread out, for example, a member may choose a slot that
|
||||
* is close by over a more distant slot. Distance can be directly used as a slot cost.
|
||||
* <p>
|
||||
* <b>IMPORTANVec2 NOTES:</b>
|
||||
* <ul>
|
||||
* <li>In order for the algorithm to work properly the slot costs can not be negative.</li>
|
||||
* <li>This algorithm is often not fast enough to be used regularly. However, slot assignment happens relatively seldom (when the
|
||||
* player selects a new pattern, for example, or adds a member to the formation, or a member is removed from the formation).</li>
|
||||
* </ul>
|
||||
* @author davebaol
|
||||
*/
|
||||
public class SoftRoleSlotAssignmentStrategy extends BoundedSlotAssignmentStrategy{
|
||||
protected SlotCostProvider slotCostProvider;
|
||||
protected float costThreshold;
|
||||
private BoolSeq filledSlots;
|
||||
|
||||
/**
|
||||
* Creates a {@code SoftRoleSlotAssignmentStrategy} with the given slot cost provider and no cost threshold.
|
||||
* @param slotCostProvider the slot cost provider
|
||||
*/
|
||||
public SoftRoleSlotAssignmentStrategy(SlotCostProvider slotCostProvider){
|
||||
this(slotCostProvider, Float.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code SoftRoleSlotAssignmentStrategy} with the given slot cost provider and cost threshold.
|
||||
* @param slotCostProvider the slot cost provider
|
||||
* @param costThreshold is a slot-cost limit, beyond which a slot is considered to be too expensive to consider occupying.
|
||||
*/
|
||||
public SoftRoleSlotAssignmentStrategy(SlotCostProvider slotCostProvider, float costThreshold){
|
||||
this.slotCostProvider = slotCostProvider;
|
||||
this.costThreshold = costThreshold;
|
||||
|
||||
this.filledSlots = new BoolSeq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSlotAssignments(Seq<SlotAssignment> assignments){
|
||||
// Holds a list of member and slot data for each member.
|
||||
Seq<MemberAndSlots> memberData = new Seq<>();
|
||||
|
||||
// Compile the member data
|
||||
int numberOfAssignments = assignments.size;
|
||||
for(int i = 0; i < numberOfAssignments; i++){
|
||||
SlotAssignment assignment = assignments.get(i);
|
||||
|
||||
// Create a new member datum, and fill it
|
||||
MemberAndSlots datum = new MemberAndSlots(assignment.member);
|
||||
|
||||
// Add each valid slot to it
|
||||
for(int j = 0; j < numberOfAssignments; j++){
|
||||
|
||||
// Get the cost of the slot
|
||||
float cost = slotCostProvider.getCost(assignment.member, j);
|
||||
|
||||
// Make sure the slot is valid
|
||||
if(cost >= costThreshold) continue;
|
||||
|
||||
SlotAssignment slot = assignments.get(j);
|
||||
|
||||
// Store the slot information
|
||||
CostAndSlot slotDatum = new CostAndSlot(cost, slot.slotNumber);
|
||||
datum.costAndSlots.add(slotDatum);
|
||||
|
||||
// Add it to the member's ease of assignment
|
||||
datum.assignmentEase += 1f / (1f + cost);
|
||||
}
|
||||
|
||||
// Add member datum
|
||||
memberData.add(datum);
|
||||
}
|
||||
|
||||
// Reset the array to keep track of which slots we have already filled.
|
||||
if(numberOfAssignments > filledSlots.size) filledSlots.ensureCapacity(numberOfAssignments - filledSlots.size);
|
||||
filledSlots.size = numberOfAssignments;
|
||||
for(int i = 0; i < numberOfAssignments; i++)
|
||||
filledSlots.set(i, false);
|
||||
|
||||
// Arrange members in order of ease of assignment, with the least easy first.
|
||||
memberData.sort();
|
||||
MEMBER_LOOP:
|
||||
for(int i = 0; i < memberData.size; i++){
|
||||
MemberAndSlots memberDatum = memberData.get(i);
|
||||
|
||||
// Choose the first slot in the list that is still empty (non-filled)
|
||||
memberDatum.costAndSlots.sort();
|
||||
int m = memberDatum.costAndSlots.size;
|
||||
for(int j = 0; j < m; j++){
|
||||
int slotNumber = memberDatum.costAndSlots.get(j).slotNumber;
|
||||
|
||||
// Check if this slot is valid
|
||||
if(!filledSlots.get(slotNumber)){
|
||||
// Fill this slot
|
||||
SlotAssignment slot = assignments.get(slotNumber);
|
||||
slot.member = memberDatum.member;
|
||||
slot.slotNumber = slotNumber;
|
||||
|
||||
// Reserve the slot
|
||||
filledSlots.set(slotNumber, true);
|
||||
|
||||
// Go to the next member
|
||||
continue MEMBER_LOOP;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, it's because a member has no valid assignment.
|
||||
//
|
||||
// TODO
|
||||
// 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>{
|
||||
float cost;
|
||||
int slotNumber;
|
||||
|
||||
public CostAndSlot(float cost, int slotNumber){
|
||||
this.cost = cost;
|
||||
this.slotNumber = slotNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CostAndSlot other){
|
||||
return Float.compare(cost, other.cost);
|
||||
}
|
||||
}
|
||||
|
||||
static class MemberAndSlots implements Comparable<MemberAndSlots>{
|
||||
FormationMember member;
|
||||
float assignmentEase;
|
||||
Seq<CostAndSlot> costAndSlots;
|
||||
|
||||
public MemberAndSlots(FormationMember member){
|
||||
this.member = member;
|
||||
this.assignmentEase = 0f;
|
||||
this.costAndSlots = new Seq<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MemberAndSlots other){
|
||||
return Float.compare(assignmentEase, other.assignmentEase);
|
||||
}
|
||||
}
|
||||
|
||||
public interface SlotCostProvider{
|
||||
float getCost(FormationMember member, int slotNumber);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package mindustry.ai.formations.patterns;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.ai.formations.*;
|
||||
|
||||
public class CircleFormation extends FormationPattern{
|
||||
/** Angle offset. */
|
||||
public float angleOffset = 0;
|
||||
|
||||
@Override
|
||||
public Vec3 calculateSlotLocation(Vec3 outLocation, int slotNumber){
|
||||
if(slots > 1){
|
||||
float angle = (360f * slotNumber) / slots;
|
||||
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, spacing * 1.1f, 360f * slotNumber);
|
||||
}
|
||||
|
||||
outLocation.z += angleOffset;
|
||||
|
||||
return outLocation;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package mindustry.ai.formations.patterns;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.ai.formations.*;
|
||||
|
||||
public class SquareFormation extends FormationPattern{
|
||||
|
||||
@Override
|
||||
public Vec3 calculateSlotLocation(Vec3 out, int slot){
|
||||
//side of each square of formation
|
||||
int side = Mathf.ceil(Mathf.sqrt(slots + 1));
|
||||
int cx = slot % side, cy = slot / side;
|
||||
|
||||
//don't hog the middle spot
|
||||
if(cx == side /2 && cy == side/2 && (side%2)==1){
|
||||
slot = slots;
|
||||
|
||||
cx = slot % side;
|
||||
cy = slot / side;
|
||||
}
|
||||
|
||||
return out.set(cx - (side/2f - 0.5f), cy - (side/2f - 0.5f), 0).scl(spacing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.entities.units.*;
|
||||
|
||||
public class AssemblerAI extends AIController{
|
||||
public Vec2 targetPos = new Vec2();
|
||||
public float targetAngle;
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(!targetPos.isZero()){
|
||||
moveTo(targetPos, 1f, 3f);
|
||||
}
|
||||
|
||||
if(unit.within(targetPos, 5f)){
|
||||
unit.lookAt(targetAngle);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean inPosition(){
|
||||
return unit.within(targetPos, 10f) && Angles.within(unit.rotation, targetAngle, 15f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.units.*;
|
||||
|
||||
//not meant to be used outside RTS-AI-controlled units
|
||||
public class BoostAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
if(unit.controller() instanceof CommandAI ai){
|
||||
ai.defaultBehavior();
|
||||
unit.updateBoosting(true);
|
||||
|
||||
//auto land when near target
|
||||
if(ai.attackTarget != null && unit.within(ai.attackTarget, unit.range())){
|
||||
unit.command().command(UnitCommand.moveCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.entities.*;
|
||||
@@ -13,20 +12,55 @@ import mindustry.world.blocks.ConstructBlock.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class BuilderAI extends AIController{
|
||||
float buildRadius = 1500;
|
||||
public static float buildRadius = 1500, retreatDst = 110f, retreatDelay = Time.toSeconds * 2f, defaultRebuildPeriod = 60f * 2f;
|
||||
|
||||
public @Nullable Unit assistFollowing;
|
||||
public @Nullable Unit following;
|
||||
public @Nullable Teamc enemy;
|
||||
public @Nullable BlockPlan lastPlan;
|
||||
|
||||
public float fleeRange = 370f, rebuildPeriod = defaultRebuildPeriod;
|
||||
public boolean alwaysFlee;
|
||||
public boolean onlyAssist;
|
||||
|
||||
boolean found = false;
|
||||
@Nullable Unit following;
|
||||
float retreatTimer;
|
||||
|
||||
public BuilderAI(boolean alwaysFlee, float fleeRange){
|
||||
this.alwaysFlee = alwaysFlee;
|
||||
this.fleeRange = fleeRange;
|
||||
}
|
||||
|
||||
public BuilderAI(){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
//rebuild much faster with buildAI; there are usually few builder units so this is fine
|
||||
if(rebuildPeriod == defaultRebuildPeriod && unit.team.rules().buildAi){
|
||||
rebuildPeriod = 10f;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
|
||||
if(target != null && shouldShoot()){
|
||||
unit.lookAt(target);
|
||||
}else if(!unit.type.flying){
|
||||
unit.lookAt(unit.prefRotation());
|
||||
}
|
||||
|
||||
unit.updateBuilding = true;
|
||||
|
||||
if(assistFollowing != null && assistFollowing.activelyBuilding()){
|
||||
following = assistFollowing;
|
||||
}
|
||||
|
||||
boolean moving = false;
|
||||
|
||||
if(following != null){
|
||||
retreatTimer = 0f;
|
||||
//try to follow and mimic someone
|
||||
|
||||
//validate follower
|
||||
@@ -39,39 +73,68 @@ public class BuilderAI extends AIController{
|
||||
//set to follower's first build plan, whatever that is
|
||||
unit.plans.clear();
|
||||
unit.plans.addFirst(following.buildPlan());
|
||||
lastPlan = null;
|
||||
}else if(unit.buildPlan() == null || alwaysFlee){
|
||||
//not following anyone or building
|
||||
if(timer.get(timerTarget4, 40)){
|
||||
enemy = target(unit.x, unit.y, fleeRange, true, true);
|
||||
}
|
||||
|
||||
//fly away from enemy when not doing anything, but only after a delay
|
||||
if((retreatTimer += Time.delta) >= retreatDelay || alwaysFlee){
|
||||
if(enemy != null){
|
||||
unit.clearBuilding();
|
||||
var core = unit.closestCore();
|
||||
if(core != null && !unit.within(core, retreatDst)){
|
||||
moveTo(core, retreatDst);
|
||||
moving = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(unit.buildPlan() != null){
|
||||
//approach request if building
|
||||
if(!alwaysFlee) retreatTimer = 0f;
|
||||
//approach plan if building
|
||||
BuildPlan req = unit.buildPlan();
|
||||
|
||||
//clear break plan if another player is breaking something.
|
||||
//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();
|
||||
//remove from list of plans
|
||||
unit.team.data().plans.remove(p -> p.x == req.x && p.y == req.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean valid =
|
||||
(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));
|
||||
!(lastPlan != null && lastPlan.removed) &&
|
||||
((req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.current == req.block) ||
|
||||
(req.breaking ?
|
||||
Build.validBreak(unit.team(), req.x, req.y) :
|
||||
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
|
||||
|
||||
if(valid){
|
||||
//move toward the request
|
||||
moveTo(req.tile(), buildingRange - 20f);
|
||||
//move toward the plan
|
||||
moveTo(req.tile(), unit.type.buildRange - 20f, 20f);
|
||||
moving = !unit.within(req.tile(), unit.type.buildRange - 10f);
|
||||
}else{
|
||||
//discard invalid request
|
||||
//discard invalid plan
|
||||
unit.plans.removeFirst();
|
||||
lastPlan = null;
|
||||
}
|
||||
}else{
|
||||
|
||||
if(assistFollowing != null){
|
||||
moveTo(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 60f);
|
||||
moving = !unit.within(assistFollowing, assistFollowing.type.hitSize + unit.type.hitSize/2f + 65f);
|
||||
}
|
||||
|
||||
//follow someone and help them build
|
||||
if(timer.get(timerTarget2, 60f)){
|
||||
if(timer.get(timerTarget2, 20f)){
|
||||
found = false;
|
||||
|
||||
Units.nearby(unit.team, unit.x, unit.y, buildRadius, u -> {
|
||||
@@ -82,9 +145,9 @@ public class BuilderAI extends AIController{
|
||||
|
||||
Building build = world.build(plan.x, plan.y);
|
||||
if(build instanceof ConstructBuild cons){
|
||||
float dist = Math.min(cons.dst(unit) - buildingRange, 0);
|
||||
float dist = Math.min(cons.dst(unit) - unit.type.buildRange, 0);
|
||||
|
||||
//make sure you can reach the request in time
|
||||
//make sure you can reach the plan in time
|
||||
if(dist / unit.speed() < cons.buildCost * 0.9f){
|
||||
following = u;
|
||||
found = true;
|
||||
@@ -92,30 +155,52 @@ public class BuilderAI extends AIController{
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(onlyAssist){
|
||||
float minDst = Float.MAX_VALUE;
|
||||
Player closest = null;
|
||||
for(var player : Groups.player){
|
||||
if(!player.dead() && player.isBuilder() && player.team() == unit.team){
|
||||
float dst = player.dst2(unit);
|
||||
if(dst < minDst){
|
||||
closest = player;
|
||||
minDst = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assistFollowing = closest == null ? null : closest.unit();
|
||||
}
|
||||
}
|
||||
|
||||
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, rebuildTime)){
|
||||
Queue<BlockPlan> blocks = unit.team.data().blocks;
|
||||
//find new plan
|
||||
if(!onlyAssist && !unit.team.data().plans.isEmpty() && following == null && timer.get(timerTarget3, rebuildPeriod)){
|
||||
Queue<BlockPlan> blocks = unit.team.data().plans;
|
||||
BlockPlan block = blocks.first();
|
||||
|
||||
//check if it's already been placed
|
||||
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block().id == block.block){
|
||||
if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){
|
||||
blocks.removeFirst();
|
||||
}else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation)){ //it's valid.
|
||||
//add build request.
|
||||
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.
|
||||
}else if(Build.validPlace(block.block, unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid
|
||||
lastPlan = block;
|
||||
//add build plan
|
||||
unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, 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);
|
||||
blocks.addLast(blocks.removeFirst());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!unit.type.flying){
|
||||
unit.updateBoosting(unit.type.boostWhenBuilding || moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep());
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean nearEnemy(int x, int y){
|
||||
return Units.nearEnemy(unit.team, x * tilesize - fleeRange/2f, y * tilesize - fleeRange/2f, fleeRange, fleeRange);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,11 +210,11 @@ public class BuilderAI extends AIController{
|
||||
|
||||
@Override
|
||||
public boolean useFallback(){
|
||||
return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().ai;
|
||||
return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().rtsAi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShoot(){
|
||||
return !unit.isBuilding();
|
||||
return !unit.isBuilding() && unit.type.canAttack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.blocks.units.UnitCargoUnloadPoint.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class CargoAI extends AIController{
|
||||
static Seq<Item> orderedItems = new Seq<>();
|
||||
static Seq<UnitCargoUnloadPointBuild> targets = new Seq<>();
|
||||
|
||||
public static float emptyWaitTime = 60f * 2f, dropSpacing = 60f * 1.5f;
|
||||
public static float transferRange = 20f, moveRange = 6f, moveSmoothing = 20f;
|
||||
|
||||
public @Nullable UnitCargoUnloadPointBuild unloadTarget;
|
||||
public @Nullable Item itemTarget;
|
||||
public float noDestTimer = 0f;
|
||||
public int targetIndex = 0;
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(!(unit instanceof BuildingTetherc tether) || tether.building() == null) return;
|
||||
|
||||
var build = tether.building();
|
||||
|
||||
if(build.items == null) return;
|
||||
|
||||
//empty, approach the loader, even if there's nothing to pick up (units hanging around doing nothing looks bad)
|
||||
if(!unit.hasItem()){
|
||||
moveTo(build, moveRange, moveSmoothing);
|
||||
|
||||
//check if ready to pick up
|
||||
if(build.items.any() && unit.within(build, transferRange)){
|
||||
if(retarget()){
|
||||
findAnyTarget(build);
|
||||
|
||||
//target has been found, grab items and go
|
||||
if(unloadTarget != null){
|
||||
Call.takeItems(build, itemTarget, Math.min(unit.type.itemCapacity, build.items.get(itemTarget)), unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{ //the unit has an item, deposit it somewhere.
|
||||
|
||||
//there may be no current target, try to find one
|
||||
if(unloadTarget == null){
|
||||
if(retarget()){
|
||||
findDropTarget(unit.item(), 0, null);
|
||||
|
||||
//if there is not even a single place to unload, dump items.
|
||||
if(unloadTarget == null){
|
||||
unit.clearItem();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
|
||||
//what if some prankster reconfigures or picks up the target while the unit is moving? we can't have that!
|
||||
if(unloadTarget.item != itemTarget || unloadTarget.isPayload()){
|
||||
unloadTarget = null;
|
||||
return;
|
||||
}
|
||||
|
||||
moveTo(unloadTarget, moveRange, moveSmoothing);
|
||||
|
||||
//deposit in bursts, unloading can take a while
|
||||
if(unit.within(unloadTarget, transferRange) && timer.get(timerTarget2, dropSpacing)){
|
||||
int max = unloadTarget.acceptStack(unit.item(), unit.stack.amount, unit);
|
||||
|
||||
//deposit items when it's possible
|
||||
if(max > 0){
|
||||
noDestTimer = 0f;
|
||||
Call.transferItemTo(unit, unit.item(), max, unit.x, unit.y, unloadTarget);
|
||||
|
||||
//try the next target later
|
||||
if(!unit.hasItem()){
|
||||
targetIndex ++;
|
||||
}
|
||||
}else if((noDestTimer += dropSpacing) >= emptyWaitTime){
|
||||
//oh no, it's out of space - wait for a while, and if nothing changes, try the next destination
|
||||
|
||||
//next targeting attempt will try the next destination point
|
||||
targetIndex = findDropTarget(unit.item(), targetIndex, unloadTarget) + 1;
|
||||
|
||||
//nothing found at all, clear item
|
||||
if(unloadTarget == null){
|
||||
unit.clearItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** find target for the unit's current item */
|
||||
public int findDropTarget(Item item, int offset, UnitCargoUnloadPointBuild ignore){
|
||||
unloadTarget = null;
|
||||
itemTarget = item;
|
||||
|
||||
//autocast for convenience... I know all of these must be cargo unload points anyway
|
||||
targets.selectFrom((Seq<UnitCargoUnloadPointBuild>)(Seq)Vars.indexer.getFlagged(unit.team, BlockFlag.unitCargoUnloadPoint), u -> u.item == item);
|
||||
|
||||
if(targets.isEmpty()) return 0;
|
||||
|
||||
UnitCargoUnloadPointBuild lastStale = null;
|
||||
|
||||
offset %= targets.size;
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(var target : targets){
|
||||
if(i >= offset && target != ignore){
|
||||
if(target.stale){
|
||||
lastStale = target;
|
||||
}else{
|
||||
unloadTarget = target;
|
||||
targets.clear();
|
||||
return i;
|
||||
}
|
||||
}
|
||||
i ++;
|
||||
}
|
||||
|
||||
//it's still possible that the ignored target may become available at some point, try that, so it doesn't waste items
|
||||
if(ignore != null){
|
||||
unloadTarget = ignore;
|
||||
}else if(lastStale != null){ //a stale target is better than nothing
|
||||
unloadTarget = lastStale;
|
||||
}
|
||||
|
||||
targets.clear();
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void findAnyTarget(Building build){
|
||||
unloadTarget = null;
|
||||
itemTarget = null;
|
||||
|
||||
//autocast for convenience... I know all of these must be cargo unload points anyway
|
||||
var baseTargets = (Seq<UnitCargoUnloadPointBuild>)(Seq)Vars.indexer.getFlagged(unit.team, BlockFlag.unitCargoUnloadPoint);
|
||||
|
||||
if(baseTargets.isEmpty()) return;
|
||||
|
||||
orderedItems.size = 0;
|
||||
for(Item item : content.items()){
|
||||
if(build.items.get(item) > 0){
|
||||
orderedItems.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
//sort by most items in descending order, and try each one.
|
||||
orderedItems.sort(i -> -build.items.get(i));
|
||||
|
||||
UnitCargoUnloadPointBuild lastStale = null;
|
||||
|
||||
outer:
|
||||
for(Item item : orderedItems){
|
||||
targets.selectFrom(baseTargets, u -> u.item == item);
|
||||
|
||||
if(targets.size > 0) itemTarget = item;
|
||||
|
||||
for(int i = 0; i < targets.size; i ++){
|
||||
var target = targets.get((i + targetIndex) % targets.size);
|
||||
|
||||
lastStale = target;
|
||||
|
||||
if(!target.stale){
|
||||
unloadTarget = target;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if the only thing that was found was a "stale" target, at least try that...
|
||||
if(unloadTarget == null && lastStale != null){
|
||||
unloadTarget = lastStale;
|
||||
}
|
||||
|
||||
targets.clear();
|
||||
}
|
||||
|
||||
//unused, might change later
|
||||
void sortTargets(Seq<UnitCargoUnloadPointBuild> targets){
|
||||
//find sort by "most desirable" first
|
||||
targets.sort(Structs.comps(Structs.comparingInt(b -> b.items.total()), Structs.comparingFloat(b -> b.dst2(unit))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.payloads.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class CommandAI extends AIController{
|
||||
protected static final int maxCommandQueueSize = 50, avoidInterval = 10;
|
||||
protected static final Vec2 vecOut = new Vec2(), vecMovePos = new Vec2();
|
||||
protected static final boolean[] noFound = {false};
|
||||
protected static final UnitPayload tmpPayload = new UnitPayload(null);
|
||||
protected static final int transferStateNone = 0, transferStateLoad = 1, transferStateUnload = 2;
|
||||
|
||||
public Seq<Position> commandQueue = new Seq<>(5);
|
||||
public @Nullable Vec2 targetPos;
|
||||
public @Nullable Teamc attackTarget;
|
||||
/** Group of units that were all commanded to reach the same point. */
|
||||
public @Nullable UnitGroup group;
|
||||
public int groupIndex = 0;
|
||||
/** All encountered unreachable buildings of this AI. Why a sequence? Because contains() is very rarely called on it. */
|
||||
public IntSeq unreachableBuildings = new IntSeq(8);
|
||||
/** ID of unit read as target. This is set up after reading. Do not access! */
|
||||
public int readAttackTarget = -1;
|
||||
|
||||
protected boolean stopAtTarget, stopWhenInRange;
|
||||
protected Vec2 lastTargetPos;
|
||||
protected boolean blockingUnit;
|
||||
protected float timeSpentBlocked;
|
||||
protected float payloadPickupCooldown;
|
||||
protected int transferState = transferStateNone;
|
||||
|
||||
/** Stance, usually related to firing mode. */
|
||||
public UnitStance stance = UnitStance.shoot;
|
||||
/** Current command this unit is following. */
|
||||
public UnitCommand command = UnitCommand.moveCommand;
|
||||
/** Current controller instance based on command. */
|
||||
protected @Nullable AIController commandController;
|
||||
/** Last command type assigned. Used for detecting command changes. */
|
||||
protected @Nullable UnitCommand lastCommand;
|
||||
|
||||
public UnitCommand currentCommand(){
|
||||
return command == null ? UnitCommand.moveCommand : command;
|
||||
}
|
||||
|
||||
/** Attempts to assign a command to this unit. If not supported by the unit type, does nothing. */
|
||||
public void command(UnitCommand command){
|
||||
if(unit.type.commands.contains(command)){
|
||||
//clear old state.
|
||||
unit.mineTile = null;
|
||||
unit.clearBuilding();
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogicControllable(){
|
||||
return !hasCommand();
|
||||
}
|
||||
|
||||
public boolean isAttacking(){
|
||||
return target != null && unit.within(target, unit.range() + 10f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
//this should not be possible
|
||||
if(stance == UnitStance.stop) stance = UnitStance.shoot;
|
||||
|
||||
//pursue the target if relevant
|
||||
if(stance == UnitStance.pursueTarget && target != null && attackTarget == null && targetPos == null){
|
||||
commandTarget(target, false);
|
||||
}
|
||||
|
||||
//pursue the target for patrol, keeping the current position
|
||||
if(stance == UnitStance.patrol && target != null && attackTarget == null){
|
||||
//commanding a target overwrites targetPos, so add it to the queue
|
||||
if(targetPos != null){
|
||||
commandQueue.add(targetPos.cpy());
|
||||
}
|
||||
commandTarget(target, false);
|
||||
}
|
||||
|
||||
//remove invalid targets
|
||||
if(commandQueue.any()){
|
||||
commandQueue.removeAll(e -> e instanceof Healthc h && !h.isValid());
|
||||
}
|
||||
|
||||
//assign defaults
|
||||
if(command == null && unit.type.commands.size > 0){
|
||||
command = unit.type.defaultCommand == null ? unit.type.commands.first() : unit.type.defaultCommand;
|
||||
}
|
||||
|
||||
//update command controller based on index.
|
||||
var curCommand = command;
|
||||
if(lastCommand != curCommand){
|
||||
lastCommand = curCommand;
|
||||
commandController = (curCommand == null ? null : curCommand.controller.get(unit));
|
||||
}
|
||||
|
||||
//use the command controller if it is provided, and bail out.
|
||||
if(commandController != null){
|
||||
if(commandController.unit() != unit) commandController.unit(unit);
|
||||
commandController.updateUnit();
|
||||
}else{
|
||||
defaultBehavior();
|
||||
//boosting control is not supported, so just don't.
|
||||
unit.updateBoosting(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearCommands(){
|
||||
commandQueue.clear();
|
||||
targetPos = null;
|
||||
attackTarget = null;
|
||||
}
|
||||
|
||||
void tryPickupUnit(Payloadc pay){
|
||||
Unit target = Units.closest(unit.team, unit.x, unit.y, unit.type.hitSize * 2f, u -> u.isAI() && u != unit && u.isGrounded() && pay.canPickup(u) && u.within(unit, u.hitSize + unit.hitSize));
|
||||
if(target != null){
|
||||
Call.pickedUnitPayload(unit, target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc findMainTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
if(!unit.type.autoFindTarget && !(targetPos == null || nearAttackTarget(unit.x, unit.y, unit.range()))){
|
||||
return null;
|
||||
}
|
||||
return super.findMainTarget(x, y, range, air, ground);
|
||||
}
|
||||
|
||||
public void defaultBehavior(){
|
||||
|
||||
if(!net.client() && unit instanceof Payloadc pay){
|
||||
payloadPickupCooldown -= Time.delta;
|
||||
|
||||
//auto-drop everything
|
||||
if(command == UnitCommand.unloadPayloadCommand && pay.hasPayload()){
|
||||
Call.payloadDropped(unit, unit.x, unit.y);
|
||||
}
|
||||
|
||||
//try to pick up what's under it
|
||||
if(command == UnitCommand.loadUnitsCommand){
|
||||
tryPickupUnit(pay);
|
||||
}
|
||||
|
||||
//try to pick up a block
|
||||
if(command == UnitCommand.loadBlocksCommand && (targetPos == null || unit.within(targetPos, 1f))){
|
||||
Building build = world.buildWorld(unit.x, unit.y);
|
||||
|
||||
if(build != null && state.teams.canInteract(unit.team, build.team)){
|
||||
//pick up block's payload
|
||||
Payload current = build.getPayload();
|
||||
if(current != null && pay.canPickupPayload(current)){
|
||||
Call.pickedBuildPayload(unit, build, false);
|
||||
//pick up whole building directly
|
||||
}else if(build.block.buildVisibility != BuildVisibility.hidden && build.canPickup() && pay.canPickup(build)){
|
||||
Call.pickedBuildPayload(unit, build, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!net.client() && command == UnitCommand.enterPayloadCommand && unit.buildOn() != null && (targetPos == null || (world.buildWorld(targetPos.x, targetPos.y) != null && world.buildWorld(targetPos.x, targetPos.y) == unit.buildOn()))){
|
||||
var build = unit.buildOn();
|
||||
tmpPayload.unit = unit;
|
||||
if(build.team == unit.team && build.acceptPayload(build, tmpPayload)){
|
||||
Call.unitEnteredPayload(unit, build);
|
||||
return; //no use updating after this, the unit is gone!
|
||||
}
|
||||
}
|
||||
|
||||
updateVisuals();
|
||||
updateTargeting();
|
||||
|
||||
if(attackTarget != null && invalid(attackTarget)){
|
||||
attackTarget = null;
|
||||
targetPos = null;
|
||||
}
|
||||
|
||||
//move on to the next target
|
||||
if(attackTarget == null && targetPos == null){
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(attackTarget != null){
|
||||
if(targetPos == null){
|
||||
targetPos = new Vec2();
|
||||
lastTargetPos = targetPos;
|
||||
}
|
||||
targetPos.set(attackTarget);
|
||||
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs && stance != UnitStance.ram){
|
||||
Tile best = build.findClosestEdge(unit, Tile::solid);
|
||||
if(best != null){
|
||||
targetPos.set(best);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean alwaysArrive = false;
|
||||
|
||||
float engageRange = unit.type.range - 10f;
|
||||
boolean withinAttackRange = attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram;
|
||||
|
||||
if(targetPos != null){
|
||||
boolean move = true, isFinalPoint = commandQueue.size == 0;
|
||||
vecOut.set(targetPos);
|
||||
vecMovePos.set(targetPos);
|
||||
|
||||
//the enter payload command requires an exact position
|
||||
if(group != null && group.valid && groupIndex < group.units.size && command != UnitCommand.enterPayloadCommand){
|
||||
vecMovePos.add(group.positions[groupIndex * 2], group.positions[groupIndex * 2 + 1]);
|
||||
}
|
||||
|
||||
Building targetBuild = world.buildWorld(targetPos.x, targetPos.y);
|
||||
|
||||
//TODO: should the unit stop when it finds a target?
|
||||
if(
|
||||
(stance == UnitStance.patrol && target != null && unit.within(target, unit.type.range - 2f) && !unit.type.circleTarget) ||
|
||||
(command == UnitCommand.enterPayloadCommand && unit.within(targetPos, 4f) || (targetBuild != null && unit.within(targetBuild, targetBuild.block.size * tilesize/2f * 0.9f))) ||
|
||||
(command == UnitCommand.loopPayloadCommand && unit.within(targetPos, 10f))
|
||||
){
|
||||
move = false;
|
||||
}
|
||||
|
||||
if(unit.isGrounded() && stance != UnitStance.ram){
|
||||
//TODO: blocking enable or disable?
|
||||
if(timer.get(timerTarget3, avoidInterval)){
|
||||
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
|
||||
float max = unit.hitSize/2f;
|
||||
float radius = Math.max(7f, max);
|
||||
float margin = 4f;
|
||||
blockingUnit = Units.nearbyCheck(unit.x + dstPos.x - radius/2f, unit.y + dstPos.y - radius/2f, radius, radius,
|
||||
u -> u != unit && u.within(unit, u.hitSize/2f + unit.hitSize/2f + margin) && u.controller() instanceof CommandAI ai && ai.targetPos != null &&
|
||||
//stop for other unit only if it's closer to the target
|
||||
(ai.targetPos.equals(targetPos) && u.dst2(targetPos) < unit.dst2(targetPos)) &&
|
||||
//don't stop if they're facing the same way
|
||||
!Angles.within(unit.rotation, u.rotation, 15f) &&
|
||||
//must be near an obstacle, stopping in open ground is pointless
|
||||
ControlPathfinder.isNearObstacle(unit, unit.tileX(), unit.tileY(), u.tileX(), u.tileY()));
|
||||
}
|
||||
|
||||
float maxBlockTime = 60f * 5f;
|
||||
|
||||
if(blockingUnit){
|
||||
timeSpentBlocked += Time.delta;
|
||||
|
||||
if(timeSpentBlocked >= maxBlockTime*2f){
|
||||
timeSpentBlocked = 0f;
|
||||
}
|
||||
}else{
|
||||
timeSpentBlocked = 0f;
|
||||
}
|
||||
|
||||
//if the unit is next to the target, stop asking the pathfinder how to get there, it's a waste of CPU
|
||||
//TODO maybe stop moving too?
|
||||
if(withinAttackRange){
|
||||
move = true;
|
||||
noFound[0] = false;
|
||||
vecOut.set(vecMovePos);
|
||||
}else{
|
||||
move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
|
||||
|
||||
//TODO: what to do when there's a target and it can't be reached?
|
||||
/*
|
||||
if(noFound[0] && attackTarget != null && attackTarget.within(unit, unit.type.range * 2f)){
|
||||
move = true;
|
||||
vecOut.set(targetPos);
|
||||
}*/
|
||||
}
|
||||
|
||||
//rare case where unit must be perfectly aligned (happens with 1-tile gaps)
|
||||
alwaysArrive = vecOut.epsilonEquals(unit.tileX() * tilesize, unit.tileY() * tilesize);
|
||||
//we've reached the final point if the returned coordinate is equal to the supplied input
|
||||
isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f);
|
||||
|
||||
//if the path is invalid, stop trying and record the end as unreachable
|
||||
if(unit.team.isAI() && (noFound[0] || unit.isPathImpassable(World.toTile(vecMovePos.x), World.toTile(vecMovePos.y)))){
|
||||
if(attackTarget instanceof Building build){
|
||||
unreachableBuildings.addUnique(build.pos());
|
||||
}
|
||||
attackTarget = null;
|
||||
finishPath();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
vecOut.set(vecMovePos);
|
||||
}
|
||||
|
||||
if(move){
|
||||
if(unit.type.circleTarget && attackTarget != null){
|
||||
target = attackTarget;
|
||||
circleAttack(80f);
|
||||
}else{
|
||||
moveTo(vecOut,
|
||||
withinAttackRange ? engageRange :
|
||||
unit.isGrounded() ? 0f :
|
||||
attackTarget != null && stance != UnitStance.ram ? engageRange : 0f,
|
||||
unit.isFlying() ? 40f : 100f, false, null, isFinalPoint || alwaysArrive);
|
||||
}
|
||||
}
|
||||
|
||||
//if stopAtTarget is set, stop trying to move to the target once it is reached - used for defending
|
||||
if(attackTarget != null && stopAtTarget && unit.within(attackTarget, engageRange - 1f)){
|
||||
attackTarget = null;
|
||||
}
|
||||
|
||||
if(unit.isFlying() && move && (attackTarget == null || !unit.within(attackTarget, unit.type.range))){
|
||||
unit.lookAt(vecMovePos);
|
||||
}else{
|
||||
faceTarget();
|
||||
}
|
||||
|
||||
//reached destination, end pathfinding
|
||||
if(attackTarget == null && unit.within(vecMovePos, command.exactArrival && commandQueue.size == 0 ? 1f : Math.max(5f, unit.hitSize / 2f))){
|
||||
finishPath();
|
||||
}
|
||||
|
||||
if(stopWhenInRange && targetPos != null && unit.within(vecMovePos, engageRange * 0.9f)){
|
||||
finishPath();
|
||||
stopWhenInRange = false;
|
||||
}
|
||||
|
||||
}else if(target != null){
|
||||
faceTarget();
|
||||
}
|
||||
}
|
||||
|
||||
void finishPath(){
|
||||
//the enter payload command never finishes until they are actually accepted
|
||||
if(command == UnitCommand.enterPayloadCommand && commandQueue.size == 0 && targetPos != null && world.buildWorld(targetPos.x, targetPos.y) != null && world.buildWorld(targetPos.x, targetPos.y).block.acceptsUnitPayloads){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!net.client() && command == UnitCommand.loopPayloadCommand && unit instanceof Payloadc pay){
|
||||
|
||||
if(transferState == transferStateNone){
|
||||
transferState = pay.hasPayload() ? transferStateUnload : transferStateLoad;
|
||||
}
|
||||
|
||||
if(payloadPickupCooldown > 0f) return;
|
||||
|
||||
if(transferState == transferStateUnload){
|
||||
//drop until there's a failure
|
||||
int prev = -1;
|
||||
while(pay.hasPayload() && prev != pay.payloads().size){
|
||||
prev = pay.payloads().size;
|
||||
Call.payloadDropped(unit, unit.x, unit.y);
|
||||
}
|
||||
|
||||
//wait for everything to unload before running code below
|
||||
if(pay.hasPayload()){
|
||||
return;
|
||||
}
|
||||
payloadPickupCooldown = 60f;
|
||||
}else if(transferState == transferStateLoad){
|
||||
//pick up units until there's a failure
|
||||
int prev = -1;
|
||||
while(prev != pay.payloads().size){
|
||||
prev = pay.payloads().size;
|
||||
tryPickupUnit(pay);
|
||||
}
|
||||
|
||||
//wait to load things before running code below
|
||||
if(!pay.hasPayload()){
|
||||
return;
|
||||
}
|
||||
payloadPickupCooldown = 60f;
|
||||
}
|
||||
|
||||
//it will never finish
|
||||
if(commandQueue.size == 0){
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
transferState = transferStateNone;
|
||||
|
||||
Vec2 prev = targetPos;
|
||||
targetPos = null;
|
||||
|
||||
if(commandQueue.size > 0){
|
||||
var next = commandQueue.remove(0);
|
||||
if(next instanceof Teamc target){
|
||||
commandTarget(target, this.stopAtTarget);
|
||||
}else if(next instanceof Vec2 position){
|
||||
commandPosition(position);
|
||||
}
|
||||
|
||||
if(prev != null && (stance == UnitStance.patrol || command == UnitCommand.loopPayloadCommand)){
|
||||
commandQueue.add(prev.cpy());
|
||||
}
|
||||
|
||||
//make sure spot in formation is reachable
|
||||
if(group != null){
|
||||
group.updateRaycast(groupIndex, next instanceof Vec2 position ? position : Tmp.v3.set(next));
|
||||
}
|
||||
}else{
|
||||
if(group != null){
|
||||
group = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Unit unit){
|
||||
clearCommands();
|
||||
}
|
||||
|
||||
public void commandQueue(Position location){
|
||||
if(targetPos == null && attackTarget == null){
|
||||
if(location instanceof Teamc t){
|
||||
commandTarget(t, this.stopAtTarget);
|
||||
}else if(location instanceof Vec2 position){
|
||||
commandPosition(position);
|
||||
}
|
||||
}else if(commandQueue.size < maxCommandQueueSize && !commandQueue.contains(location)){
|
||||
commandQueue.add(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterRead(Unit unit){
|
||||
if(readAttackTarget != -1){
|
||||
attackTarget = Groups.unit.getByID(readAttackTarget);
|
||||
readAttackTarget = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldFire(){
|
||||
return stance != UnitStance.holdFire;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet bullet){
|
||||
if(unit.team.isAI() && bullet.owner instanceof Teamc teamc && teamc.team() != unit.team && attackTarget == null &&
|
||||
//can only counter-attack every few seconds to prevent rapidly changing targets
|
||||
!(teamc instanceof Unit u && !u.checkTarget(unit.type.targetAir, unit.type.targetGround)) && timer.get(timerTarget4, 60f * 10f)){
|
||||
commandTarget(teamc, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keepState(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
return !nearAttackTarget(x, y, range) ? super.findTarget(x, y, range, air, ground) : Units.isHittable(attackTarget, air, ground) ? attackTarget : null;
|
||||
}
|
||||
|
||||
public boolean nearAttackTarget(float x, float y, float range){
|
||||
return attackTarget != null && attackTarget.within(x, y, range + 3f + (attackTarget instanceof Sized s ? s.hitSize()/2f : 0f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retarget(){
|
||||
//retarget faster when there is an explicit target
|
||||
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20);
|
||||
}
|
||||
|
||||
public boolean hasCommand(){
|
||||
return targetPos != null;
|
||||
}
|
||||
|
||||
public void setupLastPos(){
|
||||
lastTargetPos = targetPos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commandPosition(Vec2 pos){
|
||||
if(pos == null) return;
|
||||
|
||||
commandPosition(pos, false);
|
||||
if(commandController != null){
|
||||
commandController.commandPosition(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void commandPosition(Vec2 pos, boolean stopWhenInRange){
|
||||
if(pos == null) return;
|
||||
|
||||
//this is an allocation, but it's relatively rarely called anyway, and outside mutations must be prevented
|
||||
targetPos = lastTargetPos = pos.cpy();
|
||||
attackTarget = null;
|
||||
this.stopWhenInRange = stopWhenInRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commandTarget(Teamc moveTo){
|
||||
commandTarget(moveTo, false);
|
||||
if(commandController != null){
|
||||
commandController.commandTarget(moveTo);
|
||||
}
|
||||
}
|
||||
|
||||
public void commandTarget(Teamc moveTo, boolean stopAtTarget){
|
||||
attackTarget = moveTo;
|
||||
this.stopAtTarget = stopAtTarget;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class DefenderAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
unloadPayloads();
|
||||
|
||||
if(target != null){
|
||||
moveTo(target, (target instanceof Sized s ? s.hitSize()/2f * 1.1f : 0f) + unit.hitSize/2f + 15f, 50f);
|
||||
unit.lookAt(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTargeting(){
|
||||
if(retarget()) target = findTarget(unit.x, unit.y, unit.range(), true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
|
||||
//Sort by max health and closer target.
|
||||
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.type.playerControllable,
|
||||
(u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f);
|
||||
if(result != null) return result;
|
||||
|
||||
//return core if found
|
||||
var core = unit.closestCore();
|
||||
if(core != null) return core;
|
||||
|
||||
//for enemies, target the enemy core.
|
||||
if(state.rules.waves && unit.team == state.rules.waveTeam){
|
||||
return unit.closestEnemyCore();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,61 +2,98 @@ package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.world.meta.BlockFlag.*;
|
||||
|
||||
public class FlyingAI extends AIController{
|
||||
final static Rand rand = new Rand();
|
||||
final static BlockFlag[] randomTargets = {core, storage, generator, launchPad, factory, repair, battery, reactor, drill};
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(target != null && unit.hasWeapons() && command() == UnitCommand.attack){
|
||||
if(!unit.type.circleTarget){
|
||||
moveTo(target, unit.range() * 0.8f);
|
||||
unit.lookAt(target);
|
||||
unloadPayloads();
|
||||
|
||||
if(target != null && unit.hasWeapons()){
|
||||
if(unit.type.circleTarget){
|
||||
circleAttack(120f);
|
||||
}else{
|
||||
attack(120f);
|
||||
moveTo(target, unit.type.range * 0.8f);
|
||||
unit.lookAt(target);
|
||||
}
|
||||
}
|
||||
|
||||
if(target == null && command() == UnitCommand.attack && state.rules.waves && unit.team == state.rules.defaultTeam){
|
||||
moveTo(getClosestSpawner(), state.rules.dropZoneRadius + 120f);
|
||||
}
|
||||
|
||||
if(command() == UnitCommand.rally){
|
||||
moveTo(targetFlag(unit.x, unit.y, BlockFlag.rally, false), 60f);
|
||||
if(target == null && state.rules.waves && unit.team == state.rules.defaultTeam){
|
||||
moveTo(getClosestSpawner(), state.rules.dropZoneRadius + 130f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
public Teamc targetFlag(float x, float y, BlockFlag flag, boolean enemy){
|
||||
if(state.rules.randomWaveAI){
|
||||
if(unit.team == Team.derelict) return null;
|
||||
var list = enemy ? indexer.getEnemy(unit.team, flag) : indexer.getFlagged(unit.team, flag);
|
||||
if(list.isEmpty()) return null;
|
||||
|
||||
if(ground) result = targetFlag(x, y, BlockFlag.generator, true);
|
||||
if(result != null) return result;
|
||||
|
||||
if(ground) result = targetFlag(x, y, BlockFlag.core, true);
|
||||
if(result != null) return result;
|
||||
|
||||
return null;
|
||||
Building closest = null;
|
||||
float cdist = 0f;
|
||||
for(Building t : list){
|
||||
if((t.items != null && t.items.any()) || t.status() != BlockStatus.noInput){
|
||||
float dst = t.dst2(x, y);
|
||||
if(closest == null || dst < cdist){
|
||||
closest = t;
|
||||
cdist = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}else{
|
||||
return super.targetFlag(x, y, flag, enemy);
|
||||
}
|
||||
}
|
||||
|
||||
protected void attack(float circleLength){
|
||||
vec.set(target).sub(unit);
|
||||
@Override
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
var result = findMainTarget(x, y, range, air, ground);
|
||||
|
||||
float ang = unit.angleTo(target);
|
||||
float diff = Angles.angleDist(ang, unit.rotation());
|
||||
//if the main target is in range, use it, otherwise target whatever is closest
|
||||
return checkTarget(result, x, y, range) ? target(x, y, range, air, ground) : result;
|
||||
}
|
||||
|
||||
if(diff > 100f && vec.len() < circleLength){
|
||||
vec.setAngle(unit.vel().angle());
|
||||
}else{
|
||||
vec.setAngle(Mathf.slerpDelta(unit.vel().angle(), vec.angle(), 0.6f));
|
||||
@Override
|
||||
public Teamc findMainTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
var core = targetFlag(x, y, BlockFlag.core, true);
|
||||
|
||||
if(core != null && Mathf.within(x, y, core.getX(), core.getY(), range)){
|
||||
return core;
|
||||
}
|
||||
|
||||
vec.setLength(unit.speed());
|
||||
if(state.rules.randomWaveAI){
|
||||
//when there are no waves, it's just random based on the unit
|
||||
rand.setSeed(unit.type.id + (state.rules.waves ? state.wave : unit.id));
|
||||
//try a few random flags first
|
||||
for(int attempt = 0; attempt < 5; attempt++){
|
||||
Teamc result = targetFlag(x, y, randomTargets[rand.random(randomTargets.length - 1)], true);
|
||||
if(result != null) return result;
|
||||
}
|
||||
//try the closest target
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
}else{
|
||||
for(var flag : unit.type.targetFlags){
|
||||
if(flag == null){
|
||||
Teamc result = target(x, y, range, air, ground);
|
||||
if(result != null) return result;
|
||||
}else if(ground){
|
||||
Teamc result = targetFlag(x, y, flag, true);
|
||||
if(result != null) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit.moveAt(vec);
|
||||
return core;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import mindustry.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
//TODO generally strange behavior
|
||||
/** AI/wave team only! This is used for wave support flyers. */
|
||||
public class FlyingFollowAI extends FlyingAI{
|
||||
public Teamc following;
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
unloadPayloads();
|
||||
|
||||
if(following != null){
|
||||
moveTo(following, (following instanceof Sized s ? s.hitSize()/2f * 1.1f : 0f) + unit.hitSize/2f + 15f, 50f);
|
||||
}else if(target != null && unit.hasWeapons()){
|
||||
moveTo(target, 80f);
|
||||
}
|
||||
|
||||
if(shouldFaceTarget()){
|
||||
unit.lookAt(target);
|
||||
}else if(following != null){
|
||||
unit.lookAt(following);
|
||||
}
|
||||
|
||||
if(timer.get(timerTarget3, 30f)){
|
||||
following = Units.closest(unit.team, unit.x, unit.y, Math.max(unit.type.range, 400f), u -> !u.dead() && u.type != unit.type, (u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldFaceTarget(){
|
||||
return target != null && (following == null || unit.within(target, unit.range()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateVisuals(){
|
||||
if(unit.isFlying()){
|
||||
if(unit.type.wobble) unit.wobble();
|
||||
|
||||
if(!shouldFaceTarget()){
|
||||
unit.lookAt(unit.prefRotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AIController fallback(){
|
||||
return new FlyingAI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useFallback(){
|
||||
//only AI teams use this controller
|
||||
return Vars.state.rules.pvp || Vars.state.rules.waveTeam != unit.team;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.formations.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
public class FormationAI extends AIController implements FormationMember{
|
||||
public Unit leader;
|
||||
|
||||
private Vec3 target = new Vec3();
|
||||
private @Nullable Formation formation;
|
||||
|
||||
public FormationAI(Unit leader, Formation formation){
|
||||
this.leader = leader;
|
||||
this.formation = formation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
target.set(unit.x, unit.y, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
|
||||
if(leader == null || leader.dead){
|
||||
unit.resetController();
|
||||
return;
|
||||
}
|
||||
|
||||
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.aim(leader.aimX(), leader.aimY());
|
||||
|
||||
if(unit.type.rotateShooting){
|
||||
unit.lookAt(leader.aimX(), leader.aimY());
|
||||
}else if(unit.moving()){
|
||||
unit.lookAt(unit.vel.angle());
|
||||
}
|
||||
|
||||
Vec2 realtarget = vec.set(target).add(leader.vel.x, leader.vel.y);
|
||||
|
||||
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.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());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Unit unit){
|
||||
if(formation != null){
|
||||
formation.removeMember(this);
|
||||
unit.resetController();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float formationSize(){
|
||||
return unit.hitSize * 1.1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeingControlled(Unit player){
|
||||
return leader == player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 formationPos(){
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,9 @@ package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -19,41 +15,36 @@ public class GroundAI extends AIController{
|
||||
|
||||
Building core = unit.closestEnemyCore();
|
||||
|
||||
if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){
|
||||
if(core != null && unit.within(core, unit.range() / 1.3f + core.block.size * tilesize / 2f)){
|
||||
target = core;
|
||||
Arrays.fill(targets, core);
|
||||
for(var mount : unit.mounts){
|
||||
if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){
|
||||
mount.target = core;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if((core == null || !unit.within(core, unit.range() * 0.5f)) && command() == UnitCommand.attack){
|
||||
if((core == null || !unit.within(core, unit.type.range * 0.5f))){
|
||||
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(spawner == null && core == null) move = false;
|
||||
}
|
||||
|
||||
//no reason to move if there's nothing there
|
||||
if(core == null && (!state.rules.waves || getClosestSpawner() == null)){
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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()){
|
||||
unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed));
|
||||
}
|
||||
}else if(unit.moving()){
|
||||
unit.lookAt(unit.vel().angle());
|
||||
if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed);
|
||||
}
|
||||
|
||||
faceTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class HugAI extends AIController{
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
|
||||
Building core = unit.closestEnemyCore();
|
||||
|
||||
if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){
|
||||
target = core;
|
||||
for(var mount : unit.mounts){
|
||||
if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){
|
||||
mount.target = core;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//raycast for target
|
||||
if(target != null && unit.within(target, unit.type.range) && !World.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
|
||||
for(Point2 p : Geometry.d4c){
|
||||
if(!unit.canPass(x + p.x, y + p.y)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})){
|
||||
if(unit.within(target, (unit.hitSize + (target instanceof Sized s ? s.hitSize() : 1f)) * 0.5f)){
|
||||
//circle target
|
||||
unit.movePref(vec.set(target).sub(unit).rotate(90f).setLength(unit.speed()));
|
||||
}else{
|
||||
//move toward target in a straight line
|
||||
unit.movePref(vec.set(target).sub(unit).limit(unit.speed()));
|
||||
}
|
||||
}else if(move){
|
||||
pathfind(Pathfinder.fieldCore);
|
||||
}
|
||||
|
||||
if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed);
|
||||
}
|
||||
|
||||
faceTarget();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
@@ -9,19 +8,18 @@ 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;
|
||||
public static final float transferDelay = 60f * 1.5f;
|
||||
/** Time after which the unit resets its controlled and reverts to a normal unit. */
|
||||
public static final float logicControlTimeout = 10f * 60f;
|
||||
public static final float logicControlTimeout = 60f * 10f;
|
||||
|
||||
public LUnitControl control = LUnitControl.stop;
|
||||
public LUnitControl control = LUnitControl.idle;
|
||||
public float moveX, moveY, moveRad;
|
||||
public float itemTimer, payTimer, controlTimer = logicControlTimeout, targetTimer;
|
||||
public float controlTimer = logicControlTimeout, targetTimer;
|
||||
@Nullable
|
||||
public Building controller;
|
||||
public BuildPlan plan = new BuildPlan();
|
||||
@@ -43,11 +41,14 @@ public class LogicAI extends AIController{
|
||||
|
||||
private ObjectSet<Object> radars = new ObjectSet<>();
|
||||
|
||||
// LogicAI state should not be reset after reading.
|
||||
@Override
|
||||
protected void updateMovement(){
|
||||
if(itemTimer >= 0) itemTimer -= Time.delta;
|
||||
if(payTimer >= 0) payTimer -= Time.delta;
|
||||
public boolean keepState(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
if(targetTimer > 0f){
|
||||
targetTimer -= Time.delta;
|
||||
}else{
|
||||
@@ -68,27 +69,38 @@ public class LogicAI extends AIController{
|
||||
moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f);
|
||||
}
|
||||
case approach -> {
|
||||
moveTo(Tmp.v1.set(moveX, moveY), moveRad - 7f, 7);
|
||||
moveTo(Tmp.v1.set(moveX, moveY), moveRad - 7f, 7, true, null);
|
||||
}
|
||||
case pathfind -> {
|
||||
if(unit.isFlying()){
|
||||
moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f);
|
||||
}else{
|
||||
if(controlPath.getPathPosition(unit, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){
|
||||
moveTo(Tmp.v1, 1f, Tmp.v2.epsilonEquals(Tmp.v1, 4.1f) ? 30f : 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
case autoPathfind -> {
|
||||
Building core = unit.closestEnemyCore();
|
||||
|
||||
if((core == null || !unit.within(core, unit.range() * 0.5f)) && command() == UnitCommand.attack){
|
||||
if((core == null || !unit.within(core, unit.range() * 0.5f))){
|
||||
boolean move = true;
|
||||
Tile spawner = null;
|
||||
|
||||
if(state.rules.waves && unit.team == state.rules.defaultTeam){
|
||||
Tile spawner = getClosestSpawner();
|
||||
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);
|
||||
if(move){
|
||||
if(unit.isFlying()){
|
||||
var target = core == null ? spawner : core;
|
||||
if(target != null){
|
||||
moveTo(target, unit.range() * 0.5f);
|
||||
}
|
||||
}else{
|
||||
pathfind(Pathfinder.fieldCore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,13 +110,13 @@ public class LogicAI extends AIController{
|
||||
}
|
||||
|
||||
if(unit.type.canBoost && !unit.type.flying){
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid()), 0.08f);
|
||||
unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid() || (unit.isFlying() && !unit.canLand())), unit.type.riseSpeed);
|
||||
}
|
||||
|
||||
//look where moving if there's nothing to aim at
|
||||
if(!shoot){
|
||||
if(!shoot || !unit.type.omniMovement){
|
||||
unit.lookAt(unit.prefRotation());
|
||||
}else if(unit.hasWeapons() && unit.mounts.length > 0){ //if there is, look at the object
|
||||
}else if(unit.hasWeapons() && unit.mounts.length > 0 && !unit.mounts[0].weapon.ignoreRotation){ //if there is, look at the object
|
||||
unit.lookAt(unit.mounts[0].aimX, unit.mounts[0].aimY);
|
||||
}
|
||||
}
|
||||
@@ -114,42 +126,29 @@ public class LogicAI extends AIController{
|
||||
}
|
||||
|
||||
@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);
|
||||
public boolean checkTarget(Teamc target, float x, float y, float range){
|
||||
return false;
|
||||
}
|
||||
|
||||
//always retarget
|
||||
@Override
|
||||
protected boolean retarget(){
|
||||
public boolean retarget(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean invalid(Teamc target){
|
||||
public boolean invalid(Teamc target){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldShoot(){
|
||||
public 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){
|
||||
public Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
return switch(aimControl){
|
||||
case target -> posTarget;
|
||||
case targetp -> mainTarget;
|
||||
|
||||
@@ -9,29 +9,29 @@ import mindustry.world.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MinerAI extends AIController{
|
||||
boolean mining = true;
|
||||
Item targetItem;
|
||||
Tile ore;
|
||||
public boolean mining = true;
|
||||
public Item targetItem;
|
||||
public Tile ore;
|
||||
|
||||
@Override
|
||||
protected void updateMovement(){
|
||||
public void updateMovement(){
|
||||
Building core = unit.closestCore();
|
||||
|
||||
if(!(unit.canMine()) || core == null) return;
|
||||
|
||||
if(unit.mineTile != null && !unit.mineTile.within(unit, unit.type.range)){
|
||||
if(!unit.validMine(unit.mineTile)){
|
||||
unit.mineTile(null);
|
||||
}
|
||||
|
||||
if(mining){
|
||||
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));
|
||||
targetItem = unit.type.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();
|
||||
unit.mineTile(null);
|
||||
unit.mineTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ public class MinerAI extends AIController{
|
||||
if(unit.stack.amount >= unit.type.itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){
|
||||
mining = false;
|
||||
}else{
|
||||
if(timer.get(timerTarget, 60) && targetItem != null){
|
||||
if(timer.get(timerTarget3, 60) && targetItem != null){
|
||||
ore = indexer.findClosestOre(unit, targetItem);
|
||||
}
|
||||
|
||||
if(ore != null){
|
||||
moveTo(ore, unit.type.range / 2f, 20f);
|
||||
moveTo(ore, unit.type.mineRange / 2f, 20f);
|
||||
|
||||
if(unit.within(ore, unit.type.range)){
|
||||
if(ore.block() == Blocks.air && unit.within(ore, unit.type.mineRange)){
|
||||
unit.mineTile = ore;
|
||||
}
|
||||
|
||||
@@ -75,8 +75,4 @@ public class MinerAI extends AIController{
|
||||
circle(core, unit.type.range / 1.8f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTargeting(){
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class MissileAI extends AIController{
|
||||
public @Nullable Unit shooter;
|
||||
|
||||
@Override
|
||||
protected void resetTimers(){
|
||||
timer.reset(timerTarget, 5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMovement(){
|
||||
unloadPayloads();
|
||||
|
||||
float time = unit instanceof TimedKillc t ? t.time() : 1000000f;
|
||||
|
||||
if(time >= unit.type.homingDelay && shooter != null && !shooter.dead()){
|
||||
unit.lookAt(shooter.aimX, shooter.aimY);
|
||||
}
|
||||
|
||||
//move forward forever
|
||||
unit.moveAt(vec.trns(unit.rotation, unit.type.missileAccelTime <= 0f ? unit.speed() : Mathf.pow(Math.min(time / unit.type.missileAccelTime, 1f), 2f) * unit.speed()));
|
||||
|
||||
var build = unit.buildOn();
|
||||
|
||||
//kill instantly on enemy building contact
|
||||
if(build != null && build.team != unit.team && (build == target || !build.block.underBullets)){
|
||||
unit.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground) && !u.isMissile(), t -> ground && (!t.block.underBullets || (shooter != null && t == Vars.world.buildWorld(shooter.aimX, shooter.aimY))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retarget(){
|
||||
//more frequent retarget due to high speed. TODO won't this lag?
|
||||
return timer.get(timerTarget, 4f);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.util.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.blocks.ConstructBlock.*;
|
||||
|
||||
public class RepairAI extends AIController{
|
||||
public static float retreatDst = 160f, fleeRange = 310f, retreatDelay = Time.toSeconds * 3f;
|
||||
|
||||
@Nullable Teamc avoid;
|
||||
float retreatTimer;
|
||||
Building damagedTarget;
|
||||
|
||||
@Override
|
||||
protected void updateMovement(){
|
||||
public void updateMovement(){
|
||||
if(target instanceof Building){
|
||||
boolean shoot = false;
|
||||
|
||||
@@ -22,25 +28,49 @@ public class RepairAI extends AIController{
|
||||
unit.controlWeapons(false);
|
||||
}
|
||||
|
||||
if(target != null){
|
||||
if(!target.within(unit, unit.type.range * 0.65f) && target instanceof Building b && b.team == unit.team){
|
||||
if(target != null && target instanceof Building b && b.team == unit.team){
|
||||
if(unit.type.circleTarget){
|
||||
circleAttack(120f);
|
||||
}else if(!target.within(unit, unit.type.range * 0.65f)){
|
||||
moveTo(target, unit.type.range * 0.65f);
|
||||
}
|
||||
|
||||
unit.lookAt(target);
|
||||
if(!unit.type.circleTarget){
|
||||
unit.lookAt(target);
|
||||
}
|
||||
}
|
||||
|
||||
//not repairing
|
||||
if(!(target instanceof Building)){
|
||||
if(timer.get(timerTarget4, 40)){
|
||||
avoid = target(unit.x, unit.y, fleeRange, true, true);
|
||||
}
|
||||
|
||||
if((retreatTimer += Time.delta) >= retreatDelay){
|
||||
//fly away from enemy when not doing anything
|
||||
if(avoid != null){
|
||||
var core = unit.closestCore();
|
||||
if(core != null && !unit.within(core, retreatDst)){
|
||||
moveTo(core, retreatDst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
retreatTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTargeting(){
|
||||
Building target = Units.findDamagedTile(unit.team, unit.x, unit.y);
|
||||
public void updateTargeting(){
|
||||
if(timer.get(timerTarget, 15)){
|
||||
damagedTarget = Units.findDamagedTile(unit.team, unit.x, unit.y);
|
||||
if(damagedTarget instanceof ConstructBuild) damagedTarget = null;
|
||||
}
|
||||
|
||||
if(target instanceof ConstructBuild) target = null;
|
||||
|
||||
if(target == null){
|
||||
if(damagedTarget == null){
|
||||
super.updateTargeting();
|
||||
}else{
|
||||
this.target = target;
|
||||
this.target = damagedTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
package mindustry.ai.types;
|
||||
|
||||
import arc.math.geom.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.core.*;
|
||||
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.blocks.storage.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class SuicideAI extends GroundAI{
|
||||
static boolean blockedByBlock;
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
|
||||
if(Units.invalidateTarget(target, unit.team, unit.x, unit.y, Float.MAX_VALUE)){
|
||||
target = null;
|
||||
}
|
||||
@@ -28,17 +31,17 @@ public class SuicideAI extends GroundAI{
|
||||
|
||||
boolean rotate = false, shoot = false, moveToTarget = false;
|
||||
|
||||
if(target == null){
|
||||
target = core;
|
||||
}
|
||||
|
||||
if(!Units.invalidateTarget(target, unit, unit.range()) && unit.hasWeapons()){
|
||||
rotate = true;
|
||||
shoot = unit.within(target, unit.type.weapons.first().bullet.range() +
|
||||
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));
|
||||
}
|
||||
|
||||
//do not move toward walls or transport blocks
|
||||
if(!(target instanceof Building build && (
|
||||
if(!(target instanceof Building build && !(build.block instanceof CoreBlock) && (
|
||||
build.block.group == BlockGroup.walls ||
|
||||
build.block.group == BlockGroup.liquids ||
|
||||
build.block.group == BlockGroup.transportation
|
||||
@@ -46,15 +49,18 @@ public class SuicideAI extends GroundAI{
|
||||
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();
|
||||
boolean blocked = World.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
|
||||
for(Point2 p : Geometry.d4c){
|
||||
Tile tile = Vars.world.tile(x + p.x, y + p.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();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
//shoot when there's an enemy block in the way
|
||||
@@ -65,30 +71,34 @@ public class SuicideAI extends GroundAI{
|
||||
if(!blocked){
|
||||
moveToTarget = true;
|
||||
//move towards target directly
|
||||
unit.moveAt(vec.set(target).sub(unit).limit(unit.speed()));
|
||||
unit.movePref(vec.set(target).sub(unit).limit(unit.speed()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!moveToTarget){
|
||||
if(command() == UnitCommand.rally){
|
||||
Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false);
|
||||
boolean move = true;
|
||||
|
||||
if(target != null && !unit.within(target, 70f)){
|
||||
pathfind(Pathfinder.fieldRally);
|
||||
//stop moving toward the drop zone if applicable
|
||||
if(core == null && state.rules.waves && unit.team == state.rules.defaultTeam){
|
||||
Tile spawner = getClosestSpawner();
|
||||
if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)){
|
||||
move = false;
|
||||
}
|
||||
}else if(command() == UnitCommand.attack && core != null){
|
||||
pathfind(Pathfinder.fieldCore);
|
||||
}
|
||||
|
||||
if(unit.moving()) unit.lookAt(unit.vel().angle());
|
||||
if(move){
|
||||
pathfind(Pathfinder.fieldCore);
|
||||
}
|
||||
}
|
||||
|
||||
unit.controlWeapons(rotate, shoot);
|
||||
|
||||
faceTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
public Teamc target(float x, float y, float range, boolean air, boolean ground){
|
||||
return Units.closestTarget(unit.team, x, y, range, u -> u.checkTarget(air, ground), t -> ground &&
|
||||
!(t.block instanceof Conveyor || t.block instanceof Conduit)); //do not target conveyors/conduits
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mindustry.async;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.game.EventType.*;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
@@ -10,19 +11,14 @@ import static mindustry.Vars.*;
|
||||
|
||||
public class AsyncCore{
|
||||
//all processes to be executed each frame
|
||||
private final Seq<AsyncProcess> processes = Seq.with(
|
||||
public final Seq<AsyncProcess> processes = Seq.with(
|
||||
new PhysicsProcess()
|
||||
);
|
||||
|
||||
//futures to be awaited
|
||||
private final Seq<Future<?>> futures = new Seq<>();
|
||||
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(processes.size, r -> {
|
||||
Thread thread = new Thread(r, "AsyncLogic-Thread");
|
||||
thread.setDaemon(true);
|
||||
thread.setUncaughtExceptionHandler((t, e) -> Core.app.post(() -> { throw new RuntimeException(e); }));
|
||||
return thread;
|
||||
});
|
||||
private ExecutorService executor;
|
||||
|
||||
public AsyncCore(){
|
||||
Events.on(WorldLoadEvent.class, e -> {
|
||||
@@ -49,6 +45,16 @@ public class AsyncCore{
|
||||
|
||||
futures.clear();
|
||||
|
||||
//init executor with size of potentially-modified process list
|
||||
if(executor == null){
|
||||
executor = Executors.newFixedThreadPool(processes.size, r -> {
|
||||
Thread thread = new Thread(r, "AsyncLogic-Thread");
|
||||
thread.setDaemon(true);
|
||||
thread.setUncaughtExceptionHandler((t, e) -> Threads.throwAppException(e));
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
//submit all tasks
|
||||
for(AsyncProcess p : processes){
|
||||
if(p.shouldProcess()){
|
||||
@@ -71,7 +77,7 @@ public class AsyncCore{
|
||||
|
||||
private void complete(){
|
||||
//wait for all threads to stop processing
|
||||
for(Future future : futures){
|
||||
for(var future : futures){
|
||||
try{
|
||||
future.get();
|
||||
}catch(Throwable t){
|
||||
|
||||
@@ -10,11 +10,11 @@ import mindustry.entities.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class PhysicsProcess implements AsyncProcess{
|
||||
private static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
public static final int
|
||||
layers = 3,
|
||||
layerGround = 0,
|
||||
layerLegs = 1,
|
||||
layerFlying = 2;
|
||||
|
||||
private PhysicsWorld physics;
|
||||
private Seq<PhysicRef> refs = new Seq<>(false);
|
||||
@@ -24,6 +24,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
@Override
|
||||
public void begin(){
|
||||
if(physics == null) return;
|
||||
boolean local = !Vars.net.client();
|
||||
|
||||
//remove stale entities
|
||||
refs.removeAll(ref -> {
|
||||
@@ -35,32 +36,32 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
return false;
|
||||
});
|
||||
|
||||
//find Unit without bodies and assign them
|
||||
//find Units without bodies and assign them
|
||||
for(Unit entity : group){
|
||||
if(entity == null || entity.type == null || !entity.type.physics) continue;
|
||||
|
||||
if(entity.physref() == null){
|
||||
if(entity.physref == null){
|
||||
PhysicsBody body = new PhysicsBody();
|
||||
body.x = entity.x();
|
||||
body.y = entity.y();
|
||||
body.x = entity.x;
|
||||
body.y = entity.y;
|
||||
body.mass = entity.mass();
|
||||
body.radius = entity.hitSize() / 2f;
|
||||
body.radius = entity.hitSize * Vars.unitCollisionRadiusScale;
|
||||
|
||||
PhysicRef ref = new PhysicRef(entity, body);
|
||||
refs.add(ref);
|
||||
|
||||
entity.physref(ref);
|
||||
entity.physref = ref;
|
||||
|
||||
physics.add(body);
|
||||
}
|
||||
|
||||
//save last position
|
||||
PhysicRef ref = entity.physref();
|
||||
PhysicRef ref = entity.physref;
|
||||
|
||||
ref.body.layer =
|
||||
entity.type.allowLegStep ? layerLegs :
|
||||
entity.isGrounded() ? layerGround : layerFlying;
|
||||
ref.x = entity.x();
|
||||
ref.y = entity.y();
|
||||
ref.body.layer = entity.collisionLayer();
|
||||
ref.x = entity.x;
|
||||
ref.y = entity.y;
|
||||
ref.body.local = local || entity.isLocal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,21 +148,30 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
trees[i].clear();
|
||||
}
|
||||
|
||||
for(int i = 0; i < bodies.size; i++){
|
||||
PhysicsBody body = bodies.items[i];
|
||||
var bodyItems = bodies.items;
|
||||
int bodySize = bodies.size;
|
||||
|
||||
for(int i = 0; i < bodySize; i++){
|
||||
PhysicsBody body = bodyItems[i];
|
||||
body.collided = false;
|
||||
trees[body.layer].insert(body);
|
||||
}
|
||||
|
||||
for(int i = 0; i < bodies.size; i++){
|
||||
PhysicsBody body = bodies.items[i];
|
||||
for(int i = 0; i < bodySize; i++){
|
||||
PhysicsBody body = bodyItems[i];
|
||||
|
||||
//for clients, the only body that collides is the local one; all other physics simulations are handled by the server.
|
||||
if(!body.local) continue;
|
||||
|
||||
body.hitbox(rect);
|
||||
|
||||
seq.size = 0;
|
||||
trees[body.layer].intersect(rect, seq);
|
||||
int size = seq.size;
|
||||
var items = seq.items;
|
||||
|
||||
for(int j = 0; j < seq.size; j++){
|
||||
PhysicsBody other = seq.items[j];
|
||||
for(int j = 0; j < size; j++){
|
||||
PhysicsBody other = items[j];
|
||||
|
||||
if(other == body || other.collided) continue;
|
||||
|
||||
@@ -173,10 +183,14 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
float ms = body.mass + other.mass;
|
||||
float m1 = other.mass / ms, m2 = body.mass / ms;
|
||||
|
||||
//first body is always local due to guard check above
|
||||
body.x += vec.x * m1 / scl;
|
||||
body.y += vec.y * m1 / scl;
|
||||
other.x -= vec.x * m2 / scl;
|
||||
other.y -= vec.y * m2 / scl;
|
||||
|
||||
if(other.local){
|
||||
other.x -= vec.x * m2 / scl;
|
||||
other.y -= vec.y * m2 / scl;
|
||||
}
|
||||
}
|
||||
}
|
||||
body.collided = true;
|
||||
@@ -186,7 +200,7 @@ public class PhysicsProcess implements AsyncProcess{
|
||||
public static class PhysicsBody implements QuadTreeObject{
|
||||
public float x, y, radius, mass;
|
||||
public int layer = 0;
|
||||
public boolean collided = false;
|
||||
public boolean collided = false, local = true;
|
||||
|
||||
@Override
|
||||
public void hitbox(Rect out){
|
||||
|
||||
@@ -17,7 +17,7 @@ import static mindustry.Vars.*;
|
||||
|
||||
/** 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;
|
||||
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.8f, musicWaveChance = 0.46f;
|
||||
|
||||
/** normal, ambient music, plays at any time */
|
||||
public Seq<Music> ambientMusic = Seq.with();
|
||||
@@ -28,6 +28,7 @@ public class SoundControl{
|
||||
|
||||
protected Music lastRandomPlayed;
|
||||
protected Interval timer = new Interval(4);
|
||||
protected long lastPlayed;
|
||||
protected @Nullable Music current;
|
||||
protected float fade;
|
||||
protected boolean silenced;
|
||||
@@ -55,6 +56,10 @@ public class SoundControl{
|
||||
}));
|
||||
|
||||
setupFilters();
|
||||
|
||||
Events.on(ResetEvent.class, e -> {
|
||||
lastPlayed = Time.millis();
|
||||
});
|
||||
}
|
||||
|
||||
protected void setupFilters(){
|
||||
@@ -65,7 +70,7 @@ public class SoundControl{
|
||||
protected void reload(){
|
||||
current = null;
|
||||
fade = 0f;
|
||||
ambientMusic = Seq.with(Musics.game1, Musics.game3, Musics.game6, Musics.game8, Musics.game9);
|
||||
ambientMusic = Seq.with(Musics.game1, Musics.game3, Musics.game6, Musics.game8, Musics.game9, Musics.fine);
|
||||
darkMusic = Seq.with(Musics.game2, Musics.game5, Musics.game7, Musics.game4);
|
||||
bossMusic = Seq.with(Musics.boss1, Musics.boss2, Musics.game2, Musics.game5);
|
||||
|
||||
@@ -76,6 +81,8 @@ public class SoundControl{
|
||||
sound.setBus(uiBus);
|
||||
}
|
||||
}
|
||||
|
||||
Events.fire(new MusicRegisterEvent());
|
||||
}
|
||||
|
||||
public void loop(Sound sound, float volume){
|
||||
@@ -130,14 +137,21 @@ public class SoundControl{
|
||||
Core.audio.soundBus.play();
|
||||
setupFilters();
|
||||
}else{
|
||||
//stopping a single audio bus stops everything else, yay!
|
||||
Core.audio.soundBus.stop();
|
||||
//play music bus again, as it was stopped above
|
||||
Core.audio.musicBus.play();
|
||||
|
||||
Core.audio.soundBus.play();
|
||||
}
|
||||
}
|
||||
|
||||
Core.audio.setPaused(Core.audio.soundBus.id, state.isPaused());
|
||||
|
||||
if(state.isMenu()){
|
||||
silenced = false;
|
||||
if(ui.planet.isShown()){
|
||||
play(Musics.launch);
|
||||
play(ui.planet.state.planet.launchMusic);
|
||||
}else if(ui.editor.isShown()){
|
||||
play(Musics.editor);
|
||||
}else{
|
||||
@@ -150,10 +164,14 @@ public class SoundControl{
|
||||
//this just fades out the last track to make way for ingame music
|
||||
silence();
|
||||
|
||||
//play music at intervals
|
||||
if(timer.get(musicInterval)){
|
||||
if(Core.settings.getBool("alwaysmusic")){
|
||||
if(current == null){
|
||||
playRandom();
|
||||
}
|
||||
}else if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
|
||||
//chance to play it per interval
|
||||
if(Mathf.chance(musicChance)){
|
||||
lastPlayed = Time.millis();
|
||||
playRandom();
|
||||
}
|
||||
}
|
||||
@@ -172,7 +190,7 @@ public class SoundControl{
|
||||
float avol = Core.settings.getInt("ambientvol", 100) / 100f;
|
||||
|
||||
sounds.each((sound, data) -> {
|
||||
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.2f);
|
||||
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.11f);
|
||||
|
||||
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);
|
||||
@@ -198,7 +216,9 @@ public class SoundControl{
|
||||
|
||||
/** Plays a random track.*/
|
||||
public void playRandom(){
|
||||
if(isDark()){
|
||||
if(state.boss() != null){
|
||||
playOnce(bossMusic.random(lastRandomPlayed));
|
||||
}else if(isDark()){
|
||||
playOnce(darkMusic.random(lastRandomPlayed));
|
||||
}else{
|
||||
playOnce(ambientMusic.random(lastRandomPlayed));
|
||||
@@ -207,7 +227,7 @@ public class SoundControl{
|
||||
|
||||
/** Whether to play dark music.*/
|
||||
protected boolean isDark(){
|
||||
if(state.teams.get(player.team()).hasCore() && state.teams.get(player.team()).core().healthf() < 0.85f){
|
||||
if(player.team().data().hasCore() && player.team().data().core().healthf() < 0.85f){
|
||||
//core damaged -> dark
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,15 @@ public class SoundLoop{
|
||||
}
|
||||
|
||||
public void update(float x, float y, boolean play){
|
||||
update(x, y, play, 1f);
|
||||
}
|
||||
|
||||
public void update(float x, float y, boolean play, float volumeScl){
|
||||
if(baseVolume <= 0) return;
|
||||
|
||||
if(id < 0){
|
||||
if(play){
|
||||
id = sound.loop(sound.calcVolume(x, y) * volume * baseVolume, 1f, sound.calcPan(x, y));
|
||||
id = sound.loop(sound.calcVolume(x, y) * volume * baseVolume * volumeScl, 1f, sound.calcPan(x, y));
|
||||
}
|
||||
}else{
|
||||
//fade the sound in or out
|
||||
@@ -38,7 +42,7 @@ public class SoundLoop{
|
||||
}
|
||||
}
|
||||
|
||||
Core.audio.set(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 * volumeScl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4918
-559
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,26 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Bullets implements ContentList{
|
||||
/**
|
||||
* Class for holding special internal bullets.
|
||||
* Formerly used to define preset bullets for turrets; as of v7, these have been inlined at the source.
|
||||
* */
|
||||
public class Bullets{
|
||||
public static BulletType
|
||||
|
||||
//artillery
|
||||
artilleryDense, artilleryPlastic, artilleryPlasticFrag, artilleryHoming, artilleryIncendiary, artilleryExplosive,
|
||||
placeholder, spaceLiquid, damageLightning, damageLightningGround, damageLightningAir, fireball;
|
||||
|
||||
//flak
|
||||
flakScrap, flakLead, flakGlass, flakGlassFrag,
|
||||
public static void load(){
|
||||
|
||||
//frag (flak-like but hits ground)
|
||||
fragGlass, fragExplosive, fragPlastic, fragSurge, fragGlassFrag, fragPlasticFrag,
|
||||
|
||||
//missiles
|
||||
missileExplosive, missileIncendiary, missileSurge,
|
||||
|
||||
//standard
|
||||
standardCopper, standardDense, standardThorium, standardHoming, standardIncendiary,
|
||||
standardDenseBig, standardThoriumBig, standardIncendiaryBig,
|
||||
|
||||
//liquid
|
||||
waterShot, cryoShot, slagShot, oilShot, heavyWaterShot, heavyCryoShot, heavySlagShot, heavyOilShot,
|
||||
|
||||
//environment, misc.
|
||||
damageLightning, damageLightningGround, fireball, basicFlame, pyraFlame, driverBolt;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
//not allowed in weapons - used only to prevent NullPointerExceptions
|
||||
placeholder = new BasicBulletType(2.5f, 9, "ohno"){{
|
||||
width = 7f;
|
||||
height = 9f;
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 2;
|
||||
}};
|
||||
|
||||
//lightning bullets need to be initialized first.
|
||||
damageLightning = new BulletType(0.0001f, 0f){{
|
||||
@@ -50,464 +30,24 @@ public class Bullets implements ContentList{
|
||||
status = StatusEffects.shocked;
|
||||
statusDuration = 10f;
|
||||
hittable = false;
|
||||
lightColor = Color.white;
|
||||
}};
|
||||
|
||||
//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 = damageLightning.copy();
|
||||
damageLightningGround.collidesAir = false;
|
||||
|
||||
artilleryDense = new ArtilleryBulletType(3f, 20, "shell"){{
|
||||
hitEffect = Fx.flakExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 80f;
|
||||
width = height = 11f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 33f;
|
||||
}};
|
||||
damageLightningAir = damageLightning.copy();
|
||||
damageLightningAir.collidesGround = false;
|
||||
damageLightningAir.collidesTiles = false;
|
||||
|
||||
artilleryPlasticFrag = new BasicBulletType(2.5f, 10, "bullet"){{
|
||||
width = 10f;
|
||||
height = 12f;
|
||||
shrinkY = 1f;
|
||||
lifetime = 15f;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
despawnEffect = Fx.none;
|
||||
collidesAir = false;
|
||||
}};
|
||||
|
||||
artilleryPlastic = new ArtilleryBulletType(3.4f, 20, "shell"){{
|
||||
hitEffect = Fx.plasticExplosion;
|
||||
knockback = 1f;
|
||||
lifetime = 80f;
|
||||
width = height = 13f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 35f;
|
||||
splashDamage = 45f;
|
||||
fragBullet = artilleryPlasticFrag;
|
||||
fragBullets = 10;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
}};
|
||||
|
||||
artilleryHoming = new ArtilleryBulletType(3f, 20, "shell"){{
|
||||
hitEffect = Fx.flakExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 80f;
|
||||
width = height = 11f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 33f;
|
||||
reloadMultiplier = 1.2f;
|
||||
ammoMultiplier = 3f;
|
||||
homingPower = 0.08f;
|
||||
homingRange = 50f;
|
||||
}};
|
||||
|
||||
artilleryIncendiary = new ArtilleryBulletType(3f, 20, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 80f;
|
||||
width = height = 13f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 35f;
|
||||
status = StatusEffects.burning;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
makeFire = true;
|
||||
trailEffect = Fx.incendTrail;
|
||||
}};
|
||||
|
||||
artilleryExplosive = new ArtilleryBulletType(2f, 20, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 80f;
|
||||
width = height = 14f;
|
||||
collidesTiles = false;
|
||||
ammoMultiplier = 4f;
|
||||
splashDamageRadius = 45f;
|
||||
splashDamage = 50f;
|
||||
backColor = Pal.missileYellowBack;
|
||||
frontColor = Pal.missileYellow;
|
||||
|
||||
status = StatusEffects.blasted;
|
||||
statusDuration = 60f;
|
||||
}};
|
||||
|
||||
flakGlassFrag = new BasicBulletType(3f, 5, "bullet"){{
|
||||
width = 5f;
|
||||
height = 12f;
|
||||
shrinkY = 1f;
|
||||
lifetime = 20f;
|
||||
backColor = Pal.gray;
|
||||
frontColor = Color.white;
|
||||
despawnEffect = Fx.none;
|
||||
collidesGround = false;
|
||||
}};
|
||||
|
||||
flakLead = new FlakBulletType(4.2f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 4f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
width = 6f;
|
||||
height = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 27f;
|
||||
splashDamageRadius = 15f;
|
||||
}};
|
||||
|
||||
flakScrap = new FlakBulletType(4f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 5f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.5f;
|
||||
width = 6f;
|
||||
height = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 22f;
|
||||
splashDamageRadius = 24f;
|
||||
}};
|
||||
|
||||
flakGlass = new FlakBulletType(4f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 5f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.8f;
|
||||
width = 6f;
|
||||
height = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 22f;
|
||||
splashDamageRadius = 20f;
|
||||
fragBullet = flakGlassFrag;
|
||||
fragBullets = 5;
|
||||
}};
|
||||
|
||||
fragGlassFrag = new BasicBulletType(3f, 5, "bullet"){{
|
||||
width = 5f;
|
||||
height = 12f;
|
||||
shrinkY = 1f;
|
||||
lifetime = 20f;
|
||||
backColor = Pal.gray;
|
||||
frontColor = Color.white;
|
||||
despawnEffect = Fx.none;
|
||||
}};
|
||||
|
||||
fragPlasticFrag = new BasicBulletType(2.5f, 10, "bullet"){{
|
||||
width = 10f;
|
||||
height = 12f;
|
||||
shrinkY = 1f;
|
||||
lifetime = 15f;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
despawnEffect = Fx.none;
|
||||
}};
|
||||
|
||||
fragGlass = new FlakBulletType(4f, 3){{
|
||||
ammoMultiplier = 3f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.8f;
|
||||
width = 6f;
|
||||
height = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 18f;
|
||||
splashDamageRadius = 16f;
|
||||
fragBullet = fragGlassFrag;
|
||||
fragBullets = 3;
|
||||
explodeRange = 20f;
|
||||
collidesGround = true;
|
||||
}};
|
||||
|
||||
fragPlastic = new FlakBulletType(4f, 6){{
|
||||
splashDamageRadius = 40f;
|
||||
splashDamage = 25f;
|
||||
fragBullet = fragPlasticFrag;
|
||||
fragBullets = 5;
|
||||
hitEffect = Fx.plasticExplosion;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
backColor = Pal.plastaniumBack;
|
||||
shootEffect = Fx.shootBig;
|
||||
collidesGround = true;
|
||||
explodeRange = 20f;
|
||||
}};
|
||||
|
||||
fragExplosive = new FlakBulletType(4f, 5){{
|
||||
shootEffect = Fx.shootBig;
|
||||
ammoMultiplier = 4f;
|
||||
splashDamage = 18f;
|
||||
splashDamageRadius = 55f;
|
||||
collidesGround = true;
|
||||
|
||||
status = StatusEffects.blasted;
|
||||
statusDuration = 60f;
|
||||
}};
|
||||
|
||||
fragSurge = new FlakBulletType(4.5f, 13){{
|
||||
ammoMultiplier = 4f;
|
||||
splashDamage = 50f;
|
||||
splashDamageRadius = 40f;
|
||||
lightning = 2;
|
||||
lightningLength = 7;
|
||||
shootEffect = Fx.shootBig;
|
||||
collidesGround = true;
|
||||
explodeRange = 20f;
|
||||
}};
|
||||
|
||||
missileExplosive = new MissileBulletType(3.7f, 10){{
|
||||
width = 8f;
|
||||
height = 8f;
|
||||
shrinkY = 0f;
|
||||
drag = -0.01f;
|
||||
splashDamageRadius = 30f;
|
||||
splashDamage = 30f;
|
||||
ammoMultiplier = 4f;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
|
||||
status = StatusEffects.blasted;
|
||||
statusDuration = 60f;
|
||||
}};
|
||||
|
||||
missileIncendiary = new MissileBulletType(3.7f, 12){{
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
width = 7f;
|
||||
height = 8f;
|
||||
shrinkY = 0f;
|
||||
drag = -0.01f;
|
||||
homingPower = 0.08f;
|
||||
splashDamageRadius = 20f;
|
||||
splashDamage = 20f;
|
||||
makeFire = true;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
status = StatusEffects.burning;
|
||||
}};
|
||||
|
||||
missileSurge = new MissileBulletType(3.7f, 18){{
|
||||
width = 8f;
|
||||
height = 8f;
|
||||
shrinkY = 0f;
|
||||
drag = -0.01f;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 25f;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
lightningDamage = 10;
|
||||
lightning = 2;
|
||||
lightningLength = 10;
|
||||
}};
|
||||
|
||||
standardCopper = new BasicBulletType(2.5f, 9){{
|
||||
width = 7f;
|
||||
height = 9f;
|
||||
lifetime = 60f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
smokeEffect = Fx.shootSmallSmoke;
|
||||
ammoMultiplier = 2;
|
||||
}};
|
||||
|
||||
standardDense = new BasicBulletType(3.5f, 18){{
|
||||
width = 9f;
|
||||
height = 12f;
|
||||
reloadMultiplier = 0.6f;
|
||||
ammoMultiplier = 4;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardThorium = new BasicBulletType(4f, 29, "bullet"){{
|
||||
width = 10f;
|
||||
height = 13f;
|
||||
shootEffect = Fx.shootBig;
|
||||
smokeEffect = Fx.shootBigSmoke;
|
||||
ammoMultiplier = 4;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardHoming = new BasicBulletType(3f, 12, "bullet"){{
|
||||
width = 7f;
|
||||
height = 9f;
|
||||
homingPower = 0.08f;
|
||||
reloadMultiplier = 1.5f;
|
||||
ammoMultiplier = 5;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardIncendiary = new BasicBulletType(3.2f, 11, "bullet"){{
|
||||
width = 10f;
|
||||
height = 12f;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
status = StatusEffects.burning;
|
||||
makeFire = true;
|
||||
inaccuracy = 3f;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardDenseBig = new BasicBulletType(7f, 55, "bullet"){{
|
||||
width = 15f;
|
||||
height = 21f;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
standardThoriumBig = new BasicBulletType(8f, 80, "bullet"){{
|
||||
width = 16f;
|
||||
height = 23f;
|
||||
shootEffect = Fx.shootBig;
|
||||
pierceCap = 2;
|
||||
pierceBuilding = true;
|
||||
knockback = 0.7f;
|
||||
}};
|
||||
|
||||
standardIncendiaryBig = new BasicBulletType(7f, 60, "bullet"){{
|
||||
width = 16f;
|
||||
height = 21f;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
status = StatusEffects.burning;
|
||||
shootEffect = Fx.shootBig;
|
||||
makeFire = true;
|
||||
pierceCap = 2;
|
||||
pierceBuilding = true;
|
||||
knockback = 0.7f;
|
||||
}};
|
||||
|
||||
fireball = new BulletType(1f, 4){
|
||||
{
|
||||
pierce = true;
|
||||
collidesTiles = false;
|
||||
collides = false;
|
||||
drag = 0.03f;
|
||||
hitEffect = despawnEffect = Fx.none;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
b.vel.setLength(0.6f + Mathf.random(2f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Pal.lightFlame, Pal.darkFlame, Color.gray, b.fin());
|
||||
Fill.circle(b.x, b.y, 3f * b.fout());
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
if(Mathf.chance(0.04 * Time.delta)){
|
||||
Tile tile = world.tileWorld(b.x, b.y);
|
||||
if(tile != null){
|
||||
Fires.create(tile);
|
||||
}
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.1 * Time.delta)){
|
||||
Fx.fireballsmoke.at(b.x, b.y);
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.1 * Time.delta)){
|
||||
Fx.ballfire.at(b.x, b.y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
basicFlame = new BulletType(3.35f, 16f){{
|
||||
ammoMultiplier = 3f;
|
||||
hitSize = 7f;
|
||||
lifetime = 18f;
|
||||
pierce = true;
|
||||
collidesAir = false;
|
||||
statusDuration = 60f * 4;
|
||||
shootEffect = Fx.shootSmallFlame;
|
||||
hitEffect = Fx.hitFlameSmall;
|
||||
despawnEffect = Fx.none;
|
||||
status = StatusEffects.burning;
|
||||
keepVelocity = false;
|
||||
fireball = new FireBulletType(1f, 4){{
|
||||
hittable = false;
|
||||
}};
|
||||
|
||||
pyraFlame = new BulletType(3.35f, 25f){{
|
||||
ammoMultiplier = 4f;
|
||||
hitSize = 7f;
|
||||
lifetime = 18f;
|
||||
pierce = true;
|
||||
collidesAir = false;
|
||||
statusDuration = 60f * 6;
|
||||
shootEffect = Fx.shootPyraFlame;
|
||||
hitEffect = Fx.hitFlameSmall;
|
||||
despawnEffect = Fx.none;
|
||||
status = StatusEffects.burning;
|
||||
hittable = false;
|
||||
}};
|
||||
|
||||
waterShot = new LiquidBulletType(Liquids.water){{
|
||||
spaceLiquid = new SpaceLiquidBulletType(){{
|
||||
knockback = 0.7f;
|
||||
drag = 0.01f;
|
||||
}};
|
||||
|
||||
cryoShot = new LiquidBulletType(Liquids.cryofluid){{
|
||||
drag = 0.01f;
|
||||
}};
|
||||
|
||||
slagShot = new LiquidBulletType(Liquids.slag){{
|
||||
damage = 4;
|
||||
drag = 0.01f;
|
||||
}};
|
||||
|
||||
oilShot = new LiquidBulletType(Liquids.oil){{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.type.unit.*;
|
||||
import mindustry.world.blocks.defense.turrets.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.content.Blocks.*;
|
||||
import static mindustry.content.SectorPresets.*;
|
||||
import static mindustry.content.TechTree.*;
|
||||
|
||||
public class ErekirTechTree{
|
||||
static IntSet balanced = new IntSet();
|
||||
|
||||
static void rebalanceBullet(BulletType bullet){
|
||||
if(balanced.add(bullet.id)){
|
||||
bullet.damage *= 0.75f;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO remove this
|
||||
public static void rebalance(){
|
||||
for(var unit : content.units().select(u -> u instanceof ErekirUnitType)){
|
||||
for(var weapon : unit.weapons){
|
||||
rebalanceBullet(weapon.bullet);
|
||||
}
|
||||
}
|
||||
|
||||
for(var block : content.blocks()){
|
||||
if(block instanceof Turret turret && Structs.contains(block.requirements, i -> !Items.serpuloItems.contains(i.item))){
|
||||
if(turret instanceof ItemTurret item){
|
||||
for(var bullet : item.ammoTypes.values()){
|
||||
rebalanceBullet(bullet);
|
||||
}
|
||||
}else if(turret instanceof ContinuousLiquidTurret cont){
|
||||
for(var bullet : cont.ammoTypes.values()){
|
||||
rebalanceBullet(bullet);
|
||||
}
|
||||
}else if(turret instanceof ContinuousTurret cont){
|
||||
rebalanceBullet(cont.shootType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
rebalance();
|
||||
|
||||
//TODO might be unnecessary with no asteroids
|
||||
Seq<Objective> erekirSector = Seq.with(new OnPlanet(Planets.erekir));
|
||||
|
||||
var costMultipliers = new ObjectFloatMap<Item>();
|
||||
for(var item : content.items()) costMultipliers.put(item, 0.9f);
|
||||
|
||||
//these are hard to make
|
||||
costMultipliers.put(Items.oxide, 0.5f);
|
||||
costMultipliers.put(Items.surgeAlloy, 0.7f);
|
||||
costMultipliers.put(Items.carbide, 0.3f);
|
||||
costMultipliers.put(Items.phaseFabric, 0.2f);
|
||||
|
||||
Planets.erekir.techTree = nodeRoot("erekir", coreBastion, true, () -> {
|
||||
context().researchCostMultipliers = costMultipliers;
|
||||
|
||||
node(duct, erekirSector, () -> {
|
||||
node(ductRouter, () -> {
|
||||
node(ductBridge, () -> {
|
||||
node(armoredDuct, () -> {
|
||||
node(surgeConveyor, () -> {
|
||||
node(surgeRouter);
|
||||
});
|
||||
});
|
||||
|
||||
node(unitCargoLoader, () -> {
|
||||
node(unitCargoUnloadPoint, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(overflowDuct, Seq.with(new OnSector(aegis)), () -> {
|
||||
node(underflowDuct);
|
||||
node(reinforcedContainer, () -> {
|
||||
node(ductUnloader, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(reinforcedVault, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(reinforcedMessage, Seq.with(new OnSector(aegis)), () -> {
|
||||
node(canvas);
|
||||
});
|
||||
});
|
||||
|
||||
node(reinforcedPayloadConveyor, Seq.with(new OnSector(atlas)), () -> {
|
||||
//TODO should only be unlocked in unit sector
|
||||
node(payloadMassDriver, Seq.with(new Research(siliconArcFurnace), new OnSector(split)), () -> {
|
||||
//TODO further limitations
|
||||
node(payloadLoader, () -> {
|
||||
node(payloadUnloader, () -> {
|
||||
node(largePayloadMassDriver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(constructor, Seq.with(new OnSector(split)), () -> {
|
||||
node(smallDeconstructor, Seq.with(new OnSector(peaks)), () -> {
|
||||
node(largeConstructor, Seq.with(new OnSector(siege)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(deconstructor, Seq.with(new OnSector(siege)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(reinforcedPayloadRouter, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//TODO move into turbine condenser?
|
||||
node(plasmaBore, () -> {
|
||||
node(impactDrill, Seq.with(new OnSector(aegis)), () -> {
|
||||
node(largePlasmaBore, Seq.with(new OnSector(caldera)), () -> {
|
||||
node(eruptionDrill, Seq.with(new OnSector(stronghold)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(largeCliffCrusher, Seq.with(new OnSector(stronghold)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(turbineCondenser, () -> {
|
||||
node(beamNode, () -> {
|
||||
node(ventCondenser, Seq.with(new OnSector(aegis)), () -> {
|
||||
node(chemicalCombustionChamber, Seq.with(new OnSector(basin)), () -> {
|
||||
node(pyrolysisGenerator, Seq.with(new OnSector(crevice)), () -> {
|
||||
node(fluxReactor, Seq.with(new OnSector(crossroads), new Research(cyanogenSynthesizer)), () -> {
|
||||
node(neoplasiaReactor, Seq.with(new OnSector(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(beamTower, Seq.with(new OnSector(peaks)), () -> {
|
||||
|
||||
});
|
||||
|
||||
|
||||
node(regenProjector, Seq.with(new OnSector(peaks)), () -> {
|
||||
//TODO more tiers of build tower or "support" structures like overdrive projectors
|
||||
node(buildTower, Seq.with(new OnSector(stronghold)), () -> {
|
||||
node(shockwaveTower, Seq.with(new OnSector(siege)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(reinforcedConduit, Seq.with(new OnSector(aegis)), () -> {
|
||||
//TODO maybe should be even later
|
||||
node(reinforcedPump, Seq.with(new OnSector(basin)), () -> {
|
||||
//TODO T2 pump, consume cyanogen or similar
|
||||
});
|
||||
|
||||
node(reinforcedLiquidJunction, () -> {
|
||||
node(reinforcedBridgeConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(reinforcedLiquidRouter, () -> {
|
||||
node(reinforcedLiquidContainer, () -> {
|
||||
node(reinforcedLiquidTank, Seq.with(new SectorComplete(intersect)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(cliffCrusher, () -> {
|
||||
node(siliconArcFurnace, () -> {
|
||||
node(electrolyzer, Seq.with(new OnSector(atlas)), () -> {
|
||||
node(oxidationChamber, Seq.with(new Research(tankRefabricator), new OnSector(marsh)), () -> {
|
||||
|
||||
node(surgeCrucible, Seq.with(new OnSector(ravine)), () -> {
|
||||
|
||||
});
|
||||
node(heatRedirector, Seq.with(new OnSector(ravine)), () -> {
|
||||
node(electricHeater, Seq.with(new OnSector(ravine), new Research(afflict)), () -> {
|
||||
node(slagHeater, Seq.with(new OnSector(caldera)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(atmosphericConcentrator, Seq.with(new OnSector(caldera)), () -> {
|
||||
node(cyanogenSynthesizer, Seq.with(new OnSector(siege)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(carbideCrucible, Seq.with(new OnSector(crevice)), () -> {
|
||||
node(phaseSynthesizer, Seq.with(new OnSector(karst)), () -> {
|
||||
node(phaseHeater, Seq.with(new Research(phaseSynthesizer)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(heatRouter, () -> {
|
||||
node(smallHeatRedirector, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(slagIncinerator, Seq.with(new OnSector(basin)), () -> {
|
||||
|
||||
//TODO these are unused.
|
||||
//node(slagCentrifuge, () -> {});
|
||||
//node(heatReactor, () -> {});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
node(breach, Seq.with(new Research(siliconArcFurnace), new Research(tankFabricator)), () -> {
|
||||
node(berylliumWall, () -> {
|
||||
node(berylliumWallLarge, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(tungstenWall, () -> {
|
||||
node(tungstenWallLarge, () -> {
|
||||
node(blastDoor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(reinforcedSurgeWall, () -> {
|
||||
node(reinforcedSurgeWallLarge, () -> {
|
||||
node(shieldedWall, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(carbideWall, () -> {
|
||||
node(carbideWallLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(diffuse, Seq.with(new OnSector(lake)), () -> {
|
||||
node(sublimate, Seq.with(new OnSector(marsh)), () -> {
|
||||
node(afflict, Seq.with(new OnSector(ravine)), () -> {
|
||||
node(titan, Seq.with(new OnSector(stronghold)), () -> {
|
||||
node(lustre, Seq.with(new OnSector(crevice)), () -> {
|
||||
node(smite, Seq.with(new OnSector(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(disperse, Seq.with(new OnSector(stronghold)), () -> {
|
||||
node(scathe, Seq.with(new OnSector(siege)), () -> {
|
||||
node(malign, Seq.with(new SectorComplete(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
node(radar, Seq.with(new Research(beamNode), new Research(turbineCondenser), new Research(tankFabricator), new OnSector(SectorPresets.aegis)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(coreCitadel, Seq.with(new SectorComplete(peaks)), () -> {
|
||||
node(coreAcropolis, Seq.with(new SectorComplete(siege)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(tankFabricator, Seq.with(new Research(siliconArcFurnace), new Research(plasmaBore), new Research(turbineCondenser)), () -> {
|
||||
node(UnitTypes.stell);
|
||||
|
||||
node(unitRepairTower, Seq.with(new OnSector(ravine), new Research(mechRefabricator)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(shipFabricator, Seq.with(new OnSector(lake)), () -> {
|
||||
node(UnitTypes.elude);
|
||||
|
||||
node(mechFabricator, Seq.with(new OnSector(intersect)), () -> {
|
||||
node(UnitTypes.merui);
|
||||
|
||||
node(tankRefabricator, Seq.with(new OnSector(atlas)), () -> {
|
||||
node(UnitTypes.locus);
|
||||
|
||||
node(mechRefabricator, Seq.with(new OnSector(basin)), () -> {
|
||||
node(UnitTypes.cleroi);
|
||||
|
||||
node(shipRefabricator, Seq.with(new OnSector(peaks)), () -> {
|
||||
node(UnitTypes.avert);
|
||||
|
||||
//TODO
|
||||
node(primeRefabricator, Seq.with(new OnSector(stronghold)), () -> {
|
||||
node(UnitTypes.precept);
|
||||
node(UnitTypes.anthicus);
|
||||
node(UnitTypes.obviate);
|
||||
});
|
||||
|
||||
node(tankAssembler, Seq.with(new OnSector(siege), new Research(constructor), new Research(atmosphericConcentrator)), () -> {
|
||||
|
||||
node(UnitTypes.vanquish, () -> {
|
||||
node(UnitTypes.conquer, Seq.with(new OnSector(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(shipAssembler, Seq.with(new OnSector(crossroads)), () -> {
|
||||
node(UnitTypes.quell, () -> {
|
||||
node(UnitTypes.disrupt, Seq.with(new OnSector(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(mechAssembler, Seq.with(new OnSector(crossroads)), () -> {
|
||||
node(UnitTypes.tecta, () -> {
|
||||
node(UnitTypes.collaris, Seq.with(new OnSector(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(basicAssemblerModule, Seq.with(new SectorComplete(karst)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(onset, () -> {
|
||||
node(aegis, Seq.with(new SectorComplete(onset), new Research(ductRouter), new Research(ductBridge)), () -> {
|
||||
node(lake, Seq.with(new SectorComplete(aegis)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(intersect, Seq.with(new SectorComplete(aegis), new SectorComplete(lake), new Research(ventCondenser), new Research(shipFabricator)), () -> {
|
||||
node(atlas, Seq.with(new SectorComplete(intersect), new Research(mechFabricator)), () -> {
|
||||
node(split, Seq.with(new SectorComplete(atlas), new Research(reinforcedPayloadConveyor), new Research(reinforcedContainer)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(basin, Seq.with(new SectorComplete(atlas)), () -> {
|
||||
node(marsh, Seq.with(new SectorComplete(basin)), () -> {
|
||||
node(ravine, Seq.with(new SectorComplete(marsh), new Research(Liquids.slag)), () -> {
|
||||
node(caldera, Seq.with(new SectorComplete(peaks), new Research(heatRedirector)), () -> {
|
||||
node(stronghold, Seq.with(new SectorComplete(caldera), new Research(coreCitadel)), () -> {
|
||||
node(crevice, Seq.with(new SectorComplete(stronghold)), () -> {
|
||||
node(siege, Seq.with(new SectorComplete(crevice)), () -> {
|
||||
node(crossroads, Seq.with(new SectorComplete(siege)), () -> {
|
||||
node(karst, Seq.with(new SectorComplete(crossroads), new Research(coreAcropolis)), () -> {
|
||||
node(origin, Seq.with(new SectorComplete(karst), new Research(coreAcropolis), new Research(UnitTypes.vanquish), new Research(UnitTypes.disrupt), new Research(UnitTypes.collaris), new Research(malign), new Research(basicAssemblerModule), new Research(neoplasiaReactor)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(peaks, Seq.with(new SectorComplete(marsh), new SectorComplete(split)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Items.beryllium, () -> {
|
||||
nodeProduce(Items.sand, () -> {
|
||||
nodeProduce(Items.silicon, () -> {
|
||||
nodeProduce(Items.oxide, () -> {
|
||||
//nodeProduce(Items.fissileMatter, () -> {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Liquids.water, () -> {
|
||||
nodeProduce(Liquids.ozone, () -> {
|
||||
nodeProduce(Liquids.hydrogen, () -> {
|
||||
nodeProduce(Liquids.nitrogen, () -> {
|
||||
|
||||
});
|
||||
|
||||
nodeProduce(Liquids.cyanogen, () -> {
|
||||
nodeProduce(Liquids.neoplasm, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Items.graphite, () -> {
|
||||
nodeProduce(Items.tungsten, () -> {
|
||||
nodeProduce(Liquids.slag, () -> {
|
||||
|
||||
});
|
||||
|
||||
nodeProduce(Liquids.arkycite, () -> {
|
||||
|
||||
});
|
||||
|
||||
nodeProduce(Items.thorium, () -> {
|
||||
nodeProduce(Items.carbide, () -> {
|
||||
|
||||
//nodeProduce(Liquids.gallium, () -> {});
|
||||
});
|
||||
});
|
||||
|
||||
nodeProduce(Items.surgeAlloy, () -> {
|
||||
nodeProduce(Items.phaseFabric, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+1327
-195
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,18 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.*;
|
||||
import mindustry.ctype.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
public class Items implements ContentList{
|
||||
public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phaseFabric, surgeAlloy,
|
||||
sporePod, sand, blastCompound, pyratite, metaglass;
|
||||
public class Items{
|
||||
public static Item
|
||||
scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium,
|
||||
phaseFabric, surgeAlloy, sporePod, sand, blastCompound, pyratite, metaglass,
|
||||
beryllium, tungsten, oxide, carbide, fissileMatter, dormantCyst;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
public static final Seq<Item> serpuloItems = new Seq<>(), erekirItems = new Seq<>(), erekirOnlyItems = new Seq<>();
|
||||
|
||||
public static void load(){
|
||||
copper = new Item("copper", Color.valueOf("d99d73")){{
|
||||
hardness = 1;
|
||||
cost = 0.5f;
|
||||
@@ -19,7 +22,6 @@ public class Items implements ContentList{
|
||||
lead = new Item("lead", Color.valueOf("8c7fa9")){{
|
||||
hardness = 1;
|
||||
cost = 0.7f;
|
||||
alwaysUnlocked = true;
|
||||
}};
|
||||
|
||||
metaglass = new Item("metaglass", Color.valueOf("ebeef5")){{
|
||||
@@ -31,14 +33,17 @@ public class Items implements ContentList{
|
||||
}};
|
||||
|
||||
sand = new Item("sand", Color.valueOf("f7cba4")){{
|
||||
alwaysUnlocked = true;
|
||||
lowPriority = true;
|
||||
buildable = false;
|
||||
//needed to show up as requirement
|
||||
alwaysUnlocked = true;
|
||||
}};
|
||||
|
||||
coal = new Item("coal", Color.valueOf("272727")){{
|
||||
explosiveness = 0.2f;
|
||||
flammability = 1f;
|
||||
hardness = 2;
|
||||
buildable = false;
|
||||
}};
|
||||
|
||||
titanium = new Item("titanium", Color.valueOf("8da1e3")){{
|
||||
@@ -51,10 +56,11 @@ public class Items implements ContentList{
|
||||
hardness = 4;
|
||||
radioactivity = 1f;
|
||||
cost = 1.1f;
|
||||
healthScaling = 0.2f;
|
||||
}};
|
||||
|
||||
scrap = new Item("scrap", Color.valueOf("777777")){{
|
||||
|
||||
cost = 0.5f;
|
||||
}};
|
||||
|
||||
silicon = new Item("silicon", Color.valueOf("53565c")){{
|
||||
@@ -65,29 +71,81 @@ public class Items implements ContentList{
|
||||
flammability = 0.1f;
|
||||
explosiveness = 0.2f;
|
||||
cost = 1.3f;
|
||||
healthScaling = 0.1f;
|
||||
}};
|
||||
|
||||
phaseFabric = new Item("phase-fabric", Color.valueOf("f4ba6e")){{
|
||||
cost = 1.3f;
|
||||
radioactivity = 0.6f;
|
||||
healthScaling = 0.25f;
|
||||
}};
|
||||
|
||||
surgeAlloy = new Item("surge-alloy", Color.valueOf("f3e979")){{
|
||||
cost = 1.2f;
|
||||
charge = 0.75f;
|
||||
healthScaling = 0.25f;
|
||||
}};
|
||||
|
||||
sporePod = new Item("spore-pod", Color.valueOf("7457ce")){{
|
||||
flammability = 1.15f;
|
||||
buildable = false;
|
||||
}};
|
||||
|
||||
blastCompound = new Item("blast-compound", Color.valueOf("ff795e")){{
|
||||
flammability = 0.4f;
|
||||
explosiveness = 1.2f;
|
||||
buildable = false;
|
||||
}};
|
||||
|
||||
pyratite = new Item("pyratite", Color.valueOf("ffaa5f")){{
|
||||
flammability = 1.4f;
|
||||
explosiveness = 0.4f;
|
||||
buildable = false;
|
||||
}};
|
||||
|
||||
beryllium = new Item("beryllium", Color.valueOf("3a8f64")){{
|
||||
hardness = 3;
|
||||
cost = 1.2f;
|
||||
healthScaling = 0.6f;
|
||||
}};
|
||||
|
||||
tungsten = new Item("tungsten", Color.valueOf("768a9a")){{
|
||||
hardness = 5;
|
||||
cost = 1.5f;
|
||||
healthScaling = 0.8f;
|
||||
}};
|
||||
|
||||
oxide = new Item("oxide", Color.valueOf("e4ffd6")){{
|
||||
cost = 1.2f;
|
||||
healthScaling = 0.5f;
|
||||
}};
|
||||
|
||||
carbide = new Item("carbide", Color.valueOf("89769a")){{
|
||||
cost = 1.4f;
|
||||
healthScaling = 1.1f;
|
||||
}};
|
||||
|
||||
fissileMatter = new Item("fissile-matter", Color.valueOf("5e988d")){{
|
||||
radioactivity = 1.5f;
|
||||
hidden = true;
|
||||
}};
|
||||
|
||||
dormantCyst = new Item("dormant-cyst", Color.valueOf("df824d")){{
|
||||
flammability = 0.1f;
|
||||
hidden = true;
|
||||
}};
|
||||
|
||||
serpuloItems.addAll(
|
||||
scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium,
|
||||
phaseFabric, surgeAlloy, sporePod, sand, blastCompound, pyratite, metaglass
|
||||
);
|
||||
|
||||
erekirItems.addAll(
|
||||
graphite, thorium, silicon, phaseFabric, surgeAlloy, sand,
|
||||
beryllium, tungsten, oxide, carbide, fissileMatter, dormantCyst
|
||||
);
|
||||
|
||||
erekirOnlyItems.addAll(erekirItems).removeAll(serpuloItems);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
public class Liquids implements ContentList{
|
||||
public static Liquid water, slag, oil, cryofluid;
|
||||
public class Liquids{
|
||||
public static Liquid water, slag, oil, cryofluid,
|
||||
arkycite, gallium, neoplasm,
|
||||
ozone, hydrogen, nitrogen, cyanogen;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
public static void load(){
|
||||
|
||||
water = new Liquid("water", Color.valueOf("596ab8")){{
|
||||
heatCapacity = 0.4f;
|
||||
alwaysUnlocked = true;
|
||||
effect = StatusEffects.wet;
|
||||
boilPoint = 0.5f;
|
||||
gasColor = Color.grays(0.9f);
|
||||
alwaysUnlocked = true;
|
||||
}};
|
||||
|
||||
slag = new Liquid("slag", Color.valueOf("ffa166")){{
|
||||
@@ -24,12 +26,15 @@ public class Liquids implements ContentList{
|
||||
}};
|
||||
|
||||
oil = new Liquid("oil", Color.valueOf("313131")){{
|
||||
viscosity = 0.7f;
|
||||
viscosity = 0.75f;
|
||||
flammability = 1.2f;
|
||||
explosiveness = 1.2f;
|
||||
heatCapacity = 0.7f;
|
||||
barColor = Color.valueOf("6b675f");
|
||||
effect = StatusEffects.tarred;
|
||||
boilPoint = 0.65f;
|
||||
gasColor = Color.grays(0.4f);
|
||||
canStayOn.add(water);
|
||||
}};
|
||||
|
||||
cryofluid = new Liquid("cryofluid", Color.valueOf("6ecdec")){{
|
||||
@@ -37,6 +42,56 @@ public class Liquids implements ContentList{
|
||||
temperature = 0.25f;
|
||||
effect = StatusEffects.freezing;
|
||||
lightColor = Color.valueOf("0097f5").a(0.2f);
|
||||
boilPoint = 0.55f;
|
||||
gasColor = Color.valueOf("c1e8f5");
|
||||
}};
|
||||
|
||||
neoplasm = new CellLiquid("neoplasm", Color.valueOf("c33e2b")){{
|
||||
heatCapacity = 0.4f;
|
||||
temperature = 0.54f;
|
||||
viscosity = 0.85f;
|
||||
flammability = 0f;
|
||||
capPuddles = false;
|
||||
spreadTarget = Liquids.water;
|
||||
moveThroughBlocks = true;
|
||||
incinerable = false;
|
||||
blockReactive = false;
|
||||
canStayOn.addAll(water, oil, cryofluid);
|
||||
|
||||
colorFrom = Color.valueOf("e8803f");
|
||||
colorTo = Color.valueOf("8c1225");
|
||||
}};
|
||||
|
||||
arkycite = new Liquid("arkycite", Color.valueOf("84a94b")){{
|
||||
flammability = 0.4f;
|
||||
viscosity = 0.7f;
|
||||
neoplasm.canStayOn.add(this);
|
||||
}};
|
||||
|
||||
gallium = new Liquid("gallium", Color.valueOf("9a9dbf")){{
|
||||
coolant = false;
|
||||
hidden = true;
|
||||
}};
|
||||
|
||||
ozone = new Liquid("ozone", Color.valueOf("fc81dd")){{
|
||||
gas = true;
|
||||
barColor = Color.valueOf("d699f0");
|
||||
explosiveness = 1f;
|
||||
flammability = 1f;
|
||||
}};
|
||||
|
||||
hydrogen = new Liquid("hydrogen", Color.valueOf("9eabf7")){{
|
||||
gas = true;
|
||||
flammability = 1f;
|
||||
}};
|
||||
|
||||
nitrogen = new Liquid("nitrogen", Color.valueOf("efe3ff")){{
|
||||
gas = true;
|
||||
}};
|
||||
|
||||
cyanogen = new Liquid("cyanogen", Color.valueOf("89e8b6")){{
|
||||
gas = true;
|
||||
flammability = 2f;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package mindustry.content;
|
||||
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
|
||||
public class Loadouts implements ContentList{
|
||||
public class Loadouts{
|
||||
public static Schematic
|
||||
basicShard,
|
||||
basicFoundation,
|
||||
basicNucleus;
|
||||
basicNucleus,
|
||||
basicBastion;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
basicShard = Schematics.readBase64("bXNjaAB4nD2K2wqAIBiD5ymibnoRn6YnEP1BwUMoBL19FuJ2sbFvUFgYZDaJsLeQrkinN9UJHImsNzlYE7WrIUastuSbnlKx2VJJt+8IQGGKdfO/8J5yrGJSMegLg+YUIA==");
|
||||
basicFoundation = Schematics.readBase64("bXNjaAB4nD1OSQ6DMBBzFhVu8BG+0X8MQyoiJTNSukj8nlCi2Adbtg/GA4OBF8oB00rvyE/9ykafqOIw58A7SWRKy1ZiShhZ5RcOLZhYS1hefQ1gRIeptH9jq/qW2lvc1d2tgWsOfVX/tOwE86AYBA==");
|
||||
basicNucleus = Schematics.readBase64("bXNjaAB4nD2MUQqAIBBEJy0s6qOLdJXuYNtCgikYBd2+LNmdj308hkGHtkId7M4YFns4mk/yfB4a48602eDI+mlNznu0FMPFd0wYKCaewl8F0EOueqM+yKSLVfJrNKWnSw/FZGzEGXFG9sy/px4gEBW1");
|
||||
public static void load(){
|
||||
basicShard = Schematics.readBase64("bXNjaAF4nGNgZmBmZmDJS8xNZZDJKCkpKLbS16/MLy0p1UtK1XcNi/Q3cKwwyqkyYOBOSS1OLsosKMnMz2NgYGDLSUxKzSlmYIqOZWTgSs4vStUtzkgsSgFKMYIQkAAAhSEXTA==");
|
||||
basicFoundation = Schematics.readBase64("bXNjaAF4nGNgYWBhZmDJS8xNZWBNSk3MK2bgTkktTi7KLCjJzM9jYGBgy0lMSs0pZmCKjmVk4E/OL0rVTcsvzUtJhMozghCQAACx6RHB");
|
||||
basicNucleus = Schematics.readBase64("bXNjaAF4nA3CwQ2AIBAEwAXFjxRBA1ZkfCDcgwh3BiTG7iUzMDATZvaFYGOK7pPuLpYXa6QWarqfJAxVsGR/Um7Q+6Fgg1TauIdMvQFQgB7wAza8E4M=");
|
||||
basicBastion = Schematics.readBase64("bXNjaAF4nGNgYWBhZmDJS8xNZWBNzMsEUtwpqcXJRZkFJZn5eQyClfmlCin5Cnn5JQqpFZnFJVwMbDmJSak5xQxM0bGMDDzJ+UWpukmJxWDVDAyMIAQkACMdFqE=");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import mindustry.ctype.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.graphics.g3d.*;
|
||||
import mindustry.graphics.g3d.PlanetGrid.*;
|
||||
import mindustry.maps.planet.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class Planets implements ContentList{
|
||||
public class Planets{
|
||||
public static Planet
|
||||
sun,
|
||||
//tantros,
|
||||
serpulo;
|
||||
erekir,
|
||||
tantros,
|
||||
serpulo,
|
||||
gier,
|
||||
notva,
|
||||
verilus;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
sun = new Planet("sun", null, 0, 2){{
|
||||
public static void load(){
|
||||
sun = new Planet("sun", null, 4f){{
|
||||
bloom = true;
|
||||
accessible = false;
|
||||
|
||||
//lightColor = Color.valueOf("f4ee8e");
|
||||
|
||||
meshLoader = () -> new SunMesh(
|
||||
this, 4,
|
||||
5, 0.3, 1.7, 1.2, 1,
|
||||
@@ -33,22 +43,167 @@ public class Planets implements ContentList{
|
||||
);
|
||||
}};
|
||||
|
||||
/*tantros = new Planet("tantros", sun, 2, 0.8f){{
|
||||
erekir = new Planet("erekir", sun, 1f, 2){{
|
||||
generator = new ErekirPlanetGenerator();
|
||||
meshLoader = () -> new HexMesh(this, 5);
|
||||
cloudMeshLoader = () -> new MultiMesh(
|
||||
new HexSkyMesh(this, 2, 0.15f, 0.14f, 5, Color.valueOf("eba768").a(0.75f), 2, 0.42f, 1f, 0.43f),
|
||||
new HexSkyMesh(this, 3, 0.6f, 0.15f, 5, Color.valueOf("eea293").a(0.75f), 2, 0.42f, 1.2f, 0.45f)
|
||||
);
|
||||
alwaysUnlocked = true;
|
||||
landCloudColor = Color.valueOf("ed6542");
|
||||
atmosphereColor = Color.valueOf("f07218");
|
||||
defaultEnv = Env.scorching | Env.terrestrial;
|
||||
startSector = 10;
|
||||
atmosphereRadIn = 0.02f;
|
||||
atmosphereRadOut = 0.3f;
|
||||
tidalLock = true;
|
||||
orbitSpacing = 2f;
|
||||
totalRadius += 2.6f;
|
||||
lightSrcTo = 0.5f;
|
||||
lightDstFrom = 0.2f;
|
||||
clearSectorOnLose = true;
|
||||
defaultCore = Blocks.coreBastion;
|
||||
iconColor = Color.valueOf("ff9266");
|
||||
enemyBuildSpeedMultiplier = 0.4f;
|
||||
|
||||
//TODO disallowed for now
|
||||
allowLaunchToNumbered = false;
|
||||
|
||||
//TODO SHOULD there be lighting?
|
||||
updateLighting = false;
|
||||
|
||||
defaultAttributes.set(Attribute.heat, 0.8f);
|
||||
|
||||
ruleSetter = r -> {
|
||||
r.waveTeam = Team.malis;
|
||||
r.placeRangeCheck = false;
|
||||
r.showSpawns = true;
|
||||
r.fog = true;
|
||||
r.staticFog = true;
|
||||
r.lighting = false;
|
||||
r.coreDestroyClear = true;
|
||||
r.onlyDepositCore = true;
|
||||
};
|
||||
campaignRuleDefaults.fog = true;
|
||||
campaignRuleDefaults.showSpawns = true;
|
||||
|
||||
unlockedOnLand.add(Blocks.coreBastion);
|
||||
}};
|
||||
|
||||
//TODO names
|
||||
gier = makeAsteroid("gier", erekir, Blocks.ferricStoneWall, Blocks.carbonWall, -5, 0.4f, 7, 1f, gen -> {
|
||||
gen.min = 25;
|
||||
gen.max = 35;
|
||||
gen.carbonChance = 0.6f;
|
||||
gen.iceChance = 0f;
|
||||
gen.berylChance = 0.1f;
|
||||
});
|
||||
|
||||
notva = makeAsteroid("notva", sun, Blocks.ferricStoneWall, Blocks.beryllicStoneWall, -4, 0.55f, 9, 1.3f, gen -> {
|
||||
gen.berylChance = 0.8f;
|
||||
gen.iceChance = 0f;
|
||||
gen.carbonChance = 0.01f;
|
||||
gen.max += 2;
|
||||
});
|
||||
|
||||
tantros = new Planet("tantros", sun, 1f, 2){{
|
||||
generator = new TantrosPlanetGenerator();
|
||||
meshLoader = () -> new HexMesh(this, 4);
|
||||
accessible = false;
|
||||
visible = false;
|
||||
atmosphereColor = Color.valueOf("3db899");
|
||||
iconColor = Color.valueOf("597be3");
|
||||
startSector = 10;
|
||||
atmosphereRadIn = -0.01f;
|
||||
atmosphereRadOut = 0.3f;
|
||||
}};*/
|
||||
defaultEnv = Env.underwater | Env.terrestrial;
|
||||
ruleSetter = r -> {
|
||||
|
||||
serpulo = new Planet("serpulo", sun, 3, 1){{
|
||||
};
|
||||
}};
|
||||
|
||||
serpulo = new Planet("serpulo", sun, 1f, 3){{
|
||||
generator = new SerpuloPlanetGenerator();
|
||||
meshLoader = () -> new HexMesh(this, 6);
|
||||
cloudMeshLoader = () -> new MultiMesh(
|
||||
new HexSkyMesh(this, 11, 0.15f, 0.13f, 5, new Color().set(Pal.spore).mul(0.9f).a(0.75f), 2, 0.45f, 0.9f, 0.38f),
|
||||
new HexSkyMesh(this, 1, 0.6f, 0.16f, 5, Color.white.cpy().lerp(Pal.spore, 0.55f).a(0.75f), 2, 0.45f, 1f, 0.41f)
|
||||
);
|
||||
|
||||
launchCapacityMultiplier = 0.5f;
|
||||
sectorSeed = 2;
|
||||
allowWaves = true;
|
||||
allowLegacyLaunchPads = true;
|
||||
allowWaveSimulation = true;
|
||||
allowSectorInvasion = true;
|
||||
allowLaunchSchematics = true;
|
||||
enemyCoreSpawnReplace = true;
|
||||
allowLaunchLoadout = true;
|
||||
//doesn't play well with configs
|
||||
prebuildBase = false;
|
||||
ruleSetter = r -> {
|
||||
r.waveTeam = Team.crux;
|
||||
r.placeRangeCheck = false;
|
||||
r.showSpawns = false;
|
||||
r.coreDestroyClear = true;
|
||||
};
|
||||
iconColor = Color.valueOf("7d4dff");
|
||||
atmosphereColor = Color.valueOf("3c1b8f");
|
||||
atmosphereRadIn = 0.02f;
|
||||
atmosphereRadOut = 0.3f;
|
||||
startSector = 15;
|
||||
alwaysUnlocked = true;
|
||||
allowSelfSectorLaunch = true;
|
||||
landCloudColor = Pal.spore.cpy().a(0.5f);
|
||||
}};
|
||||
|
||||
verilus = makeAsteroid("verlius", sun, Blocks.stoneWall, Blocks.iceWall, -1, 0.5f, 12, 2f, gen -> {
|
||||
gen.berylChance = 0f;
|
||||
gen.iceChance = 0.6f;
|
||||
gen.carbonChance = 0.1f;
|
||||
gen.ferricChance = 0f;
|
||||
});
|
||||
}
|
||||
|
||||
private static Planet makeAsteroid(String name, Planet parent, Block base, Block tint, int seed, float tintThresh, int pieces, float scale, Cons<AsteroidGenerator> cgen){
|
||||
return new Planet(name, parent, 0.12f){{
|
||||
hasAtmosphere = false;
|
||||
updateLighting = false;
|
||||
sectors.add(new Sector(this, Ptile.empty));
|
||||
camRadius = 0.68f * scale;
|
||||
minZoom = 0.6f;
|
||||
drawOrbit = false;
|
||||
accessible = false;
|
||||
clipRadius = 2f;
|
||||
defaultEnv = Env.space;
|
||||
icon = "commandRally";
|
||||
generator = new AsteroidGenerator();
|
||||
cgen.get((AsteroidGenerator)generator);
|
||||
|
||||
meshLoader = () -> {
|
||||
iconColor = tint.mapColor;
|
||||
Color tinted = tint.mapColor.cpy().a(1f - tint.mapColor.a);
|
||||
Seq<GenericMesh> meshes = new Seq<>();
|
||||
Color color = base.mapColor;
|
||||
Rand rand = new Rand(id + 2);
|
||||
|
||||
meshes.add(new NoiseMesh(
|
||||
this, seed, 2, radius, 2, 0.55f, 0.45f, 14f,
|
||||
color, tinted, 3, 0.6f, 0.38f, tintThresh
|
||||
));
|
||||
|
||||
for(int j = 0; j < pieces; j++){
|
||||
meshes.add(new MatMesh(
|
||||
new NoiseMesh(this, seed + j + 1, 1, 0.022f + rand.random(0.039f) * scale, 2, 0.6f, 0.38f, 20f,
|
||||
color, tinted, 3, 0.6f, 0.38f, tintThresh),
|
||||
new Mat3D().setToTranslation(Tmp.v31.setToRandomDirection(rand).setLength(rand.random(0.44f, 1.4f) * scale)))
|
||||
);
|
||||
}
|
||||
|
||||
return new MultiMesh(meshes.toArray(GenericMesh.class));
|
||||
};
|
||||
}};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
package mindustry.content;
|
||||
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.content.Planets.*;
|
||||
|
||||
public class SectorPresets implements ContentList{
|
||||
public class SectorPresets{
|
||||
public static SectorPreset
|
||||
groundZero,
|
||||
craters, biomassFacility, frozenForest, ruinousShores, windsweptIslands, stainedMountains, tarFields,
|
||||
fungalPass, extractionOutpost, saltFlats, overgrowth,
|
||||
impact0078, desolateRift, nuclearComplex, planetaryTerminal;
|
||||
craters, biomassFacility, taintedWoods, frozenForest, ruinousShores, facility32m, windsweptIslands, stainedMountains, tarFields,
|
||||
frontier, fungalPass, infestedCanyons, atolls, mycelialBastion, extractionOutpost, saltFlats, testingGrounds, overgrowth, //polarAerodrome,
|
||||
impact0078, desolateRift, nuclearComplex, planetaryTerminal,
|
||||
coastline, navalFortress, weatheredChannels, seaPort,
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
geothermalStronghold, cruxscape,
|
||||
|
||||
onset, aegis, lake, intersect, basin, atlas, split, marsh, peaks, ravine, caldera,
|
||||
stronghold, crevice, siege, crossroads, karst, origin;
|
||||
|
||||
public static void load(){
|
||||
//region serpulo
|
||||
|
||||
groundZero = new SectorPreset("groundZero", serpulo, 15){{
|
||||
alwaysUnlocked = true;
|
||||
addStartingItems = true;
|
||||
captureWave = 10;
|
||||
difficulty = 1;
|
||||
overrideLaunchDefaults = true;
|
||||
noLighting = true;
|
||||
startWaveTimeMultiplier = 3f;
|
||||
}};
|
||||
|
||||
saltFlats = new SectorPreset("saltFlats", serpulo, 101){{
|
||||
difficulty = 5;
|
||||
useAI = false;
|
||||
}};
|
||||
|
||||
testingGrounds = new SectorPreset("testingGrounds", serpulo, 3){{
|
||||
difficulty = 7;
|
||||
captureWave = 33;
|
||||
}};
|
||||
|
||||
frozenForest = new SectorPreset("frozenForest", serpulo, 86){{
|
||||
@@ -37,6 +49,11 @@ public class SectorPresets implements ContentList{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
taintedWoods = new SectorPreset("taintedWoods", serpulo, 221){{
|
||||
captureWave = 33;
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
craters = new SectorPreset("craters", serpulo, 18){{
|
||||
captureWave = 20;
|
||||
difficulty = 2;
|
||||
@@ -47,6 +64,15 @@ public class SectorPresets implements ContentList{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
seaPort = new SectorPreset("seaPort", serpulo, 47){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
facility32m = new SectorPreset("facility32m", serpulo, 64){{
|
||||
captureWave = 25;
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
windsweptIslands = new SectorPreset("windsweptIslands", serpulo, 246){{
|
||||
captureWave = 30;
|
||||
difficulty = 4;
|
||||
@@ -59,17 +85,49 @@ public class SectorPresets implements ContentList{
|
||||
|
||||
extractionOutpost = new SectorPreset("extractionOutpost", serpulo, 165){{
|
||||
difficulty = 5;
|
||||
useAI = false;
|
||||
}};
|
||||
|
||||
//TODO: removed for now
|
||||
//polarAerodrome = new SectorPreset("polarAerodrome", serpulo, 68){{
|
||||
// difficulty = 7;
|
||||
//}};
|
||||
|
||||
coastline = new SectorPreset("coastline", serpulo, 108){{
|
||||
captureWave = 30;
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
weatheredChannels = new SectorPreset("weatheredChannels", serpulo, 39){{
|
||||
captureWave = 40;
|
||||
difficulty = 9;
|
||||
}};
|
||||
|
||||
navalFortress = new SectorPreset("navalFortress", serpulo, 216){{
|
||||
difficulty = 8;
|
||||
}};
|
||||
|
||||
frontier = new SectorPreset("frontier", serpulo, 203){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
fungalPass = new SectorPreset("fungalPass", serpulo, 21){{
|
||||
difficulty = 4;
|
||||
useAI = false;
|
||||
}};
|
||||
|
||||
infestedCanyons = new SectorPreset("infestedCanyons", serpulo, 210){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
atolls = new SectorPreset("atolls", serpulo, 1){{
|
||||
difficulty = 7;
|
||||
}};
|
||||
|
||||
mycelialBastion = new SectorPreset("mycelialBastion", serpulo, 260){{
|
||||
difficulty = 8;
|
||||
}};
|
||||
|
||||
overgrowth = new SectorPreset("overgrowth", serpulo, 134){{
|
||||
difficulty = 5;
|
||||
useAI = false;
|
||||
}};
|
||||
|
||||
tarFields = new SectorPreset("tarFields", serpulo, 23){{
|
||||
@@ -94,6 +152,96 @@ public class SectorPresets implements ContentList{
|
||||
|
||||
planetaryTerminal = new SectorPreset("planetaryTerminal", serpulo, 93){{
|
||||
difficulty = 10;
|
||||
isLastSector = true;
|
||||
}};
|
||||
|
||||
geothermalStronghold = new SectorPreset("geothermalStronghold", serpulo, 264){{
|
||||
difficulty = 10;
|
||||
}};
|
||||
|
||||
cruxscape = new SectorPreset("cruxscape", serpulo, 54){{
|
||||
difficulty = 10;
|
||||
}};
|
||||
|
||||
//endregion
|
||||
//region erekir
|
||||
|
||||
onset = new SectorPreset("onset", erekir, 10){{
|
||||
addStartingItems = true;
|
||||
alwaysUnlocked = true;
|
||||
difficulty = 1;
|
||||
}};
|
||||
|
||||
aegis = new SectorPreset("aegis", erekir, 88){{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
lake = new SectorPreset("lake", erekir, 41){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
intersect = new SectorPreset("intersect", erekir, 36){{
|
||||
difficulty = 5;
|
||||
captureWave = 9;
|
||||
attackAfterWaves = true;
|
||||
}};
|
||||
|
||||
atlas = new SectorPreset("atlas", erekir, 14){{ //TODO random sector, pick a better one
|
||||
difficulty = 5;
|
||||
}};
|
||||
|
||||
split = new SectorPreset("split", erekir, 19){{ //TODO random sector, pick a better one
|
||||
difficulty = 2;
|
||||
}};
|
||||
|
||||
basin = new SectorPreset("basin", erekir, 29){{
|
||||
difficulty = 6;
|
||||
}};
|
||||
|
||||
marsh = new SectorPreset("marsh", erekir, 25){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
peaks = new SectorPreset("peaks", erekir, 30){{
|
||||
difficulty = 3;
|
||||
}};
|
||||
|
||||
ravine = new SectorPreset("ravine", erekir, 39){{
|
||||
difficulty = 4;
|
||||
captureWave = 24;
|
||||
}};
|
||||
|
||||
caldera = new SectorPreset("caldera-erekir", erekir, 43){{
|
||||
difficulty = 4;
|
||||
}};
|
||||
|
||||
stronghold = new SectorPreset("stronghold", erekir, 18){{
|
||||
difficulty = 7;
|
||||
}};
|
||||
|
||||
crevice = new SectorPreset("crevice", erekir, 3){{
|
||||
difficulty = 6;
|
||||
captureWave = 46;
|
||||
}};
|
||||
|
||||
siege = new SectorPreset("siege", erekir, 58){{
|
||||
difficulty = 8;
|
||||
}};
|
||||
|
||||
crossroads = new SectorPreset("crossroads", erekir, 37){{
|
||||
difficulty = 7;
|
||||
}};
|
||||
|
||||
karst = new SectorPreset("karst", erekir, 5){{
|
||||
difficulty = 9;
|
||||
captureWave = 10;
|
||||
}};
|
||||
|
||||
origin = new SectorPreset("origin", erekir, 12){{
|
||||
difficulty = 10;
|
||||
isLastSector = true;
|
||||
}};
|
||||
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.struct.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.content.Blocks.*;
|
||||
import static mindustry.content.SectorPresets.craters;
|
||||
import static mindustry.content.SectorPresets.*;
|
||||
import static mindustry.content.TechTree.*;
|
||||
import static mindustry.content.UnitTypes.*;
|
||||
|
||||
public class SerpuloTechTree{
|
||||
|
||||
public static void load(){
|
||||
Planets.serpulo.techTree = nodeRoot("serpulo", coreShard, () -> {
|
||||
|
||||
node(conveyor, () -> {
|
||||
|
||||
node(junction, () -> {
|
||||
node(router, () -> {
|
||||
node(advancedLaunchPad, Seq.with(new SectorComplete(extractionOutpost)), () -> {
|
||||
node(landingPad, () -> {
|
||||
node(interplanetaryAccelerator, Seq.with(new SectorComplete(planetaryTerminal)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(distributor);
|
||||
node(sorter, () -> {
|
||||
node(invertedSorter);
|
||||
node(overflowGate, () -> {
|
||||
node(underflowGate);
|
||||
});
|
||||
});
|
||||
node(container, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
node(unloader);
|
||||
node(vault, Seq.with(new SectorComplete(stainedMountains)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(itemBridge, () -> {
|
||||
node(titaniumConveyor, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(phaseConveyor, () -> {
|
||||
node(massDriver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(payloadConveyor, () -> {
|
||||
node(payloadRouter, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(armoredConveyor, () -> {
|
||||
node(plastaniumConveyor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(coreFoundation, () -> {
|
||||
node(coreNucleus, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(mechanicalDrill, () -> {
|
||||
|
||||
node(mechanicalPump, () -> {
|
||||
node(conduit, () -> {
|
||||
node(liquidJunction, () -> {
|
||||
node(liquidRouter, () -> {
|
||||
node(liquidContainer, () -> {
|
||||
node(liquidTank);
|
||||
});
|
||||
|
||||
node(bridgeConduit);
|
||||
|
||||
node(pulseConduit, Seq.with(new SectorComplete(windsweptIslands)), () -> {
|
||||
node(phaseConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(platedConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(rotaryPump, () -> {
|
||||
node(impulsePump, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(graphitePress, () -> {
|
||||
node(pneumaticDrill, Seq.with(new SectorComplete(frozenForest)), () -> {
|
||||
node(cultivator, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(laserDrill, () -> {
|
||||
node(blastDrill, Seq.with(new SectorComplete(nuclearComplex)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(waterExtractor, Seq.with(new SectorComplete(saltFlats)), () -> {
|
||||
node(oilExtractor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(pyratiteMixer, () -> {
|
||||
node(blastMixer, Seq.with(new SectorComplete(facility32m)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(siliconSmelter, () -> {
|
||||
|
||||
node(sporePress, () -> {
|
||||
node(coalCentrifuge, () -> {
|
||||
node(multiPress, () -> {
|
||||
node(siliconCrucible, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(plastaniumCompressor, Seq.with(new SectorComplete(windsweptIslands), new OnSector(tarFields)), () -> {
|
||||
node(phaseWeaver, Seq.with(new SectorComplete(tarFields)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(kiln, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(pulverizer, () -> {
|
||||
node(incinerator, () -> {
|
||||
node(melter, () -> {
|
||||
node(surgeSmelter, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(separator, () -> {
|
||||
node(disassembler, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(cryofluidMixer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//logic disabled until further notice
|
||||
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(repairTurret, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(duo, () -> {
|
||||
node(copperWall, () -> {
|
||||
node(copperWallLarge, () -> {
|
||||
node(scrapWall, () -> {
|
||||
node(scrapWallLarge, () -> {
|
||||
node(scrapWallHuge, () -> {
|
||||
node(scrapWallGigantic);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(titaniumWall, () -> {
|
||||
node(titaniumWallLarge);
|
||||
|
||||
node(door, () -> {
|
||||
node(doorLarge);
|
||||
});
|
||||
|
||||
node(plastaniumWall, () -> {
|
||||
node(plastaniumWallLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
node(thoriumWall, () -> {
|
||||
node(thoriumWallLarge);
|
||||
node(surgeWall, () -> {
|
||||
node(surgeWallLarge);
|
||||
node(phaseWall, () -> {
|
||||
node(phaseWallLarge);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(scatter, () -> {
|
||||
node(hail, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(salvo, () -> {
|
||||
node(swarmer, () -> {
|
||||
node(cyclone, () -> {
|
||||
node(spectre, Seq.with(new SectorComplete(nuclearComplex)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(ripple, () -> {
|
||||
node(fuse, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(scorch, () -> {
|
||||
node(arc, () -> {
|
||||
node(wave, () -> {
|
||||
node(parallax, () -> {
|
||||
node(segment, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(tsunami, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(lancer, () -> {
|
||||
node(meltdown, () -> {
|
||||
node(foreshadow, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(shockMine, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(groundFactory, () -> {
|
||||
|
||||
node(dagger, () -> {
|
||||
node(mace, () -> {
|
||||
node(fortress, () -> {
|
||||
node(scepter, () -> {
|
||||
node(reign, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(nova, () -> {
|
||||
node(pulsar, () -> {
|
||||
node(quasar, () -> {
|
||||
node(vela, () -> {
|
||||
node(corvus, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//override research requirements to have graphite, not coal
|
||||
node(crawler, ItemStack.with(Items.silicon, 400, Items.graphite, 400), () -> {
|
||||
node(atrax, () -> {
|
||||
node(spiroct, () -> {
|
||||
node(arkyid, () -> {
|
||||
node(toxopid, Seq.with(new SectorComplete(mycelialBastion)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(airFactory, () -> {
|
||||
node(flare, () -> {
|
||||
node(horizon, () -> {
|
||||
node(zenith, () -> {
|
||||
node(antumbra, () -> {
|
||||
node(eclipse, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(mono, () -> {
|
||||
node(poly, () -> {
|
||||
node(mega, () -> {
|
||||
node(quad, () -> {
|
||||
node(oct, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(navalFactory, Seq.with(new OnSector(windsweptIslands)), () -> {
|
||||
node(risso, () -> {
|
||||
node(minke, () -> {
|
||||
node(bryde, () -> {
|
||||
node(sei, () -> {
|
||||
node(omura, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(retusa, Seq.with(new SectorComplete(windsweptIslands)), () -> {
|
||||
node(oxynoe, Seq.with(new SectorComplete(coastline)), () -> {
|
||||
node(cyerce, () -> {
|
||||
node(aegires, () -> {
|
||||
node(navanax, Seq.with(new SectorComplete(navalFortress)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(additiveReconstructor, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
node(multiplicativeReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
|
||||
node(exponentialReconstructor, () -> {
|
||||
node(tetrativeReconstructor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(groundZero, () -> {
|
||||
node(frozenForest, Seq.with(
|
||||
new SectorComplete(groundZero),
|
||||
new Research(junction),
|
||||
new Research(router)
|
||||
), () -> {
|
||||
node(craters, Seq.with(
|
||||
new SectorComplete(frozenForest),
|
||||
new Research(mender),
|
||||
new Research(combustionGenerator)
|
||||
), () -> {
|
||||
node(frontier, Seq.with(
|
||||
new Research(groundFactory),
|
||||
new Research(airFactory),
|
||||
new Research(thermalGenerator),
|
||||
new Research(dagger),
|
||||
new Research(mono)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(ruinousShores, Seq.with(
|
||||
new SectorComplete(craters),
|
||||
new Research(graphitePress),
|
||||
new Research(kiln),
|
||||
new Research(mechanicalPump)
|
||||
), () -> {
|
||||
node(windsweptIslands, Seq.with(
|
||||
new SectorComplete(ruinousShores),
|
||||
new Research(pneumaticDrill),
|
||||
new Research(hail),
|
||||
new Research(siliconSmelter),
|
||||
new Research(steamGenerator)
|
||||
), () -> {
|
||||
node(seaPort, Seq.with(
|
||||
new SectorComplete(biomassFacility),
|
||||
new Research(navalFactory),
|
||||
new Research(risso),
|
||||
new Research(retusa),
|
||||
new Research(steamGenerator),
|
||||
new Research(cultivator),
|
||||
new Research(coalCentrifuge)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
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(sei),
|
||||
new Research(omura),
|
||||
new Research(spectre),
|
||||
new Research(advancedLaunchPad),
|
||||
new Research(massDriver),
|
||||
new Research(impactReactor),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(exponentialReconstructor),
|
||||
new Research(tetrativeReconstructor)
|
||||
), () -> {
|
||||
node(geothermalStronghold, Seq.with(
|
||||
new Research(omura),
|
||||
new Research(navanax),
|
||||
new Research(eclipse),
|
||||
new Research(oct),
|
||||
new Research(reign),
|
||||
new Research(corvus),
|
||||
new Research(toxopid)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(cruxscape, Seq.with(
|
||||
new Research(omura),
|
||||
new Research(navanax),
|
||||
new Research(eclipse),
|
||||
new Research(oct),
|
||||
new Research(reign),
|
||||
new Research(corvus),
|
||||
new Research(toxopid)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(facility32m, Seq.with(
|
||||
new Research(pneumaticDrill),
|
||||
new SectorComplete(stainedMountains)
|
||||
), () -> {
|
||||
node(extractionOutpost, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new SectorComplete(facility32m),
|
||||
new Research(groundFactory),
|
||||
new Research(nova),
|
||||
new Research(airFactory),
|
||||
new Research(mono)
|
||||
), () -> {
|
||||
//TODO: removed for now
|
||||
/*node(polarAerodrome, Seq.with(
|
||||
new SectorComplete(fungalPass),
|
||||
new SectorComplete(desolateRift),
|
||||
new SectorComplete(overgrowth),
|
||||
new Research(multiplicativeReconstructor),
|
||||
new Research(zenith),
|
||||
new Research(swarmer),
|
||||
new Research(cyclone),
|
||||
new Research(blastDrill),
|
||||
new Research(blastDrill),
|
||||
new Research(massDriver)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
||||
node(saltFlats, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new Research(groundFactory),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(airFactory),
|
||||
new Research(door)
|
||||
), () -> {
|
||||
node(testingGrounds, Seq.with(
|
||||
new Research(cryofluidMixer),
|
||||
new Research(Liquids.cryofluid),
|
||||
new Research(waterExtractor),
|
||||
new Research(ripple)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(coastline, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new SectorComplete(saltFlats),
|
||||
new Research(navalFactory),
|
||||
new Research(payloadConveyor)
|
||||
), () -> {
|
||||
|
||||
node(navalFortress, Seq.with(
|
||||
new SectorComplete(coastline),
|
||||
new SectorComplete(extractionOutpost),
|
||||
new Research(coreNucleus),
|
||||
new Research(massDriver),
|
||||
new Research(oxynoe),
|
||||
new Research(minke),
|
||||
new Research(bryde),
|
||||
new Research(cyclone),
|
||||
new Research(ripple)
|
||||
), () -> {
|
||||
node(weatheredChannels, Seq.with(
|
||||
new SectorComplete(impact0078),
|
||||
new Research(bryde),
|
||||
new Research(surgeSmelter),
|
||||
new Research(overdriveProjector)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(overgrowth, Seq.with(
|
||||
new SectorComplete(craters),
|
||||
new SectorComplete(fungalPass),
|
||||
new Research(cultivator),
|
||||
new Research(sporePress),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(UnitTypes.mace),
|
||||
new Research(UnitTypes.flare)
|
||||
), () -> {
|
||||
node(mycelialBastion, Seq.with(
|
||||
new Research(atrax),
|
||||
new Research(spiroct),
|
||||
new Research(multiplicativeReconstructor),
|
||||
new Research(exponentialReconstructor)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(atolls, Seq.with(
|
||||
new SectorComplete(windsweptIslands),
|
||||
new Research(multiplicativeReconstructor),
|
||||
new Research(mega)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(biomassFacility, Seq.with(
|
||||
new SectorComplete(frozenForest),
|
||||
new Research(powerNode),
|
||||
new Research(steamGenerator),
|
||||
new Research(scatter),
|
||||
new Research(graphitePress)
|
||||
), () -> {
|
||||
node(taintedWoods, Seq.with(
|
||||
new SectorComplete(biomassFacility),
|
||||
new Research(Items.sporePod),
|
||||
new Research(wave)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(stainedMountains, Seq.with(
|
||||
new SectorComplete(biomassFacility),
|
||||
new Research(pneumaticDrill),
|
||||
new Research(siliconSmelter)
|
||||
), () -> {
|
||||
node(fungalPass, Seq.with(
|
||||
new SectorComplete(stainedMountains),
|
||||
new Research(groundFactory),
|
||||
new Research(door)
|
||||
), () -> {
|
||||
node(infestedCanyons, Seq.with(
|
||||
new SectorComplete(fungalPass),
|
||||
new Research(navalFactory),
|
||||
new Research(risso),
|
||||
new Research(minke),
|
||||
new Research(additiveReconstructor)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
|
||||
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, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,34 +3,33 @@ package mindustry.content;
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.graphics.*;
|
||||
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class StatusEffects implements ContentList{
|
||||
public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed;
|
||||
public class StatusEffects{
|
||||
public static StatusEffect none, burning, freezing, unmoving, slow, fast, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed, electrified, invincible, dynamic;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
public static void load(){
|
||||
|
||||
none = new StatusEffect("none");
|
||||
|
||||
burning = new StatusEffect("burning"){{
|
||||
color = Pal.lightFlame;
|
||||
damage = 0.12f; //over 8 seconds, this would be 60 damage
|
||||
color = Color.valueOf("ffc455");
|
||||
damage = 0.167f;
|
||||
effect = Fx.burning;
|
||||
transitionDamage = 8f;
|
||||
|
||||
init(() -> {
|
||||
opposite(wet, freezing);
|
||||
trans(tarred, ((unit, time, newTime, result) -> {
|
||||
unit.damagePierce(8f);
|
||||
affinity(tarred, (unit, result, time) -> {
|
||||
unit.damagePierce(transitionDamage);
|
||||
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));
|
||||
}));
|
||||
result.set(burning, Math.min(time + result.time, 300f));
|
||||
});
|
||||
});
|
||||
}};
|
||||
|
||||
@@ -39,25 +38,37 @@ public class StatusEffects implements ContentList{
|
||||
speedMultiplier = 0.6f;
|
||||
healthMultiplier = 0.8f;
|
||||
effect = Fx.freezing;
|
||||
transitionDamage = 18f;
|
||||
|
||||
init(() -> {
|
||||
opposite(melting, burning);
|
||||
|
||||
trans(blasted, ((unit, time, newTime, result) -> {
|
||||
unit.damagePierce(18f);
|
||||
result.set(this, time);
|
||||
}));
|
||||
affinity(blasted, (unit, result, time) -> {
|
||||
unit.damagePierce(transitionDamage);
|
||||
if(unit.team == state.rules.waveTeam){
|
||||
Events.fire(Trigger.blastFreeze);
|
||||
}
|
||||
});
|
||||
});
|
||||
}};
|
||||
|
||||
unmoving = new StatusEffect("unmoving"){{
|
||||
color = Pal.gray;
|
||||
speedMultiplier = 0.001f;
|
||||
speedMultiplier = 0f;
|
||||
}};
|
||||
|
||||
slow = new StatusEffect("slow"){{
|
||||
color = Pal.lightishGray;
|
||||
speedMultiplier = 0.4f;
|
||||
|
||||
init(() -> opposite(fast));
|
||||
}};
|
||||
|
||||
fast = new StatusEffect("fast"){{
|
||||
color = Pal.boostTo;
|
||||
speedMultiplier = 1.6f;
|
||||
|
||||
init(() -> opposite(slow));
|
||||
}};
|
||||
|
||||
wet = new StatusEffect("wet"){{
|
||||
@@ -65,24 +76,26 @@ public class StatusEffects implements ContentList{
|
||||
speedMultiplier = 0.94f;
|
||||
effect = Fx.wet;
|
||||
effectChance = 0.09f;
|
||||
transitionDamage = 14;
|
||||
|
||||
init(() -> {
|
||||
trans(shocked, ((unit, time, newTime, result) -> {
|
||||
unit.damagePierce(14f);
|
||||
affinity(shocked, (unit, result, time) -> {
|
||||
unit.damage(transitionDamage);
|
||||
|
||||
if(unit.team == state.rules.waveTeam){
|
||||
Events.fire(Trigger.shock);
|
||||
}
|
||||
result.set(this, time);
|
||||
}));
|
||||
opposite(burning);
|
||||
});
|
||||
opposite(burning, melting);
|
||||
});
|
||||
}};
|
||||
|
||||
|
||||
muddy = new StatusEffect("muddy"){{
|
||||
color = Color.valueOf("46382a");
|
||||
speedMultiplier = 0.94f;
|
||||
effect = Fx.muddy;
|
||||
effectChance = 0.09f;
|
||||
show = false;
|
||||
}};
|
||||
|
||||
melting = new StatusEffect("melting"){{
|
||||
@@ -94,11 +107,11 @@ public class StatusEffects implements ContentList{
|
||||
|
||||
init(() -> {
|
||||
opposite(wet, freezing);
|
||||
trans(tarred, ((unit, time, newTime, result) -> {
|
||||
affinity(tarred, (unit, result, time) -> {
|
||||
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));
|
||||
}));
|
||||
result.set(melting, Math.min(time + result.time, 200f));
|
||||
});
|
||||
});
|
||||
}};
|
||||
|
||||
@@ -110,6 +123,14 @@ public class StatusEffects implements ContentList{
|
||||
effectChance = 0.1f;
|
||||
}};
|
||||
|
||||
electrified = new StatusEffect("electrified"){{
|
||||
color = Pal.heal;
|
||||
speedMultiplier = 0.7f;
|
||||
reloadMultiplier = 0.6f;
|
||||
effect = Fx.electrified;
|
||||
effectChance = 0.1f;
|
||||
}};
|
||||
|
||||
sporeSlowed = new StatusEffect("spore-slowed"){{
|
||||
color = Pal.spore;
|
||||
speedMultiplier = 0.8f;
|
||||
@@ -123,8 +144,8 @@ public class StatusEffects implements ContentList{
|
||||
effect = Fx.oily;
|
||||
|
||||
init(() -> {
|
||||
trans(melting, ((unit, time, newTime, result) -> result.set(melting, newTime + time)));
|
||||
trans(burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
|
||||
affinity(melting, (unit, result, time) -> result.set(melting, result.time + time));
|
||||
affinity(burning, (unit, result, time) -> result.set(burning, result.time + time));
|
||||
});
|
||||
}};
|
||||
|
||||
@@ -153,7 +174,7 @@ public class StatusEffects implements ContentList{
|
||||
}};
|
||||
|
||||
boss = new StatusEffect("boss"){{
|
||||
color = Pal.health;
|
||||
color = Team.crux.color;
|
||||
permanent = true;
|
||||
damageMultiplier = 1.3f;
|
||||
healthMultiplier = 1.5f;
|
||||
@@ -161,15 +182,32 @@ public class StatusEffects implements ContentList{
|
||||
|
||||
shocked = new StatusEffect("shocked"){{
|
||||
color = Pal.lancerLaser;
|
||||
reactive = true;
|
||||
}};
|
||||
|
||||
blasted = new StatusEffect("blasted"){{
|
||||
color = Color.valueOf("ff795e");
|
||||
reactive = true;
|
||||
}};
|
||||
|
||||
corroded = new StatusEffect("corroded"){{
|
||||
color = Pal.plastanium;
|
||||
damage = 0.1f;
|
||||
}};
|
||||
|
||||
disarmed = new StatusEffect("disarmed"){{
|
||||
color = Color.valueOf("e9ead3");
|
||||
disarm = true;
|
||||
}};
|
||||
|
||||
invincible = new StatusEffect("invincible"){{
|
||||
healthMultiplier = Float.POSITIVE_INFINITY;
|
||||
}};
|
||||
|
||||
dynamic = new StatusEffect("dynamic"){{
|
||||
show = false;
|
||||
dynamic = true;
|
||||
permanent = true;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package mindustry.content;
|
||||
|
||||
public class TeamEntries{
|
||||
|
||||
public static void load(){
|
||||
//more will be added later - do these need references?
|
||||
|
||||
//TODO
|
||||
//new TeamEntry(Team.derelict);
|
||||
//new TeamEntry(Team.sharded);
|
||||
//new TeamEntry(Team.malis);
|
||||
//new TeamEntry(Team.crux);
|
||||
}
|
||||
}
|
||||
@@ -1,652 +1,42 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.content.Blocks.*;
|
||||
import static mindustry.content.SectorPresets.craters;
|
||||
import static mindustry.content.SectorPresets.*;
|
||||
import static mindustry.content.UnitTypes.*;
|
||||
/** Class for storing a list of TechNodes with some utility tree builder methods; context dependent. See {@link SerpuloTechTree#load} source for example usage. */
|
||||
public class TechTree{
|
||||
private static TechNode context = null;
|
||||
|
||||
public class TechTree implements ContentList{
|
||||
static ObjectMap<UnlockableContent, TechNode> map = new ObjectMap<>();
|
||||
static TechNode context = null;
|
||||
public static Seq<TechNode> all = new Seq<>();
|
||||
public static Seq<TechNode> roots = new Seq<>();
|
||||
|
||||
public static Seq<TechNode> all;
|
||||
public static TechNode root;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
setup();
|
||||
|
||||
root = node(coreShard, () -> {
|
||||
|
||||
node(conveyor, () -> {
|
||||
|
||||
node(junction, () -> {
|
||||
node(router, () -> {
|
||||
node(launchPad, Seq.with(new SectorComplete(extractionOutpost)), () -> {
|
||||
node(interplanetaryAccelerator, Seq.with(new SectorComplete(planetaryTerminal)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(distributor);
|
||||
node(sorter, () -> {
|
||||
node(invertedSorter);
|
||||
node(overflowGate, () -> {
|
||||
node(underflowGate);
|
||||
});
|
||||
});
|
||||
node(container, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
node(unloader);
|
||||
node(vault, Seq.with(new SectorComplete(stainedMountains)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(itemBridge, () -> {
|
||||
node(titaniumConveyor, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(phaseConveyor, () -> {
|
||||
node(massDriver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(payloadConveyor, () -> {
|
||||
node(payloadRouter, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(armoredConveyor, () -> {
|
||||
node(plastaniumConveyor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(coreFoundation, () -> {
|
||||
node(coreNucleus, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(mechanicalDrill, () -> {
|
||||
|
||||
node(mechanicalPump, () -> {
|
||||
node(conduit, () -> {
|
||||
node(liquidJunction, () -> {
|
||||
node(liquidRouter, () -> {
|
||||
node(liquidTank);
|
||||
|
||||
node(bridgeConduit);
|
||||
|
||||
node(pulseConduit, Seq.with(new SectorComplete(windsweptIslands)), () -> {
|
||||
node(phaseConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(platedConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(rotaryPump, () -> {
|
||||
node(thermalPump, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(graphitePress, () -> {
|
||||
node(pneumaticDrill, Seq.with(new SectorComplete(frozenForest)), () -> {
|
||||
node(cultivator, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(laserDrill, () -> {
|
||||
node(blastDrill, Seq.with(new SectorComplete(nuclearComplex)), () -> {
|
||||
|
||||
});
|
||||
|
||||
node(waterExtractor, Seq.with(new SectorComplete(saltFlats)), () -> {
|
||||
node(oilExtractor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(pyratiteMixer, () -> {
|
||||
node(blastMixer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(siliconSmelter, () -> {
|
||||
|
||||
node(sporePress, () -> {
|
||||
node(coalCentrifuge, () -> {
|
||||
node(multiPress, () -> {
|
||||
node(siliconCrucible, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(plastaniumCompressor, Seq.with(new SectorComplete(windsweptIslands)), () -> {
|
||||
node(phaseWeaver, Seq.with(new SectorComplete(tarFields)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(kiln, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(pulverizer, () -> {
|
||||
node(incinerator, () -> {
|
||||
node(melter, () -> {
|
||||
node(surgeSmelter, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(separator, () -> {
|
||||
node(disassembler, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(cryofluidMixer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(duo, () -> {
|
||||
node(copperWall, () -> {
|
||||
node(copperWallLarge, () -> {
|
||||
node(titaniumWall, () -> {
|
||||
node(titaniumWallLarge);
|
||||
|
||||
node(door, () -> {
|
||||
node(doorLarge);
|
||||
});
|
||||
node(plastaniumWall, () -> {
|
||||
node(plastaniumWallLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
node(thoriumWall, () -> {
|
||||
node(thoriumWallLarge);
|
||||
node(surgeWall, () -> {
|
||||
node(surgeWallLarge);
|
||||
node(phaseWall, () -> {
|
||||
node(phaseWallLarge);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(scatter, () -> {
|
||||
node(hail, Seq.with(new SectorComplete(craters)), () -> {
|
||||
node(salvo, () -> {
|
||||
node(swarmer, () -> {
|
||||
node(cyclone, () -> {
|
||||
node(spectre, Seq.with(new SectorComplete(nuclearComplex)), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(ripple, () -> {
|
||||
node(fuse, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(scorch, () -> {
|
||||
node(arc, () -> {
|
||||
node(wave, () -> {
|
||||
node(parallax, () -> {
|
||||
node(segment, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(tsunami, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(lancer, () -> {
|
||||
node(meltdown, () -> {
|
||||
node(foreshadow, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(shockMine, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(groundFactory, () -> {
|
||||
node(commandCenter, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(dagger, () -> {
|
||||
node(mace, () -> {
|
||||
node(fortress, () -> {
|
||||
node(scepter, () -> {
|
||||
node(reign, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(nova, () -> {
|
||||
node(pulsar, () -> {
|
||||
node(quasar, () -> {
|
||||
node(vela, () -> {
|
||||
node(corvus, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(crawler, () -> {
|
||||
node(atrax, () -> {
|
||||
node(spiroct, () -> {
|
||||
node(arkyid, () -> {
|
||||
node(toxopid, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(airFactory, () -> {
|
||||
node(flare, () -> {
|
||||
node(horizon, () -> {
|
||||
node(zenith, () -> {
|
||||
node(antumbra, () -> {
|
||||
node(eclipse, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(mono, () -> {
|
||||
node(poly, () -> {
|
||||
node(mega, () -> {
|
||||
node(quad, () -> {
|
||||
node(oct, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(navalFactory, Seq.with(new SectorComplete(ruinousShores)), () -> {
|
||||
node(risso, () -> {
|
||||
node(minke, () -> {
|
||||
node(bryde, () -> {
|
||||
node(sei, () -> {
|
||||
node(omura, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(additiveReconstructor, Seq.with(new SectorComplete(biomassFacility)), () -> {
|
||||
node(multiplicativeReconstructor, () -> {
|
||||
node(exponentialReconstructor, Seq.with(new SectorComplete(overgrowth)), () -> {
|
||||
node(tetrativeReconstructor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(groundZero, () -> {
|
||||
node(frozenForest, Seq.with(
|
||||
new SectorComplete(groundZero),
|
||||
new Research(junction),
|
||||
new Research(router)
|
||||
), () -> {
|
||||
node(craters, Seq.with(
|
||||
new SectorComplete(frozenForest),
|
||||
new Research(mender),
|
||||
new Research(combustionGenerator)
|
||||
), () -> {
|
||||
node(ruinousShores, Seq.with(
|
||||
new SectorComplete(craters),
|
||||
new Research(graphitePress),
|
||||
new Research(combustionGenerator),
|
||||
new Research(kiln),
|
||||
new Research(mechanicalPump)
|
||||
), () -> {
|
||||
node(windsweptIslands, Seq.with(
|
||||
new SectorComplete(ruinousShores),
|
||||
new Research(pneumaticDrill),
|
||||
new Research(hail),
|
||||
new Research(siliconSmelter),
|
||||
new Research(steamGenerator)
|
||||
), () -> {
|
||||
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(windsweptIslands),
|
||||
new Research(commandCenter),
|
||||
new Research(groundFactory),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(airFactory),
|
||||
new Research(door)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(overgrowth, Seq.with(
|
||||
new SectorComplete(craters),
|
||||
new SectorComplete(fungalPass),
|
||||
new Research(cultivator),
|
||||
new Research(sporePress),
|
||||
new Research(additiveReconstructor),
|
||||
new Research(UnitTypes.mace),
|
||||
new Research(UnitTypes.flare)
|
||||
), () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(biomassFacility, Seq.with(
|
||||
new SectorComplete(frozenForest),
|
||||
new Research(powerNode),
|
||||
new Research(steamGenerator),
|
||||
new Research(scatter),
|
||||
new Research(graphitePress)
|
||||
), () -> {
|
||||
node(stainedMountains, Seq.with(
|
||||
new SectorComplete(biomassFacility),
|
||||
new Research(pneumaticDrill),
|
||||
new Research(siliconSmelter)
|
||||
), () -> {
|
||||
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, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
public static TechNode nodeRoot(String name, UnlockableContent content, Runnable children){
|
||||
return nodeRoot(name, content, false, children);
|
||||
}
|
||||
|
||||
public static void setup(){
|
||||
context = null;
|
||||
map = new ObjectMap<>();
|
||||
all = new Seq<>();
|
||||
public static TechNode nodeRoot(String name, UnlockableContent content, boolean requireUnlock, Runnable children){
|
||||
var root = node(content, content.researchRequirements(), children);
|
||||
root.name = name;
|
||||
root.requiresUnlock = requireUnlock;
|
||||
roots.add(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
//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){
|
||||
public static TechNode node(UnlockableContent content, Runnable children){
|
||||
return node(content, content.researchRequirements(), children);
|
||||
}
|
||||
|
||||
static TechNode node(UnlockableContent content, ItemStack[] requirements, Runnable children){
|
||||
public static TechNode node(UnlockableContent content, ItemStack[] requirements, Runnable children){
|
||||
return node(content, requirements, null, children);
|
||||
}
|
||||
|
||||
static TechNode node(UnlockableContent content, ItemStack[] requirements, Seq<Objective> objectives, Runnable children){
|
||||
public 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);
|
||||
@@ -660,61 +50,74 @@ public class TechTree implements ContentList{
|
||||
return node;
|
||||
}
|
||||
|
||||
static TechNode node(UnlockableContent content, Seq<Objective> objectives, Runnable children){
|
||||
public static TechNode node(UnlockableContent content, Seq<Objective> objectives, Runnable children){
|
||||
return node(content, content.researchRequirements(), objectives, children);
|
||||
}
|
||||
|
||||
static TechNode node(UnlockableContent block){
|
||||
public static TechNode node(UnlockableContent block){
|
||||
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 TechNode nodeProduce(UnlockableContent content, Seq<Objective> objectives, Runnable children){
|
||||
return node(content, content.researchRequirements(), objectives.add(new Produce(content)), children);
|
||||
}
|
||||
|
||||
static TechNode nodeProduce(UnlockableContent content, Runnable children){
|
||||
public static TechNode nodeProduce(UnlockableContent content, Runnable children){
|
||||
return nodeProduce(content, new Seq<>(), children);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TechNode get(UnlockableContent content){
|
||||
return map.get(content);
|
||||
}
|
||||
|
||||
public static TechNode getNotNull(UnlockableContent content){
|
||||
return map.getThrow(content, () -> new RuntimeException(content + " does not have a tech node"));
|
||||
public static @Nullable TechNode context(){
|
||||
return context;
|
||||
}
|
||||
|
||||
public static class TechNode{
|
||||
/** Depth in tech tree. */
|
||||
public int depth;
|
||||
/** Icon displayed in tech tree selector. */
|
||||
public @Nullable Drawable icon;
|
||||
/** Name for root node - used in tech tree selector. */
|
||||
public @Nullable String name;
|
||||
/** For roots only. If true, this needs to be unlocked before it is selectable in the research dialog. Does not apply when you are on the planet itself. */
|
||||
public boolean requiresUnlock = false;
|
||||
/** Requirement node. */
|
||||
public @Nullable TechNode parent;
|
||||
/** Multipliers for research costs on a per-item basis. Inherits from parent. */
|
||||
public @Nullable ObjectFloatMap<Item> researchCostMultipliers;
|
||||
/** Content to be researched. */
|
||||
public UnlockableContent content;
|
||||
/** Item requirements for this content. */
|
||||
public ItemStack[] requirements;
|
||||
/** Requirements that have been fulfilled. Always the same length as the requirement array. */
|
||||
public final ItemStack[] finishedRequirements;
|
||||
public ItemStack[] finishedRequirements;
|
||||
/** Extra objectives needed to research this. */
|
||||
public Seq<Objective> objectives = new Seq<>();
|
||||
/** Nodes that depend on this node. */
|
||||
public final Seq<TechNode> children = new Seq<>();
|
||||
/** Planet associated with this tech node. Null to auto-detect, or use Serpulo if no associated planet is found. */
|
||||
public @Nullable Planet planet;
|
||||
|
||||
public TechNode(@Nullable TechNode parent, UnlockableContent content, ItemStack[] requirements){
|
||||
if(parent != null) parent.children.add(this);
|
||||
if(parent != null){
|
||||
parent.children.add(this);
|
||||
planet = parent.planet;
|
||||
researchCostMultipliers = parent.researchCostMultipliers;
|
||||
}else if(researchCostMultipliers == null){
|
||||
researchCostMultipliers = new ObjectFloatMap<>();
|
||||
}
|
||||
|
||||
this.parent = parent;
|
||||
this.content = content;
|
||||
this.requirements = requirements;
|
||||
this.depth = parent == null ? 0 : parent.depth + 1;
|
||||
this.finishedRequirements = new ItemStack[requirements.length];
|
||||
|
||||
//load up the requirements that have been finished if settings are available
|
||||
for(int i = 0; i < requirements.length; i++){
|
||||
finishedRequirements[i] = new ItemStack(requirements[i].item, Core.settings == null ? 0 : Core.settings.getInt("req-" + content.name + "-" + requirements[i].item.name));
|
||||
if(researchCostMultipliers.size > 0){
|
||||
requirements = ItemStack.copy(requirements);
|
||||
for(ItemStack requirement : requirements){
|
||||
requirement.amount = (int)(requirement.amount * researchCostMultipliers.get(requirement.item, 1));
|
||||
}
|
||||
}
|
||||
|
||||
setupRequirements(requirements);
|
||||
|
||||
var used = new ObjectSet<Content>();
|
||||
|
||||
//add dependencies as objectives.
|
||||
@@ -724,10 +127,47 @@ public class TechTree implements ContentList{
|
||||
}
|
||||
});
|
||||
|
||||
map.put(content, this);
|
||||
content.techNode = this;
|
||||
content.techNodes.add(this);
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
/** Recursively iterates through everything that is a child of this node. Includes itself. */
|
||||
public void each(Cons<TechNode> consumer){
|
||||
consumer.get(this);
|
||||
for(var child : children){
|
||||
child.each(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds the specified database tab to all the content in this tree. */
|
||||
public void addDatabaseTab(UnlockableContent tab){
|
||||
each(node -> node.content.databaseTabs.add(tab));
|
||||
}
|
||||
|
||||
/** Adds the specified planet to the shownPlanets of all the content in this tree. */
|
||||
public void addPlanet(Planet planet){
|
||||
each(node -> node.content.shownPlanets.add(planet));
|
||||
}
|
||||
|
||||
public Drawable icon(){
|
||||
return icon == null ? new TextureRegionDrawable(content.uiIcon) : icon;
|
||||
}
|
||||
|
||||
public String localizedName(){
|
||||
return Core.bundle.get("techtree." + name, name);
|
||||
}
|
||||
|
||||
public void setupRequirements(ItemStack[] requirements){
|
||||
this.requirements = requirements;
|
||||
this.finishedRequirements = new ItemStack[requirements.length];
|
||||
|
||||
//load up the requirements that have been finished if settings are available
|
||||
for(int i = 0; i < requirements.length; i++){
|
||||
finishedRequirements[i] = new ItemStack(requirements[i].item, Core.settings == null ? 0 : Core.settings.getInt("req-" + content.name + "-" + requirements[i].item.name));
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets finished requirements and saves. */
|
||||
public void reset(){
|
||||
for(ItemStack stack : finishedRequirements){
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,22 @@ package mindustry.content;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.type.weather.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
public class Weathers implements ContentList{
|
||||
public class Weathers{
|
||||
public static Weather
|
||||
rain,
|
||||
snow,
|
||||
sandstorm,
|
||||
sporestorm,
|
||||
fog;
|
||||
fog,
|
||||
suspendParticles;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
snow = new ParticleWeather("snow"){{
|
||||
public static void load(){
|
||||
snow = new ParticleWeather("snowing"){{
|
||||
particleRegion = "particle";
|
||||
sizeMax = 13f;
|
||||
sizeMin = 2.6f;
|
||||
@@ -102,5 +101,19 @@ public class Weathers implements ContentList{
|
||||
attrs.set(Attribute.water, 0.05f);
|
||||
opacityMultiplier = 0.47f;
|
||||
}};
|
||||
|
||||
suspendParticles = new ParticleWeather("suspend-particles"){{
|
||||
color = noiseColor = Color.valueOf("a7c1fa");
|
||||
particleRegion = "particle";
|
||||
statusGround = false;
|
||||
useWindVector = true;
|
||||
hidden = true;
|
||||
sizeMax = 4f;
|
||||
sizeMin = 1.4f;
|
||||
minAlpha = 0.5f;
|
||||
maxAlpha = 1f;
|
||||
density = 10000f;
|
||||
baseSpeed = 0.03f;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.mod.Mods.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
@@ -25,47 +27,36 @@ import static mindustry.Vars.*;
|
||||
public class ContentLoader{
|
||||
private ObjectMap<String, MappableContent>[] contentNameMap = new ObjectMap[ContentType.all.length];
|
||||
private Seq<Content>[] contentMap = new Seq[ContentType.all.length];
|
||||
private ObjectMap<String, MappableContent> nameMap = new ObjectMap<>();
|
||||
private MappableContent[][] temporaryMapper;
|
||||
private @Nullable LoadedMod currentMod;
|
||||
private @Nullable Content lastAdded;
|
||||
private ObjectSet<Cons<Content>> initialization = new ObjectSet<>();
|
||||
private ContentList[] content = {
|
||||
new Items(),
|
||||
new StatusEffects(),
|
||||
new Liquids(),
|
||||
new Bullets(),
|
||||
new AmmoTypes(),
|
||||
new UnitTypes(),
|
||||
new Blocks(),
|
||||
new Loadouts(),
|
||||
new Weathers(),
|
||||
new Planets(),
|
||||
new SectorPresets(),
|
||||
new TechTree(),
|
||||
};
|
||||
|
||||
public ContentLoader(){
|
||||
clear();
|
||||
}
|
||||
|
||||
/** Clears all initialized content.*/
|
||||
public void clear(){
|
||||
contentNameMap = new ObjectMap[ContentType.all.length];
|
||||
contentMap = new Seq[ContentType.all.length];
|
||||
initialization = new ObjectSet<>();
|
||||
|
||||
for(ContentType type : ContentType.all){
|
||||
contentMap[type.ordinal()] = new Seq<>();
|
||||
contentNameMap[type.ordinal()] = new ObjectMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Creates all base types. */
|
||||
public void createBaseContent(){
|
||||
for(ContentList list : content){
|
||||
list.load();
|
||||
}
|
||||
UnitCommand.loadAll();
|
||||
UnitStance.loadAll();
|
||||
TeamEntries.load();
|
||||
Items.load();
|
||||
StatusEffects.load();
|
||||
Liquids.load();
|
||||
Bullets.load();
|
||||
UnitTypes.load();
|
||||
Blocks.load();
|
||||
Loadouts.load();
|
||||
Weathers.load();
|
||||
Planets.load();
|
||||
SectorPresets.load();
|
||||
SerpuloTechTree.load();
|
||||
ErekirTechTree.load();
|
||||
}
|
||||
|
||||
/** Creates mod content, if applicable. */
|
||||
@@ -75,7 +66,7 @@ public class ContentLoader{
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs content statistics.*/
|
||||
/** Logs content statistics. */
|
||||
public void logContent(){
|
||||
//check up ID mapping, make sure it's linear (debug only)
|
||||
for(Seq<Content> arr : contentMap){
|
||||
@@ -91,19 +82,21 @@ public class ContentLoader{
|
||||
for(int k = 0; k < contentMap.length; k++){
|
||||
Log.debug("[@]: loaded @", ContentType.all[k].name(), contentMap[k].size);
|
||||
}
|
||||
Log.debug("Total content loaded: @", Seq.with(ContentType.all).mapInt(c -> contentMap[c.ordinal()].size).sum());
|
||||
Log.debug("Total content loaded: @", Seq.with(ContentType.all).sum(c -> contentMap[c.ordinal()].size));
|
||||
Log.debug("-------------------");
|
||||
}
|
||||
|
||||
/** Calls Content#init() on everything. Use only after all modules have been created.*/
|
||||
/** Calls Content#init() on everything. Use only after all modules have been created. */
|
||||
public void init(){
|
||||
initialize(Content::init);
|
||||
if(constants != null) constants.init();
|
||||
initialize(Content::postInit);
|
||||
if(logicVars != null) logicVars.init();
|
||||
Events.fire(new ContentInitEvent());
|
||||
}
|
||||
|
||||
/** Calls Content#load() on everything. Use only after all modules have been created on the client.*/
|
||||
/** Calls Content#loadIcon() and Content#load() on everything. Use only after all modules have been created on the client. */
|
||||
public void load(){
|
||||
initialize(Content::loadIcon);
|
||||
initialize(Content::load);
|
||||
}
|
||||
|
||||
@@ -132,9 +125,9 @@ public class ContentLoader{
|
||||
/** Loads block colors. */
|
||||
public void loadColors(){
|
||||
Pixmap pixmap = new Pixmap(files.internal("sprites/block_colors.png"));
|
||||
for(int i = 0; i < pixmap.getWidth(); i++){
|
||||
for(int i = 0; i < pixmap.width; i++){
|
||||
if(blocks().size > i){
|
||||
int color = pixmap.getPixel(i, 0);
|
||||
int color = pixmap.get(i, 0);
|
||||
|
||||
if(color == 0 || color == 255) continue;
|
||||
|
||||
@@ -150,11 +143,6 @@ public class ContentLoader{
|
||||
ColorMapper.load();
|
||||
}
|
||||
|
||||
public void dispose(){
|
||||
initialize(Content::dispose);
|
||||
clear();
|
||||
}
|
||||
|
||||
/** Get last piece of content created for error-handling purposes. */
|
||||
public @Nullable Content getLastAdded(){
|
||||
return lastAdded;
|
||||
@@ -185,6 +173,13 @@ public class ContentLoader{
|
||||
|
||||
public void handleMappableContent(MappableContent content){
|
||||
if(contentNameMap[content.getContentType().ordinal()].containsKey(content.name)){
|
||||
var list = contentMap[content.getContentType().ordinal()];
|
||||
|
||||
//this method is only called when registering content, and after handleContent.
|
||||
//If this is the last registered content, and it is invalid, make sure to remove it from the list to prevent invalid stuff from being registered
|
||||
if(list.size > 0 && list.peek() == content){
|
||||
list.pop();
|
||||
}
|
||||
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + content.name + "')");
|
||||
}
|
||||
if(currentMod != null){
|
||||
@@ -194,12 +189,18 @@ public class ContentLoader{
|
||||
}
|
||||
}
|
||||
contentNameMap[content.getContentType().ordinal()].put(content.name, content);
|
||||
nameMap.put(content.name, content);
|
||||
}
|
||||
|
||||
public void setTemporaryMapper(MappableContent[][] temporaryMapper){
|
||||
this.temporaryMapper = temporaryMapper;
|
||||
}
|
||||
|
||||
/** @return the last registered content with the specified name. Note that the content loader makes no attempt to resolve name conflicts. This method can be unreliable. */
|
||||
public @Nullable MappableContent byName(String name){
|
||||
return nameMap.get(name);
|
||||
}
|
||||
|
||||
public Seq<Content>[] getContentMap(){
|
||||
return contentMap;
|
||||
}
|
||||
@@ -211,10 +212,16 @@ public class ContentLoader{
|
||||
}
|
||||
|
||||
public <T extends MappableContent> T getByName(ContentType type, String name){
|
||||
if(contentNameMap[type.ordinal()] == null){
|
||||
return null;
|
||||
var map = contentNameMap[type.ordinal()];
|
||||
|
||||
if(map == null) return null;
|
||||
|
||||
//load fallbacks
|
||||
if(type == ContentType.block){
|
||||
name = SaveVersion.modContentNameMap.get(name, name);
|
||||
}
|
||||
return (T)contentNameMap[type.ordinal()].get(name);
|
||||
|
||||
return (T)map.get(name);
|
||||
}
|
||||
|
||||
public <T extends Content> T getByID(ContentType type, int id){
|
||||
@@ -262,6 +269,10 @@ public class ContentLoader{
|
||||
return getByID(ContentType.item, id);
|
||||
}
|
||||
|
||||
public Item item(String name){
|
||||
return getByName(ContentType.item, name);
|
||||
}
|
||||
|
||||
public Seq<Liquid> liquids(){
|
||||
return getBy(ContentType.liquid);
|
||||
}
|
||||
@@ -270,6 +281,10 @@ public class ContentLoader{
|
||||
return getByID(ContentType.liquid, id);
|
||||
}
|
||||
|
||||
public Liquid liquid(String name){
|
||||
return getByName(ContentType.liquid, name);
|
||||
}
|
||||
|
||||
public Seq<BulletType> bullets(){
|
||||
return getBy(ContentType.bullet);
|
||||
}
|
||||
@@ -278,15 +293,71 @@ public class ContentLoader{
|
||||
return getByID(ContentType.bullet, id);
|
||||
}
|
||||
|
||||
public Seq<StatusEffect> statusEffects(){
|
||||
return getBy(ContentType.status);
|
||||
}
|
||||
|
||||
public StatusEffect statusEffect(String name){
|
||||
return getByName(ContentType.status, name);
|
||||
}
|
||||
|
||||
public Seq<SectorPreset> sectors(){
|
||||
return getBy(ContentType.sector);
|
||||
}
|
||||
|
||||
public SectorPreset sector(String name){
|
||||
return getByName(ContentType.sector, name);
|
||||
}
|
||||
|
||||
public Seq<UnitType> units(){
|
||||
return getBy(ContentType.unit);
|
||||
}
|
||||
|
||||
public UnitType unit(int id){
|
||||
return getByID(ContentType.unit, id);
|
||||
}
|
||||
|
||||
public UnitType unit(String name){
|
||||
return getByName(ContentType.unit, name);
|
||||
}
|
||||
|
||||
public Seq<Planet> planets(){
|
||||
return getBy(ContentType.planet);
|
||||
}
|
||||
|
||||
public Planet planet(String name){
|
||||
return getByName(ContentType.planet, name);
|
||||
}
|
||||
|
||||
public Seq<Weather> weathers(){
|
||||
return getBy(ContentType.weather);
|
||||
}
|
||||
|
||||
public Weather weather(String name){
|
||||
return getByName(ContentType.weather, name);
|
||||
}
|
||||
|
||||
public Seq<UnitStance> unitStances(){
|
||||
return getBy(ContentType.unitStance);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(int id){
|
||||
return getByID(ContentType.unitStance, id);
|
||||
}
|
||||
|
||||
public UnitStance unitStance(String name){
|
||||
return getByName(ContentType.unitStance, name);
|
||||
}
|
||||
|
||||
public Seq<UnitCommand> unitCommands(){
|
||||
return getBy(ContentType.unitCommand);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(int id){
|
||||
return getByID(ContentType.unitCommand, id);
|
||||
}
|
||||
|
||||
public UnitCommand unitCommand(String name){
|
||||
return getByName(ContentType.unitCommand, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import mindustry.content.*;
|
||||
import mindustry.content.TechTree.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Saves.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
@@ -28,16 +28,15 @@ 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.*;
|
||||
import java.util.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.net;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/**
|
||||
@@ -50,14 +49,33 @@ public class Control implements ApplicationListener, Loadable{
|
||||
public Saves saves;
|
||||
public SoundControl sound;
|
||||
public InputHandler input;
|
||||
public AttackIndicators indicators;
|
||||
|
||||
private Interval timer = new Interval(2);
|
||||
private boolean hiscore = false;
|
||||
private boolean wasPaused = false;
|
||||
private boolean wasPaused = false, backgroundPaused = false;
|
||||
private Seq<Building> toBePlaced = new Seq<>(false);
|
||||
|
||||
public Control(){
|
||||
saves = new Saves();
|
||||
sound = new SoundControl();
|
||||
indicators = new AttackIndicators();
|
||||
|
||||
Events.on(BuildDamageEvent.class, e -> {
|
||||
if(e.build.team == Vars.player.team()){
|
||||
indicators.add(e.build.tileX(), e.build.tileY());
|
||||
}
|
||||
});
|
||||
|
||||
//show dialog saying that mod loading was skipped.
|
||||
Events.on(ClientLoadEvent.class, e -> {
|
||||
if(Vars.mods.skipModLoading() && Vars.mods.list().any()){
|
||||
Time.runTask(4f, () -> {
|
||||
ui.showInfo("@mods.initfailed");
|
||||
});
|
||||
}
|
||||
checkAutoUnlocks();
|
||||
});
|
||||
|
||||
Events.on(StateChangeEvent.class, event -> {
|
||||
if((event.from == State.playing && event.to == State.menu) || (event.from == State.menu && event.to != State.menu)){
|
||||
@@ -74,7 +92,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
if(Mathf.zero(player.x) && Mathf.zero(player.y)){
|
||||
Building core = state.teams.closestCore(0, 0, player.team());
|
||||
Building core = player.bestCore();
|
||||
if(core != null){
|
||||
player.set(core);
|
||||
camera.position.set(core);
|
||||
@@ -90,6 +108,8 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
Events.on(ResetEvent.class, event -> {
|
||||
player.reset();
|
||||
toBePlaced.clear();
|
||||
indicators.clear();
|
||||
|
||||
hiscore = false;
|
||||
saves.resetSave();
|
||||
@@ -114,6 +134,10 @@ public class Control implements ApplicationListener, Loadable{
|
||||
//add player when world loads regardless
|
||||
Events.on(WorldLoadEvent.class, e -> {
|
||||
player.add();
|
||||
//make player admin on any load when hosting
|
||||
if(net.active() && net.server()){
|
||||
player.admin = true;
|
||||
}
|
||||
});
|
||||
|
||||
//autohost for pvp maps
|
||||
@@ -121,7 +145,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
if(state.rules.pvp && !net.active()){
|
||||
try{
|
||||
net.host(port);
|
||||
player.admin(true);
|
||||
player.admin = true;
|
||||
}catch(IOException e){
|
||||
ui.showException("@server.error", e);
|
||||
state.set(State.menu);
|
||||
@@ -130,42 +154,28 @@ public class Control implements ApplicationListener, Loadable{
|
||||
}));
|
||||
|
||||
Events.on(UnlockEvent.class, e -> {
|
||||
ui.hudfrag.showUnlock(e.content);
|
||||
if(e.content.showUnlock()){
|
||||
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"));
|
||||
ui.hudfrag.showToast(new TextureRegionDrawable(node.content.uiIcon), iconLarge, bundle.get("available"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(SectorCaptureEvent.class, e -> {
|
||||
checkAutoUnlocks();
|
||||
});
|
||||
app.post(this::checkAutoUnlocks);
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, e -> {
|
||||
if(e.team == player.team()){
|
||||
if(e.breaking){
|
||||
state.stats.buildingsDeconstructed++;
|
||||
}else{
|
||||
state.stats.buildingsBuilt++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(BlockDestroyEvent.class, e -> {
|
||||
if(e.tile.team() == player.team()){
|
||||
state.stats.buildingsDestroyed++;
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(UnitDestroyEvent.class, e -> {
|
||||
if(e.unit.team() != player.team()){
|
||||
state.stats.enemyUnitsDestroyed++;
|
||||
if(!net.client() && e.sector.preset != null && e.sector.preset.isLastSector && e.initialCapture){
|
||||
Time.run(60f * 2f, () -> {
|
||||
ui.campaignComplete.show(e.sector.planet);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -181,35 +191,104 @@ public class Control implements ApplicationListener, Loadable{
|
||||
});
|
||||
|
||||
Events.run(Trigger.newGame, () -> {
|
||||
Building core = player.closestCore();
|
||||
|
||||
var core = player.bestCore();
|
||||
if(core == null) return;
|
||||
|
||||
//TODO this sounds pretty bad due to conflict
|
||||
if(settings.getInt("musicvol") > 0){
|
||||
Musics.land.stop();
|
||||
Musics.land.play();
|
||||
Musics.land.setVolume(settings.getInt("musicvol") / 100f);
|
||||
}
|
||||
|
||||
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);
|
||||
float coreDelay = 0f;
|
||||
if(!settings.getBool("skipcoreanimation") && !state.rules.pvp){
|
||||
coreDelay = core.launchDuration();
|
||||
//delay player respawn so animation can play.
|
||||
player.deathTimer = Player.deathDelay - core.launchDuration();
|
||||
//TODO this sounds pretty bad due to conflict
|
||||
if(settings.getInt("musicvol") > 0){
|
||||
//TODO what to do if another core with different music is already playing?
|
||||
Music music = core.landMusic();
|
||||
music.stop();
|
||||
music.play();
|
||||
music.setVolume(settings.getInt("musicvol") / 100f);
|
||||
}
|
||||
});
|
||||
|
||||
renderer.showLanding(core);
|
||||
}
|
||||
|
||||
if(state.isCampaign()){
|
||||
if(state.rules.sector.info.importRateCache != null){
|
||||
state.rules.sector.info.refreshImportRates(state.rules.sector.planet);
|
||||
}
|
||||
|
||||
//don't run when hosting, that doesn't really work.
|
||||
if(state.rules.sector.planet.prebuildBase){
|
||||
toBePlaced.clear();
|
||||
float unitsPerTick = 2f;
|
||||
float buildRadius = state.rules.enemyCoreBuildRadius * 1.5f;
|
||||
|
||||
//TODO if the save is unloaded or map is hosted, these blocks do not get built.
|
||||
boolean anyBuilds = false;
|
||||
for(var build : state.rules.defaultTeam.data().buildings.copy()){
|
||||
if(!(build instanceof CoreBuild) && !build.block.privileged){
|
||||
var ccore = build.closestCore();
|
||||
|
||||
if(ccore != null){
|
||||
anyBuilds = true;
|
||||
|
||||
if(!net.active()){
|
||||
build.pickedUp();
|
||||
build.tile.remove();
|
||||
|
||||
toBePlaced.add(build);
|
||||
|
||||
Time.run(build.dst(ccore) / unitsPerTick + coreDelay, () -> {
|
||||
if(build.tile.build != build){
|
||||
placeLandBuild(build);
|
||||
|
||||
toBePlaced.remove(build);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
//when already hosting, instantly build everything. this looks bad but it's better than a desync
|
||||
Fx.coreBuildBlock.at(build.x, build.y, 0f, build.block);
|
||||
build.block.placeEffect.at(build.x, build.y, build.block.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(anyBuilds){
|
||||
for(var ccore : state.rules.defaultTeam.data().cores){
|
||||
Time.run(coreDelay, () -> {
|
||||
Fx.coreBuildShockwave.at(ccore.x, ccore.y, buildRadius);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(SaveWriteEvent.class, e -> forcePlaceAll());
|
||||
Events.on(HostEvent.class, e -> forcePlaceAll());
|
||||
Events.on(HostEvent.class, e -> {
|
||||
state.set(State.playing);
|
||||
});
|
||||
}
|
||||
|
||||
private void forcePlaceAll(){
|
||||
//force set buildings when a save is done or map is hosted, to prevent desyncs
|
||||
for(var build : toBePlaced){
|
||||
placeLandBuild(build);
|
||||
}
|
||||
|
||||
toBePlaced.clear();
|
||||
}
|
||||
|
||||
private void placeLandBuild(Building build){
|
||||
build.tile.setBlock(build.block, build.team, build.rotation, () -> build);
|
||||
build.dropped();
|
||||
|
||||
Fx.coreBuildBlock.at(build.x, build.y, 0f, build.block);
|
||||
build.block.placeEffect.at(build.x, build.y, build.block.size);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,12 +309,12 @@ public class Control implements ApplicationListener, Loadable{
|
||||
saves.load();
|
||||
}
|
||||
|
||||
/** Automatically unlocks things with no requirements. */
|
||||
void checkAutoUnlocks(){
|
||||
/** Automatically unlocks things with no requirements and no locked parents. */
|
||||
public 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())){
|
||||
if(!node.content.unlocked() && (node.parent == null || node.parent.content.unlocked()) && node.requirements.length == 0 && !node.objectives.contains(o -> !o.complete())){
|
||||
node.content.unlock();
|
||||
}
|
||||
}
|
||||
@@ -244,6 +323,13 @@ public class Control implements ApplicationListener, Loadable{
|
||||
void createPlayer(){
|
||||
player = Player.create();
|
||||
player.name = Core.settings.getString("name");
|
||||
|
||||
String locale = Core.settings.getString("locale");
|
||||
if(locale.equals("default")){
|
||||
locale = Locale.getDefault().toString();
|
||||
}
|
||||
player.locale = locale;
|
||||
|
||||
player.color.set(Core.settings.getInt("color-0"));
|
||||
|
||||
if(mobile){
|
||||
@@ -271,17 +357,31 @@ public class Control implements ApplicationListener, Loadable{
|
||||
}
|
||||
|
||||
public void playMap(Map map, Rules rules){
|
||||
playMap(map, rules, false);
|
||||
}
|
||||
|
||||
public void playMap(Map map, Rules rules, boolean playtest){
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
world.loadMap(map, rules);
|
||||
state.rules = rules;
|
||||
if(playtest) state.playtestingMap = map;
|
||||
state.rules.sector = null;
|
||||
state.rules.editor = false;
|
||||
logic.play();
|
||||
if(settings.getBool("savecreate") && !world.isInvalidMap()){
|
||||
if(settings.getBool("savecreate") && !world.isInvalidMap() && !playtest){
|
||||
control.saves.addSave(map.name() + " " + new SimpleDateFormat("MMM dd h:mm", Locale.getDefault()).format(new Date()));
|
||||
}
|
||||
Events.fire(Trigger.newGame);
|
||||
|
||||
//booted out of map, resume editing
|
||||
if(world.isInvalidMap() && playtest){
|
||||
Dialog current = scene.getDialog();
|
||||
ui.editor.resumeAfterPlaytest(map);
|
||||
if(current != null){
|
||||
current.update(current::toFront);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,82 +400,101 @@ public class Control implements ApplicationListener, Loadable{
|
||||
control.saves.resetSave();
|
||||
}
|
||||
|
||||
//for planet launches, mostly
|
||||
if(sector.preset != null){
|
||||
sector.preset.quietUnlock();
|
||||
}
|
||||
|
||||
ui.planet.hide();
|
||||
SaveSlot slot = sector.save;
|
||||
sector.planet.setLastSector(sector);
|
||||
if(slot != null && !clearSectors){
|
||||
if(slot != null && !clearSectors && (!(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor) || sector.info.hasCore)){
|
||||
|
||||
try{
|
||||
boolean hadNoCore = !sector.info.hasCore;
|
||||
reloader.begin();
|
||||
slot.load();
|
||||
slot.setAutosave(true);
|
||||
state.rules.sector = sector;
|
||||
state.rules.cloudColor = sector.planet.landCloudColor;
|
||||
|
||||
//if there is no base, simulate a new game and place the right loadout at the spawn position
|
||||
if(state.rules.defaultTeam.cores().isEmpty()){
|
||||
if(state.rules.defaultTeam.cores().isEmpty() || hadNoCore){
|
||||
|
||||
//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;
|
||||
}
|
||||
if(sector.planet.clearSectorOnLose || sector.info.hasWorldProcessor){
|
||||
playNewSector(origin, sector, reloader);
|
||||
}else{
|
||||
//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;
|
||||
}
|
||||
|
||||
//set spawn for sector damage to use
|
||||
Tile spawn = world.tile(sector.info.spawnPosition);
|
||||
spawn.setBlock(Blocks.coreShard, state.rules.defaultTeam);
|
||||
//set spawn for sector damage to use
|
||||
Tile spawn = world.tile(sector.info.spawnPosition);
|
||||
spawn.setBlock(sector.planet.defaultCore, state.rules.defaultTeam);
|
||||
|
||||
//add extra damage.
|
||||
SectorDamage.apply(1f);
|
||||
//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 wave so things are more fair
|
||||
state.wave = 1;
|
||||
//set up default wave time
|
||||
state.wavetime = state.rules.initialWaveSpacing <= 0f ? (state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier)) : state.rules.initialWaveSpacing;
|
||||
state.wavetime *= sector.planet.campaignRules.difficulty.waveTimeMultiplier;
|
||||
//reset captured state
|
||||
sector.info.wasCaptured = false;
|
||||
|
||||
//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(state.rules.sector.planet.allowWaves){
|
||||
//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);
|
||||
//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().plans){
|
||||
Tile tile = world.tile(plan.x, plan.y);
|
||||
if(tile != null){
|
||||
tile.setBlock(plan.block, state.rules.waveTeam, plan.rotation);
|
||||
if(plan.config != null && tile.build != null){
|
||||
tile.build.configureAny(plan.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.rules.waveTeam.data().plans.clear();
|
||||
}
|
||||
state.rules.waveTeam.data().blocks.clear();
|
||||
|
||||
//kill all units, since they should be dead anyway
|
||||
Groups.unit.clear();
|
||||
Groups.fire.clear();
|
||||
Groups.puddle.clear();
|
||||
|
||||
//reset to 0, so replaced cores don't count
|
||||
state.rules.defaultTeam.data().unitCap = 0;
|
||||
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();
|
||||
}
|
||||
|
||||
//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);
|
||||
}else{
|
||||
state.set(State.playing);
|
||||
reloader.end();
|
||||
}
|
||||
|
||||
state.set(State.playing);
|
||||
reloader.end();
|
||||
|
||||
}catch(SaveException e){
|
||||
Log.err(e);
|
||||
sector.save = null;
|
||||
@@ -385,21 +504,26 @@ public class Control implements ApplicationListener, Loadable{
|
||||
}
|
||||
ui.planet.hide();
|
||||
}else{
|
||||
reloader.begin();
|
||||
world.loadSector(sector);
|
||||
state.rules.sector = sector;
|
||||
//assign origin when launching
|
||||
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();
|
||||
playNewSector(origin, sector, reloader);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void playNewSector(@Nullable Sector origin, Sector sector, WorldReloader reloader){
|
||||
reloader.begin();
|
||||
world.loadSector(sector);
|
||||
state.rules.sector = sector;
|
||||
//assign origin when launching
|
||||
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();
|
||||
state.set(State.playing);
|
||||
}
|
||||
|
||||
public boolean isHighScore(){
|
||||
return hiscore;
|
||||
}
|
||||
@@ -407,7 +531,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
@Override
|
||||
public void dispose(){
|
||||
//try to save when exiting
|
||||
if(saves != null && saves.getCurrent() != null && saves.getCurrent().isAutosave() && !net.client() && !state.isMenu()){
|
||||
if(saves != null && saves.getCurrent() != null && saves.getCurrent().isAutosave() && !net.client() && !state.isMenu() && !state.gameOver){
|
||||
try{
|
||||
SaveIO.save(control.saves.getCurrent().file);
|
||||
Log.info("Saved on exit.");
|
||||
@@ -420,16 +544,13 @@ public class Control implements ApplicationListener, Loadable{
|
||||
music.stop();
|
||||
}
|
||||
|
||||
content.dispose();
|
||||
net.dispose();
|
||||
Musics.dispose();
|
||||
Sounds.dispose();
|
||||
if(ui != null && ui.editor != null) ui.editor.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(){
|
||||
if(settings.getBool("backgroundpause", true)){
|
||||
if(settings.getBool("backgroundpause", true) && !net.active()){
|
||||
backgroundPaused = true;
|
||||
wasPaused = state.is(State.paused);
|
||||
if(state.is(State.playing)) state.set(State.paused);
|
||||
}
|
||||
@@ -437,9 +558,10 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
@Override
|
||||
public void resume(){
|
||||
if(state.is(State.paused) && !wasPaused && settings.getBool("backgroundpause", true)){
|
||||
if(state.is(State.paused) && !wasPaused && settings.getBool("backgroundpause", true) && !net.active()){
|
||||
state.set(State.playing);
|
||||
}
|
||||
backgroundPaused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -502,7 +624,7 @@ public class Control implements ApplicationListener, Loadable{
|
||||
if(full){
|
||||
graphics.setWindowedMode(graphics.getWidth(), graphics.getHeight());
|
||||
}else{
|
||||
graphics.setFullscreenMode(graphics.getDisplayMode());
|
||||
graphics.setFullscreen();
|
||||
}
|
||||
settings.put("fullscreen", !full);
|
||||
}
|
||||
@@ -516,6 +638,9 @@ public class Control implements ApplicationListener, Loadable{
|
||||
|
||||
if(state.isGame()){
|
||||
input.update();
|
||||
if(!state.isPaused()){
|
||||
indicators.update();
|
||||
}
|
||||
|
||||
//auto-update rpc every 5 seconds
|
||||
if(timer.get(0, 60 * 5)){
|
||||
@@ -528,8 +653,17 @@ public class Control implements ApplicationListener, Loadable{
|
||||
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);
|
||||
if(backgroundPaused && settings.getBool("backgroundpause") && !net.active()){
|
||||
state.set(State.paused);
|
||||
}
|
||||
|
||||
//cannot launch while paused
|
||||
if(state.isPaused() && renderer.isCutscene()){
|
||||
state.set(State.playing);
|
||||
}
|
||||
|
||||
if(!net.client() && Core.input.keyTap(Binding.pause) && !renderer.isCutscene() && !scene.hasDialog() && !scene.hasKeyboard() && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
|
||||
state.set(state.isPaused() ? State.playing : State.paused);
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(Binding.menu) && !ui.restart.isShown() && !ui.minimapfrag.shown()){
|
||||
@@ -537,11 +671,13 @@ public class Control implements ApplicationListener, Loadable{
|
||||
ui.chatfrag.hide();
|
||||
}else if(!ui.paused.isShown() && !scene.hasDialog()){
|
||||
ui.paused.show();
|
||||
state.set(State.paused);
|
||||
if(!net.active()){
|
||||
state.set(State.paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!mobile && Core.input.keyTap(Binding.screenshot) && !(scene.getKeyboardFocus() instanceof TextField) && !scene.hasKeyboard()){
|
||||
if(!mobile && Core.input.keyTap(Binding.screenshot) && !scene.hasField() && !scene.hasKeyboard()){
|
||||
renderer.takeMapScreenshot();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,22 @@ package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.loaders.*;
|
||||
import arc.assets.loaders.MusicLoader.*;
|
||||
import arc.assets.loaders.SoundLoader.*;
|
||||
import arc.audio.*;
|
||||
import arc.files.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
/** Handles files in a modded context. */
|
||||
public class FileTree implements FileHandleResolver{
|
||||
private ObjectMap<String, Fi> files = new ObjectMap<>();
|
||||
private ObjectMap<String, Sound> loadedSounds = new ObjectMap<>();
|
||||
private ObjectMap<String, Music> loadedMusic = new ObjectMap<>();
|
||||
|
||||
public void addFile(String path, Fi f){
|
||||
files.put(path, f);
|
||||
files.put(path.replace('\\', '/'), f);
|
||||
}
|
||||
|
||||
/** Gets an asset file.*/
|
||||
@@ -40,4 +47,42 @@ public class FileTree implements FileHandleResolver{
|
||||
public Fi resolve(String fileName){
|
||||
return get(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a sound by name from the sounds/ folder. OGG and MP3 are supported; the extension is automatically added to the end of the file name.
|
||||
* Results are cached; consecutive calls to this method with the same name will return the same sound instance.
|
||||
* */
|
||||
public Sound loadSound(String soundName){
|
||||
if(Vars.headless) return Sounds.none;
|
||||
|
||||
return loadedSounds.get(soundName, () -> {
|
||||
String name = "sounds/" + soundName;
|
||||
String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
|
||||
|
||||
var sound = new Sound();
|
||||
var desc = Core.assets.load(path, Sound.class, new SoundParameter(sound));
|
||||
desc.errored = Throwable::printStackTrace;
|
||||
|
||||
return sound;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a music file by name from the music/ folder. OGG and MP3 are supported; the extension is automatically added to the end of the file name.
|
||||
* Results are cached; consecutive calls to this method with the same name will return the same music instance.
|
||||
* */
|
||||
public Music loadMusic(String musicName){
|
||||
if(Vars.headless) return new Music();
|
||||
|
||||
return loadedMusic.get(musicName, () -> {
|
||||
String name = "music/" + musicName;
|
||||
String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
|
||||
|
||||
var music = new Music();
|
||||
var desc = Core.assets.load(path, Music.class, new MusicParameter(music));
|
||||
desc.errored = Throwable::printStackTrace;
|
||||
|
||||
return music;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,37 +16,52 @@ public class GameState{
|
||||
public int wave = 1;
|
||||
/** Wave countdown in ticks. */
|
||||
public float wavetime;
|
||||
/** Logic tick. */
|
||||
public double tick;
|
||||
/** Continuously ticks up every non-paused update. */
|
||||
public long updateId;
|
||||
/** Whether the game is in game over state. */
|
||||
public boolean gameOver = false, serverPaused = false, wasTimeout;
|
||||
public boolean gameOver = false;
|
||||
/** Whether the player's team won the match. */
|
||||
public boolean won = false;
|
||||
/** Server ticks/second. Only valid in multiplayer. */
|
||||
public int serverTps = -1;
|
||||
/** Map that is currently being played on. */
|
||||
public Map map = emptyMap;
|
||||
/** The current game rules. */
|
||||
public Rules rules = new Rules();
|
||||
/** Statistics for this save/game. Displayed after game over. */
|
||||
public GameStats stats = new GameStats();
|
||||
/** Markers not linked to objectives. Controlled by world processors. */
|
||||
public MapMarkers markers = new MapMarkers();
|
||||
/** Locale-specific string bundles of current map */
|
||||
public MapLocales mapLocales = new MapLocales();
|
||||
/** Global attributes of the environment, calculated by weather. */
|
||||
public Attributes envAttrs = new Attributes();
|
||||
/** Team data. Gets reset every new game. */
|
||||
public Teams teams = new Teams();
|
||||
/** Number of enemies in the game; only used clientside in servers. */
|
||||
public int enemies;
|
||||
/** Map being playtested (not edited!) */
|
||||
public @Nullable Map playtestingMap;
|
||||
/** Current game state. */
|
||||
private State state = State.menu;
|
||||
|
||||
@Nullable
|
||||
public Unit boss(){
|
||||
return teams.boss;
|
||||
return teams.bosses.firstOpt();
|
||||
}
|
||||
|
||||
public void set(State astate){
|
||||
//cannot pause when in multiplayer
|
||||
if(astate == State.paused && net.active()) return;
|
||||
//nothing to change.
|
||||
if(state == astate) return;
|
||||
|
||||
Events.fire(new StateChangeEvent(state, astate));
|
||||
state = astate;
|
||||
}
|
||||
|
||||
public boolean hasSpawns(){
|
||||
return rules.waves && !(isCampaign() && rules.attackMode);
|
||||
return rules.waves && ((rules.waveTeam.cores().size > 0 && rules.attackMode) || rules.spawns.size > 0);
|
||||
}
|
||||
|
||||
/** Note that being in a campaign does not necessarily mean having a sector. */
|
||||
@@ -58,21 +73,25 @@ public class GameState{
|
||||
return rules.sector != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Sector getSector(){
|
||||
public @Nullable Sector getSector(){
|
||||
return rules.sector;
|
||||
}
|
||||
|
||||
public @Nullable Planet getPlanet(){
|
||||
return rules.sector != null ? rules.sector.planet : rules.planet;
|
||||
}
|
||||
|
||||
public boolean isEditor(){
|
||||
return rules.editor;
|
||||
}
|
||||
|
||||
public boolean isPaused(){
|
||||
return (is(State.paused) && !net.active()) || (gameOver && (!net.active() || isCampaign())) || (serverPaused && !isMenu());
|
||||
return state == State.paused;
|
||||
}
|
||||
|
||||
/** @return whether there is an unpaused game in progress. */
|
||||
public boolean isPlaying(){
|
||||
return (state == State.playing) || (state == State.paused && !isPaused());
|
||||
return state == State.playing;
|
||||
}
|
||||
|
||||
/** @return whether the current state is *not* the menu. */
|
||||
|
||||
+208
-127
@@ -3,6 +3,7 @@ package mindustry.core;
|
||||
import arc.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -14,6 +15,7 @@ import mindustry.maps.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.type.Weather.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -32,85 +34,108 @@ public class Logic implements ApplicationListener{
|
||||
public Logic(){
|
||||
|
||||
Events.on(BlockDestroyEvent.class, event -> {
|
||||
//skip if rule is off
|
||||
if(!state.rules.ghostBlocks) return;
|
||||
|
||||
//blocks that get broken are appended to the team's broken block queue
|
||||
Tile tile = event.tile;
|
||||
//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;
|
||||
//skip null entities or un-rebuildables, for obvious reasons
|
||||
if(tile.build == null || !tile.block().rebuildable) return;
|
||||
|
||||
tile.build.addPlan(true);
|
||||
});
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, event -> {
|
||||
if(!event.breaking){
|
||||
TeamData data = state.teams.get(event.team);
|
||||
Iterator<BlockPlan> it = data.blocks.iterator();
|
||||
while(it.hasNext()){
|
||||
BlockPlan b = it.next();
|
||||
Block block = content.block(b.block);
|
||||
if(event.tile.block().bounds(event.tile.x, event.tile.y, Tmp.r1).overlaps(block.bounds(b.x, b.y, Tmp.r2))){
|
||||
it.remove();
|
||||
}
|
||||
checkOverlappingPlans(event.team, event.tile);
|
||||
|
||||
if(event.team == state.rules.defaultTeam){
|
||||
state.stats.placedBlockCount.increment(event.tile.block());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(PayloadDropEvent.class, e -> {
|
||||
if(e.build != null){
|
||||
checkOverlappingPlans(e.build.team, e.build.tile);
|
||||
}
|
||||
});
|
||||
|
||||
//when loading a 'damaged' sector, propagate the damage
|
||||
Events.on(SaveLoadEvent.class, e -> {
|
||||
if(state.isCampaign()){
|
||||
SectorInfo info = state.rules.sector.info;
|
||||
info.write();
|
||||
state.rules.coreIncinerates = true;
|
||||
|
||||
//how much wave time has passed
|
||||
int wavesPassed = info.wavesPassed;
|
||||
//TODO why is this even a thing?
|
||||
state.rules.canGameOver = true;
|
||||
|
||||
//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();
|
||||
//fresh map has no sector info
|
||||
if(!e.isMap){
|
||||
SectorInfo info = state.rules.sector.info;
|
||||
info.write();
|
||||
|
||||
//only simulate waves if the planet allows it
|
||||
if(state.rules.sector.planet.allowWaveSimulation){
|
||||
//how much wave time has passed
|
||||
int wavesPassed = info.wavesPassed;
|
||||
|
||||
//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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//simulate passing of waves
|
||||
if(wavesPassed > 0){
|
||||
//simulate wave counter moving forward
|
||||
state.wave += wavesPassed;
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
//simulate passing of waves
|
||||
if(wavesPassed > 0){
|
||||
//simulate wave counter moving forward
|
||||
state.wave += wavesPassed;
|
||||
state.wavetime = state.rules.waveSpacing * state.getPlanet().campaignRules.difficulty.waveTimeMultiplier;
|
||||
|
||||
SectorDamage.applyCalculatedDamage();
|
||||
|
||||
//make sure damaged buildings are counted
|
||||
for(Tile tile : world.tiles){
|
||||
if(tile.build != null && tile.build.damaged()){
|
||||
indexer.notifyTileDamaged(tile.build);
|
||||
SectorDamage.applyCalculatedDamage();
|
||||
}
|
||||
}
|
||||
|
||||
state.getSector().planet.applyRules(state.rules);
|
||||
|
||||
//reset values
|
||||
info.damage = 0f;
|
||||
info.wavesPassed = 0;
|
||||
info.hasCore = true;
|
||||
info.secondsPassed = 0;
|
||||
|
||||
state.rules.sector.saveInfo();
|
||||
}
|
||||
|
||||
//reset values
|
||||
info.damage = 0f;
|
||||
info.wavesPassed = 0;
|
||||
info.hasCore = true;
|
||||
info.secondsPassed = 0;
|
||||
|
||||
state.rules.sector.saveInfo();
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(PlayEvent.class, e -> {
|
||||
//reset weather on play
|
||||
var randomWeather = state.rules.weather.copy().shuffle();
|
||||
float sum = 0f;
|
||||
for(var weather : randomWeather){
|
||||
weather.cooldown = sum + Mathf.random(weather.maxFrequency);
|
||||
sum += weather.cooldown;
|
||||
}
|
||||
//tick resets on new save play
|
||||
state.tick = 0f;
|
||||
});
|
||||
|
||||
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.
|
||||
state.rules.coreIncinerates = true;
|
||||
state.rules.allowEditWorldProcessors = false;
|
||||
state.rules.waveTeam.rules().infiniteResources = true;
|
||||
state.rules.waveTeam.rules().buildSpeedMultiplier *= state.getPlanet().enemyBuildSpeedMultiplier;
|
||||
|
||||
//fill enemy cores by default? TODO decide
|
||||
for(var core : state.rules.waveTeam.cores()){
|
||||
for(Item item : content.items()){
|
||||
core.items.set(item, core.block.itemCapacity);
|
||||
@@ -131,61 +156,95 @@ public class Logic implements ApplicationListener{
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
state.rules.waveTeam.data().destroyToDerelict();
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(BlockDestroyEvent.class, e -> {
|
||||
if(e.tile.build instanceof CoreBuild core && core.team.isAI() && state.rules.coreDestroyClear){
|
||||
Core.app.post(() -> {
|
||||
core.team.data().timeDestroy(core.x, core.y, state.rules.enemyCoreBuildRadius);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//listen to core changes; if all cores have been destroyed, set to derelict.
|
||||
Events.on(CoreChangeEvent.class, e -> Core.app.post(() -> {
|
||||
if(state.rules.cleanupDeadTeams && state.rules.pvp && !e.core.isAdded() && e.core.team != Team.derelict && e.core.team.cores().isEmpty()){
|
||||
e.core.team.data().destroyToDerelict();
|
||||
}
|
||||
}));
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, e -> {
|
||||
if(e.team == state.rules.defaultTeam){
|
||||
if(e.breaking){
|
||||
state.stats.buildingsDeconstructed++;
|
||||
}else{
|
||||
state.stats.buildingsBuilt++;
|
||||
}
|
||||
|
||||
//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);
|
||||
Events.on(BlockDestroyEvent.class, e -> {
|
||||
if(e.tile.team() == state.rules.defaultTeam){
|
||||
state.stats.buildingsDestroyed ++;
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(UnitDestroyEvent.class, e -> {
|
||||
if(e.unit.team() != state.rules.defaultTeam){
|
||||
state.stats.enemyUnitsDestroyed ++;
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(UnitCreateEvent.class, e -> {
|
||||
if(e.unit.team == state.rules.defaultTeam){
|
||||
state.stats.unitsCreated++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void checkOverlappingPlans(Team team, Tile tile){
|
||||
TeamData data = team.data();
|
||||
Iterator<BlockPlan> it = data.plans.iterator();
|
||||
var bounds = tile.block().bounds(tile.x, tile.y, Tmp.r1);
|
||||
while(it.hasNext()){
|
||||
BlockPlan b = it.next();
|
||||
if(bounds.overlaps(b.block.bounds(b.x, b.y, Tmp.r2))){
|
||||
b.removed = true;
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds starting items, resets wave time, and sets state to playing. */
|
||||
public void play(){
|
||||
state.set(State.playing);
|
||||
//grace period of 2x wave time before game starts
|
||||
state.wavetime = state.rules.waveSpacing * 2;
|
||||
state.wavetime = (state.rules.initialWaveSpacing <= 0 ? state.rules.waveSpacing * 2 : state.rules.initialWaveSpacing) * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f);;
|
||||
Events.fire(new PlayEvent());
|
||||
|
||||
//add starting items
|
||||
if(!state.isCampaign()){
|
||||
if(!state.isCampaign() || !state.rules.sector.planet.allowLaunchLoadout || (state.rules.sector.preset != null && state.rules.sector.preset.addStartingItems)){
|
||||
for(TeamData team : state.teams.getActive()){
|
||||
if(team.hasCore()){
|
||||
Building entity = team.core();
|
||||
CoreBuild entity = team.core();
|
||||
entity.items.clear();
|
||||
|
||||
for(ItemStack stack : state.rules.loadout){
|
||||
entity.items.add(stack.item, stack.amount);
|
||||
//make sure to cap storage
|
||||
entity.items.add(stack.item, Math.min(stack.amount, entity.storageCapacity - entity.items.get(stack.item)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//heal all cores on game start
|
||||
for(TeamData team : state.teams.getActive()){
|
||||
for(var entity : team.cores){
|
||||
entity.heal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
@@ -198,19 +257,20 @@ public class Logic implements ApplicationListener{
|
||||
Groups.clear();
|
||||
Time.clear();
|
||||
Events.fire(new ResetEvent());
|
||||
world.tiles = new Tiles(0, 0);
|
||||
|
||||
//save settings on reset
|
||||
Core.settings.manualSave();
|
||||
}
|
||||
|
||||
public void skipWave(){
|
||||
state.wavetime = 0;
|
||||
runWave();
|
||||
}
|
||||
|
||||
public void runWave(){
|
||||
spawner.spawnEnemies();
|
||||
state.wave++;
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
state.wavetime = state.rules.waveSpacing * (state.isCampaign() ? state.getPlanet().campaignRules.difficulty.waveTimeMultiplier : 1f);
|
||||
|
||||
Events.fire(new WaveEvent());
|
||||
}
|
||||
@@ -234,7 +294,14 @@ public class Logic implements ApplicationListener{
|
||||
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())){
|
||||
|
||||
Call.sectorCapture();
|
||||
if(state.rules.sector.preset != null && state.rules.sector.preset.attackAfterWaves && !state.rules.attackMode){
|
||||
//activate attack mode to destroy cores after waves are done.
|
||||
state.rules.attackMode = true;
|
||||
state.rules.waves = false;
|
||||
Call.setRules(state.rules);
|
||||
}else{
|
||||
Call.sectorCapture();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(!state.rules.attackMode && state.teams.playerCores().size == 0 && !state.gameOver){
|
||||
@@ -242,19 +309,22 @@ public class Logic implements ApplicationListener{
|
||||
Events.fire(new GameOverEvent(state.rules.waveTeam));
|
||||
}else if(state.rules.attackMode){
|
||||
//count # of teams alive
|
||||
int countAlive = state.teams.getActive().count(TeamData::hasCore);
|
||||
int countAlive = state.teams.getActive().count(t -> t.hasCore() && t.team != Team.derelict);
|
||||
|
||||
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);
|
||||
TeamData left = state.teams.getActive().find(t -> t.hasCore() && t.team != Team.derelict);
|
||||
Events.fire(new GameOverEvent(left == null ? Team.derelict : left.team));
|
||||
state.gameOver = true;
|
||||
}
|
||||
}else if(!state.gameOver && state.rules.waves && (state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning())){
|
||||
state.gameOver = true;
|
||||
Events.fire(new GameOverEvent(state.rules.defaultTeam));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWeather(){
|
||||
protected void updateWeather(){
|
||||
state.rules.weather.removeAll(w -> w.weather == null);
|
||||
|
||||
for(WeatherEntry entry : state.rules.weather){
|
||||
@@ -282,14 +352,21 @@ public class Logic implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
boolean initial = !state.rules.sector.info.wasCaptured;
|
||||
|
||||
state.rules.sector.info.wasCaptured = true;
|
||||
|
||||
//fire capture event
|
||||
Events.fire(new SectorCaptureEvent(state.rules.sector));
|
||||
Events.fire(new SectorCaptureEvent(state.rules.sector, initial));
|
||||
|
||||
//disable attack mode
|
||||
state.rules.attackMode = false;
|
||||
|
||||
//map is over, no more world processor objective stuff
|
||||
state.rules.disableWorldProcessors = true;
|
||||
|
||||
Call.clearObjectives();
|
||||
|
||||
//save, just in case
|
||||
if(!headless && !net.client()){
|
||||
control.saves.saveSector(state.rules.sector);
|
||||
@@ -299,12 +376,16 @@ public class Logic implements ApplicationListener{
|
||||
@Remote(called = Loc.both)
|
||||
public static void updateGameOver(Team winner){
|
||||
state.gameOver = true;
|
||||
if(!headless){
|
||||
state.won = player.team() == winner;
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both)
|
||||
public static void gameOver(Team winner){
|
||||
state.stats.wavesLasted = state.wave;
|
||||
ui.restart.show(winner);
|
||||
state.won = player.team() == winner;
|
||||
Time.run(60f * 3f, () -> ui.restart.show(winner));
|
||||
netClient.setQuiet();
|
||||
}
|
||||
|
||||
@@ -313,51 +394,20 @@ public class Logic implements ApplicationListener{
|
||||
public static void researched(Content content){
|
||||
if(!(content instanceof UnlockableContent u)) return;
|
||||
|
||||
var node = u.node();
|
||||
boolean was = u.unlockedNowHost();
|
||||
state.rules.researched.add(u);
|
||||
|
||||
//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();
|
||||
}
|
||||
if(!was){
|
||||
Events.fire(new UnlockEvent(u));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
//save the settings before quitting
|
||||
if(netServer != null){
|
||||
netServer.admins.forceSave();
|
||||
}
|
||||
Core.settings.manualSave();
|
||||
}
|
||||
|
||||
@@ -367,16 +417,29 @@ public class Logic implements ApplicationListener{
|
||||
universe.updateGlobal();
|
||||
|
||||
if(Core.settings.modified() && !state.isPlaying()){
|
||||
netServer.admins.forceSave();
|
||||
Core.settings.forceSave();
|
||||
}
|
||||
|
||||
boolean runStateCheck = !net.client() && !world.isInvalidMap() && !state.isEditor() && state.rules.canGameOver;
|
||||
|
||||
if(state.isGame()){
|
||||
if(!net.client()){
|
||||
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.type.isCounted);
|
||||
state.enemies = Groups.unit.count(u -> u.team() == state.rules.waveTeam && u.isEnemy());
|
||||
}
|
||||
|
||||
if(!state.isPaused()){
|
||||
Events.fire(Trigger.beforeGameUpdate);
|
||||
|
||||
float delta = Core.graphics.getDeltaTime();
|
||||
state.tick += Float.isNaN(delta) || Float.isInfinite(delta) ? 0f : delta * 60f;
|
||||
state.updateId ++;
|
||||
state.teams.updateTeamStats();
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
if(state.rules.fog){
|
||||
fogControl.update();
|
||||
}
|
||||
|
||||
if(state.isCampaign()){
|
||||
state.rules.sector.info.update();
|
||||
@@ -387,17 +450,30 @@ public class Logic implements ApplicationListener{
|
||||
}
|
||||
Time.update();
|
||||
|
||||
logicVars.update();
|
||||
|
||||
//weather is serverside
|
||||
if(!net.client() && !state.isEditor()){
|
||||
updateWeather();
|
||||
|
||||
for(TeamData data : state.teams.getActive()){
|
||||
if(data.hasAI()){
|
||||
data.ai.update();
|
||||
//does not work on PvP so built-in attack maps can have it on by default without issues
|
||||
if(data.team.rules().buildAi && !state.rules.pvp){
|
||||
if(data.buildAi == null) data.buildAi = new BaseBuilderAI(data);
|
||||
data.buildAi.update();
|
||||
}
|
||||
|
||||
if(data.team.rules().rtsAi){
|
||||
if(data.rtsAi == null) data.rtsAi = new RtsAI(data);
|
||||
data.rtsAi.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!state.isEditor()){
|
||||
state.rules.objectives.update();
|
||||
}
|
||||
|
||||
if(state.rules.waves && state.rules.waveTimer && !state.gameOver){
|
||||
if(!isWaitingWave()){
|
||||
state.wavetime = Math.max(state.wavetime - Time.delta, 0);
|
||||
@@ -410,14 +486,19 @@ public class Logic implements ApplicationListener{
|
||||
|
||||
//apply weather attributes
|
||||
state.envAttrs.clear();
|
||||
state.envAttrs.add(state.rules.attributes);
|
||||
Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity));
|
||||
|
||||
Groups.update();
|
||||
|
||||
Events.fire(Trigger.afterGameUpdate);
|
||||
}
|
||||
|
||||
if(!net.client() && !world.isInvalidMap() && !state.isEditor() && state.rules.canGameOver){
|
||||
if(runStateCheck){
|
||||
checkGameState();
|
||||
}
|
||||
}else if(netServer.isWaitingForPlayers() && runStateCheck){
|
||||
checkGameState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.audio.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
@@ -9,31 +10,36 @@ import arc.util.*;
|
||||
import arc.util.CommandHandler.*;
|
||||
import arc.util.io.*;
|
||||
import arc.util.serialization.*;
|
||||
import arc.util.serialization.JsonValue.*;
|
||||
import mindustry.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.logic.*;
|
||||
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.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class NetClient implements ApplicationListener{
|
||||
private static final float dataTimeout = 60 * 18;
|
||||
private static final float playerSyncTime = 2;
|
||||
public static final float viewScale = 2f;
|
||||
private static final long entitySnapshotTimeout = 1000 * 20;
|
||||
private static final float dataTimeout = 60 * 30;
|
||||
/** ticks between syncs, e.g. 5 means 60/5 = 12 syncs/sec*/
|
||||
private static final float playerSyncTime = 4;
|
||||
private static final Reads dataReads = new Reads(null);
|
||||
private static final JsonValue tmpJsonMap = new JsonValue(ValueType.object);
|
||||
|
||||
private long ping;
|
||||
private Interval timer = new Interval(5);
|
||||
@@ -45,6 +51,8 @@ public class NetClient implements ApplicationListener{
|
||||
private boolean quietReset = false;
|
||||
/** Counter for data timeout. */
|
||||
private float timeoutTime = 0f;
|
||||
/** Timestamp for last UDP state snapshot received. */
|
||||
private long lastSnapshotTimestamp;
|
||||
/** Last sent client snapshot ID. */
|
||||
private int lastSent;
|
||||
|
||||
@@ -55,16 +63,25 @@ public class NetClient implements ApplicationListener{
|
||||
private DataInputStream dataStream = new DataInputStream(byteStream);
|
||||
/** Packet handlers for custom types of messages. */
|
||||
private ObjectMap<String, Seq<Cons<String>>> customPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for custom types of messages, in binary. */
|
||||
private ObjectMap<String, Seq<Cons<byte[]>>> customBinaryPacketHandlers = new ObjectMap<>();
|
||||
|
||||
public NetClient(){
|
||||
|
||||
net.handleClient(Connect.class, packet -> {
|
||||
Log.info("Connecting to server: @", packet.addressTCP);
|
||||
|
||||
player.admin(false);
|
||||
player.admin = false;
|
||||
|
||||
reset();
|
||||
|
||||
//connection after reset
|
||||
if(!net.client()){
|
||||
Log.info("Connection canceled.");
|
||||
disconnectQuietly();
|
||||
return;
|
||||
}
|
||||
|
||||
ui.loadfrag.hide();
|
||||
ui.loadfrag.show("@connecting.data");
|
||||
|
||||
@@ -73,12 +90,18 @@ public class NetClient implements ApplicationListener{
|
||||
disconnectQuietly();
|
||||
});
|
||||
|
||||
ConnectPacket c = new ConnectPacket();
|
||||
String locale = Core.settings.getString("locale");
|
||||
if(locale.equals("default")){
|
||||
locale = Locale.getDefault().toString();
|
||||
}
|
||||
|
||||
var c = new ConnectPacket();
|
||||
c.name = player.name;
|
||||
c.locale = locale;
|
||||
c.mods = mods.getModStrings();
|
||||
c.mobile = mobile;
|
||||
c.versionType = Version.type;
|
||||
c.color = player.color().rgba();
|
||||
c.color = player.color.rgba();
|
||||
c.usid = getUsid(packet.addressTCP);
|
||||
c.uuid = platform.getUUID();
|
||||
|
||||
@@ -89,7 +112,7 @@ public class NetClient implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
net.send(c, SendMode.tcp);
|
||||
net.send(c, true);
|
||||
});
|
||||
|
||||
net.handleClient(Disconnect.class, packet -> {
|
||||
@@ -98,19 +121,19 @@ public class NetClient implements ApplicationListener{
|
||||
connecting = false;
|
||||
logic.reset();
|
||||
platform.updateRPC();
|
||||
player.name(Core.settings.getString("name"));
|
||||
player.color().set(Core.settings.getInt("color-0"));
|
||||
player.name = Core.settings.getString("name");
|
||||
player.color.set(Core.settings.getInt("color-0"));
|
||||
|
||||
if(quiet) return;
|
||||
|
||||
Time.runTask(3f, ui.loadfrag::hide);
|
||||
|
||||
if(packet.reason != null){
|
||||
switch(packet.reason){
|
||||
case "closed" -> ui.showSmall("@disconnect", "@disconnect.closed");
|
||||
case "timeout" -> ui.showSmall("@disconnect", "@disconnect.timeout");
|
||||
case "error" -> ui.showSmall("@disconnect", "@disconnect.error");
|
||||
}
|
||||
ui.showSmall(switch(packet.reason){
|
||||
case "closed" -> "@disconnect.closed";
|
||||
case "timeout" -> "@disconnect.timeout";
|
||||
default -> "@disconnect.error";
|
||||
}, "@disconnect.closed");
|
||||
}else{
|
||||
ui.showErrorMessage("@disconnect");
|
||||
}
|
||||
@@ -122,10 +145,6 @@ public class NetClient implements ApplicationListener{
|
||||
|
||||
finishConnecting();
|
||||
});
|
||||
|
||||
net.handleClient(InvokePacket.class, packet -> {
|
||||
RemoteReadClient.readPacket(packet.reader(), packet.type);
|
||||
});
|
||||
}
|
||||
|
||||
public void addPacketHandler(String type, Cons<String> handler){
|
||||
@@ -136,10 +155,34 @@ public class NetClient implements ApplicationListener{
|
||||
return customPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addBinaryPacketHandler(String type, Cons<byte[]> handler){
|
||||
customBinaryPacketHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
|
||||
public Seq<Cons<byte[]>> getBinaryPacketHandlers(String type){
|
||||
return customBinaryPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both)
|
||||
public static void clientBinaryPacketReliable(String type, byte[] contents){
|
||||
var arr = netClient.customBinaryPacketHandlers.get(type);
|
||||
if(arr != null){
|
||||
for(var c : arr){
|
||||
c.get(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both, unreliable = true)
|
||||
public static void clientBinaryPacketUnreliable(String type, byte[] contents){
|
||||
clientBinaryPacketReliable(type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both)
|
||||
public static void clientPacketReliable(String type, String contents){
|
||||
if(netClient.customPacketHandlers.containsKey(type)){
|
||||
for(Cons<String> c : netClient.customPacketHandlers.get(type)){
|
||||
var arr = netClient.customPacketHandlers.get(type);
|
||||
if(arr != null){
|
||||
for(Cons<String> c : arr){
|
||||
c.get(contents);
|
||||
}
|
||||
}
|
||||
@@ -150,16 +193,52 @@ public class NetClient implements ApplicationListener{
|
||||
clientPacketReliable(type, contents);
|
||||
}
|
||||
|
||||
//called on all clients
|
||||
@Remote(variants = Variant.both, unreliable = true, called = Loc.server)
|
||||
public static void sound(Sound sound, float volume, float pitch, float pan){
|
||||
if(sound == null || headless) return;
|
||||
|
||||
sound.play(Mathf.clamp(volume, 0, 8f) * Core.settings.getInt("sfxvol") / 100f, Mathf.clamp(pitch, 0f, 20f), pan, false, false);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true, called = Loc.server)
|
||||
public static void soundAt(Sound sound, float x, float y, float volume, float pitch){
|
||||
if(sound == null || headless) return;
|
||||
|
||||
sound.at(x, y, Mathf.clamp(pitch, 0f, 20f), Mathf.clamp(volume, 0, 4f));
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true)
|
||||
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, unreliable = true)
|
||||
public static void effect(Effect effect, float x, float y, float rotation, Color color, Object data){
|
||||
if(effect == null) return;
|
||||
|
||||
effect.at(x, y, rotation, color, data);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void effectReliable(Effect effect, float x, float y, float rotation, Color color){
|
||||
effect(effect, x, y, rotation, color);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.server, variants = Variant.both)
|
||||
public static void sendMessage(String message, String sender, Player playersender){
|
||||
public static void sendMessage(String message, @Nullable String unformatted, @Nullable Player playersender){
|
||||
if(Vars.ui != null){
|
||||
Vars.ui.chatfrag.addMessage(message, sender);
|
||||
Vars.ui.chatfrag.addMessage(message);
|
||||
Sounds.chatMessage.play();
|
||||
}
|
||||
|
||||
if(playersender != null){
|
||||
playersender.lastText(message);
|
||||
if(playersender != null && unformatted != null){
|
||||
//display raw unformatted text above player head
|
||||
playersender.lastText(unformatted);
|
||||
playersender.textFadeTime(1f);
|
||||
|
||||
Events.fire(new PlayerChatEvent(playersender, unformatted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,76 +246,83 @@ public class NetClient implements ApplicationListener{
|
||||
@Remote(called = Loc.server, targets = Loc.server)
|
||||
public static void sendMessage(String message){
|
||||
if(Vars.ui != null){
|
||||
Vars.ui.chatfrag.addMessage(message, null);
|
||||
Vars.ui.chatfrag.addMessage(message);
|
||||
Sounds.chatMessage.play();
|
||||
}
|
||||
}
|
||||
|
||||
//called when a server receives a chat message from a player
|
||||
@Remote(called = Loc.server, targets = Loc.client)
|
||||
public static void sendChatMessage(Player player, String message){
|
||||
|
||||
//do not receive chat messages from clients that are too young or not registered
|
||||
if(net.server() && player != null && player.con != null && (Time.timeSinceMillis(player.con.connectTime) < 500 || !player.con.hasConnected || !player.isAdded())) return;
|
||||
|
||||
//detect and kick for foul play
|
||||
if(player != null && player.con != null && !player.con.chatRate.allow(2000, Config.chatSpamLimit.num())){
|
||||
player.con.kick(KickReason.kick);
|
||||
netServer.admins.blacklistDos(player.con.address);
|
||||
return;
|
||||
}
|
||||
|
||||
if(message == null) return;
|
||||
|
||||
if(message.length() > maxTextLength){
|
||||
throw new ValidateException(player, "Player has sent a message above the text limit.");
|
||||
}
|
||||
|
||||
message = message.replace("\n", "");
|
||||
|
||||
Events.fire(new PlayerChatEvent(player, message));
|
||||
|
||||
//log commands before they are handled
|
||||
if(message.startsWith(netServer.clientCommands.getPrefix())){
|
||||
//log with brackets
|
||||
Log.info("<&fi@: @&fr>", "&lk" + player.plainName(), "&lw" + message);
|
||||
}
|
||||
|
||||
//check if it's a command
|
||||
CommandResponse response = netServer.clientCommands.handleMessage(message, player);
|
||||
if(response.type == ResponseType.noCommand){ //no command to handle
|
||||
message = netServer.admins.filterMessage(player, message);
|
||||
//supress chat message if it's filtered out
|
||||
//suppress chat message if it's filtered out
|
||||
if(message == null){
|
||||
return;
|
||||
}
|
||||
|
||||
//special case; graphical server needs to see its message
|
||||
if(!headless){
|
||||
sendMessage(message, colorizeName(player.id(), player.name), player);
|
||||
sendMessage(netServer.chatFormatter.format(player, message), message, player);
|
||||
}
|
||||
|
||||
//server console logging
|
||||
Log.info("&fi@: @", "&lc" + player.name, "&lw" + message);
|
||||
Log.info("&fi@: @", "&lc" + player.plainName(), "&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);
|
||||
Call.sendMessage(netServer.chatFormatter.format(player, message), message, player);
|
||||
}else{
|
||||
//log command to console but with brackets
|
||||
Log.info("<&fi@: @&fr>", "&lk" + player.name, "&lw" + message);
|
||||
|
||||
//a command was sent, now get the output
|
||||
if(response.type != ResponseType.valid){
|
||||
String text;
|
||||
|
||||
//send usage
|
||||
if(response.type == ResponseType.manyArguments){
|
||||
text = "[scarlet]Too many arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
|
||||
}else if(response.type == ResponseType.fewArguments){
|
||||
text = "[scarlet]Too few arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
|
||||
}else{ //unknown command
|
||||
text = "[scarlet]Unknown command. Check [lightgray]/help[scarlet].";
|
||||
String text = netServer.invalidHandler.handle(player, response);
|
||||
if(text != null){
|
||||
player.sendMessage(text);
|
||||
}
|
||||
|
||||
player.sendMessage(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String colorizeName(int id, String name){
|
||||
Player player = Groups.player.getByID(id);
|
||||
if(name == null || player == null) return null;
|
||||
return "[#" + player.color().toString().toUpperCase() + "]" + name;
|
||||
}
|
||||
|
||||
@Remote(called = Loc.client, variants = Variant.one)
|
||||
public static void connect(String ip, int port){
|
||||
if(!steam && ip.startsWith("steam:")) return;
|
||||
netClient.disconnectQuietly();
|
||||
logic.reset();
|
||||
|
||||
ui.join.connect(ip, port);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
@Remote(targets = Loc.client, priority = PacketPriority.high)
|
||||
public static void ping(Player player, long time){
|
||||
Call.pingResponse(player.con, time);
|
||||
}
|
||||
@@ -257,7 +343,7 @@ public class NetClient implements ApplicationListener{
|
||||
public static void kick(KickReason reason){
|
||||
netClient.disconnectQuietly();
|
||||
logic.reset();
|
||||
|
||||
|
||||
if(reason == KickReason.serverRestarting){
|
||||
ui.join.reconnect();
|
||||
return;
|
||||
@@ -281,83 +367,42 @@ public class NetClient implements ApplicationListener{
|
||||
ui.loadfrag.hide();
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true)
|
||||
public static void setHudText(String message){
|
||||
if(message == null) return;
|
||||
|
||||
ui.hudfrag.setHudText(message);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void hideHudText(){
|
||||
ui.hudfrag.toggleHudText(false);
|
||||
}
|
||||
|
||||
/** TCP version */
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setHudTextReliable(String message){
|
||||
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;
|
||||
|
||||
ui.showText("", message);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void infoPopup(String message, float duration, int align, int top, int left, int bottom, int right){
|
||||
if(message == null) return;
|
||||
|
||||
ui.showInfoPopup(message, duration, align, top, left, bottom, right);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void label(String message, float duration, float worldx, float worldy){
|
||||
if(message == null) return;
|
||||
|
||||
ui.showLabel(message, duration, worldx, worldy);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true)
|
||||
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 effectReliable(Effect effect, float x, float y, float rotation, Color color){
|
||||
effect(effect, x, y, rotation, color);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void infoToast(String message, float duration){
|
||||
if(message == null) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setRule(String rule, String jsonData){
|
||||
try{
|
||||
//readField searches for the specified value, so create a fake parent for it.
|
||||
tmpJsonMap.child = null;
|
||||
tmpJsonMap.addChild(rule, new JsonReader().parse(jsonData));
|
||||
JsonIO.json.readField(state.rules, rule, tmpJsonMap);
|
||||
}catch(Throwable error){
|
||||
Log.err("Failed to read rule", error);
|
||||
}
|
||||
}
|
||||
|
||||
//NOTE: avoid using this, runs into packet/buffer size limitations
|
||||
@Remote(variants = Variant.both)
|
||||
public static void setObjectives(MapObjectives executor){
|
||||
state.rules.objectives = executor;
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, called = Loc.server)
|
||||
public static void clearObjectives(){
|
||||
state.rules.objectives.clear();
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, called = Loc.server)
|
||||
public static void completeObjective(int index){
|
||||
var obj = state.rules.objectives.get(index);
|
||||
if(obj != null){
|
||||
obj.done();
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void worldDataBegin(){
|
||||
Groups.clear();
|
||||
@@ -382,6 +427,13 @@ public class NetClient implements ApplicationListener{
|
||||
player.set(x, y);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, unreliable = true)
|
||||
public static void setCameraPosition(float x, float y){
|
||||
if(Core.camera != null){
|
||||
Core.camera.position.set(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote
|
||||
public static void playerDisconnect(int playerid){
|
||||
if(netClient != null){
|
||||
@@ -390,67 +442,88 @@ public class NetClient implements ApplicationListener{
|
||||
Groups.player.removeByID(playerid);
|
||||
}
|
||||
|
||||
public static void readSyncEntity(DataInputStream input, Reads read) throws IOException{
|
||||
int id = input.readInt();
|
||||
byte typeID = input.readByte();
|
||||
|
||||
Syncc entity = Groups.sync.getByID(id);
|
||||
boolean add = false, created = false;
|
||||
|
||||
if(entity == null && id == player.id()){
|
||||
entity = player;
|
||||
add = true;
|
||||
}
|
||||
|
||||
//entity must not be added yet, so create it
|
||||
if(entity == null){
|
||||
entity = (Syncc)EntityMapping.map(typeID & 0xFF).get();
|
||||
entity.id(id);
|
||||
if(!netClient.isEntityUsed(entity.id())){
|
||||
add = true;
|
||||
}
|
||||
created = true;
|
||||
}
|
||||
|
||||
//read the entity
|
||||
entity.readSync(read);
|
||||
|
||||
if(created){
|
||||
//snap initial starting position
|
||||
entity.snapSync();
|
||||
}
|
||||
|
||||
if(add){
|
||||
entity.add();
|
||||
netClient.addRemovedEntity(entity.id());
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void entitySnapshot(short amount, short dataLen, byte[] data){
|
||||
public static void entitySnapshot(short amount, byte[] data){
|
||||
try{
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen));
|
||||
netClient.lastSnapshotTimestamp = Time.millis();
|
||||
netClient.byteStream.setBytes(data);
|
||||
DataInputStream input = netClient.dataStream;
|
||||
|
||||
//go through each entity
|
||||
for(int j = 0; j < amount; j++){
|
||||
int id = input.readInt();
|
||||
byte typeID = input.readByte();
|
||||
|
||||
Syncc entity = Groups.sync.getByID(id);
|
||||
boolean add = false, created = false;
|
||||
|
||||
if(entity == null && id == player.id()){
|
||||
entity = player;
|
||||
add = true;
|
||||
}
|
||||
|
||||
//entity must not be added yet, so create it
|
||||
if(entity == null){
|
||||
entity = (Syncc)EntityMapping.map(typeID).get();
|
||||
entity.id(id);
|
||||
if(!netClient.isEntityUsed(entity.id())){
|
||||
add = true;
|
||||
}
|
||||
created = true;
|
||||
}
|
||||
|
||||
//read the entity
|
||||
entity.readSync(Reads.get(input));
|
||||
|
||||
if(created){
|
||||
//snap initial starting position
|
||||
entity.snapSync();
|
||||
}
|
||||
|
||||
if(add){
|
||||
entity.add();
|
||||
netClient.addRemovedEntity(entity.id());
|
||||
}
|
||||
readSyncEntity(input, Reads.get(input));
|
||||
}
|
||||
}catch(Exception e){
|
||||
//don't disconnect, just log it
|
||||
Log.err("Error reading entity snapshot", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void hiddenSnapshot(IntSeq ids){
|
||||
for(int i = 0; i < ids.size; i++){
|
||||
int id = ids.items[i];
|
||||
var entity = Groups.sync.getByID(id);
|
||||
if(entity != null){
|
||||
entity.handleSyncHidden();
|
||||
}
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, priority = PacketPriority.low, unreliable = true)
|
||||
public static void blockSnapshot(short amount, short dataLen, byte[] data){
|
||||
public static void blockSnapshot(short amount, byte[] data){
|
||||
try{
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen));
|
||||
netClient.byteStream.setBytes(data);
|
||||
DataInputStream input = netClient.dataStream;
|
||||
|
||||
for(int i = 0; i < amount; i++){
|
||||
int pos = input.readInt();
|
||||
short block = input.readShort();
|
||||
Tile tile = world.tile(pos);
|
||||
if(tile == null || tile.build == null){
|
||||
Log.warn("Missing entity at @. Skipping block snapshot.", tile);
|
||||
break;
|
||||
}
|
||||
tile.build.readAll(Reads.get(input), tile.build.version());
|
||||
if(tile.build.block.id != block){
|
||||
Log.warn("Block ID mismatch at @: @ != @. Skipping block snapshot.", tile, tile.build.block.id, block);
|
||||
break;
|
||||
}
|
||||
tile.build.readSync(Reads.get(input), tile.build.version());
|
||||
}
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
@@ -458,7 +531,7 @@ public class NetClient implements ApplicationListener{
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, short coreDataLen, byte[] coreData){
|
||||
public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, byte tps, long rand0, long rand1, byte[] coreData){
|
||||
try{
|
||||
if(wave > state.wave){
|
||||
state.wave = wave;
|
||||
@@ -469,22 +542,30 @@ public class NetClient implements ApplicationListener{
|
||||
state.wavetime = waveTime;
|
||||
state.wave = wave;
|
||||
state.enemies = enemies;
|
||||
state.serverPaused = paused;
|
||||
if(!state.isMenu()){
|
||||
state.set(paused ? State.paused : State.playing);
|
||||
}
|
||||
state.serverTps = tps & 0xff;
|
||||
|
||||
//note that this is far from a guarantee that random state is synced - tiny changes in delta and ping can throw everything off again.
|
||||
//syncing will only make much of a difference when rand() is called infrequently
|
||||
GlobalVars.rand.seed0 = rand0;
|
||||
GlobalVars.rand.seed1 = rand1;
|
||||
|
||||
universe.updateNetSeconds(timeData);
|
||||
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(coreData, coreDataLen));
|
||||
netClient.byteStream.setBytes(coreData);
|
||||
DataInputStream input = netClient.dataStream;
|
||||
dataReads.input = input;
|
||||
|
||||
byte cores = input.readByte();
|
||||
for(int i = 0; i < cores; i++){
|
||||
int pos = input.readInt();
|
||||
Tile tile = world.tile(pos);
|
||||
|
||||
if(tile != null && tile.build != null){
|
||||
tile.build.items.read(Reads.get(input));
|
||||
int teams = input.readUnsignedByte();
|
||||
for(int i = 0; i < teams; i++){
|
||||
int team = input.readUnsignedByte();
|
||||
TeamData data = Team.all[team].data();
|
||||
if(data.cores.any()){
|
||||
data.cores.first().items.read(dataReads);
|
||||
}else{
|
||||
new ItemModule().read(Reads.get(input));
|
||||
new ItemModule().read(dataReads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +579,18 @@ public class NetClient implements ApplicationListener{
|
||||
if(!net.client()) return;
|
||||
|
||||
if(state.isGame()){
|
||||
if(!connecting) sync();
|
||||
if(!connecting){
|
||||
sync();
|
||||
|
||||
//timeout if UDP snapshot packets are not received for a while
|
||||
if(lastSnapshotTimestamp > 0 && Time.timeSinceMillis(lastSnapshotTimestamp) > entitySnapshotTimeout){
|
||||
Log.err("Timed out after not received UDP snapshots.");
|
||||
quiet = true;
|
||||
ui.showErrorMessage("@disconnect.snapshottimeout");
|
||||
net.disconnect();
|
||||
lastSnapshotTimestamp = 0;
|
||||
}
|
||||
}
|
||||
}else if(!connecting){
|
||||
net.disconnect();
|
||||
}else{ //...must be connecting
|
||||
@@ -514,6 +606,11 @@ public class NetClient implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets the world data timeout counter. */
|
||||
public void resetTimeout(){
|
||||
timeoutTime = 0f;
|
||||
}
|
||||
|
||||
public boolean isConnecting(){
|
||||
return connecting;
|
||||
}
|
||||
@@ -530,6 +627,7 @@ public class NetClient implements ApplicationListener{
|
||||
Core.app.post(Call::connectConfirm);
|
||||
Time.runTask(40f, platform::updateRPC);
|
||||
Core.app.post(ui.loadfrag::hide);
|
||||
lastSnapshotTimestamp = Time.millis();
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
@@ -540,6 +638,7 @@ public class NetClient implements ApplicationListener{
|
||||
quietReset = false;
|
||||
quiet = false;
|
||||
lastSent = 0;
|
||||
lastSnapshotTimestamp = 0;
|
||||
|
||||
Groups.clear();
|
||||
ui.chatfrag.clearMessages();
|
||||
@@ -581,50 +680,24 @@ public class NetClient implements ApplicationListener{
|
||||
|
||||
void sync(){
|
||||
if(timer.get(0, playerSyncTime)){
|
||||
BuildPlan[] requests = null;
|
||||
if(player.isBuilder()){
|
||||
//limit to 10 to prevent buffer overflows
|
||||
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.unit().plans().get(i);
|
||||
if(plan.config instanceof byte[] b){
|
||||
int length = b.length;
|
||||
totalLength += length;
|
||||
}
|
||||
|
||||
if(totalLength > 1024){
|
||||
usedRequests = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
requests = new BuildPlan[usedRequests];
|
||||
for(int i = 0; i < usedRequests; i++){
|
||||
requests[i] = player.unit().plans().get(i);
|
||||
}
|
||||
}
|
||||
|
||||
Unit unit = player.dead() ? Nulls.unit : player.unit();
|
||||
int uid = player.dead() ? -1 : unit.id;
|
||||
boolean dead = player.dead();
|
||||
Unit unit = dead ? null : player.unit();
|
||||
int uid = dead || unit == null ? -1 : unit.id;
|
||||
|
||||
Call.clientSnapshot(
|
||||
lastSent++,
|
||||
uid,
|
||||
player.dead(),
|
||||
unit.x, unit.y,
|
||||
player.unit().aimX(), player.unit().aimY(),
|
||||
unit.rotation,
|
||||
dead,
|
||||
dead ? player.x : unit.x, dead ? player.y : unit.y,
|
||||
dead ? 0f : unit.aimX(), dead ? 0f : unit.aimY(),
|
||||
unit == null ? 0f : unit.rotation,
|
||||
unit instanceof Mechc m ? m.baseRotation() : 0,
|
||||
unit.vel.x, unit.vel.y,
|
||||
player.unit().mineTile,
|
||||
unit == null ? 0f : unit.vel.x, unit == null ? 0f : unit.vel.y,
|
||||
dead ? null : unit.mineTile,
|
||||
player.boosting, player.shooting, ui.chatfrag.shown(), control.input.isBuilding,
|
||||
requests,
|
||||
player.isBuilder() && unit != null ? unit.plans : null,
|
||||
Core.camera.position.x, Core.camera.position.y,
|
||||
Core.camera.width * viewScale, Core.camera.height * viewScale
|
||||
Core.camera.width, Core.camera.height
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,20 @@ import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import arc.util.CommandHandler.*;
|
||||
import arc.util.io.*;
|
||||
import arc.util.serialization.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.logic.*;
|
||||
import mindustry.net.*;
|
||||
import mindustry.net.Administration.*;
|
||||
import mindustry.net.Packets.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.CoreBlock.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
@@ -35,21 +34,24 @@ import static mindustry.Vars.*;
|
||||
|
||||
public class NetServer implements ApplicationListener{
|
||||
/** note that snapshots are compressed, so the max snapshot size here is above the typical UDP safe limit */
|
||||
private static final int maxSnapshotSize = 800, timerBlockSync = 0;
|
||||
private static final float serverSyncTime = 12, blockSyncTime = 60 * 6;
|
||||
private static final int maxSnapshotSize = 800;
|
||||
private static final int timerBlockSync = 0, timerHealthSync = 1;
|
||||
private static final float blockSyncTime = 60 * 6, healthSyncTime = 30;
|
||||
private static final FloatBuffer fbuffer = FloatBuffer.allocate(20);
|
||||
private static final Writes dataWrites = new Writes(null);
|
||||
private static final IntSeq hiddenIds = new IntSeq();
|
||||
private static final IntSeq healthSeq = new IntSeq(maxSnapshotSize / 4 + 1);
|
||||
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 = tilesize * 12f;
|
||||
private static final float correctDist = tilesize * 14f;
|
||||
|
||||
public final Administration admins = new Administration();
|
||||
public final CommandHandler clientCommands = new CommandHandler("/");
|
||||
public Administration admins = new Administration();
|
||||
public CommandHandler clientCommands = new CommandHandler("/");
|
||||
public TeamAssigner assigner = (player, players) -> {
|
||||
if(state.rules.pvp){
|
||||
//find team with minimum amount of players and auto-assign player to that.
|
||||
TeamData re = state.teams.getActive().min(data -> {
|
||||
if((state.rules.waveTeam == data.team && state.rules.waves) || !data.team.active()) return Integer.MAX_VALUE;
|
||||
if((state.rules.waveTeam == data.team && state.rules.waves) || !data.team.active() || data.team == Team.derelict) return Integer.MAX_VALUE;
|
||||
|
||||
int count = 0;
|
||||
for(Player other : players){
|
||||
@@ -57,16 +59,54 @@ public class NetServer implements ApplicationListener{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return (float)count + Mathf.random(-0.1f, 0.1f); //if several have the same playercount pick random
|
||||
});
|
||||
return re == null ? null : re.team;
|
||||
}
|
||||
|
||||
return state.rules.defaultTeam;
|
||||
};
|
||||
/** Converts a message + NULLABLE player sender into a single string. Override for custom prefixes/suffixes. */
|
||||
public ChatFormatter chatFormatter = (player, message) -> player == null ? message : "[coral][[" + player.coloredName() + "[coral]]:[white] " + message;
|
||||
|
||||
private boolean closing = false;
|
||||
private Interval timer = new Interval();
|
||||
/** Handles an incorrect command response. Returns text that will be sent to player. Override for customisation. */
|
||||
public InvalidCommandHandler invalidHandler = (player, response) -> {
|
||||
if(response.type == ResponseType.manyArguments){
|
||||
return "[scarlet]Too many arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
|
||||
}else if(response.type == ResponseType.fewArguments){
|
||||
return "[scarlet]Too few arguments. Usage:[lightgray] " + response.command.text + "[gray] " + response.command.paramText;
|
||||
}else{ //unknown command
|
||||
int minDst = 0;
|
||||
Command closest = null;
|
||||
|
||||
for(Command command : netServer.clientCommands.getCommandList()){
|
||||
int dst = Strings.levenshtein(command.text, response.runCommand);
|
||||
if(dst < 3 && (closest == null || dst < minDst)){
|
||||
minDst = dst;
|
||||
closest = command;
|
||||
}
|
||||
}
|
||||
|
||||
if(closest != null){
|
||||
return "[scarlet]Unknown command. Did you mean \"[lightgray]" + closest.text + "[]\"?";
|
||||
}else{
|
||||
return "[scarlet]Unknown command. Check [lightgray]/help[scarlet].";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private boolean closing = false, pvpAutoPaused = true;
|
||||
private Interval timer = new Interval(10);
|
||||
private IntSet buildHealthChanged = new IntSet();
|
||||
|
||||
/** Current kick session. */
|
||||
public @Nullable VoteSession currentlyKicking = null;
|
||||
/** Duration of a kick in seconds. */
|
||||
public static int kickDuration = 60 * 60;
|
||||
/** Voting round duration in seconds. */
|
||||
public static float voteDuration = 0.5f * 60;
|
||||
/** Cooldown between votes in seconds. */
|
||||
public static int voteCooldown = 60 * 5;
|
||||
|
||||
private ReusableByteOutStream writeBuffer = new ReusableByteOutStream(127);
|
||||
private Writes outputBuffer = new Writes(new DataOutputStream(writeBuffer));
|
||||
@@ -77,10 +117,16 @@ public class NetServer implements ApplicationListener{
|
||||
private DataOutputStream dataStream = new DataOutputStream(syncStream);
|
||||
/** Packet handlers for custom types of messages. */
|
||||
private ObjectMap<String, Seq<Cons2<Player, String>>> customPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for custom types of messages - binary version. */
|
||||
private ObjectMap<String, Seq<Cons2<Player, byte[]>>> customBinaryPacketHandlers = new ObjectMap<>();
|
||||
/** Packet handlers for logic client data */
|
||||
private ObjectMap<String, Seq<Cons2<Player, Object>>> logicClientDataHandlers = new ObjectMap<>();
|
||||
|
||||
public NetServer(){
|
||||
|
||||
net.handleServer(Connect.class, (con, connect) -> {
|
||||
Events.fire(new ConnectionEvent(con));
|
||||
|
||||
if(admins.isIPBanned(connect.addressTCP) || admins.isSubnetBanned(connect.addressTCP)){
|
||||
con.kick(KickReason.banned);
|
||||
}
|
||||
@@ -93,23 +139,19 @@ public class NetServer implements ApplicationListener{
|
||||
});
|
||||
|
||||
net.handleServer(ConnectPacket.class, (con, packet) -> {
|
||||
if(con.kicked) return;
|
||||
|
||||
if(con.address.startsWith("steam:")){
|
||||
packet.uuid = con.address.substring("steam:".length());
|
||||
}
|
||||
|
||||
String uuid = packet.uuid;
|
||||
byte[] buuid = Base64Coder.decode(uuid);
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(buuid, 0, 8);
|
||||
ByteBuffer buff = ByteBuffer.allocate(8);
|
||||
buff.put(buuid, 8, 8);
|
||||
buff.position(0);
|
||||
if(crc.getValue() != buff.getLong()){
|
||||
con.kick(KickReason.clientOutdated);
|
||||
return;
|
||||
}
|
||||
Events.fire(new ConnectPacketEvent(con, packet));
|
||||
|
||||
if(admins.isIPBanned(con.address) || admins.isSubnetBanned(con.address)) return;
|
||||
con.connectTime = Time.millis();
|
||||
|
||||
String uuid = packet.uuid;
|
||||
|
||||
if(admins.isIPBanned(con.address) || admins.isSubnetBanned(con.address) || con.kicked || !con.isConnected()) return;
|
||||
|
||||
if(con.hasBegunConnecting){
|
||||
con.kick(KickReason.idInUse);
|
||||
@@ -155,7 +197,8 @@ public class NetServer implements ApplicationListener{
|
||||
if(!extraMods.isEmpty()){
|
||||
result.append("Unnecessary mods:[lightgray]\n").append("> ").append(extraMods.toString("\n> "));
|
||||
}
|
||||
con.kick(result.toString());
|
||||
con.kick(result.toString(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!admins.isWhitelisted(packet.uuid, packet.usid)){
|
||||
@@ -164,7 +207,7 @@ public class NetServer implements ApplicationListener{
|
||||
info.id = packet.uuid;
|
||||
admins.save();
|
||||
Call.infoMessage(con, "You are not whitelisted here.");
|
||||
info("&lcDo &lywhitelist-add @&lc to whitelist the player &lb'@'", packet.uuid, packet.name);
|
||||
info("&lcDo &lywhitelist add @&lc to whitelist the player &lb'@'", packet.uuid, packet.name);
|
||||
con.kick(KickReason.whitelist);
|
||||
return;
|
||||
}
|
||||
@@ -177,15 +220,24 @@ public class NetServer implements ApplicationListener{
|
||||
boolean preventDuplicates = headless && netServer.admins.isStrict();
|
||||
|
||||
if(preventDuplicates){
|
||||
if(Groups.player.contains(p -> p.name.trim().equalsIgnoreCase(packet.name.trim()))){
|
||||
if(Groups.player.contains(p -> Strings.stripColors(p.name).trim().equalsIgnoreCase(Strings.stripColors(packet.name).trim()))){
|
||||
con.kick(KickReason.nameInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
if(Groups.player.contains(player -> player.uuid().equals(packet.uuid) || player.usid().equals(packet.usid))){
|
||||
con.uuid = packet.uuid;
|
||||
con.kick(KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
for(var otherCon : net.getConnections()){
|
||||
if(otherCon != con && uuid.equals(otherCon.uuid)){
|
||||
con.uuid = packet.uuid;
|
||||
con.kick(KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packet.name = fixName(packet.name);
|
||||
@@ -195,6 +247,10 @@ public class NetServer implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet.locale == null){
|
||||
packet.locale = "en";
|
||||
}
|
||||
|
||||
String ip = con.address;
|
||||
|
||||
admins.updatePlayerJoined(uuid, ip, packet.name);
|
||||
@@ -215,6 +271,7 @@ public class NetServer implements ApplicationListener{
|
||||
player.con.uuid = uuid;
|
||||
player.con.mobile = packet.mobile;
|
||||
player.name = packet.name;
|
||||
player.locale = packet.locale;
|
||||
player.color.set(packet.color).a(1f);
|
||||
|
||||
//save admin ID but don't overwrite it
|
||||
@@ -243,21 +300,6 @@ public class NetServer implements ApplicationListener{
|
||||
Events.fire(new PlayerConnect(player));
|
||||
});
|
||||
|
||||
net.handleServer(InvokePacket.class, (con, packet) -> {
|
||||
if(con.player == null) return;
|
||||
try{
|
||||
RemoteReadServer.readPacket(packet.reader(), packet.type, con.player);
|
||||
}catch(ValidateException e){
|
||||
debug("Validation failed for '@': @", e.player, e.getMessage());
|
||||
}catch(RuntimeException e){
|
||||
if(e.getCause() instanceof ValidateException v){
|
||||
debug("Validation failed for '@': @", v.player, v.getMessage());
|
||||
}else{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerCommands();
|
||||
}
|
||||
|
||||
@@ -276,7 +318,7 @@ public class NetServer implements ApplicationListener{
|
||||
int page = args.length > 0 ? Strings.parseInt(args[0]) : 1;
|
||||
int pages = Mathf.ceil((float)clientCommands.getCommandList().size / commandsPerPage);
|
||||
|
||||
page --;
|
||||
page--;
|
||||
|
||||
if(page >= pages || page < 0){
|
||||
player.sendMessage("[scarlet]'page' must be a number between[orange] 1[] and[orange] " + pages + "[scarlet].");
|
||||
@@ -296,74 +338,25 @@ public class NetServer implements ApplicationListener{
|
||||
clientCommands.<Player>register("t", "<message...>", "Send a message only to your teammates.", (args, player) -> {
|
||||
String message = admins.filterMessage(player, args[0]);
|
||||
if(message != null){
|
||||
Groups.player.each(p -> p.team() == player.team(), o -> o.sendMessage(message, player, "[#" + player.team().color.toString() + "]<T>" + NetClient.colorizeName(player.id(), player.name)));
|
||||
String raw = "[#" + player.team().color.toString() + "]<T> " + chatFormatter.format(player, message);
|
||||
Groups.player.each(p -> p.team() == player.team(), o -> o.sendMessage(raw, player, message));
|
||||
}
|
||||
});
|
||||
|
||||
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.");
|
||||
player.sendMessage("[scarlet]You must be an 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)));
|
||||
String raw = "[#" + Pal.adminChat.toString() + "]<A> " + chatFormatter.format(player, args[0]);
|
||||
Groups.player.each(Player::admin, a -> a.sendMessage(raw, player, args[0]));
|
||||
});
|
||||
|
||||
//duration of a a kick in seconds
|
||||
int kickDuration = 60 * 60;
|
||||
//voting round duration in seconds
|
||||
float voteDuration = 0.5f * 60;
|
||||
//cooldown between votes in seconds
|
||||
int voteCooldown = 60 * 5;
|
||||
|
||||
class VoteSession{
|
||||
Player target;
|
||||
ObjectSet<String> voted = new ObjectSet<>();
|
||||
VoteSession[] map;
|
||||
Timer.Task task;
|
||||
int votes;
|
||||
|
||||
public VoteSession(VoteSession[] map, Player target){
|
||||
this.target = target;
|
||||
this.map = map;
|
||||
this.task = Timer.schedule(() -> {
|
||||
if(!checkPass()){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote failed. Not enough votes to kick[orange] @[lightgray].", target.name));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
}
|
||||
}, voteDuration);
|
||||
}
|
||||
|
||||
void vote(Player player, int d){
|
||||
votes += d;
|
||||
voted.addAll(player.uuid(), admins.getInfo(player.uuid()).lastIP);
|
||||
|
||||
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[].[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;
|
||||
Groups.player.each(p -> p.uuid().equals(target.uuid()), p -> p.kick(KickReason.vote));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//cooldowns per player
|
||||
ObjectMap<String, Timekeeper> cooldowns = new ObjectMap<>();
|
||||
//current kick sessions
|
||||
VoteSession[] currentlyKicking = {null};
|
||||
|
||||
clientCommands.<Player>register("votekick", "[player...]", "Vote to kick a player.", (args, player) -> {
|
||||
clientCommands.<Player>register("votekick", "[player] [reason...]", "Vote to kick a player with a valid reason.", (args, player) -> {
|
||||
if(!Config.enableVotekick.bool()){
|
||||
player.sendMessage("[scarlet]Vote-kick is disabled on this server.");
|
||||
return;
|
||||
@@ -379,7 +372,7 @@ public class NetServer implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0] != null){
|
||||
if(currentlyKicking != null){
|
||||
player.sendMessage("[scarlet]A vote is already in progress.");
|
||||
return;
|
||||
}
|
||||
@@ -392,6 +385,8 @@ public class NetServer implements ApplicationListener{
|
||||
builder.append("[lightgray] ").append(p.name).append("[accent] (#").append(p.id()).append(")\n");
|
||||
});
|
||||
player.sendMessage(builder.toString());
|
||||
}else if(args.length == 1){
|
||||
player.sendMessage("[orange]You need a valid reason to kick the player. Add a reason after the player name.");
|
||||
}else{
|
||||
Player found;
|
||||
if(args[0].length() > 1 && args[0].startsWith("#") && Strings.canParseInt(args[0].substring(1))){
|
||||
@@ -402,7 +397,9 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
|
||||
if(found != null){
|
||||
if(found.admin){
|
||||
if(found == player){
|
||||
player.sendMessage("[scarlet]You can't vote to kick yourself.");
|
||||
}else if(found.admin){
|
||||
player.sendMessage("[scarlet]Did you really expect to be able to kick an admin?");
|
||||
}else if(found.isLocal()){
|
||||
player.sendMessage("[scarlet]Local players cannot be kicked.");
|
||||
@@ -416,10 +413,11 @@ public class NetServer implements ApplicationListener{
|
||||
return;
|
||||
}
|
||||
|
||||
VoteSession session = new VoteSession(currentlyKicking, found);
|
||||
VoteSession session = new VoteSession(found);
|
||||
session.vote(player, 1);
|
||||
Call.sendMessage(Strings.format("[lightgray]Reason:[orange] @[lightgray].", args[1]));
|
||||
vtime.reset();
|
||||
currentlyKicking[0] = session;
|
||||
currentlyKicking = session;
|
||||
}
|
||||
}else{
|
||||
player.sendMessage("[scarlet]No player [orange]'" + args[0] + "'[scarlet] found.");
|
||||
@@ -427,38 +425,50 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
});
|
||||
|
||||
clientCommands.<Player>register("vote", "<y/n>", "Vote to kick the current player.", (arg, player) -> {
|
||||
if(currentlyKicking[0] == null){
|
||||
clientCommands.<Player>register("vote", "<y/n/c>", "Vote to kick the current player. Admins can cancel the voting with 'c'.", (arg, player) -> {
|
||||
if(currentlyKicking == null){
|
||||
player.sendMessage("[scarlet]Nobody is being voted on.");
|
||||
}else{
|
||||
if(player.isLocal()){
|
||||
player.sendMessage("Local players can't vote. Kick the player yourself instead.");
|
||||
if(player.admin && arg[0].equalsIgnoreCase("c")){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote canceled by admin[orange] @[lightgray].", player.name));
|
||||
currentlyKicking.task.cancel();
|
||||
currentlyKicking = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.isLocal()){
|
||||
player.sendMessage("[scarlet]Local players can't vote. Kick the player yourself instead.");
|
||||
return;
|
||||
}
|
||||
|
||||
int sign = switch(arg[0].toLowerCase()){
|
||||
case "y", "yes" -> 1;
|
||||
case "n", "no" -> -1;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
//hosts can vote all they want
|
||||
if((currentlyKicking[0].voted.contains(player.uuid()) || currentlyKicking[0].voted.contains(admins.getInfo(player.uuid()).lastIP))){
|
||||
player.sendMessage("[scarlet]You've already voted. Sit down.");
|
||||
if((currentlyKicking.voted.get(player.uuid(), 2) == sign || currentlyKicking.voted.get(admins.getInfo(player.uuid()).lastIP, 2) == sign)){
|
||||
player.sendMessage(Strings.format("[scarlet]You've already voted @. Sit down.", arg[0].toLowerCase()));
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0].target == player){
|
||||
if(currentlyKicking.target == player){
|
||||
player.sendMessage("[scarlet]You can't vote on your own trial.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0].target.team() != player.team()){
|
||||
if(currentlyKicking.target.team() != player.team()){
|
||||
player.sendMessage("[scarlet]You can't vote for other teams.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!arg[0].equalsIgnoreCase("y") && !arg[0].equalsIgnoreCase("n")){
|
||||
if(sign == 0){
|
||||
player.sendMessage("[scarlet]Vote either 'y' (yes) or 'n' (no).");
|
||||
return;
|
||||
}
|
||||
|
||||
int sign = arg[0].equalsIgnoreCase("y") ? 1 : -1;
|
||||
currentlyKicking[0].vote(player, sign);
|
||||
currentlyKicking.vote(player, sign);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -509,6 +519,18 @@ public class NetServer implements ApplicationListener{
|
||||
return customPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addBinaryPacketHandler(String type, Cons2<Player, byte[]> handler){
|
||||
customBinaryPacketHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
|
||||
public Seq<Cons2<Player, byte[]>> getBinaryPacketHandlers(String type){
|
||||
return customBinaryPacketHandlers.get(type, Seq::new);
|
||||
}
|
||||
|
||||
public void addLogicDataHandler(String type, Cons2<Player, Object> handler){
|
||||
logicClientDataHandlers.get(type, Seq::new).add(handler);
|
||||
}
|
||||
|
||||
public static void onDisconnect(Player player, String reason){
|
||||
//singleplayer multiplayer weirdness
|
||||
if(player.con == null){
|
||||
@@ -523,7 +545,7 @@ public class NetServer implements ApplicationListener{
|
||||
Call.playerDisconnect(player.id());
|
||||
}
|
||||
|
||||
String message = Strings.format("&lb@&fi&lk has disconnected. &fi&lk[&lb@&fi&lk] (@)", player.name, player.uuid(), reason);
|
||||
String message = Strings.format("&lb@&fi&lk has disconnected. [&lb@&fi&lk] (@)", player.plainName(), player.uuid(), reason);
|
||||
if(Config.showConnectMessages.bool()) info(message);
|
||||
}
|
||||
|
||||
@@ -531,6 +553,38 @@ public class NetServer implements ApplicationListener{
|
||||
player.con.hasDisconnected = true;
|
||||
}
|
||||
|
||||
//these functions are for debugging only, and will be removed!
|
||||
|
||||
@Remote(targets = Loc.client, variants = Variant.one)
|
||||
public static void requestDebugStatus(Player player){
|
||||
int flags =
|
||||
(player.con.hasDisconnected ? 1 : 0) |
|
||||
(player.con.hasConnected ? 2 : 0) |
|
||||
(player.isAdded() ? 4 : 0) |
|
||||
(player.con.hasBegunConnecting ? 8 : 0);
|
||||
|
||||
Call.debugStatusClient(player.con, flags, player.con.lastReceivedClientSnapshot, player.con.snapshotsSent);
|
||||
Call.debugStatusClientUnreliable(player.con, flags, player.con.lastReceivedClientSnapshot, player.con.snapshotsSent);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, priority = PacketPriority.high)
|
||||
public static void debugStatusClient(int value, int lastClientSnapshot, int snapshotsSent){
|
||||
logClientStatus(true, value, lastClientSnapshot, snapshotsSent);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, priority = PacketPriority.high, unreliable = true)
|
||||
public static void debugStatusClientUnreliable(int value, int lastClientSnapshot, int snapshotsSent){
|
||||
logClientStatus(false, value, lastClientSnapshot, snapshotsSent);
|
||||
}
|
||||
|
||||
static void logClientStatus(boolean reliable, int value, int lastClientSnapshot, int snapshotsSent){
|
||||
Log.info("@ Debug status received. disconnected = @, connected = @, added = @, begunConnecting = @ lastClientSnapshot = @, snapshotsSent = @",
|
||||
reliable ? "[RELIABLE]" : "[UNRELIABLE]",
|
||||
(value & 1) != 0, (value & 2) != 0, (value & 4) != 0, (value & 8) != 0,
|
||||
lastClientSnapshot, snapshotsSent
|
||||
);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void serverPacketReliable(Player player, String type, String contents){
|
||||
if(netServer.customPacketHandlers.containsKey(type)){
|
||||
@@ -545,24 +599,53 @@ public class NetServer implements ApplicationListener{
|
||||
serverPacketReliable(player, type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void serverBinaryPacketReliable(Player player, String type, byte[] contents){
|
||||
if(netServer.customPacketHandlers.containsKey(type)){
|
||||
for(var c : netServer.customBinaryPacketHandlers.get(type)){
|
||||
c.get(player, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, unreliable = true)
|
||||
public static void serverBinaryPacketUnreliable(Player player, String type, byte[] contents){
|
||||
serverBinaryPacketReliable(player, type, contents);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void clientLogicDataReliable(Player player, String channel, Object value){
|
||||
Seq<Cons2<Player, Object>> handlers = netServer.logicClientDataHandlers.get(channel);
|
||||
if(handlers != null){
|
||||
for(Cons2<Player, Object> handler : handlers){
|
||||
handler.get(player, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, unreliable = true)
|
||||
public static void clientLogicDataUnreliable(Player player, String channel, Object value){
|
||||
clientLogicDataReliable(player, channel, value);
|
||||
}
|
||||
|
||||
private static boolean invalid(float f){
|
||||
return Float.isInfinite(f) || Float.isNaN(f);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, unreliable = true)
|
||||
@Remote(targets = Loc.client, unreliable = true, priority = PacketPriority.high)
|
||||
public static void clientSnapshot(
|
||||
Player player,
|
||||
int snapshotID,
|
||||
int unitID,
|
||||
boolean dead,
|
||||
float x, float y,
|
||||
float pointerX, float pointerY,
|
||||
float rotation, float baseRotation,
|
||||
float xVelocity, float yVelocity,
|
||||
Tile mining,
|
||||
boolean boosting, boolean shooting, boolean chatting, boolean building,
|
||||
@Nullable BuildPlan[] requests,
|
||||
float viewX, float viewY, float viewWidth, float viewHeight
|
||||
Player player,
|
||||
int snapshotID,
|
||||
int unitID,
|
||||
boolean dead,
|
||||
float x, float y,
|
||||
float pointerX, float pointerY,
|
||||
float rotation, float baseRotation,
|
||||
float xVelocity, float yVelocity,
|
||||
Tile mining,
|
||||
boolean boosting, boolean shooting, boolean chatting, boolean building,
|
||||
@Nullable Queue<BuildPlan> plans,
|
||||
float viewX, float viewY, float viewWidth, float viewHeight
|
||||
){
|
||||
NetConnection con = player.con;
|
||||
if(con == null || snapshotID < con.lastReceivedClientSnapshot) return;
|
||||
@@ -601,22 +684,21 @@ public class NetServer implements ApplicationListener{
|
||||
player.shooting = shooting;
|
||||
player.boosting = boosting;
|
||||
|
||||
player.unit().controlWeapons(shooting, shooting);
|
||||
player.unit().aim(pointerX, pointerY);
|
||||
@Nullable var unit = player.unit();
|
||||
|
||||
if(player.isBuilder()){
|
||||
player.unit().clearBuilding();
|
||||
player.unit().updateBuilding(building);
|
||||
unit.clearBuilding();
|
||||
unit.updateBuilding(building);
|
||||
|
||||
if(requests != null){
|
||||
for(BuildPlan req : requests){
|
||||
if(plans != null){
|
||||
for(BuildPlan req : plans){
|
||||
if(req == null) continue;
|
||||
Tile tile = world.tile(req.x, req.y);
|
||||
if(tile == null || (!req.breaking && req.block == null)) continue;
|
||||
//auto-skip done requests
|
||||
if(req.breaking && tile.block() == Blocks.air){
|
||||
continue;
|
||||
}else if(!req.breaking && tile.block() == req.block && (!req.block.rotate || (tile.build != null && tile.build.rotation == req.rotation))){
|
||||
}else if(!req.breaking && tile.block() == req.block && tile.team() != Team.derelict && (!req.block.rotate || (tile.build != null && tile.build.rotation == req.rotation))){
|
||||
continue;
|
||||
}else if(con.rejectedRequests.contains(r -> r.breaking == req.breaking && r.x == req.x && r.y == req.y)){ //check if request was recently rejected, and skip it if so
|
||||
continue;
|
||||
@@ -635,18 +717,15 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
player.unit().mineTile = mining;
|
||||
|
||||
con.rejectedRequests.clear();
|
||||
|
||||
if(!player.dead()){
|
||||
Unit unit = player.unit();
|
||||
unit.controlWeapons(shooting, shooting);
|
||||
unit.aim(pointerX, pointerY);
|
||||
unit.mineTile = mining;
|
||||
|
||||
long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime);
|
||||
float maxSpeed = unit.realSpeed();
|
||||
if(unit.isGrounded()){
|
||||
maxSpeed *= unit.floorSpeedMultiplier();
|
||||
}
|
||||
long elapsed = Math.min(Time.timeSinceMillis(con.lastReceivedClientTime), 1500);
|
||||
float maxSpeed = unit.speed();
|
||||
|
||||
float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.2f;
|
||||
|
||||
@@ -661,7 +740,6 @@ public class NetServer implements ApplicationListener{
|
||||
vector.limit(maxMove);
|
||||
|
||||
float prevx = unit.x, prevy = unit.y;
|
||||
//unit.set(con.lastPosition);
|
||||
if(!unit.isFlying()){
|
||||
unit.move(vector.x, vector.y);
|
||||
}else{
|
||||
@@ -704,52 +782,69 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, called = Loc.server)
|
||||
public static void adminRequest(Player player, Player other, AdminAction action){
|
||||
public static void adminRequest(Player player, Player other, AdminAction action, Object params){
|
||||
if(!player.admin && !player.isLocal()){
|
||||
warn("ACCESS DENIED: Player @ / @ attempted to perform admin action '@' on '@' without proper security access.",
|
||||
player.name, player.con == null ? "null" : player.con.address, action.name(), other == null ? null : other.name);
|
||||
player.plainName(), player.con == null ? "null" : player.con.address, action.name(), other == null ? null : other.plainName());
|
||||
return;
|
||||
}
|
||||
|
||||
if(other == null || ((other.admin && !player.isLocal()) && other != player)){
|
||||
warn("@ attempted to perform admin action on nonexistant or admin player.", player.name);
|
||||
warn("@ &fi&lk[&lb@&fi&lk]&fb attempted to perform admin action on nonexistant or admin player.", player.plainName(), player.uuid());
|
||||
return;
|
||||
}
|
||||
|
||||
if(action == AdminAction.wave){
|
||||
//no verification is done, so admins can hypothetically spam waves
|
||||
//not a real issue, because server owners may want to do just that
|
||||
logic.skipWave();
|
||||
}else if(action == AdminAction.ban){
|
||||
netServer.admins.banPlayerIP(other.con.address);
|
||||
netServer.admins.banPlayerID(other.con.uuid);
|
||||
other.kick(KickReason.banned);
|
||||
info("&lc@ has banned @.", player.name, other.name);
|
||||
}else if(action == AdminAction.kick){
|
||||
other.kick(KickReason.kick);
|
||||
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){
|
||||
Call.traceInfo(player.con, other, info);
|
||||
}else{
|
||||
NetClient.traceInfo(other, info);
|
||||
Events.fire(new EventType.AdminRequestEvent(player, other, action));
|
||||
|
||||
switch(action){
|
||||
case wave -> {
|
||||
//no verification is done, so admins can hypothetically spam waves
|
||||
//not a real issue, because server owners may want to do just that
|
||||
logic.skipWave();
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has skipped the wave.", player.plainName(), player.uuid());
|
||||
}
|
||||
case ban -> {
|
||||
netServer.admins.banPlayerID(other.con.uuid);
|
||||
netServer.admins.banPlayerIP(other.con.address);
|
||||
other.kick(KickReason.banned);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has banned @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}
|
||||
case kick -> {
|
||||
other.kick(KickReason.kick);
|
||||
info("&lc@ &fi&lk[&lb@&fi&lk]&fb has kicked @ &fi&lk[&lb@&fi&lk]&fb.", player.plainName(), player.uuid(), other.plainName(), other.uuid());
|
||||
}
|
||||
case trace -> {
|
||||
PlayerInfo stats = netServer.admins.getInfo(other.uuid());
|
||||
TraceInfo info = new TraceInfo(other.con.address, other.uuid(), other.locale, other.con.modclient, other.con.mobile, stats.timesJoined, stats.timesKicked, stats.ips.toArray(String.class), stats.names.toArray(String.class));
|
||||
if(player.con != null){
|
||||
Call.traceInfo(player.con, other, info);
|
||||
}else{
|
||||
NetClient.traceInfo(other, info);
|
||||
}
|
||||
}
|
||||
case switchTeam -> {
|
||||
if(params instanceof Team team){
|
||||
other.team(team);
|
||||
}
|
||||
}
|
||||
info("&lc@ has requested trace info of @.", player.name, other.name);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
@Remote(targets = Loc.client, priority = PacketPriority.high)
|
||||
public static void connectConfirm(Player player){
|
||||
if(player.con.kicked) return;
|
||||
|
||||
player.add();
|
||||
|
||||
Events.fire(new PlayerConnectionConfirmed(player));
|
||||
|
||||
if(player.con == null || player.con.hasConnected) return;
|
||||
|
||||
player.con.hasConnected = true;
|
||||
|
||||
if(Config.showConnectMessages.bool()){
|
||||
Call.sendMessage("[accent]" + player.name + "[accent] has connected.");
|
||||
String message = Strings.format("&lb@&fi&lk has connected. &fi&lk[&lb@&fi&lk]", player.name, player.uuid());
|
||||
String message = Strings.format("&lb@&fi&lk has connected. &fi&lk[&lb@&fi&lk]", player.plainName(), player.uuid());
|
||||
info(message);
|
||||
}
|
||||
|
||||
@@ -786,21 +881,38 @@ public class NetServer implements ApplicationListener{
|
||||
}
|
||||
|
||||
if(state.isGame() && net.server()){
|
||||
if(state.rules.pvp){
|
||||
state.serverPaused = isWaitingForPlayers();
|
||||
if(state.rules.pvp && state.rules.pvpAutoPause){
|
||||
boolean waiting = isWaitingForPlayers(), paused = state.isPaused();
|
||||
if(waiting != paused){
|
||||
if(waiting){
|
||||
//is now waiting, enable pausing, flag it correctly
|
||||
pvpAutoPaused = true;
|
||||
state.set(State.paused);
|
||||
}else if(pvpAutoPaused){
|
||||
//no longer waiting, stop pausing
|
||||
state.set(State.playing);
|
||||
pvpAutoPaused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sync();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO I don't like where this is, move somewhere else?
|
||||
/** Queues a building health update. This will be sent in a Call.buildHealthUpdate packet later. */
|
||||
public void buildHealthUpdate(Building build){
|
||||
buildHealthChanged.add(build.pos());
|
||||
}
|
||||
|
||||
/** Should only be used on the headless backend. */
|
||||
public void openServer(){
|
||||
try{
|
||||
net.host(Config.port.num());
|
||||
info("Opened a server on port @.", Config.port.num());
|
||||
}catch(BindException e){
|
||||
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 " + Config.port.num() + " 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);
|
||||
@@ -821,15 +933,15 @@ public class NetServer implements ApplicationListener{
|
||||
short sent = 0;
|
||||
for(Building entity : Groups.build){
|
||||
if(!entity.block.sync) continue;
|
||||
sent ++;
|
||||
sent++;
|
||||
|
||||
dataStream.writeInt(entity.pos());
|
||||
entity.writeAll(Writes.get(dataStream));
|
||||
dataStream.writeShort(entity.block.id);
|
||||
entity.writeSync(Writes.get(dataStream));
|
||||
|
||||
if(syncStream.size() > maxSnapshotSize){
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
Call.blockSnapshot(sent, syncStream.toByteArray());
|
||||
sent = 0;
|
||||
syncStream.reset();
|
||||
}
|
||||
@@ -837,46 +949,54 @@ public class NetServer implements ApplicationListener{
|
||||
|
||||
if(sent > 0){
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
Call.blockSnapshot(sent, syncStream.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
public void writeEntitySnapshot(Player player) throws IOException{
|
||||
byte tps = (byte)Math.min(Core.graphics.getFramesPerSecond(), 255);
|
||||
syncStream.reset();
|
||||
Seq<CoreBuild> cores = state.teams.cores(player.team());
|
||||
int activeTeams = (byte)state.teams.present.count(t -> t.cores.size > 0);
|
||||
|
||||
dataStream.writeByte(cores.size);
|
||||
dataStream.writeByte(activeTeams);
|
||||
dataWrites.output = dataStream;
|
||||
|
||||
for(CoreBuild entity : cores){
|
||||
dataStream.writeInt(entity.tile.pos());
|
||||
entity.items.write(Writes.get(dataStream));
|
||||
//block data isn't important, just send the items for each team, they're synced across cores
|
||||
for(TeamData data : state.teams.present){
|
||||
if(data.cores.size > 0){
|
||||
dataStream.writeByte(data.team.id);
|
||||
data.cores.first().items.write(dataWrites);
|
||||
}
|
||||
}
|
||||
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
|
||||
//write basic state data.
|
||||
Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.serverPaused, state.gameOver, universe.seconds(), (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
|
||||
viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY);
|
||||
Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.isPaused(), state.gameOver,
|
||||
universe.seconds(), tps, GlobalVars.rand.seed0, GlobalVars.rand.seed1, syncStream.toByteArray());
|
||||
|
||||
syncStream.reset();
|
||||
|
||||
hiddenIds.clear();
|
||||
int sent = 0;
|
||||
|
||||
for(Syncc entity : Groups.sync){
|
||||
//TODO write to special list
|
||||
if(entity.isSyncHidden(player)){
|
||||
hiddenIds.add(entity.id());
|
||||
continue;
|
||||
}
|
||||
|
||||
//write all entities now
|
||||
dataStream.writeInt(entity.id()); //write id
|
||||
dataStream.writeByte(entity.classId()); //write type ID
|
||||
dataStream.writeByte(entity.classId() & 0xFF); //write type ID
|
||||
entity.writeSync(Writes.get(dataStream)); //write entity
|
||||
|
||||
sent++;
|
||||
|
||||
if(syncStream.size() > maxSnapshotSize){
|
||||
dataStream.close();
|
||||
byte[] syncBytes = syncStream.toByteArray();
|
||||
Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes));
|
||||
Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray());
|
||||
sent = 0;
|
||||
syncStream.reset();
|
||||
}
|
||||
@@ -885,14 +1005,18 @@ public class NetServer implements ApplicationListener{
|
||||
if(sent > 0){
|
||||
dataStream.close();
|
||||
|
||||
byte[] syncBytes = syncStream.toByteArray();
|
||||
Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes));
|
||||
Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray());
|
||||
}
|
||||
|
||||
if(hiddenIds.size > 0){
|
||||
Call.hiddenSnapshot(player.con, hiddenIds);
|
||||
}
|
||||
|
||||
player.con.snapshotsSent++;
|
||||
}
|
||||
|
||||
String fixName(String name){
|
||||
name = name.trim();
|
||||
public String fixName(String name){
|
||||
name = name.trim().replace("\n", "").replace("\t", "");
|
||||
if(name.equals("[") || name.equals("]")){
|
||||
return "";
|
||||
}
|
||||
@@ -915,20 +1039,20 @@ public class NetServer implements ApplicationListener{
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
String checkColor(String str){
|
||||
public String checkColor(String str){
|
||||
for(int i = 1; i < str.length(); i++){
|
||||
if(str.charAt(i) == ']'){
|
||||
String color = str.substring(1, i);
|
||||
|
||||
if(Colors.get(color.toUpperCase()) != null || Colors.get(color.toLowerCase()) != null){
|
||||
Color result = (Colors.get(color.toLowerCase()) == null ? Colors.get(color.toUpperCase()) : Colors.get(color.toLowerCase()));
|
||||
if(result.a <= 0.8f){
|
||||
if(result.a < 1f){
|
||||
return str.substring(i + 1);
|
||||
}
|
||||
}else{
|
||||
try{
|
||||
Color result = Color.valueOf(color);
|
||||
if(result.a <= 0.8f){
|
||||
if(result.a < 1f){
|
||||
return str.substring(i + 1);
|
||||
}
|
||||
}catch(Exception e){
|
||||
@@ -942,20 +1066,23 @@ public class NetServer implements ApplicationListener{
|
||||
|
||||
void sync(){
|
||||
try{
|
||||
int interval = Config.snapshotInterval.num();
|
||||
Groups.player.each(p -> !p.isLocal(), player -> {
|
||||
if(player.con == null || !player.con.isConnected()){
|
||||
onDisconnect(player, "disappeared");
|
||||
return;
|
||||
}
|
||||
|
||||
NetConnection connection = player.con;
|
||||
var connection = player.con;
|
||||
|
||||
if(!player.timer(0, serverSyncTime) || !connection.hasConnected) return;
|
||||
if(Time.timeSinceMillis(connection.syncTime) < interval || !connection.hasConnected) return;
|
||||
|
||||
connection.syncTime = Time.millis();
|
||||
|
||||
try{
|
||||
writeEntitySnapshot(player);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
Log.err(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -963,12 +1090,91 @@ public class NetServer implements ApplicationListener{
|
||||
writeBlockSnapshots();
|
||||
}
|
||||
|
||||
if(Groups.player.size() > 0 && buildHealthChanged.size > 0 && timer.get(timerHealthSync, healthSyncTime)){
|
||||
healthSeq.clear();
|
||||
|
||||
var iter = buildHealthChanged.iterator();
|
||||
while(iter.hasNext){
|
||||
int next = iter.next();
|
||||
var build = world.build(next);
|
||||
|
||||
//pack pos + health into update list
|
||||
if(build != null){
|
||||
healthSeq.add(next, Float.floatToRawIntBits(build.health));
|
||||
}
|
||||
|
||||
//if size exceeds snapshot limit, send it out and begin building it up again
|
||||
if(healthSeq.size * 4 >= maxSnapshotSize){
|
||||
Call.buildHealthUpdate(healthSeq);
|
||||
healthSeq.clear();
|
||||
}
|
||||
}
|
||||
|
||||
//send any residual health updates
|
||||
if(healthSeq.size > 0){
|
||||
Call.buildHealthUpdate(healthSeq);
|
||||
}
|
||||
|
||||
buildHealthChanged.clear();
|
||||
}
|
||||
}catch(IOException e){
|
||||
Log.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
public class VoteSession{
|
||||
Player target;
|
||||
ObjectIntMap<String> voted = new ObjectIntMap<>();
|
||||
Timer.Task task;
|
||||
int votes;
|
||||
|
||||
public VoteSession(Player target){
|
||||
this.target = target;
|
||||
this.task = Timer.schedule(() -> {
|
||||
if(!checkPass()){
|
||||
Call.sendMessage(Strings.format("[lightgray]Vote failed. Not enough votes to kick[orange] @[lightgray].", target.name));
|
||||
currentlyKicking = null;
|
||||
task.cancel();
|
||||
}
|
||||
}, voteDuration);
|
||||
}
|
||||
|
||||
void vote(Player player, int d){
|
||||
int lastVote = voted.get(player.uuid(), 0) | voted.get(admins.getInfo(player.uuid()).lastIP, 0);
|
||||
votes -= lastVote;
|
||||
|
||||
votes += d;
|
||||
voted.put(player.uuid(), d);
|
||||
voted.put(admins.getInfo(player.uuid()).lastIP, d);
|
||||
|
||||
Call.sendMessage(Strings.format("[lightgray]@[lightgray] has voted on kicking[orange] @[lightgray].[accent] (@/@)\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
|
||||
player.name, target.name, votes, votesRequired()));
|
||||
|
||||
checkPass();
|
||||
}
|
||||
|
||||
boolean checkPass(){
|
||||
if(votes >= votesRequired()){
|
||||
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] @[orange] will be banned from the server for @ minutes.", target.name, (kickDuration / 60)));
|
||||
Groups.player.each(p -> p.uuid().equals(target.uuid()), p -> p.kick(KickReason.vote, kickDuration * 1000));
|
||||
currentlyKicking = null;
|
||||
task.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface TeamAssigner{
|
||||
Team assign(Player player, Iterable<Player> players);
|
||||
}
|
||||
|
||||
public interface ChatFormatter{
|
||||
/** @return text to be placed before player name */
|
||||
String format(@Nullable Player player, String message);
|
||||
}
|
||||
|
||||
public interface InvalidCommandHandler{
|
||||
String handle(Player player, CommandResponse response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.filedialogs.*;
|
||||
import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
@@ -14,16 +15,36 @@ import mindustry.type.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import rhino.*;
|
||||
|
||||
import java.io.*;
|
||||
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);
|
||||
/** Dynamically creates a class loader for a jar file. This loader must be child-first. */
|
||||
default ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{
|
||||
return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, parent){
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
|
||||
//check for loaded state
|
||||
Class<?> loadedClass = findLoadedClass(name);
|
||||
if(loadedClass == null){
|
||||
try{
|
||||
//try to load own class first
|
||||
loadedClass = findClass(name);
|
||||
}catch(ClassNotFoundException e){
|
||||
//use parent if not found
|
||||
return parent.loadClass(name);
|
||||
}
|
||||
}
|
||||
|
||||
if(resolve){
|
||||
resolveClass(loadedClass);
|
||||
}
|
||||
return loadedClass;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Steam: Update lobby visibility.*/
|
||||
@@ -60,9 +81,10 @@ public interface Platform{
|
||||
}
|
||||
|
||||
default Context getScriptContext(){
|
||||
Context c = Context.enter();
|
||||
c.setOptimizationLevel(9);
|
||||
return c;
|
||||
Context context = Context.getCurrentContext();
|
||||
if(context == null) context = Context.enter();
|
||||
context.setOptimizationLevel(9);
|
||||
return context;
|
||||
}
|
||||
|
||||
/** Update discord RPC. */
|
||||
@@ -101,7 +123,7 @@ public interface Platform{
|
||||
}else{
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
Fi result = Core.files.local(name+ "." + extension);
|
||||
Fi result = Core.files.local(name + "." + extension);
|
||||
writer.write(result);
|
||||
platform.shareFile(result);
|
||||
}catch(Throwable e){
|
||||
@@ -120,6 +142,69 @@ public interface Platform{
|
||||
* @param title The title of the native dialog
|
||||
*/
|
||||
default void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){
|
||||
if(OS.isWindows || OS.isMac){
|
||||
showNativeFileChooser(open, title, cons, extension);
|
||||
}else if(OS.isLinux && !OS.isAndroid){
|
||||
showZenity(open, title, new String[]{extension}, cons, () -> defaultFileDialog(open, title, extension, cons));
|
||||
}else{
|
||||
defaultFileDialog(open, title, extension, cons);
|
||||
}
|
||||
}
|
||||
|
||||
/** attempt to use the native file picker with zenity, or runs the fallback Runnable if the operation fails */
|
||||
static void showZenity(boolean open, String title, String[] extensions, Cons<Fi> cons, Runnable fallback){
|
||||
Threads.daemon(() -> {
|
||||
try{
|
||||
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
|
||||
|
||||
String last = FileChooser.getLastDirectory().absolutePath();
|
||||
if(!last.endsWith("/")) last += "/";
|
||||
|
||||
//zenity doesn't support filtering by extension
|
||||
Seq<String> args = Seq.with("zenity",
|
||||
"--file-selection",
|
||||
"--title=" + formatted,
|
||||
"--filename=" + last,
|
||||
"--confirm-overwrite",
|
||||
"--file-filter=" + Seq.with(extensions).toString(" ", s -> "*." + s),
|
||||
"--file-filter=All files | *" //allow anything if the user wants
|
||||
);
|
||||
|
||||
if(!open){
|
||||
args.add("--save");
|
||||
}
|
||||
|
||||
String result = OS.exec(args.toArray(String.class));
|
||||
//first line.
|
||||
if(result.length() > 1 && result.contains("\n")){
|
||||
result = result.split("\n")[0];
|
||||
}
|
||||
|
||||
//cancelled selection, ignore result
|
||||
if(result.isEmpty() || result.equals("\n")) return;
|
||||
|
||||
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
|
||||
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
|
||||
|
||||
Fi file = Core.files.absolute(result);
|
||||
Core.app.post(() -> {
|
||||
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent());
|
||||
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extensions[0]));
|
||||
}else{
|
||||
cons.get(file);
|
||||
}
|
||||
});
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
Log.warn("zenity not found, using non-native file dialog. Consider installing `zenity` for native file dialogs.");
|
||||
Core.app.post(fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void defaultFileDialog(boolean open, String title, String extension, Cons<Fi> cons){
|
||||
new FileChooser(title, file -> file.extEquals(extension), open, file -> {
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
|
||||
@@ -141,11 +226,81 @@ public interface Platform{
|
||||
default void showMultiFileChooser(Cons<Fi> cons, String... extensions){
|
||||
if(mobile){
|
||||
showFileChooser(true, extensions[0], cons);
|
||||
}else if(OS.isWindows || OS.isMac){
|
||||
showNativeFileChooser(true, "@open", cons, extensions);
|
||||
}else if(OS.isLinux && !OS.isAndroid){
|
||||
showZenity(true, "@open", extensions, cons, () -> defaultMultiFileChooser(cons, extensions));
|
||||
}else{
|
||||
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
|
||||
defaultMultiFileChooser(cons, extensions);
|
||||
}
|
||||
}
|
||||
|
||||
static void defaultMultiFileChooser(Cons<Fi> cons, String... extensions){
|
||||
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
|
||||
}
|
||||
|
||||
default void showNativeFileChooser(boolean open, String title, Cons<Fi> cons, String... shownExtensions){
|
||||
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
|
||||
|
||||
//this should never happen unless someone is being dumb with the parameters
|
||||
String[] ext = shownExtensions == null || shownExtensions.length == 0 ? new String[]{""} : shownExtensions;
|
||||
|
||||
//native file dialog
|
||||
Threads.daemon(() -> {
|
||||
try{
|
||||
FileDialogs.loadNatives();
|
||||
|
||||
String result;
|
||||
String[] patterns = new String[ext.length];
|
||||
for(int i = 0; i < ext.length; i++){
|
||||
patterns[i] = "*." + ext[i];
|
||||
}
|
||||
|
||||
//on MacOS, .msav is not properly recognized until I put garbage into the array?
|
||||
if(patterns.length == 1 && OS.isMac && open){
|
||||
patterns = new String[]{"", "*." + ext[0]};
|
||||
}
|
||||
|
||||
if(open){
|
||||
result = FileDialogs.openFileDialog(formatted, FileChooser.getLastDirectory().absolutePath(), patterns, "." + ext[0] + " files", false);
|
||||
}else{
|
||||
result = FileDialogs.saveFileDialog(formatted, FileChooser.getLastDirectory().child("file." + ext[0]).absolutePath(), patterns, "." + ext[0] + " files");
|
||||
}
|
||||
|
||||
if(result == null) return;
|
||||
|
||||
if(result.length() > 1 && result.contains("\n")){
|
||||
result = result.split("\n")[0];
|
||||
}
|
||||
|
||||
//cancelled selection, ignore result
|
||||
if(result.isEmpty() || result.equals("\n")) return;
|
||||
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
|
||||
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
|
||||
|
||||
Fi file = Core.files.absolute(result);
|
||||
Core.app.post(() -> {
|
||||
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent());
|
||||
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + ext[0]));
|
||||
}else{
|
||||
cons.get(file);
|
||||
}
|
||||
});
|
||||
}catch(Throwable error){
|
||||
Log.err("Failure to execute native file chooser", error);
|
||||
Core.app.post(() -> {
|
||||
if(ext.length > 1){
|
||||
defaultMultiFileChooser(cons, ext);
|
||||
}else{
|
||||
defaultFileDialog(open, title, ext[0], cons);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Hide the app. Android only. */
|
||||
default void hide(){
|
||||
}
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.loaders.TextureLoader.*;
|
||||
import arc.audio.*;
|
||||
import arc.files.*;
|
||||
import arc.fx.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.Texture.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.gl.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.graphics.g3d.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.blocks.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Renderer implements ApplicationListener{
|
||||
/** These are global variables, for headless access. Cached. */
|
||||
public static float laserOpacity = 0.5f, bridgeOpacity = 0.75f;
|
||||
public static float laserOpacity = 0.5f, unitLaserOpacity = 1f, bridgeOpacity = 0.75f;
|
||||
|
||||
public final BlockRenderer blocks = new BlockRenderer();
|
||||
public final FogRenderer fog = new FogRenderer();
|
||||
public final MinimapRenderer minimap = new MinimapRenderer();
|
||||
public final OverlayRenderer overlays = new OverlayRenderer();
|
||||
public final LightRenderer lights = new LightRenderer();
|
||||
@@ -31,28 +38,59 @@ public class Renderer implements ApplicationListener{
|
||||
public PlanetRenderer planets;
|
||||
|
||||
public @Nullable Bloom bloom;
|
||||
public @Nullable FrameBuffer backgroundBuffer;
|
||||
public FrameBuffer effectBuffer = new FrameBuffer();
|
||||
public boolean animateShields, drawWeather = true;
|
||||
public boolean animateShields, drawWeather = true, drawStatus, enableEffects, drawDisplays = true, drawLight = true, pixelate = false;
|
||||
public float weatherAlpha;
|
||||
/** minZoom = zooming out, maxZoom = zooming in */
|
||||
public float minZoom = 1.5f, maxZoom = 6f;
|
||||
public Seq<EnvRenderer> envRenderers = new Seq<>();
|
||||
public ObjectMap<String, Runnable> customBackgrounds = new ObjectMap<>();
|
||||
public TextureRegion[] bubbles = new TextureRegion[16], splashes = new TextureRegion[12];
|
||||
public TextureRegion[][] fluidFrames;
|
||||
|
||||
//TODO unused
|
||||
private FxProcessor fx = new FxProcessor();
|
||||
//currently landing core, null if there are no cores or it has finished landing.
|
||||
private @Nullable LaunchAnimator launchAnimator;
|
||||
private Color clearColor = new Color(0f, 0f, 0f, 1f);
|
||||
private float targetscale = Scl.scl(4);
|
||||
private float camerascale = targetscale;
|
||||
private float landscale = 0f, landTime, weatherAlpha;
|
||||
private float minZoomScl = Scl.scl(0.01f);
|
||||
private float shakeIntensity, shaketime;
|
||||
private float
|
||||
//target camera scale that is lerp-ed to
|
||||
targetscale = Scl.scl(4),
|
||||
//current actual camera scale
|
||||
camerascale = targetscale,
|
||||
//starts at coreLandDuration, ends at 0. if positive, core is landing.
|
||||
landTime,
|
||||
//intensity for screen shake
|
||||
shakeIntensity,
|
||||
//reduction rate of screen shake
|
||||
shakeReduction,
|
||||
//current duration of screen shake
|
||||
shakeTime;
|
||||
//for landTime > 0: if true, core is currently *launching*, otherwise landing.
|
||||
private boolean launching;
|
||||
private Vec2 camShakeOffset = new Vec2();
|
||||
|
||||
public Renderer(){
|
||||
camera = new Camera();
|
||||
Shaders.init();
|
||||
|
||||
Events.on(ResetEvent.class, e -> {
|
||||
shakeTime = shakeIntensity = shakeReduction = 0f;
|
||||
camShakeOffset.setZero();
|
||||
});
|
||||
}
|
||||
|
||||
public void shake(float intensity, float duration){
|
||||
shakeIntensity = Math.max(shakeIntensity, intensity);
|
||||
shaketime = Math.max(shaketime, duration);
|
||||
shakeIntensity = Math.max(shakeIntensity, Mathf.clamp(intensity, 0, 100));
|
||||
shakeTime = Math.max(shakeTime, duration);
|
||||
shakeReduction = shakeIntensity / shakeTime;
|
||||
}
|
||||
|
||||
public void addEnvRenderer(int mask, Runnable render){
|
||||
envRenderers.add(new EnvRenderer(mask, render));
|
||||
}
|
||||
|
||||
public void addCustomBackground(String name, Runnable render){
|
||||
customBackgrounds.put(name, render);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,29 +100,92 @@ public class Renderer implements ApplicationListener{
|
||||
if(settings.getBool("bloom", !ios)){
|
||||
setupBloom();
|
||||
}
|
||||
|
||||
EnvRenderers.init();
|
||||
for(int i = 0; i < bubbles.length; i++) bubbles[i] = atlas.find("bubble-" + i);
|
||||
for(int i = 0; i < splashes.length; i++) splashes[i] = atlas.find("splash-" + i);
|
||||
|
||||
loadFluidFrames();
|
||||
|
||||
Events.on(ClientLoadEvent.class, e -> {
|
||||
loadFluidFrames();
|
||||
});
|
||||
|
||||
assets.load("sprites/clouds.png", Texture.class).loaded = t -> {
|
||||
t.setWrap(TextureWrap.repeat);
|
||||
t.setFilter(TextureFilter.linear);
|
||||
};
|
||||
|
||||
Events.on(WorldLoadEvent.class, e -> {
|
||||
//reset background buffer on every world load, so it can be re-cached first render
|
||||
if(backgroundBuffer != null){
|
||||
backgroundBuffer.dispose();
|
||||
backgroundBuffer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadFluidFrames(){
|
||||
fluidFrames = new TextureRegion[2][Liquid.animationFrames];
|
||||
|
||||
String[] fluidTypes = {"liquid", "gas"};
|
||||
|
||||
for(int i = 0; i < fluidTypes.length; i++){
|
||||
|
||||
for(int j = 0; j < Liquid.animationFrames; j++){
|
||||
fluidFrames[i][j] = atlas.find("fluid-" + fluidTypes[i] + "-" + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TextureRegion[][] getFluidFrames(){
|
||||
if(fluidFrames == null || fluidFrames[0][0].texture.isDisposed()){
|
||||
loadFluidFrames();
|
||||
}
|
||||
return fluidFrames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
Color.white.set(1f, 1f, 1f, 1f);
|
||||
Gl.clear(Gl.stencilBufferBit);
|
||||
|
||||
float dest = Mathf.round(targetscale, 0.5f);
|
||||
float baseTarget = targetscale;
|
||||
|
||||
if(control.input.logicCutscene){
|
||||
baseTarget = Mathf.lerp(minZoom, maxZoom, control.input.logicCutsceneZoom);
|
||||
}
|
||||
|
||||
float dest = Mathf.clamp(Mathf.round(baseTarget, 0.5f), minScale(), maxScale());
|
||||
camerascale = Mathf.lerpDelta(camerascale, dest, 0.1f);
|
||||
if(Mathf.equal(camerascale, dest, 0.001f)) camerascale = dest;
|
||||
unitLaserOpacity = settings.getInt("unitlaseropacity") / 100f;
|
||||
laserOpacity = settings.getInt("lasersopacity") / 100f;
|
||||
bridgeOpacity = settings.getInt("bridgeopacity") / 100f;
|
||||
animateShields = settings.getBool("animatedshields");
|
||||
drawStatus = settings.getBool("blockstatus");
|
||||
enableEffects = settings.getBool("effects");
|
||||
drawDisplays = !settings.getBool("hidedisplays");
|
||||
drawLight = settings.getBool("drawlight", true);
|
||||
pixelate = settings.getBool("pixelate");
|
||||
|
||||
//don't bother drawing landing animation if core is null
|
||||
if(launchAnimator == null) landTime = 0f;
|
||||
if(landTime > 0){
|
||||
landTime -= Time.delta;
|
||||
landscale = Interp.pow5In.apply(minZoomScl, Scl.scl(4f), 1f - landTime / Fx.coreLand.lifetime);
|
||||
camerascale = landscale;
|
||||
if(!state.isPaused()) launchAnimator.updateLaunch();
|
||||
|
||||
weatherAlpha = 0f;
|
||||
camerascale = launchAnimator.zoomLaunch();
|
||||
|
||||
if(!state.isPaused()) landTime -= Time.delta;
|
||||
}else{
|
||||
weatherAlpha = Mathf.lerpDelta(weatherAlpha, 1f, 0.08f);
|
||||
}
|
||||
|
||||
if(launchAnimator != null && landTime <= 0f){
|
||||
launchAnimator.endLaunch();
|
||||
launchAnimator = null;
|
||||
}
|
||||
|
||||
camera.width = graphics.getWidth() / camerascale;
|
||||
camera.height = graphics.getHeight() / camerascale;
|
||||
|
||||
@@ -92,48 +193,49 @@ public class Renderer implements ApplicationListener{
|
||||
landTime = 0f;
|
||||
graphics.clear(Color.black);
|
||||
}else{
|
||||
updateShake(0.75f);
|
||||
if(pixelator.enabled()){
|
||||
minimap.update();
|
||||
|
||||
if(shakeTime > 0){
|
||||
float intensity = shakeIntensity * (settings.getInt("screenshake", 4) / 4f) * 0.75f;
|
||||
camShakeOffset.setToRandomDirection().scl(Mathf.random(intensity));
|
||||
camera.position.add(camShakeOffset);
|
||||
shakeIntensity -= shakeReduction * Time.delta;
|
||||
shakeTime -= Time.delta;
|
||||
shakeIntensity = Mathf.clamp(shakeIntensity, 0f, 100f);
|
||||
}else{
|
||||
camShakeOffset.setZero();
|
||||
shakeIntensity = 0f;
|
||||
}
|
||||
|
||||
if(renderer.pixelate){
|
||||
pixelator.drawPixelate();
|
||||
}else{
|
||||
draw();
|
||||
}
|
||||
|
||||
camera.position.sub(camShakeOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLanding(){
|
||||
public void updateAllDarkness(){
|
||||
blocks.updateDarkness();
|
||||
minimap.updateAll();
|
||||
}
|
||||
|
||||
/** @return whether a launch/land cutscene is playing. */
|
||||
public boolean isCutscene(){
|
||||
return landTime > 0;
|
||||
}
|
||||
|
||||
public float weatherAlpha(){
|
||||
return weatherAlpha;
|
||||
}
|
||||
|
||||
public float landScale(){
|
||||
return landTime > 0 ? landscale : 1f;
|
||||
return landTime > 0 ? camerascale : 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
minimap.dispose();
|
||||
effectBuffer.dispose();
|
||||
blocks.dispose();
|
||||
if(planets != null){
|
||||
planets.dispose();
|
||||
planets = null;
|
||||
}
|
||||
if(bloom != null){
|
||||
bloom.dispose();
|
||||
bloom = null;
|
||||
}
|
||||
Events.fire(new DisposeEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
fx.resize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume(){
|
||||
if(settings.getBool("bloom") && bloom != null){
|
||||
@@ -168,37 +270,9 @@ public class Renderer implements ApplicationListener{
|
||||
}
|
||||
}
|
||||
|
||||
void beginFx(){
|
||||
if(!fx.hasEnabledEffects()) return;
|
||||
|
||||
Draw.flush();
|
||||
fx.clear();
|
||||
fx.begin();
|
||||
}
|
||||
|
||||
void endFx(){
|
||||
if(!fx.hasEnabledEffects()) return;
|
||||
|
||||
Draw.flush();
|
||||
fx.end();
|
||||
fx.applyEffects();
|
||||
fx.render(0, 0, fx.getWidth(), fx.getHeight());
|
||||
}
|
||||
|
||||
void updateShake(float scale){
|
||||
if(shaketime > 0){
|
||||
float intensity = shakeIntensity * (settings.getInt("screenshake", 4) / 4f) * scale;
|
||||
camera.position.add(Mathf.range(intensity), Mathf.range(intensity));
|
||||
shakeIntensity -= 0.25f * Time.delta;
|
||||
shaketime -= Time.delta;
|
||||
shakeIntensity = Mathf.clamp(shakeIntensity, 0f, 100f);
|
||||
}else{
|
||||
shakeIntensity = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void draw(){
|
||||
Events.fire(Trigger.preDraw);
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
camera.update();
|
||||
|
||||
@@ -209,27 +283,29 @@ public class Renderer implements ApplicationListener{
|
||||
graphics.clear(clearColor);
|
||||
Draw.reset();
|
||||
|
||||
if(Core.settings.getBool("animatedwater") || animateShields){
|
||||
if(settings.getBool("animatedwater") || animateShields){
|
||||
effectBuffer.resize(graphics.getWidth(), graphics.getHeight());
|
||||
}
|
||||
|
||||
Draw.proj(camera);
|
||||
|
||||
blocks.checkChanges();
|
||||
blocks.floor.checkChanges();
|
||||
blocks.processBlocks();
|
||||
|
||||
Draw.sort(true);
|
||||
|
||||
Events.fire(Trigger.draw);
|
||||
MapPreviewLoader.checkPreviews();
|
||||
|
||||
if(pixelator.enabled()){
|
||||
if(renderer.pixelate){
|
||||
pixelator.register();
|
||||
}
|
||||
|
||||
Draw.draw(Layer.background, this::drawBackground);
|
||||
Draw.draw(Layer.floor, blocks.floor::drawFloor);
|
||||
Draw.draw(Layer.block - 1, blocks::drawShadows);
|
||||
Draw.draw(Layer.block, () -> {
|
||||
Draw.draw(Layer.block - 0.09f, () -> {
|
||||
blocks.floor.beginDraw();
|
||||
blocks.floor.drawLayer(CacheLayer.walls);
|
||||
blocks.floor.endDraw();
|
||||
@@ -237,7 +313,14 @@ public class Renderer implements ApplicationListener{
|
||||
|
||||
Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader);
|
||||
|
||||
if(state.rules.lighting){
|
||||
//render all matching environments
|
||||
for(var renderer : envRenderers){
|
||||
if((renderer.env & state.rules.env) == renderer.env){
|
||||
renderer.renderer.run();
|
||||
}
|
||||
}
|
||||
|
||||
if(state.rules.lighting && drawLight){
|
||||
Draw.draw(Layer.light, lights::draw);
|
||||
}
|
||||
|
||||
@@ -246,14 +329,19 @@ 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);
|
||||
bloom.resize(graphics.getWidth(), graphics.getHeight());
|
||||
bloom.setBloomIntensity(settings.getInt("bloomintensity", 6) / 4f + 1f);
|
||||
bloom.blurPasses = settings.getInt("bloomblur", 1);
|
||||
Draw.draw(Layer.bullet - 0.02f, bloom::capture);
|
||||
Draw.draw(Layer.effect + 0.02f, bloom::render);
|
||||
}
|
||||
|
||||
control.input.drawCommanded();
|
||||
|
||||
Draw.draw(Layer.plans, overlays::drawBottom);
|
||||
|
||||
if(animateShields && Shaders.shield != null){
|
||||
//TODO would be nice if there were a way to detect if any shields or build beams actually *exist* before beginning/ending buffers, otherwise you're just blitting and swapping shaders for nothing
|
||||
Draw.drawRange(Layer.shields, 1f, () -> effectBuffer.begin(Color.clear), () -> {
|
||||
effectBuffer.end();
|
||||
effectBuffer.blit(Shaders.shield);
|
||||
@@ -265,9 +353,38 @@ public class Renderer implements ApplicationListener{
|
||||
});
|
||||
}
|
||||
|
||||
Draw.draw(Layer.overlayUI, overlays::drawTop);
|
||||
Draw.draw(Layer.space, this::drawLanding);
|
||||
float scaleFactor = 4f / renderer.getDisplayScale();
|
||||
|
||||
//draw objective markers
|
||||
state.rules.objectives.eachRunning(obj -> {
|
||||
for(var marker : obj.markers){
|
||||
if(marker.world){
|
||||
marker.draw(marker.autoscale ? scaleFactor : 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for(var marker : state.markers){
|
||||
if(marker.world){
|
||||
marker.draw(marker.autoscale ? scaleFactor : 1);
|
||||
}
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
|
||||
Draw.draw(Layer.overlayUI, overlays::drawTop);
|
||||
if(state.rules.fog) Draw.draw(Layer.fogOfWar, fog::drawFog);
|
||||
Draw.draw(Layer.space, () -> {
|
||||
if(launchAnimator == null || landTime <= 0f) return;
|
||||
launchAnimator.drawLaunch();
|
||||
});
|
||||
if(launchAnimator != null){
|
||||
Draw.z(Layer.space);
|
||||
launchAnimator.drawLaunchGlobalZ();
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
Events.fire(Trigger.drawOver);
|
||||
blocks.drawBlocks();
|
||||
|
||||
Groups.draw.draw(Drawc::draw);
|
||||
@@ -279,32 +396,79 @@ public class Renderer implements ApplicationListener{
|
||||
Events.fire(Trigger.postDraw);
|
||||
}
|
||||
|
||||
private void drawBackground(){
|
||||
protected void drawBackground(){
|
||||
//draw background only if there is no planet background with a skybox
|
||||
if(state.rules.backgroundTexture != null && (state.rules.planetBackground == null || !state.rules.planetBackground.drawSkybox)){
|
||||
if(!assets.isLoaded(state.rules.backgroundTexture, Texture.class)){
|
||||
var file = assets.getFileHandleResolver().resolve(state.rules.backgroundTexture);
|
||||
|
||||
}
|
||||
//don't draw invalid/non-existent backgrounds.
|
||||
if(!file.exists() || !file.extEquals("png")){
|
||||
return;
|
||||
}
|
||||
|
||||
private void drawLanding(){
|
||||
if(landTime > 0 && player.closestCore() != null){
|
||||
float fract = landTime / Fx.coreLand.lifetime;
|
||||
Building entity = player.closestCore();
|
||||
var desc = assets.load(state.rules.backgroundTexture, Texture.class, new TextureParameter(){{
|
||||
wrapU = wrapV = TextureWrap.mirroredRepeat;
|
||||
magFilter = minFilter = TextureFilter.linear;
|
||||
}});
|
||||
|
||||
TextureRegion reg = entity.block.icon(Cicon.full);
|
||||
float scl = Scl.scl(4f) / camerascale;
|
||||
float s = reg.width * Draw.scl * scl * 4f * fract;
|
||||
assets.finishLoadingAsset(desc);
|
||||
}
|
||||
|
||||
Draw.color(Pal.lightTrail);
|
||||
Draw.rect("circle-shadow", entity.getX(), entity.getY(), s, s);
|
||||
Texture tex = assets.get(state.rules.backgroundTexture, Texture.class);
|
||||
Tmp.tr1.set(tex);
|
||||
Tmp.tr1.u = 0f;
|
||||
Tmp.tr1.v = 0f;
|
||||
|
||||
Angles.randLenVectors(1, (1f- fract), 100, 1000f * scl * (1f-fract), (x, y, fin, fout) -> {
|
||||
Lines.stroke(scl * fin);
|
||||
Lines.lineAngle(entity.getX() + x, entity.getY() + y, Mathf.angle(x, y), (fin * 20 + 1f) * scl);
|
||||
});
|
||||
float ratio = camera.width / camera.height;
|
||||
float size = state.rules.backgroundScl;
|
||||
|
||||
Draw.color();
|
||||
Draw.mixcol(Color.white, fract);
|
||||
Draw.rect(reg, entity.getX(), entity.getY(), reg.width * Draw.scl * scl, reg.height * Draw.scl * scl, fract * 135f);
|
||||
Tmp.tr1.u2 = size;
|
||||
Tmp.tr1.v2 = size / ratio;
|
||||
|
||||
Draw.reset();
|
||||
float sx = 0f, sy = 0f;
|
||||
|
||||
if(!Mathf.zero(state.rules.backgroundSpeed)){
|
||||
sx = (camera.position.x) / state.rules.backgroundSpeed;
|
||||
sy = (camera.position.y) / state.rules.backgroundSpeed;
|
||||
}
|
||||
|
||||
Tmp.tr1.scroll(sx + state.rules.backgroundOffsetX, -sy + state.rules.backgroundOffsetY);
|
||||
|
||||
Draw.rect(Tmp.tr1, camera.position.x, camera.position.y, camera.width, camera.height);
|
||||
}
|
||||
|
||||
if(state.rules.planetBackground != null){
|
||||
int size = Math.max(graphics.getWidth(), graphics.getHeight());
|
||||
|
||||
boolean resized = false;
|
||||
if(backgroundBuffer == null){
|
||||
resized = true;
|
||||
backgroundBuffer = new FrameBuffer(size, size);
|
||||
}
|
||||
|
||||
if(resized || backgroundBuffer.resizeCheck(size, size)){
|
||||
backgroundBuffer.begin(Color.clear);
|
||||
|
||||
var params = state.rules.planetBackground;
|
||||
|
||||
//override some values
|
||||
params.viewW = size;
|
||||
params.viewH = size;
|
||||
params.alwaysDrawAtmosphere = true;
|
||||
params.drawUi = false;
|
||||
|
||||
planets.render(params);
|
||||
|
||||
backgroundBuffer.end();
|
||||
}
|
||||
|
||||
float drawSize = Math.max(camera.width, camera.height);
|
||||
Draw.rect(Draw.wrap(backgroundBuffer.getTexture()), camera.position.x, camera.position.y, drawSize, -drawSize);
|
||||
}
|
||||
|
||||
if(state.rules.customBackgroundCallback != null && customBackgrounds.containsKey(state.rules.customBackgroundCallback)){
|
||||
customBackgrounds.get(state.rules.customBackgroundCallback).run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,16 +502,52 @@ public class Renderer implements ApplicationListener{
|
||||
clampScale();
|
||||
}
|
||||
|
||||
public void zoomIn(float duration){
|
||||
landscale = minZoomScl;
|
||||
landTime = duration;
|
||||
public boolean isLaunching(){
|
||||
return launching;
|
||||
}
|
||||
|
||||
public float getLandTime(){
|
||||
return landTime;
|
||||
}
|
||||
|
||||
public float getLandTimeIn(){
|
||||
if(launchAnimator == null) return 0f;
|
||||
float fin = landTime / launchAnimator.launchDuration();
|
||||
if(!launching) fin = 1f - fin;
|
||||
return fin;
|
||||
}
|
||||
|
||||
public void showLanding(LaunchAnimator landCore){
|
||||
this.launchAnimator = landCore;
|
||||
launching = false;
|
||||
landTime = landCore.launchDuration();
|
||||
|
||||
landCore.beginLaunch(false);
|
||||
camerascale = landCore.zoomLaunch();
|
||||
}
|
||||
|
||||
public void showLaunch(LaunchAnimator landCore){
|
||||
control.input.config.hideConfig();
|
||||
control.input.planConfig.hide();
|
||||
control.input.inv.hide();
|
||||
|
||||
this.launchAnimator = landCore;
|
||||
launching = true;
|
||||
landTime = landCore.launchDuration();
|
||||
|
||||
Music music = landCore.launchMusic();
|
||||
music.stop();
|
||||
music.play();
|
||||
music.setVolume(settings.getInt("musicvol") / 100f);
|
||||
|
||||
landCore.beginLaunch(true);
|
||||
}
|
||||
|
||||
public void takeMapScreenshot(){
|
||||
int w = world.width() * tilesize, h = world.height() * tilesize;
|
||||
int memory = w * h * 4 / 1024 / 1024;
|
||||
|
||||
if(memory >= 65){
|
||||
if(Vars.checkScreenshotMemory && memory >= (mobile ? 65 : 120)){
|
||||
ui.showInfo("@screenshot.invalid");
|
||||
return;
|
||||
}
|
||||
@@ -363,25 +563,39 @@ public class Renderer implements ApplicationListener{
|
||||
camera.position.y = h / 2f + tilesize / 2f;
|
||||
buffer.begin();
|
||||
draw();
|
||||
Draw.flush();
|
||||
byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true);
|
||||
buffer.end();
|
||||
disableUI = false;
|
||||
camera.width = vpW;
|
||||
camera.height = vpH;
|
||||
camera.position.set(px, py);
|
||||
buffer.begin();
|
||||
byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true);
|
||||
for(int i = 0; i < lines.length; i += 4){
|
||||
lines[i + 3] = (byte)255;
|
||||
}
|
||||
buffer.end();
|
||||
Pixmap fullPixmap = new Pixmap(w, h, Pixmap.Format.rgba8888);
|
||||
Buffers.copy(lines, 0, fullPixmap.getPixels(), lines.length);
|
||||
Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png");
|
||||
PixmapIO.writePNG(file, fullPixmap);
|
||||
fullPixmap.dispose();
|
||||
ui.showInfoFade(Core.bundle.format("screenshot", file.toString()));
|
||||
drawWeather = true;
|
||||
|
||||
buffer.dispose();
|
||||
|
||||
Threads.thread(() -> {
|
||||
for(int i = 0; i < lines.length; i += 4){
|
||||
lines[i + 3] = (byte)255;
|
||||
}
|
||||
Pixmap fullPixmap = new Pixmap(w, h);
|
||||
Buffers.copy(lines, 0, fullPixmap.pixels, lines.length);
|
||||
Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png");
|
||||
PixmapIO.writePng(file, fullPixmap);
|
||||
fullPixmap.dispose();
|
||||
app.post(() -> ui.showInfoFade(bundle.format("screenshot", file.toString())));
|
||||
});
|
||||
}
|
||||
|
||||
public static class EnvRenderer{
|
||||
/** Environment bitmask; must match env exactly when and-ed. */
|
||||
public final int env;
|
||||
/** Rendering callback. */
|
||||
public final Runnable renderer;
|
||||
|
||||
public EnvRenderer(int env, Runnable renderer){
|
||||
this.env = env;
|
||||
this.renderer = renderer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+235
-60
@@ -33,12 +33,14 @@ import static arc.scene.actions.Actions.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class UI implements ApplicationListener, Loadable{
|
||||
public static String billions, millions, thousands;
|
||||
|
||||
public static PixmapPacker packer;
|
||||
|
||||
public MenuFragment menufrag;
|
||||
public HudFragment hudfrag;
|
||||
public ChatFragment chatfrag;
|
||||
public ScriptConsoleFragment scriptfrag;
|
||||
public ConsoleFragment consolefrag;
|
||||
public MinimapFragment minimapfrag;
|
||||
public PlayerListFragment listfrag;
|
||||
public LoadingFragment loadfrag;
|
||||
@@ -49,14 +51,14 @@ public class UI implements ApplicationListener, Loadable{
|
||||
public AboutDialog about;
|
||||
public GameOverDialog restart;
|
||||
public CustomGameDialog custom;
|
||||
public MapsDialog maps;
|
||||
public EditorMapsDialog maps;
|
||||
public LoadDialog load;
|
||||
public DiscordDialog discord;
|
||||
public JoinDialog join;
|
||||
public HostDialog host;
|
||||
public PausedDialog paused;
|
||||
public SettingsMenuDialog settings;
|
||||
public ControlsDialog controls;
|
||||
public KeybindDialog controls;
|
||||
public MapEditorDialog editor;
|
||||
public LanguageDialog language;
|
||||
public BansDialog bans;
|
||||
@@ -69,14 +71,29 @@ public class UI implements ApplicationListener, Loadable{
|
||||
public SchematicsDialog schematics;
|
||||
public ModsDialog mods;
|
||||
public ColorPicker picker;
|
||||
public EffectsDialog effects;
|
||||
public LogicDialog logic;
|
||||
public FullTextDialog fullText;
|
||||
public CampaignCompleteDialog campaignComplete;
|
||||
|
||||
public Cursor drillCursor, unloadCursor;
|
||||
public IntMap<Dialog> followUpMenus;
|
||||
|
||||
public Cursor drillCursor, unloadCursor, targetCursor, repairCursor;
|
||||
|
||||
private @Nullable Element lastAnnouncement;
|
||||
|
||||
public UI(){
|
||||
Fonts.loadFonts();
|
||||
}
|
||||
|
||||
public static void loadColors(){
|
||||
Colors.put("accent", Pal.accent);
|
||||
Colors.put("unlaunched", Color.valueOf("8982ed"));
|
||||
Colors.put("highlight", Pal.accent.cpy().lerp(Color.white, 0.3f));
|
||||
Colors.put("stat", Pal.stat);
|
||||
Colors.put("negstat", Pal.negativeStat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
|
||||
@@ -108,7 +125,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
Dialog.setHideAction(() -> sequence(fadeOut(0.1f)));
|
||||
|
||||
Tooltips.getInstance().animations = false;
|
||||
Tooltips.getInstance().textProvider = text -> new Tooltip(t -> t.background(Styles.black5).margin(4f).add(text));
|
||||
Tooltips.getInstance().textProvider = text -> new Tooltip(t -> t.background(Styles.black6).margin(4f).add(text));
|
||||
if(mobile){
|
||||
Tooltips.getInstance().offsetY += Scl.scl(60f);
|
||||
}
|
||||
|
||||
Core.settings.setErrorHandler(e -> {
|
||||
Log.err(e);
|
||||
@@ -117,18 +137,15 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
ClickListener.clicked = () -> Sounds.press.play();
|
||||
|
||||
Colors.put("accent", Pal.accent);
|
||||
Colors.put("unlaunched", Color.valueOf("8982ed"));
|
||||
Colors.put("highlight", Pal.accent.cpy().lerp(Color.white, 0.3f));
|
||||
Colors.put("stat", Pal.stat);
|
||||
|
||||
drillCursor = Core.graphics.newCursor("drill", Fonts.cursorScale());
|
||||
unloadCursor = Core.graphics.newCursor("unload", Fonts.cursorScale());
|
||||
targetCursor = Core.graphics.newCursor("target", Fonts.cursorScale());
|
||||
repairCursor = Core.graphics.newCursor("repair", Fonts.cursorScale());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Seq<AssetDescriptor> getDependencies(){
|
||||
return Seq.with(new AssetDescriptor<>(Control.class), new AssetDescriptor<>("outline", Font.class), new AssetDescriptor<>("default", Font.class), new AssetDescriptor<>("chat", Font.class));
|
||||
return Seq.with(new AssetDescriptor<>(Control.class), new AssetDescriptor<>("outline", Font.class), new AssetDescriptor<>("default", Font.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,8 +157,8 @@ public class UI implements ApplicationListener, Loadable{
|
||||
Core.scene.act();
|
||||
Core.scene.draw();
|
||||
|
||||
if(Core.input.keyTap(KeyCode.mouseLeft) && Core.scene.getKeyboardFocus() instanceof TextField){
|
||||
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
|
||||
if(Core.input.keyTap(KeyCode.mouseLeft) && Core.scene.hasField()){
|
||||
Element e = Core.scene.getHoverElement();
|
||||
if(!(e instanceof TextField)){
|
||||
Core.scene.setKeyboardFocus(null);
|
||||
}
|
||||
@@ -152,6 +169,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
billions = Core.bundle.get("unit.billions");
|
||||
millions = Core.bundle.get("unit.millions");
|
||||
thousands = Core.bundle.get("unit.thousands");
|
||||
|
||||
menuGroup = new WidgetGroup();
|
||||
hudGroup = new WidgetGroup();
|
||||
|
||||
@@ -162,11 +183,12 @@ public class UI implements ApplicationListener, Loadable{
|
||||
minimapfrag = new MinimapFragment();
|
||||
listfrag = new PlayerListFragment();
|
||||
loadfrag = new LoadingFragment();
|
||||
scriptfrag = new ScriptConsoleFragment();
|
||||
consolefrag = new ConsoleFragment();
|
||||
|
||||
picker = new ColorPicker();
|
||||
effects = new EffectsDialog();
|
||||
editor = new MapEditorDialog();
|
||||
controls = new ControlsDialog();
|
||||
controls = new KeybindDialog();
|
||||
restart = new GameOverDialog();
|
||||
join = new JoinDialog();
|
||||
discord = new DiscordDialog();
|
||||
@@ -181,13 +203,16 @@ public class UI implements ApplicationListener, Loadable{
|
||||
bans = new BansDialog();
|
||||
admins = new AdminsDialog();
|
||||
traces = new TraceDialog();
|
||||
maps = new MapsDialog();
|
||||
maps = new EditorMapsDialog();
|
||||
content = new ContentInfoDialog();
|
||||
planet = new PlanetDialog();
|
||||
research = new ResearchDialog();
|
||||
mods = new ModsDialog();
|
||||
schematics = new SchematicsDialog();
|
||||
logic = new LogicDialog();
|
||||
fullText = new FullTextDialog();
|
||||
campaignComplete = new CampaignCompleteDialog();
|
||||
followUpMenus = new IntMap<>();
|
||||
|
||||
Group group = Core.scene.root;
|
||||
|
||||
@@ -203,10 +228,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
|
||||
hudfrag.build(hudGroup);
|
||||
menufrag.build(menuGroup);
|
||||
chatfrag.container().build(hudGroup);
|
||||
chatfrag.build(hudGroup);
|
||||
minimapfrag.build(hudGroup);
|
||||
listfrag.build(hudGroup);
|
||||
scriptfrag.container().build(hudGroup);
|
||||
consolefrag.build(hudGroup);
|
||||
loadfrag.build(group);
|
||||
new FadeInFragment().build(group);
|
||||
}
|
||||
@@ -214,18 +239,17 @@ public class UI implements ApplicationListener, Loadable{
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
if(Core.scene == null) return;
|
||||
|
||||
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];
|
||||
|
||||
Core.scene.resize(width, height);
|
||||
Events.fire(new ResizeEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
if(packer != null){
|
||||
packer.dispose();
|
||||
packer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public TextureRegionDrawable getIcon(String name){
|
||||
if(Icon.icons.containsKey(name)) return Icon.icons.get(name);
|
||||
return Core.atlas.getDrawable("error");
|
||||
@@ -248,37 +272,53 @@ public class UI implements ApplicationListener, Loadable{
|
||||
});
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String dtext, int textLength, String def, boolean inumeric, Cons<String> confirmed){
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, boolean numbers, Cons<String> confirmed, Runnable closed) {
|
||||
showTextInput(titleText, text, textLength, def, numbers, false, confirmed, closed);
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, boolean numbers, boolean allowEmpty, Cons<String> confirmed, Runnable closed){
|
||||
if(mobile){
|
||||
var description = (text.startsWith("@") ? Core.bundle.get(text.substring(1)) : text);
|
||||
var empty = allowEmpty;
|
||||
Core.input.getTextInput(new TextInput(){{
|
||||
this.title = (titleText.startsWith("@") ? Core.bundle.get(titleText.substring(1)) : titleText);
|
||||
this.text = def;
|
||||
this.numeric = inumeric;
|
||||
this.numeric = numbers;
|
||||
this.maxLength = textLength;
|
||||
this.accepted = confirmed;
|
||||
this.canceled = closed;
|
||||
this.allowEmpty = empty;
|
||||
this.message = description;
|
||||
}});
|
||||
}else{
|
||||
new Dialog(titleText){{
|
||||
cont.margin(30).add(dtext).padRight(6f);
|
||||
TextFieldFilter filter = inumeric ? TextFieldFilter.digitsOnly : (f, c) -> true;
|
||||
cont.margin(30).add(text).padRight(6f);
|
||||
TextFieldFilter filter = numbers ? TextFieldFilter.digitsOnly : (f, c) -> true;
|
||||
TextField field = cont.field(def, t -> {}).size(330f, 50f).get();
|
||||
field.setFilter((f, c) -> field.getText().length() < textLength && filter.acceptChar(f, c));
|
||||
field.setMaxLength(textLength);
|
||||
field.setFilter(filter);
|
||||
buttons.defaults().size(120, 54).pad(4);
|
||||
buttons.button("@cancel", this::hide);
|
||||
buttons.button("@cancel", () -> {
|
||||
closed.run();
|
||||
hide();
|
||||
});
|
||||
buttons.button("@ok", () -> {
|
||||
confirmed.get(field.getText());
|
||||
hide();
|
||||
}).disabled(b -> field.getText().isEmpty());
|
||||
}).disabled(b -> !allowEmpty && field.getText().isEmpty());
|
||||
|
||||
keyDown(KeyCode.enter, () -> {
|
||||
String text = field.getText();
|
||||
if(!text.isEmpty()){
|
||||
if(allowEmpty || !text.isEmpty()){
|
||||
confirmed.get(text);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
keyDown(KeyCode.escape, this::hide);
|
||||
keyDown(KeyCode.back, this::hide);
|
||||
|
||||
closeOnBack(closed);
|
||||
show();
|
||||
|
||||
Core.scene.setKeyboardFocus(field);
|
||||
field.setCursorPosition(def.length());
|
||||
}};
|
||||
@@ -289,30 +329,61 @@ public class UI implements ApplicationListener, Loadable{
|
||||
showTextInput(title, text, 32, def, confirmed);
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String text, int textLength, String def, Cons<String> confirmed){
|
||||
showTextInput(titleText, text, textLength, def, false, confirmed);
|
||||
public void showTextInput(String title, String text, int textLength, String def, Cons<String> confirmed){
|
||||
showTextInput(title, text, textLength, def, false, confirmed);
|
||||
}
|
||||
|
||||
public void showTextInput(String title, String text, int textLength, String def, boolean numeric, Cons<String> confirmed){
|
||||
showTextInput(title, text, textLength, def, numeric, confirmed, () -> {});
|
||||
}
|
||||
|
||||
public void showInfoFade(String info){
|
||||
showInfoFade(info, 7f);
|
||||
}
|
||||
|
||||
public void showInfoFade(String info, float duration){
|
||||
var cinfo = Core.scene.find("coreinfo");
|
||||
Table table = new Table();
|
||||
table.touchable = Touchable.disabled;
|
||||
table.setFillParent(true);
|
||||
table.actions(Actions.fadeOut(7f, Interp.fade), Actions.remove());
|
||||
if(cinfo.visible && !state.isMenu()) table.marginTop(cinfo.getPrefHeight() / Scl.scl() / 2);
|
||||
table.actions(Actions.fadeOut(duration, Interp.fade), Actions.remove());
|
||||
table.top().add(info).style(Styles.outlineLabel).padTop(10);
|
||||
Core.scene.add(table);
|
||||
}
|
||||
|
||||
public void addDescTooltip(Element elem, String description){
|
||||
if(description == null) return;
|
||||
|
||||
elem.addListener(new Tooltip(t -> t.background(Styles.black8).margin(4f).add(description).color(Color.lightGray)){
|
||||
{
|
||||
allowMobile = true;
|
||||
}
|
||||
@Override
|
||||
protected void setContainerPosition(Element element, float x, float y){
|
||||
this.targetActor = element;
|
||||
Vec2 pos = element.localToStageCoordinates(Tmp.v1.set(0, 0));
|
||||
container.pack();
|
||||
container.setPosition(pos.x, pos.y, Align.topLeft);
|
||||
container.setOrigin(0, element.getHeight());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Shows a fading label at the top of the screen. */
|
||||
public void showInfoToast(String info, float duration){
|
||||
var cinfo = Core.scene.find("coreinfo");
|
||||
Table table = new Table();
|
||||
table.setFillParent(true);
|
||||
table.touchable = Touchable.disabled;
|
||||
table.setFillParent(true);
|
||||
if(cinfo.visible && !state.isMenu()) table.marginTop(cinfo.getPrefHeight() / Scl.scl() / 2);
|
||||
table.update(() -> {
|
||||
if(state.isMenu()) table.remove();
|
||||
});
|
||||
table.actions(Actions.delay(duration * 0.9f), Actions.fadeOut(duration * 0.1f, Interp.fade), Actions.remove());
|
||||
table.top().table(Styles.black3, t -> t.margin(4).add(info).style(Styles.outlineLabel)).padTop(10);
|
||||
Core.scene.add(table);
|
||||
lastAnnouncement = table;
|
||||
}
|
||||
|
||||
/** Shows a label at some position on the screen. Does not fade. */
|
||||
@@ -348,17 +419,21 @@ public class UI implements ApplicationListener, Loadable{
|
||||
}
|
||||
|
||||
public void showInfo(String info){
|
||||
showInfo(info, () -> {});
|
||||
}
|
||||
|
||||
public void showInfo(String info, Runnable listener){
|
||||
new Dialog(""){{
|
||||
getCell(cont).growX();
|
||||
cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center);
|
||||
buttons.button("@ok", () -> {
|
||||
hide();
|
||||
listener.run();
|
||||
}).size(110, 50).pad(4);
|
||||
buttons.button("@ok", this::hide).size(110, 50).pad(4);
|
||||
keyDown(KeyCode.enter, this::hide);
|
||||
closeOnBack();
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showInfoOnHidden(String info, Runnable listener){
|
||||
new Dialog(""){{
|
||||
getCell(cont).growX();
|
||||
cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center);
|
||||
buttons.button("@ok", this::hide).size(110, 50).pad(4);
|
||||
hidden(listener);
|
||||
closeOnBack();
|
||||
}}.show();
|
||||
}
|
||||
@@ -392,6 +467,8 @@ public class UI implements ApplicationListener, Loadable{
|
||||
}
|
||||
|
||||
public void showException(String text, Throwable exc){
|
||||
if(loadfrag == null) return;
|
||||
|
||||
loadfrag.hide();
|
||||
new Dialog(""){{
|
||||
String message = Strings.getFinalMessage(exc);
|
||||
@@ -449,6 +526,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showConfirm(String text, Runnable confirmed){
|
||||
showConfirm("@confirm", text, null, confirmed);
|
||||
}
|
||||
|
||||
public void showConfirm(String title, String text, Runnable confirmed){
|
||||
showConfirm(title, text, null, confirmed);
|
||||
}
|
||||
@@ -458,8 +539,8 @@ public class UI implements ApplicationListener, Loadable{
|
||||
dialog.cont.add(text).width(mobile ? 400f : 500f).wrap().pad(4f).get().setAlignment(Align.center, Align.center);
|
||||
dialog.buttons.defaults().size(200f, 54f).pad(2f);
|
||||
dialog.setFillParent(false);
|
||||
dialog.buttons.button("@cancel", dialog::hide);
|
||||
dialog.buttons.button("@ok", () -> {
|
||||
dialog.buttons.button("@cancel", Icon.cancel, dialog::hide);
|
||||
dialog.buttons.button("@ok", Icon.ok, () -> {
|
||||
dialog.hide();
|
||||
confirmed.run();
|
||||
});
|
||||
@@ -497,6 +578,10 @@ public class UI implements ApplicationListener, Loadable{
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public boolean hasAnnouncement(){
|
||||
return lastAnnouncement != null && lastAnnouncement.parent != null;
|
||||
}
|
||||
|
||||
/** Display text in the middle of the screen, then fade out. */
|
||||
public void announce(String text){
|
||||
announce(text, 3);
|
||||
@@ -512,6 +597,7 @@ public class UI implements ApplicationListener, Loadable{
|
||||
t.pack();
|
||||
t.act(0.1f);
|
||||
Core.scene.add(t);
|
||||
lastAnnouncement = t;
|
||||
}
|
||||
|
||||
public void showOkText(String title, String text, Runnable confirmed){
|
||||
@@ -526,17 +612,106 @@ public class UI implements ApplicationListener, Loadable{
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
//TODO move?
|
||||
// TODO REPLACE INTEGER WITH arc.fun.IntCons(int, T) or something like that.
|
||||
public Dialog newMenuDialog(String title, String message, String[][] options, Cons2<Integer, Dialog> buttonListener){
|
||||
return new Dialog(title){{
|
||||
setFillParent(true);
|
||||
removeChild(titleTable);
|
||||
cont.add(titleTable).width(400f);
|
||||
|
||||
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){
|
||||
return Strings.fixed(number / 1_000_000f, 1) + "[gray]" + Core.bundle.get("unit.millions") + "[]";
|
||||
}else if(number >= 10_000){
|
||||
return number / 1000 + "[gray]" + Core.bundle.get("unit.thousands") + "[]";
|
||||
}else if(number >= 1000){
|
||||
return Strings.fixed(number / 1000f, 1) + "[gray]" + Core.bundle.get("unit.thousands") + "[]";
|
||||
cont.row();
|
||||
cont.image().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent).bottom();
|
||||
cont.row();
|
||||
cont.pane(table -> {
|
||||
table.add(message).width(400f).wrap().get().setAlignment(Align.center);
|
||||
table.row();
|
||||
|
||||
int option = 0;
|
||||
for(var optionsRow : options){
|
||||
if(optionsRow.length == 0) continue;
|
||||
Table buttonRow = table.row().table().get().row();
|
||||
int fullWidth = 400 - (optionsRow.length - 1) * 8; // adjust to count padding as well
|
||||
int width = fullWidth / optionsRow.length;
|
||||
int lastWidth = fullWidth - width * (optionsRow.length - 1); // take the rest of space for uneven table
|
||||
|
||||
for(int i = 0; i < optionsRow.length; i++){
|
||||
if(optionsRow[i] == null) continue;
|
||||
|
||||
String optionName = optionsRow[i];
|
||||
int finalOption = option;
|
||||
buttonRow.button(optionName, () -> buttonListener.get(finalOption, this))
|
||||
.size(i == optionsRow.length - 1 ? lastWidth : width, 50).pad(4);
|
||||
option++;
|
||||
}
|
||||
}
|
||||
}).growX();
|
||||
}};
|
||||
}
|
||||
|
||||
/** Shows a menu that fires a callback when an option is selected. If nothing is selected, -1 is returned. */
|
||||
public void showMenu(String title, String message, String[][] options, Intc callback){
|
||||
Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> {
|
||||
callback.get(option);
|
||||
myself.hide();
|
||||
});
|
||||
dialog.closeOnBack(() -> callback.get(-1));
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/** Shows a menu that hides when another followUp-menu is shown or when nothing is selected.
|
||||
* @see UI#showMenu(String, String, String[][], Intc) */
|
||||
public void showFollowUpMenu(int menuId, String title, String message, String[][] options, Intc callback) {
|
||||
Dialog dialog = newMenuDialog(title, message, options, (option, myself) -> callback.get(option));
|
||||
dialog.closeOnBack(() -> {
|
||||
followUpMenus.remove(menuId);
|
||||
callback.get(-1);
|
||||
});
|
||||
|
||||
Dialog oldDialog = followUpMenus.remove(menuId);
|
||||
if(oldDialog != null){
|
||||
dialog.show(Core.scene, null);
|
||||
oldDialog.hide(null);
|
||||
}else{
|
||||
dialog.show();
|
||||
}
|
||||
followUpMenus.put(menuId, dialog);
|
||||
}
|
||||
|
||||
public void hideFollowUpMenu(int menuId) {
|
||||
if(!followUpMenus.containsKey(menuId)) return;
|
||||
followUpMenus.remove(menuId).hide();
|
||||
}
|
||||
|
||||
/** Formats time with hours:minutes:seconds. */
|
||||
public static String formatTime(float ticks){
|
||||
int seconds = (int)(ticks / 60);
|
||||
if(seconds < 60) return "0:" + (seconds < 10 ? "0" : "") + seconds;
|
||||
|
||||
int minutes = seconds / 60;
|
||||
int modSec = seconds % 60;
|
||||
if(minutes < 60) return minutes + ":" + (modSec < 10 ? "0" : "") + modSec;
|
||||
|
||||
int hours = minutes / 60;
|
||||
int modMinute = minutes % 60;
|
||||
|
||||
return hours + ":" + (modMinute < 10 ? "0" : "") + modMinute + ":" + (modSec < 10 ? "0" : "") + modSec;
|
||||
}
|
||||
|
||||
public static String formatAmount(long number){
|
||||
//prevent things like bars displaying erroneous representations of casted infinities
|
||||
if(number == Long.MAX_VALUE) return "∞";
|
||||
if(number == Long.MIN_VALUE) return "-∞";
|
||||
|
||||
long mag = Math.abs(number);
|
||||
String sign = number < 0 ? "-" : "";
|
||||
if(mag >= 1_000_000_000){
|
||||
return sign + Strings.fixed(mag / 1_000_000_000f, 1) + "[gray]" + billions + "[]";
|
||||
}else if(mag >= 1_000_000){
|
||||
return sign + Strings.fixed(mag / 1_000_000f, 1) + "[gray]" + millions + "[]";
|
||||
}else if(mag >= 10_000){
|
||||
return number / 1000 + "[gray]" + thousands + "[]";
|
||||
}else if(mag >= 1000){
|
||||
return sign + Strings.fixed(mag / 1000f, 1) + "[gray]" + thousands + "[]";
|
||||
}else{
|
||||
return number + "";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public class Version{
|
||||
public static String type = "unknown";
|
||||
/** Build modifier, e.g. 'alpha' or 'release' */
|
||||
public static String modifier = "unknown";
|
||||
/** Git commit hash (short) */
|
||||
public static String commitHash = "unknown";
|
||||
/** Number specifying the major version, e.g. '4' */
|
||||
public static int number;
|
||||
/** Build number, e.g. '43'. set to '-1' for custom builds. */
|
||||
@@ -32,6 +34,7 @@ public class Version{
|
||||
type = map.get("type");
|
||||
number = Integer.parseInt(map.get("number", "4"));
|
||||
modifier = map.get("modifier");
|
||||
commitHash = map.get("commitHash");
|
||||
if(map.get("build").contains(".")){
|
||||
String[] split = map.get("build").split("\\.");
|
||||
try{
|
||||
@@ -46,8 +49,13 @@ public class Version{
|
||||
}
|
||||
}
|
||||
|
||||
/** @return whether the version is greater than the specified version string, e.g. "120.1"*/
|
||||
/** @return whether the current game version is greater than the specified version string, e.g. "120.1"*/
|
||||
public static boolean isAtLeast(String str){
|
||||
return isAtLeast(build, revision, str);
|
||||
}
|
||||
|
||||
/** @return whether the version numbers are greater than the specified version string, e.g. "120.1"*/
|
||||
public static boolean isAtLeast(int build, int revision, String str){
|
||||
if(build <= 0 || str == null || str.isEmpty()) return true;
|
||||
|
||||
int dot = str.indexOf('.');
|
||||
@@ -68,6 +76,6 @@ public class Version{
|
||||
if(build == -1){
|
||||
return "custom build";
|
||||
}
|
||||
return (type.equals("official") ? modifier : type) + " build " + build + (revision == 0 ? "" : "." + revision);
|
||||
return (type.equals("official") ? modifier : type) + " build " + build + (revision == 0 ? "" : "." + revision) + (commitHash.equals("unknown") ? "" : " (" + commitHash + ")");
|
||||
}
|
||||
}
|
||||
|
||||
+165
-151
@@ -4,10 +4,11 @@ import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.math.geom.Geometry.*;
|
||||
import arc.struct.*;
|
||||
import arc.struct.ObjectIntMap.*;
|
||||
import arc.util.*;
|
||||
import arc.util.noise.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.ctype.*;
|
||||
@@ -20,7 +21,6 @@ import mindustry.maps.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.maps.filters.GenerateFilter.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.type.Weather.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
import mindustry.world.blocks.legacy.*;
|
||||
@@ -31,12 +31,20 @@ public class World{
|
||||
public final Context context = new Context();
|
||||
|
||||
public Tiles tiles = new Tiles(0, 0);
|
||||
/** The number of times tiles have changed in this session. Used for blocks that need to poll world state, but not frequently. */
|
||||
public int tileChanges = -1;
|
||||
|
||||
private boolean generating, invalidMap;
|
||||
private ObjectMap<Map, Runnable> customMapLoaders = new ObjectMap<>();
|
||||
|
||||
public World(){
|
||||
Events.on(TileChangeEvent.class, e -> {
|
||||
tileChanges ++;
|
||||
});
|
||||
|
||||
Events.on(WorldLoadEvent.class, e -> {
|
||||
tileChanges = -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds a custom handler function for loading a custom map - usually a generated one. */
|
||||
@@ -162,7 +170,11 @@ public class World{
|
||||
return Math.round(coord / tilesize);
|
||||
}
|
||||
|
||||
private void clearTileEntities(){
|
||||
public int packArray(int x, int y){
|
||||
return x + y * tiles.width;
|
||||
}
|
||||
|
||||
public void clearBuildings(){
|
||||
for(Tile tile : tiles){
|
||||
if(tile != null && tile.build != null){
|
||||
tile.build.remove();
|
||||
@@ -175,7 +187,7 @@ public class World{
|
||||
* Only use for loading saves!
|
||||
*/
|
||||
public Tiles resize(int width, int height){
|
||||
clearTileEntities();
|
||||
clearBuildings();
|
||||
|
||||
if(tiles.width != width || tiles.height != height){
|
||||
tiles = new Tiles(width, height);
|
||||
@@ -190,6 +202,7 @@ public class World{
|
||||
*/
|
||||
public void beginMapLoad(){
|
||||
generating = true;
|
||||
Events.fire(new WorldLoadBeginEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +210,7 @@ public class World{
|
||||
* A WorldLoadEvent will be fire.
|
||||
*/
|
||||
public void endMapLoad(){
|
||||
Events.fire(new WorldLoadEndEvent());
|
||||
|
||||
for(Tile tile : tiles){
|
||||
//remove legacy blocks; they need to stop existing
|
||||
@@ -219,7 +233,7 @@ public class World{
|
||||
}
|
||||
|
||||
public Rect getQuadBounds(Rect in){
|
||||
return in.set(-finalWorldBounds, -finalWorldBounds, world.width() * tilesize + finalWorldBounds * 2, world.height() * tilesize + finalWorldBounds * 2);
|
||||
return in.set(-finalWorldBounds, -finalWorldBounds, width() * tilesize + finalWorldBounds * 2, height() * tilesize + finalWorldBounds * 2);
|
||||
}
|
||||
|
||||
public void setGenerating(boolean gen){
|
||||
@@ -240,98 +254,80 @@ public class World{
|
||||
}
|
||||
|
||||
public void loadSector(Sector sector){
|
||||
setSectorRules(sector);
|
||||
loadSector(sector, 0, true);
|
||||
}
|
||||
|
||||
public void loadSector(Sector sector, int seedOffset, boolean saveInfo){
|
||||
setSectorRules(sector, saveInfo);
|
||||
|
||||
int size = sector.getSize();
|
||||
loadGenerator(size, size, tiles -> {
|
||||
if(sector.preset != null){
|
||||
sector.preset.generator.generate(tiles);
|
||||
sector.preset.rules.get(state.rules); //apply extra rules
|
||||
}else if(sector.planet.generator != null){
|
||||
sector.planet.generator.generate(tiles, sector, seedOffset);
|
||||
}else{
|
||||
sector.planet.generator.generate(tiles, sector);
|
||||
throw new RuntimeException("Sector " + sector.id + " on planet " + sector.planet.name + " has no generator or preset defined. Provide a planet generator or preset map.");
|
||||
}
|
||||
//just in case
|
||||
state.rules.sector = sector;
|
||||
});
|
||||
|
||||
if(saveInfo && state.rules.waves){
|
||||
sector.info.waves = state.rules.waves;
|
||||
}
|
||||
|
||||
//postgenerate for bases
|
||||
if(sector.preset == null){
|
||||
if(sector.preset == null && sector.planet.generator != null){
|
||||
sector.planet.generator.postGenerate(tiles);
|
||||
}
|
||||
|
||||
//reset rules
|
||||
setSectorRules(sector);
|
||||
setSectorRules(sector, saveInfo);
|
||||
|
||||
if(state.rules.defaultTeam.core() != null){
|
||||
sector.info.spawnPosition = state.rules.defaultTeam.core().pos();
|
||||
}
|
||||
}
|
||||
|
||||
private void setSectorRules(Sector sector){
|
||||
private void setSectorRules(Sector sector, boolean saveInfo){
|
||||
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();
|
||||
|
||||
//apply weather based on terrain
|
||||
ObjectIntMap<Block> floorc = new ObjectIntMap<>();
|
||||
sector.planet.generator.addWeather(sector, state.rules);
|
||||
|
||||
ObjectSet<UnlockableContent> content = new ObjectSet<>();
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
if(world.getDarkness(tile.x, tile.y) >= 3){
|
||||
//resources can be outside area
|
||||
boolean border = state.rules.limitMapArea;
|
||||
state.rules.limitMapArea = false;
|
||||
|
||||
//TODO duplicate code?
|
||||
for(Tile tile : tiles){
|
||||
if(getDarkness(tile.x, tile.y) >= 3){
|
||||
continue;
|
||||
}
|
||||
|
||||
Liquid liquid = tile.floor().liquidDrop;
|
||||
if(tile.floor().itemDrop != null) content.add(tile.floor().itemDrop);
|
||||
if(tile.overlay().itemDrop != null) content.add(tile.overlay().itemDrop);
|
||||
if(tile.floor().itemDrop != null && tile.block() == Blocks.air) content.add(tile.floor().itemDrop);
|
||||
if(tile.overlay().itemDrop != null && tile.block() == Blocks.air) content.add(tile.overlay().itemDrop);
|
||||
if(tile.wallDrop() != null) content.add(tile.wallDrop());
|
||||
if(liquid != null) content.add(liquid);
|
||||
|
||||
if(!tile.block().isStatic()){
|
||||
floorc.increment(tile.floor());
|
||||
if(tile.overlay() != Blocks.air){
|
||||
floorc.increment(tile.overlay());
|
||||
}
|
||||
}
|
||||
}
|
||||
state.rules.limitMapArea = border;
|
||||
|
||||
//sort counts in descending order
|
||||
Seq<Entry<Block>> entries = floorc.entries().toArray();
|
||||
entries.sort(e -> -e.value);
|
||||
//remove all blocks occuring < 30 times - unimportant
|
||||
entries.removeAll(e -> e.value < 30);
|
||||
|
||||
Block[] floors = new Block[entries.size];
|
||||
for(int i = 0; i < entries.size; i++){
|
||||
floors[i] = entries.get(i).key;
|
||||
}
|
||||
|
||||
//TODO bad code
|
||||
boolean hasSnow = floors[0].name.contains("ice") || floors[0].name.contains("snow");
|
||||
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){
|
||||
state.rules.weather.add(new WeatherEntry(Weathers.snow));
|
||||
}
|
||||
|
||||
if(hasRain){
|
||||
state.rules.weather.add(new WeatherEntry(Weathers.rain));
|
||||
state.rules.weather.add(new WeatherEntry(Weathers.fog));
|
||||
}
|
||||
|
||||
if(hasDesert){
|
||||
state.rules.weather.add(new WeatherEntry(Weathers.sandstorm));
|
||||
}
|
||||
|
||||
if(hasSpores){
|
||||
state.rules.weather.add(new WeatherEntry(Weathers.sporestorm));
|
||||
}
|
||||
|
||||
sector.info.resources = content.asArray();
|
||||
state.rules.cloudColor = sector.planet.landCloudColor;
|
||||
state.rules.env = sector.planet.defaultEnv;
|
||||
state.rules.planet = sector.planet;
|
||||
sector.planet.applyRules(state.rules);
|
||||
sector.info.resources = content.toSeq();
|
||||
sector.info.resources.sort(Structs.comps(Structs.comparing(Content::getContentType), Structs.comparingInt(c -> c.id)));
|
||||
sector.saveInfo();
|
||||
|
||||
if(saveInfo){
|
||||
sector.saveInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public Context filterContext(Map map){
|
||||
@@ -367,18 +363,18 @@ public class World{
|
||||
invalidMap = false;
|
||||
|
||||
if(!headless){
|
||||
if(state.teams.playerCores().size == 0 && !checkRules.pvp){
|
||||
ui.showErrorMessage("@map.nospawn");
|
||||
if(state.teams.cores(checkRules.defaultTeam).size == 0 && !checkRules.pvp){
|
||||
invalidMap = true;
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn", checkRules.defaultTeam.coloredName()));
|
||||
}else if(checkRules.pvp){ //pvp maps need two cores to be valid
|
||||
if(state.teams.getActive().count(TeamData::hasCore) < 2){
|
||||
invalidMap = true;
|
||||
ui.showErrorMessage("@map.nospawn.pvp");
|
||||
}
|
||||
}else if(checkRules.attackMode){ //attack maps need two cores to be valid
|
||||
invalidMap = state.teams.get(state.rules.waveTeam).noCores();
|
||||
invalidMap = state.rules.waveTeam.data().noCores();
|
||||
if(invalidMap){
|
||||
ui.showErrorMessage("@map.nospawn.attack");
|
||||
ui.showErrorMessage(Core.bundle.format("map.nospawn.attack", checkRules.waveTeam.coloredName()));
|
||||
}
|
||||
}
|
||||
}else{
|
||||
@@ -392,78 +388,11 @@ public class World{
|
||||
if(invalidMap) Core.app.post(() -> state.set(State.menu));
|
||||
}
|
||||
|
||||
public void notifyChanged(Tile tile){
|
||||
if(!generating){
|
||||
Core.app.post(() -> Events.fire(new TileChangeEvent(tile)));
|
||||
}
|
||||
}
|
||||
|
||||
public void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){
|
||||
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
|
||||
}
|
||||
|
||||
public void raycastEach(int x0f, int y0f, int x1, int y1, Raycaster cons){
|
||||
int x0 = x0f;
|
||||
int y0 = y0f;
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
|
||||
int err = dx - dy;
|
||||
int e2;
|
||||
while(true){
|
||||
|
||||
if(cons.accept(x0, y0)) break;
|
||||
if(x0 == x1 && y0 == y1) break;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x0 = x0 + sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y0 = y0 + sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean raycast(int x0f, int y0f, int x1, int y1, Raycaster cons){
|
||||
int x0 = x0f;
|
||||
int y0 = y0f;
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
|
||||
int err = dx - dy;
|
||||
int e2;
|
||||
while(true){
|
||||
if(cons.accept(x0, y0)) return true;
|
||||
if(x0 == x1 && y0 == y1) return false;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x0 = x0 + sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y0 = y0 + sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addDarkness(Tiles tiles){
|
||||
byte[] dark = new byte[tiles.width * tiles.height];
|
||||
byte[] writeBuffer = new byte[tiles.width * tiles.height];
|
||||
|
||||
byte darkIterations = 4;
|
||||
byte darkIterations = darkRadius;
|
||||
|
||||
for(int i = 0; i < dark.length; i++){
|
||||
Tile tile = tiles.geti(i);
|
||||
@@ -497,7 +426,7 @@ public class World{
|
||||
tile.data = dark[idx];
|
||||
}
|
||||
|
||||
if(dark[idx] == 4){
|
||||
if(dark[idx] == darkRadius){
|
||||
boolean full = true;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int px = p.x + tile.x, py = p.y + tile.y;
|
||||
@@ -508,22 +437,60 @@ public class World{
|
||||
}
|
||||
}
|
||||
|
||||
if(full) tile.data = 5;
|
||||
if(full) tile.data = darkRadius + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float getDarkness(int x, int y){
|
||||
int edgeBlend = 2;
|
||||
public byte getWallDarkness(Tile tile){
|
||||
if(tile.isDarkened()){
|
||||
int minDst = darkRadius + 1;
|
||||
for(int cx = tile.x - darkRadius; cx <= tile.x + darkRadius; cx++){
|
||||
for(int cy = tile.y - darkRadius; cy <= tile.y + darkRadius; cy++){
|
||||
if(tiles.in(cx, cy) && !rawTile(cx, cy).isDarkened()){
|
||||
minDst = Math.min(minDst, Math.abs(cx - tile.x) + Math.abs(cy - tile.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (byte)Math.max((minDst - 1), 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void checkMapArea(){
|
||||
for(var build : Groups.build){
|
||||
//reset map-area-based disabled blocks.
|
||||
if(!build.enabled && build.block.autoResetEnabled){
|
||||
build.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO optimize; this is very slow and called too often!
|
||||
public float getDarkness(int x, int y){
|
||||
float dark = 0;
|
||||
int edgeDst = Math.min(x, Math.min(y, Math.min(Math.abs(x - (tiles.width - 1)), Math.abs(y - (tiles.height - 1)))));
|
||||
if(edgeDst <= edgeBlend){
|
||||
dark = Math.max((edgeBlend - edgeDst) * (4f / edgeBlend), dark);
|
||||
|
||||
if(Vars.state.rules.borderDarkness){
|
||||
int edgeBlend = 2;
|
||||
int edgeDst;
|
||||
|
||||
if(!state.rules.limitMapArea){
|
||||
edgeDst = Math.min(x, Math.min(y, Math.min(-(x - (tiles.width - 1)), -(y - (tiles.height - 1)))));
|
||||
}else{
|
||||
edgeDst =
|
||||
Math.min(x - state.rules.limitX,
|
||||
Math.min(y - state.rules.limitY,
|
||||
Math.min(-(x - (state.rules.limitX + state.rules.limitWidth - 1)), -(y - (state.rules.limitY + state.rules.limitHeight - 1)))));
|
||||
}
|
||||
|
||||
if(edgeDst <= edgeBlend){
|
||||
dark = Math.max((edgeBlend - edgeDst) * (4f / edgeBlend), dark);
|
||||
}
|
||||
}
|
||||
|
||||
if(state.hasSector() && state.getSector().preset == null){
|
||||
int circleBlend = 14;
|
||||
int circleBlend = 5;
|
||||
//quantized angle
|
||||
float offset = state.getSector().rect.rotation + 90;
|
||||
float angle = Angles.angle(x, y, tiles.width/2, tiles.height/2) + offset;
|
||||
@@ -546,22 +513,65 @@ public class World{
|
||||
}
|
||||
}
|
||||
|
||||
Tile tile = world.tile(x, y);
|
||||
if(tile != null && tile.block().solid && tile.block().fillsTile && !tile.block().synthetic()){
|
||||
Tile tile = tile(x, y);
|
||||
if(tile != null && tile.isDarkened()){
|
||||
dark = Math.max(dark, tile.data);
|
||||
}
|
||||
|
||||
return dark;
|
||||
}
|
||||
|
||||
public interface Raycaster{
|
||||
boolean accept(int x, int y);
|
||||
public static void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){
|
||||
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
|
||||
}
|
||||
|
||||
public static void raycastEach(int x1, int y1, int x2, int y2, Raycaster cons){
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
while(true){
|
||||
if(cons.accept(x, y)) break;
|
||||
if(x == x2 && y == y2) break;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean raycast(int x1, int y1, int x2, int y2, Raycaster cons){
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
while(true){
|
||||
if(cons.accept(x, y)) return true;
|
||||
if(x == x2 && y == y2) return false;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x = x + sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y = y + sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Context implements WorldContext{
|
||||
|
||||
Context(){
|
||||
}
|
||||
Context(){}
|
||||
|
||||
@Override
|
||||
public Tile tile(int index){
|
||||
@@ -597,15 +607,21 @@ public class World{
|
||||
}
|
||||
|
||||
/** World context that applies filters after generation end. */
|
||||
private class FilterContext extends Context{
|
||||
public class FilterContext extends Context{
|
||||
final Map map;
|
||||
|
||||
FilterContext(Map map){
|
||||
public FilterContext(Map map){
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(){
|
||||
applyFilters();
|
||||
|
||||
super.end();
|
||||
}
|
||||
|
||||
public void applyFilters(){
|
||||
Seq<GenerateFilter> filters = map.filters();
|
||||
|
||||
if(!filters.isEmpty()){
|
||||
@@ -614,12 +630,10 @@ public class World{
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
filter.randomize();
|
||||
input.begin(filter, width(), height(), (x, y) -> tiles.getn(x, y));
|
||||
input.begin(width(), height(), (x, y) -> tiles.getn(x, y));
|
||||
filter.apply(tiles, input);
|
||||
}
|
||||
}
|
||||
|
||||
super.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import mindustry.*;
|
||||
import mindustry.mod.Mods.*;
|
||||
|
||||
/** Base class for a content type that is loaded in {@link mindustry.core.ContentLoader}. */
|
||||
public abstract class Content implements Comparable<Content>, Disposable{
|
||||
public final short id;
|
||||
public abstract class Content implements Comparable<Content>{
|
||||
public short id;
|
||||
/** Info on which mod this content was loaded from. */
|
||||
public ModContentInfo minfo = new ModContentInfo();
|
||||
|
||||
@@ -25,20 +25,31 @@ public abstract class Content implements Comparable<Content>, Disposable{
|
||||
/** Called after all content and modules are created. Do not use to load regions or texture data! */
|
||||
public void init(){}
|
||||
|
||||
/** Called after init(). */
|
||||
public void postInit(){}
|
||||
|
||||
/**
|
||||
* Called after all content is created, only on non-headless versions.
|
||||
* Use for loading regions or other image data.
|
||||
*/
|
||||
public void load(){}
|
||||
|
||||
/** Called right before load(). */
|
||||
public void loadIcon(){}
|
||||
|
||||
/** @return whether an error occurred during mod loading. */
|
||||
public boolean hasErrored(){
|
||||
return minfo.error != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
//does nothing by default
|
||||
/** @return whether this is content from the base game. */
|
||||
public boolean isVanilla(){
|
||||
return minfo.mod == null;
|
||||
}
|
||||
|
||||
/** @return whether this content is from a mod. */
|
||||
public boolean isModded(){
|
||||
return !isVanilla();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
/** Interface for a list of content to be loaded in {@link mindustry.core.ContentLoader}. */
|
||||
public interface ContentList{
|
||||
/** This method should create all the content. */
|
||||
void load();
|
||||
}
|
||||
@@ -1,22 +1,37 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
import arc.util.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
/** Do not rearrange, ever! */
|
||||
public enum ContentType{
|
||||
item,
|
||||
block,
|
||||
mech_UNUSED,
|
||||
bullet,
|
||||
liquid,
|
||||
status,
|
||||
unit,
|
||||
weather,
|
||||
effect_UNUSED,
|
||||
sector,
|
||||
loadout_UNUSED,
|
||||
typeid_UNUSED,
|
||||
error,
|
||||
planet,
|
||||
ammo;
|
||||
item(Item.class),
|
||||
block(Block.class),
|
||||
mech_UNUSED(null),
|
||||
bullet(BulletType.class),
|
||||
liquid(Liquid.class),
|
||||
status(StatusEffect.class),
|
||||
unit(UnitType.class),
|
||||
weather(Weather.class),
|
||||
effect_UNUSED(null),
|
||||
sector(SectorPreset.class),
|
||||
loadout_UNUSED(null),
|
||||
typeid_UNUSED(null),
|
||||
error(null),
|
||||
planet(Planet.class),
|
||||
ammo_UNUSED(null),
|
||||
team(TeamEntry.class),
|
||||
unitCommand(UnitCommand.class),
|
||||
unitStance(UnitStance.class);
|
||||
|
||||
public static final ContentType[] all = values();
|
||||
|
||||
public final @Nullable Class<? extends Content> contentClass;
|
||||
|
||||
ContentType(Class<? extends Content> contentClass){
|
||||
this.contentClass = contentClass;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package mindustry.ctype;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.g2d.TextureAtlas.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.content.TechTree.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.graphics.MultiPacker.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.world.meta.*;
|
||||
@@ -28,11 +32,38 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
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[Cicon.all.length];
|
||||
/** Unlock state. Loaded from settings. Do not modify outside of the constructor. */
|
||||
/** Whether details are hidden in custom games if this hasn't been unlocked in campaign mode. */
|
||||
public boolean hideDetails = true;
|
||||
/** Whether this is hidden from the Core Database. */
|
||||
public boolean hideDatabase = false;
|
||||
/** If false, all icon generation is disabled for this content; createIcons is not called. */
|
||||
public boolean generateIcons = true;
|
||||
/** How big the content appears in certain selection menus */
|
||||
public float selectionSize = 24f;
|
||||
/** Icon of the content to use in UI. */
|
||||
public TextureRegion uiIcon;
|
||||
/** Icon of the full content. Unscaled.*/
|
||||
public TextureRegion fullIcon;
|
||||
/** Override for the full icon. Useful for mod content with duplicate icons. Overrides any other full icon.*/
|
||||
public String fullOverride = "";
|
||||
/** If true, this content will appear in all database tabs. */
|
||||
public boolean allDatabaseTabs = false;
|
||||
/**
|
||||
* Planets that this content is made for. If empty, a planet is decided based on item requirements.
|
||||
* Currently, this is only meaningful for blocks.
|
||||
* */
|
||||
public ObjectSet<Planet> shownPlanets = new ObjectSet<>();
|
||||
/**
|
||||
* Content - usually a planet - that dictates which database tab(s) this content will appear in.
|
||||
* If nothing is defined, it will use the values in shownPlanets.
|
||||
* If shownPlanets is also empty, it will use Serpulo as the "default" tab.
|
||||
* */
|
||||
public ObjectSet<UnlockableContent> databaseTabs = new ObjectSet<>();
|
||||
/** The tech tree node for this content, if applicable. Null if not part of a tech tree. */
|
||||
public @Nullable TechNode techNode;
|
||||
/** Tech nodes for all trees that this content is part of. */
|
||||
public Seq<TechNode> techNodes = new Seq<>();
|
||||
/** Unlock state. Loaded from settings. Do not modify outside the constructor. */
|
||||
protected boolean unlocked;
|
||||
|
||||
public UnlockableContent(String name){
|
||||
@@ -44,13 +75,36 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
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);
|
||||
@Override
|
||||
public void postInit(){
|
||||
super.postInit();
|
||||
|
||||
databaseTabs.addAll(shownPlanets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadIcon(){
|
||||
fullIcon =
|
||||
Core.atlas.find(fullOverride == null ? "" : fullOverride,
|
||||
Core.atlas.find(getContentType().name() + "-" + name + "-full",
|
||||
Core.atlas.find(name + "-full",
|
||||
Core.atlas.find(name,
|
||||
Core.atlas.find(getContentType().name() + "-" + name,
|
||||
Core.atlas.find(name + "1"))))));
|
||||
|
||||
uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon);
|
||||
}
|
||||
|
||||
public boolean isOnPlanet(@Nullable Planet planet){
|
||||
return planet == null || planet == Planets.sun || shownPlanets.isEmpty() || shownPlanets.contains(planet);
|
||||
}
|
||||
|
||||
public int getLogicId(){
|
||||
return logicVars.lookupLogicId(this);
|
||||
}
|
||||
|
||||
public String displayDescription(){
|
||||
return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName());
|
||||
return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName);
|
||||
}
|
||||
|
||||
/** Checks stat initialization state. Call before displaying stats. */
|
||||
@@ -65,12 +119,50 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
public void setStats(){
|
||||
}
|
||||
|
||||
/** Generate any special icons for this content. Called asynchronously.*/
|
||||
/** Display any extra info after details. */
|
||||
public void displayExtra(Table table){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate any special icons for this content. Called synchronously.
|
||||
* No regions are loaded at this point; grab pixmaps from the packer.
|
||||
* */
|
||||
@CallSuper
|
||||
public void createIcons(MultiPacker packer){
|
||||
|
||||
}
|
||||
|
||||
protected void makeOutline(PageType page, MultiPacker packer, TextureRegion region, boolean makeNew, Color outlineColor, int outlineRadius){
|
||||
if(region instanceof AtlasRegion at && region.found()){
|
||||
String name = at.name;
|
||||
if(!makeNew || !packer.has(name + "-outline")){
|
||||
String regName = name + (makeNew ? "-outline" : "");
|
||||
if(packer.registerOutlined(regName)){
|
||||
PixmapRegion base = Core.atlas.getPixmap(region);
|
||||
var result = Pixmaps.outline(base, outlineColor, outlineRadius);
|
||||
Drawf.checkBleed(result);
|
||||
packer.add(page, regName, result);
|
||||
result.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void makeOutline(MultiPacker packer, TextureRegion region, String name, Color outlineColor, int outlineRadius){
|
||||
if(region.found() && packer.registerOutlined(name)){
|
||||
PixmapRegion base = Core.atlas.getPixmap(region);
|
||||
var result = Pixmaps.outline(base, outlineColor, outlineRadius);
|
||||
Drawf.checkBleed(result);
|
||||
packer.add(PageType.main, name, result);
|
||||
result.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected void makeOutline(MultiPacker packer, TextureRegion region, String name, Color outlineColor){
|
||||
makeOutline(packer, region, name, outlineColor, 4);
|
||||
}
|
||||
|
||||
/** @return items needed to research this content */
|
||||
public ItemStack[] researchRequirements(){
|
||||
return ItemStack.empty;
|
||||
@@ -80,19 +172,13 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
return Fonts.getUnicodeStr(name);
|
||||
}
|
||||
|
||||
/** Returns a specific content icon, or the region {contentType}-{name} if not found.*/
|
||||
public TextureRegion icon(Cicon icon){
|
||||
if(cicons[icon.ordinal()] == null){
|
||||
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")))))));
|
||||
}
|
||||
return cicons[icon.ordinal()];
|
||||
public int emojiChar(){
|
||||
return Fonts.getUnicode(name);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasEmoji(){
|
||||
return Fonts.hasUnicodeStr(name);
|
||||
}
|
||||
|
||||
/** Iterates through any implicit dependencies of this content.
|
||||
@@ -101,11 +187,6 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
|
||||
}
|
||||
|
||||
/** This should show all necessary info about this content in the specified table. */
|
||||
public void display(Table table){
|
||||
|
||||
}
|
||||
|
||||
/** Called when this content is unlocked. Use this to unlock other related content. */
|
||||
public void onUnlock(){
|
||||
}
|
||||
@@ -115,6 +196,15 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return whether to show a notification toast when this is unlocked */
|
||||
public boolean showUnlock(){
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean logicVisible(){
|
||||
return !isHidden();
|
||||
}
|
||||
|
||||
/** Makes this piece of content unlocked; if it already unlocked, nothing happens. */
|
||||
public void unlock(){
|
||||
if(!unlocked && !alwaysUnlocked){
|
||||
@@ -134,9 +224,26 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unlockedNowHost(){
|
||||
return !state.isCampaign() || unlockedHost();
|
||||
}
|
||||
|
||||
/** @return in multiplayer, whether this is unlocked for the host player, otherwise, whether it is unlocked for the local player (same as unlocked()) */
|
||||
public boolean unlockedHost(){
|
||||
return net != null && net.client() ?
|
||||
alwaysUnlocked || state.rules.researched.contains(this) :
|
||||
unlocked || alwaysUnlocked;
|
||||
}
|
||||
|
||||
/** @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 unlocked(){
|
||||
if(net != null && net.client()) return unlocked || alwaysUnlocked || state.rules.researched.contains(name);
|
||||
return unlocked || alwaysUnlocked;
|
||||
return net != null && net.client() ?
|
||||
alwaysUnlocked || unlocked || state.rules.researched.contains(this) :
|
||||
unlocked || alwaysUnlocked;
|
||||
}
|
||||
|
||||
/** Locks this content again. */
|
||||
@@ -147,11 +254,6 @@ public abstract class UnlockableContent extends MappableContent{
|
||||
}
|
||||
}
|
||||
|
||||
/** @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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class BannedContentDialog<T extends UnlockableContent> extends BaseDialog{
|
||||
private final ContentType type;
|
||||
private Table selectedTable;
|
||||
private Table deselectedTable;
|
||||
private ObjectSet<T> contentSet;
|
||||
private final Boolf<T> pred;
|
||||
private String contentSearch;
|
||||
private Category selectedCategory;
|
||||
private Seq<T> filteredContent;
|
||||
|
||||
public BannedContentDialog(String title, ContentType type, Boolf<T> pred){
|
||||
super(title);
|
||||
this.type = type;
|
||||
this.pred = pred;
|
||||
contentSearch = "";
|
||||
|
||||
selectedTable = new Table();
|
||||
deselectedTable = new Table();
|
||||
|
||||
addCloseButton();
|
||||
|
||||
shown(this::build);
|
||||
resized(this::build);
|
||||
}
|
||||
|
||||
public void show(ObjectSet<T> contentSet){
|
||||
this.contentSet = contentSet;
|
||||
show();
|
||||
}
|
||||
|
||||
public void build(){
|
||||
cont.clear();
|
||||
|
||||
var cell = cont.table(t -> {
|
||||
t.table(s -> {
|
||||
s.label(() -> "@search").padRight(10);
|
||||
var field = s.field(contentSearch, value -> {
|
||||
contentSearch = value;
|
||||
rebuildTables();
|
||||
}).get();
|
||||
s.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
contentSearch = "";
|
||||
field.setText("");
|
||||
rebuildTables();
|
||||
}).padLeft(10f).size(35f);
|
||||
});
|
||||
if(type == ContentType.block){
|
||||
t.row();
|
||||
t.table(c -> {
|
||||
c.marginTop(8f);
|
||||
c.defaults().marginRight(4f);
|
||||
for(Category category : Category.values()){
|
||||
c.button(ui.getIcon(category.name()), Styles.squareTogglei, () -> {
|
||||
if(selectedCategory == category){
|
||||
selectedCategory = null;
|
||||
}else{
|
||||
selectedCategory = category;
|
||||
}
|
||||
rebuildTables();
|
||||
}).size(45f).update(i -> i.setChecked(selectedCategory == category)).padLeft(4f);
|
||||
}
|
||||
c.add("").padRight(4f);
|
||||
}).center();
|
||||
}
|
||||
});
|
||||
cont.row();
|
||||
if(!Core.graphics.isPortrait()) cell.colspan(2);
|
||||
|
||||
filteredContent = content.<T>getBy(type).select(pred);
|
||||
if(!contentSearch.isEmpty()) filteredContent.removeAll(content -> !content.localizedName.toLowerCase().contains(contentSearch.toLowerCase()));
|
||||
|
||||
cont.table(table -> {
|
||||
if(type == ContentType.block){
|
||||
table.add("@bannedblocks").color(Color.valueOf("f25555")).padBottom(-1).top().row();
|
||||
}else{
|
||||
table.add("@bannedunits").color(Color.valueOf("f25555")).padBottom(-1).top().row();
|
||||
}
|
||||
|
||||
table.image().color(Color.valueOf("f25555")).height(3f).padBottom(5f).fillX().expandX().top().row();
|
||||
table.pane(table2 -> selectedTable = table2).fill().expand().row();
|
||||
table.button("@addall", Icon.add, () -> {
|
||||
contentSet.addAll(filteredContent);
|
||||
rebuildTables();
|
||||
}).disabled(button -> contentSet.toSeq().containsAll(filteredContent)).padTop(10f).bottom().fillX();
|
||||
}).fill().expandY().uniform();
|
||||
|
||||
if(Core.graphics.isPortrait()) cont.row();
|
||||
|
||||
var cell2 = cont.table(table -> {
|
||||
if(type == ContentType.block){
|
||||
table.add("@unbannedblocks").color(Pal.accent).padBottom(-1).top().row();
|
||||
}else{
|
||||
table.add("@unbannedunits").color(Pal.accent).padBottom(-1).top().row();
|
||||
}
|
||||
|
||||
table.image().color(Pal.accent).height(3f).padBottom(5f).fillX().top().row();
|
||||
table.pane(table2 -> deselectedTable = table2).fill().expand().row();
|
||||
table.button("@addall", Icon.add, () -> {
|
||||
contentSet.removeAll(filteredContent);
|
||||
rebuildTables();
|
||||
}).disabled(button -> {
|
||||
Seq<T> array = content.getBy(type);
|
||||
array = array.copy();
|
||||
array.removeAll(contentSet.toSeq());
|
||||
return array.containsAll(filteredContent);
|
||||
}).padTop(10f).bottom().fillX();
|
||||
}).fill().expandY().uniform();
|
||||
if(Core.graphics.isPortrait()){
|
||||
cell2.padTop(10f);
|
||||
}else{
|
||||
cell2.padLeft(10f);
|
||||
}
|
||||
|
||||
rebuildTables();
|
||||
}
|
||||
|
||||
private void rebuildTables(){
|
||||
filteredContent.clear();
|
||||
filteredContent = content.getBy(type);
|
||||
filteredContent = filteredContent.select(pred);
|
||||
|
||||
if(!contentSearch.isEmpty()) filteredContent.removeAll(content -> !content.localizedName.toLowerCase().contains(contentSearch.toLowerCase()));
|
||||
if(type == ContentType.block){
|
||||
filteredContent.removeAll(content -> selectedCategory != null && ((Block)content).category != selectedCategory);
|
||||
}
|
||||
|
||||
rebuildTable(selectedTable, true);
|
||||
rebuildTable(deselectedTable, false);
|
||||
}
|
||||
|
||||
private void rebuildTable(Table table, boolean isSelected){
|
||||
table.clear();
|
||||
|
||||
int cols;
|
||||
if(Core.graphics.isPortrait()){
|
||||
cols = Math.max(4, (int)((Core.graphics.getWidth() / Scl.scl() - 100f) / 50f));
|
||||
}else{
|
||||
cols = Math.max(4, (int)((Core.graphics.getWidth() / Scl.scl() - 300f) / 50f / 2));
|
||||
}
|
||||
|
||||
if((isSelected && contentSet.isEmpty()) || (!isSelected && contentSet.size == content.<T>getBy(type).count(pred))){
|
||||
table.add("@empty").width(50f * cols).padBottom(5f).get().setAlignment(Align.center);
|
||||
}else{
|
||||
Seq<T> array;
|
||||
if(!isSelected){
|
||||
array = content.getBy(type);
|
||||
array = array.copy();
|
||||
array.removeAll(contentSet.toSeq());
|
||||
}else{
|
||||
array = contentSet.toSeq();
|
||||
}
|
||||
array.sort();
|
||||
array.removeAll(content -> !filteredContent.contains(content));
|
||||
|
||||
if(array.isEmpty()){
|
||||
table.add("@empty").width(50f * cols).padBottom(5f).get().setAlignment(Align.center);
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
boolean requiresPad = true;
|
||||
|
||||
for(T content : array){
|
||||
TextureRegion region = content.uiIcon;
|
||||
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNonei);
|
||||
button.getStyle().imageUp = new TextureRegionDrawable(region);
|
||||
button.resizeImage(8 * 4f);
|
||||
if(isSelected) button.clicked(() -> {
|
||||
contentSet.remove(content);
|
||||
rebuildTables();
|
||||
});
|
||||
else button.clicked(() -> {
|
||||
contentSet.add(content);
|
||||
rebuildTables();
|
||||
});
|
||||
table.add(button).size(50f).tooltip(content.localizedName);
|
||||
|
||||
if(++i % cols == 0){
|
||||
table.row();
|
||||
requiresPad = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(requiresPad){
|
||||
table.add("").padRight(50f * (cols - i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,8 @@ import mindustry.world.blocks.environment.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class DrawOperation{
|
||||
private MapEditor editor;
|
||||
private LongSeq array = new LongSeq();
|
||||
|
||||
public DrawOperation(MapEditor editor){
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
public boolean isEmpty(){
|
||||
return array.isEmpty();
|
||||
}
|
||||
@@ -61,7 +56,9 @@ public class DrawOperation{
|
||||
void setTile(Tile tile, byte type, short to){
|
||||
editor.load(() -> {
|
||||
if(type == OpType.floor.ordinal()){
|
||||
tile.setFloor((Floor)content.block(to));
|
||||
if(content.block(to) instanceof Floor floor){
|
||||
tile.setFloor(floor);
|
||||
}
|
||||
}else if(type == OpType.block.ordinal()){
|
||||
tile.getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
|
||||
|
||||
|
||||
@@ -39,9 +39,14 @@ public class EditorTile extends Tile{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(Block type, Team team, int rotation){
|
||||
public boolean isEditorTile(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(Block type, Team team, int rotation, Prov<Building> entityprov){
|
||||
if(skip()){
|
||||
super.setBlock(type, team, rotation);
|
||||
super.setBlock(type, team, rotation, entityprov);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,7 +68,7 @@ public class EditorTile extends Tile{
|
||||
|
||||
}
|
||||
|
||||
super.setBlock(type, team, rotation);
|
||||
super.setBlock(type, team, rotation, entityprov);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,7 +82,7 @@ public class EditorTile extends Tile{
|
||||
op(OpType.team, (byte)getTeamID());
|
||||
super.setTeam(team);
|
||||
|
||||
getLinkedTiles(t -> ui.editor.editor.renderer.updatePoint(t.x, t.y));
|
||||
getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,7 +92,7 @@ public class EditorTile extends Tile{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!floor.hasSurface() && overlay.asFloor().needsSurface) return;
|
||||
if(!floor.hasSurface() && overlay.asFloor().needsSurface && (overlay instanceof OreBlock || !floor.supportsOverlay)) return;
|
||||
if(overlay() == overlay) return;
|
||||
op(OpType.overlay, this.overlay.id);
|
||||
super.setOverlay(overlay);
|
||||
@@ -102,6 +107,15 @@ public class EditorTile extends Tile{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firePreChanged(){
|
||||
if(skip()){
|
||||
super.firePreChanged();
|
||||
}else{
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recache(){
|
||||
if(skip()){
|
||||
@@ -132,7 +146,6 @@ public class EditorTile extends Tile{
|
||||
|
||||
if(block.hasBuilding()){
|
||||
build = entityprov.get().init(this, team, false, rotation);
|
||||
build.cons = new ConsumeModule(build);
|
||||
if(block.hasItems) build.items = new ItemModule();
|
||||
if(block.hasLiquids) build.liquids(new LiquidModule());
|
||||
if(block.hasPower) build.power(new PowerModule());
|
||||
@@ -140,14 +153,14 @@ public class EditorTile extends Tile{
|
||||
}
|
||||
|
||||
private void update(){
|
||||
ui.editor.editor.renderer.updatePoint(x, y);
|
||||
editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
|
||||
private boolean skip(){
|
||||
return state.isGame() || ui.editor.editor.isLoading();
|
||||
return state.isGame() || editor.isLoading() || world.isGenerating();
|
||||
}
|
||||
|
||||
private void op(OpType type, short value){
|
||||
ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
|
||||
editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import mindustry.content.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public enum EditorTool{
|
||||
zoom(KeyCode.v),
|
||||
pick(KeyCode.i){
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
public void touched(int x, int y){
|
||||
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
|
||||
|
||||
Tile tile = editor.tile(x, y);
|
||||
@@ -23,7 +25,7 @@ public enum EditorTool{
|
||||
line(KeyCode.l, "replace", "orthogonal"){
|
||||
|
||||
@Override
|
||||
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){
|
||||
public void touchedLine(int x1, int y1, int x2, int y2){
|
||||
//straight
|
||||
if(mode == 1){
|
||||
if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){
|
||||
@@ -44,14 +46,15 @@ public enum EditorTool{
|
||||
});
|
||||
}
|
||||
},
|
||||
pencil(KeyCode.b, "replace", "square", "drawteams"){
|
||||
//the "under liquid" rendering is too buggy to make public
|
||||
pencil(KeyCode.b, "replace", "square", "drawteams"/*, "underliquid"*/){
|
||||
{
|
||||
edit = true;
|
||||
draggable = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
public void touched(int x, int y){
|
||||
if(mode == -1){
|
||||
//normal mode
|
||||
editor.drawBlocks(x, y);
|
||||
@@ -60,10 +63,12 @@ public enum EditorTool{
|
||||
editor.drawBlocksReplace(x, y);
|
||||
}else if(mode == 1){
|
||||
//square mode
|
||||
editor.drawBlocks(x, y, true, tile -> true);
|
||||
editor.drawBlocks(x, y, true, false, tile -> true);
|
||||
}else if(mode == 2){
|
||||
//draw teams
|
||||
editor.drawCircle(x, y, tile -> tile.setTeam(editor.drawTeam));
|
||||
}else if(mode == 3){
|
||||
editor.drawBlocks(x, y, false, true, tile -> tile.floor().isLiquid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -75,7 +80,7 @@ public enum EditorTool{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
public void touched(int x, int y){
|
||||
editor.drawCircle(x, y, tile -> {
|
||||
if(mode == -1){
|
||||
//erase block
|
||||
@@ -87,7 +92,7 @@ public enum EditorTool{
|
||||
});
|
||||
}
|
||||
},
|
||||
fill(KeyCode.g, "replaceall", "fillteams"){
|
||||
fill(KeyCode.g, "replaceall", "fillteams", "fillerase"){
|
||||
{
|
||||
edit = true;
|
||||
}
|
||||
@@ -95,17 +100,19 @@ public enum EditorTool{
|
||||
IntSeq stack = new IntSeq();
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
public void touched(int x, int y){
|
||||
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
|
||||
Tile tile = editor.tile(x, y);
|
||||
|
||||
if(editor.drawBlock.isMultiblock()){
|
||||
if(tile == null) return;
|
||||
|
||||
if(editor.drawBlock.isMultiblock() && (mode == 0 || mode == -1)){
|
||||
//don't fill multiblocks, thanks
|
||||
pencil.touched(editor, x, y);
|
||||
pencil.touched(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
//mode 0 or 1, fill everything with the floor/tile or replace it
|
||||
//mode 0 or standard, fill everything with the floor/tile or replace it
|
||||
if(mode == 0 || mode == -1){
|
||||
//can't fill parts or multiblocks
|
||||
if(tile.block().isMultiblock()){
|
||||
@@ -133,19 +140,40 @@ public enum EditorTool{
|
||||
}
|
||||
|
||||
//replace only when the mode is 0 using the specified functions
|
||||
fill(editor, x, y, mode == 0, tester, setter);
|
||||
fill(x, y, mode == 0, tester, setter);
|
||||
}else if(mode == 1){ //mode 1 is team fill
|
||||
|
||||
//only fill synthetic blocks, it's meaningless otherwise
|
||||
if(tile.synthetic()){
|
||||
Team dest = tile.team();
|
||||
if(dest == editor.drawTeam) return;
|
||||
fill(editor, x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam));
|
||||
fill(x, y, true, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam));
|
||||
}
|
||||
}else if(mode == 2){ //erase mode
|
||||
Boolf<Tile> tester;
|
||||
Cons<Tile> setter;
|
||||
|
||||
if(tile.block() != Blocks.air){
|
||||
Block dest = tile.block();
|
||||
tester = t -> t.block() == dest;
|
||||
setter = t -> t.setBlock(Blocks.air);
|
||||
}else if(tile.overlay() != Blocks.air){
|
||||
Block dest = tile.overlay();
|
||||
tester = t -> t.overlay() == dest;
|
||||
setter = t -> t.setOverlay(Blocks.air);
|
||||
}else{
|
||||
//trying to erase floor (no)
|
||||
tester = null;
|
||||
setter = null;
|
||||
}
|
||||
|
||||
if(setter != null){
|
||||
fill(x, y, false, tester, setter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fill(MapEditor editor, int x, int y, boolean replace, Boolf<Tile> tester, Cons<Tile> filler){
|
||||
void fill(int x, int y, boolean replace, Boolf<Tile> tester, Cons<Tile> filler){
|
||||
int width = editor.width(), height = editor.height();
|
||||
|
||||
if(replace){
|
||||
@@ -215,7 +243,7 @@ public enum EditorTool{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
public void touched(int x, int y){
|
||||
|
||||
//floor spray
|
||||
if(editor.drawBlock.isFloor()){
|
||||
@@ -263,7 +291,7 @@ public enum EditorTool{
|
||||
this.key = code;
|
||||
}
|
||||
|
||||
public void touched(MapEditor editor, int x, int y){}
|
||||
public void touched(int x, int y){}
|
||||
|
||||
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){}
|
||||
public void touchedLine(int x1, int y1, int x2, int y2){}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.editor.DrawOperation.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.io.*;
|
||||
@@ -17,17 +18,17 @@ import mindustry.world.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapEditor{
|
||||
public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20};
|
||||
public static final float[] brushSizes = {1, 1.5f, 2, 3, 4, 5, 9, 15, 20};
|
||||
|
||||
public StringMap tags = new StringMap();
|
||||
public MapRenderer renderer = new MapRenderer(this);
|
||||
public MapRenderer renderer = new MapRenderer();
|
||||
|
||||
private final Context context = new Context();
|
||||
private OperationStack stack = new OperationStack();
|
||||
private DrawOperation currentOp;
|
||||
private boolean loading;
|
||||
|
||||
public int brushSize = 1;
|
||||
public float brushSize = 1;
|
||||
public int rotation;
|
||||
public Block drawBlock = Blocks.stone;
|
||||
public Team drawTeam = Team.sharded;
|
||||
@@ -61,11 +62,31 @@ public class MapEditor{
|
||||
public void beginEdit(Pixmap pixmap){
|
||||
reset();
|
||||
|
||||
createTiles(pixmap.getWidth(), pixmap.getHeight());
|
||||
createTiles(pixmap.width, pixmap.height);
|
||||
load(() -> MapIO.readImage(pixmap, tiles()));
|
||||
renderer.resize(width(), height());
|
||||
}
|
||||
|
||||
public void updateRenderer(){
|
||||
Tiles tiles = world.tiles;
|
||||
Seq<Building> builds = new Seq<>();
|
||||
|
||||
for(int i = 0; i < tiles.width * tiles.height; i++){
|
||||
Tile tile = tiles.geti(i);
|
||||
var build = tile.build;
|
||||
if(build != null && tile.isCenter()){
|
||||
builds.add(build);
|
||||
}
|
||||
tiles.seti(i, new EditorTile(tile.x, tile.y, tile.floorID(), tile.overlayID(), build == null ? tile.blockID() : 0));
|
||||
}
|
||||
|
||||
for(var build : builds){
|
||||
tiles.get(build.tileX(), build.tileY()).setBlock(build.block, build.team, build.rotation, () -> build);
|
||||
}
|
||||
|
||||
renderer.resize(width(), height());
|
||||
}
|
||||
|
||||
public void load(Runnable r){
|
||||
loading = true;
|
||||
r.run();
|
||||
@@ -115,14 +136,14 @@ public class MapEditor{
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y){
|
||||
drawBlocks(x, y, false, tile -> true);
|
||||
drawBlocks(x, y, false, false, tile -> true);
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y, Boolf<Tile> tester){
|
||||
drawBlocks(x, y, false, tester);
|
||||
drawBlocks(x, y, false, false, tester);
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y, boolean square, Boolf<Tile> tester){
|
||||
public void drawBlocks(int x, int y, boolean square, boolean forceOverlay, Boolf<Tile> tester){
|
||||
if(drawBlock.isMultiblock()){
|
||||
x = Mathf.clamp(x, (drawBlock.size - 1) / 2, width() - drawBlock.size / 2 - 1);
|
||||
y = Mathf.clamp(y, (drawBlock.size - 1) / 2, height() - drawBlock.size / 2 - 1);
|
||||
@@ -136,7 +157,13 @@ public class MapEditor{
|
||||
if(!tester.get(tile)) return;
|
||||
|
||||
if(isFloor){
|
||||
tile.setFloor(drawBlock.asFloor());
|
||||
if(forceOverlay){
|
||||
tile.setOverlay(drawBlock.asFloor());
|
||||
}else{
|
||||
if(!(drawBlock.asFloor().wallOre && !tile.block().solid)){
|
||||
tile.setFloor(drawBlock.asFloor());
|
||||
}
|
||||
}
|
||||
}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));
|
||||
@@ -226,8 +253,9 @@ public class MapEditor{
|
||||
}
|
||||
|
||||
public void drawCircle(int x, int y, Cons<Tile> drawer){
|
||||
for(int rx = -brushSize; rx <= brushSize; rx++){
|
||||
for(int ry = -brushSize; ry <= brushSize; ry++){
|
||||
int clamped = (int)brushSize;
|
||||
for(int rx = -clamped; rx <= clamped; rx++){
|
||||
for(int ry = -clamped; ry <= clamped; ry++){
|
||||
if(Mathf.within(rx, ry, brushSize - 0.5f + 0.0001f)){
|
||||
int wx = x + rx, wy = y + ry;
|
||||
|
||||
@@ -242,8 +270,9 @@ public class MapEditor{
|
||||
}
|
||||
|
||||
public void drawSquare(int x, int y, Cons<Tile> drawer){
|
||||
for(int rx = -brushSize; rx <= brushSize; rx++){
|
||||
for(int ry = -brushSize; ry <= brushSize; ry++){
|
||||
int clamped = (int)brushSize;
|
||||
for(int rx = -clamped; rx <= clamped; rx++){
|
||||
for(int ry = -clamped; ry <= clamped; ry++){
|
||||
int wx = x + rx, wy = y + ry;
|
||||
|
||||
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
|
||||
@@ -255,27 +284,51 @@ public class MapEditor{
|
||||
}
|
||||
}
|
||||
|
||||
public void resize(int width, int height){
|
||||
public void resize(int width, int height, int shiftX, int shiftY){
|
||||
clearOp();
|
||||
|
||||
Tiles previous = world.tiles;
|
||||
int offsetX = -(width - width()) / 2, offsetY = -(height - height()) / 2;
|
||||
int offsetX = (width() - width) / 2 - shiftX, offsetY = (height() - height) / 2 - shiftY;
|
||||
loading = true;
|
||||
|
||||
Tiles tiles = world.resize(width, height);
|
||||
world.clearBuildings();
|
||||
|
||||
Tiles tiles = world.tiles = new Tiles(width, height);
|
||||
|
||||
for(int x = 0; x < width; x++){
|
||||
for(int y = 0; y < height; y++){
|
||||
int px = offsetX + x, py = offsetY + y;
|
||||
if(previous.in(px, py)){
|
||||
tiles.set(x, y, previous.getn(px, py));
|
||||
Tile tile = tiles.getn(x, y);
|
||||
|
||||
Object config = null;
|
||||
|
||||
//fetch the old config first, configs can be relative to block position (tileX/tileY) before those are reassigned
|
||||
if(tile.build != null && tile.isCenter()){
|
||||
config = tile.build.config();
|
||||
}
|
||||
|
||||
tile.x = (short)x;
|
||||
tile.y = (short)y;
|
||||
|
||||
if(tile.build != null && tile.isCenter()){
|
||||
tile.build.x = x * tilesize + tile.block().offset;
|
||||
tile.build.y = y * tilesize + tile.block().offset;
|
||||
|
||||
//shift links to account for map resize
|
||||
if(config != null){
|
||||
Object out = BuildPlan.pointConfig(tile.block(), config, p -> {
|
||||
if(!tile.build.block.ignoreResizeConfig){
|
||||
p.sub(offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
if(out != config){
|
||||
tile.build.configureAny(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}else{
|
||||
tiles.set(x, y, new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0));
|
||||
}
|
||||
@@ -319,7 +372,7 @@ public class MapEditor{
|
||||
public void addTileOp(long data){
|
||||
if(loading) return;
|
||||
|
||||
if(currentOp == null) currentOp = new DrawOperation(this);
|
||||
if(currentOp == null) currentOp = new DrawOperation();
|
||||
currentOp.addOperation(data);
|
||||
|
||||
renderer.updatePoint(TileOp.x(data), TileOp.y(data));
|
||||
|
||||
@@ -19,6 +19,7 @@ import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
@@ -33,18 +34,18 @@ import mindustry.world.meta.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapEditorDialog extends Dialog implements Disposable{
|
||||
public final MapEditor editor;
|
||||
|
||||
private MapView view;
|
||||
private MapInfoDialog infoDialog;
|
||||
private MapLoadDialog loadDialog;
|
||||
private MapResizeDialog resizeDialog;
|
||||
private MapGenerateDialog generateDialog;
|
||||
private SectorGenerateDialog sectorGenDialog;
|
||||
private MapPlayDialog playtestDialog;
|
||||
private ScrollPane pane;
|
||||
private BaseDialog menu;
|
||||
private Table blockSelection;
|
||||
private Rules lastSavedRules;
|
||||
private boolean saved = false;
|
||||
private boolean saved = false; //currently never read
|
||||
private boolean shownWithMap = false;
|
||||
private Seq<Block> blocksOut = new Seq<>();
|
||||
|
||||
@@ -53,10 +54,11 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
background(Styles.black);
|
||||
|
||||
editor = new MapEditor();
|
||||
view = new MapView(editor);
|
||||
infoDialog = new MapInfoDialog(editor);
|
||||
generateDialog = new MapGenerateDialog(editor, true);
|
||||
view = new MapView();
|
||||
infoDialog = new MapInfoDialog();
|
||||
generateDialog = new MapGenerateDialog(true);
|
||||
sectorGenDialog = new SectorGenerateDialog();
|
||||
playtestDialog = new MapPlayDialog();
|
||||
|
||||
menu = new BaseDialog("@menu");
|
||||
menu.addCloseButton();
|
||||
@@ -120,9 +122,15 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
"@editor.exportimage", "@editor.exportimage.description", Icon.fileImage,
|
||||
(Runnable)() -> platform.export(editor.tags.get("name", "unknown"), "png", file -> {
|
||||
Pixmap out = MapIO.writeImage(editor.tiles());
|
||||
file.writePNG(out);
|
||||
file.writePng(out);
|
||||
out.dispose();
|
||||
})));
|
||||
|
||||
t.row();
|
||||
|
||||
t.button("@editor.ingame", Icon.right, this::editInGame);
|
||||
|
||||
t.button("@editor.playtest", Icon.play, this::playtest);
|
||||
});
|
||||
|
||||
menu.cont.row();
|
||||
@@ -156,24 +164,31 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
}
|
||||
|
||||
platform.publish(map);
|
||||
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.tags.containsKey("steamid") ? editor.tags.get("author").equals(player.name) ? "@workshop.listing" : "@view.workshop" : "@editor.publish.workshop"));
|
||||
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b ->
|
||||
b.setText(editor.tags.containsKey("steamid") ?
|
||||
editor.tags.get("author", "").equals(steamPlayerName) ? "@workshop.listing" : "@view.workshop" :
|
||||
"@editor.publish.workshop"));
|
||||
|
||||
menu.cont.row();
|
||||
}
|
||||
|
||||
menu.cont.button("@editor.ingame", Icon.right, this::playtest).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
menu.cont.button("@editor.sectorgenerate", Icon.terrain, () -> {
|
||||
menu.hide();
|
||||
sectorGenDialog.show();
|
||||
}).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.button("@quit", Icon.exit, () -> {
|
||||
tryExit();
|
||||
menu.hide();
|
||||
}).size(swidth * 2f + 10, 60f);
|
||||
}).padTop(1).size(swidth * 2f + 10, 60f);
|
||||
|
||||
resizeDialog = new MapResizeDialog(editor, (x, y) -> {
|
||||
if(!(editor.width() == x && editor.height() == y)){
|
||||
resizeDialog = new MapResizeDialog((width, height, shiftX, shiftY) -> {
|
||||
if(!(editor.width() == width && editor.height() == height && shiftX == 0 && shiftY == 0)){
|
||||
ui.loadAnd(() -> {
|
||||
editor.resize(x, y);
|
||||
editor.resize(width, height, shiftX, shiftY);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -193,11 +208,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
margin(0);
|
||||
|
||||
update(() -> {
|
||||
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
|
||||
return;
|
||||
}
|
||||
|
||||
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
|
||||
if(hasKeyboard()){
|
||||
doInput();
|
||||
}
|
||||
});
|
||||
@@ -238,7 +249,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
editor.renderer.updateAll();
|
||||
}
|
||||
|
||||
private void playtest(){
|
||||
private void editInGame(){
|
||||
menu.hide();
|
||||
ui.loadAnd(() -> {
|
||||
lastSavedRules = state.rules;
|
||||
@@ -247,7 +258,9 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
state.teams = new Teams();
|
||||
player.reset();
|
||||
state.rules = Gamemode.editor.apply(lastSavedRules.copy());
|
||||
state.rules.limitMapArea = false;
|
||||
state.rules.sector = null;
|
||||
state.rules.fog = false;
|
||||
state.map = new Map(StringMap.of(
|
||||
"name", "Editor Playtesting",
|
||||
"width", editor.width(),
|
||||
@@ -255,22 +268,60 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
));
|
||||
world.endMapLoad();
|
||||
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
|
||||
Core.camera.position.set(player);
|
||||
player.clearUnit();
|
||||
Groups.unit.clear();
|
||||
|
||||
for(var unit : Groups.unit){
|
||||
if(unit.spawnedByCore){
|
||||
unit.remove();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
var unit = (state.rules.hasEnv(Env.scorching) ? UnitTypes.evoke : UnitTypes.alpha).spawn(player.team(), player.x, player.y);
|
||||
unit.spawnedByCore = true;
|
||||
player.unit(unit);
|
||||
}
|
||||
|
||||
player.checkSpawn();
|
||||
});
|
||||
}
|
||||
|
||||
public void resumeAfterPlaytest(Map map){
|
||||
beginEditMap(map.file);
|
||||
}
|
||||
|
||||
private void playtest(){
|
||||
menu.hide();
|
||||
Map map = save();
|
||||
|
||||
if(map != null){
|
||||
//skip dialog, play immediately when shift clicked
|
||||
if(Core.input.shift()){
|
||||
hide();
|
||||
//auto pick best fit
|
||||
control.playMap(map, map.applyRules(
|
||||
Gamemode.survival.valid(map) ? Gamemode.survival :
|
||||
Gamemode.attack.valid(map) ? Gamemode.attack :
|
||||
Gamemode.sandbox), true
|
||||
);
|
||||
}else{
|
||||
playtestDialog.playListener = this::hide;
|
||||
playtestDialog.show(map, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Map save(){
|
||||
boolean isEditor = state.rules.editor;
|
||||
state.rules.editor = false;
|
||||
state.rules.objectiveFlags.clear();
|
||||
state.rules.objectives.each(MapObjective::reset);
|
||||
String name = editor.tags.get("name", "").trim();
|
||||
editor.tags.put("rules", JsonIO.write(state.rules));
|
||||
editor.tags.remove("width");
|
||||
@@ -278,6 +329,12 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
player.clearUnit();
|
||||
|
||||
//remove player unit
|
||||
Unit unit = Groups.unit.find(u -> u.spawnedByCore);
|
||||
if(unit != null){
|
||||
unit.remove();
|
||||
}
|
||||
|
||||
Map returned = null;
|
||||
|
||||
if(name.isEmpty()){
|
||||
@@ -285,10 +342,19 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
Core.app.post(() -> ui.showErrorMessage("@editor.save.noname"));
|
||||
}else{
|
||||
Map map = maps.all().find(m -> m.name().equals(name));
|
||||
if(map != null && !map.custom){
|
||||
if(map != null && !map.custom && !map.workshop){
|
||||
handleSaveBuiltin(map);
|
||||
}else{
|
||||
boolean workshop = false;
|
||||
//try to preserve Steam ID
|
||||
if(map != null && map.tags.containsKey("steamid")){
|
||||
editor.tags.put("steamid", map.tags.get("steamid"));
|
||||
workshop = true;
|
||||
}
|
||||
returned = maps.saveMap(editor.tags);
|
||||
if(workshop){
|
||||
returned.workshop = workshop;
|
||||
}
|
||||
ui.showInfoFade("@editor.saved");
|
||||
}
|
||||
}
|
||||
@@ -392,7 +458,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
}
|
||||
|
||||
public void build(){
|
||||
float size = 58f;
|
||||
float size = mobile ? 50f : 58f;
|
||||
|
||||
clearChildren();
|
||||
table(cont -> {
|
||||
@@ -408,7 +474,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
Cons<EditorTool> addTool = tool -> {
|
||||
|
||||
ImageButton button = new ImageButton(ui.getIcon(tool.name()), Styles.clearTogglei);
|
||||
ImageButton button = new ImageButton(ui.getIcon(tool.name()), Styles.squareTogglei);
|
||||
button.clicked(() -> {
|
||||
view.setTool(tool);
|
||||
if(lastTable[0] != null){
|
||||
@@ -444,7 +510,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
table.button(b -> {
|
||||
b.left();
|
||||
b.marginLeft(6);
|
||||
b.setStyle(Styles.clearTogglet);
|
||||
b.setStyle(Styles.flatTogglet);
|
||||
b.add(Core.bundle.get("toolmode." + name)).left();
|
||||
b.row();
|
||||
b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.lightGray).left();
|
||||
@@ -484,16 +550,16 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
tools.defaults().size(size, size);
|
||||
|
||||
tools.button(Icon.menu, Styles.cleari, menu::show);
|
||||
tools.button(Icon.menu, Styles.flati, menu::show);
|
||||
|
||||
ImageButton grid = tools.button(Icon.grid, Styles.clearTogglei, () -> view.setGrid(!view.isGrid())).get();
|
||||
ImageButton grid = tools.button(Icon.grid, Styles.squareTogglei, () -> view.setGrid(!view.isGrid())).get();
|
||||
|
||||
addTool.get(EditorTool.zoom);
|
||||
|
||||
tools.row();
|
||||
|
||||
ImageButton undo = tools.button(Icon.undo, Styles.cleari, editor::undo).get();
|
||||
ImageButton redo = tools.button(Icon.redo, Styles.cleari, editor::redo).get();
|
||||
ImageButton undo = tools.button(Icon.undo, Styles.flati, editor::undo).get();
|
||||
ImageButton redo = tools.button(Icon.redo, Styles.flati, editor::redo).get();
|
||||
|
||||
addTool.get(EditorTool.pick);
|
||||
|
||||
@@ -515,7 +581,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
addTool.get(EditorTool.fill);
|
||||
addTool.get(EditorTool.spray);
|
||||
|
||||
ImageButton rotate = tools.button(Icon.right, Styles.cleari, () -> editor.rotation = (editor.rotation + 1) % 4).get();
|
||||
ImageButton rotate = tools.button(Icon.right, Styles.flati, () -> editor.rotation = (editor.rotation + 1) % 4).get();
|
||||
rotate.getImage().update(() -> {
|
||||
rotate.getImage().setRotation(editor.rotation * 90);
|
||||
rotate.getImage().setOrigin(Align.center);
|
||||
@@ -533,7 +599,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
int i = 0;
|
||||
|
||||
for(Team team : Team.baseTeams){
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglePartiali);
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
|
||||
button.margin(4f);
|
||||
button.getImageCell().grow();
|
||||
button.getStyle().imageUpColor = team.color;
|
||||
@@ -558,27 +624,27 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
}
|
||||
}
|
||||
|
||||
t.top();
|
||||
t.add("@editor.brush");
|
||||
var label = new Label("@editor.brush");
|
||||
label.setAlignment(Align.center);
|
||||
label.touchable = Touchable.disabled;
|
||||
|
||||
t.top().stack(slider, label).width(size * 3f - 20).padTop(4f);
|
||||
t.row();
|
||||
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);
|
||||
t.button("@editor.center", Icon.move, Styles.flatt, view::center).growX().margin(9f);
|
||||
}).growX().top();
|
||||
}
|
||||
|
||||
if(experimental){
|
||||
mid.row();
|
||||
mid.row();
|
||||
|
||||
mid.table(t -> {
|
||||
t.button("Cliffs", Icon.terrain, Styles.cleart, editor::addCliffs).growX().margin(9f);
|
||||
}).growX().top();
|
||||
}
|
||||
mid.table(t -> {
|
||||
t.button("@editor.cliffs", Icon.terrain, Styles.flatt, editor::addCliffs).growX().margin(9f);
|
||||
}).growX().top();
|
||||
}).margin(0).left().growY();
|
||||
|
||||
|
||||
@@ -627,28 +693,6 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
editor.undo();
|
||||
}
|
||||
|
||||
//more undocumented features, fantastic
|
||||
if(Core.input.keyTap(KeyCode.t)){
|
||||
|
||||
//clears all 'decoration' from the map
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
Tile tile = editor.tile(x, y);
|
||||
if(tile.block().breakable && tile.block() instanceof Boulder){
|
||||
tile.setBlock(Blocks.air);
|
||||
editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
|
||||
if(tile.overlay() != Blocks.air && tile.overlay() != Blocks.spawn){
|
||||
tile.setOverlay(Blocks.air);
|
||||
editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.flushOp();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.y)){
|
||||
editor.redo();
|
||||
}
|
||||
@@ -669,7 +713,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
private void addBlockSelection(Table cont){
|
||||
blockSelection = new Table();
|
||||
pane = new ScrollPane(blockSelection);
|
||||
pane = new ScrollPane(blockSelection, Styles.smallPane);
|
||||
pane.setFadeScrollBars(false);
|
||||
pane.setOverscroll(true, false);
|
||||
pane.exited(() -> {
|
||||
@@ -680,13 +724,13 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
cont.table(search -> {
|
||||
search.image(Icon.zoom).padRight(8);
|
||||
search.field("", this::rebuildBlockSelection)
|
||||
search.field("", this::rebuildBlockSelection).growX()
|
||||
.name("editor/search").maxTextLength(maxNameLength).get().setMessageText("@players.search");
|
||||
}).pad(-2);
|
||||
}).growX().pad(-2).padLeft(6f);
|
||||
cont.row();
|
||||
cont.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
|
||||
cont.row();
|
||||
cont.add(pane).expandY().top().left();
|
||||
cont.add(pane).expandY().growX().top().left();
|
||||
|
||||
rebuildBlockSelection("");
|
||||
}
|
||||
@@ -709,14 +753,14 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
int i = 0;
|
||||
|
||||
for(Block block : blocksOut){
|
||||
TextureRegion region = block.icon(Cicon.medium);
|
||||
TextureRegion region = block.uiIcon;
|
||||
|
||||
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);
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
|
||||
button.getStyle().imageUp = new TextureRegionDrawable(region);
|
||||
button.clicked(() -> editor.drawBlock = block);
|
||||
button.resizeImage(8 * 4f);
|
||||
@@ -725,13 +769,13 @@ public class MapEditorDialog extends Dialog implements Disposable{
|
||||
|
||||
if(i == 0) editor.drawBlock = block;
|
||||
|
||||
if(++i % 4 == 0){
|
||||
if(++i % 6 == 0){
|
||||
blockSelection.row();
|
||||
}
|
||||
}
|
||||
|
||||
if(i == 0){
|
||||
blockSelection.add("@none").color(Color.lightGray).padLeft(80f).padTop(10f);
|
||||
blockSelection.add("@none.found").padLeft(54f).padTop(10f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import arc.scene.ui.ImageButton.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.maps.filters.GenerateFilter.*;
|
||||
import mindustry.ui.*;
|
||||
@@ -22,32 +22,26 @@ import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
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, SpawnPathFilter::new
|
||||
};
|
||||
private final MapEditor editor;
|
||||
private final boolean applied;
|
||||
final boolean applied;
|
||||
|
||||
private Pixmap pixmap;
|
||||
private Texture texture;
|
||||
private GenerateInput input = new GenerateInput();
|
||||
Pixmap pixmap;
|
||||
Texture texture;
|
||||
GenerateInput input = new GenerateInput();
|
||||
Seq<GenerateFilter> filters = new Seq<>();
|
||||
private int scaling = mobile ? 3 : 1;
|
||||
private Table filterTable;
|
||||
int scaling = mobile ? 3 : 1;
|
||||
Table filterTable;
|
||||
|
||||
private AsyncExecutor executor = new AsyncExecutor(1);
|
||||
private AsyncResult<Void> result;
|
||||
Future<?> result;
|
||||
boolean generating;
|
||||
|
||||
private long[] buffer1, buffer2;
|
||||
private Cons<Seq<GenerateFilter>> applier;
|
||||
long[] buffer1, buffer2;
|
||||
Cons<Seq<GenerateFilter>> applier;
|
||||
CachedTile ctile = new CachedTile(){
|
||||
//nothing.
|
||||
@Override
|
||||
@@ -62,35 +56,79 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
};
|
||||
|
||||
/** @param applied whether or not to use the applied in-game mode. */
|
||||
public MapGenerateDialog(MapEditor editor, boolean applied){
|
||||
public MapGenerateDialog(boolean applied){
|
||||
super("@editor.generate");
|
||||
this.editor = editor;
|
||||
this.applied = applied;
|
||||
|
||||
shown(this::setup);
|
||||
addCloseButton();
|
||||
addCloseListener();
|
||||
|
||||
var style = Styles.flatt;
|
||||
|
||||
buttons.defaults().size(180f, 64f).pad(2f);
|
||||
buttons.button("@back", Icon.left, this::hide);
|
||||
|
||||
if(applied){
|
||||
buttons.button("@editor.apply", Icon.ok, () -> {
|
||||
ui.loadAnd(() -> {
|
||||
apply();
|
||||
hide();
|
||||
});
|
||||
}).size(160f, 64f);
|
||||
}else{
|
||||
buttons.button("@settings.reset", () -> {
|
||||
filters.set(maps.readFilters(""));
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).size(160f, 64f);
|
||||
});
|
||||
}
|
||||
|
||||
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(150f);
|
||||
buttons.button("@edit", Icon.edit, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@editor.export");
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, in -> {
|
||||
in.defaults().size(280f, 60f).left();
|
||||
|
||||
in.button("@waves.copy", Icon.copy, style, () -> {
|
||||
dialog.hide();
|
||||
|
||||
Core.app.setClipboardText(JsonIO.write(filters));
|
||||
}).marginLeft(12f).row();
|
||||
in.button("@waves.load", Icon.download, style, () -> {
|
||||
dialog.hide();
|
||||
try{
|
||||
filters.set(JsonIO.read(Seq.class, Core.app.getClipboardText()));
|
||||
|
||||
rebuildFilters();
|
||||
update();
|
||||
}catch(Throwable e){
|
||||
ui.showException(e);
|
||||
}
|
||||
}).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null).row();
|
||||
in.button("@clear", Icon.none, style, () -> {
|
||||
dialog.hide();
|
||||
filters.clear();
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).marginLeft(12f).row();
|
||||
if(!applied){
|
||||
in.button("@settings.reset", Icon.refresh, style, () -> {
|
||||
dialog.hide();
|
||||
filters.set(maps.readFilters(""));
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).marginLeft(12f).row();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
buttons.button("@add", Icon.add, this::showAdd);
|
||||
|
||||
if(!applied){
|
||||
hidden(this::apply);
|
||||
@@ -115,13 +153,13 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
long[] writeTiles = new long[editor.width() * editor.height()];
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
input.begin(filter, editor.width(), editor.height(), editor::tile);
|
||||
input.begin(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.block(), tile.floor(), tile.overlay());
|
||||
input.set(x, y, tile.block(), tile.floor(), tile.overlay());
|
||||
filter.apply(input);
|
||||
writeTiles[x + y*world.width()] = PackTile.get(input.block.id, input.floor.id, input.overlay.id);
|
||||
}
|
||||
@@ -173,7 +211,7 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
@Override
|
||||
public void draw(){
|
||||
super.draw();
|
||||
for(GenerateFilter filter : filters){
|
||||
for(var filter : filters){
|
||||
filter.draw(this);
|
||||
}
|
||||
}
|
||||
@@ -194,7 +232,7 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
}else{
|
||||
Core.scene.setScrollFocus(null);
|
||||
}
|
||||
}).grow().uniformX().get().setScrollingDisabled(true, false);
|
||||
}).grow().uniformX().scrollX(false);
|
||||
}).grow();
|
||||
|
||||
buffer1 = create();
|
||||
@@ -214,7 +252,7 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
filterTable.top().left();
|
||||
int i = 0;
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
for(var filter : filters){
|
||||
|
||||
//main container
|
||||
filterTable.table(Tex.pane, c -> {
|
||||
@@ -230,30 +268,44 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
t.add().growX();
|
||||
|
||||
ImageButtonStyle style = Styles.geni;
|
||||
t.defaults().size(42f);
|
||||
t.defaults().size(42f).padLeft(-5f);
|
||||
|
||||
t.button(Icon.refresh, style, () -> {
|
||||
filter.randomize();
|
||||
update();
|
||||
});
|
||||
}).padLeft(-16f).tooltip("@editor.randomize");
|
||||
|
||||
t.button(Icon.upOpen, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.max(0, idx - 1));
|
||||
if(filter != filters.first()){
|
||||
t.button(Icon.upOpen, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.max(0, idx - 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).tooltip("@editor.moveup");
|
||||
}
|
||||
|
||||
if(filter != filters.peek()){
|
||||
t.button(Icon.downOpen, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).tooltip("@editor.movedown");
|
||||
}
|
||||
|
||||
t.button(Icon.copy, style, () -> {
|
||||
GenerateFilter copy = filter.copy();
|
||||
copy.randomize();
|
||||
filters.insert(filters.indexOf(filter) + 1, copy);
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
t.button(Icon.downOpen, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
}).tooltip("@editor.copy");
|
||||
|
||||
t.button(Icon.cancel, style, () -> {
|
||||
filters.remove(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
}).tooltip("@waves.remove");
|
||||
}).growX();
|
||||
|
||||
c.row();
|
||||
@@ -272,7 +324,6 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
}).grow().left().pad(6).top();
|
||||
}).width(280f).pad(3).top().left().fillY();
|
||||
|
||||
|
||||
if(++i % cols == 0){
|
||||
filterTable.row();
|
||||
}
|
||||
@@ -284,30 +335,35 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
}
|
||||
|
||||
void showAdd(){
|
||||
BaseDialog selection = new BaseDialog("@add");
|
||||
selection.setFillParent(false);
|
||||
selection.cont.defaults().size(210f, 60f);
|
||||
int i = 0;
|
||||
for(Prov<GenerateFilter> gen : filterTypes){
|
||||
GenerateFilter filter = gen.get();
|
||||
var selection = new BaseDialog("@add");
|
||||
selection.cont.pane(p -> {
|
||||
p.background(Tex.button);
|
||||
p.marginRight(14);
|
||||
p.defaults().size(195f, 56f);
|
||||
int i = 0;
|
||||
for(var gen : Maps.allFilterTypes){
|
||||
var filter = gen.get();
|
||||
var icon = filter.icon();
|
||||
|
||||
if((filter.isPost() && applied)) continue;
|
||||
if(filter.isPost() && applied) continue;
|
||||
|
||||
selection.cont.button(filter.name(), () -> {
|
||||
filters.add(filter);
|
||||
p.button((icon == '\0' ? "" : icon + " ") + filter.name(), Styles.flatt, () -> {
|
||||
filter.randomize();
|
||||
filters.add(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left);
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
|
||||
p.button(Iconc.refresh + " " + Core.bundle.get("filter.defaultores"), Styles.flatt, () -> {
|
||||
maps.addDefaultOres(filters);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
});
|
||||
if(++i % 2 == 0) selection.cont.row();
|
||||
}
|
||||
|
||||
selection.cont.button("@filter.defaultores", () -> {
|
||||
maps.addDefaultOres(filters);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
});
|
||||
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left);
|
||||
}).scrollX(false);
|
||||
|
||||
selection.addCloseButton();
|
||||
selection.show();
|
||||
@@ -326,7 +382,10 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
|
||||
void apply(){
|
||||
if(result != null){
|
||||
result.get();
|
||||
//ignore errors yay
|
||||
try{
|
||||
result.get();
|
||||
}catch(Exception e){}
|
||||
}
|
||||
|
||||
buffer1 = null;
|
||||
@@ -348,31 +407,31 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
return;
|
||||
}
|
||||
|
||||
Seq<GenerateFilter> copy = new Seq<>(filters);
|
||||
var copy = filters.copy();
|
||||
|
||||
result = executor.submit(() -> {
|
||||
result = mainExecutor.submit(() -> {
|
||||
try{
|
||||
int w = pixmap.getWidth();
|
||||
int w = pixmap.width;
|
||||
world.setGenerating(true);
|
||||
generating = true;
|
||||
|
||||
if(!filters.isEmpty()){
|
||||
//write to buffer1 for reading
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
for(int px = 0; px < pixmap.width; px++){
|
||||
for(int py = 0; py < pixmap.height; py++){
|
||||
buffer1[px + py*w] = pack(editor.tile(px * scaling, py * scaling));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : copy){
|
||||
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)]));
|
||||
for(var filter : copy){
|
||||
input.begin(editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.width -1) + w* Mathf.clamp(y / scaling, 0, pixmap.height -1)]));
|
||||
|
||||
//read from buffer1 and write to buffer2
|
||||
pixmap.each((px, py) -> {
|
||||
int x = px * scaling, y = py * scaling;
|
||||
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)));
|
||||
input.set(x, y, content.block(PackTile.block(tile)), content.block(PackTile.floor(tile)), content.block(PackTile.overlay(tile)));
|
||||
filter.apply(input);
|
||||
buffer2[px + py * w] = PackTile.get(input.block.id, input.floor.id, input.overlay.id);
|
||||
});
|
||||
@@ -380,8 +439,8 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
pixmap.each((px, py) -> buffer1[px + py*w] = buffer2[px + py*w]);
|
||||
}
|
||||
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
for(int px = 0; px < pixmap.width; px++){
|
||||
for(int py = 0; py < pixmap.height; py++){
|
||||
int color;
|
||||
//get result from buffer1 if there's filters left, otherwise get from editor directly
|
||||
if(filters.isEmpty()){
|
||||
@@ -391,7 +450,7 @@ public class MapGenerateDialog extends BaseDialog{
|
||||
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);
|
||||
pixmap.set(px, pixmap.height - 1 - py, color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
public class MapInfoDialog extends BaseDialog{
|
||||
private final MapEditor editor;
|
||||
private final WaveInfoDialog waveInfo;
|
||||
private final MapGenerateDialog generate;
|
||||
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public MapInfoDialog(MapEditor editor){
|
||||
public class MapInfoDialog extends BaseDialog{
|
||||
private WaveInfoDialog waveInfo = new WaveInfoDialog();
|
||||
private MapGenerateDialog generate = new MapGenerateDialog(false);
|
||||
private CustomRulesDialog ruleInfo = new CustomRulesDialog();
|
||||
private MapObjectivesDialog objectives = new MapObjectivesDialog();
|
||||
private MapLocalesDialog locales = new MapLocalesDialog();
|
||||
private MapProcessorsDialog processors = new MapProcessorsDialog();
|
||||
|
||||
public MapInfoDialog(){
|
||||
super("@editor.mapinfo");
|
||||
this.editor = editor;
|
||||
this.waveInfo = new WaveInfoDialog(editor);
|
||||
this.generate = new MapGenerateDialog(editor, false);
|
||||
|
||||
addCloseButton();
|
||||
|
||||
@@ -37,7 +40,7 @@ public class MapInfoDialog extends BaseDialog{
|
||||
|
||||
TextField name = t.field(tags.get("name", ""), text -> {
|
||||
tags.put("name", text);
|
||||
}).size(400, 55f).addInputDialog(50).get();
|
||||
}).size(400, 55f).maxTextLength(50).get();
|
||||
name.setMessageText("@unknown");
|
||||
|
||||
t.row();
|
||||
@@ -45,38 +48,72 @@ public class MapInfoDialog extends BaseDialog{
|
||||
|
||||
TextArea description = t.area(tags.get("description", ""), Styles.areaField, text -> {
|
||||
tags.put("description", text);
|
||||
}).size(400f, 140f).addInputDialog(1000).get();
|
||||
}).size(400f, 140f).maxTextLength(1000).get();
|
||||
|
||||
t.row();
|
||||
t.add("@editor.author").padRight(8).left();
|
||||
|
||||
TextField author = t.field(tags.get("author", Core.settings.getString("mapAuthor", "")), text -> {
|
||||
TextField author = t.field(tags.get("author", ""), text -> {
|
||||
tags.put("author", text);
|
||||
Core.settings.put("mapAuthor", text);
|
||||
}).size(400, 55f).addInputDialog(50).get();
|
||||
}).size(400, 55f).maxTextLength(50).get();
|
||||
author.setMessageText("@unknown");
|
||||
|
||||
t.row();
|
||||
t.add("@editor.rules").padRight(8).left();
|
||||
t.button("@edit", () -> {
|
||||
ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules());
|
||||
hide();
|
||||
}).left().width(200f);
|
||||
|
||||
t.row();
|
||||
t.add("@editor.waves").padRight(8).left();
|
||||
t.button("@edit", () -> {
|
||||
waveInfo.show();
|
||||
hide();
|
||||
}).left().width(200f);
|
||||
t.table(Tex.button, r -> {
|
||||
r.defaults().width(230f).height(60f);
|
||||
|
||||
t.row();
|
||||
t.add("@editor.generation").padRight(8).left();
|
||||
t.button("@edit", () -> {
|
||||
generate.show(Vars.maps.readFilters(editor.tags.get("genfilters", "")),
|
||||
filters -> editor.tags.put("genfilters", JsonIO.write(filters)));
|
||||
hide();
|
||||
}).left().width(200f);
|
||||
var style = Styles.flatt;
|
||||
|
||||
r.button("@editor.rules", Icon.list, style, () -> {
|
||||
ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules());
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.button("@editor.waves", Icon.units, style, () -> {
|
||||
waveInfo.show();
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.row();
|
||||
|
||||
r.button("@editor.objectives", Icon.info, style, () -> {
|
||||
objectives.show(state.rules.objectives.all, state.rules.objectives.all::set);
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.button("@editor.generation", Icon.terrain, style, () -> {
|
||||
//randomize so they're not all the same seed
|
||||
var res = maps.readFilters(editor.tags.get("genfilters", ""));
|
||||
res.each(GenerateFilter::randomize);
|
||||
|
||||
generate.show(res,
|
||||
filters -> {
|
||||
//reset seed to 0 so it is not written
|
||||
filters.each(f -> f.seed = 0);
|
||||
editor.tags.put("genfilters", JsonIO.write(filters));
|
||||
});
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.row();
|
||||
|
||||
r.button("@editor.locales", Icon.fileText, style, () -> {
|
||||
try{
|
||||
MapLocales res = JsonIO.read(MapLocales.class, editor.tags.get("locales", "{}"));
|
||||
locales.show(res);
|
||||
}catch(Throwable e){
|
||||
locales.show(new MapLocales());
|
||||
ui.showException(e);
|
||||
}
|
||||
hide();
|
||||
}).marginLeft(10f);
|
||||
|
||||
r.button("@editor.worldprocessors", Icon.logic, style, () -> {
|
||||
hide();
|
||||
processors.show();
|
||||
}).marginLeft(10f);
|
||||
}).colspan(2).center();
|
||||
|
||||
name.change();
|
||||
description.change();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
@@ -11,66 +13,60 @@ import mindustry.ui.dialogs.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapLoadDialog extends BaseDialog{
|
||||
private Map selected = null;
|
||||
private @Nullable Map selected = null;
|
||||
|
||||
public MapLoadDialog(Cons<Map> loader){
|
||||
super("@editor.loadmap");
|
||||
|
||||
shown(this::rebuild);
|
||||
hidden(() -> selected = null);
|
||||
onResize(this::rebuild);
|
||||
|
||||
TextButton button = new TextButton("@load");
|
||||
button.setDisabled(() -> selected == null);
|
||||
button.clicked(() -> {
|
||||
buttons.defaults().size(210f, 64f);
|
||||
buttons.button("@cancel", Icon.cancel, this::hide);
|
||||
buttons.button("@load", Icon.ok, () -> {
|
||||
if(selected != null){
|
||||
loader.get(selected);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.button("@cancel", this::hide);
|
||||
buttons.add(button);
|
||||
}).disabled(b -> selected == null);
|
||||
addCloseListener();
|
||||
makeButtonOverlay();
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
cont.clear();
|
||||
if(maps.all().size > 0){
|
||||
selected = maps.all().first();
|
||||
}
|
||||
|
||||
ButtonGroup<TextButton> group = new ButtonGroup<>();
|
||||
|
||||
int maxcol = 3;
|
||||
ButtonGroup<Button> group = new ButtonGroup<>();
|
||||
|
||||
int i = 0;
|
||||
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(300f)), 1);
|
||||
|
||||
Table table = new Table();
|
||||
table.defaults().size(200f, 90f).pad(4f);
|
||||
table.defaults().size(250f, 90f).pad(4f);
|
||||
table.margin(10f);
|
||||
|
||||
ScrollPane pane = new ScrollPane(table, Styles.horizontalPane);
|
||||
ScrollPane pane = new ScrollPane(table);
|
||||
pane.setFadeScrollBars(false);
|
||||
pane.setScrollingDisabledX(true);
|
||||
|
||||
for(Map map : maps.all()){
|
||||
table.button(b -> {
|
||||
b.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).padLeft(5f).size(16 * 4f);
|
||||
b.add(map.name()).wrap().grow().labelAlign(Align.center).padLeft(5f);
|
||||
}, Styles.squareTogglet, () -> selected = map).group(group).margin(8f).checked(b -> selected == map);
|
||||
|
||||
TextButton button = new TextButton(map.name(), Styles.togglet);
|
||||
button.add(new BorderImage(map.safeTexture(), 2f).setScaling(Scaling.fit)).size(16 * 4f);
|
||||
button.getCells().reverse();
|
||||
button.clicked(() -> selected = map);
|
||||
button.getLabelCell().grow().left().padLeft(5f);
|
||||
group.add(button);
|
||||
table.add(button);
|
||||
if(++i % maxcol == 0) table.row();
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
|
||||
if(maps.all().size == 0){
|
||||
group.uncheckAll();
|
||||
|
||||
if(maps.all().isEmpty()){
|
||||
table.add("@maps.none").center();
|
||||
}else{
|
||||
cont.add("@editor.loadmap");
|
||||
cont.add("@editor.selectmap");
|
||||
}
|
||||
|
||||
cont.row();
|
||||
cont.add(pane);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,774 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.Core;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.TextButton.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.scene.utils.*;
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapLocalesDialog extends BaseDialog{
|
||||
/** Width of UI property card. */
|
||||
private static final float cardWidth = 400f;
|
||||
/** Style for filter options buttons */
|
||||
private static final TextButtonStyle filterStyle = new TextButtonStyle(){{
|
||||
up = down = checked = over = Tex.whitePane;
|
||||
font = Fonts.outline;
|
||||
fontColor = Color.lightGray;
|
||||
overFontColor = Pal.accent;
|
||||
disabledFontColor = Color.gray;
|
||||
disabled = Styles.black;
|
||||
}};
|
||||
/** Icons for use in map locales dialog. */
|
||||
private static final ContentType[] contentIcons = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit};
|
||||
|
||||
private MapLocales locales;
|
||||
private MapLocales lastSaved;
|
||||
private boolean saved = true;
|
||||
private Table langs;
|
||||
private Table main;
|
||||
private Table propView;
|
||||
private String selectedLocale;
|
||||
|
||||
private boolean applytoall = true;
|
||||
private boolean collapsed = false;
|
||||
private String searchString = "";
|
||||
private boolean searchByValue = false;
|
||||
private boolean showCorrect = true;
|
||||
private boolean showMissing = true;
|
||||
private boolean showSame = true;
|
||||
|
||||
public MapLocalesDialog(){
|
||||
super("@editor.locales");
|
||||
|
||||
selectedLocale = MapLocales.currentLocale();
|
||||
|
||||
langs = new Table(Tex.button);
|
||||
main = new Table();
|
||||
propView = new Table();
|
||||
|
||||
buttons.add("").uniform();
|
||||
|
||||
buttons.table(t -> {
|
||||
t.defaults().pad(3).center();
|
||||
|
||||
t.button("@back", Icon.left, () -> {
|
||||
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
});
|
||||
hide();
|
||||
}).size(210f, 64f);
|
||||
closeOnBack(() -> {
|
||||
if(!saved) ui.showConfirm("@editor.locales", "@editor.savechanges", () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
});
|
||||
});
|
||||
|
||||
t.button("@editor.apply", Icon.ok, () -> {
|
||||
editor.tags.put("locales", JsonIO.write(locales));
|
||||
state.mapLocales = locales;
|
||||
lastSaved = locales.copy();
|
||||
saved = true;
|
||||
}).size(210f, 64f).disabled(b -> saved);
|
||||
|
||||
t.button("@edit", Icon.edit, this::editDialog).size(210f, 64f);
|
||||
}).growX();
|
||||
|
||||
resized(this::buildMain);
|
||||
|
||||
buttons.button("?", () -> ui.showInfo("@locales.info")).size(60f, 64f).uniform();
|
||||
|
||||
shown(this::setup);
|
||||
}
|
||||
|
||||
public void show(MapLocales locales){
|
||||
this.locales = locales;
|
||||
lastSaved = locales.copy();
|
||||
saved = true;
|
||||
show();
|
||||
}
|
||||
|
||||
private void setup(){
|
||||
cont.clear();
|
||||
|
||||
buildTables();
|
||||
|
||||
cont.add(langs).left();
|
||||
|
||||
cont.table(t -> {
|
||||
// search/collapse all/filter
|
||||
t.table(a -> {
|
||||
a.button(Icon.downOpen, Styles.emptyTogglei, () -> {
|
||||
collapsed = !collapsed;
|
||||
buildMain();
|
||||
}).update(b -> {
|
||||
b.replaceImage(new Image(collapsed ? Icon.upOpen : Icon.downOpen));
|
||||
b.setChecked(collapsed);
|
||||
}).size(35f);
|
||||
|
||||
a.button(Icon.filter, Styles.emptyi, () -> filterDialog(this::buildMain)).padLeft(10f).size(35f);
|
||||
|
||||
var field = a.field("", v -> {
|
||||
searchString = v;
|
||||
buildMain();
|
||||
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue": "@locales.searchname")).get();
|
||||
|
||||
a.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
searchString = "";
|
||||
field.setText("");
|
||||
buildMain();
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
t.check("@locales.applytoall", applytoall, b -> applytoall = b).pad(10f).row();
|
||||
|
||||
t.add(main).center().grow().row();
|
||||
}).pad(10f).grow();
|
||||
|
||||
// property addition
|
||||
cont.table(Tex.button, t -> {
|
||||
TextField name = t.field("name", s -> {}).maxTextLength(64).fillX().padTop(10f).get();
|
||||
t.row();
|
||||
TextField value = t.area("text", s -> {}).maxTextLength(1000).fillX().height(140f).get();
|
||||
t.row();
|
||||
|
||||
t.button("@add", Icon.add, () -> {
|
||||
if(applytoall){
|
||||
for(var locale : locales.values()){
|
||||
locale.put(name.getText(), value.getText());
|
||||
}
|
||||
}else{
|
||||
locales.get(selectedLocale).put(name.getText(), value.getText());
|
||||
}
|
||||
|
||||
saved = false;
|
||||
buildMain();
|
||||
}).padTop(10f).size(cardWidth, 50f).fillX().row();
|
||||
}).right();
|
||||
}
|
||||
|
||||
private void buildTables(){
|
||||
if(!locales.containsKey(selectedLocale)){
|
||||
locales.put(selectedLocale, new StringMap());
|
||||
}
|
||||
|
||||
buildLocalesTable();
|
||||
buildMain();
|
||||
}
|
||||
|
||||
private void buildLocalesTable(){
|
||||
langs.clear();
|
||||
|
||||
langs.pane(p -> {
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
|
||||
if(locales.containsKey(name)){
|
||||
p.button(loc.getDisplayName(Core.bundle.getLocale()), Styles.flatTogglet, () -> {
|
||||
if(name.equals(selectedLocale)) return;
|
||||
|
||||
selectedLocale = name;
|
||||
buildTables();
|
||||
}).update(b -> b.setChecked(selectedLocale.equals(name))).width(200f).minHeight(50f);
|
||||
p.button(Icon.edit, Styles.flati, () -> localeEditDialog(name)).size(50f);
|
||||
p.button(Icon.trash, Styles.flati, () -> ui.showConfirm("@confirm", "@locales.deletelocale", () -> {
|
||||
locales.remove(name);
|
||||
|
||||
selectedLocale = (locales.size != 0 ? locales.keys().next() : Core.settings.getString("locale"));
|
||||
saved = false;
|
||||
buildTables();
|
||||
})).size(50f).row();
|
||||
}
|
||||
}
|
||||
}).row();
|
||||
langs.button("@add", Icon.add, this::addLocaleDialog).padTop(10f).width(250f);
|
||||
}
|
||||
|
||||
private void buildMain(){
|
||||
main.clear();
|
||||
|
||||
StringMap props = locales.get(selectedLocale);
|
||||
|
||||
main.image().color(Pal.gray).height(3f).growX().expandY().top().row();
|
||||
main.pane(p -> {
|
||||
int cols = Math.max(1, (int)((Core.graphics.getWidth() / Scl.scl() - 410f) / cardWidth) - 1);
|
||||
if(props.size == 0){
|
||||
main.add("@empty").center().row();
|
||||
return;
|
||||
}
|
||||
p.defaults().top();
|
||||
|
||||
Table[] colTables = new Table[cols];
|
||||
for(var i = 0; i < cols; i++){
|
||||
colTables[i] = new Table();
|
||||
}
|
||||
int i = 0;
|
||||
|
||||
// To sort properties in alphabetic order
|
||||
Seq<String> keys = props.keys().toSeq().sort();
|
||||
|
||||
for(var key : keys){
|
||||
var comparsionString = (searchByValue ? props.get(key).toLowerCase() : key.toLowerCase());
|
||||
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
|
||||
|
||||
PropertyStatus status = getPropertyStatus(key, props.get(key), selectedLocale, false);
|
||||
if(status == PropertyStatus.correct && !showCorrect) continue;
|
||||
if(status == PropertyStatus.missing && !showMissing) continue;
|
||||
if(status == PropertyStatus.same && !showSame) continue;
|
||||
|
||||
colTables[i].table(Tex.whitePane, t -> {
|
||||
boolean[] shown = {!collapsed};
|
||||
String[] propKey = {key};
|
||||
String[] propValue = {props.get(key)};
|
||||
|
||||
// collapse button
|
||||
t.button(Icon.downOpen, Styles.emptyTogglei, () -> shown[0] = !shown[0]).update(b -> {
|
||||
b.replaceImage(new Image(shown[0] ? Icon.upOpen : Icon.downOpen));
|
||||
b.setChecked(shown[0]);
|
||||
}).size(35f);
|
||||
|
||||
// property name field
|
||||
t.field(propKey[0], (f, c) -> c != '=' && c != ':', v -> {
|
||||
if(props.containsKey(v)){
|
||||
t.setColor(Color.valueOf("f25555"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(applytoall){
|
||||
for(var bundle : locales.values()){
|
||||
if(!bundle.containsKey(v)){
|
||||
String value = bundle.get(propKey[0]);
|
||||
if(value == null) continue;
|
||||
|
||||
bundle.remove(propKey[0]);
|
||||
bundle.put(v, value);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(!props.containsKey(v)){
|
||||
props.remove(propKey[0]);
|
||||
props.put(v, propValue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
propKey[0] = v;
|
||||
updateCard(t, v, propValue[0]);
|
||||
saved = false;
|
||||
}).maxTextLength(64).width(cardWidth - 125f);
|
||||
|
||||
// remove button
|
||||
t.button(Icon.trash, Styles.emptyi, () -> {
|
||||
if(applytoall){
|
||||
for(var bundle : locales.values()){
|
||||
bundle.remove(propKey[0]);
|
||||
}
|
||||
}else{
|
||||
props.remove(propKey[0]);
|
||||
}
|
||||
saved = false;
|
||||
buildMain();
|
||||
}).size(35f);
|
||||
|
||||
// more actions
|
||||
t.button(Icon.edit, Styles.emptyi, () -> propEditDialog(t, propKey[0], propValue[0])).size(35f).row();
|
||||
|
||||
// property value area
|
||||
t.collapser(c -> c.area(propValue[0], v -> {
|
||||
props.put(propKey[0], v);
|
||||
updateCard(t, propKey[0], v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).update(a -> {
|
||||
propValue[0] = props.get(propKey[0]);
|
||||
a.setText(props.get(propKey[0]));
|
||||
}).growX(), () -> shown[0]).colspan(4).growX();
|
||||
|
||||
updateCard(t, propKey[0], propValue[0]);
|
||||
}).top().width(cardWidth).pad(5f).row();
|
||||
|
||||
i = ++i % cols;
|
||||
}
|
||||
|
||||
if(!colTables[0].hasChildren()){
|
||||
main.add("@empty").center().row();
|
||||
}else{
|
||||
p.add(colTables);
|
||||
}
|
||||
}).growX().row();
|
||||
main.image().color(Pal.gray).height(3f).growX().expandY().bottom().row();
|
||||
}
|
||||
|
||||
private void updateCard(Table table, String propKey, String propValue){
|
||||
updateCard(table, propKey, propValue, selectedLocale, false);
|
||||
}
|
||||
|
||||
private void updateCard(Table table, String propKey, String propValue, String locale, boolean viewCard){
|
||||
switch(getPropertyStatus(propKey, propValue, locale, viewCard)){
|
||||
case missing -> table.setColor(Pal.accent);
|
||||
case same -> table.setColor(Pal.techBlue);
|
||||
case correct -> table.setColor(Pal.gray);
|
||||
}
|
||||
}
|
||||
|
||||
// Property statuses for main dialog and property view dialog are a bit different
|
||||
private PropertyStatus getPropertyStatus(String propKey, String propValue, String locale, boolean forView){
|
||||
if(forView && propValue == null) return PropertyStatus.missing;
|
||||
|
||||
for(var bundle : locales.entries()){
|
||||
if(!forView && bundle.key.equals(selectedLocale)) continue;
|
||||
if(forView && bundle.key.equals(locale)) continue;
|
||||
|
||||
StringMap props = bundle.value;
|
||||
|
||||
if(!props.containsKey(propKey)){
|
||||
if(!forView) return PropertyStatus.missing;
|
||||
}else{
|
||||
if(props.get(propKey).equals(propValue)){
|
||||
return PropertyStatus.same;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PropertyStatus.correct;
|
||||
}
|
||||
|
||||
private void addLocaleDialog(){
|
||||
BaseDialog dialog = new BaseDialog("@add");
|
||||
|
||||
dialog.cont.pane(t -> {
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
|
||||
if(!locales.containsKey(name)){
|
||||
t.button(loc.getDisplayName(Core.bundle.getLocale()), Styles.flatTogglet, () -> {
|
||||
if(name.equals(selectedLocale)) return;
|
||||
|
||||
locales.put(name, new StringMap());
|
||||
|
||||
selectedLocale = name;
|
||||
saved = false;
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).update(b -> b.setChecked(selectedLocale.equals(name))).size(400f, 50f).row();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void propEditDialog(Table card, String key, String value){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(450f, 60f).left();
|
||||
|
||||
t.button("@locales.addtoother", Icon.add, Styles.flatt, () -> {
|
||||
for(var bundle : locales.values()){
|
||||
if(!bundle.containsKey(key)){
|
||||
bundle.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
saved = false;
|
||||
updateCard(card, key, value);
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.viewproperty", Icon.zoom, Styles.flatt, () -> {
|
||||
viewPropertyDialog(key);
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.addicon", Icon.image, Styles.flatt, () -> {
|
||||
addIconDialog(res -> {
|
||||
locales.get(selectedLocale).put(key, value + res);
|
||||
saved = false;
|
||||
});
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
|
||||
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
|
||||
locales.get(selectedLocale).put(key, lastSaved.get(selectedLocale).get(key));
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).disabled(b -> {
|
||||
if(!lastSaved.containsKey(selectedLocale)) return true;
|
||||
StringMap savedMap = lastSaved.get(selectedLocale);
|
||||
return !savedMap.containsKey(key) || savedMap.get(key).equals(locales.get(selectedLocale).get(key));
|
||||
}).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void localeEditDialog(String locale){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(350f, 60f).left();
|
||||
|
||||
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
|
||||
Core.app.setClipboardText(writeLocale(locale));
|
||||
ui.showInfoFade("@copied");
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
|
||||
locales.put(locale, readLocale(Core.app.getClipboardText()));
|
||||
buildTables();
|
||||
saved = false;
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void editDialog(){
|
||||
BaseDialog dialog = new BaseDialog("@edit");
|
||||
|
||||
dialog.cont.pane(p -> {
|
||||
p.margin(10f);
|
||||
p.table(Tex.button, t -> {
|
||||
t.defaults().size(450f, 60f).left();
|
||||
|
||||
t.button("@waves.copy", Icon.copy, Styles.flatt, () -> {
|
||||
Core.app.setClipboardText(writeBundles());
|
||||
ui.showInfoFade("@copied");
|
||||
dialog.hide();
|
||||
}).marginLeft(12f).row();
|
||||
t.button("@waves.load", Icon.download, Styles.flatt, () -> {
|
||||
locales = readBundles(Core.app.getClipboardText());
|
||||
buildTables();
|
||||
saved = false;
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null).marginLeft(12f).row();
|
||||
t.button("@locales.rollback", Icon.undo, Styles.flatt, () -> {
|
||||
locales = lastSaved.copy();
|
||||
saved = true;
|
||||
buildTables();
|
||||
dialog.hide();
|
||||
}).disabled(b -> saved).marginLeft(12f).row();
|
||||
});
|
||||
});
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void viewPropertyDialog(String key){
|
||||
BaseDialog dialog = new BaseDialog(Core.bundle.format("locales.viewing", key));
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.button(Icon.filter, Styles.emptyi, () -> filterDialog(() -> buildPropView(key))).size(35f);
|
||||
|
||||
var field = t.field(searchString, v -> {
|
||||
searchString = v;
|
||||
buildPropView(key);
|
||||
}).update(f -> f.setText(searchString)).maxTextLength(64).padLeft(10f).width(250f).update(f -> f.setMessageText(searchByValue ? "@locales.searchvalue" : "@locales.searchlocale")).get();
|
||||
|
||||
t.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
searchString = "";
|
||||
field.setText("");
|
||||
buildPropView(key);
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
buildPropView(key);
|
||||
dialog.cont.add(propView).grow().center().row();
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.closeOnBack();
|
||||
dialog.hidden(this::buildMain);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void buildPropView(String key){
|
||||
propView.clear();
|
||||
|
||||
propView.image().color(Pal.gray).height(3f).fillX().top().row();
|
||||
propView.pane(p -> {
|
||||
int cols = Math.max(1, (int)((Core.graphics.getWidth() / Scl.scl() - 100f) / cardWidth));
|
||||
if(cols == 0){
|
||||
propView.add("@empty").center().row();
|
||||
return;
|
||||
}
|
||||
p.defaults().top();
|
||||
|
||||
Table[] colTables = new Table[cols];
|
||||
for(var i = 0; i < cols; i++){
|
||||
colTables[i] = new Table();
|
||||
}
|
||||
int i = 0;
|
||||
|
||||
for(var loc : Vars.locales){
|
||||
String name = loc.toString();
|
||||
if(!locales.containsKey(name)) continue;
|
||||
|
||||
PropertyStatus status = getPropertyStatus(key, locales.get(name).get(key), name, true);
|
||||
if(status == PropertyStatus.correct && !showCorrect) continue;
|
||||
if(status == PropertyStatus.missing && !showMissing) continue;
|
||||
if(status == PropertyStatus.same && !showSame) continue;
|
||||
|
||||
if(status != PropertyStatus.missing){
|
||||
var comparsionString = (searchByValue ? locales.get(name).get(key).toLowerCase() : loc.getDisplayName(Core.bundle.getLocale()).toLowerCase());
|
||||
if(!searchString.isEmpty() && !comparsionString.contains(searchString.toLowerCase())) continue;
|
||||
}
|
||||
|
||||
colTables[i].table(Tex.whitePane, t -> {
|
||||
t.add(loc.getDisplayName(Core.bundle.getLocale())).left().color(Pal.accent).row();
|
||||
t.image().color(Pal.accent).fillX().row();
|
||||
|
||||
if(status == PropertyStatus.missing){
|
||||
t.table(b ->
|
||||
b.button("@add", Icon.add, () -> {
|
||||
locales.get(name).put(key, "moai");
|
||||
|
||||
t.getCells().get(2).clearElement();
|
||||
t.getCells().remove(2);
|
||||
|
||||
t.area(locales.get(name).get(key), v -> {
|
||||
locales.get(name).put(key, v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).growX().row();
|
||||
}).size(160f, 50f)).height(140f).growX().row();
|
||||
}else{
|
||||
t.area(locales.get(name).get(key), v -> {
|
||||
locales.get(name).put(key, v);
|
||||
saved = false;
|
||||
}).maxTextLength(1000).height(140f).growX().row();
|
||||
}
|
||||
}).update(t -> updateCard(t, key, locales.get(name).get(key), name, true)).top().width(cardWidth).pad(5f).row();
|
||||
|
||||
i = ++i % cols;
|
||||
}
|
||||
|
||||
if(!colTables[0].hasChildren()){
|
||||
propView.add("@empty").center().row();
|
||||
}else{
|
||||
p.add(colTables);
|
||||
}
|
||||
}).grow().row();
|
||||
propView.image().color(Pal.gray).height(3f).fillX().bottom().row();
|
||||
}
|
||||
|
||||
private void filterDialog(Runnable hidden){
|
||||
BaseDialog dialog = new BaseDialog("@locales.filter");
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.add("@search").row();
|
||||
t.table(b -> {
|
||||
b.button("@locales.byname", Styles.togglet, () -> searchByValue = false).size(300f, 50f).checked(v -> !searchByValue);
|
||||
b.button("@locales.byvalue", Styles.togglet, () -> searchByValue = true).padLeft(10f).size(300f, 50f).checked(v -> searchByValue);
|
||||
}).padTop(5f);
|
||||
}).row();
|
||||
|
||||
dialog.cont.button("@locales.showcorrect", Icon.ok, filterStyle, () -> showCorrect = !showCorrect).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showCorrect ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showCorrect);
|
||||
}).size(450f, 100f).color(Pal.gray).padTop(65f);
|
||||
|
||||
dialog.cont.row();
|
||||
|
||||
dialog.cont.button("@locales.showmissing", Icon.ok, filterStyle, () -> showMissing = !showMissing).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showMissing ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showMissing);
|
||||
}).size(450f, 100f).color(Pal.accent).padTop(65f);
|
||||
|
||||
dialog.cont.row();
|
||||
|
||||
dialog.cont.button("@locales.showsame", Icon.ok, filterStyle, () -> showSame = !showSame).update(b -> {
|
||||
((Image)b.getChildren().get(1)).setDrawable(showSame ? Icon.ok : Icon.cancel);
|
||||
b.setChecked(showSame);
|
||||
}).size(450f, 100f).color(Pal.techBlue).padTop(65f);
|
||||
|
||||
dialog.buttons.button("@back", Icon.left, () -> {
|
||||
hidden.run();
|
||||
dialog.hide();
|
||||
}).size(210f, 64f);
|
||||
dialog.closeOnBack(hidden);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void addIconDialog(Cons<String> cons){
|
||||
BaseDialog dialog = new BaseDialog("@locales.addicon");
|
||||
|
||||
Table icons = new Table();
|
||||
TextField search = Elem.newField("", v -> iconsTable(icons, v.replace(" ", "").toLowerCase(), dialog, cons));
|
||||
search.setMessageText("@search");
|
||||
|
||||
dialog.cont.table(t -> {
|
||||
t.add(search).maxTextLength(64).padLeft(10f).width(250f);
|
||||
|
||||
t.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
search.setText("");
|
||||
iconsTable(icons, "", dialog, cons);
|
||||
}).padLeft(10f).size(35f);
|
||||
}).row();
|
||||
|
||||
dialog.cont.pane(icons).scrollX(false);
|
||||
dialog.resized(true, () -> iconsTable(icons, search.getText().replace(" ", "").toLowerCase(), dialog, cons));
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.closeOnBack();
|
||||
dialog.setFillParent(true);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void iconsTable(Table table, String search, Dialog dialog, Cons<String> cons){
|
||||
table.clear();
|
||||
|
||||
table.marginRight(19f).marginLeft(12f);
|
||||
table.defaults().size(48f);
|
||||
|
||||
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
|
||||
|
||||
int i = 0;
|
||||
|
||||
var codes = new ObjectIntMap<>(Iconc.codes);
|
||||
|
||||
for(var name : codes.keys()){
|
||||
if(!name.toLowerCase().contains(search)) codes.remove(name);
|
||||
}
|
||||
|
||||
if(codes.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
for(var icon : codes){
|
||||
String res = (char)icon.value + "";
|
||||
|
||||
table.button(Icon.icons.get(icon.key), Styles.flati, iconMed, () -> {
|
||||
cons.get(res);
|
||||
dialog.hide();
|
||||
}).tooltip(icon.key);
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
|
||||
for(ContentType ctype : contentIcons){
|
||||
var all = content.getBy(ctype).<UnlockableContent>as().select(u -> u.localizedName.replace(" ", "").toLowerCase().contains(search) && u.uiIcon.found());
|
||||
|
||||
table.row();
|
||||
if(all.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
i = 0;
|
||||
for(UnlockableContent u : all){
|
||||
table.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
|
||||
cons.get(u.emoji() + "");
|
||||
dialog.hide();
|
||||
}).tooltip(u.localizedName);
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
}
|
||||
|
||||
var teams = new Seq<>(Team.baseTeams);
|
||||
teams = teams.select(u -> u.localized().toLowerCase().contains(search) && Core.atlas.has("team-" + u.name));
|
||||
|
||||
table.row();
|
||||
if(teams.size > 0) table.image().colspan(cols).growX().width(-1f).height(3f).color(Pal.accent).row();
|
||||
|
||||
for(Team team : teams){
|
||||
var region = Core.atlas.find("team-" + team.name);
|
||||
|
||||
table.button(new TextureRegionDrawable(region), Styles.flati, iconMed, () -> {
|
||||
cons.get(team.emoji);
|
||||
dialog.hide();
|
||||
}).tooltip(team.localized());
|
||||
|
||||
if(++i % cols == 0) table.row();
|
||||
}
|
||||
}
|
||||
|
||||
private String writeBundles(){
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
for(var locale : locales.keys()){
|
||||
data.append(locale).append(":\n").append(writeLocale(locale));
|
||||
}
|
||||
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
private String writeLocale(String key){
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
if(!locales.containsKey(key)) return "";
|
||||
|
||||
for(var prop : locales.get(key).entries()){
|
||||
// Convert \n in plain text to \\n, then convert newlines to \n
|
||||
data.append(prop.key).append(" = ").append(prop.value
|
||||
.replace("\\n", "\\\\n").replace("\n", "\\n")).append("\n");
|
||||
}
|
||||
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
private MapLocales readBundles(String data){
|
||||
MapLocales bundles = new MapLocales();
|
||||
|
||||
String currentLocale = "";
|
||||
|
||||
for(var line : data.split("\\r?\\n|\\r")){
|
||||
if(line.endsWith(":") && !line.contains("=")){
|
||||
currentLocale = line.substring(0, line.length() - 1);
|
||||
bundles.put(currentLocale, new StringMap());
|
||||
}else{
|
||||
int sepIndex = line.indexOf(" = ");
|
||||
if(sepIndex != -1 && !currentLocale.isEmpty()){
|
||||
// Convert \n in file to newlines in text, then revert newlines with escape characters
|
||||
bundles.get(currentLocale).put(line.substring(0, sepIndex), line.substring(sepIndex + 3)
|
||||
.replace("\\n", "\n").replace("\\\n", "\\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
private StringMap readLocale(String data){
|
||||
StringMap map = new StringMap();
|
||||
|
||||
for(var line : data.split("\\r?\\n|\\r")){
|
||||
int sepIndex = line.indexOf(" = ");
|
||||
if(sepIndex != -1){
|
||||
// Convert \n in file to newlines in text, then revert newlines with escape characters
|
||||
map.put(line.substring(0, sepIndex), line.substring(sepIndex + 3)
|
||||
.replace("\\n", "\n").replace("\\\n", "\\n"));
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private enum PropertyStatus{
|
||||
correct,
|
||||
missing,
|
||||
same
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.editor.MapObjectivesCanvas.ObjectiveTilemap.ObjectiveTile.*;
|
||||
import mindustry.editor.MapObjectivesDialog.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class MapObjectivesCanvas extends WidgetGroup{
|
||||
public static final int
|
||||
objWidth = 5, objHeight = 2,
|
||||
bounds = 100;
|
||||
|
||||
public final float unitSize = Scl.scl(48f);
|
||||
|
||||
public Seq<MapObjective> objectives = new Seq<>();
|
||||
public ObjectiveTilemap tilemap;
|
||||
|
||||
protected MapObjective query;
|
||||
|
||||
private boolean pressed;
|
||||
private long visualPressed;
|
||||
private int queryX = -objWidth, queryY = -objHeight;
|
||||
|
||||
public MapObjectivesCanvas(){
|
||||
setFillParent(true);
|
||||
addChild(tilemap = new ObjectiveTilemap());
|
||||
|
||||
addCaptureListener(new InputListener(){
|
||||
@Override
|
||||
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(query != null && button == KeyCode.mouseRight){
|
||||
stopQuery();
|
||||
|
||||
event.stop();
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addCaptureListener(new ElementGestureListener(){
|
||||
int pressPointer = -1;
|
||||
|
||||
@Override
|
||||
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
|
||||
if(tilemap.moving != null || tilemap.connecting != null) return;
|
||||
tilemap.x = Mathf.clamp(tilemap.x + deltaX, -bounds * unitSize + width, bounds * unitSize);
|
||||
tilemap.y = Mathf.clamp(tilemap.y + deltaY, -bounds * unitSize + height, bounds * unitSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tap(InputEvent event, float x, float y, int count, KeyCode button){
|
||||
if(query == null) return;
|
||||
|
||||
Vec2 pos = localToDescendantCoordinates(tilemap, Tmp.v1.set(x, y));
|
||||
queryX = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
|
||||
queryY = Mathf.floor((pos.y - unitSize) / unitSize);
|
||||
|
||||
// In mobile, placing the query is done in a separate button.
|
||||
if(!mobile) placeQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(pressPointer != -1) return;
|
||||
pressPointer = pointer;
|
||||
pressed = true;
|
||||
visualPressed = Time.millis() + 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(pointer == pressPointer){
|
||||
pressPointer = -1;
|
||||
pressed = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void clearObjectives(){
|
||||
stopQuery();
|
||||
tilemap.clearTiles();
|
||||
tilemap.x = 0f;
|
||||
tilemap.y = 0f;
|
||||
}
|
||||
|
||||
protected void stopQuery(){
|
||||
if(query == null) return;
|
||||
query = null;
|
||||
|
||||
Core.graphics.restoreCursor();
|
||||
}
|
||||
|
||||
public void query(MapObjective obj){
|
||||
stopQuery();
|
||||
query = obj;
|
||||
}
|
||||
|
||||
public void placeQuery(){
|
||||
if(isQuerying() && tilemap.createTile(queryX, queryY, query)){
|
||||
objectives.add(query);
|
||||
stopQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isQuerying(){
|
||||
return query != null;
|
||||
}
|
||||
|
||||
public boolean isVisualPressed(){
|
||||
return pressed || visualPressed > Time.millis();
|
||||
}
|
||||
|
||||
public class ObjectiveTilemap extends WidgetGroup{
|
||||
|
||||
/** The connector button that is being pressed. */
|
||||
protected @Nullable Connector connecting;
|
||||
/** The current tile that is being moved. */
|
||||
protected @Nullable ObjectiveTile moving;
|
||||
|
||||
public ObjectiveTilemap(){
|
||||
setTransform(false);
|
||||
setSize(getPrefWidth(), getPrefHeight());
|
||||
touchable(() -> isQuerying() ? Touchable.disabled : Touchable.childrenOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
validate();
|
||||
int minX = Math.max(Mathf.floor((x - width - 1f) / unitSize), -bounds), minY = Math.max(Mathf.floor((y - height - 1f) / unitSize), -bounds),
|
||||
maxX = Math.min(Mathf.ceil((x + width + 1f) / unitSize), bounds), maxY = Math.min(Mathf.ceil((y + height + 1f) / unitSize), bounds);
|
||||
float progX = x % unitSize, progY = y % unitSize;
|
||||
|
||||
Lines.stroke(3f);
|
||||
Draw.color(Pal.darkestGray, parentAlpha);
|
||||
|
||||
for(int x = minX; x <= maxX; x++) Lines.line(progX + x * unitSize, minY * unitSize, progX + x * unitSize, maxY * unitSize);
|
||||
for(int y = minY; y <= maxY; y++) Lines.line(minX * unitSize, progY + y * unitSize, maxX * unitSize, progY + y * unitSize);
|
||||
|
||||
if(isQuerying()){
|
||||
int tx, ty;
|
||||
if(mobile){
|
||||
tx = queryX;
|
||||
ty = queryY;
|
||||
}else{
|
||||
Vec2 pos = screenToLocalCoordinates(Core.input.mouse());
|
||||
tx = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
|
||||
ty = Mathf.floor((pos.y - unitSize) / unitSize);
|
||||
}
|
||||
|
||||
Lines.stroke(4f);
|
||||
Draw.color(
|
||||
isVisualPressed() ? Pal.metalGrayDark : validPlace(tx, ty, null) ? Pal.accent : Pal.remove,
|
||||
parentAlpha
|
||||
);
|
||||
|
||||
Lines.rect(x + tx * unitSize, y + ty * unitSize, objWidth * unitSize, objHeight * unitSize);
|
||||
}
|
||||
|
||||
if(moving != null){
|
||||
int tx, ty;
|
||||
float x = this.x + (tx = Mathf.round(moving.x / unitSize)) * unitSize;
|
||||
float y = this.y + (ty = Mathf.round(moving.y / unitSize)) * unitSize;
|
||||
|
||||
Draw.color(
|
||||
validPlace(tx, ty, moving) ? Pal.accent : Pal.remove,
|
||||
0.5f * parentAlpha
|
||||
);
|
||||
|
||||
Fill.crect(x, y, objWidth * unitSize, objHeight * unitSize);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
super.draw();
|
||||
|
||||
Draw.reset();
|
||||
Seq<ObjectiveTile> tiles = getChildren().as();
|
||||
|
||||
Connector conTarget = null;
|
||||
if(connecting != null){
|
||||
Vec2 pos = connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY));
|
||||
if(hit(pos.x, pos.y, true) instanceof Connector con && connecting.canConnectTo(con)) conTarget = con;
|
||||
}
|
||||
|
||||
boolean removing = false;
|
||||
for(var tile : tiles){
|
||||
for(var parent : tile.obj.parents){
|
||||
var parentTile = tiles.find(t -> t.obj == parent);
|
||||
|
||||
if(parentTile == null) continue;
|
||||
|
||||
Connector
|
||||
conFrom = parentTile.conChildren,
|
||||
conTo = tile.conParent;
|
||||
|
||||
if(conTarget != null && (
|
||||
(connecting.findParent && connecting == conTo && conTarget == conFrom) ||
|
||||
(!connecting.findParent && connecting == conFrom && conTarget == conTo)
|
||||
)){
|
||||
removing = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
Vec2
|
||||
from = conFrom.localToAscendantCoordinates(this, Tmp.v1.set(conFrom.getWidth() / 2f, conFrom.getHeight() / 2f)).add(x, y),
|
||||
to = conTo.localToAscendantCoordinates(this, Tmp.v2.set(conTo.getWidth() / 2f, conTo.getHeight() / 2f)).add(x, y);
|
||||
|
||||
drawCurve(false, from.x, from.y, to.x, to.y);
|
||||
}
|
||||
}
|
||||
|
||||
if(connecting != null){
|
||||
Vec2
|
||||
mouse = (conTarget == null
|
||||
? connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY))
|
||||
: conTarget.localToAscendantCoordinates(this, Tmp.v1.set(conTarget.getWidth() / 2f, conTarget.getHeight() / 2f))
|
||||
).add(x, y),
|
||||
anchor = connecting.localToAscendantCoordinates(this, Tmp.v2.set(connecting.getWidth() / 2f, connecting.getHeight() / 2f)).add(x, y);
|
||||
|
||||
Vec2
|
||||
from = connecting.findParent ? mouse : anchor,
|
||||
to = connecting.findParent ? anchor : mouse;
|
||||
|
||||
drawCurve(removing, from.x, from.y, to.x, to.y);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
protected void drawCurve(boolean remove, float x1, float y1, float x2, float y2){
|
||||
Lines.stroke(4f);
|
||||
Draw.color(remove ? Pal.remove : Pal.accent, parentAlpha);
|
||||
|
||||
Fill.square(x1, y1, 8f, 45f);
|
||||
Fill.square(x2, y2, 8f, 45f);
|
||||
|
||||
float dist = Math.abs(x1 - x2) / 2f;
|
||||
float cx1 = x1 + dist;
|
||||
float cx2 = x2 - dist;
|
||||
Lines.curve(x1, y1, cx1, y1, cx2, y2, x2, y2, Math.max(4, (int) (Mathf.dst(x1, y1, x2, y2) / 4f)));
|
||||
|
||||
float progress = (Time.time % (60 * 4)) / (60 * 4);
|
||||
|
||||
float t2 = progress * progress;
|
||||
float t3 = progress * t2;
|
||||
float t1 = 1 - progress;
|
||||
float t13 = t1 * t1 * t1;
|
||||
float kx1 = t13 * x1 + 3 * progress * t1 * t1 * cx1 + 3 * t2 * t1 * cx2 + t3 * x2;
|
||||
float ky1 = t13 *y1 + 3 * progress * t1 * t1 * y1 + 3 * t2 * t1 * y2 + t3 * y2;
|
||||
|
||||
Fill.circle(kx1, ky1, 6f);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public boolean validPlace(int x, int y, @Nullable ObjectiveTile ignore){
|
||||
Tmp.r1.set(x, y, objWidth, objHeight).grow(-0.001f);
|
||||
|
||||
if(!Tmp.r2.setCentered(0, 0, bounds * 2, bounds * 2).contains(Tmp.r1)){
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var other : children){
|
||||
if(other instanceof ObjectiveTile tile && tile != ignore && Tmp.r2.set(tile.tx, tile.ty, objWidth, objHeight).overlaps(Tmp.r1)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean createTile(MapObjective obj){
|
||||
return createTile(obj.editorX, obj.editorY, obj);
|
||||
}
|
||||
|
||||
public boolean createTile(int x, int y, MapObjective obj){
|
||||
if(!validPlace(x, y, null)) return false;
|
||||
|
||||
ObjectiveTile tile = new ObjectiveTile(obj, x, y);
|
||||
tile.pack();
|
||||
|
||||
addChild(tile);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveTile(ObjectiveTile tile, int newX, int newY){
|
||||
if(!validPlace(newX, newY, tile)) return false;
|
||||
|
||||
tile.pos(newX, newY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void removeTile(ObjectiveTile tile){
|
||||
if(!tile.isDescendantOf(this)) return;
|
||||
tile.remove();
|
||||
}
|
||||
|
||||
public void clearTiles(){
|
||||
clearChildren();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPrefWidth(){
|
||||
return bounds * unitSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPrefHeight(){
|
||||
return bounds * unitSize;
|
||||
}
|
||||
|
||||
public class ObjectiveTile extends Table{
|
||||
public final MapObjective obj;
|
||||
public int tx, ty;
|
||||
|
||||
public final Mover mover;
|
||||
public final Connector conParent, conChildren;
|
||||
|
||||
public ObjectiveTile(MapObjective obj, int x, int y){
|
||||
this.obj = obj;
|
||||
setTransform(false);
|
||||
setClip(false);
|
||||
|
||||
add(conParent = new Connector(true)).size(unitSize / Scl.scl(1f), unitSize * 2 / Scl.scl(1f));
|
||||
table(Tex.whiteui, t -> {
|
||||
float pad = (unitSize / Scl.scl(1f) - 32f) / 2f - 4f;
|
||||
t.margin(pad);
|
||||
t.touchable(() -> Touchable.enabled);
|
||||
t.setColor(Pal.gray);
|
||||
|
||||
t.labelWrap(obj.typeName())
|
||||
.style(Styles.outlineLabel)
|
||||
.left().grow().get()
|
||||
.setAlignment(Align.left);
|
||||
|
||||
t.row();
|
||||
|
||||
t.table(b -> {
|
||||
b.left().defaults().size(40f);
|
||||
|
||||
b.button(Icon.pencilSmall, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@editor.objectives");
|
||||
dialog.cont.pane(Styles.noBarPane, list -> list.top().table(e -> {
|
||||
e.margin(0f);
|
||||
MapObjectivesDialog.getInterpreter((Class<MapObjective>)obj.getClass()).build(
|
||||
e, obj.typeName(), new TypeInfo(obj.getClass()),
|
||||
null, null, null,
|
||||
() -> obj,
|
||||
res -> {}
|
||||
);
|
||||
}).width(Math.min(Core.graphics.getWidth() * 0.95f / Scl.scl(1f) - Scl.scl(20f), 700f)).fillY()).grow();
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
});
|
||||
b.button(Icon.trashSmall, () -> removeTile(this));
|
||||
}).left().grow();
|
||||
}).growX().height(unitSize / Scl.scl(1f) * 2).get().addCaptureListener(mover = new Mover());
|
||||
add(conChildren = new Connector(false)).size(unitSize / Scl.scl(1f), unitSize / Scl.scl(1f) * 2);
|
||||
|
||||
setSize(getPrefWidth(), getPrefHeight());
|
||||
pos(x, y);
|
||||
}
|
||||
|
||||
public void pos(int x, int y){
|
||||
tx = obj.editorX = x;
|
||||
ty = obj.editorY = y;
|
||||
this.x = x * unitSize;
|
||||
this.y = y * unitSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPrefWidth(){
|
||||
return objWidth * unitSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPrefHeight(){
|
||||
return objHeight * unitSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(){
|
||||
if(super.remove()){
|
||||
obj.parents.clear();
|
||||
|
||||
var it = objectives.iterator();
|
||||
while(it.hasNext()){
|
||||
var next = it.next();
|
||||
if(next == obj){
|
||||
it.remove();
|
||||
}else{
|
||||
next.parents.remove(obj);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class Mover extends InputListener{
|
||||
public int prevX, prevY;
|
||||
public float lastX, lastY;
|
||||
|
||||
@Override
|
||||
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(moving != null) return false;
|
||||
moving = ObjectiveTile.this;
|
||||
moving.toFront();
|
||||
|
||||
prevX = moving.tx;
|
||||
prevY = moving.ty;
|
||||
|
||||
// Convert to world pos first because the button gets dragged too.
|
||||
Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchDragged(InputEvent event, float x, float y, int pointer){
|
||||
Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));
|
||||
|
||||
moving.moveBy(pos.x - lastX, pos.y - lastY);
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(!moveTile(moving,
|
||||
Mathf.round(moving.x / unitSize),
|
||||
Mathf.round(moving.y / unitSize)
|
||||
)) moving.pos(prevX, prevY);
|
||||
moving = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class Connector extends Button{
|
||||
public float pointX, pointY;
|
||||
public final boolean findParent;
|
||||
|
||||
public Connector(boolean findParent){
|
||||
super(new ButtonStyle(){{
|
||||
down = findParent ? Tex.buttonSideLeftDown : Tex.buttonSideRightDown;
|
||||
up = findParent ? Tex.buttonSideLeft : Tex.buttonSideRight;
|
||||
over = findParent ? Tex.buttonSideLeftOver : Tex.buttonSideRightOver;
|
||||
}});
|
||||
|
||||
this.findParent = findParent;
|
||||
|
||||
clearChildren();
|
||||
|
||||
addCaptureListener(new InputListener(){
|
||||
int conPointer = -1;
|
||||
|
||||
@Override
|
||||
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(conPointer != -1) return false;
|
||||
conPointer = pointer;
|
||||
|
||||
if(connecting != null) return false;
|
||||
connecting = Connector.this;
|
||||
|
||||
pointX = x;
|
||||
pointY = y;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchDragged(InputEvent event, float x, float y, int pointer){
|
||||
if(conPointer != pointer) return;
|
||||
pointX = x;
|
||||
pointY = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(conPointer != pointer || connecting != Connector.this) return;
|
||||
conPointer = -1;
|
||||
|
||||
Vec2 pos = Connector.this.localToAscendantCoordinates(ObjectiveTilemap.this, Tmp.v1.set(x, y));
|
||||
if(ObjectiveTilemap.this.hit(pos.x, pos.y, true) instanceof Connector con && con.canConnectTo(Connector.this)){
|
||||
if(findParent){
|
||||
if(!obj.parents.remove(con.tile().obj)) obj.parents.add(con.tile().obj);
|
||||
}else{
|
||||
if(!con.tile().obj.parents.remove(obj)) con.tile().obj.parents.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
connecting = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean canConnectTo(Connector other){
|
||||
return
|
||||
findParent != other.findParent &&
|
||||
tile() != other.tile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
super.draw();
|
||||
float cx = x + width / 2f;
|
||||
float cy = y + height / 2f;
|
||||
|
||||
// these are all magic numbers tweaked until they looked good in-game, don't mind them.
|
||||
Lines.stroke(3f, Pal.accent);
|
||||
if(findParent){
|
||||
Lines.line(cx, cy + 9f, cx + 9f, cy);
|
||||
Lines.line(cx + 9f, cy, cx, cy - 9f);
|
||||
}else{
|
||||
Lines.square(cx, cy, 9f, 45f);
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectiveTile tile(){
|
||||
return ObjectiveTile.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPressed(){
|
||||
return super.isPressed() || connecting == this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOver(){
|
||||
return super.isOver() && (connecting == null || connecting.canConnectTo(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.TextField.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.MapObjectives.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.lang.reflect.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.editor.MapObjectivesCanvas.*;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public class MapObjectivesDialog extends BaseDialog{
|
||||
public MapObjectivesCanvas canvas;
|
||||
protected Cons<Seq<MapObjective>> out = arr -> {};
|
||||
|
||||
/** Defines default value providers. */
|
||||
private static final ObjectMap<Class<?>, FieldProvider<?>> providers = new ObjectMap<>();
|
||||
/** Maps annotation type with its field parsers. Non-annotated fields are mapped with {@link Override}. */
|
||||
private static final ObjectMap<Class<? extends Annotation>, ObjectMap<Class<?>, FieldInterpreter<?>>> interpreters = new ObjectMap<>();
|
||||
|
||||
static{
|
||||
// Default un-annotated field interpreters.
|
||||
setProvider(String.class, (type, cons) -> cons.get(""));
|
||||
setInterpreter(String.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
|
||||
if(field != null && field.isAnnotationPresent(Multiline.class)){
|
||||
cont.area(get.get(), set).height(100f).growX();
|
||||
}else{
|
||||
cont.field(get.get(), set).growX();
|
||||
}
|
||||
});
|
||||
|
||||
setProvider(boolean.class, (type, cons) -> cons.get(false));
|
||||
setInterpreter(boolean.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.check("", get.get(), set::get).growX().fillY().get().getLabelCell().growX();
|
||||
});
|
||||
|
||||
setProvider(byte.class, (type, cons) -> cons.get((byte)0));
|
||||
setInterpreter(byte.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.field(Byte.toString(get.get()), str -> set.get((byte)Strings.parseInt(str)))
|
||||
.growX().fillY()
|
||||
.valid(Strings::canParseInt)
|
||||
.get().setFilter(TextFieldFilter.digitsOnly);
|
||||
});
|
||||
|
||||
setProvider(int.class, (type, cons) -> cons.get(0));
|
||||
setInterpreter(int.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.field(Integer.toString(get.get()), str -> set.get(Strings.parseInt(str)))
|
||||
.growX().fillY()
|
||||
.valid(Strings::canParseInt)
|
||||
.get().setFilter(TextFieldFilter.digitsOnly);
|
||||
});
|
||||
|
||||
setProvider(float.class, (type, cons) -> cons.get(0f));
|
||||
setInterpreter(float.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
float m = 1f;
|
||||
if(field != null){
|
||||
if(field.isAnnotationPresent(Second.class)){
|
||||
m = 60f;
|
||||
}else if(field.isAnnotationPresent(TilePos.class)){
|
||||
m = 8f;
|
||||
}
|
||||
}
|
||||
|
||||
float mult = m;
|
||||
|
||||
name(cont, name, remover, indexer);
|
||||
cont.field(Float.toString(get.get() / mult), str -> set.get(Strings.parseFloat(str) * mult))
|
||||
.growX().fillY()
|
||||
.valid(Strings::canParseFloat)
|
||||
.get().setFilter(TextFieldFilter.floatsOnly);
|
||||
});
|
||||
|
||||
setProvider(UnlockableContent.class, (type, cons) -> cons.get(Blocks.coreShard));
|
||||
setInterpreter(UnlockableContent.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image().size(iconSmall).scaling(Scaling.fit).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
() -> showContentSelect(null, set, b -> (field != null && !field.isAnnotationPresent(Researchable.class)) || b.techNode != null)
|
||||
).fill().pad(4)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Block.class, (type, cons) -> cons.get(Blocks.copperWall));
|
||||
setInterpreter(Block.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
() -> showContentSelect(ContentType.block, set, b -> (field != null && !field.isAnnotationPresent(Synthetic.class)) || b.synthetic())
|
||||
).fill().pad(4f)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Item.class, (type, cons) -> cons.get(Items.copper));
|
||||
setInterpreter(Item.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
() -> showContentSelect(ContentType.item, set, item -> true)
|
||||
).fill().pad(4f)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(UnitType.class, (type, cons) -> cons.get(UnitTypes.dagger));
|
||||
setInterpreter(UnitType.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
|
||||
() -> showContentSelect(ContentType.unit, set, unit -> true)
|
||||
).fill().pad(4f)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Team.class, (type, cons) -> cons.get(Team.sharded));
|
||||
setInterpreter(Team.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)),
|
||||
() -> showTeamSelect(set)
|
||||
).fill().pad(4f)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Color.class, (type, cons) -> cons.get(Pal.accent.cpy()));
|
||||
setInterpreter(Color.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
var out = get.get();
|
||||
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> t.left().button(
|
||||
b -> b.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
|
||||
update(() -> setColor(out));
|
||||
}}).grow(),
|
||||
Styles.squarei,
|
||||
() -> ui.picker.show(out, res -> set.get(out.set(res)))
|
||||
).margin(4f).pad(4f).size(50f)).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Vec2.class, (type, cons) -> cons.get(new Vec2()));
|
||||
setInterpreter(Vec2.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
var obj = get.get();
|
||||
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> {
|
||||
boolean isInt = type.raw == int.class;
|
||||
|
||||
FieldInterpreter in = getInterpreter(float.class);
|
||||
if(isInt) in = getInterpreter(int.class);
|
||||
|
||||
in.build(
|
||||
t, "x", new TypeInfo(isInt ? int.class : float.class),
|
||||
field, null, null,
|
||||
isInt ? () -> (int)obj.x : () -> obj.x,
|
||||
res -> {
|
||||
obj.x = isInt ? (Integer)res : (Float)res;
|
||||
set.get(obj);
|
||||
}
|
||||
);
|
||||
|
||||
in.build(
|
||||
t.row(), "y", new TypeInfo(isInt ? int.class : float.class),
|
||||
field, null, null,
|
||||
isInt ? () -> (int)obj.y : () -> obj.y,
|
||||
res -> {
|
||||
obj.y = isInt ? (Integer)res : (Float)res;
|
||||
set.get(obj);
|
||||
}
|
||||
);
|
||||
}).growX().fillY();
|
||||
});
|
||||
|
||||
setProvider(Point2.class, (type, cons) -> cons.get(new Point2()));
|
||||
setInterpreter(Point2.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
var obj = get.get();
|
||||
var vec = new Vec2(obj.x, obj.y);
|
||||
getInterpreter(Vec2.class).build(
|
||||
cont, name, new TypeInfo(int.class),
|
||||
field, remover, indexer,
|
||||
() -> vec,
|
||||
res -> {
|
||||
vec.set(res);
|
||||
set.get(obj.set((int)vec.x, (int)vec.y));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Types that have a provider, but delegate to the default interpreter.
|
||||
setProvider(MapObjective.class, (type, cons) -> new BaseDialog("@add"){{
|
||||
cont.pane(p -> {
|
||||
p.background(Tex.button);
|
||||
p.marginRight(14f);
|
||||
p.defaults().size(195f, 56f);
|
||||
|
||||
int i = 0;
|
||||
for(var gen : MapObjectives.allObjectiveTypes){
|
||||
var obj = gen.get();
|
||||
p.button(obj.typeName(), Styles.flatt, () -> {
|
||||
cons.get(obj);
|
||||
hide();
|
||||
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left);
|
||||
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
}).scrollX(false);
|
||||
|
||||
addCloseButton();
|
||||
show();
|
||||
}});
|
||||
|
||||
setProvider(ObjectiveMarker.class, (type, cons) -> new BaseDialog("@add"){{
|
||||
cont.pane(p -> {
|
||||
p.background(Tex.button);
|
||||
p.marginRight(14f);
|
||||
p.defaults().size(195f, 56f);
|
||||
|
||||
int i = 0;
|
||||
for(var gen : MapObjectives.allMarkerTypes){
|
||||
var marker = gen.get();
|
||||
p.button(marker.typeName(), Styles.flatt, () -> {
|
||||
cons.get(marker);
|
||||
hide();
|
||||
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left);
|
||||
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
}).scrollX(false);
|
||||
|
||||
addCloseButton();
|
||||
show();
|
||||
}});
|
||||
|
||||
setInterpreter(Vertices.class, float[].class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
|
||||
float[] data = get.get();
|
||||
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> {
|
||||
t.left().defaults().left();
|
||||
|
||||
String[] names = {"x", "y", "color", "u", "v"};
|
||||
int stride = 6;
|
||||
int vertices = data.length / stride;
|
||||
|
||||
for(int i = 0; i < vertices; i++){
|
||||
int offset = i * stride;
|
||||
|
||||
t.table(row -> {
|
||||
for(int j = 0; j < names.length; j++){
|
||||
int index = offset + j;
|
||||
|
||||
if("color".equals(names[j])) {
|
||||
getInterpreter(Color.class).build(row, names[j], new TypeInfo(Color.class), null, null, null, () -> new Color().abgr8888(data[index]), value -> data[index] = value.toFloatBits());
|
||||
}else{
|
||||
float scale = j <= 1 ? tilesize : 1;
|
||||
getInterpreter(float.class).build(row, names[j], new TypeInfo(float.class), null, null, null, () -> data[index] / scale, value -> data[index] = value * scale);
|
||||
}
|
||||
|
||||
row.add().pad(4);
|
||||
}
|
||||
}).row();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Types that use the default interpreter. It would be nice if all types could use it, but I don't know how to reliably prevent classes like [? extends Content] from using it.
|
||||
for(var obj : MapObjectives.allObjectiveTypes) setInterpreter(obj.get().getClass(), defaultInterpreter());
|
||||
for(var mark : MapObjectives.allMarkerTypes) setInterpreter(mark.get().getClass(), defaultInterpreter());
|
||||
|
||||
// Annotated field interpreters.
|
||||
setInterpreter(LabelFlag.class, byte.class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
name(cont, name, remover, indexer);
|
||||
cont.table(t -> {
|
||||
t.left().defaults().left();
|
||||
byte
|
||||
value = get.get(),
|
||||
bg = WorldLabel.flagBackground, out = WorldLabel.flagOutline;
|
||||
|
||||
t.check("@marker.background", (value & bg) == bg, res -> set.get((byte)(res ? value | bg : value & ~bg)))
|
||||
.growX().fillY()
|
||||
.padTop(4f).padBottom(4f).get().getLabelCell().growX();
|
||||
|
||||
t.row();
|
||||
t.check("@marker.outline", (value & out) == out, res -> set.get((byte)(res ? value | out : value & ~out)))
|
||||
.growX().fillY().get().getLabelCell().growX();
|
||||
}).growX().fillY();
|
||||
});
|
||||
|
||||
// Special data structure interpreters.
|
||||
// Instantiate default `Seq`s with a reflectively allocated array.
|
||||
setProvider(Seq.class, (type, cons) -> cons.get(new Seq<>(type.element.raw)));
|
||||
setInterpreter(Seq.class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
|
||||
Runnable[] rebuild = {null};
|
||||
var arr = get.get();
|
||||
|
||||
main.margin(0f, 10f, 0f, 10f);
|
||||
var header = main.table(Tex.button, t -> {
|
||||
t.left();
|
||||
t.margin(10f);
|
||||
|
||||
if(name.length() > 0) t.add(name + ":").color(Pal.accent);
|
||||
t.add().growX();
|
||||
|
||||
if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f);
|
||||
if(indexer != null){
|
||||
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
|
||||
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f);
|
||||
}
|
||||
|
||||
if(!field.isAnnotationPresent(Immutable.class)) {
|
||||
t.button(Icon.add, Styles.emptyi, () -> getProvider(type.element.raw).get(type.element, res -> {
|
||||
arr.add(res);
|
||||
rebuild[0].run();
|
||||
})).fill();
|
||||
}
|
||||
}).growX().height(46f).pad(0f, -10f, 0f, -10f).get();
|
||||
|
||||
main.row().table(Tex.button, t -> rebuild[0] = () -> {
|
||||
t.clear();
|
||||
t.top();
|
||||
|
||||
if(arr.isEmpty()){
|
||||
t.background(Tex.clear).margin(0f).setSize(0f);
|
||||
}else{
|
||||
t.background(Tex.button).margin(10f).marginTop(20f);
|
||||
}
|
||||
|
||||
for(int i = 0, len = arr.size; i < len; i++){
|
||||
int index = i;
|
||||
if(index > 0) t.row();
|
||||
|
||||
getInterpreter((Class<Object>)arr.get(index).getClass()).build(
|
||||
t, "", new TypeInfo(arr.get(index).getClass()),
|
||||
field, field == null || !field.isAnnotationPresent(Immutable.class) ? () -> {
|
||||
arr.remove(index);
|
||||
rebuild[0].run();
|
||||
} : null, field == null || !field.isAnnotationPresent(Unordered.class) ? in -> {
|
||||
if(in && index > 0){
|
||||
arr.swap(index, index - 1);
|
||||
rebuild[0].run();
|
||||
}else if(!in && index < len - 1){
|
||||
arr.swap(index, index + 1);
|
||||
rebuild[0].run();
|
||||
}
|
||||
} : null,
|
||||
() -> arr.get(index),
|
||||
res -> {
|
||||
arr.set(index, res);
|
||||
set.get(arr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
set.get(arr);
|
||||
}).padTop(-10f).growX().fillY();
|
||||
rebuild[0].run();
|
||||
|
||||
header.toFront();
|
||||
}).growX().fillY().pad(4f).colspan(2));
|
||||
|
||||
// Reserved for array types that are not explicitly handled. Essentially handles it the same way as `Seq`.
|
||||
setProvider(Object[].class, (type, cons) -> cons.get(Reflect.newArray(type.element.raw, 0)));
|
||||
setInterpreter(Object[].class, (cont, name, type, field, remover, indexer, get, set) -> {
|
||||
var arr = Seq.with(get.get());
|
||||
getInterpreter(Seq.class).build(
|
||||
cont, name, new TypeInfo(Seq.class, type.element),
|
||||
field, remover, indexer,
|
||||
() -> arr,
|
||||
res -> set.get(arr.toArray(type.element.raw))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> FieldInterpreter<T> defaultInterpreter(){
|
||||
return (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
|
||||
main.margin(0f, 10f, 0f, 10f);
|
||||
var header = main.table(Tex.button, t -> {
|
||||
t.left();
|
||||
t.margin(10f);
|
||||
|
||||
if(name.length() > 0) t.add(name + ":").color(Pal.accent);
|
||||
t.add().growX();
|
||||
|
||||
Cell<ImageButton> remove = null;
|
||||
if(remover != null) remove = t.button(Icon.trash, Styles.emptyi, remover).fill();
|
||||
if(indexer != null){
|
||||
if(remove != null) remove.padRight(4f);
|
||||
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
|
||||
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill();
|
||||
}
|
||||
}).growX().height(46f).pad(0f, -10f, -0f, -10f).get();
|
||||
|
||||
main.row().table(Tex.button, t -> {
|
||||
t.left();
|
||||
t.top().margin(10f).marginTop(20f);
|
||||
|
||||
t.defaults().minHeight(40f).left();
|
||||
var obj = get.get();
|
||||
|
||||
int i = 0;
|
||||
for(var e : JsonIO.json.getFields(type.raw).values()){
|
||||
if(i++ > 0) t.row();
|
||||
|
||||
var f = e.field;
|
||||
var ft = f.getType();
|
||||
int mods = f.getModifiers();
|
||||
|
||||
if(!Modifier.isPublic(mods) || (Modifier.isFinal(mods) && (
|
||||
String.class.isAssignableFrom(ft) ||
|
||||
unbox(ft).isPrimitive()
|
||||
))) continue;
|
||||
|
||||
var anno = Structs.find(f.getDeclaredAnnotations(), a -> hasInterpreter(a.annotationType(), ft));
|
||||
getInterpreter(anno == null ? Override.class : anno.annotationType(), ft).build(
|
||||
t, f.getName(), new TypeInfo(f),
|
||||
f, null, null,
|
||||
() -> Reflect.get(obj, f),
|
||||
Modifier.isFinal(mods) ? res -> {} : res -> Reflect.set(obj, f, res)
|
||||
);
|
||||
}
|
||||
}).padTop(-10f).growX().fillY();
|
||||
|
||||
header.toFront();
|
||||
}).growX().fillY().pad(4f).colspan(2);
|
||||
}
|
||||
|
||||
public static void name(Table cont, CharSequence name, @Nullable Runnable remover, @Nullable Boolc indexer){
|
||||
if(indexer != null || remover != null){
|
||||
cont.table(t -> {
|
||||
if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f);
|
||||
if(indexer != null){
|
||||
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
|
||||
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f);
|
||||
}
|
||||
}).fill();
|
||||
}else{
|
||||
cont.add(name + ": ");
|
||||
}
|
||||
}
|
||||
|
||||
public MapObjectivesDialog(){
|
||||
super("@editor.objectives");
|
||||
clear();
|
||||
margin(0f);
|
||||
|
||||
stack(
|
||||
new Image(Styles.black5),
|
||||
canvas = new MapObjectivesCanvas(),
|
||||
new Table(){{
|
||||
buttons.defaults().size(160f, 64f).pad(2f);
|
||||
buttons.button("@back", Icon.left, MapObjectivesDialog.this::hide);
|
||||
buttons.button("@add", Icon.add, () -> getProvider(MapObjective.class).get(new TypeInfo(MapObjective.class), canvas::query));
|
||||
buttons.button("@waves.edit", Icon.edit, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@waves.edit");
|
||||
dialog.addCloseButton();
|
||||
dialog.setFillParent(false);
|
||||
dialog.cont.table(Tex.button, t -> {
|
||||
var style = Styles.cleart;
|
||||
t.defaults().size(280f, 64f).pad(2f);
|
||||
|
||||
t.button("@waves.copy", Icon.copy, style, () -> {
|
||||
ui.showInfoFade("@copied");
|
||||
Core.app.setClipboardText(JsonIO.write(new MapObjectives(canvas.objectives)));
|
||||
dialog.hide();
|
||||
}).disabled(b -> canvas.objectives.isEmpty()).marginLeft(12f).row();
|
||||
|
||||
t.button("@waves.load", Icon.download, style, () -> {
|
||||
try{
|
||||
rebuildObjectives(new Seq<>(JsonIO.read(MapObjectives.class, Core.app.getClipboardText()).all));
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showErrorMessage("@waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("[")).marginLeft(12f).row();
|
||||
|
||||
t.button("@clear", Icon.none, style, () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
|
||||
rebuildObjectives(new Seq<>());
|
||||
dialog.hide();
|
||||
})).marginLeft(12f).row();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
if(mobile){
|
||||
buttons.button("@cancel", Icon.cancel, canvas::stopQuery).visible(() -> canvas.isQuerying());
|
||||
buttons.button("@ok", Icon.ok, canvas::placeQuery).visible(() -> canvas.isQuerying());
|
||||
}
|
||||
|
||||
setFillParent(true);
|
||||
margin(3f);
|
||||
|
||||
add(titleTable).growX().fillY();
|
||||
row().add().grow();
|
||||
row().add(buttons).fill();
|
||||
addCloseListener();
|
||||
}}
|
||||
).grow().pad(0f).margin(0f);
|
||||
|
||||
hidden(() -> {
|
||||
out.get(canvas.objectives);
|
||||
out = arr -> {};
|
||||
});
|
||||
}
|
||||
|
||||
public void show(Seq<MapObjective> objectives, Cons<Seq<MapObjective>> out){
|
||||
this.out = out;
|
||||
|
||||
rebuildObjectives(objectives);
|
||||
show();
|
||||
}
|
||||
|
||||
public void rebuildObjectives(Seq<MapObjective> objectives){
|
||||
canvas.clearObjectives();
|
||||
if(
|
||||
objectives.any() && (
|
||||
// If the objectives were previously programmatically made...
|
||||
objectives.contains(obj -> obj.editorX == -1 || obj.editorY == -1) ||
|
||||
// ... or some idiot somehow made it not work...
|
||||
objectives.contains(obj -> !canvas.tilemap.createTile(obj))
|
||||
)){
|
||||
// ... then rebuild the structure.
|
||||
canvas.clearObjectives();
|
||||
|
||||
// This is definitely NOT a good way to do it, but only insane people or people from the distant past would actually encounter this anyway.
|
||||
int w = objWidth + 2,
|
||||
len = objectives.size * w,
|
||||
columns = objectives.size,
|
||||
rows = 1;
|
||||
|
||||
if(len > bounds){
|
||||
rows = len / bounds;
|
||||
columns = bounds / w;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
loop:
|
||||
for(int y = 0; y < rows; y++){
|
||||
for(int x = 0; x < columns; x++){
|
||||
if(canvas.tilemap.createTile(x * w, y, objectives.get(i))){
|
||||
i++;
|
||||
}
|
||||
if(i >= objectives.size) break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.objectives.set(objectives);
|
||||
}
|
||||
|
||||
public static <T extends UnlockableContent> void showContentSelect(@Nullable ContentType type, Cons<T> cons, Boolf<T> check){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.cont.pane(Styles.noBarPane, t -> {
|
||||
int i = 0;
|
||||
for(var content : (type == null ? content.blocks().copy().<UnlockableContent>as()
|
||||
.add(content.items())
|
||||
.add(content.liquids())
|
||||
.add(content.units()) :
|
||||
content.getBy(type).<UnlockableContent>as()
|
||||
)){
|
||||
if(content.isHidden() || !check.get((T)content)) continue;
|
||||
t.image(content == Blocks.air ? Icon.none.getRegion() : content.uiIcon).size(iconMed).pad(3).scaling(Scaling.fit)
|
||||
.with(b -> b.addListener(new HandCursorListener()))
|
||||
.tooltip(content.localizedName).get().clicked(() -> {
|
||||
cons.get((T)content);
|
||||
dialog.hide();
|
||||
});
|
||||
|
||||
if(++i % 10 == 0) t.row();
|
||||
}
|
||||
}).fill();
|
||||
|
||||
dialog.closeOnBack();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showTeamSelect(Cons<Team> cons){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
for(var team : Team.baseTeams){
|
||||
dialog.cont.image(Tex.whiteui).size(iconMed).color(team.color).pad(4)
|
||||
.with(i -> i.addListener(new HandCursorListener()))
|
||||
.tooltip(team.localized()).get().clicked(() -> {
|
||||
cons.get(team);
|
||||
dialog.hide();
|
||||
});
|
||||
}
|
||||
|
||||
dialog.closeOnBack();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static Class<?> unbox(Class<?> boxed){
|
||||
return switch(boxed.getSimpleName()){
|
||||
case "Boolean" -> boolean.class;
|
||||
case "Byte" -> byte.class;
|
||||
case "Character" -> char.class;
|
||||
case "Short" -> short.class;
|
||||
case "Integer" -> int.class;
|
||||
case "Long" -> long.class;
|
||||
case "Float" -> float.class;
|
||||
case "Double" -> double.class;
|
||||
default -> boxed;
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> void setInterpreter(Class<T> type, FieldInterpreter<? super T> interpreter){
|
||||
setInterpreter(Override.class, type, interpreter);
|
||||
}
|
||||
|
||||
public static <T> void setInterpreter(Class<? extends Annotation> anno, Class<T> type, FieldInterpreter<? super T> interpreter){
|
||||
interpreters.get(anno, ObjectMap::new).put(type, interpreter);
|
||||
}
|
||||
|
||||
public static boolean hasInterpreter(Class<?> type){
|
||||
return hasInterpreter(Override.class, type);
|
||||
}
|
||||
|
||||
public static boolean hasInterpreter(Class<? extends Annotation> anno, Class<?> type){
|
||||
return interpreters.get(anno, ObjectMap::new).containsKey(unbox(type));
|
||||
}
|
||||
|
||||
public static <T> FieldInterpreter<T> getInterpreter(Class<T> type){
|
||||
return getInterpreter(Override.class, type);
|
||||
}
|
||||
|
||||
public static <T> FieldInterpreter<T> getInterpreter(Class<? extends Annotation> anno, Class<T> type){
|
||||
if(hasInterpreter(anno, type)){
|
||||
return (FieldInterpreter<T>)interpreters.get(anno, ObjectMap::new).get(unbox(type));
|
||||
}else if(hasInterpreter(Override.class, type)){
|
||||
return (FieldInterpreter<T>)interpreters.get(Override.class, ObjectMap::new).get(unbox(type));
|
||||
}else if(type.isArray() && !type.getComponentType().isPrimitive()){
|
||||
return (FieldInterpreter<T>)(hasInterpreter(anno, Object[].class)
|
||||
? interpreters.get(anno).get(Object[].class)
|
||||
: interpreters.get(Override.class).get(Object[].class)
|
||||
);
|
||||
}else{
|
||||
throw new IllegalArgumentException("Interpreter for type " + type + " not set up yet.");
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void setProvider(Class<T> type, FieldProvider<T> provider){
|
||||
providers.put(unbox(type), provider);
|
||||
}
|
||||
|
||||
public static boolean hasProvider(Class<?> type){
|
||||
return providers.containsKey(unbox(type));
|
||||
}
|
||||
|
||||
public static <T> FieldProvider<T> getProvider(Class<T> type){
|
||||
return (FieldProvider<T>)providers.getThrow(unbox(type), () -> new IllegalArgumentException("Provider for type " + type + " not set up yet."));
|
||||
}
|
||||
|
||||
public interface FieldInterpreter<T>{
|
||||
/**
|
||||
* Builds the interpreter for (not-necessarily) a possibly annotated field. Implementations must add exactly
|
||||
* 2 columns to the table.
|
||||
* @param name May be empty.
|
||||
* @param remover If this callback is not {@code null}, this interpreter should add a button that invokes the
|
||||
* callback to signal element removal.
|
||||
* @param indexer If this callback is not {@code null}, this interpreter should add 2 buttons that invoke the
|
||||
* callback to signal element rearrangement with the following values:<ul>
|
||||
* <li>{@code true}: Swap element with previous index.</li>
|
||||
* <li>{@code false}: Swap element with next index.</li>
|
||||
* </ul>
|
||||
*/
|
||||
void build(Table cont,
|
||||
CharSequence name, TypeInfo type,
|
||||
@Nullable Field field,
|
||||
@Nullable Runnable remover, @Nullable Boolc indexer,
|
||||
Prov<T> get, Cons<T> set);
|
||||
}
|
||||
|
||||
public interface FieldProvider<T>{
|
||||
void get(TypeInfo type, Cons<T> cons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores parameterized or array type information for convenience.
|
||||
* For {@code A[]}: {@link #raw} is {@code A[]}, {@link #element} is {@code A}, {@link #key} is {@code null}.
|
||||
* For {@code Seq<A>}: {@link #raw} is {@link Seq}, {@link #element} is {@code A}, {@link #key} is {@code null}.
|
||||
* For {@code ObjectMap<A, B>}: {@link #raw} is {@link ObjectMap}, {@link #element} is {@code B}, {@link #key} is {@code A}.
|
||||
*/
|
||||
public static class TypeInfo{
|
||||
public final Class<?> raw;
|
||||
public final TypeInfo element, key;
|
||||
|
||||
public TypeInfo(Field field){
|
||||
this(field.getType(), field.getGenericType());
|
||||
}
|
||||
|
||||
public TypeInfo(Class<?> raw){
|
||||
this(raw, raw);
|
||||
}
|
||||
|
||||
/** Use with care! */
|
||||
public TypeInfo(Class<?> raw, TypeInfo element){
|
||||
this.raw = unbox(raw);
|
||||
this.element = element;
|
||||
key = null;
|
||||
}
|
||||
|
||||
public TypeInfo(Class<?> raw, Type generic){
|
||||
this.raw = unbox(raw);
|
||||
if(raw.isArray()){
|
||||
key = null;
|
||||
element = new TypeInfo(raw.getComponentType(), generic instanceof GenericArrayType type ? type.getGenericComponentType() : raw.getComponentType());
|
||||
}else if(Seq.class.isAssignableFrom(raw)){
|
||||
key = null;
|
||||
element = getParam(generic, 0);
|
||||
}else if(ObjectMap.class.isAssignableFrom(raw)){
|
||||
key = getParam(generic, 0);
|
||||
element = getParam(generic, 1);
|
||||
}else{
|
||||
key = element = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static TypeInfo getParam(Type generic, int index){
|
||||
Type[] params =
|
||||
generic instanceof ParameterizedType type ? type.getActualTypeArguments() :
|
||||
generic instanceof GenericDeclaration type ? type.getTypeParameters() : null;
|
||||
|
||||
if(params != null && index < params.length){
|
||||
var target = params[index];
|
||||
return new TypeInfo(raw(target), target);
|
||||
}
|
||||
|
||||
return new TypeInfo(Object.class, Object.class);
|
||||
}
|
||||
|
||||
public static Class<?> raw(Type type){
|
||||
if(type instanceof Class<?> c) return c;
|
||||
if(type instanceof ParameterizedType c) return (Class<?>)c.getRawType();
|
||||
if(type instanceof GenericArrayType c) return Reflect.newArray(raw(c.getGenericComponentType()), 0).getClass();
|
||||
if(type instanceof TypeVariable<?> c) return raw(c.getBounds()[0]);
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
import mindustry.world.blocks.logic.*;
|
||||
import mindustry.world.blocks.logic.LogicBlock.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapProcessorsDialog extends BaseDialog{
|
||||
private IconSelectDialog iconSelect = new IconSelectDialog(true);
|
||||
private TextField search;
|
||||
private Seq<Building> processors = new Seq<>();
|
||||
private Table list;
|
||||
|
||||
public MapProcessorsDialog(){
|
||||
super("@editor.worldprocessors");
|
||||
|
||||
shown(this::setup);
|
||||
|
||||
addCloseButton();
|
||||
buttons.button("@add", Icon.add, () -> {
|
||||
boolean foundAny = false;
|
||||
|
||||
outer:
|
||||
for(int y = 0; y < Vars.world.height(); y++){
|
||||
for(int x = 0; x < Vars.world.width(); x++){
|
||||
Tile tile = Vars.world.rawTile(x, y);
|
||||
if(!tile.synthetic()){
|
||||
foundAny = true;
|
||||
tile.setNet(Blocks.worldProcessor, Team.sharded, 0);
|
||||
if(ui.editor.isShown()){
|
||||
Vars.editor.renderer.updatePoint(x, y);
|
||||
}
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!foundAny){
|
||||
ui.showErrorMessage("@editor.worldprocessors.nospace");
|
||||
}else{
|
||||
setup();
|
||||
}
|
||||
}).size(210f, 64f);
|
||||
|
||||
cont.top();
|
||||
getCell(cont).grow();
|
||||
|
||||
cont.table(s -> {
|
||||
s.image(Icon.zoom).padRight(8);
|
||||
search = s.field(null, text -> rebuild()).growX().get();
|
||||
search.setMessageText("@players.search");
|
||||
}).width(440f).fillX().padBottom(4).row();
|
||||
|
||||
cont.pane(t -> {
|
||||
list = t;
|
||||
});
|
||||
}
|
||||
|
||||
private void rebuild(){
|
||||
list.clearChildren();
|
||||
|
||||
if(processors.isEmpty()){
|
||||
list.add("@editor.worldprocessors.none");
|
||||
}else{
|
||||
Table t = list;
|
||||
var text = search.getText().toLowerCase();
|
||||
|
||||
t.defaults().pad(4f);
|
||||
float h = 50f;
|
||||
for(var build : processors){
|
||||
if(build instanceof LogicBuild log && (text.isEmpty() || (log.tag != null && log.tag.toLowerCase().contains(text)))){
|
||||
|
||||
t.button(log.iconTag == 0 ? Styles.none : new TextureRegionDrawable(Fonts.getLargeIcon(Fonts.unicodeToName(log.iconTag))), Styles.graySquarei, iconMed, () -> {
|
||||
iconSelect.show(ic -> {
|
||||
log.iconTag = (char)ic;
|
||||
rebuild();
|
||||
});
|
||||
}).size(h);
|
||||
|
||||
t.button((log.tag == null ? "<no name>\n" : "[accent]" + log.tag + "\n") + "[lightgray][[" + log.tile.x + ", " + log.tile.y + "]", Styles.grayt, () -> {
|
||||
//TODO: bug: if you edit name inside of the edit dialog, it won't show up in the list properly
|
||||
log.showEditDialog(true);
|
||||
}).size(Vars.mobile ? 390f : 450f, h).margin(10f).with(b -> {
|
||||
b.getLabel().setAlignment(Align.left, Align.left);
|
||||
});
|
||||
|
||||
t.button(Icon.pencil, Styles.graySquarei, Vars.iconMed, () -> {
|
||||
ui.showTextInput("", "@editor.name", LogicBlock.maxNameLength, log.tag == null ? "" : log.tag, tag -> {
|
||||
//bypass configuration and set it directly in case privileged checks mess things up
|
||||
log.tag = tag;
|
||||
setup();
|
||||
});
|
||||
}).size(h);
|
||||
|
||||
if(Vars.state.isGame() && state.isEditor()){
|
||||
t.button(Icon.eyeSmall, Styles.graySquarei, Vars.iconMed, () -> {
|
||||
hide();
|
||||
control.input.config.showConfig(build);
|
||||
control.input.panCamera(Tmp.v1.set(build));
|
||||
}).size(h);
|
||||
}
|
||||
|
||||
t.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
|
||||
ui.showConfirm("@editor.worldprocessors.delete.confirm", () -> {
|
||||
boolean surrounded = true;
|
||||
for(int i = 0; i < 4; i++){
|
||||
Tile other = build.tile.nearby(i);
|
||||
if(other != null && !(other.block().privileged || other.block().isStatic())){
|
||||
surrounded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(surrounded){
|
||||
build.tile.setNet(build.tile.floor().wall instanceof StaticWall ? build.tile.floor().wall : Blocks.stoneWall);
|
||||
}else{
|
||||
build.tile.setNet(Blocks.air);
|
||||
}
|
||||
processors.remove(build);
|
||||
rebuild();
|
||||
});
|
||||
}).size(h);
|
||||
|
||||
t.row();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setup(){
|
||||
|
||||
processors.clear();
|
||||
|
||||
//scan the entire world for processor (Groups.build can be empty, indexer is probably inaccurate)
|
||||
Vars.world.tiles.eachTile(t -> {
|
||||
if(t.isCenter() && t.block() == Blocks.worldProcessor){
|
||||
processors.add(t.build);
|
||||
}
|
||||
});
|
||||
|
||||
rebuild();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import mindustry.content.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.environment.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@@ -18,14 +19,8 @@ public class MapRenderer implements Disposable{
|
||||
private IndexedRenderer[][] chunks;
|
||||
private IntSet updates = new IntSet();
|
||||
private IntSet delayedUpdates = new IntSet();
|
||||
private MapEditor editor;
|
||||
private TextureRegion clearEditor;
|
||||
private int width, height;
|
||||
private Texture texture;
|
||||
|
||||
public MapRenderer(MapEditor editor){
|
||||
this.editor = editor;
|
||||
this.texture = Core.atlas.find("clear-editor").texture;
|
||||
}
|
||||
|
||||
public void resize(int width, int height){
|
||||
updates.clear();
|
||||
@@ -52,6 +47,7 @@ public class MapRenderer implements Disposable{
|
||||
|
||||
public void draw(float tx, float ty, float tw, float th){
|
||||
Draw.flush();
|
||||
clearEditor = Core.atlas.find("clear-editor");
|
||||
|
||||
updates.each(i -> render(i % width, i / width));
|
||||
updates.clear();
|
||||
@@ -64,6 +60,8 @@ public class MapRenderer implements Disposable{
|
||||
return;
|
||||
}
|
||||
|
||||
var texture = Core.atlas.find("clear-editor").texture;
|
||||
|
||||
for(int x = 0; x < chunks.length; x++){
|
||||
for(int y = 0; y < chunks[0].length; y++){
|
||||
IndexedRenderer mesh = chunks[x][y];
|
||||
@@ -85,6 +83,7 @@ public class MapRenderer implements Disposable{
|
||||
}
|
||||
|
||||
public void updateAll(){
|
||||
clearEditor = Core.atlas.find("clear-editor");
|
||||
for(int x = 0; x < width; x++){
|
||||
for(int y = 0; y < height; y++){
|
||||
render(x, y);
|
||||
@@ -92,13 +91,22 @@ public class MapRenderer implements Disposable{
|
||||
}
|
||||
}
|
||||
|
||||
private TextureRegion getIcon(Block wall, int index){
|
||||
return !wall.editorIcon().found() ?
|
||||
clearEditor : wall.variants > 0 ?
|
||||
wall.editorVariantRegions()[Mathf.randomSeed(index, 0, wall.editorVariantRegions().length - 1)] :
|
||||
wall.editorIcon();
|
||||
}
|
||||
|
||||
private void render(int wx, int wy){
|
||||
int x = wx / chunkSize, y = wy / chunkSize;
|
||||
if(x >= chunks.length || y >= chunks[0].length) return;
|
||||
IndexedRenderer mesh = chunks[x][y];
|
||||
Tile tile = editor.tiles().getn(wx, wy);
|
||||
|
||||
Team team = tile.team();
|
||||
Block floor = tile.floor();
|
||||
Floor floor = tile.floor();
|
||||
Floor overlay = tile.overlay();
|
||||
Block wall = tile.block();
|
||||
|
||||
TextureRegion region;
|
||||
@@ -106,15 +114,23 @@ public class MapRenderer implements Disposable{
|
||||
int idxWall = (wx % chunkSize) + (wy % chunkSize) * chunkSize;
|
||||
int idxDecal = (wx % chunkSize) + (wy % chunkSize) * chunkSize + chunkSize * chunkSize;
|
||||
boolean center = tile.isCenter();
|
||||
boolean useSyntheticWall = wall.synthetic() || overlay.wallOre;
|
||||
|
||||
if(wall != Blocks.air && wall.synthetic()){
|
||||
region = !Core.atlas.isFound(wall.editorIcon()) || !center ? Core.atlas.find("clear-editor") : wall.editorIcon();
|
||||
//draw synthetic wall or floor OR standard wall if wall ore
|
||||
if(wall != Blocks.air && useSyntheticWall){
|
||||
region = !center ? clearEditor : getIcon(wall, idxWall);
|
||||
|
||||
float width = region.width * Draw.scl, height = region.height * Draw.scl;
|
||||
float width = region.width * region.scl(), height = region.height * region.scl(), ox = wall.offset + (tilesize - width) / 2f, oy = wall.offset + (tilesize - height) / 2f;
|
||||
|
||||
//force fit to tile
|
||||
if(overlay.wallOre && !wall.synthetic()){
|
||||
width = height = tilesize;
|
||||
ox = oy = 0f;
|
||||
}
|
||||
|
||||
mesh.draw(idxWall, region,
|
||||
wx * tilesize + wall.offset + (tilesize - width) / 2f,
|
||||
wy * tilesize + wall.offset + (tilesize - height) / 2f,
|
||||
wx * tilesize + ox,
|
||||
wy * tilesize + oy,
|
||||
width, height,
|
||||
tile.build == null || !wall.rotate ? 0 : tile.build.rotdeg());
|
||||
}else{
|
||||
@@ -123,27 +139,39 @@ public class MapRenderer implements Disposable{
|
||||
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
|
||||
}
|
||||
|
||||
float offsetX = -(wall.size / 3) * tilesize, offsetY = -(wall.size / 3) * tilesize;
|
||||
float offsetX = -((wall.size + 1) / 3) * tilesize, offsetY = -((wall.size + 1) / 3) * tilesize;
|
||||
|
||||
//draw non-synthetic wall or ore
|
||||
if((wall.update || wall.destructible) && center){
|
||||
mesh.setColor(team.color);
|
||||
region = Core.atlas.find("block-border-editor");
|
||||
}else if(!wall.synthetic() && wall != Blocks.air && center){
|
||||
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
|
||||
offsetX = tilesize / 2f - region.width / 2f * Draw.scl;
|
||||
offsetY = tilesize / 2f - region.height / 2f * Draw.scl;
|
||||
}else if(wall == Blocks.air && !tile.overlay().isAir()){
|
||||
region = tile.overlay().editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
|
||||
if(wall.size == 2){
|
||||
offsetX += tilesize;
|
||||
offsetY += tilesize;
|
||||
}
|
||||
}else if(!useSyntheticWall && wall != Blocks.air && center){
|
||||
region = getIcon(wall, idxWall);
|
||||
|
||||
if(wall == Blocks.cliff){
|
||||
mesh.setColor(Tmp.c1.set(floor.mapColor).mul(1.6f));
|
||||
region = ((Cliff)Blocks.cliff).editorCliffs[tile.data & 0xff];
|
||||
}
|
||||
|
||||
offsetX = tilesize / 2f - region.width * region.scl() / 2f;
|
||||
offsetY = tilesize / 2f - region.height * region.scl() / 2f;
|
||||
}else if((wall == Blocks.air || overlay.wallOre) && !overlay.isAir()){
|
||||
if(floor.isLiquid){
|
||||
mesh.setColor(Tmp.c1.set(1f, 1f, 1f, floor.overlayAlpha));
|
||||
}
|
||||
region = overlay.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
|
||||
}else{
|
||||
region = Core.atlas.find("clear-editor");
|
||||
region = clearEditor;
|
||||
}
|
||||
|
||||
float width = region.width * Draw.scl, height = region.height * Draw.scl;
|
||||
float width = region.width * region.scl(), height = region.height * region.scl();
|
||||
if(!wall.synthetic() && wall != Blocks.air && !wall.isMultiblock()){
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
width = tilesize;
|
||||
height = tilesize;
|
||||
offsetX = offsetY = 0f;
|
||||
width = height = tilesize;
|
||||
}
|
||||
|
||||
mesh.draw(idxDecal, region, wx * tilesize + offsetX, wy * tilesize + offsetY, width, height);
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.TextField.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
public class MapResizeDialog extends BaseDialog{
|
||||
public static int minSize = 50, maxSize = 500, increment = 50;
|
||||
int width, height;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public MapResizeDialog(MapEditor editor, Intc2 cons){
|
||||
public class MapResizeDialog extends BaseDialog{
|
||||
public static int minSize = 50, maxSize = 600, increment = 50;
|
||||
|
||||
int width, height, shiftX, shiftY;
|
||||
|
||||
public MapResizeDialog(ResizeListener cons){
|
||||
super("@editor.resizemap");
|
||||
|
||||
closeOnBack();
|
||||
shown(() -> {
|
||||
cont.clear();
|
||||
width = editor.width();
|
||||
@@ -27,10 +31,23 @@ public class MapResizeDialog extends BaseDialog{
|
||||
table.field((w ? width : height) + "", TextFieldFilter.digitsOnly, value -> {
|
||||
int val = Integer.parseInt(value);
|
||||
if(w) width = val; else height = val;
|
||||
}).valid(value -> Strings.canParsePositiveInt(value) && Integer.parseInt(value) <= maxSize && Integer.parseInt(value) >= minSize).addInputDialog(3);
|
||||
}).valid(value -> Strings.canParsePositiveInt(value) && Integer.parseInt(value) <= maxSize && Integer.parseInt(value) >= minSize).maxTextLength(3);
|
||||
|
||||
table.row();
|
||||
}
|
||||
|
||||
for(boolean x : Mathf.booleans){
|
||||
table.add(x ? "@editor.shiftx" : "@editor.shifty").padRight(8f);
|
||||
table.defaults().height(60f).padTop(8);
|
||||
|
||||
table.field((x ? shiftX : shiftY) + "", value -> {
|
||||
int val = Integer.parseInt(value);
|
||||
if(x) shiftX = val; else shiftY = val;
|
||||
}).valid(Strings::canParseInt).maxTextLength(4);
|
||||
|
||||
table.row();
|
||||
}
|
||||
|
||||
cont.row();
|
||||
cont.add(table);
|
||||
|
||||
@@ -39,8 +56,12 @@ public class MapResizeDialog extends BaseDialog{
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.button("@cancel", this::hide);
|
||||
buttons.button("@ok", () -> {
|
||||
cons.get(width, height);
|
||||
cons.get(width, height, shiftX, shiftY);
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
public interface ResizeListener{
|
||||
void get(int width, int height, int shiftX, int shiftY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.graphics.*;
|
||||
@@ -19,7 +18,6 @@ import mindustry.ui.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapView extends Element implements GestureListener{
|
||||
private MapEditor editor;
|
||||
EditorTool tool = EditorTool.pencil;
|
||||
private float offsetx, offsety;
|
||||
private float zoom = 1f;
|
||||
@@ -35,12 +33,12 @@ public class MapView extends Element implements GestureListener{
|
||||
float mousex, mousey;
|
||||
EditorTool lastTool;
|
||||
|
||||
public MapView(MapEditor editor){
|
||||
this.editor = editor;
|
||||
public MapView(){
|
||||
|
||||
for(int i = 0; i < MapEditor.brushSizes.length; i++){
|
||||
float size = MapEditor.brushSizes[i];
|
||||
brushPolygons[i] = Geometry.pixelCircle(size, (index, x, y) -> Mathf.dst(x, y, index, index) <= index - 0.5f);
|
||||
float mod = size % 1f;
|
||||
brushPolygons[i] = Geometry.pixelCircle(size, (index, x, y) -> Mathf.dst(x, y, index - mod, index - mod) <= size - 0.5f);
|
||||
}
|
||||
|
||||
Core.input.getInputProcessors().insert(0, new GestureDetector(20, 0.5f, 2, 0.15f, this));
|
||||
@@ -92,7 +90,7 @@ public class MapView extends Element implements GestureListener{
|
||||
lasty = p.y;
|
||||
startx = p.x;
|
||||
starty = p.y;
|
||||
tool.touched(editor, p.x, p.y);
|
||||
tool.touched(p.x, p.y);
|
||||
firstTouch.set(p);
|
||||
|
||||
if(tool.edit){
|
||||
@@ -115,7 +113,7 @@ public class MapView extends Element implements GestureListener{
|
||||
|
||||
if(tool == EditorTool.line){
|
||||
ui.editor.resetSaved();
|
||||
tool.touchedLine(editor, startx, starty, p.x, p.y);
|
||||
tool.touchedLine(startx, starty, p.x, p.y);
|
||||
}
|
||||
|
||||
editor.flushOp();
|
||||
@@ -136,7 +134,7 @@ public class MapView extends Element implements GestureListener{
|
||||
|
||||
if(drawing && tool.draggable && !(p.x == lastx && p.y == lasty)){
|
||||
ui.editor.resetSaved();
|
||||
Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(editor, cx, cy));
|
||||
Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(cx, cy));
|
||||
}
|
||||
|
||||
if(tool == EditorTool.line && tool.mode == 1){
|
||||
@@ -179,26 +177,26 @@ public class MapView extends Element implements GestureListener{
|
||||
public void act(float delta){
|
||||
super.act(delta);
|
||||
|
||||
if(Core.scene.getKeyboardFocus() == null || !(Core.scene.getKeyboardFocus() instanceof TextField) && !Core.input.keyDown(KeyCode.controlLeft)){
|
||||
if(Core.scene.getKeyboardFocus() == null || !Core.scene.hasField() && !Core.input.keyDown(KeyCode.controlLeft)){
|
||||
float ax = Core.input.axis(Binding.move_x);
|
||||
float ay = Core.input.axis(Binding.move_y);
|
||||
offsetx -= ax * 15f / zoom;
|
||||
offsety -= ay * 15f / zoom;
|
||||
offsetx -= ax * 15 * Time.delta / zoom;
|
||||
offsety -= ay * 15 * Time.delta / zoom;
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.shiftLeft)){
|
||||
if(Core.input.keyTap(KeyCode.shiftLeft) || Core.input.keyTap(KeyCode.altLeft)){
|
||||
lastTool = tool;
|
||||
tool = EditorTool.pick;
|
||||
}
|
||||
|
||||
if(Core.input.keyRelease(KeyCode.shiftLeft) && lastTool != null){
|
||||
if((Core.input.keyRelease(KeyCode.shiftLeft) || Core.input.keyRelease(KeyCode.altLeft)) && lastTool != null){
|
||||
tool = lastTool;
|
||||
lastTool = null;
|
||||
}
|
||||
|
||||
if(Core.scene.getScrollFocus() != this) return;
|
||||
|
||||
zoom += Core.input.axis(KeyCode.scroll) / 10f * zoom;
|
||||
zoom += Core.input.axis(Binding.zoom) / 10f * zoom;
|
||||
clampZoom();
|
||||
}
|
||||
|
||||
@@ -243,14 +241,14 @@ public class MapView extends Element implements GestureListener{
|
||||
|
||||
image.setImageSize(editor.width(), editor.height());
|
||||
|
||||
if(!ScissorStack.push(rect.set(x, y + Core.scene.marginBottom, width, height))){
|
||||
if(!ScissorStack.push(rect.set(x + Core.scene.marginLeft, 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 + Core.scene.marginBottom, sclwidth, sclheight);
|
||||
editor.renderer.draw(centerx - sclwidth / 2 + Core.scene.marginLeft, centery - sclheight / 2 + Core.scene.marginBottom, sclwidth, sclheight);
|
||||
Draw.reset();
|
||||
|
||||
if(grid){
|
||||
@@ -258,6 +256,13 @@ public class MapView extends Element implements GestureListener{
|
||||
image.setBounds(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
image.draw();
|
||||
|
||||
Lines.stroke(2f);
|
||||
Draw.color(Pal.bulletYellowBack);
|
||||
Lines.line(centerx - sclwidth/2f, centery - sclheight/4f, centerx + sclwidth/2f, centery - sclheight/4f);
|
||||
Lines.line(centerx - sclwidth/4f, centery - sclheight/2f, centerx - sclwidth/4f, centery + sclheight/2f);
|
||||
Lines.line(centerx - sclwidth/2f, centery + sclheight/4f, centerx + sclwidth/2f, centery + sclheight/4f);
|
||||
Lines.line(centerx + sclwidth/4f, centery - sclheight/2f, centerx + sclwidth/4f, centery + sclheight/2f);
|
||||
|
||||
Lines.stroke(3f);
|
||||
Draw.color(Pal.accent);
|
||||
Lines.line(centerx - sclwidth/2f, centery, centerx + sclwidth/2f, centery);
|
||||
@@ -295,7 +300,7 @@ public class MapView extends Element implements GestureListener{
|
||||
|
||||
//pencil square outline
|
||||
if(tool == EditorTool.pencil && tool.mode == 1){
|
||||
Lines.square(v.x + scaling/2f, v.y + scaling/2f, scaling * (editor.brushSize + 0.5f));
|
||||
Lines.square(v.x + scaling/2f, v.y + scaling/2f, scaling * ((editor.brushSize == 1.5f ? 1f : editor.brushSize) + 0.5f));
|
||||
}else{
|
||||
Lines.poly(brushPolygons[index], v.x, v.y, scaling);
|
||||
}
|
||||
@@ -324,7 +329,7 @@ public class MapView extends Element implements GestureListener{
|
||||
return Core.scene != null && Core.scene.getKeyboardFocus() != null
|
||||
&& Core.scene.getKeyboardFocus().isDescendantOf(ui.editor)
|
||||
&& ui.editor.isShown() && tool == EditorTool.zoom &&
|
||||
Core.scene.hit(Core.input.mouse().x, Core.input.mouse().y, true) == this;
|
||||
Core.scene.getHoverElement() == this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class SectorGenerateDialog extends BaseDialog{
|
||||
Planet planet = Planets.erekir;
|
||||
int sector = 0, seed = 0;
|
||||
|
||||
public SectorGenerateDialog(){
|
||||
super("@editor.sectorgenerate");
|
||||
setup();
|
||||
}
|
||||
|
||||
void setup(){
|
||||
cont.clear();
|
||||
buttons.clear();
|
||||
|
||||
addCloseButton();
|
||||
|
||||
cont.defaults().left();
|
||||
|
||||
cont.add("@editor.planet").padRight(10f);
|
||||
|
||||
cont.button(planet.localizedName, () -> {
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.cont.pane(p -> {
|
||||
p.background(Tex.button).margin(10f);
|
||||
int i = 0;
|
||||
|
||||
for(var plan : content.planets()){
|
||||
if(plan.generator == null || plan.sectors.size == 0 || !plan.accessible) continue;
|
||||
|
||||
p.button(plan.localizedName, Styles.flatTogglet, () -> {
|
||||
planet = plan;
|
||||
sector = Math.min(sector, planet.sectors.size - 1);
|
||||
seed = 0;
|
||||
dialog.hide();
|
||||
}).size(110f, 45f).checked(planet == plan);
|
||||
|
||||
if(++i % 4 == 0){
|
||||
p.row();
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.setFillParent(false);
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}).size(200f, 40f).get().getLabel().setText(() -> planet.localizedName);
|
||||
|
||||
cont.row();
|
||||
|
||||
cont.add("@editor.sector").padRight(10f);
|
||||
|
||||
cont.field(sector + "", text -> {
|
||||
sector = Strings.parseInt(text);
|
||||
}).width(200f).valid(text -> planet.sectors.size > Strings.parseInt(text, 99999) && Strings.parseInt(text, 9999) >= 0);
|
||||
|
||||
cont.row();
|
||||
|
||||
cont.add("@editor.seed").padRight(10f);
|
||||
|
||||
cont.field(seed + "", text -> {
|
||||
seed = Strings.parseInt(text);
|
||||
}).width(200f).valid(Strings::canParseInt);
|
||||
|
||||
cont.row();
|
||||
|
||||
cont.label(() -> "[ " + planet.sectors.get(sector).getSize() + "x" + planet.sectors.get(sector).getSize() + " ]").color(Pal.accent).center().labelAlign(Align.center).padTop(5).colspan(2);
|
||||
|
||||
buttons.button("@editor.apply", Icon.ok, () -> {
|
||||
ui.loadAnd(() -> {
|
||||
apply();
|
||||
hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void apply(){
|
||||
ui.loadAnd(() -> {
|
||||
editor.clearOp();
|
||||
editor.load(() -> {
|
||||
var sectorobj = planet.sectors.get(sector);
|
||||
|
||||
//remove presets during generation: massive hack, but it works
|
||||
var preset = sectorobj.preset;
|
||||
sectorobj.preset = null;
|
||||
|
||||
logic.reset(); //TODO: is this a good idea? all rules and map state are cleared, but it fixes inconsistent gen
|
||||
world.loadSector(sectorobj, seed, false);
|
||||
|
||||
sectorobj.preset = preset;
|
||||
|
||||
editor.updateRenderer();
|
||||
state.rules.sector = null;
|
||||
//clear extra filters
|
||||
editor.tags.put("genfilters", "{}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
@@ -17,7 +22,6 @@ import mindustry.ui.*;
|
||||
|
||||
public class WaveGraph extends Table{
|
||||
public Seq<SpawnGroup> groups = new Seq<>();
|
||||
public int from = 0, to = 20;
|
||||
|
||||
private Mode mode = Mode.counts;
|
||||
private int[][] values;
|
||||
@@ -26,41 +30,114 @@ public class WaveGraph extends Table{
|
||||
private float maxHealth;
|
||||
private Table colors;
|
||||
private ObjectSet<UnitType> hidden = new ObjectSet<>();
|
||||
private StringBuilder countStr = new StringBuilder();
|
||||
|
||||
private float pan;
|
||||
private float zoom = 1f;
|
||||
private int from = 0, to = 20;
|
||||
private int lastFrom = -1, lastTo = -1;
|
||||
private float lastZoom = -1f;
|
||||
|
||||
private float defaultSpace = Scl.scl(40f);
|
||||
private FloatSeq points = new FloatSeq(40);
|
||||
|
||||
public WaveGraph(){
|
||||
background(Tex.pane);
|
||||
|
||||
scrolled((scroll) -> {
|
||||
zoom -= scroll * 2f / 10f * zoom;
|
||||
clampZoom();
|
||||
});
|
||||
|
||||
touchable = Touchable.enabled;
|
||||
addListener(new InputListener(){
|
||||
|
||||
@Override
|
||||
public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){
|
||||
requestScroll();
|
||||
}
|
||||
});
|
||||
|
||||
addListener(new ElementGestureListener(){
|
||||
@Override
|
||||
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
|
||||
pan -= deltaX/zoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void zoom(InputEvent event, float initialDistance, float distance){
|
||||
if(lastZoom < 0) lastZoom = zoom;
|
||||
|
||||
zoom = distance / initialDistance * lastZoom;
|
||||
clampZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
lastZoom = zoom;
|
||||
}
|
||||
});
|
||||
|
||||
rect((x, y, width, height) -> {
|
||||
Lines.stroke(Scl.scl(3f));
|
||||
countStr.setLength(0);
|
||||
|
||||
Vec2 mouse = stageToLocalCoordinates(Core.input.mouse());
|
||||
|
||||
GlyphLayout lay = Pools.obtain(GlyphLayout.class, GlyphLayout::new);
|
||||
Font font = Fonts.outline;
|
||||
|
||||
int maxY = switch(mode){
|
||||
case counts -> nextStep(max);
|
||||
case health -> nextStep((int)maxHealth);
|
||||
case totals -> nextStep(maxTotal);
|
||||
};
|
||||
|
||||
lay.setText(font, "1");
|
||||
|
||||
float fh = lay.height;
|
||||
float offsetX = Scl.scl(30f), offsetY = Scl.scl(22f) + fh + Scl.scl(5f);
|
||||
float spacing = zoom * defaultSpace;
|
||||
pan = Math.max(pan, (width/2f)/zoom-defaultSpace);
|
||||
|
||||
float graphX = x + offsetX, graphY = y + offsetY, graphW = width - offsetX, graphH = height - offsetY;
|
||||
float spacing = graphW / (values.length - 1);
|
||||
float fh = lay.height;
|
||||
float offsetX = 0f, offsetY = Scl.scl(22f) + fh + Scl.scl(5f);
|
||||
float graphX = x + offsetX - pan * zoom + width/2f, graphY = y + offsetY, graphW = width - offsetX, graphH = height - offsetY;
|
||||
|
||||
float left = (x-graphX)/spacing, right = (x + width - graphX)/spacing;
|
||||
|
||||
//int radius = Mathf.ceil(graphW / spacing / 2f);
|
||||
|
||||
from = (int)left - 1;
|
||||
to = (int)right + 1;
|
||||
|
||||
if(lastFrom != from || lastTo != to){
|
||||
rebuild();
|
||||
}
|
||||
|
||||
lastFrom = from;
|
||||
lastTo = to;
|
||||
|
||||
if(!clipBegin(x + offsetX, y + offsetY, graphW, graphH)) return;
|
||||
|
||||
int selcol = Rect.contains(x, y, width, height, mouse.x, mouse.y) ? Mathf.round((mouse.x - graphX - (from * spacing)) / spacing) : -1;
|
||||
if(selcol + from <= -1) selcol = -1;
|
||||
|
||||
if(mode == Mode.counts){
|
||||
for(UnitType type : used.orderedItems()){
|
||||
Draw.color(color(type));
|
||||
Draw.alpha(parentAlpha);
|
||||
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
for(int i = 0; i < values.length; i++){
|
||||
int val = values[i][type.id];
|
||||
float cx = graphX + i*spacing, cy = 2f + graphY + val * (graphH - 4f) / max;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + val * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}
|
||||
}else if(mode == Mode.totals){
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
Draw.color(Pal.accent);
|
||||
for(int i = 0; i < values.length; i++){
|
||||
@@ -69,13 +146,13 @@ public class WaveGraph extends Table{
|
||||
sum += values[i][type.id];
|
||||
}
|
||||
|
||||
float cx = graphX + i*spacing, cy = 2f + graphY + sum * (graphH - 4f) / maxTotal;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + sum * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}else if(mode == Mode.health){
|
||||
Lines.beginLine();
|
||||
beginLine();
|
||||
|
||||
Draw.color(Pal.health);
|
||||
for(int i = 0; i < values.length; i++){
|
||||
@@ -84,37 +161,62 @@ public class WaveGraph extends Table{
|
||||
sum += (type.health) * values[i][type.id];
|
||||
}
|
||||
|
||||
float cx = graphX + i*spacing, cy = 2f + graphY + sum * (graphH - 4f) / maxHealth;
|
||||
Lines.linePoint(cx, cy);
|
||||
float cx = graphX + (i+from) * spacing, cy = graphY + sum * graphH / maxY;
|
||||
linePoint(cx, cy);
|
||||
}
|
||||
|
||||
Lines.endLine();
|
||||
endLine();
|
||||
}
|
||||
|
||||
//how many numbers can fit here
|
||||
float totalMarks = (height - offsetY - getMarginBottom() *2f - 1f) / (lay.height * 2);
|
||||
|
||||
int markSpace = Math.max(1, Mathf.ceil(max / totalMarks));
|
||||
if(selcol >= 0 && selcol < values.length){
|
||||
Draw.color(1f, 0f, 0f, 0.2f);
|
||||
Fill.crect((selcol+from) * spacing + graphX - spacing/2f, graphY, spacing, graphH);
|
||||
Draw.color();
|
||||
font.getData().setScale(1.5f);
|
||||
for(UnitType type : used.orderedItems()){
|
||||
int amount = values[Mathf.clamp(selcol, 0, values.length - 1)][type.id];
|
||||
if(amount > 0){
|
||||
countStr.append(type.emoji()).append(" ").append(amount).append("\n");
|
||||
}
|
||||
}
|
||||
float pad = Scl.scl(5f);
|
||||
font.draw(countStr, (selcol+from) * spacing + graphX - spacing/2f + pad, graphY + graphH - pad);
|
||||
font.getData().setScale(1f);
|
||||
}
|
||||
|
||||
clipEnd();
|
||||
|
||||
//how many numbers can fit here
|
||||
float totalMarks = Mathf.clamp(maxY, 1, 10);
|
||||
|
||||
int markSpace = Math.max(1, Mathf.ceil(maxY / totalMarks));
|
||||
|
||||
Draw.color(Color.lightGray);
|
||||
for(int i = 0; i < max; i += markSpace){
|
||||
float cy = 2f + y + i * (height - 4f) / max + offsetY, cx = x + offsetX;
|
||||
//Lines.line(cx, cy, cx + len, cy);
|
||||
Draw.alpha(0.1f);
|
||||
|
||||
for(int i = 0; i < maxY; i += markSpace){
|
||||
float cy = graphY + i * graphH / maxY, cx = x;
|
||||
|
||||
Lines.line(cx, cy, cx + graphW, cy);
|
||||
|
||||
lay.setText(font, "" + i);
|
||||
|
||||
font.draw("" + i, cx, cy + lay.height/2f - Scl.scl(3f), Align.right);
|
||||
font.draw("" + i, cx, cy + lay.height / 2f, Align.left);
|
||||
}
|
||||
Draw.alpha(1f);
|
||||
|
||||
float len = Scl.scl(4f);
|
||||
font.setColor(Color.lightGray);
|
||||
|
||||
for(int i = 0; i < values.length; i++){
|
||||
float cy = y + fh, cx = x + graphW / (values.length - 1) * i + offsetX;
|
||||
float cy = y + fh, cx = graphX + spacing * (i + from);
|
||||
|
||||
Lines.line(cx, cy, cx, cy + len);
|
||||
if(i == values.length/2){
|
||||
font.draw("" + (i + from + 1), cx, cy - 2f, Align.center);
|
||||
if(cx >= x + offsetX && cx <= x + offsetX + graphW){
|
||||
Lines.line(cx, cy, cx, cy + len);
|
||||
}
|
||||
if(i == selcol){
|
||||
font.draw("" + (i + from + 1), cx, cy - Scl.scl(2f), Align.center);
|
||||
}
|
||||
}
|
||||
font.setColor(Color.white);
|
||||
@@ -142,6 +244,28 @@ public class WaveGraph extends Table{
|
||||
}).growX();
|
||||
}
|
||||
|
||||
private void clampZoom(){
|
||||
zoom = Mathf.clamp(zoom, 0.5f / Scl.scl(1f), 40f / Scl.scl(1f));
|
||||
}
|
||||
|
||||
private void linePoint(float x, float y){
|
||||
points.add(x, y);
|
||||
}
|
||||
|
||||
private void beginLine(){
|
||||
points.clear();
|
||||
}
|
||||
|
||||
private void endLine(){
|
||||
var items = points.items;
|
||||
for(int i = 0; i < points.size - 2; i += 2){
|
||||
Lines.line(items[i], items[i + 1], items[i + 2], items[i + 3], false);
|
||||
Fill.circle(items[i], items[i + 1], Lines.getStroke()/2f);
|
||||
}
|
||||
Fill.circle(items[points.size - 2], items[points.size - 1], Lines.getStroke());
|
||||
points.clear();
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
values = new int[to - from + 1][Vars.content.units().size];
|
||||
used.clear();
|
||||
@@ -164,20 +288,33 @@ public class WaveGraph extends Table{
|
||||
sum += spawned;
|
||||
}
|
||||
maxTotal = Math.max(maxTotal, sum);
|
||||
maxHealth = Math.max(maxHealth,healthsum);
|
||||
maxHealth = Math.max(maxHealth, healthsum);
|
||||
}
|
||||
|
||||
used.orderedItems().sort();
|
||||
|
||||
ObjectSet<UnitType> usedCopy = new ObjectSet<>(used);
|
||||
|
||||
colors.clear();
|
||||
colors.left();
|
||||
colors.button("@waves.units.hide", Styles.flatt, () -> {
|
||||
if(hidden.size == usedCopy.size){
|
||||
hidden.clear();
|
||||
}else{
|
||||
hidden.addAll(usedCopy);
|
||||
}
|
||||
|
||||
used.clear();
|
||||
used.addAll(usedCopy);
|
||||
for(UnitType o : hidden) used.remove(o);
|
||||
}).update(b -> b.setText(hidden.size == usedCopy.size ? "@waves.units.show" : "@waves.units.hide")).height(32f).width(130f);
|
||||
colors.pane(t -> {
|
||||
t.left();
|
||||
for(UnitType type : used){
|
||||
t.button(b -> {
|
||||
Color tcolor = color(type).cpy();
|
||||
b.image().size(32f).update(i -> i.setColor(b.isChecked() ? Tmp.c1.set(tcolor).mul(0.5f) : tcolor)).get().act(1);
|
||||
b.image(type.icon(Cicon.medium)).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1);
|
||||
b.image(type.uiIcon).size(32f).scaling(Scaling.fit).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1);
|
||||
b.margin(0f);
|
||||
}, Styles.fullTogglet, () -> {
|
||||
if(!hidden.add(type)){
|
||||
@@ -189,7 +326,9 @@ public class WaveGraph extends Table{
|
||||
for(UnitType o : hidden) used.remove(o);
|
||||
}).update(b -> b.setChecked(hidden.contains(type)));
|
||||
}
|
||||
}).get().setScrollingDisabled(false, true);
|
||||
}).scrollY(false);
|
||||
|
||||
colors.act(0.000001f);
|
||||
|
||||
for(UnitType type : hidden){
|
||||
used.remove(type);
|
||||
@@ -200,6 +339,23 @@ public class WaveGraph extends Table{
|
||||
return Tmp.c1.fromHsv(type.id / (float)Vars.content.units().size * 360f, 0.7f, 1f);
|
||||
}
|
||||
|
||||
int nextStep(float value){
|
||||
int order = 1;
|
||||
while(order < value){
|
||||
if(order * 2 > value){
|
||||
return order * 2;
|
||||
}
|
||||
if(order * 5 > value){
|
||||
return order * 5;
|
||||
}
|
||||
if(order * 10 > value){
|
||||
return order * 10;
|
||||
}
|
||||
order *= 10;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
enum Mode{
|
||||
counts, totals, health;
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.TextField.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
@@ -21,121 +27,121 @@ import static mindustry.Vars.*;
|
||||
import static mindustry.game.SpawnGroup.*;
|
||||
|
||||
public class WaveInfoDialog extends BaseDialog{
|
||||
private int displayed = 20;
|
||||
Seq<SpawnGroup> groups = new Seq<>();
|
||||
private @Nullable SpawnGroup expandedGroup;
|
||||
|
||||
private Table table;
|
||||
private int start = 0;
|
||||
private UnitType lastType = UnitTypes.dagger;
|
||||
private float updateTimer, updatePeriod = 1f;
|
||||
private int search = -1;
|
||||
private @Nullable UnitType filterType;
|
||||
private Sort sort = Sort.begin;
|
||||
private boolean reverseSort = false;
|
||||
private boolean checkedSpawns;
|
||||
private WaveGraph graph = new WaveGraph();
|
||||
|
||||
public WaveInfoDialog(MapEditor editor){
|
||||
public WaveInfoDialog(){
|
||||
super("@waves.title");
|
||||
|
||||
shown(this::setup);
|
||||
shown(() -> {
|
||||
checkedSpawns = false;
|
||||
setup();
|
||||
});
|
||||
hidden(() -> state.rules.spawns = groups);
|
||||
|
||||
addCloseListener();
|
||||
|
||||
onResize(this::setup);
|
||||
addCloseButton();
|
||||
|
||||
buttons.button("@waves.edit", () -> {
|
||||
buttons.button("@waves.edit", Icon.edit, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@waves.edit");
|
||||
dialog.addCloseButton();
|
||||
dialog.setFillParent(false);
|
||||
dialog.cont.defaults().size(210f, 64f);
|
||||
dialog.cont.button("@waves.copy", () -> {
|
||||
ui.showInfoFade("@waves.copied");
|
||||
Core.app.setClipboardText(maps.writeWaves(groups));
|
||||
dialog.hide();
|
||||
}).disabled(b -> groups == null);
|
||||
dialog.cont.row();
|
||||
dialog.cont.button("@waves.load", () -> {
|
||||
try{
|
||||
groups = maps.readWaves(Core.app.getClipboardText());
|
||||
dialog.cont.table(Tex.button, t -> {
|
||||
var style = Styles.cleart;
|
||||
t.defaults().size(280f, 64f).pad(2f);
|
||||
|
||||
t.button("@waves.copy", Icon.copy, style, () -> {
|
||||
ui.showInfoFade("@waves.copied");
|
||||
Core.app.setClipboardText(maps.writeWaves(groups));
|
||||
dialog.hide();
|
||||
}).disabled(b -> groups == null || groups.isEmpty()).marginLeft(12f).row();
|
||||
|
||||
t.button("@waves.load", Icon.download, style, () -> {
|
||||
try{
|
||||
groups = maps.readWaves(Core.app.getClipboardText());
|
||||
buildGroups();
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showErrorMessage("@waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("[")).marginLeft(12f).row();
|
||||
|
||||
t.button("@clear", Icon.none, style, () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
|
||||
groups.clear();
|
||||
buildGroups();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
ui.showErrorMessage("@waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
}).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(waves.get());
|
||||
buildGroups();
|
||||
dialog.hide();
|
||||
}));
|
||||
dialog.hide();
|
||||
})).marginLeft(12f).row();
|
||||
|
||||
t.button("@settings.reset", Icon.refresh, style, () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
|
||||
groups = JsonIO.copy(waves.get());
|
||||
buildGroups();
|
||||
dialog.hide();
|
||||
})).marginLeft(12f);
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}).size(270f, 64f);
|
||||
}).size(250f, 64f);
|
||||
|
||||
buttons.defaults().width(60f);
|
||||
|
||||
buttons.button("<", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
shift(-1);
|
||||
}
|
||||
});
|
||||
buttons.button(">", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
shift(1);
|
||||
}
|
||||
});
|
||||
|
||||
buttons.button("-", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
view(-1);
|
||||
}
|
||||
});
|
||||
buttons.button("+", () -> {}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
view(1);
|
||||
}
|
||||
});
|
||||
|
||||
if(experimental){
|
||||
buttons.button("Random", Icon.refresh, () -> {
|
||||
groups.clear();
|
||||
groups = Waves.generate(1f / 10f);
|
||||
updateWaves();
|
||||
}).width(200f);
|
||||
}
|
||||
}
|
||||
|
||||
void view(int amount){
|
||||
updateTimer += Time.delta;
|
||||
if(updateTimer >= updatePeriod){
|
||||
displayed += amount;
|
||||
if(displayed < 5) displayed = 5;
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
}
|
||||
|
||||
void shift(int amount){
|
||||
updateTimer += Time.delta;
|
||||
if(updateTimer >= updatePeriod){
|
||||
start += amount;
|
||||
if(start < 0) start = 0;
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
buttons.button(Core.bundle.get("waves.random"), Icon.refresh, () -> {
|
||||
groups.clear();
|
||||
groups = Waves.generate(1f / 10f);
|
||||
buildGroups();
|
||||
}).width(200f);
|
||||
}
|
||||
|
||||
void setup(){
|
||||
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? waves.get() : state.rules.spawns);
|
||||
if(groups == null) groups = new Seq<>();
|
||||
|
||||
cont.clear();
|
||||
cont.stack(new Table(Tex.clear, main -> {
|
||||
main.pane(t -> table = t).growX().growY().padRight(8f).get().setScrollingDisabled(true, false);
|
||||
main.row();
|
||||
main.button("@add", () -> {
|
||||
if(groups == null) groups = new Seq<>();
|
||||
groups.add(new SpawnGroup(lastType));
|
||||
buildGroups();
|
||||
}).growX().height(70f);
|
||||
main.table(s -> {
|
||||
s.image(Icon.zoom).padRight(8);
|
||||
s.field(search < 0 ? "" : (search + 1) + "", TextFieldFilter.digitsOnly, text -> {
|
||||
search = groups.any() ? Strings.parseInt(text, 0) - 1 : -1;
|
||||
buildGroups();
|
||||
}).growX().maxTextLength(8).get().setMessageText("@waves.search");
|
||||
s.button(Icon.units, Styles.emptyi, () -> showUnits(type -> filterType = type, true)).size(46f).tooltip("@waves.filter")
|
||||
.update(b -> b.getStyle().imageUp = filterType != null ? new TextureRegionDrawable(filterType.uiIcon) : Icon.filter);
|
||||
}).growX().pad(6f).row();
|
||||
|
||||
main.pane(t -> table = t).grow().padRight(8f).scrollX(false).row();
|
||||
|
||||
main.table(t -> {
|
||||
t.button("@add", () -> {
|
||||
showUnits(type -> groups.add(expandedGroup = new SpawnGroup(type)), false);
|
||||
buildGroups();
|
||||
}).growX().height(70f);
|
||||
|
||||
t.button(Icon.filter, () -> {
|
||||
BaseDialog dialog = new BaseDialog("@waves.sort");
|
||||
dialog.setFillParent(false);
|
||||
dialog.cont.table(Tex.button, f -> {
|
||||
for(Sort s : Sort.all){
|
||||
f.button("@waves.sort." + s, Styles.flatTogglet, () -> {
|
||||
sort = s;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).size(150f, 60f).checked(s == sort);
|
||||
}
|
||||
}).row();
|
||||
dialog.cont.check("@waves.sort.reverse", b -> {
|
||||
reverseSort = b;
|
||||
buildGroups();
|
||||
}).padTop(4).checked(reverseSort).padBottom(8f);
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}).size(64f, 70f).padLeft(6f);
|
||||
}).growX();
|
||||
|
||||
}), new Label("@waves.none"){{
|
||||
visible(() -> groups.isEmpty());
|
||||
this.touchable = Touchable.disabled;
|
||||
@@ -154,99 +160,195 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
table.margin(10f);
|
||||
|
||||
if(groups != null){
|
||||
groups.sort(Structs.comps(Structs.comparingFloat(sort.sort), Structs.comparingFloat(sort.secondary)));
|
||||
if(reverseSort) groups.reverse();
|
||||
|
||||
for(SpawnGroup group : groups){
|
||||
if(group.effect == StatusEffects.none) group.effect = null;
|
||||
if((search >= 0 && group.getSpawned(search) <= 0) || (filterType != null && group.type != filterType)) continue;
|
||||
|
||||
table.table(Tex.button, t -> {
|
||||
t.margin(0).defaults().pad(3).padLeft(5f).growX().left();
|
||||
t.button(b -> {
|
||||
b.left();
|
||||
b.image(group.type.icon(Cicon.medium)).size(32f).padRight(3).scaling(Scaling.fit);
|
||||
b.add(group.type.localizedName).color(Pal.accent);
|
||||
b.image(group.type.uiIcon).size(32f).padRight(3).scaling(Scaling.fit);
|
||||
b.add(group.type.localizedName).ellipsis(true).width(110f).left().color(Pal.accent);
|
||||
|
||||
b.add().growX();
|
||||
|
||||
b.button(Icon.cancel, () -> {
|
||||
b.label(() -> (group.begin + 1) + "").color(Color.lightGray).minWidth(45f).labelAlign(Align.left).left();
|
||||
|
||||
b.button(Icon.copySmall, Styles.emptyi, () -> {
|
||||
groups.insert(groups.indexOf(group) + 1, expandedGroup = group.copy());
|
||||
buildGroups();
|
||||
}).pad(-6).size(46f).tooltip("@editor.copy");
|
||||
|
||||
b.button(group.effect != null ?
|
||||
new TextureRegionDrawable(group.effect.uiIcon) :
|
||||
Icon.logicSmall,
|
||||
Styles.emptyi, () -> showEffects(group)).pad(-6).size(46f).scaling(Scaling.fit).tooltip(group.effect != null ? group.effect.localizedName : "@none");
|
||||
|
||||
b.button(Icon.unitsSmall, Styles.emptyi, () -> showUnits(type -> group.type = type, false)).pad(-6).size(46f).tooltip("@stat.unittype");
|
||||
b.button(Icon.cancel, Styles.emptyi, () -> {
|
||||
groups.remove(group);
|
||||
if(expandedGroup == group) expandedGroup = null;
|
||||
table.getCell(t).pad(0f);
|
||||
t.remove();
|
||||
updateWaves();
|
||||
}).pad(-6).size(46f).padRight(-12f);
|
||||
}, () -> showUpdate(group)).height(46f).pad(-6f).padBottom(0f);
|
||||
buildGroups();
|
||||
}).pad(-6).size(46f).padRight(-12f).tooltip("@waves.remove");
|
||||
b.clicked(KeyCode.mouseMiddle, () -> {
|
||||
groups.insert(groups.indexOf(group) + 1, expandedGroup = group.copy());
|
||||
buildGroups();
|
||||
});
|
||||
}, () -> {
|
||||
expandedGroup = expandedGroup == group ? null : group;
|
||||
buildGroups();
|
||||
}).height(46f).pad(-6f).padBottom(0f).row();
|
||||
|
||||
t.row();
|
||||
t.table(spawns -> {
|
||||
spawns.field("" + (group.begin + 1), TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.begin = Strings.parseInt(text) - 1;
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
spawns.add("@waves.to").padLeft(4).padRight(4);
|
||||
spawns.field(group.end == never ? "" : (group.end + 1) + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.end = Strings.parseInt(text) - 1;
|
||||
updateWaves();
|
||||
}else if(text.isEmpty()){
|
||||
group.end = never;
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f).get().setMessageText(Core.bundle.get("waves.never"));
|
||||
});
|
||||
t.row();
|
||||
t.table(p -> {
|
||||
p.add("@waves.every").padRight(4);
|
||||
p.field(group.spacing + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text) && Strings.parseInt(text) > 0){
|
||||
group.spacing = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
p.add("@waves.waves").padLeft(4);
|
||||
});
|
||||
if(expandedGroup == group){
|
||||
t.table(spawns -> {
|
||||
spawns.field("" + (group.begin + 1), TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.begin = Strings.parseInt(text) - 1;
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
spawns.add("@waves.to").padLeft(4).padRight(4);
|
||||
spawns.field(group.end == never ? "" : (group.end + 1) + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.end = Strings.parseInt(text) - 1;
|
||||
updateWaves();
|
||||
}else if(text.isEmpty()){
|
||||
group.end = never;
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f).get().setMessageText("∞");
|
||||
}).row();
|
||||
|
||||
t.row();
|
||||
t.table(a -> {
|
||||
a.field(group.unitAmount + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.unitAmount = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
t.table(p -> {
|
||||
p.add("@waves.every").padRight(4);
|
||||
p.field(group.spacing + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text) && Strings.parseInt(text) > 0){
|
||||
group.spacing = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
p.add("@waves.waves").padLeft(4);
|
||||
}).row();
|
||||
|
||||
a.add(" + ");
|
||||
a.field(Strings.fixed(Math.max((Mathf.zero(group.unitScaling) ? 0 : 1f / group.unitScaling), 0), 2), TextFieldFilter.floatsOnly, text -> {
|
||||
if(Strings.canParsePositiveFloat(text)){
|
||||
group.unitScaling = 1f / Strings.parseFloat(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
a.add("@waves.perspawn").padLeft(4);
|
||||
});
|
||||
t.row();
|
||||
t.table(a -> {
|
||||
a.field((int)group.shields + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.shields = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
t.table(a -> {
|
||||
a.field(group.unitAmount + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.unitAmount = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
|
||||
a.add(" + ");
|
||||
a.field((int)group.shieldScaling + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.shieldScaling = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
a.add("@waves.shields").padLeft(4);
|
||||
});
|
||||
a.add(" + ");
|
||||
a.field(Strings.fixed(Math.max((Mathf.zero(group.unitScaling) ? 0 : 1f / group.unitScaling), 0), 2), TextFieldFilter.floatsOnly, text -> {
|
||||
if(Strings.canParsePositiveFloat(text)){
|
||||
group.unitScaling = 1f / Strings.parseFloat(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
a.add("@waves.perspawn").padLeft(4);
|
||||
}).row();
|
||||
|
||||
t.row();
|
||||
t.check("@waves.guardian", b -> group.effect = (b ? StatusEffects.boss : null)).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f);
|
||||
t.table(a -> {
|
||||
a.field(group.max + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.max = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
|
||||
a.add("@waves.max").padLeft(5);
|
||||
}).row();
|
||||
|
||||
t.table(a -> {
|
||||
a.field((int)group.shields + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.shields = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
|
||||
a.add(" + ");
|
||||
a.field((int)group.shieldScaling + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePositiveInt(text)){
|
||||
group.shieldScaling = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
a.add("@waves.shields").padLeft(4);
|
||||
}).row();
|
||||
|
||||
t.check("@waves.guardian", b -> {
|
||||
group.effect = (b ? StatusEffects.boss : null);
|
||||
buildGroups();
|
||||
}).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f).row();
|
||||
|
||||
t.table(a -> {
|
||||
a.add("@waves.spawn").padRight(8);
|
||||
|
||||
a.button("", () -> {
|
||||
if(!checkedSpawns){
|
||||
//recalculate waves when changed
|
||||
Vars.spawner.reset();
|
||||
checkedSpawns = true;
|
||||
}
|
||||
|
||||
BaseDialog dialog = new BaseDialog("@waves.spawn.select");
|
||||
dialog.cont.pane(p -> {
|
||||
p.background(Tex.button).margin(10f);
|
||||
int i = 0;
|
||||
int cols = 4;
|
||||
int max = 20;
|
||||
|
||||
if(spawner.getSpawns().size >= max){
|
||||
p.add("[lightgray](first " + max + ")").colspan(cols).padBottom(4).row();
|
||||
}
|
||||
|
||||
for(var spawn : spawner.getSpawns()){
|
||||
p.button(spawn.x + ", " + spawn.y, Styles.flatTogglet, () -> {
|
||||
group.spawn = Point2.pack(spawn.x, spawn.y);
|
||||
dialog.hide();
|
||||
}).size(110f, 45f).checked(spawn.pos() == group.spawn);
|
||||
|
||||
if(++i % cols == 0){
|
||||
p.row();
|
||||
}
|
||||
|
||||
//only display first 20 spawns, you don't need to see more.
|
||||
if(i >= 20){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(spawner.getSpawns().isEmpty()){
|
||||
p.add("@waves.spawn.none");
|
||||
}else{
|
||||
p.button("@waves.spawn.all", Styles.flatTogglet, () -> {
|
||||
group.spawn = -1;
|
||||
dialog.hide();
|
||||
}).size(110f, 45f).checked(-1 == group.spawn);
|
||||
}
|
||||
}).grow();
|
||||
dialog.setFillParent(false);
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}).width(160f).height(36f).get().getLabel().setText(() -> group.spawn == -1 ? "@waves.spawn.all" : Point2.x(group.spawn) + ", " + Point2.y(group.spawn));
|
||||
|
||||
}).padBottom(8f).row();
|
||||
}
|
||||
}).width(340f).pad(8);
|
||||
|
||||
table.row();
|
||||
}
|
||||
|
||||
if(table.getChildren().isEmpty() && groups.any()){
|
||||
table.add("@none.found");
|
||||
}
|
||||
}else{
|
||||
table.add("@editor.default");
|
||||
}
|
||||
@@ -254,33 +356,93 @@ public class WaveInfoDialog extends BaseDialog{
|
||||
updateWaves();
|
||||
}
|
||||
|
||||
void showUpdate(SpawnGroup group){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.setFillParent(true);
|
||||
void showUnits(Cons<UnitType> cons, boolean reset){
|
||||
BaseDialog dialog = new BaseDialog(reset ? "@waves.filter" : "");
|
||||
dialog.cont.pane(p -> {
|
||||
int i = 0;
|
||||
p.defaults().pad(2).fillX();
|
||||
if(reset){
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
t.image(Icon.none).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add("@settings.resetKey");
|
||||
}, () -> {
|
||||
cons.get(null);
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).margin(12f);
|
||||
}
|
||||
int i = reset ? 1 : 0;
|
||||
for(UnitType type : content.units()){
|
||||
if(type.isHidden()) continue;
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
t.image(type.icon(Cicon.medium)).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.image(type.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add(type.localizedName);
|
||||
}, () -> {
|
||||
lastType = type;
|
||||
group.type = type;
|
||||
cons.get(type);
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).pad(2).margin(12f).fillX();
|
||||
}).margin(12f);
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
});
|
||||
}).growX().scrollX(false);
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
void showEffects(SpawnGroup group){
|
||||
BaseDialog dialog = new BaseDialog("");
|
||||
dialog.cont.pane(p -> {
|
||||
p.defaults().pad(2).fillX();
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
t.image(Icon.none).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add("@settings.resetKey");
|
||||
}, () -> {
|
||||
group.effect = null;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).margin(12f);
|
||||
int i = 1;
|
||||
for(StatusEffect effect : content.statusEffects()){
|
||||
if(effect.isHidden() || effect.reactive) continue;
|
||||
p.button(t -> {
|
||||
t.left();
|
||||
t.image(effect.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f);
|
||||
t.add(effect.localizedName);
|
||||
}, () -> {
|
||||
group.effect = effect;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).margin(12f);
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
}).growX().scrollX(false);
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
enum Sort{
|
||||
begin(g -> g.begin, g -> g.type.id),
|
||||
health(g -> g.type.health),
|
||||
type(g -> g.type.id);
|
||||
|
||||
static final Sort[] all = values();
|
||||
|
||||
final Floatf<SpawnGroup> sort, secondary;
|
||||
|
||||
Sort(Floatf<SpawnGroup> sort){
|
||||
this(sort, g -> g.begin);
|
||||
}
|
||||
|
||||
Sort(Floatf<SpawnGroup> sort, Floatf<SpawnGroup> secondary){
|
||||
this.sort = sort;
|
||||
this.secondary = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
void updateWaves(){
|
||||
graph.groups = groups;
|
||||
graph.from = start;
|
||||
graph.to = start + displayed;
|
||||
graph.rebuild();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ 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 arc.util.pooling.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.game.EventType.*;
|
||||
@@ -15,33 +16,81 @@ import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Utility class for damaging in an area. */
|
||||
public class Damage{
|
||||
private static final UnitDamageEvent bulletDamageEvent = new UnitDamageEvent();
|
||||
private static final Rect rect = new Rect();
|
||||
private static final Rect hitrect = new Rect();
|
||||
private static final Vec2 vec = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2();
|
||||
private static final IntSet collidedBlocks = new IntSet();
|
||||
private static final IntFloatMap damages = new IntFloatMap();
|
||||
private static final Seq<Collided> collided = new Seq<>();
|
||||
private static final Pool<Collided> collidePool = Pools.get(Collided.class, Collided::new);
|
||||
private static final Seq<Building> builds = new Seq<>();
|
||||
private static final FloatSeq distances = new FloatSeq();
|
||||
|
||||
private static Tile furthest;
|
||||
private static Rect rect = new Rect();
|
||||
private static Rect hitrect = new Rect();
|
||||
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();
|
||||
private static float maxDst = 0f;
|
||||
private static Building tmpBuilding;
|
||||
private static Unit tmpUnit;
|
||||
|
||||
public static void applySuppression(Team team, float x, float y, float range, float reload, float maxDelay, float applyParticleChance, @Nullable Position source){
|
||||
applySuppression(team, x, y, range, reload, maxDelay, applyParticleChance, source, Pal.sapBullet);
|
||||
}
|
||||
|
||||
public static void applySuppression(Team team, float x, float y, float range, float reload, float maxDelay, float applyParticleChance, @Nullable Position source, Color effectColor){
|
||||
builds.clear();
|
||||
indexer.eachBlock(null, x, y, range, build -> build.team != team, build -> {
|
||||
float prev = build.healSuppressionTime;
|
||||
build.applyHealSuppression(reload + 1f, effectColor);
|
||||
|
||||
//TODO maybe should be block field instead of instanceof check
|
||||
if(build.wasRecentlyHealed(60f * 12f) || build.block.suppressable){
|
||||
|
||||
//add prev check so ability spam doesn't lead to particle spam (essentially, recently suppressed blocks don't get new particles)
|
||||
if(!headless && prev - Time.time <= reload/2f){
|
||||
builds.add(build);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//to prevent particle spam, the amount of particles is to remain constant (scales with number of buildings)
|
||||
float scaledChance = applyParticleChance / builds.size;
|
||||
for(var build : builds){
|
||||
if(Mathf.chance(scaledChance)){
|
||||
Time.run(Mathf.random(maxDelay), () -> {
|
||||
Fx.regenSuppressSeek.at(build.x + Mathf.range(build.block.size * tilesize / 2f), build.y + Mathf.range(build.block.size * tilesize / 2f), 0f, effectColor, source);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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){
|
||||
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null);
|
||||
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, Fx.dynamicExplosion);
|
||||
}
|
||||
|
||||
/** 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, Effect explosionFx){
|
||||
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx);
|
||||
}
|
||||
|
||||
/** 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){
|
||||
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, Fx.dynamicExplosion);
|
||||
}
|
||||
|
||||
/** 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, Effect explosionFx){
|
||||
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(power / 700, 0, 8); i++){
|
||||
int length = 5 + Mathf.clamp((int)(Mathf.pow(power, 0.98f) / 500), 1, 18);
|
||||
Time.run(i * 0.8f + Mathf.random(4f), () -> Lightning.create(Team.derelict, Pal.power, 3 + Mathf.pow(power, 0.35f), x, y, Mathf.random(360f), length + Mathf.range(2)));
|
||||
}
|
||||
|
||||
if(fire){
|
||||
@@ -50,12 +99,17 @@ public class Damage{
|
||||
}
|
||||
}
|
||||
|
||||
int waves = Mathf.clamp((int)(explosiveness / 4), 0, 30);
|
||||
int waves = explosiveness <= 2 ? 0 : Mathf.clamp((int)(explosiveness / 11), 1, 25);
|
||||
float damagePerWave = explosiveness / 2f;
|
||||
|
||||
for(int i = 0; i < waves; i++){
|
||||
var shields = ignoreTeam == null ? null : indexer.getEnemy(ignoreTeam, BlockFlag.shield);
|
||||
int f = i;
|
||||
Time.run(i * 2f, () -> {
|
||||
Damage.damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f, false);
|
||||
if(shields == null || shields.isEmpty() || !shields.contains(b -> b instanceof ExplosionShield s && s.absorbExplosion(x, y, damagePerWave))){
|
||||
damage(ignoreTeam, x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), damagePerWave, false);
|
||||
}
|
||||
|
||||
Fx.blockExplosionSmoke.at(x + Mathf.range(radius), y + Mathf.range(radius));
|
||||
});
|
||||
}
|
||||
@@ -71,7 +125,7 @@ public class Damage{
|
||||
|
||||
float shake = Math.min(explosiveness / 4f + 3f, 9f);
|
||||
Effect.shake(shake, shake, x, y);
|
||||
Fx.dynamicExplosion.at(x, y, radius / 8f);
|
||||
explosionFx.at(x, y, radius / 8f);
|
||||
}
|
||||
|
||||
public static void createIncend(float x, float y, float range, int amount){
|
||||
@@ -85,22 +139,86 @@ public class Damage{
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Building findAbsorber(Team team, float x1, float y1, float x2, float y2){
|
||||
tmpBuilding = null;
|
||||
|
||||
boolean found = World.raycast(World.toTile(x1), World.toTile(y1), World.toTile(x2), World.toTile(y2),
|
||||
(x, y) -> (tmpBuilding = world.build(x, y)) != null && tmpBuilding.team != team && tmpBuilding.block.absorbLasers);
|
||||
|
||||
return found ? tmpBuilding : null;
|
||||
}
|
||||
|
||||
public static float findLength(Bullet b, float length, boolean laser, int pierceCap){
|
||||
if(pierceCap > 0){
|
||||
length = findPierceLength(b, pierceCap, laser, length);
|
||||
}else if(laser){
|
||||
length = findLaserLength(b, length);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public static float findLaserLength(Bullet b, float length){
|
||||
Tmp.v1.trns(b.rotation(), length);
|
||||
vec.trnsExact(b.rotation(), length);
|
||||
|
||||
furthest = null;
|
||||
|
||||
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);
|
||||
boolean found = World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + vec.x), World.toTile(b.y + vec.y),
|
||||
(x, y) -> (furthest = world.tile(x, y)) != null && furthest.team() != b.team && (furthest.build != null && furthest.build.absorbLasers()));
|
||||
|
||||
return found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length;
|
||||
}
|
||||
|
||||
/** Collides a bullet with blocks in a laser, taking into account absorption blocks. Resulting length is stored in the bullet's fdata. */
|
||||
public static float collideLaser(Bullet b, float length, boolean large){
|
||||
float resultLength = findLaserLength(b, length);
|
||||
public static float findPierceLength(Bullet b, int pierceCap, float length){
|
||||
return findPierceLength(b, pierceCap, b.type.laserAbsorb, length);
|
||||
}
|
||||
|
||||
collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), resultLength, large);
|
||||
public static float findPierceLength(Bullet b, int pierceCap, boolean laser, float length){
|
||||
vec.trnsExact(b.rotation(), length);
|
||||
rect.setPosition(b.x, b.y).setSize(vec.x, vec.y).normalize().grow(3f);
|
||||
|
||||
maxDst = Float.POSITIVE_INFINITY;
|
||||
|
||||
distances.clear();
|
||||
|
||||
if(b.type.collidesGround && b.type.collidesTiles){
|
||||
World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + vec.x), World.toTile(b.y + vec.y), (x, y) -> {
|
||||
//add distance to list so it can be processed
|
||||
var build = world.build(x, y);
|
||||
|
||||
if(build != null && build.team != b.team && build.collide(b) && b.checkUnderBuild(build, x * tilesize, y * tilesize)){
|
||||
distances.add(b.dst(build));
|
||||
|
||||
if(laser && build.absorbLasers()){
|
||||
maxDst = Math.min(maxDst, b.dst(build));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Units.nearbyEnemies(b.team, rect, u -> {
|
||||
u.hitbox(hitrect);
|
||||
|
||||
if(u.checkTarget(b.type.collidesAir, b.type.collidesGround) && u.hittable() && Intersector.intersectSegmentRectangle(b.x, b.y, b.x + vec.x, b.y + vec.y, hitrect)){
|
||||
distances.add(u.dst(b));
|
||||
}
|
||||
});
|
||||
|
||||
distances.sort();
|
||||
|
||||
//return either the length when not enough things were pierced,
|
||||
//or the last pierced object if there were enough blockages
|
||||
return Math.min(distances.size < pierceCap || pierceCap < 0 ? length : Math.max(6f, distances.get(pierceCap - 1)), maxDst);
|
||||
}
|
||||
|
||||
/** Collides a bullet with blocks in a laser, taking into account absorption blocks. Resulting length is stored in the bullet's fdata. */
|
||||
public static float collideLaser(Bullet b, float length, boolean large, boolean laser, int pierceCap){
|
||||
float resultLength = findPierceLength(b, pierceCap, laser, length);
|
||||
|
||||
collideLine(b, b.team, b.type.hitEffect, b.x, b.y, b.rotation(), resultLength, large, laser, pierceCap);
|
||||
|
||||
b.fdata = resultLength;
|
||||
|
||||
@@ -116,88 +234,128 @@ public class Damage{
|
||||
* Only enemies of the specified team are damaged.
|
||||
*/
|
||||
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large){
|
||||
length = findLaserLength(hitter, length);
|
||||
collideLine(hitter, team, effect, x, y, angle, length, large, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Damages entities in a line.
|
||||
* Only enemies of the specified team are damaged.
|
||||
*/
|
||||
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large, boolean laser){
|
||||
collideLine(hitter, team, effect, x, y, angle, length, large, laser, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Damages entities in a line.
|
||||
* Only enemies of the specified team are damaged.
|
||||
*/
|
||||
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large, boolean laser, int pierceCap){
|
||||
length = findLength(hitter, length, laser, pierceCap);
|
||||
hitter.fdata = length;
|
||||
|
||||
collidedBlocks.clear();
|
||||
tr.trns(angle, length);
|
||||
vec.trnsExact(angle, length);
|
||||
|
||||
Intc2 collider = (cx, cy) -> {
|
||||
Building tile = world.build(cx, cy);
|
||||
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){
|
||||
if(hitter.type.collidesGround && hitter.type.collidesTiles){
|
||||
seg1.set(x, y);
|
||||
seg2.set(seg1).add(tr);
|
||||
world.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {
|
||||
collider.get(cx, cy);
|
||||
seg2.set(seg1).add(vec);
|
||||
World.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {
|
||||
Building tile = world.build(cx, cy);
|
||||
boolean collide = tile != null && tile.collide(hitter) && hitter.checkUnderBuild(tile, cx * tilesize, cy * tilesize)
|
||||
&& ((tile.team != team && tile.collide(hitter)) || hitter.type.testCollision(hitter, tile)) && collidedBlocks.add(tile.pos());
|
||||
if(collide){
|
||||
collided.add(collidePool.obtain().set(cx * tilesize, cy * tilesize, tile));
|
||||
|
||||
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);
|
||||
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)))){
|
||||
Building build = other.build;
|
||||
if(build != null && hitter.checkUnderBuild(build, cx * tilesize, cy * tilesize) && collidedBlocks.add(build.pos())){
|
||||
collided.add(collidePool.obtain().set((p.x + cx * tilesize), (p.y + cy) * tilesize, build));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
rect.setPosition(x, y).setSize(tr.x, tr.y);
|
||||
float x2 = tr.x + x, y2 = tr.y + y;
|
||||
|
||||
if(rect.width < 0){
|
||||
rect.x += rect.width;
|
||||
rect.width *= -1;
|
||||
}
|
||||
|
||||
if(rect.height < 0){
|
||||
rect.y += rect.height;
|
||||
rect.height *= -1;
|
||||
}
|
||||
|
||||
float expand = 3f;
|
||||
|
||||
rect.y -= expand;
|
||||
rect.x -= expand;
|
||||
rect.width += expand * 2;
|
||||
rect.height += expand * 2;
|
||||
|
||||
Cons<Unit> cons = e -> {
|
||||
e.hitbox(hitrect);
|
||||
|
||||
Vec2 vec = Geometry.raycastRect(x, y, x2, y2, hitrect.grow(expand * 2));
|
||||
|
||||
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.clear();
|
||||
rect.setPosition(x, y).setSize(vec.x, vec.y).normalize().grow(expand * 2f);
|
||||
float x2 = vec.x + x, y2 = vec.y + y;
|
||||
|
||||
Units.nearbyEnemies(team, rect, u -> {
|
||||
if(u.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround)){
|
||||
units.add(u);
|
||||
if(u.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround) && u.hittable()){
|
||||
u.hitbox(hitrect);
|
||||
|
||||
Vec2 vec = Geometry.raycastRect(x, y, x2, y2, hitrect.grow(expand * 2));
|
||||
|
||||
if(vec != null){
|
||||
collided.add(collidePool.obtain().set(vec.x, vec.y, u));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
units.sort(u -> u.dst2(hitter));
|
||||
units.each(cons);
|
||||
int[] collideCount = {0};
|
||||
collided.sort(c -> hitter.dst2(c.x, c.y));
|
||||
collided.each(c -> {
|
||||
if(hitter.damage > 0 && (pierceCap <= 0 || collideCount[0] < pierceCap)){
|
||||
if(c.target instanceof Unit u){
|
||||
u.collision(hitter, c.x, c.y);
|
||||
hitter.collision(u, c.x, c.y);
|
||||
collideCount[0]++;
|
||||
}else if(c.target instanceof Building tile){
|
||||
float health = tile.health;
|
||||
|
||||
if(tile.team != team && tile.collide(hitter)){
|
||||
tile.collision(hitter);
|
||||
hitter.type.hit(hitter, c.x, c.y);
|
||||
collideCount[0]++;
|
||||
}
|
||||
|
||||
//try to heal the tile
|
||||
if(hitter.type.testCollision(hitter, tile)){
|
||||
hitter.type.hitTile(hitter, tile, c.x, c.y, health, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collidePool.freeAll(collided);
|
||||
collided.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Damages entities on a point.
|
||||
* Only enemies of the specified team are damaged.
|
||||
*/
|
||||
public static void collidePoint(Bullet hitter, Team team, Effect effect, float x, float y){
|
||||
|
||||
if(hitter.type.collidesGround){
|
||||
Building build = world.build(World.toTile(x), World.toTile(y));
|
||||
|
||||
if(build != null && hitter.damage > 0){
|
||||
float health = build.health;
|
||||
|
||||
if(build.team != team && build.collide(hitter)){
|
||||
build.collision(hitter);
|
||||
hitter.type.hit(hitter, x, y);
|
||||
}
|
||||
|
||||
//try to heal the tile
|
||||
if(hitter.type.testCollision(hitter, build)){
|
||||
hitter.type.hitTile(hitter, build, x, y, health, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Units.nearbyEnemies(team, rect.setCentered(x, y, 1f), u -> {
|
||||
if(u.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround) && u.hittable()){
|
||||
u.collision(hitter, x, y);
|
||||
hitter.collision(u, x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,12 +363,12 @@ public class Damage{
|
||||
* @return the first encountered object.
|
||||
*/
|
||||
public static Healthc linecast(Bullet hitter, float x, float y, float angle, float length){
|
||||
tr.trns(angle, length);
|
||||
vec.trns(angle, length);
|
||||
|
||||
tmpBuilding = null;
|
||||
|
||||
if(hitter.type.collidesGround){
|
||||
tmpBuilding = null;
|
||||
|
||||
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
|
||||
World.raycastEachWorld(x, y, x + vec.x, y + vec.y, (cx, cy) -> {
|
||||
Building tile = world.build(cx, cy);
|
||||
if(tile != null && tile.team != hitter.team){
|
||||
tmpBuilding = tile;
|
||||
@@ -218,12 +376,10 @@ public class Damage{
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if(tmpBuilding != null) return tmpBuilding;
|
||||
}
|
||||
|
||||
rect.setPosition(x, y).setSize(tr.x, tr.y);
|
||||
float x2 = tr.x + x, y2 = tr.y + y;
|
||||
rect.setPosition(x, y).setSize(vec.x, vec.y);
|
||||
float x2 = vec.x + x, y2 = vec.y + y;
|
||||
|
||||
if(rect.width < 0){
|
||||
rect.x += rect.width;
|
||||
@@ -244,8 +400,8 @@ public class Damage{
|
||||
|
||||
tmpUnit = null;
|
||||
|
||||
Cons<Unit> cons = e -> {
|
||||
if((tmpUnit != null && e.dst2(x, y) > tmpUnit.dst2(x, y)) || !e.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround)) return;
|
||||
Units.nearbyEnemies(hitter.team, rect, e -> {
|
||||
if((tmpUnit != null && e.dst2(x, y) > tmpUnit.dst2(x, y)) || !e.checkTarget(hitter.type.collidesAir, hitter.type.collidesGround) || !e.targetable(hitter.team)) return;
|
||||
|
||||
e.hitbox(hitrect);
|
||||
Rect other = hitrect;
|
||||
@@ -259,9 +415,15 @@ public class Damage{
|
||||
if(vec != null){
|
||||
tmpUnit = e;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Units.nearbyEnemies(hitter.team, rect, cons);
|
||||
if(tmpBuilding != null && tmpUnit != null){
|
||||
if(Mathf.dst2(x, y, tmpBuilding.getX(), tmpBuilding.getY()) <= Mathf.dst2(x, y, tmpUnit.getX(), tmpUnit.getY())){
|
||||
return tmpBuilding;
|
||||
}
|
||||
}else if(tmpBuilding != null){
|
||||
return tmpBuilding;
|
||||
}
|
||||
|
||||
return tmpUnit;
|
||||
}
|
||||
@@ -269,7 +431,7 @@ public class Damage{
|
||||
/** Damages all entities and blocks in a radius that are enemies of the team. */
|
||||
public static void damageUnits(Team team, float x, float y, float size, float damage, Boolf<Unit> predicate, Cons<Unit> acceptor){
|
||||
Cons<Unit> cons = entity -> {
|
||||
if(!predicate.get(entity)) return;
|
||||
if(!predicate.get(entity) || !entity.hittable()) return;
|
||||
|
||||
entity.hitbox(hitrect);
|
||||
if(!hitrect.overlaps(rect)){
|
||||
@@ -305,7 +467,7 @@ public class Damage{
|
||||
/** Applies a status effect to all enemy units in a range. */
|
||||
public static void status(Team team, float x, float y, float radius, StatusEffect effect, float duration, boolean air, boolean ground){
|
||||
Cons<Unit> cons = entity -> {
|
||||
if(entity.team == team || !entity.within(x, y, radius) || (entity.isFlying() && !air) || (entity.isGrounded() && !ground)){
|
||||
if(entity.team == team || !entity.checkTarget(air, ground) || !entity.hittable() || !entity.within(x, y, radius)){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,17 +489,34 @@ public class Damage{
|
||||
|
||||
/** Damages all entities and blocks in a radius that are enemies of the team. */
|
||||
public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground){
|
||||
Cons<Unit> cons = entity -> {
|
||||
if(entity.team == team || !entity.within(x, y, radius) || (entity.isFlying() && !air) || (entity.isGrounded() && !ground)){
|
||||
damage(team, x, y, radius, damage, complete, air, ground, false, null);
|
||||
}
|
||||
|
||||
/** Damages all entities and blocks in a radius that are enemies of the team. */
|
||||
public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground, boolean scaled, @Nullable Bullet source){
|
||||
Cons<Unit> cons = unit -> {
|
||||
if(unit.team == team || !unit.checkTarget(air, ground) || !unit.hittable() || !unit.within(x, y, radius + (scaled ? unit.hitSize / 2f : 0f))){
|
||||
return;
|
||||
}
|
||||
float amount = calculateDamage(x, y, entity.getX(), entity.getY(), radius, damage);
|
||||
entity.damage(amount);
|
||||
//TODO better velocity displacement
|
||||
float dst = tr.set(entity.getX() - x, entity.getY() - y).len();
|
||||
entity.vel.add(tr.setLength((1f - dst / radius) * 2f / entity.mass()));
|
||||
|
||||
if(complete && damage >= 9999999f && entity.isPlayer()){
|
||||
boolean dead = unit.dead;
|
||||
|
||||
float amount = calculateDamage(scaled ? Math.max(0, unit.dst(x, y) - unit.type.hitSize/2) : unit.dst(x, y), radius, damage);
|
||||
unit.damage(amount);
|
||||
|
||||
if(source != null){
|
||||
Events.fire(bulletDamageEvent.set(unit, source));
|
||||
unit.controller().hit(source);
|
||||
|
||||
if(!dead && unit.dead){
|
||||
Events.fire(new UnitBulletDestroyEvent(unit, source));
|
||||
}
|
||||
}
|
||||
//TODO better velocity displacement
|
||||
float dst = vec.set(unit.x - x, unit.y - y).len();
|
||||
unit.vel.add(vec.setLength((1f - dst / radius) * 2f / unit.mass()));
|
||||
|
||||
if(complete && damage >= 9999999f && unit.isPlayer()){
|
||||
Events.fire(Trigger.exclusionDeath);
|
||||
}
|
||||
};
|
||||
@@ -351,89 +530,130 @@ public class Damage{
|
||||
|
||||
if(ground){
|
||||
if(!complete){
|
||||
int trad = (int)(radius / tilesize);
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
if(tile != null){
|
||||
tileDamage(team, tile.x, tile.y, trad, damage);
|
||||
}
|
||||
tileDamage(team, World.toTile(x), World.toTile(y), radius / tilesize, damage * (source == null ? 1f : source.type.buildingDamageMultiplier), source);
|
||||
}else{
|
||||
completeDamage(team, x, y, radius, damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void tileDamage(Team team, int startx, int starty, int baseRadius, float baseDamage){
|
||||
//tile damage is posted, so that destroying a block that causes a chain explosion will run in the next frame
|
||||
//this prevents recursive damage calls from messing up temporary variables
|
||||
public static void tileDamage(Team team, int x, int y, float baseRadius, float damage){
|
||||
tileDamage(team, x, y, baseRadius, damage, null);
|
||||
}
|
||||
|
||||
public static void tileDamage(Team team, int x, int y, float baseRadius, float damage, @Nullable Bullet source){
|
||||
Core.app.post(() -> {
|
||||
var in = world.build(x, y);
|
||||
//spawned inside a multiblock. this means that damage needs to be dealt directly.
|
||||
//why? because otherwise the building would absorb everything in one cell, which means much less damage than a nearby explosion.
|
||||
//this needs to be compensated
|
||||
if(in != null && in.team != team && in.block.size > 1 && in.health > damage){
|
||||
//deal the damage of an entire side, to be equivalent with maximum 'standard' damage
|
||||
in.damage(team, damage * Math.min((in.block.size), baseRadius * 0.4f));
|
||||
//no need to continue with the explosion
|
||||
return;
|
||||
}
|
||||
|
||||
bits.clear();
|
||||
propagation.clear();
|
||||
int bitOffset = bits.width() / 2;
|
||||
//cap radius to prevent lag
|
||||
float radius = Math.min(baseRadius, 100), rad2 = radius * radius;
|
||||
int rays = Mathf.ceil(radius * 2 * Mathf.pi);
|
||||
double spacing = Math.PI * 2.0 / rays;
|
||||
damages.clear();
|
||||
|
||||
propagation.addFirst(PropCell.get((byte)0, (byte)0, (short)baseDamage));
|
||||
//clamp radius to fit bits
|
||||
int radius = Math.min(baseRadius, bits.width() / 2);
|
||||
//raycast from each angle
|
||||
for(int i = 0; i <= rays; i++){
|
||||
float dealt = 0f;
|
||||
int startX = x;
|
||||
int startY = y;
|
||||
int endX = x + (int)(Math.cos(spacing * i) * radius), endY = y + (int)(Math.sin(spacing * i) * radius);
|
||||
|
||||
while(!propagation.isEmpty()){
|
||||
int prop = propagation.removeLast();
|
||||
int x = PropCell.x(prop);
|
||||
int y = PropCell.y(prop);
|
||||
int damage = PropCell.damage(prop);
|
||||
//manhattan distance used for calculating falloff, results in a diamond pattern
|
||||
int dst = Math.abs(x) + Math.abs(y);
|
||||
int xDist = Math.abs(endX - startX);
|
||||
int yDist = -Math.abs(endY - startY);
|
||||
int xStep = (startX < endX ? +1 : -1);
|
||||
int yStep = (startY < endY ? +1 : -1);
|
||||
int error = xDist + yDist;
|
||||
|
||||
int scaledDamage = (int)(damage * (1f - (float)dst / radius));
|
||||
while(startX != endX || startY != endY){
|
||||
var build = world.build(startX, startY);
|
||||
if(build != null && build.team != team){
|
||||
//damage dealt at circle edge
|
||||
float edgeScale = 0.6f;
|
||||
float mult = (1f-(Mathf.dst2(startX, startY, x, y) / rad2) + edgeScale) / (1f + edgeScale);
|
||||
float next = damage * mult - dealt;
|
||||
//register damage dealt
|
||||
int p = Point2.pack(startX, startY);
|
||||
damages.put(p, Math.max(damages.get(p), next));
|
||||
//register as hit
|
||||
dealt += build.health;
|
||||
|
||||
bits.set(bitOffset + x, bitOffset + y);
|
||||
Tile tile = world.tile(startx + x, starty + y);
|
||||
if(next - dealt <= 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(scaledDamage <= 0 || tile == null) continue;
|
||||
|
||||
//apply damage to entity if needed
|
||||
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;
|
||||
|
||||
if(scaledDamage <= 0) continue;
|
||||
if(2 * error - yDist > xDist - 2 * error){
|
||||
error += yDist;
|
||||
startX += xStep;
|
||||
}else{
|
||||
error += xDist;
|
||||
startY += yStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Point2 p : Geometry.d4){
|
||||
if(!bits.get(bitOffset + x + p.x, bitOffset + y + p.y)){
|
||||
propagation.addFirst(PropCell.get((byte)(x + p.x), (byte)(y + p.y), (short)scaledDamage));
|
||||
//apply damage
|
||||
for(var e : damages){
|
||||
int cx = Point2.x(e.key), cy = Point2.y(e.key);
|
||||
var build = world.build(cx, cy);
|
||||
if(build != null){
|
||||
if(source != null){
|
||||
build.damage(source, team, e.value);
|
||||
}else{
|
||||
build.damage(team, e.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static void completeDamage(Team team, float x, float y, float radius, float damage){
|
||||
|
||||
int trad = (int)(radius / tilesize);
|
||||
for(int dx = -trad; dx <= trad; dx++){
|
||||
for(int dy = -trad; dy <= trad; dy++){
|
||||
Tile tile = world.tile(Math.round(x / tilesize) + dx, Math.round(y / tilesize) + dy);
|
||||
if(tile != null && tile.build != null && (team == null ||team.isEnemy(tile.team())) && Mathf.dst(dx, dy) <= trad){
|
||||
tile.build.damage(damage);
|
||||
if(tile != null && tile.build != null && (team == null || team != tile.team()) && dx*dx + dy*dy <= trad*trad){
|
||||
tile.build.damage(team, damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float calculateDamage(float x, float y, float tx, float ty, float radius, float damage){
|
||||
float dist = Mathf.dst(x, y, tx, ty);
|
||||
private static float calculateDamage(float dist, float radius, float damage){
|
||||
float falloff = 0.4f;
|
||||
float scaled = Mathf.lerp(1f - dist / radius, 1f, falloff);
|
||||
return damage * scaled;
|
||||
}
|
||||
|
||||
@Struct
|
||||
static class PropCellStruct{
|
||||
byte x;
|
||||
byte y;
|
||||
short damage;
|
||||
/** @return resulting armor calculated based off of damage */
|
||||
public static float applyArmor(float damage, float armor){
|
||||
return Math.max(damage - armor, minArmorDamage * damage);
|
||||
}
|
||||
|
||||
public static class Collided implements Pool.Poolable{
|
||||
public float x, y;
|
||||
public Teamc target;
|
||||
|
||||
public Collided set(float x, float y, Teamc target){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.target = target;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
@@ -19,7 +20,8 @@ import static mindustry.Vars.*;
|
||||
public class Effect{
|
||||
private static final float shakeFalloff = 10000f;
|
||||
private static final EffectContainer container = new EffectContainer();
|
||||
private static final Seq<Effect> all = new Seq<>();
|
||||
|
||||
public static final Seq<Effect> all = new Seq<>();
|
||||
|
||||
private boolean initialized;
|
||||
|
||||
@@ -29,6 +31,14 @@ public class Effect{
|
||||
public float lifetime = 50f;
|
||||
/** Clip size. */
|
||||
public float clip;
|
||||
/** Time delay before the effect starts */
|
||||
public float startDelay;
|
||||
/** Amount added to rotation */
|
||||
public float baseRotation;
|
||||
/** If true, parent unit is data are followed. */
|
||||
public boolean followParent = true;
|
||||
/** If this and followParent are true, the effect will offset and rotate with the parent's rotation. */
|
||||
public boolean rotWithParent;
|
||||
|
||||
public float layer = Layer.effect;
|
||||
public float layerDuration;
|
||||
@@ -51,49 +61,117 @@ public class Effect{
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
public Effect startDelay(float d){
|
||||
startDelay = d;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void init(){}
|
||||
|
||||
public Effect followParent(boolean follow){
|
||||
followParent = follow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Effect rotWithParent(boolean follow){
|
||||
rotWithParent = follow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Effect layer(float l){
|
||||
layer = l;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Effect baseRotation(float d){
|
||||
baseRotation = d;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Effect layer(float l, float duration){
|
||||
layer = l;
|
||||
this.layerDuration = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WrapEffect wrap(Color color){
|
||||
return new WrapEffect(this, color);
|
||||
}
|
||||
|
||||
public WrapEffect wrap(Color color, float rotation){
|
||||
return new WrapEffect(this, color, rotation);
|
||||
}
|
||||
|
||||
public void at(Position pos){
|
||||
create(this, pos.getX(), pos.getY(), 0, Color.white, null);
|
||||
create(pos.getX(), pos.getY(), 0, Color.white, null);
|
||||
}
|
||||
|
||||
public void at(Position pos, boolean parentize){
|
||||
create(pos.getX(), pos.getY(), 0, Color.white, parentize ? pos : null);
|
||||
}
|
||||
|
||||
public void at(Position pos, float rotation){
|
||||
create(this, pos.getX(), pos.getY(), rotation, Color.white, null);
|
||||
create(pos.getX(), pos.getY(), rotation, Color.white, null);
|
||||
}
|
||||
|
||||
public void at(float x, float y){
|
||||
create(this, x, y, 0, Color.white, null);
|
||||
create(x, y, 0, Color.white, null);
|
||||
}
|
||||
|
||||
public void at(float x, float y, float rotation){
|
||||
create(this, x, y, rotation, Color.white, null);
|
||||
create(x, y, rotation, Color.white, null);
|
||||
}
|
||||
|
||||
public void at(float x, float y, float rotation, Color color){
|
||||
create(this, x, y, rotation, color, null);
|
||||
create(x, y, rotation, color, null);
|
||||
}
|
||||
|
||||
public void at(float x, float y, Color color){
|
||||
create(this, x, y, 0, color, null);
|
||||
create(x, y, 0, color, null);
|
||||
}
|
||||
|
||||
public void at(float x, float y, float rotation, Color color, Object data){
|
||||
create(this, x, y, rotation, color, data);
|
||||
create(x, y, rotation, color, data);
|
||||
}
|
||||
|
||||
public void at(float x, float y, float rotation, Object data){
|
||||
create(this, x, y, rotation, Color.white, data);
|
||||
create(x, y, rotation, Color.white, data);
|
||||
}
|
||||
|
||||
public boolean shouldCreate(){
|
||||
return !headless && this != Fx.none && Vars.renderer.enableEffects;
|
||||
}
|
||||
|
||||
public void create(float x, float y, float rotation, Color color, Object data){
|
||||
if(!shouldCreate()) return;
|
||||
|
||||
if(Core.camera.bounds(Tmp.r1).overlaps(Tmp.r2.setCentered(x, y, clip))){
|
||||
if(!initialized){
|
||||
initialized = true;
|
||||
init();
|
||||
}
|
||||
|
||||
if(startDelay <= 0f){
|
||||
add(x, y, rotation, color, data);
|
||||
}else{
|
||||
Time.run(startDelay, () -> add(x, y, rotation, color, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void add(float x, float y, float rotation, Color color, Object data){
|
||||
var entity = EffectState.create();
|
||||
entity.effect = this;
|
||||
entity.rotation = baseRotation + rotation;
|
||||
entity.data = data;
|
||||
entity.lifetime = lifetime;
|
||||
entity.set(x, y);
|
||||
entity.color.set(color);
|
||||
if(followParent && data instanceof Posc p){
|
||||
entity.parent = p;
|
||||
entity.rotWithParent = rotWithParent;
|
||||
}
|
||||
entity.add();
|
||||
}
|
||||
|
||||
public float render(int id, Color color, float life, float lifetime, float rotation, float x, float y, Object data){
|
||||
@@ -133,28 +211,19 @@ public class Effect{
|
||||
shake(intensity, duration, loc.getX(), loc.getY());
|
||||
}
|
||||
|
||||
public static void create(Effect effect, float x, float y, float rotation, Color color, Object data){
|
||||
if(headless || effect == Fx.none) return;
|
||||
if(Core.settings.getBool("effects")){
|
||||
Rect view = Core.camera.bounds(Tmp.r1);
|
||||
Rect pos = Tmp.r2.setSize(effect.clip).setCenter(x, y);
|
||||
public static void floorDust(float x, float y, float size){
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
if(tile != null){
|
||||
Color color = tile.floor().mapColor;
|
||||
Fx.unitLand.at(x, y, size, color);
|
||||
}
|
||||
}
|
||||
|
||||
if(view.overlaps(pos)){
|
||||
if(!effect.initialized){
|
||||
effect.initialized = true;
|
||||
effect.init();
|
||||
}
|
||||
|
||||
EffectState entity = EffectState.create();
|
||||
entity.effect = effect;
|
||||
entity.rotation = rotation;
|
||||
entity.data = (data);
|
||||
entity.lifetime = (effect.lifetime);
|
||||
entity.set(x, y);
|
||||
entity.color.set(color);
|
||||
if(data instanceof Posc) entity.parent = ((Posc)data);
|
||||
entity.add();
|
||||
}
|
||||
public static void floorDustAngle(Effect effect, float x, float y, float angle){
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
if(tile != null){
|
||||
Color color = tile.floor().mapColor;
|
||||
effect.at(x, y, angle, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +284,10 @@ public class Effect{
|
||||
return (T)data;
|
||||
}
|
||||
|
||||
public EffectContainer inner(){
|
||||
return innerContainer == null ? (innerContainer = new EffectContainer()) : innerContainer;
|
||||
}
|
||||
|
||||
public void scaled(float lifetime, Cons<EffectContainer> cons){
|
||||
if(innerContainer == null) innerContainer = new EffectContainer();
|
||||
if(time <= lifetime){
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class EntityCollisions{
|
||||
//range for tile collision scanning
|
||||
private static final int r = 1;
|
||||
//move in 1-unit chunks
|
||||
//move in 1-unit chunks (can this be made more efficient?)
|
||||
private static final float seg = 1f;
|
||||
|
||||
//tile collisions
|
||||
private Rect tmp = new Rect();
|
||||
private Vec2 vector = new Vec2();
|
||||
private Vec2 l1 = new Vec2();
|
||||
private Rect r1 = new Rect();
|
||||
private Rect r2 = new Rect();
|
||||
private Vec2 vector = new Vec2(), l1 = new Vec2();
|
||||
private Rect r1 = new Rect(), r2 = new Rect(), tmp = new Rect();
|
||||
|
||||
//entity collisions
|
||||
private Seq<Hitboxc> arrOut = new Seq<>();
|
||||
private Seq<Hitboxc> arrOut = new Seq<>(Hitboxc.class);
|
||||
private Cons<Hitboxc> hitCons = this::updateCollision;
|
||||
private Cons<QuadTree> treeCons = tree -> tree.intersect(r2, arrOut);
|
||||
|
||||
public void moveCheck(Hitboxc entity, float deltax, float deltay, SolidPred solidCheck){
|
||||
if(!solidCheck.solid(entity.tileX(), entity.tileY())){
|
||||
@@ -35,13 +34,15 @@ public class EntityCollisions{
|
||||
}
|
||||
|
||||
public void move(Hitboxc entity, float deltax, float deltay, SolidPred solidCheck){
|
||||
if(Math.abs(deltax) < 0.0001f & Math.abs(deltay) < 0.0001f) return;
|
||||
if(Math.abs(deltax) < 0.0001f & Math.abs(deltay) < 0.0001f) return;
|
||||
|
||||
boolean movedx = false;
|
||||
entity.hitboxTile(r1);
|
||||
int r = Math.max(Math.round(r1.width / tilesize), 1);
|
||||
|
||||
while(Math.abs(deltax) > 0 || !movedx){
|
||||
movedx = true;
|
||||
moveDelta(entity, Math.min(Math.abs(deltax), seg) * Mathf.sign(deltax), 0, true, solidCheck);
|
||||
moveDelta(entity, Math.min(Math.abs(deltax), seg) * Mathf.sign(deltax), 0, r, true, solidCheck);
|
||||
|
||||
if(Math.abs(deltax) >= seg){
|
||||
deltax -= seg * Mathf.sign(deltax);
|
||||
@@ -54,7 +55,7 @@ public class EntityCollisions{
|
||||
|
||||
while(Math.abs(deltay) > 0 || !movedy){
|
||||
movedy = true;
|
||||
moveDelta(entity, 0, Math.min(Math.abs(deltay), seg) * Mathf.sign(deltay), false, solidCheck);
|
||||
moveDelta(entity, 0, Math.min(Math.abs(deltay), seg) * Mathf.sign(deltay), r, false, solidCheck);
|
||||
|
||||
if(Math.abs(deltay) >= seg){
|
||||
deltay -= seg * Mathf.sign(deltay);
|
||||
@@ -64,7 +65,7 @@ public class EntityCollisions{
|
||||
}
|
||||
}
|
||||
|
||||
public void moveDelta(Hitboxc entity, float deltax, float deltay, boolean x, SolidPred solidCheck){
|
||||
public void moveDelta(Hitboxc entity, float deltax, float deltay, int r, boolean x, SolidPred solidCheck){
|
||||
entity.hitboxTile(r1);
|
||||
entity.hitboxTile(r2);
|
||||
r1.x += deltax;
|
||||
@@ -80,8 +81,8 @@ public class EntityCollisions{
|
||||
|
||||
if(tmp.overlaps(r1)){
|
||||
Vec2 v = Geometry.overlap(r1, tmp, x);
|
||||
if(x) r1.x += v.x;
|
||||
if(!x) r1.y += v.y;
|
||||
r1.x += v.x;
|
||||
r1.y += v.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,9 +91,11 @@ public class EntityCollisions{
|
||||
entity.trns(r1.x - r2.x, r1.y - r2.y);
|
||||
}
|
||||
|
||||
public boolean overlapsTile(Rect rect){
|
||||
public boolean overlapsTile(Rect rect, @Nullable SolidPred solidChecker){
|
||||
if(solidChecker == null) return false;
|
||||
|
||||
rect.getCenter(vector);
|
||||
int r = 1;
|
||||
int r = Math.max(Math.round(r1.width / tilesize), 1);
|
||||
|
||||
//assumes tiles are centered
|
||||
int tilex = Math.round(vector.x / tilesize);
|
||||
@@ -101,10 +104,9 @@ public class EntityCollisions{
|
||||
for(int dx = -r; dx <= r; dx++){
|
||||
for(int dy = -r; dy <= r; dy++){
|
||||
int wx = dx + tilex, wy = dy + tiley;
|
||||
if(solid(wx, wy)){
|
||||
r2.setSize(tilesize).setCenter(wx * tilesize, wy * tilesize);
|
||||
if(solidChecker.solid(wx, wy)){
|
||||
|
||||
if(r2.overlaps(rect)){
|
||||
if(r2.setCentered(wx * tilesize, wy * tilesize, tilesize).overlaps(rect)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ public class EntityCollisions{
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Hitboxc> void updatePhysics(EntityGroup<T> group){
|
||||
QuadTree tree = group.tree();
|
||||
var tree = group.tree();
|
||||
tree.clear();
|
||||
|
||||
group.each(s -> {
|
||||
@@ -126,12 +128,12 @@ public class EntityCollisions{
|
||||
|
||||
public static boolean legsSolid(int x, int y){
|
||||
Tile tile = world.tile(x, y);
|
||||
return tile == null || tile.staticDarkness() >= 2 || tile.floor().solid;
|
||||
return tile == null || tile.legSolid();
|
||||
}
|
||||
|
||||
public static boolean waterSolid(int x, int y){
|
||||
Tile tile = world.tile(x, y);
|
||||
return tile == null || (tile.solid() || !tile.floor().isLiquid);
|
||||
return tile == null || tile.solid() || !tile.floor().isLiquid;
|
||||
}
|
||||
|
||||
public static boolean solid(int x, int y){
|
||||
@@ -153,7 +155,7 @@ public class EntityCollisions{
|
||||
float vbx = b.getX() - b.lastX();
|
||||
float vby = b.getY() - b.lastY();
|
||||
|
||||
if(a != b && a.collides(b)){
|
||||
if(a != b && a.collides(b) && b.collides(a)){
|
||||
l1.set(a.getX(), a.getY());
|
||||
boolean collide = r1.overlaps(r2) || collide(r1.x, r1.y, r1.width, r1.height, vax, vay,
|
||||
r2.x, r2.y, r2.width, r2.height, vbx, vby, l1);
|
||||
@@ -190,14 +192,10 @@ public class EntityCollisions{
|
||||
yInvExit = y2 - (y1 + h1);
|
||||
}
|
||||
|
||||
float xEntry, yEntry;
|
||||
float xExit, yExit;
|
||||
|
||||
xEntry = xInvEntry / vx1;
|
||||
xExit = xInvExit / vx1;
|
||||
|
||||
yEntry = yInvEntry / vy1;
|
||||
yExit = yInvExit / vy1;
|
||||
float xEntry = xInvEntry / vx1;
|
||||
float xExit = xInvExit / vx1;
|
||||
float yEntry = yInvEntry / vy1;
|
||||
float yExit = yInvExit / vy1;
|
||||
|
||||
float entryTime = Math.max(xEntry, yEntry);
|
||||
float exitTime = Math.min(xExit, yExit);
|
||||
@@ -216,31 +214,37 @@ 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());
|
||||
r1.y += (solid.lastY() - solid.getY());
|
||||
groupa.each((Cons<T>)hitCons);
|
||||
}
|
||||
|
||||
solid.hitbox(r2);
|
||||
r2.merge(r1);
|
||||
private void updateCollision(Hitboxc solid){
|
||||
solid.hitbox(r1);
|
||||
r1.x += (solid.lastX() - solid.getX());
|
||||
r1.y += (solid.lastY() - solid.getY());
|
||||
|
||||
arrOut.clear();
|
||||
solid.hitbox(r2);
|
||||
r2.merge(r1);
|
||||
|
||||
//get all targets based on what entity wants to collide with
|
||||
solid.getCollisions(tree -> tree.intersect(r2, arrOut));
|
||||
arrOut.clear();
|
||||
|
||||
for(Hitboxc sc : arrOut){
|
||||
sc.hitbox(r1);
|
||||
if(r2.overlaps(r1)){
|
||||
checkCollide(solid, sc);
|
||||
//break out of loop when this object hits something
|
||||
if(!solid.isAdded()) return;
|
||||
}
|
||||
//get all targets based on what entity wants to collide with
|
||||
solid.getCollisions(treeCons);
|
||||
|
||||
var items = arrOut.items;
|
||||
int size = arrOut.size;
|
||||
|
||||
for(int i = 0; i < size; i++){
|
||||
Hitboxc sc = items[i];
|
||||
sc.hitbox(r1);
|
||||
if(r2.overlaps(r1)){
|
||||
checkCollide(solid, sc);
|
||||
//break out of loop when this object hits something
|
||||
if(!solid.isAdded()) return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface SolidPred{
|
||||
boolean solid(int x, int y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
private final Seq<T> intersectArray = new Seq<>();
|
||||
private final Rect viewport = new Rect();
|
||||
private final Rect intersectRect = new Rect();
|
||||
private final EntityIndexer indexer;
|
||||
private IntMap<T> map;
|
||||
private QuadTree tree;
|
||||
private boolean clearing;
|
||||
@@ -27,10 +28,20 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
private int index;
|
||||
|
||||
public static int nextId(){
|
||||
if(lastId >= Integer.MAX_VALUE - 2) lastId = 0;
|
||||
return lastId++;
|
||||
}
|
||||
|
||||
/** Makes sure the next ID counter is higher than this number, so future entities cannot possibly use this ID. */
|
||||
public static void checkNextId(int id){
|
||||
lastId = Math.max(lastId, id + 1);
|
||||
}
|
||||
|
||||
public EntityGroup(Class<T> type, boolean spatial, boolean mapping){
|
||||
this(type, spatial, mapping, null);
|
||||
}
|
||||
|
||||
public EntityGroup(Class<T> type, boolean spatial, boolean mapping, EntityIndexer indexer){
|
||||
array = new Seq<>(false, 32, type);
|
||||
|
||||
if(spatial){
|
||||
@@ -40,6 +51,20 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
if(mapping){
|
||||
map = new IntMap<>();
|
||||
}
|
||||
|
||||
this.indexer = indexer;
|
||||
}
|
||||
|
||||
/** @return entities with colliding IDs, or an empty array. */
|
||||
public Seq<T> checkIDCollisions(){
|
||||
Seq<T> out = new Seq<>();
|
||||
IntSet ints = new IntSet();
|
||||
each(u -> {
|
||||
if(!ints.add(u.id())){
|
||||
out.add(u);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
public void sort(Comparator<? super T> comp){
|
||||
@@ -55,7 +80,13 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
}
|
||||
|
||||
public void update(){
|
||||
each(Entityc::update);
|
||||
for(index = 0; index < array.size; index++){
|
||||
array.items[index].update();
|
||||
}
|
||||
}
|
||||
|
||||
public Seq<T> copy(){
|
||||
return copy(new Seq<>());
|
||||
}
|
||||
|
||||
public Seq<T> copy(Seq<T> arr){
|
||||
@@ -78,16 +109,17 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
public void draw(Cons<T> cons){
|
||||
Core.camera.bounds(viewport);
|
||||
|
||||
each(e -> {
|
||||
Drawc draw = (Drawc)e;
|
||||
if(viewport.overlaps(draw.x() - draw.clipSize()/2f, draw.y() - draw.clipSize()/2f, draw.clipSize(), draw.clipSize())){
|
||||
cons.get(e);
|
||||
for(index = 0; index < array.size; index++){
|
||||
Drawc draw = (Drawc)array.items[index];
|
||||
float clip = draw.clipSize();
|
||||
if(viewport.overlaps(draw.x() - clip/2f, draw.y() - clip/2f, clip, clip)){
|
||||
cons.get((T)draw);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public boolean useTree(){
|
||||
return map != null;
|
||||
return tree != null;
|
||||
}
|
||||
|
||||
public boolean mappingEnabled(){
|
||||
@@ -114,6 +146,12 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
tree.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
public boolean intersect(float x, float y, float width, float height, Boolf<? super T> out){
|
||||
//don't waste time for empty groups
|
||||
if(isEmpty()) return false;
|
||||
return tree.intersect(x, y, width, height, out);
|
||||
}
|
||||
|
||||
public Seq<T> intersect(float x, float y, float width, float height){
|
||||
intersectArray.clear();
|
||||
//don't waste time for empty groups
|
||||
@@ -163,12 +201,25 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
}
|
||||
}
|
||||
|
||||
public int addIndex(T type){
|
||||
int index = array.size;
|
||||
add(type);
|
||||
return index;
|
||||
}
|
||||
|
||||
public void remove(T type){
|
||||
if(clearing) return;
|
||||
if(type == null) throw new RuntimeException("Cannot remove a null entity!");
|
||||
int idx = array.indexOf(type, true);
|
||||
if(idx != -1){
|
||||
array.remove(idx);
|
||||
|
||||
//fix incorrect HEAD index since it was swapped
|
||||
if(array.size > 0 && idx != array.size){
|
||||
var swapped = array.items[idx];
|
||||
if(indexer != null) indexer.change(swapped, idx);
|
||||
}
|
||||
|
||||
if(map != null){
|
||||
map.remove(type.id());
|
||||
}
|
||||
@@ -180,6 +231,38 @@ public class EntityGroup<T extends Entityc> implements Iterable<T>{
|
||||
}
|
||||
}
|
||||
|
||||
public void removeIndex(T type, int position){
|
||||
if(clearing) return;
|
||||
if(type == null) throw new RuntimeException("Cannot remove a null entity!");
|
||||
if(position != -1 && position < array.size){
|
||||
|
||||
//rarely the entity index is wrong; fallback to slow implementation
|
||||
if(array.items[position] != type){
|
||||
remove(type);
|
||||
return;
|
||||
}
|
||||
|
||||
//swap head with current
|
||||
if(array.size > 1){
|
||||
var head = array.items[array.size - 1];
|
||||
if(indexer != null) indexer.change(head, position);
|
||||
array.items[position] = head;
|
||||
}
|
||||
|
||||
array.size --;
|
||||
array.items[array.size] = null;
|
||||
|
||||
if(map != null){
|
||||
map.remove(type.id());
|
||||
}
|
||||
|
||||
//fix iteration index when removing
|
||||
if(index >= position){
|
||||
index --;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(){
|
||||
clearing = true;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user