it is done
This commit is contained in:
206
core/src/mindustry/ClientLauncher.java
Normal file
206
core/src/mindustry/ClientLauncher.java
Normal file
@@ -0,0 +1,206 @@
|
||||
package mindustry;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.assets.loaders.*;
|
||||
import arc.audio.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.ctype.Content;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.mod.*;
|
||||
import mindustry.net.Net;
|
||||
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public abstract class ClientLauncher extends ApplicationCore implements Platform{
|
||||
private static final int loadingFPS = 20;
|
||||
|
||||
private float smoothProgress;
|
||||
private long lastTime;
|
||||
private long beginTime;
|
||||
private boolean finished = false;
|
||||
|
||||
@Override
|
||||
public void setup(){
|
||||
Vars.loadLogger();
|
||||
Vars.platform = this;
|
||||
beginTime = Time.millis();
|
||||
|
||||
Time.setDeltaProvider(() -> {
|
||||
float result = Core.graphics.getDeltaTime() * 60f;
|
||||
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
|
||||
});
|
||||
|
||||
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.load("sprites/error.png", Texture.class);
|
||||
atlas = TextureAtlas.blankAtlas();
|
||||
Vars.net = new Net(platform.getNet());
|
||||
mods = new Mods();
|
||||
|
||||
UI.loadSystemCursors();
|
||||
|
||||
assets.load(new Vars());
|
||||
|
||||
UI.loadDefaultFont();
|
||||
|
||||
assets.load(new AssetDescriptor<>("sprites/sprites.atlas", TextureAtlas.class)).loaded = t -> {
|
||||
atlas = (TextureAtlas)t;
|
||||
};
|
||||
|
||||
assets.loadRun("maps", Map.class, () -> maps.loadPreviews());
|
||||
|
||||
Musics.load();
|
||||
Sounds.load();
|
||||
|
||||
assets.loadRun("contentcreate", Content.class, () -> {
|
||||
content.createBaseContent();
|
||||
content.loadColors();
|
||||
}, () -> {
|
||||
mods.loadScripts();
|
||||
content.createModContent();
|
||||
});
|
||||
|
||||
add(logic = new Logic());
|
||||
add(control = new Control());
|
||||
add(renderer = new Renderer());
|
||||
add(ui = new UI());
|
||||
add(netServer = new NetServer());
|
||||
add(netClient = new NetClient());
|
||||
|
||||
assets.load(mods);
|
||||
assets.load(schematics);
|
||||
|
||||
assets.loadRun("contentinit", ContentLoader.class, () -> {
|
||||
content.init();
|
||||
content.load();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(ApplicationListener module){
|
||||
super.add(module);
|
||||
|
||||
//autoload modules when necessary
|
||||
if(module instanceof Loadable){
|
||||
assets.load((Loadable)module);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
if(assets == null) return;
|
||||
|
||||
if(!finished){
|
||||
Draw.proj().setOrtho(0, 0, width, height);
|
||||
}else{
|
||||
super.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(!finished){
|
||||
drawLoading();
|
||||
if(assets.update(1000 / loadingFPS)){
|
||||
Log.info("Total time to load: {0}", Time.timeSinceMillis(beginTime));
|
||||
for(ApplicationListener listener : modules){
|
||||
listener.init();
|
||||
}
|
||||
mods.eachClass(Mod::init);
|
||||
finished = true;
|
||||
Events.fire(new ClientLoadEvent());
|
||||
super.resize(graphics.getWidth(), graphics.getHeight());
|
||||
app.post(() -> app.post(() -> app.post(() -> app.post(() -> super.resize(graphics.getWidth(), graphics.getHeight())))));
|
||||
}
|
||||
}else{
|
||||
super.update();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = Time.nanos();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
setup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume(){
|
||||
if(finished){
|
||||
super.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(){
|
||||
if(finished){
|
||||
super.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void drawLoading(){
|
||||
smoothProgress = Mathf.lerpDelta(smoothProgress, assets.getProgress(), 0.1f);
|
||||
|
||||
Core.graphics.clear(Pal.darkerGray);
|
||||
Draw.proj().setOrtho(0, 0, Core.graphics.getWidth(), Core.graphics.getHeight());
|
||||
|
||||
float height = Scl.scl(50f);
|
||||
|
||||
Draw.color(Color.black);
|
||||
Fill.poly(graphics.getWidth()/2f, graphics.getHeight()/2f, 6, Mathf.dst(graphics.getWidth()/2f, graphics.getHeight()/2f) * smoothProgress);
|
||||
Draw.reset();
|
||||
|
||||
float w = graphics.getWidth()*0.6f;
|
||||
|
||||
Draw.color(Color.black);
|
||||
Fill.rect(graphics.getWidth()/2f, graphics.getHeight()/2f, w, height);
|
||||
|
||||
Draw.color(Pal.accent);
|
||||
Fill.crect(graphics.getWidth()/2f-w/2f, graphics.getHeight()/2f - height/2f, w * smoothProgress, height);
|
||||
|
||||
for(int i : Mathf.signs){
|
||||
Fill.tri(graphics.getWidth()/2f + w/2f*i, graphics.getHeight()/2f + height/2f, graphics.getWidth()/2f + w/2f*i, graphics.getHeight()/2f - height/2f, graphics.getWidth()/2f + w/2f*i + height/2f*i, graphics.getHeight()/2f);
|
||||
}
|
||||
|
||||
if(assets.isLoaded("outline")){
|
||||
BitmapFont font = assets.get("outline");
|
||||
font.draw((int)(assets.getProgress() * 100) + "%", graphics.getWidth() / 2f, graphics.getHeight() / 2f + Scl.scl(10f), Align.center);
|
||||
font.draw(bundle.get("loading", "").replace("[accent]", ""), graphics.getWidth() / 2f, graphics.getHeight() / 2f + height / 2f + Scl.scl(20), Align.center);
|
||||
|
||||
if(assets.getCurrentLoading() != null){
|
||||
String name = assets.getCurrentLoading().fileName.toLowerCase();
|
||||
String key = name.contains("script") ? "scripts" : name.contains("content") ? "content" : name.contains("mod") ? "mods" : name.contains("msav") ||
|
||||
name.contains("maps") ? "map" : name.contains("ogg") || name.contains("mp3") ? "sound" : name.contains("png") ? "image" : "system";
|
||||
font.draw(bundle.get("load." + key, ""), graphics.getWidth() / 2f, graphics.getHeight() / 2f - height / 2f - Scl.scl(10f), Align.center);
|
||||
}
|
||||
}
|
||||
Draw.flush();
|
||||
}
|
||||
}
|
||||
354
core/src/mindustry/Vars.java
Normal file
354
core/src/mindustry/Vars.java
Normal file
@@ -0,0 +1,354 @@
|
||||
package mindustry;
|
||||
|
||||
import arc.*;
|
||||
import arc.Application.*;
|
||||
import arc.assets.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.graphics.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.io.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.mod.*;
|
||||
import mindustry.net.Net;
|
||||
import mindustry.world.blocks.defense.ForceProjector.*;
|
||||
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
|
||||
import static arc.Core.settings;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class Vars implements Loadable{
|
||||
/** Whether to load locales.*/
|
||||
public static boolean loadLocales = true;
|
||||
/** Whether the logger is loaded. */
|
||||
public static boolean loadedLogger = false;
|
||||
/** Maximum schematic size.*/
|
||||
public static final int maxSchematicSize = 32;
|
||||
/** All schematic base64 starts with this string.*/
|
||||
public static final String schematicBaseStart ="bXNjaAB";
|
||||
/** IO buffer size. */
|
||||
public static final int bufferSize = 8192;
|
||||
/** global charset, since Android doesn't support the Charsets class */
|
||||
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";
|
||||
/** 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/";
|
||||
/** URL to the JSON file containing all the global, public servers. */
|
||||
public static final String serverJsonURL = "https://raw.githubusercontent.com/Anuken/Mindustry/master/servers.json";
|
||||
/** URL the links to the wiki's modding guide.*/
|
||||
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?template=bug_report.md";
|
||||
/** list of built-in servers.*/
|
||||
public static final Array<String> defaultServers = Array.with();
|
||||
/** maximum distance between mine and core that supports automatic transferring */
|
||||
public static final float mineTransferRange = 220f;
|
||||
/** team of the player by default */
|
||||
public static final Team defaultTeam = Team.sharded;
|
||||
/** team of the enemy in waves/sectors */
|
||||
public static final Team waveTeam = Team.crux;
|
||||
/** whether to enable editing of units in the editor */
|
||||
public static final boolean enableUnitEditing = false;
|
||||
/** max chat message length */
|
||||
public static final int maxTextLength = 150;
|
||||
/** max player name length in bytes */
|
||||
public static final int maxNameLength = 40;
|
||||
/** displayed item size when ingame, TODO remove. */
|
||||
public static final float itemSize = 5f;
|
||||
/** extra padding around the world; units outside this bound will begin to self-destruct. */
|
||||
public static final float worldBounds = 100f;
|
||||
/** units outside of this bound will simply die instantly */
|
||||
public static final float finalWorldBounds = worldBounds + 500;
|
||||
/** ticks spent out of bound until self destruct. */
|
||||
public static final float boundsCountdown = 60 * 7;
|
||||
/** for map generator dialog */
|
||||
public static boolean updateEditorOnChange = false;
|
||||
/** size of tiles in units */
|
||||
public static final int tilesize = 8;
|
||||
/** all choosable player colors in join/host dialog */
|
||||
public static final Color[] playerColors = {
|
||||
Color.valueOf("82759a"),
|
||||
Color.valueOf("c0c1c5"),
|
||||
Color.valueOf("fff0e7"),
|
||||
Color.valueOf("7d2953"),
|
||||
Color.valueOf("ff074e"),
|
||||
Color.valueOf("ff072a"),
|
||||
Color.valueOf("ff76a6"),
|
||||
Color.valueOf("a95238"),
|
||||
Color.valueOf("ffa108"),
|
||||
Color.valueOf("feeb2c"),
|
||||
Color.valueOf("ffcaa8"),
|
||||
Color.valueOf("008551"),
|
||||
Color.valueOf("00e339"),
|
||||
Color.valueOf("423c7b"),
|
||||
Color.valueOf("4b5ef1"),
|
||||
Color.valueOf("2cabfe"),
|
||||
};
|
||||
/** default server port */
|
||||
public static final int port = 6567;
|
||||
/** multicast discovery port.*/
|
||||
public static final int multicastPort = 20151;
|
||||
/** multicast group for discovery.*/
|
||||
public static final String multicastGroup = "227.2.7.7";
|
||||
/** if true, UI is not drawn */
|
||||
public static boolean disableUI;
|
||||
/** 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 */
|
||||
public static boolean mobile;
|
||||
/** whether the game is running on an iOS device */
|
||||
public static boolean ios;
|
||||
/** whether the game is running on an Android device */
|
||||
public static boolean android;
|
||||
/** whether the game is running on a headless server */
|
||||
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;
|
||||
/** application data directory, equivalent to {@link Settings#getDataDirectory()} */
|
||||
public static Fi dataDirectory;
|
||||
/** data subdirectory used for screenshots */
|
||||
public static Fi screenshotDirectory;
|
||||
/** data subdirectory used for custom mmaps */
|
||||
public static Fi customMapDirectory;
|
||||
/** data subdirectory used for custom mmaps */
|
||||
public static Fi mapPreviewDirectory;
|
||||
/** tmp subdirectory for map conversion */
|
||||
public static Fi tmpDirectory;
|
||||
/** data subdirectory used for saves */
|
||||
public static Fi saveDirectory;
|
||||
/** data subdirectory used for mods */
|
||||
public static Fi modDirectory;
|
||||
/** data subdirectory used for schematics */
|
||||
public static Fi schematicDirectory;
|
||||
/** 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";
|
||||
|
||||
/** list of all locales that can be switched to */
|
||||
public static Locale[] locales;
|
||||
|
||||
public static FileTree tree;
|
||||
public static Net net;
|
||||
public static ContentLoader content;
|
||||
public static GameState state;
|
||||
public static GlobalData data;
|
||||
public static EntityCollisions collisions;
|
||||
public static DefaultWaves defaultWaves;
|
||||
public static LoopControl loops;
|
||||
public static Platform platform = new Platform(){};
|
||||
public static Mods mods;
|
||||
public static Schematics schematics = new Schematics();
|
||||
|
||||
public static World world;
|
||||
public static Maps maps;
|
||||
public static WaveSpawner spawner;
|
||||
public static BlockIndexer indexer;
|
||||
public static Pathfinder pathfinder;
|
||||
|
||||
public static Control control;
|
||||
public static Logic logic;
|
||||
public static Renderer renderer;
|
||||
public static UI ui;
|
||||
public static NetServer netServer;
|
||||
public static NetClient netClient;
|
||||
|
||||
public static Entities entities;
|
||||
public static EntityGroup<Player> playerGroup;
|
||||
public static EntityGroup<TileEntity> tileGroup;
|
||||
public static EntityGroup<Bullet> bulletGroup;
|
||||
public static EntityGroup<EffectEntity> effectGroup;
|
||||
public static EntityGroup<DrawTrait> groundEffectGroup;
|
||||
public static EntityGroup<ShieldEntity> shieldGroup;
|
||||
public static EntityGroup<Puddle> puddleGroup;
|
||||
public static EntityGroup<Fire> fireGroup;
|
||||
public static EntityGroup<BaseUnit>[] unitGroups;
|
||||
|
||||
public static Player player;
|
||||
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
loadSettings();
|
||||
init();
|
||||
}
|
||||
|
||||
public static void init(){
|
||||
Serialization.init();
|
||||
DefaultSerializers.typeMappings.put("mindustry.type.ContentType", "mindustry.ctype.ContentType");
|
||||
|
||||
if(loadLocales){
|
||||
//load locales
|
||||
String[] stra = Core.files.internal("locales").readString().split("\n");
|
||||
locales = new Locale[stra.length];
|
||||
for(int i = 0; i < locales.length; i++){
|
||||
String code = stra[i];
|
||||
if(code.contains("_")){
|
||||
locales[i] = new Locale(code.split("_")[0], code.split("_")[1]);
|
||||
}else{
|
||||
locales[i] = new Locale(code);
|
||||
}
|
||||
}
|
||||
|
||||
Arrays.sort(locales, Structs.comparing(l -> l.getDisplayName(l), String.CASE_INSENSITIVE_ORDER));
|
||||
}
|
||||
|
||||
Version.init();
|
||||
|
||||
if(tree == null) tree = new FileTree();
|
||||
if(mods == null) mods = new Mods();
|
||||
|
||||
content = new ContentLoader();
|
||||
loops = new LoopControl();
|
||||
defaultWaves = new DefaultWaves();
|
||||
collisions = new EntityCollisions();
|
||||
world = new World();
|
||||
|
||||
maps = new Maps();
|
||||
spawner = new WaveSpawner();
|
||||
indexer = new BlockIndexer();
|
||||
pathfinder = new Pathfinder();
|
||||
|
||||
entities = new Entities();
|
||||
playerGroup = entities.add(Player.class).enableMapping();
|
||||
tileGroup = entities.add(TileEntity.class, false);
|
||||
bulletGroup = entities.add(Bullet.class).enableMapping();
|
||||
effectGroup = entities.add(EffectEntity.class, false);
|
||||
groundEffectGroup = entities.add(DrawTrait.class, false);
|
||||
puddleGroup = entities.add(Puddle.class).enableMapping();
|
||||
shieldGroup = entities.add(ShieldEntity.class, false);
|
||||
fireGroup = entities.add(Fire.class).enableMapping();
|
||||
unitGroups = new EntityGroup[Team.all.length];
|
||||
|
||||
for(Team team : Team.all){
|
||||
unitGroups[team.ordinal()] = entities.add(BaseUnit.class).enableMapping();
|
||||
}
|
||||
|
||||
for(EntityGroup<?> group : entities.all()){
|
||||
group.setRemoveListener(entity -> {
|
||||
if(entity instanceof SyncTrait && net.client()){
|
||||
netClient.addRemovedEntity((entity).getID());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state = new GameState();
|
||||
data = new GlobalData();
|
||||
|
||||
mobile = Core.app.getType() == ApplicationType.Android || Core.app.getType() == ApplicationType.iOS || testMobile;
|
||||
ios = Core.app.getType() == ApplicationType.iOS;
|
||||
android = Core.app.getType() == ApplicationType.Android;
|
||||
|
||||
dataDirectory = Core.settings.getDataDirectory();
|
||||
screenshotDirectory = dataDirectory.child("screenshots/");
|
||||
customMapDirectory = dataDirectory.child("maps/");
|
||||
mapPreviewDirectory = dataDirectory.child("previews/");
|
||||
saveDirectory = dataDirectory.child("saves/");
|
||||
tmpDirectory = dataDirectory.child("tmp/");
|
||||
modDirectory = dataDirectory.child("mods/");
|
||||
schematicDirectory = dataDirectory.child("schematics/");
|
||||
|
||||
modDirectory.mkdirs();
|
||||
|
||||
mods.load();
|
||||
maps.load();
|
||||
}
|
||||
|
||||
public static void loadLogger(){
|
||||
if(loadedLogger) return;
|
||||
|
||||
String[] tags = {"[green][D][]", "[royal][I][]", "[yellow][W][]", "[scarlet][E][]", ""};
|
||||
String[] stags = {"&lc&fb[D]", "&lg&fb[I]", "&ly&fb[W]", "&lr&fb[E]", ""};
|
||||
|
||||
Array<String> logBuffer = new Array<>();
|
||||
Log.setLogger((level, text, args) -> {
|
||||
String result = Log.format(text, args);
|
||||
System.out.println(Log.format(stags[level.ordinal()] + "&fr " + text, args));
|
||||
|
||||
result = tags[level.ordinal()] + " " + result;
|
||||
|
||||
if(!headless && (ui == null || ui.scriptfrag == null)){
|
||||
logBuffer.add(result);
|
||||
}else if(!headless){
|
||||
ui.scriptfrag.addMessage(result);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(ClientLoadEvent.class, e -> logBuffer.each(ui.scriptfrag::addMessage));
|
||||
|
||||
loadedLogger = true;
|
||||
}
|
||||
|
||||
public static void loadSettings(){
|
||||
Core.settings.setAppName(appName);
|
||||
|
||||
if(steam || (Version.modifier != null && Version.modifier.contains("steam"))){
|
||||
Core.settings.setDataDirectory(Core.files.local("saves/"));
|
||||
}
|
||||
|
||||
Core.settings.defaults("locale", "default", "blocksync", true);
|
||||
Core.keybinds.setDefaults(Binding.values());
|
||||
Core.settings.load();
|
||||
|
||||
Scl.setProduct(settings.getInt("uiscale", 100) / 100f);
|
||||
|
||||
if(!loadLocales) return;
|
||||
|
||||
try{
|
||||
//try loading external bundle
|
||||
Fi handle = Core.files.local("bundle");
|
||||
|
||||
Locale locale = Locale.ENGLISH;
|
||||
Core.bundle = I18NBundle.createBundle(handle, locale);
|
||||
|
||||
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."));
|
||||
}
|
||||
}catch(Throwable e){
|
||||
//no external bundle found
|
||||
|
||||
Fi handle = Core.files.internal("bundles/bundle");
|
||||
Locale locale;
|
||||
String loc = Core.settings.getString("locale");
|
||||
if(loc.equals("default")){
|
||||
locale = Locale.getDefault();
|
||||
}else{
|
||||
Locale lastLocale;
|
||||
if(loc.contains("_")){
|
||||
String[] split = loc.split("_");
|
||||
lastLocale = new Locale(split[0], split[1]);
|
||||
}else{
|
||||
lastLocale = new Locale(loc);
|
||||
}
|
||||
|
||||
locale = lastLocale;
|
||||
}
|
||||
|
||||
Locale.setDefault(locale);
|
||||
Core.bundle = I18NBundle.createBundle(handle, locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
357
core/src/mindustry/ai/BlockIndexer.java
Normal file
357
core/src/mindustry/ai/BlockIndexer.java
Normal file
@@ -0,0 +1,357 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Class used for indexing special target blocks for AI. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BlockIndexer{
|
||||
/** Size of one quadrant. */
|
||||
private final static int quadrantSize = 16;
|
||||
|
||||
/** Set of all ores that are being scanned. */
|
||||
private final ObjectSet<Item> scanOres = new ObjectSet<>();
|
||||
private final ObjectSet<Item> itemSet = new ObjectSet<>();
|
||||
/** Stores all ore quadtrants on the map. */
|
||||
private ObjectMap<Item, ObjectSet<Tile>> ores = new ObjectMap<>();
|
||||
/** Tags all quadrants. */
|
||||
private GridBits[] structQuadrants;
|
||||
/** Stores all damaged tile entities by team. */
|
||||
private ObjectSet<Tile>[] damagedTiles = new ObjectSet[Team.all.length];
|
||||
/**All ores available on this map.*/
|
||||
private ObjectSet<Item> allOres = new ObjectSet<>();
|
||||
|
||||
/** Maps teams to a map of flagged tiles by type. */
|
||||
private ObjectSet<Tile>[][] flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
|
||||
/** Maps tile positions to their last known tile index data. */
|
||||
private IntMap<TileIndex> typeMap = new IntMap<>();
|
||||
/** Empty set used for returning. */
|
||||
private ObjectSet<Tile> emptySet = new ObjectSet<>();
|
||||
/** Array used for returning and reusing. */
|
||||
private Array<Tile> returnArray = new Array<>();
|
||||
|
||||
public BlockIndexer(){
|
||||
Events.on(TileChangeEvent.class, event -> {
|
||||
if(typeMap.get(event.tile.pos()) != null){
|
||||
TileIndex index = typeMap.get(event.tile.pos());
|
||||
for(BlockFlag flag : index.flags){
|
||||
getFlagged(index.team)[flag.ordinal()].remove(event.tile);
|
||||
}
|
||||
}
|
||||
process(event.tile);
|
||||
updateQuadrant(event.tile);
|
||||
});
|
||||
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
scanOres.clear();
|
||||
scanOres.addAll(Item.getAllOres());
|
||||
damagedTiles = new ObjectSet[Team.all.length];
|
||||
flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
|
||||
|
||||
for(int i = 0; i < flagMap.length; i++){
|
||||
for(int j = 0; j < BlockFlag.all.length; j++){
|
||||
flagMap[i][j] = new ObjectSet<>();
|
||||
}
|
||||
}
|
||||
|
||||
typeMap.clear();
|
||||
allOres.clear();
|
||||
ores = null;
|
||||
|
||||
//create bitset for each team type that contains each quadrant
|
||||
structQuadrants = new GridBits[Team.all.length];
|
||||
for(int i = 0; i < Team.all.length; i++){
|
||||
structQuadrants[i] = new GridBits(Mathf.ceil(world.width() / (float)quadrantSize), Mathf.ceil(world.height() / (float)quadrantSize));
|
||||
}
|
||||
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
Tile tile = world.tile(x, y);
|
||||
|
||||
process(tile);
|
||||
|
||||
if(tile.entity != null && tile.entity.damaged()){
|
||||
notifyTileDamaged(tile.entity);
|
||||
}
|
||||
|
||||
if(tile.drop() != null) allOres.add(tile.drop());
|
||||
}
|
||||
}
|
||||
|
||||
for(int x = 0; x < quadWidth(); x++){
|
||||
for(int y = 0; y < quadHeight(); y++){
|
||||
updateQuadrant(world.tile(x * quadrantSize, y * quadrantSize));
|
||||
}
|
||||
}
|
||||
|
||||
scanOres();
|
||||
});
|
||||
}
|
||||
|
||||
private ObjectSet<Tile>[] getFlagged(Team team){
|
||||
return flagMap[team.ordinal()];
|
||||
}
|
||||
|
||||
/** @return whether this item is present on this map.*/
|
||||
public boolean hasOre(Item item){
|
||||
return allOres.contains(item);
|
||||
}
|
||||
|
||||
/** Returns all damaged tiles by team. */
|
||||
public ObjectSet<Tile> getDamaged(Team team){
|
||||
returnArray.clear();
|
||||
|
||||
if(damagedTiles[team.ordinal()] == null){
|
||||
damagedTiles[team.ordinal()] = new ObjectSet<>();
|
||||
}
|
||||
|
||||
ObjectSet<Tile> set = damagedTiles[team.ordinal()];
|
||||
for(Tile tile : set){
|
||||
if((tile.entity == null || tile.entity.getTeam() != team || !tile.entity.damaged()) || tile.block() instanceof BuildBlock){
|
||||
returnArray.add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
for(Tile tile : returnArray){
|
||||
set.remove(tile);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/** Get all allied blocks with a flag. */
|
||||
public ObjectSet<Tile> getAllied(Team team, BlockFlag type){
|
||||
return flagMap[team.ordinal()][type.ordinal()];
|
||||
}
|
||||
|
||||
/** Get all enemy blocks with a flag. */
|
||||
public Array<Tile> getEnemy(Team team, BlockFlag type){
|
||||
returnArray.clear();
|
||||
for(Team enemy : state.teams.enemiesOf(team)){
|
||||
if(state.teams.isActive(enemy)){
|
||||
ObjectSet<Tile> set = getFlagged(enemy)[type.ordinal()];
|
||||
if(set != null){
|
||||
for(Tile tile : set){
|
||||
returnArray.add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnArray;
|
||||
}
|
||||
|
||||
public void notifyTileDamaged(TileEntity entity){
|
||||
if(damagedTiles[entity.getTeam().ordinal()] == null){
|
||||
damagedTiles[entity.getTeam().ordinal()] = new ObjectSet<>();
|
||||
}
|
||||
|
||||
ObjectSet<Tile> set = damagedTiles[entity.getTeam().ordinal()];
|
||||
set.add(entity.tile);
|
||||
}
|
||||
|
||||
public TileEntity findTile(Team team, float x, float y, float range, Boolf<Tile> pred){
|
||||
return findTile(team, x, y, range, pred, false);
|
||||
}
|
||||
|
||||
public TileEntity findTile(Team team, float x, float y, float range, Boolf<Tile> pred, boolean usePriority){
|
||||
TileEntity closest = null;
|
||||
float dst = 0;
|
||||
|
||||
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++){
|
||||
|
||||
if(!getQuad(team, rx, ry)) continue;
|
||||
|
||||
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++){
|
||||
Tile other = world.ltile(tx, ty);
|
||||
|
||||
if(other == null) continue;
|
||||
|
||||
if(other.entity == null || other.getTeam() != team || !pred.get(other) || !other.block().targetable)
|
||||
continue;
|
||||
|
||||
TileEntity e = other.entity;
|
||||
|
||||
float ndst = Mathf.dst(x, y, e.x, e.y);
|
||||
if(ndst < range && (closest == null || ndst < dst || (usePriority && closest.block.priority.ordinal() < e.block.priority.ordinal()))){
|
||||
dst = ndst;
|
||||
closest = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ObjectSet<Tile> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void process(Tile tile){
|
||||
if(tile.block().flags.size() > 0 && tile.getTeam() != Team.derelict){
|
||||
ObjectSet<Tile>[] map = getFlagged(tile.getTeam());
|
||||
|
||||
for(BlockFlag flag : tile.block().flags){
|
||||
|
||||
ObjectSet<Tile> arr = map[flag.ordinal()];
|
||||
|
||||
arr.add(tile);
|
||||
|
||||
map[flag.ordinal()] = arr;
|
||||
}
|
||||
typeMap.put(tile.pos(), new TileIndex(tile.block().flags, tile.getTeam()));
|
||||
}
|
||||
|
||||
if(ores == null) return;
|
||||
|
||||
int quadrantX = tile.x / quadrantSize;
|
||||
int quadrantY = tile.y / quadrantSize;
|
||||
itemSet.clear();
|
||||
|
||||
Tile rounded = world.tile(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){
|
||||
ObjectSet<Tile> 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;
|
||||
int index = quadrantX + quadrantY * quadWidth();
|
||||
|
||||
for(Team team : Team.all){
|
||||
TeamData data = state.teams.get(team);
|
||||
|
||||
//fast-set this quadrant to 'occupied' if the tile just placed is already of this team
|
||||
if(tile.getTeam() == data.team && tile.entity != null && tile.block().targetable){
|
||||
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY);
|
||||
continue; //no need to process futher
|
||||
}
|
||||
|
||||
structQuadrants[data.team.ordinal()].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++){
|
||||
Tile result = world.ltile(x, y);
|
||||
//when a targetable block is found, mark this quadrant as occupied and stop searching
|
||||
if(result.entity != null && result.getTeam() == data.team){
|
||||
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY);
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getQuad(Team team, int quadrantX, int quadrantY){
|
||||
return structQuadrants[team.ordinal()].get(quadrantX, quadrantY);
|
||||
}
|
||||
|
||||
private int quadWidth(){
|
||||
return Mathf.ceil(world.width() / (float)quadrantSize);
|
||||
}
|
||||
|
||||
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 ObjectSet<>());
|
||||
}
|
||||
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
int qx = (x / quadrantSize);
|
||||
int qy = (y / quadrantSize);
|
||||
|
||||
Tile tile = world.tile(x, y);
|
||||
|
||||
//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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TileIndex{
|
||||
public final EnumSet<BlockFlag> flags;
|
||||
public final Team team;
|
||||
|
||||
public TileIndex(EnumSet<BlockFlag> flags, Team team){
|
||||
this.flags = flags;
|
||||
this.team = team;
|
||||
}
|
||||
}
|
||||
}
|
||||
372
core/src/mindustry/ai/Pathfinder.java
Normal file
372
core/src/mindustry/ai/Pathfinder.java
Normal file
@@ -0,0 +1,372 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Pathfinder implements Runnable{
|
||||
private static final long maxUpdate = Time.millisToNanos(4);
|
||||
private static final int updateFPS = 60;
|
||||
private static final int updateInterval = 1000 / updateFPS;
|
||||
private static final int impassable = -1;
|
||||
|
||||
/** tile data, see PathTileStruct */
|
||||
private int[][] tiles;
|
||||
/** unordered array of path data for iteration only. DO NOT iterate ot access this in the main thread.*/
|
||||
private Array<PathData> list = new Array<>();
|
||||
/** Maps teams + flags to a valid path to get to that flag for that team. */
|
||||
private PathData[][] pathMap = new PathData[Team.all.length][PathTarget.all.length];
|
||||
/** Grid map of created path data that should not be queued again. */
|
||||
private GridBits created = new GridBits(Team.all.length, PathTarget.all.length);
|
||||
/** handles task scheduling on the update thread. */
|
||||
private TaskQueue queue = new TaskQueue();
|
||||
/** current pathfinding thread */
|
||||
private @Nullable
|
||||
Thread thread;
|
||||
|
||||
public Pathfinder(){
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
stop();
|
||||
|
||||
//reset and update internal tile array
|
||||
tiles = new int[world.width()][world.height()];
|
||||
pathMap = new PathData[Team.all.length][PathTarget.all.length];
|
||||
created = new GridBits(Team.all.length, PathTarget.all.length);
|
||||
list = new Array<>();
|
||||
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
tiles[x][y] = packTile(world.rawTile(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
//special preset which may help speed things up; this is optional
|
||||
preloadPath(waveTeam, PathTarget.enemyCores);
|
||||
|
||||
start();
|
||||
});
|
||||
|
||||
Events.on(ResetEvent.class, event -> stop());
|
||||
|
||||
Events.on(TileChangeEvent.class, event -> updateTile(event.tile));
|
||||
}
|
||||
|
||||
/** Packs a tile into its internal representation. */
|
||||
private int packTile(Tile tile){
|
||||
return PathTile.get(tile.cost, tile.getTeamID(), (byte)0, !tile.solid() && tile.floor().drownTime <= 0f);
|
||||
}
|
||||
|
||||
/** Starts or restarts the pathfinding thread. */
|
||||
private void start(){
|
||||
stop();
|
||||
thread = Threads.daemon(this);
|
||||
}
|
||||
|
||||
/** Stops the pathfinding thread. */
|
||||
private void stop(){
|
||||
if(thread != null){
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
public int debugValue(Team team, int x, int y){
|
||||
if(pathMap[team.ordinal()][PathTarget.enemyCores.ordinal()] == null) return 0;
|
||||
return pathMap[team.ordinal()][PathTarget.enemyCores.ordinal()].weights[x][y];
|
||||
}
|
||||
|
||||
/** Update a tile in the internal pathfinding grid. Causes a complete pathfinding reclaculation. */
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
//can't iterate through array so use the map, which should not lead to problems
|
||||
for(PathData[] arr : pathMap){
|
||||
for(PathData path : arr){
|
||||
if(path != null){
|
||||
synchronized(path.targets){
|
||||
path.targets.clear();
|
||||
path.target.getTargets(path.team, path.targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue.post(() -> {
|
||||
for(PathData data : list){
|
||||
updateTargets(data, x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Thread implementation. */
|
||||
@Override
|
||||
public void run(){
|
||||
while(true){
|
||||
if(net.client()) return;
|
||||
try{
|
||||
|
||||
queue.run();
|
||||
|
||||
//total update time no longer than maxUpdate
|
||||
for(PathData data : list){
|
||||
updateFrontier(data, maxUpdate / list.size);
|
||||
}
|
||||
|
||||
try{
|
||||
Thread.sleep(updateInterval);
|
||||
}catch(InterruptedException e){
|
||||
//stop looping when interrupted externally
|
||||
return;
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets next tile to travel to. Main thread only. */
|
||||
public Tile getTargetTile(Tile tile, Team team, PathTarget target){
|
||||
if(tile == null) return null;
|
||||
|
||||
PathData data = pathMap[team.ordinal()][target.ordinal()];
|
||||
|
||||
if(data == null){
|
||||
//if this combination is not found, create it on request
|
||||
if(!created.get(team.ordinal(), target.ordinal())){
|
||||
created.set(team.ordinal(), target.ordinal());
|
||||
//grab targets since this is run on main thread
|
||||
IntArray targets = target.getTargets(team, new IntArray());
|
||||
queue.post(() -> createPath(team, target, targets));
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
int[][] values = data.weights;
|
||||
int value = values[tile.x][tile.y];
|
||||
|
||||
Tile current = null;
|
||||
int tl = 0;
|
||||
for(Point2 point : Geometry.d8){
|
||||
int dx = tile.x + point.x, dy = tile.y + point.y;
|
||||
|
||||
Tile other = world.tile(dx, dy);
|
||||
if(other == null) continue;
|
||||
|
||||
if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && !other.solid() && other.floor().drownTime <= 0 &&
|
||||
!(point.x != 0 && point.y != 0 && (world.solid(tile.x + point.x, tile.y) || world.solid(tile.x, tile.y + point.y)))){ //diagonal corner trap
|
||||
current = other;
|
||||
tl = values[dx][dy];
|
||||
}
|
||||
}
|
||||
|
||||
if(current == null || tl == impassable) return tile;
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/** @return whether a tile can be passed through by this team. Pathfinding thread only.*/
|
||||
private boolean passable(int x, int y, Team team){
|
||||
int tile = tiles[x][y];
|
||||
return PathTile.passable(tile) || (PathTile.team(tile) != team.ordinal() && PathTile.team(tile) != Team.derelict.ordinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the frontier, increments the search and sets up all flow sources.
|
||||
* This only occurs for active teams.
|
||||
*/
|
||||
private void updateTargets(PathData 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;
|
||||
}
|
||||
|
||||
//assign impassability to the tile
|
||||
if(!passable(x, y, path.team)){
|
||||
path.weights[x][y] = impassable;
|
||||
}
|
||||
|
||||
//increment search, clear frontier
|
||||
path.search++;
|
||||
path.frontier.clear();
|
||||
|
||||
synchronized(path.targets){
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
int tx = Pos.x(pos), ty = Pos.y(pos);
|
||||
|
||||
path.weights[tx][ty] = 0;
|
||||
path.searches[tx][ty] = (short)path.search;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void preloadPath(Team team, PathTarget target){
|
||||
updateFrontier(createPath(team, target, target.getTargets(team, new IntArray())), -1);
|
||||
}
|
||||
|
||||
/** Created a new flowfield that aims to get to a certain target for a certain team.
|
||||
* Pathfinding thread only. */
|
||||
private PathData createPath(Team team, PathTarget target, IntArray targets){
|
||||
PathData path = new PathData(team, target, world.width(), world.height());
|
||||
|
||||
list.add(path);
|
||||
pathMap[team.ordinal()][target.ordinal()] = path;
|
||||
|
||||
//grab targets from passed array
|
||||
synchronized(path.targets){
|
||||
path.targets.clear();
|
||||
path.targets.addAll(targets);
|
||||
}
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
path.weights[Pos.x(pos)][Pos.y(pos)] = 0;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/** Update the frontier for a path. Pathfinding thread only. */
|
||||
private void updateFrontier(PathData path, long nsToRun){
|
||||
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];
|
||||
|
||||
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
|
||||
if(path.frontier.size >= world.width() * world.height()){
|
||||
path.frontier.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if(cost != impassable){
|
||||
for(Point2 point : Geometry.d4){
|
||||
|
||||
int dx = tile.x + point.x, dy = tile.y + point.y;
|
||||
Tile other = world.tile(dx, dy);
|
||||
|
||||
if(other != null && (path.weights[dx][dy] > cost + other.cost || path.searches[dx][dy] < path.search) && passable(dx, dy, path.team)){
|
||||
if(other.cost < 0) throw new IllegalArgumentException("Tile cost cannot be negative! " + other);
|
||||
path.frontier.addFirst(Pos.get(dx, dy));
|
||||
path.weights[dx][dy] = cost + other.cost;
|
||||
path.searches[dx][dy] = (short)path.search;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A path target defines a set of targets for a path.*/
|
||||
public enum PathTarget{
|
||||
enemyCores((team, out) -> {
|
||||
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
|
||||
out.add(other.pos());
|
||||
}
|
||||
|
||||
//spawn points are also enemies.
|
||||
if(state.rules.waves && team == defaultTeam){
|
||||
for(Tile other : spawner.getGroundSpawns()){
|
||||
out.add(other.pos());
|
||||
}
|
||||
}
|
||||
}),
|
||||
rallyPoints((team, out) -> {
|
||||
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
|
||||
out.add(other.pos());
|
||||
}
|
||||
});
|
||||
|
||||
public static final PathTarget[] all = values();
|
||||
|
||||
private final Cons2<Team, IntArray> targeter;
|
||||
|
||||
PathTarget(Cons2<Team, IntArray> targeter){
|
||||
this.targeter = targeter;
|
||||
}
|
||||
|
||||
/** Get targets. This must run on the main thread.*/
|
||||
public IntArray getTargets(Team team, IntArray out){
|
||||
targeter.get(team, out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/** Data for a specific flow field to some set of destinations. */
|
||||
class PathData{
|
||||
/** Team this path is for. */
|
||||
final Team team;
|
||||
/** Flag that is being targeted. */
|
||||
final PathTarget target;
|
||||
/** costs of getting to a specific tile */
|
||||
final int[][] weights;
|
||||
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
|
||||
final short[][] searches;
|
||||
/** search frontier, these are Pos objects */
|
||||
final IntQueue frontier = new IntQueue();
|
||||
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
|
||||
final IntArray targets = new IntArray();
|
||||
/** current search ID */
|
||||
int search = 1;
|
||||
|
||||
PathData(Team team, PathTarget target, int width, int height){
|
||||
this.team = team;
|
||||
this.target = target;
|
||||
|
||||
this.weights = new int[width][height];
|
||||
this.searches = new short[width][height];
|
||||
this.frontier.ensureCapacity((width + height) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds a copy of tile data for a specific tile position. */
|
||||
@Struct
|
||||
class PathTileStruct{
|
||||
//traversal cost
|
||||
byte cost;
|
||||
//team of block, if applicable (0 by default)
|
||||
byte team;
|
||||
//type of target; TODO remove
|
||||
byte type;
|
||||
//whether it's viable to pass this block
|
||||
boolean passable;
|
||||
}
|
||||
}
|
||||
159
core/src/mindustry/ai/WaveSpawner.java
Normal file
159
core/src/mindustry/ai/WaveSpawner.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.Events;
|
||||
import arc.struct.Array;
|
||||
import arc.func.Floatc2;
|
||||
import arc.math.Angles;
|
||||
import arc.math.Mathf;
|
||||
import arc.util.Time;
|
||||
import arc.util.Tmp;
|
||||
import mindustry.content.Blocks;
|
||||
import mindustry.content.Fx;
|
||||
import mindustry.entities.Damage;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.entities.type.BaseUnit;
|
||||
import mindustry.game.EventType.WorldLoadEvent;
|
||||
import mindustry.game.SpawnGroup;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class WaveSpawner{
|
||||
private static final float margin = 40f, coreMargin = tilesize * 3; //how far away from the edge flying units spawn
|
||||
|
||||
private Array<FlyerSpawn> flySpawns = new Array<>();
|
||||
private Array<Tile> groundSpawns = new Array<>();
|
||||
private boolean spawning = false;
|
||||
|
||||
public WaveSpawner(){
|
||||
Events.on(WorldLoadEvent.class, e -> reset());
|
||||
}
|
||||
|
||||
public int countSpawns(){
|
||||
return groundSpawns.size;
|
||||
}
|
||||
|
||||
public Array<Tile> getGroundSpawns(){
|
||||
return groundSpawns;
|
||||
}
|
||||
|
||||
/** @return true if the player is near a ground spawn point. */
|
||||
public boolean playerNear(){
|
||||
return groundSpawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius);
|
||||
}
|
||||
|
||||
public void spawnEnemies(){
|
||||
spawning = true;
|
||||
|
||||
for(SpawnGroup group : state.rules.spawns){
|
||||
int spawned = group.getUnitsSpawned(state.wave - 1);
|
||||
|
||||
if(group.type.flying){
|
||||
float spread = margin / 1.5f;
|
||||
|
||||
eachFlyerSpawn((spawnX, spawnY) -> {
|
||||
for(int i = 0; i < spawned; i++){
|
||||
BaseUnit unit = group.createUnit(waveTeam);
|
||||
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
|
||||
unit.add();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
float spread = tilesize * 2;
|
||||
|
||||
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
|
||||
|
||||
for(int i = 0; i < spawned; i++){
|
||||
Tmp.v1.rnd(spread);
|
||||
|
||||
BaseUnit unit = group.createUnit(waveTeam);
|
||||
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
|
||||
|
||||
Time.run(Math.min(i * 5, 60 * 2), () -> spawnEffect(unit));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
|
||||
if(doShockwave){
|
||||
Time.run(20f, () -> Effects.effect(Fx.spawnShockwave, spawnX, spawnY, state.rules.dropZoneRadius));
|
||||
Time.run(40f, () -> Damage.damage(waveTeam, spawnX, spawnY, state.rules.dropZoneRadius, 99999999f, true));
|
||||
}
|
||||
});
|
||||
|
||||
Time.runTask(121f, () -> spawning = false);
|
||||
}
|
||||
|
||||
private void eachGroundSpawn(SpawnConsumer cons){
|
||||
for(Tile spawn : groundSpawns){
|
||||
cons.accept(spawn.worldx(), spawn.worldy(), true);
|
||||
}
|
||||
|
||||
if(state.rules.attackMode && state.teams.isActive(waveTeam) && !state.teams.get(defaultTeam).cores.isEmpty()){
|
||||
Tile firstCore = state.teams.get(defaultTeam).cores.first();
|
||||
for(Tile core : state.teams.get(waveTeam).cores){
|
||||
Tmp.v1.set(firstCore).sub(core.worldx(), core.worldy()).limit(coreMargin + core.block().size*tilesize);
|
||||
cons.accept(core.worldx() + Tmp.v1.x, core.worldy() + Tmp.v1.y, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void eachFlyerSpawn(Floatc2 cons){
|
||||
for(FlyerSpawn spawn : flySpawns){
|
||||
float trns = (world.width() + world.height()) * tilesize;
|
||||
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(spawn.angle, trns), -margin, world.width() * tilesize + margin);
|
||||
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(spawn.angle, trns), -margin, world.height() * tilesize + margin);
|
||||
cons.get(spawnX, spawnY);
|
||||
}
|
||||
|
||||
if(state.rules.attackMode && state.teams.isActive(waveTeam)){
|
||||
for(Tile core : state.teams.get(waveTeam).cores){
|
||||
cons.get(core.worldx(), core.worldy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSpawning(){
|
||||
return spawning && !net.client();
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
|
||||
flySpawns.clear();
|
||||
groundSpawns.clear();
|
||||
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
|
||||
if(world.tile(x, y).overlay() == Blocks.spawn){
|
||||
addSpawns(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addSpawns(int x, int y){
|
||||
groundSpawns.add(world.tile(x, y));
|
||||
|
||||
FlyerSpawn fspawn = new FlyerSpawn();
|
||||
fspawn.angle = Angles.angle(world.width() / 2f, world.height() / 2f, x, y);
|
||||
flySpawns.add(fspawn);
|
||||
}
|
||||
|
||||
private void spawnEffect(BaseUnit unit){
|
||||
Effects.effect(Fx.unitSpawn, unit.x, unit.y, 0f, unit);
|
||||
Time.run(30f, () -> {
|
||||
unit.add();
|
||||
Effects.effect(Fx.spawn, unit);
|
||||
});
|
||||
}
|
||||
|
||||
private interface SpawnConsumer{
|
||||
void accept(float x, float y, boolean shockwave);
|
||||
}
|
||||
|
||||
private class FlyerSpawn{
|
||||
float angle;
|
||||
}
|
||||
}
|
||||
1837
core/src/mindustry/content/Blocks.java
Normal file
1837
core/src/mindustry/content/Blocks.java
Normal file
File diff suppressed because it is too large
Load Diff
691
core/src/mindustry/content/Bullets.java
Normal file
691
core/src/mindustry/content/Bullets.java
Normal file
@@ -0,0 +1,691 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Bullets implements ContentList{
|
||||
public static BulletType
|
||||
|
||||
//artillery
|
||||
artilleryDense, artilleryPlastic, artilleryPlasticFrag, artilleryHoming, artilleryIncendiary, artilleryExplosive, artilleryUnit,
|
||||
|
||||
//flak
|
||||
flakScrap, flakLead, flakPlastic, flakExplosive, flakSurge, flakGlass, glassFrag,
|
||||
|
||||
//missiles
|
||||
missileExplosive, missileIncendiary, missileSurge, missileJavelin, missileSwarm, missileRevenant,
|
||||
|
||||
//standard
|
||||
standardCopper, standardDense, standardThorium, standardHoming, standardIncendiary, standardMechSmall,
|
||||
standardGlaive, standardDenseBig, standardThoriumBig, standardIncendiaryBig,
|
||||
|
||||
//electric
|
||||
lancerLaser, meltdownLaser, lightning, arc, damageLightning,
|
||||
|
||||
//liquid
|
||||
waterShot, cryoShot, slagShot, oilShot,
|
||||
|
||||
//environment, misc.
|
||||
fireball, basicFlame, pyraFlame, driverBolt, healBullet, healBulletBig, frag, eruptorShot,
|
||||
|
||||
//bombs
|
||||
bombExplosive, bombIncendiary, bombOil;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
artilleryDense = new ArtilleryBulletType(3f, 0, "shell"){{
|
||||
hitEffect = Fx.flakExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 50f;
|
||||
bulletWidth = bulletHeight = 11f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 33f;
|
||||
}};
|
||||
|
||||
artilleryPlasticFrag = new BasicBulletType(2.5f, 10, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
bulletShrink = 1f;
|
||||
lifetime = 15f;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
despawnEffect = Fx.none;
|
||||
}};
|
||||
|
||||
artilleryPlastic = new ArtilleryBulletType(3.4f, 0, "shell"){{
|
||||
hitEffect = Fx.plasticExplosion;
|
||||
knockback = 1f;
|
||||
lifetime = 55f;
|
||||
bulletWidth = bulletHeight = 13f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 35f;
|
||||
splashDamage = 45f;
|
||||
fragBullet = artilleryPlasticFrag;
|
||||
fragBullets = 10;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
}};
|
||||
|
||||
artilleryHoming = new ArtilleryBulletType(3f, 0, "shell"){{
|
||||
hitEffect = Fx.flakExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 45f;
|
||||
bulletWidth = bulletHeight = 11f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 33f;
|
||||
homingPower = 2f;
|
||||
homingRange = 50f;
|
||||
}};
|
||||
|
||||
artilleryIncendiary = new ArtilleryBulletType(3f, 0, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 60f;
|
||||
bulletWidth = bulletHeight = 13f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 30f;
|
||||
status = StatusEffects.burning;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
trailEffect = Fx.incendTrail;
|
||||
}};
|
||||
|
||||
artilleryExplosive = new ArtilleryBulletType(2f, 0, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 70f;
|
||||
bulletWidth = bulletHeight = 14f;
|
||||
collidesTiles = false;
|
||||
ammoMultiplier = 4f;
|
||||
splashDamageRadius = 45f;
|
||||
splashDamage = 50f;
|
||||
backColor = Pal.missileYellowBack;
|
||||
frontColor = Pal.missileYellow;
|
||||
}};
|
||||
|
||||
artilleryUnit = new ArtilleryBulletType(2f, 0, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 90f;
|
||||
bulletWidth = bulletHeight = 14f;
|
||||
collides = true;
|
||||
collidesTiles = true;
|
||||
splashDamageRadius = 20f;
|
||||
splashDamage = 38f;
|
||||
backColor = Pal.bulletYellowBack;
|
||||
frontColor = Pal.bulletYellow;
|
||||
}};
|
||||
|
||||
glassFrag = new BasicBulletType(3f, 6, "bullet"){{
|
||||
bulletWidth = 5f;
|
||||
bulletHeight = 12f;
|
||||
bulletShrink = 1f;
|
||||
lifetime = 20f;
|
||||
backColor = Pal.gray;
|
||||
frontColor = Color.white;
|
||||
despawnEffect = Fx.none;
|
||||
}};
|
||||
|
||||
flakLead = new FlakBulletType(4.2f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 4f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
bulletWidth = 6f;
|
||||
bulletHeight = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 27f;
|
||||
splashDamageRadius = 15f;
|
||||
}};
|
||||
|
||||
flakScrap = new FlakBulletType(4f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 5f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.5f;
|
||||
bulletWidth = 6f;
|
||||
bulletHeight = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 22f;
|
||||
splashDamageRadius = 24f;
|
||||
}};
|
||||
|
||||
flakGlass = new FlakBulletType(4f, 3){{
|
||||
lifetime = 70f;
|
||||
ammoMultiplier = 5f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.8f;
|
||||
bulletWidth = 6f;
|
||||
bulletHeight = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 30f;
|
||||
splashDamageRadius = 26f;
|
||||
fragBullet = glassFrag;
|
||||
fragBullets = 6;
|
||||
}};
|
||||
|
||||
flakPlastic = new FlakBulletType(4f, 6){{
|
||||
splashDamageRadius = 50f;
|
||||
splashDamage = 25f;
|
||||
fragBullet = artilleryPlasticFrag;
|
||||
fragBullets = 6;
|
||||
hitEffect = Fx.plasticExplosion;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
backColor = Pal.plastaniumBack;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
flakExplosive = new FlakBulletType(4f, 5){{
|
||||
//default bullet type, no changes
|
||||
shootEffect = Fx.shootBig;
|
||||
ammoMultiplier = 4f;
|
||||
}};
|
||||
|
||||
flakSurge = new FlakBulletType(4f, 7){{
|
||||
splashDamage = 33f;
|
||||
lightining = 2;
|
||||
lightningLength = 12;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
missileExplosive = new MissileBulletType(2.7f, 10, "missile"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.01f;
|
||||
splashDamageRadius = 30f;
|
||||
splashDamage = 30f;
|
||||
ammoMultiplier = 4f;
|
||||
lifetime = 150f;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
}};
|
||||
|
||||
missileIncendiary = new MissileBulletType(2.9f, 12, "missile"){{
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
bulletWidth = 7f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.01f;
|
||||
homingPower = 7f;
|
||||
splashDamageRadius = 10f;
|
||||
splashDamage = 10f;
|
||||
lifetime = 160f;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
status = StatusEffects.burning;
|
||||
}};
|
||||
|
||||
missileSurge = new MissileBulletType(4.4f, 15, "bullet"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.01f;
|
||||
splashDamageRadius = 30f;
|
||||
splashDamage = 22f;
|
||||
lifetime = 150f;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
lightining = 2;
|
||||
lightningLength = 14;
|
||||
}};
|
||||
|
||||
missileJavelin = new MissileBulletType(5f, 10.5f, "missile"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.003f;
|
||||
keepVelocity = false;
|
||||
splashDamageRadius = 20f;
|
||||
splashDamage = 1f;
|
||||
lifetime = 90f;
|
||||
trailColor = Color.valueOf("b6c6fd");
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
backColor = Pal.bulletYellowBack;
|
||||
frontColor = Pal.bulletYellow;
|
||||
weaveScale = 8f;
|
||||
weaveMag = 2f;
|
||||
}};
|
||||
|
||||
missileSwarm = new MissileBulletType(2.7f, 12, "missile"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.003f;
|
||||
homingRange = 60f;
|
||||
keepVelocity = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 10f;
|
||||
lifetime = 120f;
|
||||
trailColor = Color.gray;
|
||||
backColor = Pal.bulletYellowBack;
|
||||
frontColor = Pal.bulletYellow;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
weaveScale = 8f;
|
||||
weaveMag = 2f;
|
||||
}};
|
||||
|
||||
missileRevenant = new MissileBulletType(2.7f, 12, "missile"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 8f;
|
||||
bulletShrink = 0f;
|
||||
drag = -0.003f;
|
||||
homingRange = 60f;
|
||||
keepVelocity = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 10f;
|
||||
lifetime = 60f;
|
||||
trailColor = Pal.unitBack;
|
||||
backColor = Pal.unitBack;
|
||||
frontColor = Pal.unitFront;
|
||||
hitEffect = Fx.blastExplosion;
|
||||
despawnEffect = Fx.blastExplosion;
|
||||
weaveScale = 6f;
|
||||
weaveMag = 1f;
|
||||
}};
|
||||
|
||||
standardCopper = new BasicBulletType(2.5f, 9, "bullet"){{
|
||||
bulletWidth = 7f;
|
||||
bulletHeight = 9f;
|
||||
lifetime = 60f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
smokeEffect = Fx.shootSmallSmoke;
|
||||
ammoMultiplier = 2;
|
||||
}};
|
||||
|
||||
standardDense = new BasicBulletType(3.5f, 18, "bullet"){{
|
||||
bulletWidth = 9f;
|
||||
bulletHeight = 12f;
|
||||
reloadMultiplier = 0.6f;
|
||||
ammoMultiplier = 4;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardThorium = new BasicBulletType(4f, 29, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 13f;
|
||||
shootEffect = Fx.shootBig;
|
||||
smokeEffect = Fx.shootBigSmoke;
|
||||
ammoMultiplier = 4;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardHoming = new BasicBulletType(3f, 9, "bullet"){{
|
||||
bulletWidth = 7f;
|
||||
bulletHeight = 9f;
|
||||
homingPower = 5f;
|
||||
reloadMultiplier = 1.4f;
|
||||
ammoMultiplier = 5;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardIncendiary = new BasicBulletType(3.2f, 11, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
status = StatusEffects.burning;
|
||||
inaccuracy = 3f;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardGlaive = new BasicBulletType(4f, 7.5f, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
frontColor = Color.valueOf("feb380");
|
||||
backColor = Color.valueOf("ea8878");
|
||||
status = StatusEffects.burning;
|
||||
lifetime = 60f;
|
||||
}};
|
||||
|
||||
standardMechSmall = new BasicBulletType(4f, 9, "bullet"){{
|
||||
bulletWidth = 11f;
|
||||
bulletHeight = 14f;
|
||||
lifetime = 40f;
|
||||
inaccuracy = 5f;
|
||||
despawnEffect = Fx.hitBulletSmall;
|
||||
}};
|
||||
|
||||
standardDenseBig = new BasicBulletType(7f, 42, "bullet"){{
|
||||
bulletWidth = 15f;
|
||||
bulletHeight = 21f;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
standardThoriumBig = new BasicBulletType(8f, 65, "bullet"){{
|
||||
bulletWidth = 16f;
|
||||
bulletHeight = 23f;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
standardIncendiaryBig = new BasicBulletType(7f, 38, "bullet"){{
|
||||
bulletWidth = 16f;
|
||||
bulletHeight = 21f;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
status = StatusEffects.burning;
|
||||
shootEffect = Fx.shootBig;
|
||||
}};
|
||||
|
||||
damageLightning = new BulletType(0.0001f, 0f){{
|
||||
lifetime = Lightning.lifetime;
|
||||
hitEffect = Fx.hitLancer;
|
||||
despawnEffect = Fx.none;
|
||||
status = StatusEffects.shocked;
|
||||
statusDuration = 10f;
|
||||
}};
|
||||
|
||||
healBullet = new HealBulletType(5.2f, 13){{
|
||||
healPercent = 3f;
|
||||
}};
|
||||
|
||||
healBulletBig = new HealBulletType(5.2f, 15){{
|
||||
healPercent = 5.5f;
|
||||
}};
|
||||
|
||||
fireball = new BulletType(1f, 4){
|
||||
{
|
||||
pierce = true;
|
||||
hitTiles = false;
|
||||
collides = false;
|
||||
collidesTiles = false;
|
||||
drag = 0.03f;
|
||||
hitEffect = despawnEffect = Fx.none;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
b.velocity().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){
|
||||
Fire.create(tile);
|
||||
}
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.1 * Time.delta())){
|
||||
Effects.effect(Fx.fireballsmoke, b.x, b.y);
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.1 * Time.delta())){
|
||||
Effects.effect(Fx.ballfire, b.x, b.y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
basicFlame = new BulletType(3f, 6f){
|
||||
{
|
||||
ammoMultiplier = 3f;
|
||||
hitSize = 7f;
|
||||
lifetime = 42f;
|
||||
pierce = true;
|
||||
drag = 0.05f;
|
||||
statusDuration = 60f * 4;
|
||||
shootEffect = Fx.shootSmallFlame;
|
||||
hitEffect = Fx.hitFlameSmall;
|
||||
despawnEffect = Fx.none;
|
||||
status = StatusEffects.burning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float range(){
|
||||
return 50f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
}
|
||||
};
|
||||
|
||||
pyraFlame = new BulletType(3.3f, 9f){
|
||||
{
|
||||
ammoMultiplier = 4f;
|
||||
hitSize = 7f;
|
||||
lifetime = 42f;
|
||||
pierce = true;
|
||||
drag = 0.05f;
|
||||
statusDuration = 60f * 6;
|
||||
shootEffect = Fx.shootPyraFlame;
|
||||
hitEffect = Fx.hitFlameSmall;
|
||||
despawnEffect = Fx.none;
|
||||
status = StatusEffects.burning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
}
|
||||
};
|
||||
|
||||
lancerLaser = new BulletType(0.001f, 140){
|
||||
Color[] colors = {Pal.lancerLaser.cpy().mul(1f, 1f, 1f, 0.4f), Pal.lancerLaser, Color.white};
|
||||
float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
|
||||
float[] lenscales = {1f, 1.1f, 1.13f, 1.14f};
|
||||
float length = 160f;
|
||||
|
||||
{
|
||||
hitEffect = Fx.hitLancer;
|
||||
despawnEffect = Fx.none;
|
||||
hitSize = 4;
|
||||
lifetime = 16f;
|
||||
pierce = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float range(){
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
Damage.collideLine(b, b.getTeam(), hitEffect, b.x, b.y, b.rot(), length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float f = Mathf.curve(b.fin(), 0f, 0.2f);
|
||||
float baseLen = length * f;
|
||||
|
||||
Lines.lineAngle(b.x, b.y, b.rot(), baseLen);
|
||||
for(int s = 0; s < 3; s++){
|
||||
Draw.color(colors[s]);
|
||||
for(int i = 0; i < tscales.length; i++){
|
||||
Lines.stroke(7f * b.fout() * (s == 0 ? 1.5f : s == 1 ? 1f : 0.3f) * tscales[i]);
|
||||
Lines.lineAngle(b.x, b.y, b.rot(), baseLen * lenscales[i]);
|
||||
}
|
||||
}
|
||||
Draw.reset();
|
||||
}
|
||||
};
|
||||
|
||||
meltdownLaser = new BulletType(0.001f, 70){
|
||||
Color tmpColor = new Color();
|
||||
Color[] colors = {Color.valueOf("ec745855"), Color.valueOf("ec7458aa"), Color.valueOf("ff9c5a"), Color.white};
|
||||
float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
|
||||
float[] strokes = {2f, 1.5f, 1f, 0.3f};
|
||||
float[] lenscales = {1f, 1.12f, 1.15f, 1.17f};
|
||||
float length = 220f;
|
||||
|
||||
{
|
||||
hitEffect = Fx.hitMeltdown;
|
||||
despawnEffect = Fx.none;
|
||||
hitSize = 4;
|
||||
drawSize = 420f;
|
||||
lifetime = 16f;
|
||||
pierce = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(1, 5f)){
|
||||
Damage.collideLine(b, b.getTeam(), hitEffect, b.x, b.y, b.rot(), length, true);
|
||||
}
|
||||
Effects.shake(1f, 1f, b.x, b.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.effect(hitEffect, colors[2], hitx, hity);
|
||||
if(Mathf.chance(0.4)){
|
||||
Fire.create(world.tileWorld(hitx + Mathf.range(5f), hity + Mathf.range(5f)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float baseLen = (length) * b.fout();
|
||||
|
||||
Lines.lineAngle(b.x, b.y, b.rot(), baseLen);
|
||||
for(int s = 0; s < colors.length; s++){
|
||||
Draw.color(tmpColor.set(colors[s]).mul(1f + Mathf.absin(Time.time(), 1f, 0.1f)));
|
||||
for(int i = 0; i < tscales.length; i++){
|
||||
Tmp.v1.trns(b.rot() + 180f, (lenscales[i] - 1f) * 35f);
|
||||
Lines.stroke((9f + Mathf.absin(Time.time(), 0.8f, 1.5f)) * b.fout() * strokes[s] * tscales[i]);
|
||||
Lines.lineAngle(b.x + Tmp.v1.x, b.y + Tmp.v1.y, b.rot(), baseLen * lenscales[i], CapStyle.none);
|
||||
}
|
||||
}
|
||||
Draw.reset();
|
||||
}
|
||||
};
|
||||
|
||||
waterShot = new LiquidBulletType(Liquids.water){{
|
||||
knockback = 0.7f;
|
||||
}};
|
||||
|
||||
cryoShot = new LiquidBulletType(Liquids.cryofluid){{
|
||||
|
||||
}};
|
||||
|
||||
slagShot = new LiquidBulletType(Liquids.slag){{
|
||||
damage = 4;
|
||||
drag = 0.03f;
|
||||
}};
|
||||
|
||||
eruptorShot = new LiquidBulletType(Liquids.slag){{
|
||||
damage = 2;
|
||||
speed = 2.1f;
|
||||
drag = 0.02f;
|
||||
}};
|
||||
|
||||
oilShot = new LiquidBulletType(Liquids.oil){{
|
||||
drag = 0.03f;
|
||||
}};
|
||||
|
||||
lightning = new BulletType(0.001f, 12f){
|
||||
{
|
||||
lifetime = 1f;
|
||||
shootEffect = Fx.hitLancer;
|
||||
smokeEffect = Fx.none;
|
||||
despawnEffect = Fx.none;
|
||||
hitEffect = Fx.hitLancer;
|
||||
keepVelocity = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float range(){
|
||||
return 70f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
Lightning.create(b.getTeam(), Pal.lancerLaser, damage * (b.getOwner() instanceof Player ? state.rules.playerDamageMultiplier : 1f), b.x, b.y, b.rot(), 30);
|
||||
}
|
||||
};
|
||||
|
||||
arc = new BulletType(0.001f, 21){
|
||||
{
|
||||
lifetime = 1;
|
||||
despawnEffect = Fx.none;
|
||||
hitEffect = Fx.hitLancer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
Lightning.create(b.getTeam(), Pal.lancerLaser, damage, b.x, b.y, b.rot(), 25);
|
||||
}
|
||||
};
|
||||
|
||||
driverBolt = new MassDriverBolt();
|
||||
|
||||
frag = new BasicBulletType(5f, 8, "bullet"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 9f;
|
||||
bulletShrink = 0.5f;
|
||||
lifetime = 50f;
|
||||
drag = 0.04f;
|
||||
}};
|
||||
|
||||
bombExplosive = new BombBulletType(10f, 20f, "shell"){{
|
||||
bulletWidth = 9f;
|
||||
bulletHeight = 13f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
shootEffect = Fx.none;
|
||||
smokeEffect = Fx.none;
|
||||
}};
|
||||
|
||||
bombIncendiary = new BombBulletType(7f, 10f, "shell"){{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 12f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
backColor = Pal.lightOrange;
|
||||
frontColor = Pal.lightishOrange;
|
||||
incendChance = 1f;
|
||||
incendAmount = 3;
|
||||
incendSpread = 10f;
|
||||
}};
|
||||
|
||||
bombOil = new BombBulletType(2f, 3f, "shell"){
|
||||
{
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 12f;
|
||||
hitEffect = Fx.pulverize;
|
||||
backColor = new Color(0x4f4f4fff);
|
||||
frontColor = Color.gray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet b, float x, float y){
|
||||
super.hit(b, x, y);
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
Tile tile = world.tileWorld(x + Mathf.range(8f), y + Mathf.range(8f));
|
||||
Puddle.deposit(tile, Liquids.oil, 5f);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
1099
core/src/mindustry/content/Fx.java
Normal file
1099
core/src/mindustry/content/Fx.java
Normal file
File diff suppressed because it is too large
Load Diff
101
core/src/mindustry/content/Items.java
Normal file
101
core/src/mindustry/content/Items.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.Color;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.type.Item;
|
||||
import mindustry.type.ItemType;
|
||||
|
||||
public class Items implements ContentList{
|
||||
public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phasefabric, surgealloy,
|
||||
sporePod, sand, blastCompound, pyratite, metaglass;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
copper = new Item("copper", Color.valueOf("d99d73")){{
|
||||
type = ItemType.material;
|
||||
hardness = 1;
|
||||
cost = 0.5f;
|
||||
alwaysUnlocked = true;
|
||||
}};
|
||||
|
||||
lead = new Item("lead", Color.valueOf("8c7fa9")){{
|
||||
type = ItemType.material;
|
||||
hardness = 1;
|
||||
cost = 0.7f;
|
||||
}};
|
||||
|
||||
metaglass = new Item("metaglass", Color.valueOf("ebeef5")){{
|
||||
type = ItemType.material;
|
||||
cost = 1.5f;
|
||||
}};
|
||||
|
||||
graphite = new Item("graphite", Color.valueOf("b2c6d2")){{
|
||||
type = ItemType.material;
|
||||
cost = 1f;
|
||||
}};
|
||||
|
||||
sand = new Item("sand", Color.valueOf("f7cba4")){{
|
||||
|
||||
}};
|
||||
|
||||
coal = new Item("coal", Color.valueOf("272727")){{
|
||||
explosiveness = 0.2f;
|
||||
flammability = 1f;
|
||||
hardness = 2;
|
||||
}};
|
||||
|
||||
titanium = new Item("titanium", Color.valueOf("8da1e3")){{
|
||||
type = ItemType.material;
|
||||
hardness = 3;
|
||||
cost = 1f;
|
||||
}};
|
||||
|
||||
thorium = new Item("thorium", Color.valueOf("f9a3c7")){{
|
||||
type = ItemType.material;
|
||||
explosiveness = 0.2f;
|
||||
hardness = 4;
|
||||
radioactivity = 1f;
|
||||
cost = 1.1f;
|
||||
}};
|
||||
|
||||
scrap = new Item("scrap", Color.valueOf("777777")){{
|
||||
|
||||
}};
|
||||
|
||||
silicon = new Item("silicon", Color.valueOf("53565c")){{
|
||||
type = ItemType.material;
|
||||
cost = 0.8f;
|
||||
}};
|
||||
|
||||
plastanium = new Item("plastanium", Color.valueOf("cbd97f")){{
|
||||
type = ItemType.material;
|
||||
flammability = 0.1f;
|
||||
explosiveness = 0.2f;
|
||||
cost = 1.3f;
|
||||
}};
|
||||
|
||||
phasefabric = new Item("phase-fabric", Color.valueOf("f4ba6e")){{
|
||||
type = ItemType.material;
|
||||
cost = 1.3f;
|
||||
radioactivity = 0.6f;
|
||||
}};
|
||||
|
||||
surgealloy = new Item("surge-alloy", Color.valueOf("f3e979")){{
|
||||
type = ItemType.material;
|
||||
}};
|
||||
|
||||
sporePod = new Item("spore-pod", Color.valueOf("7457ce")){{
|
||||
flammability = 1.15f;
|
||||
}};
|
||||
|
||||
blastCompound = new Item("blast-compound", Color.valueOf("ff795e")){{
|
||||
flammability = 0.4f;
|
||||
explosiveness = 1.2f;
|
||||
}};
|
||||
|
||||
pyratite = new Item("pyratite", Color.valueOf("ffaa5f")){{
|
||||
flammability = 1.4f;
|
||||
explosiveness = 0.4f;
|
||||
}};
|
||||
}
|
||||
}
|
||||
41
core/src/mindustry/content/Liquids.java
Normal file
41
core/src/mindustry/content/Liquids.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.graphics.Color;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.type.Liquid;
|
||||
|
||||
public class Liquids implements ContentList{
|
||||
public static Liquid water, slag, oil, cryofluid;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
water = new Liquid("water", Color.valueOf("596ab8")){{
|
||||
heatCapacity = 0.4f;
|
||||
effect = StatusEffects.wet;
|
||||
}};
|
||||
|
||||
slag = new Liquid("slag", Color.valueOf("ffa166")){{
|
||||
temperature = 1f;
|
||||
viscosity = 0.8f;
|
||||
effect = StatusEffects.melting;
|
||||
lightColor = Color.valueOf("f0511d").a(0.4f);
|
||||
}};
|
||||
|
||||
oil = new Liquid("oil", Color.valueOf("313131")){{
|
||||
viscosity = 0.7f;
|
||||
flammability = 1.2f;
|
||||
explosiveness = 1.2f;
|
||||
heatCapacity = 0.7f;
|
||||
barColor = Color.valueOf("6b675f");
|
||||
effect = StatusEffects.tarred;
|
||||
}};
|
||||
|
||||
cryofluid = new Liquid("cryofluid", Color.valueOf("6ecdec")){{
|
||||
heatCapacity = 0.9f;
|
||||
temperature = 0.25f;
|
||||
effect = StatusEffects.freezing;
|
||||
lightColor = Color.valueOf("0097f5").a(0.2f);
|
||||
}};
|
||||
}
|
||||
}
|
||||
26
core/src/mindustry/content/Loadouts.java
Normal file
26
core/src/mindustry/content/Loadouts.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package mindustry.content;
|
||||
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class Loadouts implements ContentList{
|
||||
public static Schematic
|
||||
basicShard,
|
||||
advancedShard,
|
||||
basicFoundation,
|
||||
basicNucleus;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
try{
|
||||
basicShard = Schematics.readBase64("bXNjaAB4nD2K2wqAIBiD5ymibnoRn6YnEP1BwUMoBL19FuJ2sbFvUFgYZDaJsLeQrkinN9UJHImsNzlYE7WrIUastuSbnlKx2VJJt+8IQGGKdfO/8J5yrGJSMegLg+YUIA==");
|
||||
advancedShard = Schematics.readBase64("bXNjaAB4nD2LjQqAIAyET7OMIOhFfJqeYMxBgSkYCL199gu33fFtB4tOwUTaBCP5QpHFzwtl32DahBeKK1NwPq8hoOcUixwpY+CUxe3XIwBbB/pa6tadVCUP02hgHvp5vZq/0b7pBHPYFOQ=");
|
||||
basicFoundation = Schematics.readBase64("bXNjaAB4nD1OSQ6DMBBzFhVu8BG+0X8MQyoiJTNSukj8nlCi2Adbtg/GA4OBF8oB00rvyE/9ykafqOIw58A7SWRKy1ZiShhZ5RcOLZhYS1hefQ1gRIeptH9jq/qW2lvc1d2tgWsOfVX/tOwE86AYBA==");
|
||||
basicNucleus = Schematics.readBase64("bXNjaAB4nD2MUQqAIBBEJy0s6qOLdJXuYNtCgikYBd2+LNmdj308hkGHtkId7M4YFns4mk/yfB4a48602eDI+mlNznu0FMPFd0wYKCaewl8F0EOueqM+yKSLVfJrNKWnSw/FZGzEGXFG9sy/px4gEBW1");
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
378
core/src/mindustry/content/Mechs.java
Normal file
378
core/src/mindustry/content/Mechs.java
Normal file
@@ -0,0 +1,378 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
public class Mechs implements ContentList{
|
||||
public static Mech alpha, delta, tau, omega, dart, javelin, trident, glaive;
|
||||
|
||||
public static Mech starter;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
alpha = new Mech("alpha-mech", false){
|
||||
{
|
||||
drillPower = 1;
|
||||
mineSpeed = 1.5f;
|
||||
mass = 1.2f;
|
||||
speed = 0.5f;
|
||||
itemCapacity = 40;
|
||||
boostSpeed = 0.95f;
|
||||
buildPower = 1.2f;
|
||||
engineColor = Color.valueOf("ffd37f");
|
||||
health = 250f;
|
||||
|
||||
weapon = new Weapon("blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 14f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardMechSmall;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlt(Player player){
|
||||
player.healBy(Time.delta() * 0.09f);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
delta = new Mech("delta-mech", false){
|
||||
float cooldown = 120;
|
||||
|
||||
{
|
||||
drillPower = -1;
|
||||
speed = 0.75f;
|
||||
boostSpeed = 0.95f;
|
||||
itemCapacity = 15;
|
||||
mass = 0.9f;
|
||||
health = 150f;
|
||||
buildPower = 0.9f;
|
||||
weaponOffsetX = -1;
|
||||
weaponOffsetY = -1;
|
||||
engineColor = Color.valueOf("d3ddff");
|
||||
|
||||
weapon = new Weapon("shockgun"){{
|
||||
shake = 2f;
|
||||
length = 1f;
|
||||
reload = 55f;
|
||||
shotDelay = 3f;
|
||||
alternate = true;
|
||||
shots = 2;
|
||||
inaccuracy = 0f;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.lightning;
|
||||
shootSound = Sounds.spark;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLand(Player player){
|
||||
if(player.timer.get(Player.timerAbility, cooldown)){
|
||||
Effects.shake(1f, 1f, player);
|
||||
Effects.effect(Fx.landShock, player);
|
||||
for(int i = 0; i < 8; i++){
|
||||
Time.run(Mathf.random(8f), () -> Lightning.create(player.getTeam(), Pal.lancerLaser, 17f * Vars.state.rules.playerDamageMultiplier, player.x, player.y, Mathf.random(360f), 14));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tau = new Mech("tau-mech", false){
|
||||
float healRange = 60f;
|
||||
float healAmount = 10f;
|
||||
float healReload = 160f;
|
||||
boolean wasHealed;
|
||||
|
||||
{
|
||||
drillPower = 4;
|
||||
mineSpeed = 3f;
|
||||
itemCapacity = 70;
|
||||
weaponOffsetY = -1;
|
||||
weaponOffsetX = 1;
|
||||
mass = 1.75f;
|
||||
speed = 0.44f;
|
||||
drag = 0.35f;
|
||||
boostSpeed = 0.8f;
|
||||
canHeal = true;
|
||||
health = 200f;
|
||||
buildPower = 1.6f;
|
||||
engineColor = Pal.heal;
|
||||
|
||||
weapon = new Weapon("heal-blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 24f;
|
||||
alternate = false;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBullet;
|
||||
shootSound = Sounds.pew;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlt(Player player){
|
||||
|
||||
if(player.timer.get(Player.timerAbility, healReload)){
|
||||
wasHealed = false;
|
||||
|
||||
Units.nearby(player.getTeam(), player.x, player.y, healRange, unit -> {
|
||||
if(unit.health < unit.maxHealth()){
|
||||
Effects.effect(Fx.heal, unit);
|
||||
wasHealed = true;
|
||||
}
|
||||
unit.healBy(healAmount);
|
||||
});
|
||||
|
||||
if(wasHealed){
|
||||
Effects.effect(Fx.healWave, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
omega = new Mech("omega-mech", false){
|
||||
protected TextureRegion armorRegion;
|
||||
|
||||
{
|
||||
drillPower = 2;
|
||||
mineSpeed = 1.5f;
|
||||
itemCapacity = 80;
|
||||
speed = 0.36f;
|
||||
boostSpeed = 0.6f;
|
||||
mass = 4f;
|
||||
shake = 4f;
|
||||
weaponOffsetX = 1;
|
||||
weaponOffsetY = 0;
|
||||
engineColor = Color.valueOf("feb380");
|
||||
health = 350f;
|
||||
buildPower = 1.5f;
|
||||
weapon = new Weapon("swarmer"){{
|
||||
length = 1.5f;
|
||||
recoil = 4f;
|
||||
reload = 38f;
|
||||
shots = 4;
|
||||
spacing = 8f;
|
||||
inaccuracy = 8f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
shake = 3f;
|
||||
bullet = Bullets.missileSwarm;
|
||||
shootSound = Sounds.shootBig;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRotationAlpha(Player player){
|
||||
return 0.6f - player.shootHeat * 0.3f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float spreadX(Player player){
|
||||
return player.shootHeat * 2f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
super.load();
|
||||
armorRegion = Core.atlas.find(name + "-armor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlt(Player player){
|
||||
float scl = 1f - player.shootHeat / 2f*Time.delta();
|
||||
player.velocity().scl(scl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getExtraArmor(Player player){
|
||||
return player.shootHeat * 30f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Player player){
|
||||
if(player.shootHeat <= 0.01f) return;
|
||||
|
||||
Shaders.build.progress = player.shootHeat;
|
||||
Shaders.build.region = armorRegion;
|
||||
Shaders.build.time = Time.time() / 10f;
|
||||
Shaders.build.color.set(Pal.accent).a = player.shootHeat;
|
||||
Draw.shader(Shaders.build);
|
||||
Draw.rect(armorRegion, player.x, player.y, player.rotation);
|
||||
Draw.shader();
|
||||
}
|
||||
};
|
||||
|
||||
dart = new Mech("dart-ship", true){
|
||||
{
|
||||
drillPower = 1;
|
||||
mineSpeed = 3f;
|
||||
speed = 0.5f;
|
||||
drag = 0.09f;
|
||||
health = 200f;
|
||||
weaponOffsetX = -1;
|
||||
weaponOffsetY = -1;
|
||||
engineColor = Pal.lightTrail;
|
||||
cellTrnsY = 1f;
|
||||
buildPower = 1.1f;
|
||||
weapon = new Weapon("blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 15f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardCopper;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean alwaysUnlocked(){
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
javelin = new Mech("javelin-ship", true){
|
||||
float minV = 3.6f;
|
||||
float maxV = 6f;
|
||||
TextureRegion shield;
|
||||
|
||||
{
|
||||
drillPower = -1;
|
||||
speed = 0.11f;
|
||||
drag = 0.01f;
|
||||
mass = 2f;
|
||||
health = 170f;
|
||||
engineColor = Color.valueOf("d3ddff");
|
||||
cellTrnsY = 1f;
|
||||
weapon = new Weapon("missiles"){{
|
||||
length = 1.5f;
|
||||
reload = 70f;
|
||||
shots = 4;
|
||||
inaccuracy = 2f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
bullet = Bullets.missileJavelin;
|
||||
shootSound = Sounds.missile;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
super.load();
|
||||
shield = Core.atlas.find(name + "-shield");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRotationAlpha(Player player){
|
||||
return 0.5f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlt(Player player){
|
||||
float scl = scld(player);
|
||||
if(Mathf.chance(Time.delta() * (0.15 * scl))){
|
||||
Effects.effect(Fx.hitLancer, Pal.lancerLaser, player.x, player.y);
|
||||
Lightning.create(player.getTeam(), Pal.lancerLaser, 10f * Vars.state.rules.playerDamageMultiplier,
|
||||
player.x + player.velocity().x, player.y + player.velocity().y, player.rotation, 14);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Player player){
|
||||
float scl = scld(player);
|
||||
if(scl < 0.01f) return;
|
||||
Draw.color(Pal.lancerLaser);
|
||||
Draw.alpha(scl / 2f);
|
||||
Draw.blend(Blending.additive);
|
||||
Draw.rect(shield, player.x + Mathf.range(scl / 2f), player.y + Mathf.range(scl / 2f), player.rotation - 90);
|
||||
Draw.blend();
|
||||
}
|
||||
|
||||
float scld(Player player){
|
||||
return Mathf.clamp((player.velocity().len() - minV) / (maxV - minV));
|
||||
}
|
||||
};
|
||||
|
||||
trident = new Mech("trident-ship", true){
|
||||
{
|
||||
drillPower = 2;
|
||||
speed = 0.15f;
|
||||
drag = 0.034f;
|
||||
mass = 2.5f;
|
||||
turnCursor = false;
|
||||
health = 250f;
|
||||
itemCapacity = 30;
|
||||
engineColor = Color.valueOf("84f491");
|
||||
cellTrnsY = 1f;
|
||||
buildPower = 2.5f;
|
||||
weapon = new Weapon("bomber"){{
|
||||
length = 0f;
|
||||
width = 2f;
|
||||
reload = 25f;
|
||||
shots = 2;
|
||||
shotDelay = 1f;
|
||||
shots = 8;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 1f;
|
||||
inaccuracy = 20f;
|
||||
ignoreRotation = true;
|
||||
bullet = new BombBulletType(16f, 25f, "shell"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 14f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
shootEffect = Fx.none;
|
||||
smokeEffect = Fx.none;
|
||||
shootSound = Sounds.artillery;
|
||||
}};
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canShoot(Player player){
|
||||
return player.velocity().len() > 1.2f;
|
||||
}
|
||||
};
|
||||
|
||||
glaive = new Mech("glaive-ship", true){
|
||||
{
|
||||
drillPower = 4;
|
||||
mineSpeed = 1.3f;
|
||||
speed = 0.32f;
|
||||
drag = 0.06f;
|
||||
mass = 3f;
|
||||
health = 240f;
|
||||
itemCapacity = 60;
|
||||
engineColor = Color.valueOf("feb380");
|
||||
cellTrnsY = 1f;
|
||||
buildPower = 1.2f;
|
||||
|
||||
weapon = new Weapon("bomber"){{
|
||||
length = 1.5f;
|
||||
reload = 13f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardGlaive;
|
||||
shootSound = Sounds.shootSnap;
|
||||
}};
|
||||
}
|
||||
};
|
||||
|
||||
starter = dart;
|
||||
}
|
||||
}
|
||||
107
core/src/mindustry/content/StatusEffects.java
Normal file
107
core/src/mindustry/content/StatusEffects.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.Mathf;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.type.StatusEffect;
|
||||
|
||||
import static mindustry.Vars.waveTeam;
|
||||
|
||||
public class StatusEffects implements ContentList{
|
||||
public static StatusEffect none, burning, freezing, wet, melting, tarred, overdrive, shielded, shocked, corroded, boss;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
none = new StatusEffect("none");
|
||||
|
||||
burning = new StatusEffect("burning"){{
|
||||
damage = 0.06f;
|
||||
effect = Fx.burning;
|
||||
|
||||
init(() -> {
|
||||
opposite(wet,freezing);
|
||||
trans(tarred, ((unit, time, newTime, result) -> {
|
||||
unit.damage(1f);
|
||||
Effects.effect(Fx.burning, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
|
||||
result.set(this, Math.min(time + newTime, 300f));
|
||||
}));
|
||||
});
|
||||
}};
|
||||
|
||||
freezing = new StatusEffect("freezing"){{
|
||||
speedMultiplier = 0.6f;
|
||||
armorMultiplier = 0.8f;
|
||||
effect = Fx.freezing;
|
||||
|
||||
init(() -> {
|
||||
opposite(melting, burning);
|
||||
});
|
||||
}};
|
||||
|
||||
wet = new StatusEffect("wet"){{
|
||||
speedMultiplier = 0.9f;
|
||||
effect = Fx.wet;
|
||||
|
||||
init(() -> {
|
||||
trans(shocked, ((unit, time, newTime, result) -> {
|
||||
unit.damage(20f);
|
||||
if(unit.getTeam() == waveTeam){
|
||||
Events.fire(Trigger.shock);
|
||||
}
|
||||
result.set(this, time);
|
||||
}));
|
||||
opposite(burning);
|
||||
});
|
||||
}};
|
||||
|
||||
melting = new StatusEffect("melting"){{
|
||||
speedMultiplier = 0.8f;
|
||||
armorMultiplier = 0.8f;
|
||||
damage = 0.3f;
|
||||
effect = Fx.melting;
|
||||
|
||||
init(() -> {
|
||||
trans(tarred, ((unit, time, newTime, result) -> result.set(this, Math.min(time + newTime / 2f, 140f))));
|
||||
opposite(wet, freezing);
|
||||
});
|
||||
}};
|
||||
|
||||
tarred = new StatusEffect("tarred"){{
|
||||
speedMultiplier = 0.6f;
|
||||
effect = Fx.oily;
|
||||
|
||||
init(() -> {
|
||||
trans(melting, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
|
||||
trans(burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
|
||||
});
|
||||
}};
|
||||
|
||||
overdrive = new StatusEffect("overdrive"){{
|
||||
armorMultiplier = 0.95f;
|
||||
speedMultiplier = 1.15f;
|
||||
damageMultiplier = 1.4f;
|
||||
damage = -0.01f;
|
||||
effect = Fx.overdriven;
|
||||
}};
|
||||
|
||||
shielded = new StatusEffect("shielded"){{
|
||||
armorMultiplier = 3f;
|
||||
}};
|
||||
|
||||
boss = new StatusEffect("boss"){{
|
||||
armorMultiplier = 3f;
|
||||
damageMultiplier = 3f;
|
||||
speedMultiplier = 1.1f;
|
||||
}};
|
||||
|
||||
shocked = new StatusEffect("shocked");
|
||||
|
||||
//no effects, just small amounts of damage.
|
||||
corroded = new StatusEffect("corroded"){{
|
||||
damage = 0.1f;
|
||||
}};
|
||||
}
|
||||
}
|
||||
361
core/src/mindustry/content/TechTree.java
Normal file
361
core/src/mindustry/content/TechTree.java
Normal file
@@ -0,0 +1,361 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.struct.Array;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.type.ItemStack;
|
||||
import mindustry.world.Block;
|
||||
|
||||
import static mindustry.content.Blocks.*;
|
||||
|
||||
public class TechTree implements ContentList{
|
||||
public static Array<TechNode> all;
|
||||
public static TechNode root;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
TechNode.context = null;
|
||||
all = new Array<>();
|
||||
|
||||
root = node(coreShard, () -> {
|
||||
|
||||
node(conveyor, () -> {
|
||||
|
||||
node(junction, () -> {
|
||||
node(itemBridge);
|
||||
node(router, () -> {
|
||||
node(launchPad, () -> {
|
||||
node(launchPadLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(distributor);
|
||||
node(sorter, () -> {
|
||||
node(invertedSorter);
|
||||
node(message);
|
||||
node(overflowGate);
|
||||
});
|
||||
node(container, () -> {
|
||||
node(unloader);
|
||||
node(vault, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(titaniumConveyor, () -> {
|
||||
node(phaseConveyor, () -> {
|
||||
node(massDriver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(armoredConveyor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(duo, () -> {
|
||||
node(scatter, () -> {
|
||||
node(hail, () -> {
|
||||
|
||||
node(salvo, () -> {
|
||||
node(swarmer, () -> {
|
||||
node(cyclone, () -> {
|
||||
node(spectre, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(ripple, () -> {
|
||||
node(fuse, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(scorch, () -> {
|
||||
node(arc, () -> {
|
||||
node(wave, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(lancer, () -> {
|
||||
node(meltdown, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(shockMine, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
node(copperWall, () -> {
|
||||
node(copperWallLarge);
|
||||
node(titaniumWall, () -> {
|
||||
node(door, () -> {
|
||||
node(doorLarge);
|
||||
});
|
||||
node(plastaniumWall, () -> {
|
||||
node(plastaniumWallLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
node(titaniumWallLarge);
|
||||
node(thoriumWall, () -> {
|
||||
node(thoriumWallLarge);
|
||||
node(surgeWall, () -> {
|
||||
node(surgeWallLarge);
|
||||
node(phaseWall, () -> {
|
||||
node(phaseWallLarge);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(mechanicalDrill, () -> {
|
||||
node(graphitePress, () -> {
|
||||
node(pneumaticDrill, () -> {
|
||||
node(cultivator, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(laserDrill, () -> {
|
||||
node(blastDrill, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(waterExtractor, () -> {
|
||||
node(oilExtractor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(pyratiteMixer, () -> {
|
||||
node(blastMixer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(siliconSmelter, () -> {
|
||||
|
||||
node(sporePress, () -> {
|
||||
node(coalCentrifuge, () -> {
|
||||
|
||||
});
|
||||
node(multiPress, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(plastaniumCompressor, () -> {
|
||||
node(phaseWeaver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(kiln, () -> {
|
||||
node(incinerator, () -> {
|
||||
node(melter, () -> {
|
||||
node(surgeSmelter, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(separator, () -> {
|
||||
node(pulverizer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(cryofluidMixer, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
node(mechanicalPump, () -> {
|
||||
node(conduit, () -> {
|
||||
node(liquidJunction, () -> {
|
||||
node(liquidRouter, () -> {
|
||||
node(liquidTank);
|
||||
|
||||
node(pulseConduit, () -> {
|
||||
node(phaseConduit, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(platedConduit, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(rotaryPump, () -> {
|
||||
node(thermalPump, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
node(bridgeConduit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(combustionGenerator, () -> {
|
||||
node(powerNode, () -> {
|
||||
node(powerNodeLarge, () -> {
|
||||
node(diode, () -> {
|
||||
node(surgeTower, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(battery, () -> {
|
||||
node(batteryLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(mender, () -> {
|
||||
node(mendProjector, () -> {
|
||||
node(forceProjector, () -> {
|
||||
node(overdriveProjector, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(repairPoint, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(turbineGenerator, () -> {
|
||||
node(thermalGenerator, () -> {
|
||||
node(differentialGenerator, () -> {
|
||||
node(thoriumReactor, () -> {
|
||||
node(impactReactor, () -> {
|
||||
|
||||
});
|
||||
|
||||
node(rtgGenerator, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(solarPanel, () -> {
|
||||
node(largeSolarPanel, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(draugFactory, () -> {
|
||||
node(spiritFactory, () -> {
|
||||
node(phantomFactory);
|
||||
});
|
||||
|
||||
node(daggerFactory, () -> {
|
||||
node(commandCenter, () -> {});
|
||||
node(crawlerFactory, () -> {
|
||||
node(titanFactory, () -> {
|
||||
node(fortressFactory, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(wraithFactory, () -> {
|
||||
node(ghoulFactory, () -> {
|
||||
node(revenantFactory, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(dartPad, () -> {
|
||||
node(deltaPad, () -> {
|
||||
|
||||
node(javelinPad, () -> {
|
||||
node(tridentPad, () -> {
|
||||
node(glaivePad);
|
||||
});
|
||||
});
|
||||
|
||||
node(tauPad, () -> {
|
||||
node(omegaPad, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static TechNode node(Block block, Runnable children){
|
||||
ItemStack[] requirements = new ItemStack[block.requirements.length];
|
||||
for(int i = 0; i < requirements.length; i++){
|
||||
requirements[i] = new ItemStack(block.requirements[i].item, 30 + block.requirements[i].amount * 6);
|
||||
}
|
||||
|
||||
return new TechNode(block, requirements, children);
|
||||
}
|
||||
|
||||
private static TechNode node(Block block){
|
||||
return node(block, () -> {});
|
||||
}
|
||||
|
||||
public static TechNode create(Block parent, Block block){
|
||||
TechNode.context = all.find(t -> t.block == parent);
|
||||
return node(block, () -> {});
|
||||
}
|
||||
|
||||
public static class TechNode{
|
||||
static TechNode context;
|
||||
|
||||
public final Block block;
|
||||
public final ItemStack[] requirements;
|
||||
public final Array<TechNode> children = new Array<>();
|
||||
|
||||
TechNode(TechNode ccontext, Block block, ItemStack[] requirements, Runnable children){
|
||||
if(ccontext != null){
|
||||
ccontext.children.add(this);
|
||||
}
|
||||
|
||||
this.block = block;
|
||||
this.requirements = requirements;
|
||||
|
||||
context = this;
|
||||
children.run();
|
||||
context = ccontext;
|
||||
all.add(this);
|
||||
}
|
||||
|
||||
TechNode(Block block, ItemStack[] requirements, Runnable children){
|
||||
this(context, block, requirements, children);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
core/src/mindustry/content/TypeIDs.java
Normal file
18
core/src/mindustry/content/TypeIDs.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package mindustry.content;
|
||||
|
||||
import mindustry.entities.effect.Fire;
|
||||
import mindustry.entities.effect.Puddle;
|
||||
import mindustry.entities.type.Player;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.type.TypeID;
|
||||
|
||||
public class TypeIDs implements ContentList{
|
||||
public static TypeID fire, puddle, player;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
fire = new TypeID("fire", Fire::new);
|
||||
puddle = new TypeID("puddle", Puddle::new);
|
||||
player = new TypeID("player", Player::new);
|
||||
}
|
||||
}
|
||||
390
core/src/mindustry/content/UnitTypes.java
Normal file
390
core/src/mindustry/content/UnitTypes.java
Normal file
@@ -0,0 +1,390 @@
|
||||
package mindustry.content;
|
||||
|
||||
import arc.struct.*;
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.entities.type.base.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
public class UnitTypes implements ContentList{
|
||||
public static UnitType
|
||||
draug, spirit, phantom,
|
||||
wraith, ghoul, revenant, lich, reaper,
|
||||
dagger, crawler, titan, fortress, eruptor, chaosArray, eradicator;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
draug = new UnitType("draug", MinerDrone::new){{
|
||||
flying = true;
|
||||
drag = 0.01f;
|
||||
speed = 0.3f;
|
||||
maxVelocity = 1.2f;
|
||||
range = 50f;
|
||||
health = 80;
|
||||
minePower = 0.9f;
|
||||
engineSize = 1.8f;
|
||||
engineOffset = 5.7f;
|
||||
weapon = new Weapon("you have incurred my wrath. prepare to die."){{
|
||||
bullet = Bullets.lancerLaser;
|
||||
}};
|
||||
}};
|
||||
|
||||
spirit = new UnitType("spirit", RepairDrone::new){{
|
||||
flying = true;
|
||||
drag = 0.01f;
|
||||
speed = 0.42f;
|
||||
maxVelocity = 1.6f;
|
||||
range = 50f;
|
||||
health = 100;
|
||||
engineSize = 1.8f;
|
||||
engineOffset = 5.7f;
|
||||
weapon = new Weapon(){{
|
||||
length = 1.5f;
|
||||
reload = 40f;
|
||||
width = 0.5f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBulletBig;
|
||||
shootSound = Sounds.pew;
|
||||
}};
|
||||
}};
|
||||
|
||||
phantom = new UnitType("phantom", BuilderDrone::new){{
|
||||
flying = true;
|
||||
drag = 0.01f;
|
||||
mass = 2f;
|
||||
speed = 0.45f;
|
||||
maxVelocity = 1.9f;
|
||||
range = 70f;
|
||||
itemCapacity = 70;
|
||||
health = 400;
|
||||
buildPower = 0.4f;
|
||||
engineOffset = 6.5f;
|
||||
toMine = ObjectSet.with(Items.lead, Items.copper, Items.titanium);
|
||||
weapon = new Weapon(){{
|
||||
length = 1.5f;
|
||||
reload = 20f;
|
||||
width = 0.5f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBullet;
|
||||
}};
|
||||
}};
|
||||
|
||||
dagger = new UnitType("dagger", GroundUnit::new){{
|
||||
maxVelocity = 1.1f;
|
||||
speed = 0.2f;
|
||||
drag = 0.4f;
|
||||
hitsize = 8f;
|
||||
mass = 1.75f;
|
||||
health = 130;
|
||||
weapon = new Weapon("chain-blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 28f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardCopper;
|
||||
}};
|
||||
}};
|
||||
|
||||
crawler = new UnitType("crawler", GroundUnit::new){{
|
||||
maxVelocity = 1.27f;
|
||||
speed = 0.285f;
|
||||
drag = 0.4f;
|
||||
hitsize = 8f;
|
||||
mass = 1.75f;
|
||||
health = 120;
|
||||
weapon = new Weapon(){{
|
||||
reload = 12f;
|
||||
ejectEffect = Fx.none;
|
||||
shootSound = Sounds.explosion;
|
||||
bullet = new BombBulletType(2f, 3f, "clear"){
|
||||
{
|
||||
hitEffect = Fx.pulverize;
|
||||
lifetime = 30f;
|
||||
speed = 1.1f;
|
||||
splashDamageRadius = 55f;
|
||||
splashDamage = 30f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
if(b.getOwner() instanceof Unit){
|
||||
((Unit)b.getOwner()).kill();
|
||||
}
|
||||
b.time(b.lifetime());
|
||||
}
|
||||
};
|
||||
}};
|
||||
}};
|
||||
|
||||
titan = new UnitType("titan", GroundUnit::new){{
|
||||
maxVelocity = 0.8f;
|
||||
speed = 0.22f;
|
||||
drag = 0.4f;
|
||||
mass = 3.5f;
|
||||
hitsize = 9f;
|
||||
range = 10f;
|
||||
rotatespeed = 0.1f;
|
||||
health = 460;
|
||||
immunities.add(StatusEffects.burning);
|
||||
weapon = new Weapon("flamethrower"){{
|
||||
shootSound = Sounds.flame;
|
||||
length = 1f;
|
||||
reload = 14f;
|
||||
range = 30f;
|
||||
alternate = true;
|
||||
recoil = 1f;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.basicFlame;
|
||||
}};
|
||||
}};
|
||||
|
||||
fortress = new UnitType("fortress", GroundUnit::new){{
|
||||
maxVelocity = 0.78f;
|
||||
speed = 0.15f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 10f;
|
||||
rotatespeed = 0.06f;
|
||||
targetAir = false;
|
||||
health = 750;
|
||||
weapon = new Weapon("artillery"){{
|
||||
length = 1f;
|
||||
reload = 60f;
|
||||
width = 10f;
|
||||
alternate = true;
|
||||
recoil = 4f;
|
||||
shake = 2f;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.artilleryUnit;
|
||||
shootSound = Sounds.artillery;
|
||||
}};
|
||||
}};
|
||||
|
||||
eruptor = new UnitType("eruptor", GroundUnit::new){{
|
||||
maxVelocity = 0.81f;
|
||||
speed = 0.16f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 9f;
|
||||
rotatespeed = 0.05f;
|
||||
targetAir = false;
|
||||
health = 600;
|
||||
immunities = ObjectSet.with(StatusEffects.burning, StatusEffects.melting);
|
||||
weapon = new Weapon("eruption"){{
|
||||
length = 3f;
|
||||
reload = 10f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.eruptorShot;
|
||||
recoil = 1f;
|
||||
width = 7f;
|
||||
shootSound = Sounds.flame;
|
||||
}};
|
||||
}};
|
||||
|
||||
chaosArray = new UnitType("chaos-array", GroundUnit::new){{
|
||||
maxVelocity = 0.68f;
|
||||
speed = 0.12f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
rotatespeed = 0.06f;
|
||||
health = 3000;
|
||||
weapon = new Weapon("chaos"){{
|
||||
length = 8f;
|
||||
reload = 50f;
|
||||
width = 17f;
|
||||
alternate = true;
|
||||
recoil = 3f;
|
||||
shake = 2f;
|
||||
shots = 4;
|
||||
spacing = 4f;
|
||||
shotDelay = 5;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.flakSurge;
|
||||
shootSound = Sounds.shootBig;
|
||||
}};
|
||||
}};
|
||||
|
||||
eradicator = new UnitType("eradicator", GroundUnit::new){{
|
||||
maxVelocity = 0.68f;
|
||||
speed = 0.12f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
rotatespeed = 0.06f;
|
||||
health = 9000;
|
||||
weapon = new Weapon("eradication"){{
|
||||
length = 13f;
|
||||
reload = 30f;
|
||||
width = 22f;
|
||||
alternate = true;
|
||||
recoil = 3f;
|
||||
shake = 2f;
|
||||
inaccuracy = 3f;
|
||||
shots = 4;
|
||||
spacing = 0f;
|
||||
shotDelay = 3;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.standardThoriumBig;
|
||||
shootSound = Sounds.shootBig;
|
||||
}};
|
||||
}};
|
||||
|
||||
wraith = new UnitType("wraith", FlyingUnit::new){{
|
||||
speed = 0.3f;
|
||||
maxVelocity = 1.9f;
|
||||
drag = 0.01f;
|
||||
mass = 1.5f;
|
||||
flying = true;
|
||||
health = 75;
|
||||
engineOffset = 5.5f;
|
||||
range = 140f;
|
||||
weapon = new Weapon(){{
|
||||
length = 1.5f;
|
||||
reload = 28f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardCopper;
|
||||
shootSound = Sounds.shoot;
|
||||
}};
|
||||
}};
|
||||
|
||||
ghoul = new UnitType("ghoul", FlyingUnit::new){{
|
||||
health = 220;
|
||||
speed = 0.2f;
|
||||
maxVelocity = 1.4f;
|
||||
mass = 3f;
|
||||
drag = 0.01f;
|
||||
flying = true;
|
||||
targetAir = false;
|
||||
engineOffset = 7.8f;
|
||||
range = 140f;
|
||||
weapon = new Weapon(){{
|
||||
length = 0f;
|
||||
width = 2f;
|
||||
reload = 12f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 1f;
|
||||
inaccuracy = 40f;
|
||||
ignoreRotation = true;
|
||||
bullet = Bullets.bombExplosive;
|
||||
shootSound = Sounds.none;
|
||||
}};
|
||||
}};
|
||||
|
||||
revenant = new UnitType("revenant", HoverUnit::new){{
|
||||
health = 1000;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
speed = 0.1f;
|
||||
maxVelocity = 1f;
|
||||
drag = 0.01f;
|
||||
range = 80f;
|
||||
shootCone = 40f;
|
||||
flying = true;
|
||||
rotateWeapon = true;
|
||||
engineOffset = 12f;
|
||||
engineSize = 3f;
|
||||
rotatespeed = 0.01f;
|
||||
attackLength = 90f;
|
||||
baseRotateSpeed = 0.06f;
|
||||
weapon = new Weapon("revenant-missiles"){{
|
||||
length = 3f;
|
||||
reload = 70f;
|
||||
width = 10f;
|
||||
shots = 2;
|
||||
inaccuracy = 2f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
shootSound = Sounds.missile;
|
||||
bullet = Bullets.missileRevenant;
|
||||
}};
|
||||
}};
|
||||
|
||||
lich = new UnitType("lich", HoverUnit::new){{
|
||||
health = 6000;
|
||||
mass = 20f;
|
||||
hitsize = 40f;
|
||||
speed = 0.01f;
|
||||
maxVelocity = 0.6f;
|
||||
drag = 0.02f;
|
||||
range = 80f;
|
||||
shootCone = 20f;
|
||||
flying = true;
|
||||
rotateWeapon = true;
|
||||
engineOffset = 21;
|
||||
engineSize = 5.3f;
|
||||
rotatespeed = 0.01f;
|
||||
attackLength = 90f;
|
||||
baseRotateSpeed = 0.04f;
|
||||
weapon = new Weapon("lich-missiles"){{
|
||||
length = 4f;
|
||||
reload = 160f;
|
||||
width = 22f;
|
||||
shots = 16;
|
||||
shootCone = 100f;
|
||||
shotDelay = 2;
|
||||
inaccuracy = 10f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
bullet = Bullets.missileRevenant;
|
||||
shootSound = Sounds.artillery;
|
||||
}};
|
||||
}};
|
||||
|
||||
reaper = new UnitType("reaper", HoverUnit::new){{
|
||||
health = 11000;
|
||||
mass = 30f;
|
||||
hitsize = 56f;
|
||||
speed = 0.01f;
|
||||
maxVelocity = 0.6f;
|
||||
drag = 0.02f;
|
||||
range = 80f;
|
||||
shootCone = 30f;
|
||||
flying = true;
|
||||
rotateWeapon = true;
|
||||
engineOffset = 40;
|
||||
engineSize = 7.3f;
|
||||
rotatespeed = 0.01f;
|
||||
baseRotateSpeed = 0.04f;
|
||||
weapon = new Weapon("reaper-gun"){{
|
||||
length = 3f;
|
||||
reload = 10f;
|
||||
width = 32f;
|
||||
shots = 1;
|
||||
shootCone = 100f;
|
||||
|
||||
shake = 1f;
|
||||
inaccuracy = 3f;
|
||||
alternate = true;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = new BasicBulletType(7f, 42, "bullet"){
|
||||
{
|
||||
bulletWidth = 15f;
|
||||
bulletHeight = 21f;
|
||||
shootEffect = Fx.shootBig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float range(){
|
||||
return 165f;
|
||||
}
|
||||
};
|
||||
shootSound = Sounds.shootBig;
|
||||
}};
|
||||
}};
|
||||
}
|
||||
}
|
||||
252
core/src/mindustry/content/Zones.java
Normal file
252
core/src/mindustry/content/Zones.java
Normal file
@@ -0,0 +1,252 @@
|
||||
package mindustry.content;
|
||||
|
||||
import mindustry.ctype.ContentList;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.maps.generators.*;
|
||||
import mindustry.maps.generators.MapGenerator.*;
|
||||
import mindustry.maps.zonegen.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static arc.struct.Array.with;
|
||||
import static mindustry.content.Items.*;
|
||||
import static mindustry.type.ItemStack.list;
|
||||
|
||||
public class Zones implements ContentList{
|
||||
public static Zone
|
||||
groundZero, desertWastes,
|
||||
craters, frozenForest, ruinousShores, stainedMountains, tarFields, fungalPass,
|
||||
saltFlats, overgrowth, impact0078, crags,
|
||||
desolateRift, nuclearComplex;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
groundZero = new Zone("groundZero", new MapGenerator("groundZero", 1)){{
|
||||
baseLaunchCost = list(copper, -60);
|
||||
startingItems = list(copper, 60);
|
||||
alwaysUnlocked = true;
|
||||
conditionWave = 5;
|
||||
launchPeriod = 5;
|
||||
resources = with(copper, scrap, lead);
|
||||
}};
|
||||
|
||||
desertWastes = new Zone("desertWastes", new DesertWastesGenerator(260, 260)){{
|
||||
startingItems = list(copper, 120);
|
||||
conditionWave = 20;
|
||||
launchPeriod = 10;
|
||||
loadout = Loadouts.advancedShard;
|
||||
resources = with(copper, lead, coal, sand);
|
||||
rules = r -> {
|
||||
r.waves = true;
|
||||
r.waveTimer = true;
|
||||
r.launchWaveMultiplier = 3f;
|
||||
r.waveSpacing = 60 * 50f;
|
||||
r.spawns = with(
|
||||
new SpawnGroup(UnitTypes.crawler){{
|
||||
unitScaling = 3f;
|
||||
}},
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
unitScaling = 4f;
|
||||
begin = 2;
|
||||
spacing = 2;
|
||||
}},
|
||||
new SpawnGroup(UnitTypes.wraith){{
|
||||
unitScaling = 3f;
|
||||
begin = 11;
|
||||
spacing = 3;
|
||||
}},
|
||||
new SpawnGroup(UnitTypes.eruptor){{
|
||||
unitScaling = 3f;
|
||||
begin = 22;
|
||||
unitAmount = 1;
|
||||
spacing = 4;
|
||||
}},
|
||||
new SpawnGroup(UnitTypes.titan){{
|
||||
unitScaling = 3f;
|
||||
begin = 37;
|
||||
unitAmount = 2;
|
||||
spacing = 4;
|
||||
}},
|
||||
new SpawnGroup(UnitTypes.fortress){{
|
||||
unitScaling = 2f;
|
||||
effect = StatusEffects.boss;
|
||||
begin = 41;
|
||||
spacing = 20;
|
||||
}}
|
||||
);
|
||||
};
|
||||
requirements = with(
|
||||
new ZoneWave(groundZero, 20),
|
||||
new Unlock(Blocks.combustionGenerator)
|
||||
);
|
||||
}};
|
||||
|
||||
saltFlats = new Zone("saltFlats", new MapGenerator("saltFlats")){{
|
||||
startingItems = list(copper, 200, Items.silicon, 200, lead, 200);
|
||||
loadout = Loadouts.basicFoundation;
|
||||
conditionWave = 10;
|
||||
launchPeriod = 5;
|
||||
configureObjective = new Launched(this);
|
||||
resources = with(copper, scrap, lead, coal, sand, titanium);
|
||||
requirements = with(
|
||||
new ZoneWave(desertWastes, 60),
|
||||
new Unlock(Blocks.daggerFactory),
|
||||
new Unlock(Blocks.draugFactory),
|
||||
new Unlock(Blocks.door),
|
||||
new Unlock(Blocks.waterExtractor)
|
||||
);
|
||||
}};
|
||||
|
||||
frozenForest = new Zone("frozenForest", new MapGenerator("frozenForest", 1)
|
||||
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = list(copper, 250);
|
||||
conditionWave = 10;
|
||||
resources = with(copper, lead, coal);
|
||||
requirements = with(
|
||||
new ZoneWave(groundZero, 10),
|
||||
new Unlock(Blocks.junction),
|
||||
new Unlock(Blocks.router)
|
||||
);
|
||||
}};
|
||||
|
||||
craters = new Zone("craters", new MapGenerator("craters", 1).decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.004))){{
|
||||
startingItems = list(copper, 100);
|
||||
conditionWave = 10;
|
||||
resources = with(copper, lead, coal, sand, scrap);
|
||||
requirements = with(
|
||||
new ZoneWave(frozenForest, 10),
|
||||
new Unlock(Blocks.mender),
|
||||
new Unlock(Blocks.combustionGenerator)
|
||||
);
|
||||
}};
|
||||
|
||||
ruinousShores = new Zone("ruinousShores", new MapGenerator("ruinousShores", 1)){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = list(copper, 140, lead, 50);
|
||||
conditionWave = 20;
|
||||
launchPeriod = 20;
|
||||
resources = with(copper, scrap, lead, coal, sand);
|
||||
requirements = with(
|
||||
new ZoneWave(desertWastes, 20),
|
||||
new ZoneWave(craters, 15),
|
||||
new Unlock(Blocks.graphitePress),
|
||||
new Unlock(Blocks.combustionGenerator),
|
||||
new Unlock(Blocks.kiln),
|
||||
new Unlock(Blocks.mechanicalPump)
|
||||
);
|
||||
}};
|
||||
|
||||
stainedMountains = new Zone("stainedMountains", new MapGenerator("stainedMountains", 2)
|
||||
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = list(copper, 200, lead, 50);
|
||||
conditionWave = 10;
|
||||
launchPeriod = 10;
|
||||
resources = with(copper, scrap, lead, coal, titanium, sand);
|
||||
requirements = with(
|
||||
new ZoneWave(frozenForest, 15),
|
||||
new Unlock(Blocks.pneumaticDrill),
|
||||
new Unlock(Blocks.powerNode),
|
||||
new Unlock(Blocks.turbineGenerator)
|
||||
);
|
||||
}};
|
||||
|
||||
fungalPass = new Zone("fungalPass", new MapGenerator("fungalPass")){{
|
||||
startingItems = list(copper, 250, lead, 250, Items.metaglass, 100, Items.graphite, 100);
|
||||
resources = with(copper, lead, coal, titanium, sand);
|
||||
configureObjective = new Launched(this);
|
||||
requirements = with(
|
||||
new ZoneWave(stainedMountains, 15),
|
||||
new Unlock(Blocks.daggerFactory),
|
||||
new Unlock(Blocks.crawlerFactory),
|
||||
new Unlock(Blocks.door),
|
||||
new Unlock(Blocks.siliconSmelter)
|
||||
);
|
||||
}};
|
||||
|
||||
overgrowth = new Zone("overgrowth", new MapGenerator("overgrowth")){{
|
||||
startingItems = list(copper, 1500, lead, 1000, Items.silicon, 500, Items.metaglass, 250);
|
||||
conditionWave = 12;
|
||||
launchPeriod = 4;
|
||||
loadout = Loadouts.basicNucleus;
|
||||
configureObjective = new Launched(this);
|
||||
resources = with(copper, lead, coal, titanium, sand, thorium, scrap);
|
||||
requirements = with(
|
||||
new ZoneWave(craters, 40),
|
||||
new Launched(fungalPass),
|
||||
new Unlock(Blocks.cultivator),
|
||||
new Unlock(Blocks.sporePress),
|
||||
new Unlock(Blocks.titanFactory),
|
||||
new Unlock(Blocks.wraithFactory)
|
||||
);
|
||||
}};
|
||||
|
||||
tarFields = new Zone("tarFields", new MapGenerator("tarFields")
|
||||
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = list(copper, 250, lead, 100);
|
||||
conditionWave = 15;
|
||||
launchPeriod = 10;
|
||||
resources = with(copper, scrap, lead, coal, titanium, thorium, sand);
|
||||
requirements = with(
|
||||
new ZoneWave(ruinousShores, 20),
|
||||
new Unlock(Blocks.coalCentrifuge),
|
||||
new Unlock(Blocks.conduit),
|
||||
new Unlock(Blocks.wave)
|
||||
);
|
||||
}};
|
||||
|
||||
desolateRift = new Zone("desolateRift", new MapGenerator("desolateRift")){{
|
||||
loadout = Loadouts.basicNucleus;
|
||||
startingItems = list(copper, 1000, lead, 1000, Items.graphite, 250, titanium, 250, Items.silicon, 250);
|
||||
conditionWave = 3;
|
||||
launchPeriod = 2;
|
||||
resources = with(copper, scrap, lead, coal, titanium, sand, thorium);
|
||||
requirements = with(
|
||||
new ZoneWave(tarFields, 20),
|
||||
new Unlock(Blocks.thermalGenerator),
|
||||
new Unlock(Blocks.thoriumReactor)
|
||||
);
|
||||
}};
|
||||
|
||||
/*
|
||||
crags = new Zone("crags", new MapGenerator("crags").dist(2f)){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
baseLaunchCost = ItemStack.with();
|
||||
startingItems = ItemStack.list(Items.copper, 2000, Items.lead, 2000, Items.graphite, 500, Items.titanium, 500, Items.silicon, 500);
|
||||
conditionWave = 3;
|
||||
launchPeriod = 2;
|
||||
requirements = with(stainedMountains, 40);
|
||||
blockRequirements = new Block[]{Blocks.thermalGenerator};
|
||||
resources = Array.with(Items.copper, Items.scrap, Items.lead, Items.coal, Items.sand};
|
||||
}};*/
|
||||
|
||||
nuclearComplex = new Zone("nuclearComplex", new MapGenerator("nuclearProductionComplex", 1)
|
||||
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.01))){{
|
||||
loadout = Loadouts.basicNucleus;
|
||||
startingItems = list(copper, 1250, lead, 1500, Items.silicon, 400, Items.metaglass, 250);
|
||||
conditionWave = 30;
|
||||
launchPeriod = 15;
|
||||
resources = with(copper, scrap, lead, coal, titanium, thorium, sand);
|
||||
requirements = with(
|
||||
new Launched(fungalPass),
|
||||
new Unlock(Blocks.thermalGenerator),
|
||||
new Unlock(Blocks.laserDrill)
|
||||
);
|
||||
}};
|
||||
|
||||
/*
|
||||
impact0078 = new Zone("impact0078", new MapGenerator("impact0078").dist(2f)){{
|
||||
loadout = Loadouts.basicNucleus;
|
||||
baseLaunchCost = ItemStack.with();
|
||||
startingItems = ItemStack.list(Items.copper, 2000, Items.lead, 2000, Items.graphite, 500, Items.titanium, 500, Items.silicon, 500);
|
||||
conditionWave = 3;
|
||||
launchPeriod = 2;
|
||||
requirements = with(nuclearComplex, 40);
|
||||
blockRequirements = new Block[]{Blocks.thermalGenerator};
|
||||
resources = Array.with(Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.thorium};
|
||||
}};*/
|
||||
}
|
||||
}
|
||||
268
core/src/mindustry/core/ContentLoader.java
Normal file
268
core/src/mindustry/core/ContentLoader.java
Normal file
@@ -0,0 +1,268 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.mod.Mods.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static arc.Core.files;
|
||||
import static mindustry.Vars.mods;
|
||||
|
||||
/**
|
||||
* Loads all game content.
|
||||
* Call load() before doing anything with content.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ContentLoader{
|
||||
private ObjectMap<String, MappableContent>[] contentNameMap = new ObjectMap[ContentType.values().length];
|
||||
private Array<Content>[] contentMap = new Array[ContentType.values().length];
|
||||
private MappableContent[][] temporaryMapper;
|
||||
private @Nullable LoadedMod currentMod;
|
||||
private @Nullable Content lastAdded;
|
||||
private ObjectSet<Cons<Content>> initialization = new ObjectSet<>();
|
||||
private ContentList[] content = {
|
||||
new Fx(),
|
||||
new Items(),
|
||||
new StatusEffects(),
|
||||
new Liquids(),
|
||||
new Bullets(),
|
||||
new Mechs(),
|
||||
new UnitTypes(),
|
||||
new Blocks(),
|
||||
new Loadouts(),
|
||||
new TechTree(),
|
||||
new Zones(),
|
||||
new TypeIDs(),
|
||||
|
||||
//these are not really content classes, but this makes initialization easier
|
||||
new LegacyColorMapper(),
|
||||
};
|
||||
|
||||
public ContentLoader(){
|
||||
clear();
|
||||
}
|
||||
|
||||
/** Clears all initialized content.*/
|
||||
public void clear(){
|
||||
contentNameMap = new ObjectMap[ContentType.values().length];
|
||||
contentMap = new Array[ContentType.values().length];
|
||||
initialization = new ObjectSet<>();
|
||||
|
||||
for(ContentType type : ContentType.values()){
|
||||
contentMap[type.ordinal()] = new Array<>();
|
||||
contentNameMap[type.ordinal()] = new ObjectMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Creates all base types. */
|
||||
public void createBaseContent(){
|
||||
for(ContentList list : content){
|
||||
list.load();
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates mod content, if applicable. */
|
||||
public void createModContent(){
|
||||
if(mods != null){
|
||||
mods.loadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs content statistics.*/
|
||||
public void logContent(){
|
||||
//check up ID mapping, make sure it's linear (debug only)
|
||||
for(Array<Content> arr : contentMap){
|
||||
for(int i = 0; i < arr.size; i++){
|
||||
int id = arr.get(i).id;
|
||||
if(id != i){
|
||||
throw new IllegalArgumentException("Out-of-order IDs for content '" + arr.get(i) + "' (expected " + i + " but got " + id + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug("--- CONTENT INFO ---");
|
||||
for(int k = 0; k < contentMap.length; k++){
|
||||
Log.debug("[{0}]: loaded {1}", ContentType.values()[k].name(), contentMap[k].size);
|
||||
}
|
||||
Log.debug("Total content loaded: {0}", Array.with(ContentType.values()).mapInt(c -> contentMap[c.ordinal()].size).sum());
|
||||
Log.debug("-------------------");
|
||||
}
|
||||
|
||||
/** Calls Content#init() on everything. Use only after all modules have been created.*/
|
||||
public void init(){
|
||||
initialize(Content::init);
|
||||
}
|
||||
|
||||
/** Calls Content#load() on everything. Use only after all modules have been created on the client.*/
|
||||
public void load(){
|
||||
initialize(Content::load);
|
||||
}
|
||||
|
||||
/** Initializes all content with the specified function. */
|
||||
private void initialize(Cons<Content> callable){
|
||||
if(initialization.contains(callable)) return;
|
||||
|
||||
for(ContentType type : ContentType.values()){
|
||||
for(Content content : contentMap[type.ordinal()]){
|
||||
try{
|
||||
callable.get(content);
|
||||
}catch(Throwable e){
|
||||
if(content.minfo.mod != null){
|
||||
mods.handleContentError(content, e);
|
||||
}else{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initialization.add(callable);
|
||||
}
|
||||
|
||||
/** Loads block colors. */
|
||||
public void loadColors(){
|
||||
Pixmap pixmap = new Pixmap(files.internal("sprites/block_colors.png"));
|
||||
for(int i = 0; i < pixmap.getWidth(); i++){
|
||||
if(blocks().size > i){
|
||||
int color = pixmap.getPixel(i, 0);
|
||||
|
||||
if(color == 0) continue;
|
||||
|
||||
Block block = block(i);
|
||||
Color.rgba8888ToColor(block.color, color);
|
||||
}
|
||||
}
|
||||
pixmap.dispose();
|
||||
}
|
||||
|
||||
public void dispose(){
|
||||
//clear all content, currently not used
|
||||
}
|
||||
|
||||
/** Get last piece of content created for error-handling purposes. */
|
||||
public @Nullable Content getLastAdded(){
|
||||
return lastAdded;
|
||||
}
|
||||
|
||||
/** Remove last content added in case of an exception. */
|
||||
public void removeLast(){
|
||||
if(lastAdded != null && contentMap[lastAdded.getContentType().ordinal()].peek() == lastAdded){
|
||||
contentMap[lastAdded.getContentType().ordinal()].pop();
|
||||
if(lastAdded instanceof MappableContent){
|
||||
contentNameMap[lastAdded.getContentType().ordinal()].remove(((MappableContent)lastAdded).name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleContent(Content content){
|
||||
this.lastAdded = content;
|
||||
contentMap[content.getContentType().ordinal()].add(content);
|
||||
}
|
||||
|
||||
public void setCurrentMod(@Nullable LoadedMod mod){
|
||||
this.currentMod = mod;
|
||||
}
|
||||
|
||||
public String transformName(String name){
|
||||
return currentMod == null ? name : currentMod.name + "-" + name;
|
||||
}
|
||||
|
||||
public void handleMappableContent(MappableContent content){
|
||||
if(contentNameMap[content.getContentType().ordinal()].containsKey(content.name)){
|
||||
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + content.name + "')");
|
||||
}
|
||||
if(currentMod != null){
|
||||
content.minfo.mod = currentMod;
|
||||
}
|
||||
contentNameMap[content.getContentType().ordinal()].put(content.name, content);
|
||||
}
|
||||
|
||||
public void setTemporaryMapper(MappableContent[][] temporaryMapper){
|
||||
this.temporaryMapper = temporaryMapper;
|
||||
}
|
||||
|
||||
public Array<Content>[] getContentMap(){
|
||||
return contentMap;
|
||||
}
|
||||
|
||||
public <T extends MappableContent> T getByName(ContentType type, String name){
|
||||
if(contentNameMap[type.ordinal()] == null){
|
||||
return null;
|
||||
}
|
||||
return (T)contentNameMap[type.ordinal()].get(name);
|
||||
}
|
||||
|
||||
public <T extends Content> T getByID(ContentType type, int id){
|
||||
|
||||
if(temporaryMapper != null && temporaryMapper[type.ordinal()] != null && temporaryMapper[type.ordinal()].length != 0){
|
||||
//-1 = invalid content
|
||||
if(id < 0){
|
||||
return null;
|
||||
}
|
||||
if(temporaryMapper[type.ordinal()].length <= id || temporaryMapper[type.ordinal()][id] == null){
|
||||
return (T)contentMap[type.ordinal()].get(0); //default value is always ID 0
|
||||
}
|
||||
return (T)temporaryMapper[type.ordinal()][id];
|
||||
}
|
||||
|
||||
if(id >= contentMap[type.ordinal()].size || id < 0){
|
||||
return null;
|
||||
}
|
||||
return (T)contentMap[type.ordinal()].get(id);
|
||||
}
|
||||
|
||||
public <T extends Content> Array<T> getBy(ContentType type){
|
||||
return (Array<T>)contentMap[type.ordinal()];
|
||||
}
|
||||
|
||||
//utility methods, just makes things a bit shorter
|
||||
|
||||
public Array<Block> blocks(){
|
||||
return getBy(ContentType.block);
|
||||
}
|
||||
|
||||
public Block block(int id){
|
||||
return (Block)getByID(ContentType.block, id);
|
||||
}
|
||||
|
||||
public Array<Item> items(){
|
||||
return getBy(ContentType.item);
|
||||
}
|
||||
|
||||
public Item item(int id){
|
||||
return (Item)getByID(ContentType.item, id);
|
||||
}
|
||||
|
||||
public Array<Liquid> liquids(){
|
||||
return getBy(ContentType.liquid);
|
||||
}
|
||||
|
||||
public Liquid liquid(int id){
|
||||
return (Liquid)getByID(ContentType.liquid, id);
|
||||
}
|
||||
|
||||
public Array<BulletType> bullets(){
|
||||
return getBy(ContentType.bullet);
|
||||
}
|
||||
|
||||
public BulletType bullet(int id){
|
||||
return (BulletType)getByID(ContentType.bullet, id);
|
||||
}
|
||||
|
||||
public Array<Zone> zones(){
|
||||
return getBy(ContentType.zone);
|
||||
}
|
||||
|
||||
public Array<UnitType> units(){
|
||||
return getBy(ContentType.unit);
|
||||
}
|
||||
}
|
||||
482
core/src/mindustry/core/Control.java
Normal file
482
core/src/mindustry/core/Control.java
Normal file
@@ -0,0 +1,482 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.audio.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.input.*;
|
||||
import mindustry.maps.Map;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.net;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/**
|
||||
* Control module.
|
||||
* Handles all input, saving, keybinds and keybinds.
|
||||
* Should <i>not</i> handle any logic-critical state.
|
||||
* This class is not created in the headless server.
|
||||
*/
|
||||
public class Control implements ApplicationListener, Loadable{
|
||||
public Saves saves;
|
||||
public MusicControl music;
|
||||
public Tutorial tutorial;
|
||||
public InputHandler input;
|
||||
|
||||
private Interval timer = new Interval(2);
|
||||
private boolean hiscore = false;
|
||||
private boolean wasPaused = false;
|
||||
|
||||
public Control(){
|
||||
saves = new Saves();
|
||||
tutorial = new Tutorial();
|
||||
music = new MusicControl();
|
||||
|
||||
Events.on(StateChangeEvent.class, event -> {
|
||||
if((event.from == State.playing && event.to == State.menu) || (event.from == State.menu && event.to != State.menu)){
|
||||
Time.runTask(5f, platform::updateRPC);
|
||||
for(Sound sound : assets.getAll(Sound.class, new Array<>())){
|
||||
sound.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(PlayEvent.class, event -> {
|
||||
player.setTeam(state.rules.pvp ? netServer.assignTeam(player, playerGroup.all()) : defaultTeam);
|
||||
player.setDead(true);
|
||||
player.add();
|
||||
|
||||
state.set(State.playing);
|
||||
});
|
||||
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
Core.app.post(() -> Core.app.post(() -> {
|
||||
if(net.active() && player.getClosestCore() != null){
|
||||
//set to closest core since that's where the player will probably respawn; prevents camera jumps
|
||||
Core.camera.position.set(player.isDead() ? player.getClosestCore() : player);
|
||||
}else{
|
||||
//locally, set to player position since respawning occurs immediately
|
||||
Core.camera.position.set(player);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Events.on(ResetEvent.class, event -> {
|
||||
player.reset();
|
||||
tutorial.reset();
|
||||
|
||||
hiscore = false;
|
||||
|
||||
saves.resetSave();
|
||||
});
|
||||
|
||||
Events.on(WaveEvent.class, event -> {
|
||||
if(world.getMap().getHightScore() < state.wave){
|
||||
hiscore = true;
|
||||
world.getMap().setHighScore(state.wave);
|
||||
}
|
||||
|
||||
Sounds.wave.play();
|
||||
});
|
||||
|
||||
Events.on(GameOverEvent.class, event -> {
|
||||
state.stats.wavesLasted = state.wave;
|
||||
Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
|
||||
//the restart dialog can show info for any number of scenarios
|
||||
Call.onGameOver(event.winner);
|
||||
if(state.rules.zone != null && !net.client()){
|
||||
//remove zone save on game over
|
||||
if(saves.getZoneSlot() != null && !state.rules.tutorial){
|
||||
saves.getZoneSlot().delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//autohost for pvp maps
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
if(state.rules.pvp && !net.active()){
|
||||
try{
|
||||
net.host(port);
|
||||
player.isAdmin = true;
|
||||
}catch(IOException e){
|
||||
ui.showException("$server.error", e);
|
||||
Core.app.post(() -> state.set(State.menu));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(UnlockEvent.class, e -> ui.hudfrag.showUnlock(e.content));
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, e -> {
|
||||
if(e.team == player.getTeam()){
|
||||
if(e.breaking){
|
||||
state.stats.buildingsDeconstructed++;
|
||||
}else{
|
||||
state.stats.buildingsBuilt++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(BlockDestroyEvent.class, e -> {
|
||||
if(e.tile.getTeam() == player.getTeam()){
|
||||
state.stats.buildingsDestroyed++;
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(UnitDestroyEvent.class, e -> {
|
||||
if(e.unit.getTeam() != player.getTeam()){
|
||||
state.stats.enemyUnitsDestroyed++;
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(ZoneRequireCompleteEvent.class, e -> {
|
||||
if(e.objective.display() != null){
|
||||
ui.hudfrag.showToast(Core.bundle.format("zone.requirement.complete", e.zoneForMet.localizedName, e.objective.display()));
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(ZoneConfigureCompleteEvent.class, e -> {
|
||||
if(e.zone.configureObjective.display() != null){
|
||||
ui.hudfrag.showToast(Core.bundle.format("zone.config.unlocked", e.zone.configureObjective.display()));
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(Trigger.newGame, () -> {
|
||||
TileEntity core = player.getClosestCore();
|
||||
|
||||
if(core == null) return;
|
||||
|
||||
app.post(() -> ui.hudfrag.showLand());
|
||||
renderer.zoomIn(Fx.coreLand.lifetime);
|
||||
app.post(() -> Effects.effect(Fx.coreLand, core.x, core.y, 0, core.block));
|
||||
Time.run(Fx.coreLand.lifetime, () -> {
|
||||
Effects.effect(Fx.launch, core);
|
||||
Effects.shake(5f, 5f, core);
|
||||
});
|
||||
});
|
||||
|
||||
Events.on(UnitDestroyEvent.class, e -> {
|
||||
if(e.unit instanceof BaseUnit && world.isZone()){
|
||||
data.unlockContent(((BaseUnit)e.unit).getType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
Draw.scl = 1f / Core.atlas.find("scale_marker").getWidth();
|
||||
|
||||
Core.input.setCatch(KeyCode.BACK, true);
|
||||
|
||||
data.load();
|
||||
|
||||
Core.settings.defaults(
|
||||
"ip", "localhost",
|
||||
"color-0", Color.rgba8888(playerColors[8]),
|
||||
"name", "",
|
||||
"lastBuild", 0
|
||||
);
|
||||
|
||||
createPlayer();
|
||||
|
||||
saves.load();
|
||||
}
|
||||
|
||||
void createPlayer(){
|
||||
player = new Player();
|
||||
player.name = Core.settings.getString("name");
|
||||
player.color.set(Core.settings.getInt("color-0"));
|
||||
player.isLocal = true;
|
||||
player.isMobile = mobile;
|
||||
|
||||
if(mobile){
|
||||
input = new MobileInput();
|
||||
}else{
|
||||
input = new DesktopInput();
|
||||
}
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
player.add();
|
||||
}
|
||||
|
||||
Events.on(ClientLoadEvent.class, e -> input.add());
|
||||
}
|
||||
|
||||
public void setInput(InputHandler newInput){
|
||||
Block block = input.block;
|
||||
boolean added = Core.input.getInputProcessors().contains(input);
|
||||
input.remove();
|
||||
this.input = newInput;
|
||||
newInput.block = block;
|
||||
if(added){
|
||||
newInput.add();
|
||||
}
|
||||
}
|
||||
|
||||
public void playMap(Map map, Rules rules){
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
world.loadMap(map, rules);
|
||||
state.rules = rules;
|
||||
state.rules.zone = null;
|
||||
state.rules.editor = false;
|
||||
logic.play();
|
||||
if(settings.getBool("savecreate") && !world.isInvalidMap()){
|
||||
control.saves.addSave(map.name() + " " + new SimpleDateFormat("MMM dd h:mm", Locale.getDefault()).format(new Date()));
|
||||
}
|
||||
Events.fire(Trigger.newGame);
|
||||
});
|
||||
}
|
||||
|
||||
public void playZone(Zone zone){
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
net.reset();
|
||||
world.loadGenerator(zone.generator);
|
||||
zone.rules.get(state.rules);
|
||||
state.rules.zone = zone;
|
||||
for(Tile core : state.teams.get(defaultTeam).cores){
|
||||
for(ItemStack stack : zone.getStartingItems()){
|
||||
core.entity.items.add(stack.item, stack.amount);
|
||||
}
|
||||
}
|
||||
state.set(State.playing);
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
control.saves.zoneSave();
|
||||
logic.play();
|
||||
Events.fire(Trigger.newGame);
|
||||
});
|
||||
}
|
||||
|
||||
public void playTutorial(){
|
||||
Zone zone = Zones.groundZero;
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
net.reset();
|
||||
|
||||
world.beginMapLoad();
|
||||
|
||||
world.createTiles(zone.generator.width, zone.generator.height);
|
||||
zone.generator.generate(world.getTiles());
|
||||
|
||||
Tile coreb = null;
|
||||
|
||||
out:
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
if(world.rawTile(x, y).block() instanceof CoreBlock){
|
||||
coreb = world.rawTile(x, y);
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Geometry.circle(coreb.x, coreb.y, 10, (cx, cy) -> {
|
||||
Tile tile = world.ltile(cx, cy);
|
||||
if(tile != null && tile.getTeam() == defaultTeam && !(tile.block() instanceof CoreBlock)){
|
||||
world.removeBlock(tile);
|
||||
}
|
||||
});
|
||||
|
||||
Geometry.circle(coreb.x, coreb.y, 5, (cx, cy) -> world.tile(cx, cy).clearOverlay());
|
||||
|
||||
world.endMapLoad();
|
||||
|
||||
zone.rules.get(state.rules);
|
||||
state.rules.zone = zone;
|
||||
for(Tile core : state.teams.get(defaultTeam).cores){
|
||||
for(ItemStack stack : zone.getStartingItems()){
|
||||
core.entity.items.add(stack.item, stack.amount);
|
||||
}
|
||||
}
|
||||
Tile core = state.teams.get(defaultTeam).cores.first();
|
||||
core.entity.items.clear();
|
||||
|
||||
logic.play();
|
||||
state.rules.waveTimer = false;
|
||||
state.rules.waveSpacing = 60f * 30;
|
||||
state.rules.buildCostMultiplier = 0.3f;
|
||||
state.rules.tutorial = true;
|
||||
Events.fire(Trigger.newGame);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isHighScore(){
|
||||
return hiscore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
content.dispose();
|
||||
net.dispose();
|
||||
Musics.dispose();
|
||||
Sounds.dispose();
|
||||
ui.editor.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(){
|
||||
wasPaused = state.is(State.paused);
|
||||
if(state.is(State.playing)) state.set(State.paused);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume(){
|
||||
if(state.is(State.paused) && !wasPaused){
|
||||
state.set(State.playing);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
platform.updateRPC();
|
||||
|
||||
//play tutorial on stop
|
||||
if(!settings.getBool("playedtutorial", false)){
|
||||
Core.app.post(() -> Core.app.post(this::playTutorial));
|
||||
}
|
||||
|
||||
//display UI scale changed dialog
|
||||
if(Core.settings.getBool("uiscalechanged", false)){
|
||||
Core.app.post(() -> Core.app.post(() -> {
|
||||
FloatingDialog dialog = new FloatingDialog("$confirm");
|
||||
dialog.setFillParent(true);
|
||||
|
||||
float[] countdown = {60 * 11};
|
||||
Runnable exit = () -> {
|
||||
Core.settings.put("uiscale", 100);
|
||||
Core.settings.put("uiscalechanged", false);
|
||||
settings.save();
|
||||
dialog.hide();
|
||||
Core.app.exit();
|
||||
};
|
||||
|
||||
dialog.cont.label(() -> {
|
||||
if(countdown[0] <= 0){
|
||||
exit.run();
|
||||
}
|
||||
return Core.bundle.format("uiscale.reset", (int)((countdown[0] -= Time.delta()) / 60f));
|
||||
}).pad(10f).expand().center();
|
||||
|
||||
dialog.buttons.defaults().size(200f, 60f);
|
||||
dialog.buttons.addButton("$uiscale.cancel", exit);
|
||||
|
||||
dialog.buttons.addButton("$ok", () -> {
|
||||
Core.settings.put("uiscalechanged", false);
|
||||
settings.save();
|
||||
dialog.hide();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}));
|
||||
}
|
||||
|
||||
if(android){
|
||||
Sounds.empty.loop(0f, 1f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
//TODO find out why this happens on Android
|
||||
if(assets == null) return;
|
||||
|
||||
saves.update();
|
||||
|
||||
//update and load any requested assets
|
||||
try{
|
||||
assets.update();
|
||||
}catch(Exception ignored){
|
||||
}
|
||||
|
||||
input.updateState();
|
||||
|
||||
//autosave global data if it's modified
|
||||
data.checkSave();
|
||||
|
||||
music.update();
|
||||
loops.update();
|
||||
Time.updateGlobal();
|
||||
|
||||
if(Core.input.keyTap(Binding.fullscreen)){
|
||||
boolean full = settings.getBool("fullscreen");
|
||||
if(full){
|
||||
graphics.setWindowedMode(graphics.getWidth(), graphics.getHeight());
|
||||
}else{
|
||||
graphics.setFullscreenMode(graphics.getDisplayMode());
|
||||
}
|
||||
settings.put("fullscreen", !full);
|
||||
settings.save();
|
||||
}
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
input.update();
|
||||
|
||||
if(world.isZone()){
|
||||
for(Tile tile : state.teams.get(player.getTeam()).cores){
|
||||
for(Item item : content.items()){
|
||||
if(tile.entity != null && tile.entity.items.has(item)){
|
||||
data.unlockContent(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(state.rules.tutorial){
|
||||
tutorial.update();
|
||||
}
|
||||
|
||||
//auto-update rpc every 5 seconds
|
||||
if(timer.get(0, 60 * 5)){
|
||||
platform.updateRPC();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(Binding.pause) && !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(Core.input.keyTap(Binding.menu) && !ui.restart.isShown()){
|
||||
if(ui.chatfrag.shown()){
|
||||
ui.chatfrag.hide();
|
||||
}else if(!ui.paused.isShown() && !scene.hasDialog()){
|
||||
ui.paused.show();
|
||||
state.set(State.paused);
|
||||
}
|
||||
}
|
||||
|
||||
if(!mobile && Core.input.keyTap(Binding.screenshot) && !(scene.getKeyboardFocus() instanceof TextField) && !scene.hasKeyboard()){
|
||||
renderer.takeMapScreenshot();
|
||||
}
|
||||
|
||||
}else{
|
||||
if(!state.isPaused()){
|
||||
Time.update();
|
||||
}
|
||||
|
||||
if(!scene.hasDialog() && !scene.root.getChildren().isEmpty() && !(scene.root.getChildren().peek() instanceof Dialog) && Core.input.keyTap(KeyCode.BACK)){
|
||||
platform.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
core/src/mindustry/core/FileTree.java
Normal file
36
core/src/mindustry/core/FileTree.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.loaders.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
|
||||
/** Handles files in a modded context. */
|
||||
public class FileTree implements FileHandleResolver{
|
||||
private ObjectMap<String, Fi> files = new ObjectMap<>();
|
||||
|
||||
public void addFile(String path, Fi f){
|
||||
files.put(path, f);
|
||||
}
|
||||
|
||||
/** Gets an asset file.*/
|
||||
public Fi get(String path){
|
||||
if(files.containsKey(path)){
|
||||
return files.get(path);
|
||||
}else if(files.containsKey("/" + path)){
|
||||
return files.get("/" + path);
|
||||
}else{
|
||||
return Core.files.internal(path);
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all mod files.*/
|
||||
public void clear(){
|
||||
files.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fi resolve(String fileName){
|
||||
return get(fileName);
|
||||
}
|
||||
}
|
||||
61
core/src/mindustry/core/GameState.java
Normal file
61
core/src/mindustry/core/GameState.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.entities.type.base.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class GameState{
|
||||
/** Current wave number, can be anything in non-wave modes. */
|
||||
public int wave = 1;
|
||||
/** Wave countdown in ticks. */
|
||||
public float wavetime;
|
||||
/** Whether the game is in game over state. */
|
||||
public boolean gameOver = false, launched = false;
|
||||
/** The current game rules. */
|
||||
public Rules rules = new Rules();
|
||||
/** Statistics for this save/game. Displayed after game over. */
|
||||
public Stats stats = new Stats();
|
||||
/** 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;
|
||||
/** Current game state. */
|
||||
private State state = State.menu;
|
||||
|
||||
public int enemies(){
|
||||
return net.client() ? enemies : unitGroups[waveTeam.ordinal()].count(b -> !(b instanceof BaseDrone));
|
||||
}
|
||||
|
||||
public BaseUnit boss(){
|
||||
return unitGroups[waveTeam.ordinal()].find(BaseUnit::isBoss);
|
||||
}
|
||||
|
||||
public void set(State astate){
|
||||
Events.fire(new StateChangeEvent(state, astate));
|
||||
state = astate;
|
||||
}
|
||||
|
||||
public boolean isEditor(){
|
||||
return rules.editor;
|
||||
}
|
||||
|
||||
public boolean isPaused(){
|
||||
return (is(State.paused) && !net.active()) || (gameOver && !net.active());
|
||||
}
|
||||
|
||||
public boolean is(State astate){
|
||||
return state == astate;
|
||||
}
|
||||
|
||||
public State getState(){
|
||||
return state;
|
||||
}
|
||||
|
||||
public enum State{
|
||||
paused, playing, menu
|
||||
}
|
||||
}
|
||||
275
core/src/mindustry/core/Logic.java
Normal file
275
core/src/mindustry/core/Logic.java
Normal file
@@ -0,0 +1,275 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.Teams.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.BuildBlock.*;
|
||||
import mindustry.world.blocks.power.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/**
|
||||
* Logic module.
|
||||
* Handles all logic for entities and waves.
|
||||
* Handles game state events.
|
||||
* Does not store any game state itself.
|
||||
* <p>
|
||||
* This class should <i>not</i> call any outside methods to change state of modules, but instead fire events.
|
||||
*/
|
||||
public class Logic implements ApplicationListener{
|
||||
|
||||
public Logic(){
|
||||
Events.on(WaveEvent.class, event -> {
|
||||
for(Player p : playerGroup.all()){
|
||||
p.respawns = state.rules.respawns;
|
||||
}
|
||||
|
||||
if(world.isZone()){
|
||||
world.getZone().updateWave(state.wave);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(BlockDestroyEvent.class, event -> {
|
||||
//blocks that get broken are appended to the team's broken block queue
|
||||
Tile tile = event.tile;
|
||||
Block block = tile.block();
|
||||
//skip null entities or nukes, for obvious reasons
|
||||
if(tile.entity == null || tile.block() instanceof NuclearReactor) return;
|
||||
|
||||
if(block instanceof BuildBlock){
|
||||
|
||||
BuildEntity entity = tile.ent();
|
||||
|
||||
//update block to reflect the fact that something was being constructed
|
||||
if(entity.cblock != null && entity.cblock.synthetic()){
|
||||
block = entity.cblock;
|
||||
}else{
|
||||
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TeamData data = state.teams.get(tile.getTeam());
|
||||
|
||||
//remove existing blocks that have been placed here.
|
||||
//painful O(n) iteration + copy
|
||||
for(int i = 0; i < data.brokenBlocks.size; i++){
|
||||
BrokenBlock b = data.brokenBlocks.get(i);
|
||||
if(b.x == tile.x && b.y == tile.y){
|
||||
data.brokenBlocks.removeIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
data.brokenBlocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config()));
|
||||
});
|
||||
|
||||
Events.on(BlockBuildEndEvent.class, event -> {
|
||||
if(!event.breaking){
|
||||
TeamData data = state.teams.get(event.team);
|
||||
Iterator<BrokenBlock> it = data.brokenBlocks.iterator();
|
||||
while(it.hasNext()){
|
||||
BrokenBlock 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the event of content being used by either the player or some block. */
|
||||
public void handleContent(UnlockableContent content){
|
||||
if(!headless){
|
||||
data.unlockContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
public void play(){
|
||||
state.set(State.playing);
|
||||
state.wavetime = state.rules.waveSpacing * 2; //grace period of 2x wave time before game starts
|
||||
Events.fire(new PlayEvent());
|
||||
|
||||
//add starting items
|
||||
if(!world.isZone()){
|
||||
for(Team team : Team.all){
|
||||
if(!state.teams.get(team).cores.isEmpty()){
|
||||
TileEntity entity = state.teams.get(team).cores.first().entity;
|
||||
entity.items.clear();
|
||||
for(ItemStack stack : state.rules.loadout){
|
||||
entity.items.add(stack.item, stack.amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
state.wave = 1;
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
state.gameOver = state.launched = false;
|
||||
state.teams = new Teams();
|
||||
state.rules = new Rules();
|
||||
state.stats = new Stats();
|
||||
|
||||
entities.clear();
|
||||
Time.clear();
|
||||
TileEntity.sleepingEntities = 0;
|
||||
|
||||
Events.fire(new ResetEvent());
|
||||
}
|
||||
|
||||
public void runWave(){
|
||||
spawner.spawnEnemies();
|
||||
state.wave++;
|
||||
state.wavetime = world.isZone() && world.getZone().isLaunchWave(state.wave) ? state.rules.waveSpacing * state.rules.launchWaveMultiplier : state.rules.waveSpacing;
|
||||
|
||||
Events.fire(new WaveEvent());
|
||||
}
|
||||
|
||||
private void checkGameOver(){
|
||||
if(!state.rules.attackMode && state.teams.get(defaultTeam).cores.size == 0 && !state.gameOver){
|
||||
state.gameOver = true;
|
||||
Events.fire(new GameOverEvent(waveTeam));
|
||||
}else if(state.rules.attackMode){
|
||||
Team alive = null;
|
||||
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.get(team).cores.size > 0){
|
||||
if(alive != null){
|
||||
return;
|
||||
}
|
||||
alive = team;
|
||||
}
|
||||
}
|
||||
|
||||
if(alive != null && !state.gameOver){
|
||||
if(world.isZone() && alive == defaultTeam){
|
||||
//in attack maps, a victorious game over is equivalent to a launch
|
||||
Call.launchZone();
|
||||
}else{
|
||||
Events.fire(new GameOverEvent(alive));
|
||||
}
|
||||
state.gameOver = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both)
|
||||
public static void launchZone(){
|
||||
if(!headless){
|
||||
ui.hudfrag.showLaunch();
|
||||
}
|
||||
|
||||
for(Tile tile : state.teams.get(defaultTeam).cores){
|
||||
Effects.effect(Fx.launch, tile);
|
||||
}
|
||||
|
||||
if(world.getZone() != null){
|
||||
world.getZone().setLaunched();
|
||||
}
|
||||
|
||||
Time.runTask(30f, () -> {
|
||||
for(Tile tile : state.teams.get(defaultTeam).cores){
|
||||
for(Item item : content.items()){
|
||||
if(tile == null || tile.entity == null || tile.entity.items == null) continue;
|
||||
data.addItem(item, tile.entity.items.get(item));
|
||||
Events.fire(new LaunchItemEvent(item, tile.entity.items.get(item)));
|
||||
}
|
||||
world.removeBlock(tile);
|
||||
}
|
||||
state.launched = true;
|
||||
state.gameOver = true;
|
||||
Events.fire(new LaunchEvent());
|
||||
//manually fire game over event now
|
||||
Events.fire(new GameOverEvent(defaultTeam));
|
||||
});
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both)
|
||||
public static void onGameOver(Team winner){
|
||||
state.stats.wavesLasted = state.wave;
|
||||
ui.restart.show(winner);
|
||||
netClient.setQuiet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
|
||||
if(!state.isPaused()){
|
||||
Time.update();
|
||||
|
||||
if(state.rules.waves && state.rules.waveTimer && !state.gameOver){
|
||||
if(!state.rules.waitForWaveToEnd || unitGroups[waveTeam.ordinal()].size() == 0){
|
||||
state.wavetime = Math.max(state.wavetime - Time.delta(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(!net.client() && state.wavetime <= 0 && state.rules.waves){
|
||||
runWave();
|
||||
}
|
||||
|
||||
if(!headless){
|
||||
effectGroup.update();
|
||||
groundEffectGroup.update();
|
||||
}
|
||||
|
||||
if(!state.isEditor()){
|
||||
for(EntityGroup group : unitGroups){
|
||||
group.update();
|
||||
}
|
||||
|
||||
puddleGroup.update();
|
||||
shieldGroup.update();
|
||||
bulletGroup.update();
|
||||
tileGroup.update();
|
||||
fireGroup.update();
|
||||
}else{
|
||||
for(EntityGroup<?> group : unitGroups){
|
||||
group.updateEvents();
|
||||
collisions.updatePhysics(group);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
playerGroup.update();
|
||||
|
||||
//effect group only contains item transfers in the headless version, update it!
|
||||
if(headless){
|
||||
effectGroup.update();
|
||||
}
|
||||
|
||||
if(!state.isEditor()){
|
||||
|
||||
for(EntityGroup group : unitGroups){
|
||||
if(group.isEmpty()) continue;
|
||||
collisions.collideGroups(bulletGroup, group);
|
||||
}
|
||||
|
||||
collisions.collideGroups(bulletGroup, playerGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if(!net.client() && !world.isInvalidMap() && !state.isEditor()){
|
||||
checkGameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
517
core/src/mindustry/core/NetClient.java
Normal file
517
core/src/mindustry/core/NetClient.java
Normal file
@@ -0,0 +1,517 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.util.CommandHandler.*;
|
||||
import arc.util.*;
|
||||
import arc.util.io.*;
|
||||
import arc.util.serialization.*;
|
||||
import mindustry.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.traits.BuilderTrait.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.net.Administration.*;
|
||||
import mindustry.net.Net.*;
|
||||
import mindustry.net.*;
|
||||
import mindustry.net.Packets.*;
|
||||
import mindustry.type.TypeID;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.modules.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class NetClient implements ApplicationListener{
|
||||
private final static float dataTimeout = 60 * 18;
|
||||
private final static float playerSyncTime = 2;
|
||||
public final static float viewScale = 2f;
|
||||
|
||||
private long ping;
|
||||
private Interval timer = new Interval(5);
|
||||
/** Whether the client is currently connecting. */
|
||||
private boolean connecting = false;
|
||||
/** If true, no message will be shown on disconnect. */
|
||||
private boolean quiet = false;
|
||||
/** Whether to supress disconnect events completely.*/
|
||||
private boolean quietReset = false;
|
||||
/** Counter for data timeout. */
|
||||
private float timeoutTime = 0f;
|
||||
/** Last sent client snapshot ID. */
|
||||
private int lastSent;
|
||||
|
||||
/** List of entities that were removed, and need not be added while syncing. */
|
||||
private IntSet removed = new IntSet();
|
||||
/** Byte stream for reading in snapshots. */
|
||||
private ReusableByteInStream byteStream = new ReusableByteInStream();
|
||||
private DataInputStream dataStream = new DataInputStream(byteStream);
|
||||
|
||||
public NetClient(){
|
||||
|
||||
net.handleClient(Connect.class, packet -> {
|
||||
Log.info("Connecting to server: {0}", packet.addressTCP);
|
||||
|
||||
player.isAdmin = false;
|
||||
|
||||
reset();
|
||||
|
||||
ui.loadfrag.hide();
|
||||
ui.loadfrag.show("$connecting.data");
|
||||
|
||||
ui.loadfrag.setButton(() -> {
|
||||
ui.loadfrag.hide();
|
||||
connecting = false;
|
||||
quiet = true;
|
||||
net.disconnect();
|
||||
});
|
||||
|
||||
ConnectPacket c = new ConnectPacket();
|
||||
c.name = player.name;
|
||||
c.mods = mods.getModStrings();
|
||||
c.mobile = mobile;
|
||||
c.versionType = Version.type;
|
||||
c.color = Color.rgba8888(player.color);
|
||||
c.usid = getUsid(packet.addressTCP);
|
||||
c.uuid = platform.getUUID();
|
||||
|
||||
if(c.uuid == null){
|
||||
ui.showErrorMessage("$invalidid");
|
||||
ui.loadfrag.hide();
|
||||
disconnectQuietly();
|
||||
return;
|
||||
}
|
||||
|
||||
net.send(c, SendMode.tcp);
|
||||
});
|
||||
|
||||
net.handleClient(Disconnect.class, packet -> {
|
||||
if(quietReset) return;
|
||||
|
||||
connecting = false;
|
||||
state.set(State.menu);
|
||||
logic.reset();
|
||||
platform.updateRPC();
|
||||
|
||||
if(quiet) return;
|
||||
|
||||
Time.runTask(3f, ui.loadfrag::hide);
|
||||
|
||||
if(packet.reason != null){
|
||||
if(packet.reason.equals("closed")){
|
||||
ui.showSmall("$disconnect", "$disconnect.closed");
|
||||
}else if(packet.reason.equals("timeout")){
|
||||
ui.showSmall("$disconnect", "$disconnect.timeout");
|
||||
}else if(packet.reason.equals("error")){
|
||||
ui.showSmall("$disconnect", "$disconnect.error");
|
||||
}
|
||||
}else{
|
||||
ui.showErrorMessage("$disconnect");
|
||||
}
|
||||
});
|
||||
|
||||
net.handleClient(WorldStream.class, data -> {
|
||||
Log.info("Recieved world data: {0} bytes.", data.stream.available());
|
||||
NetworkIO.loadWorld(new InflaterInputStream(data.stream));
|
||||
|
||||
finishConnecting();
|
||||
});
|
||||
|
||||
net.handleClient(InvokePacket.class, packet -> {
|
||||
packet.writeBuffer.position(0);
|
||||
RemoteReadClient.readPacket(packet.writeBuffer, packet.type);
|
||||
});
|
||||
}
|
||||
|
||||
//called on all clients
|
||||
@Remote(called = Loc.server, targets = Loc.server, variants = Variant.both)
|
||||
public static void sendMessage(String message, String sender, Player playersender){
|
||||
if(Vars.ui != null && !(playersender != null && net.server() && sender.startsWith("[#" + player.getTeam().color.toString() + "]<T>"))){
|
||||
Vars.ui.chatfrag.addMessage(message, sender);
|
||||
}
|
||||
|
||||
if(playersender != null){
|
||||
playersender.lastText = message;
|
||||
playersender.textFadeTime = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
//equivalent to above method but there's no sender and no console log
|
||||
@Remote(called = Loc.server, targets = Loc.server)
|
||||
public static void sendMessage(String message){
|
||||
if(Vars.ui != null){
|
||||
Vars.ui.chatfrag.addMessage(message, null);
|
||||
}
|
||||
}
|
||||
|
||||
//called when a server recieves a chat message from a player
|
||||
@Remote(called = Loc.server, targets = Loc.client)
|
||||
public static void sendChatMessage(Player player, String message){
|
||||
if(message.length() > maxTextLength){
|
||||
throw new ValidateException(player, "Player has sent a message above the text limit.");
|
||||
}
|
||||
|
||||
//check if it's a command
|
||||
CommandResponse response = netServer.clientCommands.handleMessage(message, player);
|
||||
if(response.type == ResponseType.noCommand){ //no command to handle
|
||||
//server console logging
|
||||
Log.info("&y{0}: &lb{1}", player.name, message);
|
||||
|
||||
//invoke event for all clients but also locally
|
||||
//this is required so other clients get the correct name even if they don't know who's sending it yet
|
||||
Call.sendMessage(message, colorizeName(player.id, player.name), player);
|
||||
}else{
|
||||
//log command to console but with brackets
|
||||
Log.info("<&y{0}: &lm{1}&lg>", player.name, 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].";
|
||||
}
|
||||
|
||||
player.sendMessage(text);
|
||||
}
|
||||
}
|
||||
|
||||
Events.fire(new PlayerChatEvent(player, message));
|
||||
}
|
||||
|
||||
public static String colorizeName(int id, String name){
|
||||
Player player = playerGroup.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 onConnect(String ip, int port){
|
||||
netClient.disconnectQuietly();
|
||||
state.set(State.menu);
|
||||
logic.reset();
|
||||
|
||||
ui.join.connect(ip, port);
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void onPing(Player player, long time){
|
||||
Call.onPingResponse(player.con, time);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one)
|
||||
public static void onPingResponse(long time){
|
||||
netClient.ping = Time.timeSinceMillis(time);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one)
|
||||
public static void onTraceInfo(Player player, TraceInfo info){
|
||||
if(player != null){
|
||||
ui.traces.show(player, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.high)
|
||||
public static void onKick(KickReason reason){
|
||||
netClient.disconnectQuietly();
|
||||
state.set(State.menu);
|
||||
logic.reset();
|
||||
|
||||
if(!reason.quiet){
|
||||
if(reason.extraText() != null){
|
||||
ui.showText(reason.toString(), reason.extraText());
|
||||
}else{
|
||||
ui.showText("$disconnect", reason.toString());
|
||||
}
|
||||
}
|
||||
ui.loadfrag.hide();
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.high)
|
||||
public static void onKick(String reason){
|
||||
netClient.disconnectQuietly();
|
||||
state.set(State.menu);
|
||||
logic.reset();
|
||||
ui.showText("$disconnect", reason, Align.left);
|
||||
ui.loadfrag.hide();
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void onInfoMessage(String message){
|
||||
ui.showText("", message);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void onSetRules(Rules rules){
|
||||
state.rules = rules;
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both)
|
||||
public static void onWorldDataBegin(){
|
||||
entities.clear();
|
||||
netClient.removed.clear();
|
||||
logic.reset();
|
||||
|
||||
ui.chatfrag.clearMessages();
|
||||
net.setClientLoaded(false);
|
||||
|
||||
ui.loadfrag.show("$connecting.data");
|
||||
|
||||
ui.loadfrag.setButton(() -> {
|
||||
ui.loadfrag.hide();
|
||||
netClient.connecting = false;
|
||||
netClient.quiet = true;
|
||||
net.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one)
|
||||
public static void onPositionSet(float x, float y){
|
||||
player.x = x;
|
||||
player.y = y;
|
||||
}
|
||||
|
||||
@Remote
|
||||
public static void onPlayerDisconnect(int playerid){
|
||||
playerGroup.removeByID(playerid);
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void onEntitySnapshot(byte groupID, short amount, short dataLen, byte[] data){
|
||||
try{
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen));
|
||||
DataInputStream input = netClient.dataStream;
|
||||
|
||||
EntityGroup group = entities.get(groupID);
|
||||
|
||||
//go through each entity
|
||||
for(int j = 0; j < amount; j++){
|
||||
int id = input.readInt();
|
||||
byte typeID = input.readByte();
|
||||
|
||||
SyncTrait entity = group == null ? null : (SyncTrait)group.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 = (SyncTrait)content.<TypeID>getByID(ContentType.typeid, typeID).constructor.get();
|
||||
entity.resetID(id);
|
||||
if(!netClient.isEntityUsed(entity.getID())){
|
||||
add = true;
|
||||
}
|
||||
created = true;
|
||||
}
|
||||
|
||||
//read the entity
|
||||
entity.read(input);
|
||||
|
||||
if(created && entity.getInterpolator() != null && entity.getInterpolator().target != null){
|
||||
//set initial starting position
|
||||
entity.setNet(entity.getInterpolator().target.x, entity.getInterpolator().target.y);
|
||||
if(entity instanceof Unit && entity.getInterpolator().targets.length > 0){
|
||||
((Unit)entity).rotation = entity.getInterpolator().targets[0];
|
||||
}
|
||||
}
|
||||
|
||||
if(add){
|
||||
entity.add();
|
||||
netClient.addRemovedEntity(entity.getID());
|
||||
}
|
||||
}
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.both, priority = PacketPriority.low, unreliable = true)
|
||||
public static void onBlockSnapshot(short amount, short dataLen, byte[] data){
|
||||
try{
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen));
|
||||
DataInputStream input = netClient.dataStream;
|
||||
|
||||
for(int i = 0; i < amount; i++){
|
||||
int pos = input.readInt();
|
||||
Tile tile = world.tile(pos);
|
||||
if(tile == null || tile.entity == null){
|
||||
Log.warn("Missing entity at {0}. Skipping block snapshot.", tile);
|
||||
break;
|
||||
}
|
||||
tile.entity.read(input, tile.entity.version());
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void onStateSnapshot(float waveTime, int wave, int enemies, short coreDataLen, byte[] coreData){
|
||||
try{
|
||||
if(wave > state.wave){
|
||||
state.wave = wave;
|
||||
Events.fire(new WaveEvent());
|
||||
}
|
||||
|
||||
state.wavetime = waveTime;
|
||||
state.wave = wave;
|
||||
state.enemies = enemies;
|
||||
|
||||
netClient.byteStream.setBytes(net.decompressSnapshot(coreData, coreDataLen));
|
||||
DataInputStream input = netClient.dataStream;
|
||||
|
||||
byte cores = input.readByte();
|
||||
for(int i = 0; i < cores; i++){
|
||||
int pos = input.readInt();
|
||||
Tile tile = world.tile(pos);
|
||||
|
||||
if(tile != null && tile.entity != null){
|
||||
tile.entity.items.read(input);
|
||||
}else{
|
||||
new ItemModule().read(input);
|
||||
}
|
||||
}
|
||||
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(!net.client()) return;
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
if(!connecting) sync();
|
||||
}else if(!connecting){
|
||||
net.disconnect();
|
||||
}else{ //...must be connecting
|
||||
timeoutTime += Time.delta();
|
||||
if(timeoutTime > dataTimeout){
|
||||
Log.err("Failed to load data!");
|
||||
ui.loadfrag.hide();
|
||||
quiet = true;
|
||||
ui.showErrorMessage("$disconnect.data");
|
||||
net.disconnect();
|
||||
timeoutTime = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnecting(){
|
||||
return connecting;
|
||||
}
|
||||
|
||||
public int getPing(){
|
||||
return (int)ping;
|
||||
}
|
||||
|
||||
private void finishConnecting(){
|
||||
state.set(State.playing);
|
||||
connecting = false;
|
||||
ui.join.hide();
|
||||
net.setClientLoaded(true);
|
||||
Core.app.post(Call::connectConfirm);
|
||||
Time.runTask(40f, platform::updateRPC);
|
||||
Core.app.post(() -> ui.loadfrag.hide());
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
net.setClientLoaded(false);
|
||||
removed.clear();
|
||||
timeoutTime = 0f;
|
||||
connecting = true;
|
||||
quietReset = false;
|
||||
quiet = false;
|
||||
lastSent = 0;
|
||||
|
||||
entities.clear();
|
||||
ui.chatfrag.clearMessages();
|
||||
}
|
||||
|
||||
public void beginConnecting(){
|
||||
connecting = true;
|
||||
}
|
||||
|
||||
/** Disconnects, resetting state to the menu. */
|
||||
public void disconnectQuietly(){
|
||||
quiet = true;
|
||||
net.disconnect();
|
||||
}
|
||||
|
||||
/** Disconnects, causing no further changes or reset.*/
|
||||
public void disconnectNoReset(){
|
||||
quiet = quietReset = true;
|
||||
net.disconnect();
|
||||
}
|
||||
|
||||
/** When set, any disconnects will be ignored and no dialogs will be shown. */
|
||||
public void setQuiet(){
|
||||
quiet = true;
|
||||
}
|
||||
|
||||
public void addRemovedEntity(int id){
|
||||
removed.add(id);
|
||||
}
|
||||
|
||||
public boolean isEntityUsed(int id){
|
||||
return removed.contains(id);
|
||||
}
|
||||
|
||||
void sync(){
|
||||
|
||||
if(timer.get(0, playerSyncTime)){
|
||||
BuildRequest[] requests;
|
||||
//limit to 10 to prevent buffer overflows
|
||||
int usedRequests = Math.min(player.buildQueue().size, 10);
|
||||
|
||||
requests = new BuildRequest[usedRequests];
|
||||
for(int i = 0; i < usedRequests; i++){
|
||||
requests[i] = player.buildQueue().get(i);
|
||||
}
|
||||
|
||||
Call.onClientShapshot(lastSent++, player.x, player.y,
|
||||
player.pointerX, player.pointerY, player.rotation, player.baseRotation,
|
||||
player.velocity().x, player.velocity().y,
|
||||
player.getMineTile(),
|
||||
player.isBoosting, player.isShooting, ui.chatfrag.shown(), player.isBuilding,
|
||||
requests,
|
||||
Core.camera.position.x, Core.camera.position.y,
|
||||
Core.camera.width * viewScale, Core.camera.height * viewScale);
|
||||
}
|
||||
|
||||
if(timer.get(1, 60)){
|
||||
Call.onPing(Time.millis());
|
||||
}
|
||||
}
|
||||
|
||||
String getUsid(String ip){
|
||||
if(Core.settings.getString("usid-" + ip, null) != null){
|
||||
return Core.settings.getString("usid-" + ip, null);
|
||||
}else{
|
||||
byte[] bytes = new byte[8];
|
||||
new RandomXS128().nextBytes(bytes);
|
||||
String result = new String(Base64Coder.encode(bytes));
|
||||
Core.settings.put("usid-" + ip, result);
|
||||
Core.settings.save();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
787
core/src/mindustry/core/NetServer.java
Normal file
787
core/src/mindustry/core/NetServer.java
Normal file
@@ -0,0 +1,787 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.CommandHandler.*;
|
||||
import arc.util.io.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.traits.BuilderTrait.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.net.*;
|
||||
import mindustry.net.Administration.*;
|
||||
import mindustry.net.Packets.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class NetServer implements ApplicationListener{
|
||||
private final static int maxSnapshotSize = 430, timerBlockSync = 0;
|
||||
private final static float serverSyncTime = 12, kickDuration = 30 * 1000, blockSyncTime = 60 * 10;
|
||||
private final static Vector2 vector = new Vector2();
|
||||
private final static Rectangle viewport = new Rectangle();
|
||||
/** If a player goes away of their server-side coordinates by this distance, they get teleported back. */
|
||||
private final static float correctDist = 16f;
|
||||
|
||||
public final Administration admins = new Administration();
|
||||
public final CommandHandler clientCommands = new CommandHandler("/");
|
||||
|
||||
private boolean closing = false;
|
||||
private Interval timer = new Interval();
|
||||
|
||||
private ByteBuffer writeBuffer = ByteBuffer.allocate(127);
|
||||
private ByteBufferOutput outputBuffer = new ByteBufferOutput(writeBuffer);
|
||||
|
||||
/** Stream for writing player sync data to. */
|
||||
private ReusableByteOutStream syncStream = new ReusableByteOutStream();
|
||||
/** Data stream for writing player sync data to. */
|
||||
private DataOutputStream dataStream = new DataOutputStream(syncStream);
|
||||
|
||||
public NetServer(){
|
||||
|
||||
net.handleServer(Connect.class, (con, connect) -> {
|
||||
if(admins.isIPBanned(connect.addressTCP)){
|
||||
con.kick(KickReason.banned);
|
||||
}
|
||||
});
|
||||
|
||||
net.handleServer(Disconnect.class, (con, packet) -> {
|
||||
if(con.player != null){
|
||||
onDisconnect(con.player, packet.reason);
|
||||
}
|
||||
});
|
||||
|
||||
net.handleServer(ConnectPacket.class, (con, packet) -> {
|
||||
if(con.address.startsWith("steam:")){
|
||||
packet.uuid = con.address.substring("steam:".length());
|
||||
}
|
||||
|
||||
String uuid = packet.uuid;
|
||||
|
||||
if(admins.isIPBanned(con.address)) return;
|
||||
|
||||
if(con.hasBegunConnecting){
|
||||
con.kick(KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerInfo info = admins.getInfo(uuid);
|
||||
|
||||
con.hasBegunConnecting = true;
|
||||
con.mobile = packet.mobile;
|
||||
|
||||
if(packet.uuid == null || packet.usid == null){
|
||||
con.kick(KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
if(admins.isIDBanned(uuid)){
|
||||
con.kick(KickReason.banned);
|
||||
return;
|
||||
}
|
||||
|
||||
if(Time.millis() - info.lastKicked < kickDuration){
|
||||
con.kick(KickReason.recentKick);
|
||||
return;
|
||||
}
|
||||
|
||||
if(admins.getPlayerLimit() > 0 && playerGroup.size() >= admins.getPlayerLimit()){
|
||||
con.kick(KickReason.playerLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
Array<String> extraMods = packet.mods.copy();
|
||||
Array<String> missingMods = mods.getIncompatibility(extraMods);
|
||||
|
||||
if(!extraMods.isEmpty() || !missingMods.isEmpty()){
|
||||
//can't easily be localized since kick reasons can't have formatted text with them
|
||||
StringBuilder result = new StringBuilder("[accent]Incompatible mods![]\n\n");
|
||||
if(!missingMods.isEmpty()){
|
||||
result.append("Missing:[lightgray]\n").append("> ").append(missingMods.toString("\n> "));
|
||||
result.append("[]\n");
|
||||
}
|
||||
|
||||
if(!extraMods.isEmpty()){
|
||||
result.append("Unnecessary mods:[lightgray]\n").append("> ").append(extraMods.toString("\n> "));
|
||||
}
|
||||
con.kick(result.toString());
|
||||
}
|
||||
|
||||
if(!admins.isWhitelisted(packet.uuid, packet.usid)){
|
||||
info.adminUsid = packet.usid;
|
||||
info.lastName = packet.name;
|
||||
info.id = packet.uuid;
|
||||
admins.save();
|
||||
Call.onInfoMessage(con, "You are not whitelisted here.");
|
||||
Log.info("&lcDo &lywhitelist-add {0}&lc to whitelist the player &lb'{1}'", packet.uuid, packet.name);
|
||||
con.kick(KickReason.whitelist);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet.versionType == null || ((packet.version == -1 || !packet.versionType.equals(Version.type)) && Version.build != -1 && !admins.allowsCustomClients())){
|
||||
con.kick(!Version.type.equals(packet.versionType) ? KickReason.typeMismatch : KickReason.customClient);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean preventDuplicates = headless && netServer.admins.getStrict();
|
||||
|
||||
if(preventDuplicates){
|
||||
for(Player player : playerGroup.all()){
|
||||
if(player.name.trim().equalsIgnoreCase(packet.name.trim())){
|
||||
con.kick(KickReason.nameInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.uuid != null && player.usid != null && (player.uuid.equals(packet.uuid) || player.usid.equals(packet.usid))){
|
||||
con.kick(KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packet.name = fixName(packet.name);
|
||||
|
||||
if(packet.name.trim().length() <= 0){
|
||||
con.kick(KickReason.nameEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
String ip = con.address;
|
||||
|
||||
admins.updatePlayerJoined(uuid, ip, packet.name);
|
||||
|
||||
if(packet.version != Version.build && Version.build != -1 && packet.version != -1){
|
||||
con.kick(packet.version > Version.build ? KickReason.serverOutdated : KickReason.clientOutdated);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet.version == -1){
|
||||
con.modclient = true;
|
||||
}
|
||||
|
||||
Player player = new Player();
|
||||
player.isAdmin = admins.isAdmin(uuid, packet.usid);
|
||||
player.con = con;
|
||||
player.usid = packet.usid;
|
||||
player.name = packet.name;
|
||||
player.uuid = uuid;
|
||||
player.isMobile = packet.mobile;
|
||||
player.dead = true;
|
||||
player.setNet(player.x, player.y);
|
||||
player.color.set(packet.color);
|
||||
player.color.a = 1f;
|
||||
|
||||
try{
|
||||
writeBuffer.position(0);
|
||||
player.write(outputBuffer);
|
||||
}catch(Throwable t){
|
||||
t.printStackTrace();
|
||||
con.kick(KickReason.nameEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
con.player = player;
|
||||
|
||||
//playing in pvp mode automatically assigns players to teams
|
||||
if(state.rules.pvp){
|
||||
player.setTeam(assignTeam(player, playerGroup.all()));
|
||||
Log.info("Auto-assigned player {0} to team {1}.", player.name, player.getTeam());
|
||||
}
|
||||
|
||||
sendWorldData(player);
|
||||
|
||||
platform.updateRPC();
|
||||
|
||||
Events.fire(new PlayerConnect(player));
|
||||
});
|
||||
|
||||
net.handleServer(InvokePacket.class, (con, packet) -> {
|
||||
if(con.player == null) return;
|
||||
RemoteReadServer.readPacket(packet.writeBuffer, packet.type, con.player);
|
||||
});
|
||||
|
||||
registerCommands();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
mods.eachClass(mod -> mod.registerClientCommands(clientCommands));
|
||||
}
|
||||
|
||||
private void registerCommands(){
|
||||
clientCommands.<Player>register("help", "[page]", "Lists all commands.", (args, player) -> {
|
||||
if(args.length > 0 && !Strings.canParseInt(args[0])){
|
||||
player.sendMessage("[scarlet]'page' must be a number.");
|
||||
return;
|
||||
}
|
||||
int commandsPerPage = 6;
|
||||
int page = args.length > 0 ? Strings.parseInt(args[0]) : 1;
|
||||
int pages = Mathf.ceil((float)clientCommands.getCommandList().size / commandsPerPage);
|
||||
|
||||
page --;
|
||||
|
||||
if(page > pages || page < 0){
|
||||
player.sendMessage("[scarlet]'page' must be a number between[orange] 1[] and[orange] " + pages + "[scarlet].");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(Strings.format("[orange]-- Commands Page[lightgray] {0}[gray]/[lightgray]{1}[orange] --\n\n", (page+1), pages));
|
||||
|
||||
for(int i = commandsPerPage * page; i < Math.min(commandsPerPage * (page + 1), clientCommands.getCommandList().size); i++){
|
||||
Command command = clientCommands.getCommandList().get(i);
|
||||
result.append("[orange] /").append(command.text).append("[white] ").append(command.paramText).append("[lightgray] - ").append(command.description).append("\n");
|
||||
}
|
||||
player.sendMessage(result.toString());
|
||||
});
|
||||
|
||||
clientCommands.<Player>register("t", "<message...>", "Send a message only to your teammates.", (args, player) -> {
|
||||
playerGroup.all().each(p -> p.getTeam() == player.getTeam(), o -> o.sendMessage(args[0], player, "[#" + player.getTeam().color.toString() + "]<T>" + NetClient.colorizeName(player.id, player.name)));
|
||||
});
|
||||
|
||||
//duration of a a kick in seconds
|
||||
int kickDuration = 15 * 60;
|
||||
|
||||
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] {0}[lightgray].", target.name));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
}
|
||||
}, 60 * 1);
|
||||
}
|
||||
|
||||
void vote(Player player, int d){
|
||||
votes += d;
|
||||
voted.addAll(player.uuid, admins.getInfo(player.uuid).lastIP);
|
||||
|
||||
Call.sendMessage(Strings.format("[orange]{0}[lightgray] has voted to kick[orange] {1}[].[accent] ({2}/{3})\n[lightgray]Type[orange] /vote <y/n>[] to agree.",
|
||||
player.name, target.name, votes, votesRequired()));
|
||||
}
|
||||
|
||||
boolean checkPass(){
|
||||
if(votes >= votesRequired()){
|
||||
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] {0}[orange] will be banned from the server for {1} minutes.", target.name, (kickDuration/60)));
|
||||
target.getInfo().lastKicked = Time.millis() + kickDuration*1000;
|
||||
playerGroup.all().each(p -> p.uuid != null && p.uuid.equals(target.uuid), p -> p.con.kick(KickReason.vote));
|
||||
map[0] = null;
|
||||
task.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//cooldown between votes
|
||||
int voteTime = 60 * 3;
|
||||
Timekeeper vtime = new Timekeeper(voteTime);
|
||||
//current kick sessions
|
||||
VoteSession[] currentlyKicking = {null};
|
||||
|
||||
clientCommands.<Player>register("votekick", "[player...]", "Vote to kick a player, with a cooldown.", (args, player) -> {
|
||||
if(playerGroup.size() < 3){
|
||||
player.sendMessage("[scarlet]At least 3 players are needed to start a votekick.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.isLocal){
|
||||
player.sendMessage("[scarlet]Just kick them yourself if you're the host.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(args.length == 0){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[orange]Players to kick: \n");
|
||||
for(Player p : playerGroup.all()){
|
||||
if(p.isAdmin || p.con == null || p == player) continue;
|
||||
|
||||
builder.append("[lightgray] ").append(p.name).append("[accent] (#").append(p.id).append(")\n");
|
||||
}
|
||||
player.sendMessage(builder.toString());
|
||||
}else{
|
||||
Player found;
|
||||
if(args[0].length() > 1 && args[0].startsWith("#") && Strings.canParseInt(args[0].substring(1))){
|
||||
int id = Strings.parseInt(args[0].substring(1));
|
||||
found = playerGroup.find(p -> p.id == id);
|
||||
}else{
|
||||
found = playerGroup.find(p -> p.name.equalsIgnoreCase(args[0]));
|
||||
}
|
||||
|
||||
if(found != null){
|
||||
if(found.isAdmin){
|
||||
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.");
|
||||
}else if(found.getTeam() != player.getTeam()){
|
||||
player.sendMessage("[scarlet]Only players on your team can be kicked.");
|
||||
}else{
|
||||
if(!vtime.get()){
|
||||
player.sendMessage("[scarlet]You must wait " + voteTime/60 + " minutes between votekicks.");
|
||||
return;
|
||||
}
|
||||
|
||||
VoteSession session = new VoteSession(currentlyKicking, found);
|
||||
session.vote(player, 1);
|
||||
vtime.reset();
|
||||
currentlyKicking[0] = session;
|
||||
}
|
||||
}else{
|
||||
player.sendMessage("[scarlet]No player[orange]'" + args[0] + "'[scarlet] found.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientCommands.<Player>register("vote", "<y/n>", "Vote to kick the current player.", (arg, player) -> {
|
||||
if(currentlyKicking[0] == 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.");
|
||||
return;
|
||||
}
|
||||
|
||||
//hosts can vote all they want
|
||||
if(player.uuid != null && (currentlyKicking[0].voted.contains(player.uuid) || currentlyKicking[0].voted.contains(admins.getInfo(player.uuid).lastIP))){
|
||||
player.sendMessage("[scarlet]You've already voted. Sit down.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentlyKicking[0].target == player){
|
||||
player.sendMessage("[scarlet]You can't vote on your own trial.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!arg[0].toLowerCase().equals("y") && !arg[0].toLowerCase().equals("n")){
|
||||
player.sendMessage("[scarlet]Vote either 'y' (yes) or 'n' (no).");
|
||||
return;
|
||||
}
|
||||
|
||||
int sign = arg[0].toLowerCase().equals("y") ? 1 : -1;
|
||||
currentlyKicking[0].vote(player, sign);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
clientCommands.<Player>register("sync", "Re-synchronize world state.", (args, player) -> {
|
||||
if(player.isLocal){
|
||||
player.sendMessage("[scarlet]Re-synchronizing as the host is pointless.");
|
||||
}else{
|
||||
Call.onWorldDataBegin(player.con);
|
||||
netServer.sendWorldData(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int votesRequired(){
|
||||
return 2 + (playerGroup.size() > 4 ? 1 : 0);
|
||||
}
|
||||
|
||||
public Team assignTeam(Player current, Iterable<Player> players){
|
||||
//find team with minimum amount of players and auto-assign player to that.
|
||||
return Structs.findMin(Team.all, team -> {
|
||||
if(state.teams.isActive(team) && !state.teams.get(team).cores.isEmpty()){
|
||||
int count = 0;
|
||||
for(Player other : players){
|
||||
if(other.getTeam() == team && other != current){
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
});
|
||||
}
|
||||
|
||||
public void sendWorldData(Player player){
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream def = new FastDeflaterOutputStream(stream);
|
||||
NetworkIO.writeWorld(player, def);
|
||||
WorldStream data = new WorldStream();
|
||||
data.stream = new ByteArrayInputStream(stream.toByteArray());
|
||||
player.con.sendStream(data);
|
||||
|
||||
Log.debug("Packed {0} compressed bytes of world data.", stream.size());
|
||||
}
|
||||
|
||||
public static void onDisconnect(Player player, String reason){
|
||||
//singleplayer multiplayer wierdness
|
||||
if(player.con == null){
|
||||
player.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!player.con.hasDisconnected){
|
||||
if(player.con.hasConnected){
|
||||
Events.fire(new PlayerLeave(player));
|
||||
Call.sendMessage("[accent]" + player.name + "[accent] has disconnected.");
|
||||
Call.onPlayerDisconnect(player.id);
|
||||
}
|
||||
|
||||
Log.info("&lm[{1}] &lc{0} has disconnected. &lg&fi({2})", player.name, player.uuid, reason);
|
||||
}
|
||||
|
||||
player.remove();
|
||||
player.con.hasDisconnected = true;
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, unreliable = true)
|
||||
public static void onClientShapshot(
|
||||
Player player,
|
||||
int snapshotID,
|
||||
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,
|
||||
BuildRequest[] requests,
|
||||
float viewX, float viewY, float viewWidth, float viewHeight
|
||||
){
|
||||
NetConnection connection = player.con;
|
||||
if(connection == null || snapshotID < connection.lastRecievedClientSnapshot) return;
|
||||
|
||||
boolean verifyPosition = !player.isDead() && netServer.admins.getStrict() && headless;
|
||||
|
||||
if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = Time.millis() - 16;
|
||||
|
||||
connection.viewX = viewX;
|
||||
connection.viewY = viewY;
|
||||
connection.viewWidth = viewWidth;
|
||||
connection.viewHeight = viewHeight;
|
||||
|
||||
long elapsed = Time.timeSinceMillis(connection.lastRecievedClientTime);
|
||||
|
||||
float maxSpeed = boosting && !player.mech.flying ? player.mech.compoundSpeedBoost : player.mech.compoundSpeed;
|
||||
float maxMove = elapsed / 1000f * 60f * Math.min(maxSpeed, player.mech.maxSpeed) * 1.2f;
|
||||
|
||||
player.pointerX = pointerX;
|
||||
player.pointerY = pointerY;
|
||||
player.setMineTile(mining);
|
||||
player.isTyping = chatting;
|
||||
player.isBoosting = boosting;
|
||||
player.isShooting = shooting;
|
||||
player.isBuilding = building;
|
||||
player.buildQueue().clear();
|
||||
for(BuildRequest req : requests){
|
||||
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.rotation() == req.rotation)){
|
||||
continue;
|
||||
}
|
||||
player.buildQueue().addLast(req);
|
||||
}
|
||||
|
||||
vector.set(x - player.getInterpolator().target.x, y - player.getInterpolator().target.y);
|
||||
vector.limit(maxMove);
|
||||
|
||||
float prevx = player.x, prevy = player.y;
|
||||
player.set(player.getInterpolator().target.x, player.getInterpolator().target.y);
|
||||
if(!player.mech.flying && player.boostHeat < 0.01f){
|
||||
player.move(vector.x, vector.y);
|
||||
}else{
|
||||
player.x += vector.x;
|
||||
player.y += vector.y;
|
||||
}
|
||||
float newx = player.x, newy = player.y;
|
||||
|
||||
if(!verifyPosition){
|
||||
player.x = prevx;
|
||||
player.y = prevy;
|
||||
newx = x;
|
||||
newy = y;
|
||||
}else if(Mathf.dst(x, y, newx, newy) > correctDist){
|
||||
Call.onPositionSet(player.con, newx, newy); //teleport and correct position when necessary
|
||||
}
|
||||
|
||||
//reset player to previous synced position so it gets interpolated
|
||||
player.x = prevx;
|
||||
player.y = prevy;
|
||||
|
||||
//set interpolator target to *new* position so it moves toward it
|
||||
player.getInterpolator().read(player.x, player.y, newx, newy, rotation, baseRotation);
|
||||
player.velocity().set(xVelocity, yVelocity); //only for visual calculation purposes, doesn't actually update the player
|
||||
|
||||
connection.lastRecievedClientSnapshot = snapshotID;
|
||||
connection.lastRecievedClientTime = Time.millis();
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client, called = Loc.server)
|
||||
public static void onAdminRequest(Player player, Player other, AdminAction action){
|
||||
|
||||
if(!player.isAdmin){
|
||||
Log.warn("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
|
||||
player.name, player.con.address);
|
||||
return;
|
||||
}
|
||||
|
||||
if(other == null || ((other.isAdmin && !player.isLocal) && other != player)){
|
||||
Log.warn("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
|
||||
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
|
||||
state.wavetime = 0f;
|
||||
}else if(action == AdminAction.ban){
|
||||
netServer.admins.banPlayerIP(other.con.address);
|
||||
other.con.kick(KickReason.banned);
|
||||
Log.info("&lc{0} has banned {1}.", player.name, other.name);
|
||||
}else if(action == AdminAction.kick){
|
||||
other.con.kick(KickReason.kick);
|
||||
Log.info("&lc{0} has kicked {1}.", 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.onTraceInfo(player.con, other, info);
|
||||
}else{
|
||||
NetClient.onTraceInfo(other, info);
|
||||
}
|
||||
Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(targets = Loc.client)
|
||||
public static void connectConfirm(Player player){
|
||||
if(player.con == null || player.con.hasConnected) return;
|
||||
|
||||
player.add();
|
||||
player.con.hasConnected = true;
|
||||
Call.sendMessage("[accent]" + player.name + "[accent] has connected.");
|
||||
Log.info("&lm[{1}] &y{0} has connected. ", player.name, player.uuid);
|
||||
|
||||
Events.fire(new PlayerJoin(player));
|
||||
}
|
||||
|
||||
public boolean isWaitingForPlayers(){
|
||||
if(state.rules.pvp){
|
||||
int used = 0;
|
||||
for(Team t : Team.all){
|
||||
if(playerGroup.count(p -> p.getTeam() == t) > 0){
|
||||
used++;
|
||||
}
|
||||
}
|
||||
return used < 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void update(){
|
||||
|
||||
if(!headless && !closing && net.server() && state.is(State.menu)){
|
||||
closing = true;
|
||||
ui.loadfrag.show("$server.closing");
|
||||
Time.runTask(5f, () -> {
|
||||
net.closeServer();
|
||||
ui.loadfrag.hide();
|
||||
closing = false;
|
||||
});
|
||||
}
|
||||
|
||||
if(!state.is(State.menu) && net.server()){
|
||||
sync();
|
||||
}
|
||||
}
|
||||
|
||||
public void kickAll(KickReason reason){
|
||||
for(NetConnection con : net.getConnections()){
|
||||
con.kick(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sends a block snapshot to all players. */
|
||||
public void writeBlockSnapshots() throws IOException{
|
||||
syncStream.reset();
|
||||
|
||||
short sent = 0;
|
||||
for(TileEntity entity : tileGroup.all()){
|
||||
if(!entity.block.sync) continue;
|
||||
sent ++;
|
||||
|
||||
dataStream.writeInt(entity.tile.pos());
|
||||
entity.write(dataStream);
|
||||
|
||||
if(syncStream.size() > maxSnapshotSize){
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
Call.onBlockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
sent = 0;
|
||||
syncStream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if(sent > 0){
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
Call.onBlockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
}
|
||||
}
|
||||
|
||||
public void writeEntitySnapshot(Player player) throws IOException{
|
||||
syncStream.reset();
|
||||
ObjectSet<Tile> cores = state.teams.get(player.getTeam()).cores;
|
||||
|
||||
dataStream.writeByte(cores.size);
|
||||
|
||||
for(Tile tile : cores){
|
||||
dataStream.writeInt(tile.pos());
|
||||
tile.entity.items.write(dataStream);
|
||||
}
|
||||
|
||||
dataStream.close();
|
||||
byte[] stateBytes = syncStream.toByteArray();
|
||||
|
||||
//write basic state data.
|
||||
Call.onStateSnapshot(player.con, state.wavetime, state.wave, state.enemies(), (short)stateBytes.length, net.compressSnapshot(stateBytes));
|
||||
|
||||
viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY);
|
||||
|
||||
//check for syncable groups
|
||||
for(EntityGroup<?> group : entities.all()){
|
||||
if(group.isEmpty() || !(group.all().get(0) instanceof SyncTrait)) continue;
|
||||
|
||||
//make sure mapping is enabled for this group
|
||||
if(!group.mappingEnabled()){
|
||||
throw new RuntimeException("Entity group '" + group.getType() + "' contains SyncTrait entities, yet mapping is not enabled. In order for syncing to work, you must enable mapping for this group.");
|
||||
}
|
||||
|
||||
syncStream.reset();
|
||||
|
||||
int sent = 0;
|
||||
|
||||
for(Entity entity : group.all()){
|
||||
SyncTrait sync = (SyncTrait)entity;
|
||||
if(!sync.isSyncing()) continue;
|
||||
|
||||
//write all entities now
|
||||
dataStream.writeInt(entity.getID()); //write id
|
||||
dataStream.writeByte(sync.getTypeID().id); //write type ID
|
||||
sync.write(dataStream); //write entity
|
||||
|
||||
sent++;
|
||||
|
||||
if(syncStream.size() > maxSnapshotSize){
|
||||
dataStream.close();
|
||||
byte[] syncBytes = syncStream.toByteArray();
|
||||
Call.onEntitySnapshot(player.con, (byte)group.getID(), (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes));
|
||||
sent = 0;
|
||||
syncStream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if(sent > 0){
|
||||
dataStream.close();
|
||||
|
||||
byte[] syncBytes = syncStream.toByteArray();
|
||||
Call.onEntitySnapshot(player.con, (byte)group.getID(), (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String fixName(String name){
|
||||
name = name.trim();
|
||||
if(name.equals("[") || name.equals("]")){
|
||||
return "";
|
||||
}
|
||||
|
||||
for(int i = 0; i < name.length(); i++){
|
||||
if(name.charAt(i) == '[' && i != name.length() - 1 && name.charAt(i + 1) != '[' && (i == 0 || name.charAt(i - 1) != '[')){
|
||||
String prev = name.substring(0, i);
|
||||
String next = name.substring(i);
|
||||
String result = checkColor(next);
|
||||
|
||||
name = prev + result;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
int curChar = 0;
|
||||
while(curChar < name.length() && result.toString().getBytes().length < maxNameLength){
|
||||
result.append(name.charAt(curChar++));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
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){
|
||||
return str.substring(i + 1);
|
||||
}
|
||||
}else{
|
||||
try{
|
||||
Color result = Color.valueOf(color);
|
||||
if(result.a <= 0.8f){
|
||||
return str.substring(i + 1);
|
||||
}
|
||||
}catch(Exception e){
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
void sync(){
|
||||
|
||||
try{
|
||||
//iterate through each player
|
||||
for(int i = 0; i < playerGroup.size(); i++){
|
||||
Player player = playerGroup.all().get(i);
|
||||
if(player.isLocal) continue;
|
||||
|
||||
if(player.con == null || !player.con.isConnected()){
|
||||
onDisconnect(player, "disappeared");
|
||||
continue;
|
||||
}
|
||||
|
||||
NetConnection connection = player.con;
|
||||
|
||||
if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
|
||||
|
||||
writeEntitySnapshot(player);
|
||||
}
|
||||
|
||||
if(playerGroup.size() > 0 && Core.settings.getBool("blocksync") && timer.get(timerBlockSync, blockSyncTime)){
|
||||
writeBlockSnapshots();
|
||||
}
|
||||
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
133
core/src/mindustry/core/Platform.java
Normal file
133
core/src/mindustry/core/Platform.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.Input.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.util.serialization.*;
|
||||
import mindustry.mod.*;
|
||||
import mindustry.net.*;
|
||||
import mindustry.net.Net.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import org.mozilla.javascript.*;
|
||||
|
||||
import static mindustry.Vars.mobile;
|
||||
|
||||
public interface Platform{
|
||||
|
||||
/** Steam: Update lobby visibility.*/
|
||||
default void updateLobby(){}
|
||||
|
||||
/** Steam: Show multiplayer friend invite dialog.*/
|
||||
default void inviteFriends(){}
|
||||
|
||||
/** Steam: Share a map on the workshop.*/
|
||||
default void publish(Publishable pub){}
|
||||
|
||||
/** Steam: View a listing on the workshop.*/
|
||||
default void viewListing(Publishable pub){}
|
||||
|
||||
/** Steam: View a listing on the workshop by an ID.*/
|
||||
default void viewListingID(String mapid){}
|
||||
|
||||
/** Steam: Return external workshop maps to be loaded.*/
|
||||
default Array<Fi> getWorkshopContent(Class<? extends Publishable> type){
|
||||
return new Array<>(0);
|
||||
}
|
||||
|
||||
/** Steam: Open workshop for maps.*/
|
||||
default void openWorkshop(){}
|
||||
|
||||
/** Get the networking implementation.*/
|
||||
default NetProvider getNet(){
|
||||
return new ArcNetProvider();
|
||||
}
|
||||
|
||||
/** Gets the scripting implementation. */
|
||||
default Scripts createScripts(){
|
||||
return new Scripts();
|
||||
}
|
||||
|
||||
default Context getScriptContext(){
|
||||
Context c = Context.enter();
|
||||
c.setOptimizationLevel(9);
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Add a text input dialog that should show up after the field is tapped. */
|
||||
default void addDialog(TextField field){
|
||||
addDialog(field, 16);
|
||||
}
|
||||
|
||||
/** See addDialog(). */
|
||||
default void addDialog(TextField field, int maxLength){
|
||||
if(!mobile) return; //this is mobile only, desktop doesn't need dialogs
|
||||
|
||||
field.tapped(() -> {
|
||||
TextInput input = new TextInput();
|
||||
input.text = field.getText();
|
||||
input.maxLength = maxLength;
|
||||
input.accepted = text -> {
|
||||
field.clearText();
|
||||
field.appendText(text);
|
||||
field.change();
|
||||
Core.input.setOnscreenKeyboardVisible(false);
|
||||
};
|
||||
Core.input.getTextInput(input);
|
||||
});
|
||||
}
|
||||
|
||||
/** Update discord RPC. */
|
||||
default void updateRPC(){
|
||||
}
|
||||
|
||||
/** Must be a base64 string 8 bytes in length. */
|
||||
default String getUUID(){
|
||||
String uuid = Core.settings.getString("uuid", "");
|
||||
if(uuid.isEmpty()){
|
||||
byte[] result = new byte[8];
|
||||
new RandomXS128().nextBytes(result);
|
||||
uuid = new String(Base64Coder.encode(result));
|
||||
Core.settings.put("uuid", uuid);
|
||||
Core.settings.save();
|
||||
return uuid;
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/** Only used for iOS or android: open the share menu for a map or save. */
|
||||
default void shareFile(Fi file){
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a file chooser.
|
||||
* @param cons Selection listener
|
||||
* @param open Whether to open or save files
|
||||
* @param extension File extension to filter
|
||||
*/
|
||||
default void showFileChooser(boolean open, String extension, Cons<Fi> cons){
|
||||
new FileChooser(open ? "$open" : "$save", file -> file.extension().toLowerCase().equals(extension), open, file -> {
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
|
||||
}else{
|
||||
cons.get(file);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
/** Hide the app. Android only. */
|
||||
default void hide(){
|
||||
}
|
||||
|
||||
/** Forces the app into landscape mode.*/
|
||||
default void beginForceLandscape(){
|
||||
}
|
||||
|
||||
/** Stops forcing the app into landscape orientation.*/
|
||||
default void endForceLandscape(){
|
||||
}
|
||||
}
|
||||
470
core/src/mindustry/core/Renderer.java
Normal file
470
core/src/mindustry/core/Renderer.java
Normal file
@@ -0,0 +1,470 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.gl.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.pooling.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.effect.GroundEffectEntity.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.input.*;
|
||||
import mindustry.ui.Cicon;
|
||||
import mindustry.world.blocks.defense.ForceProjector.*;
|
||||
|
||||
import static arc.Core.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Renderer implements ApplicationListener{
|
||||
public final BlockRenderer blocks = new BlockRenderer();
|
||||
public final MinimapRenderer minimap = new MinimapRenderer();
|
||||
public final OverlayRenderer overlays = new OverlayRenderer();
|
||||
public final LightRenderer lights = new LightRenderer();
|
||||
public final Pixelator pixelator = new Pixelator();
|
||||
|
||||
public FrameBuffer shieldBuffer = new FrameBuffer(2, 2);
|
||||
private Bloom bloom;
|
||||
private Color clearColor;
|
||||
private float targetscale = Scl.scl(4);
|
||||
private float camerascale = targetscale;
|
||||
private float landscale = 0f, landTime;
|
||||
private float minZoomScl = Scl.scl(0.01f);
|
||||
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
|
||||
private float shakeIntensity, shaketime;
|
||||
|
||||
public Renderer(){
|
||||
camera = new Camera();
|
||||
Shaders.init();
|
||||
|
||||
Effects.setScreenShakeProvider((intensity, duration) -> {
|
||||
shakeIntensity = Math.max(intensity, shakeIntensity);
|
||||
shaketime = Math.max(shaketime, duration);
|
||||
});
|
||||
|
||||
Effects.setEffectProvider((effect, color, x, y, rotation, data) -> {
|
||||
if(effect == Fx.none) return;
|
||||
if(Core.settings.getBool("effects")){
|
||||
Rectangle view = camera.bounds(rect);
|
||||
Rectangle pos = rect2.setSize(effect.size).setCenter(x, y);
|
||||
|
||||
if(view.overlaps(pos)){
|
||||
|
||||
if(!(effect instanceof GroundEffect)){
|
||||
EffectEntity entity = Pools.obtain(EffectEntity.class, EffectEntity::new);
|
||||
entity.effect = effect;
|
||||
entity.color.set(color);
|
||||
entity.rotation = rotation;
|
||||
entity.data = data;
|
||||
entity.id++;
|
||||
entity.set(x, y);
|
||||
if(data instanceof Entity){
|
||||
entity.setParent((Entity)data);
|
||||
}
|
||||
effectGroup.add(entity);
|
||||
}else{
|
||||
GroundEffectEntity entity = Pools.obtain(GroundEffectEntity.class, GroundEffectEntity::new);
|
||||
entity.effect = effect;
|
||||
entity.color.set(color);
|
||||
entity.rotation = rotation;
|
||||
entity.id++;
|
||||
entity.data = data;
|
||||
entity.set(x, y);
|
||||
if(data instanceof Entity){
|
||||
entity.setParent((Entity)data);
|
||||
}
|
||||
groundEffectGroup.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clearColor = new Color(0f, 0f, 0f, 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
if(settings.getBool("bloom")){
|
||||
setupBloom();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
Color.white.set(1f, 1f, 1f, 1f);
|
||||
|
||||
camerascale = Mathf.lerpDelta(camerascale, targetscale, 0.1f);
|
||||
|
||||
if(landTime > 0){
|
||||
landTime -= Time.delta();
|
||||
landscale = Interpolation.pow5In.apply(minZoomScl, Scl.scl(4f), 1f - landTime / Fx.coreLand.lifetime);
|
||||
camerascale = landscale;
|
||||
}
|
||||
|
||||
camera.width = graphics.getWidth() / camerascale;
|
||||
camera.height = graphics.getHeight() / camerascale;
|
||||
|
||||
if(state.is(State.menu)){
|
||||
landTime = 0f;
|
||||
graphics.clear(Color.black);
|
||||
}else{
|
||||
Vector2 position = Tmp.v3.set(player);
|
||||
|
||||
if(player.isDead()){
|
||||
TileEntity core = player.getClosestCore();
|
||||
if(core != null && player.spawner == null){
|
||||
camera.position.lerpDelta(core.x, core.y, 0.08f);
|
||||
}else{
|
||||
camera.position.lerpDelta(position, 0.08f);
|
||||
}
|
||||
}else if(control.input instanceof DesktopInput){
|
||||
camera.position.lerpDelta(position, 0.08f);
|
||||
}
|
||||
|
||||
updateShake(0.75f);
|
||||
if(pixelator.enabled()){
|
||||
pixelator.drawPixelate();
|
||||
}else{
|
||||
draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float landScale(){
|
||||
return landTime > 0 ? landscale : 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
minimap.dispose();
|
||||
shieldBuffer.dispose();
|
||||
blocks.dispose();
|
||||
if(bloom != null){
|
||||
bloom.dispose();
|
||||
bloom = null;
|
||||
}
|
||||
Events.fire(new DisposeEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
if(settings.getBool("bloom")){
|
||||
setupBloom();
|
||||
}
|
||||
}
|
||||
|
||||
void setupBloom(){
|
||||
try{
|
||||
if(bloom != null){
|
||||
bloom.dispose();
|
||||
bloom = null;
|
||||
}
|
||||
bloom = new Bloom(true);
|
||||
bloom.setClearColor(0f, 0f, 0f, 0f);
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
settings.put("bloom", false);
|
||||
settings.save();
|
||||
ui.showErrorMessage("$error.bloom");
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleBloom(boolean enabled){
|
||||
if(enabled){
|
||||
if(bloom == null){
|
||||
setupBloom();
|
||||
}
|
||||
}else{
|
||||
if(bloom != null){
|
||||
bloom.dispose();
|
||||
bloom = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(){
|
||||
camera.update();
|
||||
|
||||
if(Float.isNaN(camera.position.x) || Float.isNaN(camera.position.y)){
|
||||
camera.position.x = player.x;
|
||||
camera.position.y = player.y;
|
||||
}
|
||||
|
||||
graphics.clear(clearColor);
|
||||
|
||||
if(!graphics.isHidden() && (Core.settings.getBool("animatedwater") || Core.settings.getBool("animatedshields")) && (shieldBuffer.getWidth() != graphics.getWidth() || shieldBuffer.getHeight() != graphics.getHeight())){
|
||||
shieldBuffer.resize(graphics.getWidth(), graphics.getHeight());
|
||||
}
|
||||
|
||||
Draw.proj(camera.projection());
|
||||
|
||||
blocks.floor.drawFloor();
|
||||
|
||||
groundEffectGroup.draw(e -> e instanceof BelowLiquidTrait);
|
||||
puddleGroup.draw();
|
||||
groundEffectGroup.draw(e -> !(e instanceof BelowLiquidTrait));
|
||||
|
||||
blocks.processBlocks();
|
||||
|
||||
blocks.drawShadows();
|
||||
Draw.color();
|
||||
|
||||
blocks.floor.beginDraw();
|
||||
blocks.floor.drawLayer(CacheLayer.walls);
|
||||
blocks.floor.endDraw();
|
||||
|
||||
blocks.drawBlocks(Layer.block);
|
||||
blocks.drawFog();
|
||||
|
||||
blocks.drawDestroyed();
|
||||
|
||||
Draw.shader(Shaders.blockbuild, true);
|
||||
blocks.drawBlocks(Layer.placement);
|
||||
Draw.shader();
|
||||
|
||||
blocks.drawBlocks(Layer.overlay);
|
||||
|
||||
drawGroundShadows();
|
||||
|
||||
drawAllTeams(false);
|
||||
|
||||
blocks.drawBlocks(Layer.turret);
|
||||
|
||||
drawFlyerShadows();
|
||||
|
||||
blocks.drawBlocks(Layer.power);
|
||||
blocks.drawBlocks(Layer.lights);
|
||||
|
||||
drawAllTeams(true);
|
||||
|
||||
Draw.flush();
|
||||
if(bloom != null && !pixelator.enabled()){
|
||||
bloom.capture();
|
||||
}
|
||||
|
||||
bulletGroup.draw();
|
||||
effectGroup.draw();
|
||||
|
||||
Draw.flush();
|
||||
if(bloom != null && !pixelator.enabled()){
|
||||
bloom.render();
|
||||
}
|
||||
|
||||
overlays.drawBottom();
|
||||
playerGroup.draw(p -> p.isLocal, Player::drawBuildRequests);
|
||||
|
||||
if(shieldGroup.countInBounds() > 0){
|
||||
if(settings.getBool("animatedshields") && Shaders.shield != null){
|
||||
Draw.flush();
|
||||
shieldBuffer.begin();
|
||||
graphics.clear(Color.clear);
|
||||
shieldGroup.draw();
|
||||
shieldGroup.draw(shield -> true, ShieldEntity::drawOver);
|
||||
Draw.flush();
|
||||
shieldBuffer.end();
|
||||
Draw.shader(Shaders.shield);
|
||||
Draw.color(Pal.accent);
|
||||
Draw.rect(Draw.wrap(shieldBuffer.getTexture()), camera.position.x, camera.position.y, camera.width, -camera.height);
|
||||
Draw.color();
|
||||
Draw.shader();
|
||||
}else{
|
||||
shieldGroup.draw(shield -> true, ShieldEntity::drawSimple);
|
||||
}
|
||||
}
|
||||
|
||||
overlays.drawTop();
|
||||
|
||||
playerGroup.draw(p -> !p.isDead(), Player::drawName);
|
||||
|
||||
if(state.rules.lighting){
|
||||
lights.draw();
|
||||
}
|
||||
|
||||
drawLanding();
|
||||
|
||||
Draw.color();
|
||||
Draw.flush();
|
||||
}
|
||||
|
||||
private void drawLanding(){
|
||||
if(landTime > 0 && player.getClosestCore() != null){
|
||||
float fract = landTime / Fx.coreLand.lifetime;
|
||||
TileEntity entity = player.getClosestCore();
|
||||
|
||||
TextureRegion reg = entity.block.icon(Cicon.full);
|
||||
float scl = Scl.scl(4f) / camerascale;
|
||||
float s = reg.getWidth() * Draw.scl * scl * 4f * fract;
|
||||
|
||||
Draw.color(Pal.lightTrail);
|
||||
Draw.rect("circle-shadow", entity.x, entity.y, s, s);
|
||||
|
||||
Angles.randLenVectors(1, (1f- fract), 100, 1000f * scl * (1f-fract), (x, y, fin, fout) -> {
|
||||
Lines.stroke(scl * fin);
|
||||
Lines.lineAngle(entity.x + x, entity.y + y, Mathf.angle(x, y), (fin * 20 + 1f) * scl);
|
||||
});
|
||||
|
||||
Draw.color();
|
||||
Draw.mixcol(Color.white, fract);
|
||||
Draw.rect(reg, entity.x, entity.y, reg.getWidth() * Draw.scl * scl, reg.getHeight() * Draw.scl * scl, fract * 135f);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGroundShadows(){
|
||||
Draw.color(0, 0, 0, 0.4f);
|
||||
float rad = 1.6f;
|
||||
|
||||
Cons<Unit> draw = u -> {
|
||||
float size = Math.max(u.getIconRegion().getWidth(), u.getIconRegion().getHeight()) * Draw.scl;
|
||||
Draw.rect("circle-shadow", u.x, u.y, size * rad, size * rad);
|
||||
};
|
||||
|
||||
for(EntityGroup<? extends BaseUnit> group : unitGroups){
|
||||
if(!group.isEmpty()){
|
||||
group.draw(unit -> !unit.isDead(), draw::get);
|
||||
}
|
||||
}
|
||||
|
||||
if(!playerGroup.isEmpty()){
|
||||
playerGroup.draw(unit -> !unit.isDead(), draw::get);
|
||||
}
|
||||
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
private void drawFlyerShadows(){
|
||||
float trnsX = -12, trnsY = -13;
|
||||
Draw.color(0, 0, 0, 0.22f);
|
||||
|
||||
for(EntityGroup<? extends BaseUnit> group : unitGroups){
|
||||
if(!group.isEmpty()){
|
||||
group.draw(unit -> unit.isFlying() && !unit.isDead(), baseUnit -> baseUnit.drawShadow(trnsX, trnsY));
|
||||
}
|
||||
}
|
||||
|
||||
if(!playerGroup.isEmpty()){
|
||||
playerGroup.draw(unit -> unit.isFlying() && !unit.isDead(), player -> player.drawShadow(trnsX, trnsY));
|
||||
}
|
||||
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
private void drawAllTeams(boolean flying){
|
||||
for(Team team : Team.all){
|
||||
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
|
||||
|
||||
if(group.count(p -> p.isFlying() == flying) + playerGroup.count(p -> p.isFlying() == flying && p.getTeam() == team) == 0 && flying) continue;
|
||||
|
||||
unitGroups[team.ordinal()].draw(u -> u.isFlying() == flying && !u.isDead(), Unit::drawUnder);
|
||||
playerGroup.draw(p -> p.isFlying() == flying && p.getTeam() == team && !p.isDead(), Unit::drawUnder);
|
||||
|
||||
unitGroups[team.ordinal()].draw(u -> u.isFlying() == flying && !u.isDead(), Unit::drawAll);
|
||||
playerGroup.draw(p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawAll);
|
||||
|
||||
unitGroups[team.ordinal()].draw(u -> u.isFlying() == flying && !u.isDead(), Unit::drawOver);
|
||||
playerGroup.draw(p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawOver);
|
||||
}
|
||||
}
|
||||
|
||||
public void scaleCamera(float amount){
|
||||
targetscale += amount;
|
||||
clampScale();
|
||||
}
|
||||
|
||||
public void clampScale(){
|
||||
float s = Scl.scl(1f);
|
||||
targetscale = Mathf.clamp(targetscale, s * 1.5f, Math.round(s * 6));
|
||||
}
|
||||
|
||||
public float getScale(){
|
||||
return targetscale;
|
||||
}
|
||||
|
||||
public void setScale(float scl){
|
||||
targetscale = scl;
|
||||
clampScale();
|
||||
}
|
||||
|
||||
public void zoomIn(float duration){
|
||||
landscale = minZoomScl;
|
||||
landTime = duration;
|
||||
}
|
||||
|
||||
public void takeMapScreenshot(){
|
||||
drawGroundShadows();
|
||||
|
||||
int w = world.width() * tilesize, h = world.height() * tilesize;
|
||||
int memory = w * h * 4 / 1024 / 1024;
|
||||
|
||||
if(memory >= 65){
|
||||
ui.showInfo("$screenshot.invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hadShields = Core.settings.getBool("animatedshields");
|
||||
boolean hadWater = Core.settings.getBool("animatedwater");
|
||||
Core.settings.put("animatedwater", false);
|
||||
Core.settings.put("animatedshields", false);
|
||||
|
||||
FrameBuffer buffer = new FrameBuffer(w, h);
|
||||
|
||||
float vpW = camera.width, vpH = camera.height, px = camera.position.x, py = camera.position.y;
|
||||
disableUI = true;
|
||||
camera.width = w;
|
||||
camera.height = h;
|
||||
camera.position.x = w / 2f + tilesize / 2f;
|
||||
camera.position.y = h / 2f + tilesize / 2f;
|
||||
Draw.flush();
|
||||
buffer.begin();
|
||||
draw();
|
||||
Draw.flush();
|
||||
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);
|
||||
BufferUtils.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()));
|
||||
|
||||
buffer.dispose();
|
||||
|
||||
Core.settings.put("animatedwater", hadWater);
|
||||
Core.settings.put("animatedshields", hadShields);
|
||||
}
|
||||
|
||||
}
|
||||
507
core/src/mindustry/core/UI.java
Normal file
507
core/src/mindustry/core/UI.java
Normal file
@@ -0,0 +1,507 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.Graphics.*;
|
||||
import arc.Graphics.Cursor.*;
|
||||
import arc.Input.*;
|
||||
import arc.assets.*;
|
||||
import arc.assets.loaders.*;
|
||||
import arc.assets.loaders.resolvers.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.freetype.*;
|
||||
import arc.freetype.FreeTypeFontGenerator.*;
|
||||
import arc.freetype.FreetypeFontLoader.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.Texture.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.actions.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.TextField.*;
|
||||
import arc.scene.ui.Tooltip.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.editor.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.ui.fragments.*;
|
||||
|
||||
import static arc.scene.actions.Actions.*;
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class UI implements ApplicationListener, Loadable{
|
||||
public MenuFragment menufrag;
|
||||
public HudFragment hudfrag;
|
||||
public ChatFragment chatfrag;
|
||||
public ScriptConsoleFragment scriptfrag;
|
||||
public PlayerListFragment listfrag;
|
||||
public LoadingFragment loadfrag;
|
||||
|
||||
public WidgetGroup menuGroup, hudGroup;
|
||||
|
||||
public AboutDialog about;
|
||||
public GameOverDialog restart;
|
||||
public CustomGameDialog custom;
|
||||
public MapsDialog maps;
|
||||
public LoadDialog load;
|
||||
public DiscordDialog discord;
|
||||
public JoinDialog join;
|
||||
public HostDialog host;
|
||||
public PausedDialog paused;
|
||||
public SettingsMenuDialog settings;
|
||||
public ControlsDialog controls;
|
||||
public MapEditorDialog editor;
|
||||
public LanguageDialog language;
|
||||
public BansDialog bans;
|
||||
public AdminsDialog admins;
|
||||
public TraceDialog traces;
|
||||
public DatabaseDialog database;
|
||||
public ContentInfoDialog content;
|
||||
public DeployDialog deploy;
|
||||
public TechTreeDialog tech;
|
||||
public MinimapDialog minimap;
|
||||
public SchematicsDialog schematics;
|
||||
public ModsDialog mods;
|
||||
public ColorPicker picker;
|
||||
|
||||
public Cursor drillCursor, unloadCursor;
|
||||
|
||||
public UI(){
|
||||
setupFonts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAsync(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSync(){
|
||||
Fonts.outline.getData().markupEnabled = true;
|
||||
Fonts.def.getData().markupEnabled = true;
|
||||
Fonts.def.setOwnsTexture(false);
|
||||
|
||||
Core.assets.getAll(BitmapFont.class, new Array<>()).each(font -> font.setUseIntegerPositions(true));
|
||||
Core.scene = new Scene();
|
||||
Core.input.addProcessor(Core.scene);
|
||||
|
||||
Tex.load();
|
||||
Icon.load();
|
||||
Styles.load();
|
||||
Tex.loadStyles();
|
||||
|
||||
Dialog.setShowAction(() -> sequence(alpha(0f), fadeIn(0.1f)));
|
||||
Dialog.setHideAction(() -> sequence(fadeOut(0.1f)));
|
||||
|
||||
Tooltips.getInstance().animations = false;
|
||||
|
||||
Core.settings.setErrorHandler(e -> {
|
||||
e.printStackTrace();
|
||||
Core.app.post(() -> showErrorMessage("Failed to access local storage.\nSettings will not be saved."));
|
||||
});
|
||||
|
||||
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);
|
||||
loadExtraCursors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Array<AssetDescriptor> getDependencies(){
|
||||
return Array.with(new AssetDescriptor<>(Control.class), new AssetDescriptor<>("outline", BitmapFont.class), new AssetDescriptor<>("default", BitmapFont.class), new AssetDescriptor<>("chat", BitmapFont.class));
|
||||
}
|
||||
|
||||
/** Called from a static context to make the cursor appear immediately upon startup.*/
|
||||
public static void loadSystemCursors(){
|
||||
SystemCursor.arrow.set(Core.graphics.newCursor("cursor"));
|
||||
SystemCursor.hand.set(Core.graphics.newCursor("hand"));
|
||||
SystemCursor.ibeam.set(Core.graphics.newCursor("ibeam"));
|
||||
|
||||
Core.graphics.restoreCursor();
|
||||
}
|
||||
|
||||
/** Called from a static context for use in the loading screen.*/
|
||||
public static void loadDefaultFont(){
|
||||
FileHandleResolver resolver = new InternalFileHandleResolver();
|
||||
Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
|
||||
Core.assets.setLoader(BitmapFont.class, null, new FreetypeFontLoader(resolver){
|
||||
@Override
|
||||
public BitmapFont loadSync(AssetManager manager, String fileName, Fi file, FreeTypeFontLoaderParameter parameter){
|
||||
if(fileName.equals("outline")){
|
||||
parameter.fontParameters.borderWidth = Scl.scl(2f);
|
||||
parameter.fontParameters.spaceX -= parameter.fontParameters.borderWidth;
|
||||
}
|
||||
parameter.fontParameters.magFilter = TextureFilter.Linear;
|
||||
parameter.fontParameters.minFilter = TextureFilter.Linear;
|
||||
parameter.fontParameters.size = fontParameter().size;
|
||||
return super.loadSync(manager, fileName, file, parameter);
|
||||
}
|
||||
});
|
||||
|
||||
FreeTypeFontParameter param = new FreeTypeFontParameter(){{
|
||||
borderColor = Color.darkGray;
|
||||
incremental = true;
|
||||
}};
|
||||
|
||||
Core.assets.load("outline", BitmapFont.class, new FreeTypeFontLoaderParameter("fonts/font.ttf", param)).loaded = t -> Fonts.outline = (BitmapFont)t;
|
||||
}
|
||||
|
||||
void loadExtraCursors(){
|
||||
drillCursor = Core.graphics.newCursor("drill");
|
||||
unloadCursor = Core.graphics.newCursor("unload");
|
||||
}
|
||||
|
||||
public void setupFonts(){
|
||||
String fontName = "fonts/font.ttf";
|
||||
|
||||
FreeTypeFontParameter param = fontParameter();
|
||||
|
||||
Core.assets.load("default", BitmapFont.class, new FreeTypeFontLoaderParameter(fontName, param)).loaded = f -> Fonts.def = (BitmapFont)f;
|
||||
Core.assets.load("chat", BitmapFont.class, new FreeTypeFontLoaderParameter(fontName, param)).loaded = f -> Fonts.chat = (BitmapFont)f;
|
||||
}
|
||||
|
||||
static FreeTypeFontParameter fontParameter(){
|
||||
return new FreeTypeFontParameter(){{
|
||||
size = (int)(Scl.scl(18f));
|
||||
shadowColor = Color.darkGray;
|
||||
shadowOffsetY = 2;
|
||||
incremental = true;
|
||||
}};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(disableUI || Core.scene == null) return;
|
||||
|
||||
Core.scene.act();
|
||||
Core.scene.draw();
|
||||
|
||||
if(Core.input.keyTap(KeyCode.MOUSE_LEFT) && Core.scene.getKeyboardFocus() instanceof TextField){
|
||||
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
|
||||
if(!(e instanceof TextField)){
|
||||
Core.scene.setKeyboardFocus(null);
|
||||
}
|
||||
}
|
||||
|
||||
//draw overlay for buttons
|
||||
if(state.rules.tutorial){
|
||||
control.tutorial.draw();
|
||||
Draw.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
menuGroup = new WidgetGroup();
|
||||
hudGroup = new WidgetGroup();
|
||||
|
||||
menufrag = new MenuFragment();
|
||||
hudfrag = new HudFragment();
|
||||
chatfrag = new ChatFragment();
|
||||
listfrag = new PlayerListFragment();
|
||||
loadfrag = new LoadingFragment();
|
||||
scriptfrag = new ScriptConsoleFragment();
|
||||
|
||||
picker = new ColorPicker();
|
||||
editor = new MapEditorDialog();
|
||||
controls = new ControlsDialog();
|
||||
restart = new GameOverDialog();
|
||||
join = new JoinDialog();
|
||||
discord = new DiscordDialog();
|
||||
load = new LoadDialog();
|
||||
custom = new CustomGameDialog();
|
||||
language = new LanguageDialog();
|
||||
database = new DatabaseDialog();
|
||||
settings = new SettingsMenuDialog();
|
||||
host = new HostDialog();
|
||||
paused = new PausedDialog();
|
||||
about = new AboutDialog();
|
||||
bans = new BansDialog();
|
||||
admins = new AdminsDialog();
|
||||
traces = new TraceDialog();
|
||||
maps = new MapsDialog();
|
||||
content = new ContentInfoDialog();
|
||||
deploy = new DeployDialog();
|
||||
tech = new TechTreeDialog();
|
||||
minimap = new MinimapDialog();
|
||||
mods = new ModsDialog();
|
||||
schematics = new SchematicsDialog();
|
||||
|
||||
Group group = Core.scene.root;
|
||||
|
||||
menuGroup.setFillParent(true);
|
||||
menuGroup.touchable(Touchable.childrenOnly);
|
||||
menuGroup.visible(() -> state.is(State.menu));
|
||||
hudGroup.setFillParent(true);
|
||||
hudGroup.touchable(Touchable.childrenOnly);
|
||||
hudGroup.visible(() -> !state.is(State.menu));
|
||||
|
||||
Core.scene.add(menuGroup);
|
||||
Core.scene.add(hudGroup);
|
||||
|
||||
hudfrag.build(hudGroup);
|
||||
menufrag.build(menuGroup);
|
||||
chatfrag.container().build(hudGroup);
|
||||
listfrag.build(hudGroup);
|
||||
scriptfrag.container().build(hudGroup);
|
||||
loadfrag.build(group);
|
||||
new FadeInFragment().build(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
if(Core.scene == null) return;
|
||||
Core.scene.resize(width, height);
|
||||
Events.fire(new ResizeEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
//generator.dispose();
|
||||
}
|
||||
|
||||
public void loadAnd(Runnable call){
|
||||
loadAnd("$loading", call);
|
||||
}
|
||||
|
||||
public void loadAnd(String text, Runnable call){
|
||||
loadfrag.show(text);
|
||||
Time.runTask(7f, () -> {
|
||||
call.run();
|
||||
loadfrag.hide();
|
||||
});
|
||||
}
|
||||
|
||||
public void showTextInput(String titleText, String dtext, int textLength, String def, boolean inumeric, Cons<String> confirmed){
|
||||
if(mobile){
|
||||
Core.input.getTextInput(new TextInput(){{
|
||||
this.title = (titleText.startsWith("$") ? Core.bundle.get(titleText.substring(1)) : titleText);
|
||||
this.text = def;
|
||||
this.numeric = inumeric;
|
||||
this.maxLength = textLength;
|
||||
this.accepted = confirmed;
|
||||
}});
|
||||
}else{
|
||||
new Dialog(titleText){{
|
||||
cont.margin(30).add(dtext).padRight(6f);
|
||||
TextFieldFilter filter = inumeric ? TextFieldFilter.digitsOnly : (f, c) -> true;
|
||||
TextField field = cont.addField(def, t -> {}).size(330f, 50f).get();
|
||||
field.setFilter((f, c) -> field.getText().length() < textLength && filter.acceptChar(f, c));
|
||||
buttons.defaults().size(120, 54).pad(4);
|
||||
buttons.addButton("$ok", () -> {
|
||||
confirmed.get(field.getText());
|
||||
hide();
|
||||
}).disabled(b -> field.getText().isEmpty());
|
||||
buttons.addButton("$cancel", this::hide);
|
||||
}}.show();
|
||||
}
|
||||
}
|
||||
|
||||
public void showTextInput(String title, String text, String def, Cons<String> confirmed){
|
||||
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 showInfoFade(String info){
|
||||
Table table = new Table();
|
||||
table.setFillParent(true);
|
||||
table.actions(Actions.fadeOut(7f, Interpolation.fade), Actions.remove());
|
||||
table.top().add(info).style(Styles.outlineLabel).padTop(10);
|
||||
Core.scene.add(table);
|
||||
}
|
||||
|
||||
public void showInfo(String info){
|
||||
new Dialog(""){{
|
||||
getCell(cont).growX();
|
||||
cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center);
|
||||
buttons.addButton("$ok", this::hide).size(90, 50).pad(4);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showErrorMessage(String text){
|
||||
new Dialog(""){{
|
||||
setFillParent(true);
|
||||
cont.margin(15f);
|
||||
cont.add("$error.title");
|
||||
cont.row();
|
||||
cont.addImage().width(300f).pad(2).height(4f).color(Color.scarlet);
|
||||
cont.row();
|
||||
cont.add(text).pad(2f).growX().wrap().get().setAlignment(Align.center);
|
||||
cont.row();
|
||||
cont.addButton("$ok", this::hide).size(120, 50).pad(4);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showException(Throwable t){
|
||||
showException("", t);
|
||||
}
|
||||
|
||||
public void showException(String text, Throwable exc){
|
||||
loadfrag.hide();
|
||||
new Dialog(""){{
|
||||
String message = Strings.getFinalMesage(exc);
|
||||
|
||||
setFillParent(true);
|
||||
cont.margin(15);
|
||||
cont.add("$error.title").colspan(2);
|
||||
cont.row();
|
||||
cont.addImage().width(300f).pad(2).colspan(2).height(4f).color(Color.scarlet);
|
||||
cont.row();
|
||||
cont.add((text.startsWith("$") ? Core.bundle.get(text.substring(1)) : text) + (message == null ? "" : "\n[lightgray](" + message + ")")).colspan(2).wrap().growX().center().get().setAlignment(Align.center);
|
||||
cont.row();
|
||||
|
||||
Collapser col = new Collapser(base -> base.pane(t -> t.margin(14f).add(Strings.parseException(exc, true)).color(Color.lightGray).left()), true);
|
||||
|
||||
cont.addButton("$details", Styles.togglet, col::toggle).size(180f, 50f).checked(b -> !col.isCollapsed()).fillX().right();
|
||||
cont.addButton("$ok", this::hide).size(100, 50).fillX().left();
|
||||
cont.row();
|
||||
cont.add(col).colspan(2).pad(2);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showExceptions(String text, String... messages){
|
||||
loadfrag.hide();
|
||||
new Dialog(""){{
|
||||
|
||||
setFillParent(true);
|
||||
cont.margin(15);
|
||||
cont.add("$error.title").colspan(2);
|
||||
cont.row();
|
||||
cont.addImage().width(300f).pad(2).colspan(2).height(4f).color(Color.scarlet);
|
||||
cont.row();
|
||||
cont.add(text).colspan(2).wrap().growX().center().get().setAlignment(Align.center);
|
||||
cont.row();
|
||||
|
||||
//cont.pane(p -> {
|
||||
for(int i = 0; i < messages.length; i += 2){
|
||||
String btext = messages[i];
|
||||
String details = messages[i + 1];
|
||||
Collapser col = new Collapser(base -> base.pane(t -> t.margin(14f).add(details).color(Color.lightGray).left()), true);
|
||||
|
||||
cont.add(btext).right();
|
||||
cont.addButton("$details", Styles.togglet, col::toggle).size(180f, 50f).checked(b -> !col.isCollapsed()).fillX().left();
|
||||
cont.row();
|
||||
cont.add(col).colspan(2).pad(2);
|
||||
cont.row();
|
||||
}
|
||||
//}).colspan(2);
|
||||
|
||||
cont.addButton("$ok", this::hide).size(300, 50).fillX().colspan(2);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showText(String titleText, String text){
|
||||
showText(titleText, text, Align.center);
|
||||
}
|
||||
|
||||
public void showText(String titleText, String text, int align){
|
||||
new Dialog(titleText){{
|
||||
cont.row();
|
||||
cont.addImage().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent);
|
||||
cont.row();
|
||||
cont.add(text).width(400f).wrap().get().setAlignment(align, align);
|
||||
cont.row();
|
||||
buttons.addButton("$ok", this::hide).size(90, 50).pad(4);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showInfoText(String titleText, String text){
|
||||
new Dialog(titleText){{
|
||||
cont.margin(15).add(text).width(400f).wrap().left().get().setAlignment(Align.left, Align.left);
|
||||
buttons.addButton("$ok", this::hide).size(90, 50).pad(4);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showSmall(String titleText, String text){
|
||||
new Dialog(titleText){{
|
||||
cont.margin(10).add(text);
|
||||
titleTable.row();
|
||||
titleTable.addImage().color(Pal.accent).height(3f).growX().pad(2f);
|
||||
buttons.addButton("$ok", this::hide).size(90, 50).pad(4);
|
||||
}}.show();
|
||||
}
|
||||
|
||||
public void showConfirm(String title, String text, Runnable confirmed){
|
||||
showConfirm(title, text, null, confirmed);
|
||||
}
|
||||
|
||||
public void showConfirm(String title, String text, Boolp hide, Runnable confirmed){
|
||||
FloatingDialog dialog = new FloatingDialog(title);
|
||||
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.addButton("$cancel", dialog::hide);
|
||||
dialog.buttons.addButton("$ok", () -> {
|
||||
dialog.hide();
|
||||
confirmed.run();
|
||||
});
|
||||
if(hide != null){
|
||||
dialog.update(() -> {
|
||||
if(hide.get()){
|
||||
dialog.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
dialog.keyDown(KeyCode.ESCAPE, dialog::hide);
|
||||
dialog.keyDown(KeyCode.BACK, dialog::hide);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
public void showCustomConfirm(String title, String text, String yes, String no, Runnable confirmed, Runnable denied){
|
||||
FloatingDialog dialog = new FloatingDialog(title);
|
||||
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.addButton(no, () -> {
|
||||
dialog.hide();
|
||||
denied.run();
|
||||
});
|
||||
dialog.buttons.addButton(yes, () -> {
|
||||
dialog.hide();
|
||||
confirmed.run();
|
||||
});
|
||||
dialog.keyDown(KeyCode.ESCAPE, dialog::hide);
|
||||
dialog.keyDown(KeyCode.BACK, dialog::hide);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void showOkText(String title, String text, Runnable confirmed){
|
||||
FloatingDialog dialog = new FloatingDialog(title);
|
||||
dialog.cont.add(text).width(500f).wrap().pad(4f).get().setAlignment(Align.center, Align.center);
|
||||
dialog.buttons.defaults().size(200f, 54f).pad(2f);
|
||||
dialog.setFillParent(false);
|
||||
dialog.buttons.addButton("$ok", () -> {
|
||||
dialog.hide();
|
||||
confirmed.run();
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public String formatAmount(int number){
|
||||
if(number >= 1000000){
|
||||
return Strings.fixed(number / 1000000f, 1) + "[gray]" + Core.bundle.getOrNull("unit.millions") + "[]";
|
||||
}else if(number >= 10000){
|
||||
return number / 1000 + "[gray]k[]";
|
||||
}else if(number >= 1000){
|
||||
return Strings.fixed(number / 1000f, 1) + "[gray]" + Core.bundle.getOrNull("unit.thousands") + "[]";
|
||||
}else{
|
||||
return number + "";
|
||||
}
|
||||
}
|
||||
}
|
||||
48
core/src/mindustry/core/Version.java
Normal file
48
core/src/mindustry/core/Version.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.Files.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.util.*;
|
||||
import arc.util.io.*;
|
||||
|
||||
public class Version{
|
||||
/** Build type. 'official' for official releases; 'custom' or 'bleeding edge' are also used. */
|
||||
public static String type;
|
||||
/** Build modifier, e.g. 'alpha' or 'release' */
|
||||
public static String modifier;
|
||||
/** Number specifying the major version, e.g. '4' */
|
||||
public static int number;
|
||||
/** Build number, e.g. '43'. set to '-1' for custom builds. */
|
||||
public static int build = 0;
|
||||
/** Revision number. Used for hotfixes. Does not affect server compatibility. */
|
||||
public static int revision = 0;
|
||||
/** Whether version loading is enabled. */
|
||||
public static boolean enabled = true;
|
||||
|
||||
public static void init(){
|
||||
if(!enabled) return;
|
||||
|
||||
Fi file = OS.isAndroid || OS.isIos ? Core.files.internal("version.properties") : new Fi("version.properties", FileType.internal);
|
||||
|
||||
ObjectMap<String, String> map = new ObjectMap<>();
|
||||
PropertiesUtils.load(map, file.reader());
|
||||
|
||||
type = map.get("type");
|
||||
number = Integer.parseInt(map.get("number", "4"));
|
||||
modifier = map.get("modifier");
|
||||
if(map.get("build").contains(".")){
|
||||
String[] split = map.get("build").split("\\.");
|
||||
try{
|
||||
build = Integer.parseInt(split[0]);
|
||||
revision = Integer.parseInt(split[1]);
|
||||
}catch(Throwable e){
|
||||
e.printStackTrace();
|
||||
build = -1;
|
||||
}
|
||||
}else{
|
||||
build = Strings.canParseInt(map.get("build")) ? Integer.parseInt(map.get("build")) : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
521
core/src/mindustry/core/World.java
Normal file
521
core/src/mindustry/core/World.java
Normal file
@@ -0,0 +1,521 @@
|
||||
package mindustry.core;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.maps.filters.GenerateFilter.*;
|
||||
import mindustry.maps.generators.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class World{
|
||||
public final Context context = new Context();
|
||||
|
||||
private Map currentMap;
|
||||
private Tile[][] tiles;
|
||||
|
||||
private boolean generating, invalidMap;
|
||||
|
||||
public World(){
|
||||
|
||||
}
|
||||
|
||||
public boolean isInvalidMap(){
|
||||
return invalidMap;
|
||||
}
|
||||
|
||||
public boolean solid(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
|
||||
return tile == null || tile.solid();
|
||||
}
|
||||
|
||||
public boolean passable(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
|
||||
return tile != null && tile.passable();
|
||||
}
|
||||
|
||||
public boolean wallSolid(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
return tile == null || tile.block().solid;
|
||||
}
|
||||
|
||||
public boolean isAccessible(int x, int y){
|
||||
return !wallSolid(x, y - 1) || !wallSolid(x, y + 1) || !wallSolid(x - 1, y) || !wallSolid(x + 1, y);
|
||||
}
|
||||
|
||||
public Map getMap(){
|
||||
return currentMap;
|
||||
}
|
||||
|
||||
public void setMap(Map map){
|
||||
this.currentMap = map;
|
||||
}
|
||||
|
||||
public int width(){
|
||||
return tiles == null ? 0 : tiles.length;
|
||||
}
|
||||
|
||||
public int height(){
|
||||
return tiles == null ? 0 : tiles[0].length;
|
||||
}
|
||||
|
||||
public int unitWidth(){
|
||||
return width()*tilesize;
|
||||
}
|
||||
|
||||
public int unitHeight(){
|
||||
return height()*tilesize;
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
Tile tile(int pos){
|
||||
return tiles == null ? null : tile(Pos.x(pos), Pos.y(pos));
|
||||
}
|
||||
|
||||
public @Nullable Tile tile(int x, int y){
|
||||
if(tiles == null){
|
||||
return null;
|
||||
}
|
||||
if(!Structs.inBounds(x, y, tiles)) return null;
|
||||
return tiles[x][y];
|
||||
}
|
||||
|
||||
public @Nullable Tile ltile(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
if(tile == null) return null;
|
||||
return tile.block().linked(tile);
|
||||
}
|
||||
|
||||
public Tile rawTile(int x, int y){
|
||||
return tiles[x][y];
|
||||
}
|
||||
|
||||
public @Nullable Tile tileWorld(float x, float y){
|
||||
return tile(Math.round(x / tilesize), Math.round(y / tilesize));
|
||||
}
|
||||
|
||||
public @Nullable Tile ltileWorld(float x, float y){
|
||||
return ltile(Math.round(x / tilesize), Math.round(y / tilesize));
|
||||
}
|
||||
|
||||
public int toTile(float coord){
|
||||
return Math.round(coord / tilesize);
|
||||
}
|
||||
|
||||
public Tile[][] getTiles(){
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private void clearTileEntities(){
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
if(tiles[x][y] != null && tiles[x][y].entity != null){
|
||||
tiles[x][y].entity.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the tile array to the specified size and returns the resulting tile array.
|
||||
* Only use for loading saves!
|
||||
*/
|
||||
public Tile[][] createTiles(int width, int height){
|
||||
if(tiles != null){
|
||||
clearTileEntities();
|
||||
|
||||
if(tiles.length != width || tiles[0].length != height){
|
||||
tiles = new Tile[width][height];
|
||||
}
|
||||
}else{
|
||||
tiles = new Tile[width][height];
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to signify the beginning of map loading.
|
||||
* TileChangeEvents will not be fired until endMapLoad().
|
||||
*/
|
||||
public void beginMapLoad(){
|
||||
generating = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to signify the end of map loading. Updates tile occlusions and sets up physics for the world.
|
||||
* A WorldLoadEvent will be fire.
|
||||
*/
|
||||
public void endMapLoad(){
|
||||
prepareTiles(tiles);
|
||||
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
Tile tile = tiles[x][y];
|
||||
tile.updateOcclusion();
|
||||
|
||||
if(tile.entity != null){
|
||||
tile.entity.updateProximity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!headless){
|
||||
addDarkness(tiles);
|
||||
}
|
||||
|
||||
entities.all().each(group -> group.resize(-finalWorldBounds, -finalWorldBounds, tiles.length * tilesize + finalWorldBounds * 2, tiles[0].length * tilesize + finalWorldBounds * 2));
|
||||
|
||||
generating = false;
|
||||
Events.fire(new WorldLoadEvent());
|
||||
}
|
||||
|
||||
public void setGenerating(boolean gen){
|
||||
this.generating = gen;
|
||||
}
|
||||
|
||||
public boolean isGenerating(){
|
||||
return generating;
|
||||
}
|
||||
|
||||
public boolean isZone(){
|
||||
return getZone() != null;
|
||||
}
|
||||
|
||||
public Zone getZone(){
|
||||
return state.rules.zone;
|
||||
}
|
||||
|
||||
public void loadGenerator(Generator generator){
|
||||
beginMapLoad();
|
||||
|
||||
createTiles(generator.width, generator.height);
|
||||
generator.generate(tiles);
|
||||
|
||||
endMapLoad();
|
||||
}
|
||||
|
||||
public void loadMap(Map map){
|
||||
loadMap(map, new Rules());
|
||||
}
|
||||
|
||||
public void loadMap(Map map, Rules checkRules){
|
||||
try{
|
||||
SaveIO.load(map.file, new FilterContext(map));
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
if(!headless){
|
||||
ui.showErrorMessage("$map.invalid");
|
||||
Core.app.post(() -> state.set(State.menu));
|
||||
invalidMap = true;
|
||||
}
|
||||
generating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentMap = map;
|
||||
|
||||
invalidMap = false;
|
||||
|
||||
if(!headless){
|
||||
if(state.teams.get(defaultTeam).cores.size == 0 && !checkRules.pvp){
|
||||
ui.showErrorMessage("$map.nospawn");
|
||||
invalidMap = true;
|
||||
}else if(checkRules.pvp){ //pvp maps need two cores to be valid
|
||||
int teams = 0;
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.get(team).cores.size != 0){
|
||||
teams ++;
|
||||
}
|
||||
}
|
||||
if(teams < 2){
|
||||
invalidMap = true;
|
||||
ui.showErrorMessage("$map.nospawn.pvp");
|
||||
}
|
||||
}else if(checkRules.attackMode){ //attack maps need two cores to be valid
|
||||
invalidMap = state.teams.get(waveTeam).cores.isEmpty();
|
||||
if(invalidMap){
|
||||
ui.showErrorMessage("$map.nospawn.attack");
|
||||
}
|
||||
}
|
||||
}else{
|
||||
invalidMap = true;
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.get(team).cores.size != 0){
|
||||
invalidMap = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(invalidMap){
|
||||
throw new MapException(map, "Map has no cores!");
|
||||
}
|
||||
}
|
||||
|
||||
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 removeBlock(Tile tile){
|
||||
if(tile == null) return;
|
||||
tile.link().getLinkedTiles(other -> other.setBlock(Blocks.air));
|
||||
}
|
||||
|
||||
public void setBlock(Tile tile, Block block, Team team){
|
||||
setBlock(tile, block, team, 0);
|
||||
}
|
||||
|
||||
public void setBlock(Tile tile, Block block, Team team, int rotation){
|
||||
tile.setBlock(block, team, rotation);
|
||||
if(block.isMultiblock()){
|
||||
int offsetx = -(block.size - 1) / 2;
|
||||
int offsety = -(block.size - 1) / 2;
|
||||
|
||||
for(int dx = 0; dx < block.size; dx++){
|
||||
for(int dy = 0; dy < block.size; dy++){
|
||||
int worldx = dx + offsetx + tile.x;
|
||||
int worldy = dy + offsety + tile.y;
|
||||
if(!(worldx == tile.x && worldy == tile.y)){
|
||||
Tile toplace = world.tile(worldx, worldy);
|
||||
if(toplace != null){
|
||||
toplace.setBlock(BlockPart.get(dx + offsetx, dy + offsety), team);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 void addDarkness(Tile[][] tiles){
|
||||
byte[][] dark = new byte[tiles.length][tiles[0].length];
|
||||
byte[][] writeBuffer = new byte[tiles.length][tiles[0].length];
|
||||
|
||||
byte darkIterations = 4;
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
Tile tile = tiles[x][y];
|
||||
if(tile.isDarkened()){
|
||||
dark[x][y] = darkIterations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < darkIterations; i++){
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
boolean min = false;
|
||||
for(Point2 point : Geometry.d4){
|
||||
int newX = x + point.x, newY = y + point.y;
|
||||
if(Structs.inBounds(newX, newY, tiles) && dark[newX][newY] < dark[x][y]){
|
||||
min = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeBuffer[x][y] = (byte)Math.max(0, dark[x][y] - Mathf.num(min));
|
||||
}
|
||||
}
|
||||
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
System.arraycopy(writeBuffer[x], 0, dark[x], 0, tiles[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
Tile tile = tiles[x][y];
|
||||
if(tile.isDarkened()){
|
||||
tiles[x][y].rotation(dark[x][y]);
|
||||
}
|
||||
if(dark[x][y] == 4){
|
||||
boolean full = true;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int px = p.x + x, py = p.y + y;
|
||||
if(Structs.inBounds(px, py, tiles) && !(tiles[px][py].isDarkened() && dark[px][py] == 4)){
|
||||
full = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(full) tiles[x][y].rotation(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Prepares' a tile array by:<br>
|
||||
* - setting up multiblocks<br>
|
||||
* - updating occlusion<br>
|
||||
* Usually used before placing structures on a tile array.
|
||||
*/
|
||||
public void prepareTiles(Tile[][] tiles){
|
||||
|
||||
//find multiblocks
|
||||
IntArray multiblocks = new IntArray();
|
||||
|
||||
for(int x = 0; x < tiles.length; x++){
|
||||
for(int y = 0; y < tiles[0].length; y++){
|
||||
Tile tile = tiles[x][y];
|
||||
|
||||
if(tile.block().isMultiblock()){
|
||||
multiblocks.add(tile.pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//place multiblocks now
|
||||
for(int i = 0; i < multiblocks.size; i++){
|
||||
int pos = multiblocks.get(i);
|
||||
|
||||
int x = Pos.x(pos);
|
||||
int y = Pos.y(pos);
|
||||
|
||||
Block result = tiles[x][y].block();
|
||||
Team team = tiles[x][y].getTeam();
|
||||
|
||||
int offsetx = -(result.size - 1) / 2;
|
||||
int offsety = -(result.size - 1) / 2;
|
||||
|
||||
for(int dx = 0; dx < result.size; dx++){
|
||||
for(int dy = 0; dy < result.size; dy++){
|
||||
int worldx = dx + offsetx + x;
|
||||
int worldy = dy + offsety + y;
|
||||
if(!(worldx == x && worldy == y)){
|
||||
Tile toplace = world.tile(worldx, worldy);
|
||||
if(toplace != null){
|
||||
toplace.setBlock(BlockPart.get(dx + offsetx, dy + offsety), team);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Raycaster{
|
||||
boolean accept(int x, int y);
|
||||
}
|
||||
|
||||
private class Context implements WorldContext{
|
||||
@Override
|
||||
public Tile tile(int x, int y){
|
||||
return tiles[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
createTiles(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
|
||||
return (tiles[x][y] = new Tile(x, y, floorID, overlayID, wallID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerating(){
|
||||
return World.this.isGenerating();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin(){
|
||||
beginMapLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(){
|
||||
endMapLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/** World context that applies filters after generation end. */
|
||||
private class FilterContext extends Context{
|
||||
final Map map;
|
||||
|
||||
FilterContext(Map map){
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(){
|
||||
Array<GenerateFilter> filters = map.filters();
|
||||
if(!filters.isEmpty()){
|
||||
//input for filter queries
|
||||
GenerateInput input = new GenerateInput();
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
input.begin(filter, width(), height(), (x, y) -> tiles[x][y]);
|
||||
|
||||
//actually apply the filter
|
||||
for(int x = 0; x < width(); x++){
|
||||
for(int y = 0; y < height(); y++){
|
||||
Tile tile = rawTile(x, y);
|
||||
input.apply(x, y, tile.floor(), tile.block(), tile.overlay());
|
||||
filter.apply(input);
|
||||
|
||||
tile.setFloor((Floor)input.floor);
|
||||
tile.setOverlay(input.ore);
|
||||
|
||||
if(!tile.block().synthetic() && !input.block.synthetic()){
|
||||
tile.setBlock(input.block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
63
core/src/mindustry/ctype/Content.java
Normal file
63
core/src/mindustry/ctype/Content.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
import arc.files.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
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>{
|
||||
public final short id;
|
||||
/** Info on which mod this content was loaded from. */
|
||||
public @NonNull ModContentInfo minfo = new ModContentInfo();
|
||||
|
||||
|
||||
public Content(){
|
||||
this.id = (short) Vars.content.getBy(getContentType()).size;
|
||||
Vars.content.handleContent(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type name of this piece of content.
|
||||
* This should return the same value for all instances of this content type.
|
||||
*/
|
||||
public abstract ContentType getContentType();
|
||||
|
||||
/** Called after all content and modules are created. Do not use to load regions or texture data! */
|
||||
public void init(){
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after all content is created, only on non-headless versions.
|
||||
* Use for loading regions or other image data.
|
||||
*/
|
||||
public void load(){
|
||||
}
|
||||
|
||||
/** @return whether an error ocurred during mod loading. */
|
||||
public boolean hasErrored(){
|
||||
return minfo.error != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Content c){
|
||||
return Integer.compare(id, c.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return getContentType().name() + "#" + id;
|
||||
}
|
||||
|
||||
public static class ModContentInfo{
|
||||
/** The mod that loaded this piece of content. */
|
||||
public @Nullable LoadedMod mod;
|
||||
/** File that this content was loaded from. */
|
||||
public @Nullable Fi sourceFile;
|
||||
/** The error that occurred during loading, if applicable. Null if no error occurred. */
|
||||
public @Nullable String error;
|
||||
/** Base throwable that caused the error. */
|
||||
public @Nullable Throwable baseError;
|
||||
}
|
||||
}
|
||||
7
core/src/mindustry/ctype/ContentList.java
Normal file
7
core/src/mindustry/ctype/ContentList.java
Normal file
@@ -0,0 +1,7 @@
|
||||
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();
|
||||
}
|
||||
20
core/src/mindustry/ctype/ContentType.java
Normal file
20
core/src/mindustry/ctype/ContentType.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
/** Do not rearrange, ever! */
|
||||
public enum ContentType{
|
||||
item,
|
||||
block,
|
||||
mech,
|
||||
bullet,
|
||||
liquid,
|
||||
status,
|
||||
unit,
|
||||
weather,
|
||||
effect,
|
||||
zone,
|
||||
loadout,
|
||||
typeid,
|
||||
error;
|
||||
|
||||
public static final ContentType[] all = values();
|
||||
}
|
||||
17
core/src/mindustry/ctype/MappableContent.java
Normal file
17
core/src/mindustry/ctype/MappableContent.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
import mindustry.*;
|
||||
|
||||
public abstract class MappableContent extends Content{
|
||||
public final String name;
|
||||
|
||||
public MappableContent(String name){
|
||||
this.name = Vars.content.transformName(name);
|
||||
Vars.content.handleMappableContent(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return name;
|
||||
}
|
||||
}
|
||||
74
core/src/mindustry/ctype/UnlockableContent.java
Normal file
74
core/src/mindustry/ctype/UnlockableContent.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package mindustry.ctype;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import mindustry.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.ui.Cicon;
|
||||
|
||||
/** Base interface for an unlockable content type. */
|
||||
public abstract class UnlockableContent extends MappableContent{
|
||||
/** Localized, formal name. Never null. Set to block name if not found in bundle. */
|
||||
public String localizedName;
|
||||
/** Localized description. May be null. */
|
||||
public String description;
|
||||
/** Icons by Cicon ID.*/
|
||||
protected TextureRegion[] cicons = new TextureRegion[mindustry.ui.Cicon.all.length];
|
||||
|
||||
public UnlockableContent(String name){
|
||||
super(name);
|
||||
|
||||
this.localizedName = Core.bundle.get(getContentType() + "." + this.name + ".name", this.name);
|
||||
this.description = Core.bundle.getOrNull(getContentType() + "." + this.name + ".description");
|
||||
}
|
||||
|
||||
/** Generate any special icons for this content. Called asynchronously.*/
|
||||
@CallSuper
|
||||
public void createIcons(MultiPacker packer){
|
||||
|
||||
}
|
||||
|
||||
/** 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(getContentType().name() + "-" + name,
|
||||
Core.atlas.find(name,
|
||||
Core.atlas.find(name + "1")))));
|
||||
}
|
||||
return cicons[icon.ordinal()];
|
||||
}
|
||||
|
||||
/** This should show all necessary info about this content in the specified table. */
|
||||
public abstract void displayInfo(Table table);
|
||||
|
||||
/** Called when this content is unlocked. Use this to unlock other related content. */
|
||||
public void onUnlock(){
|
||||
}
|
||||
|
||||
/** Whether this content is always hidden in the content info dialog. */
|
||||
public boolean isHidden(){
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Override to make content always unlocked. */
|
||||
public boolean alwaysUnlocked(){
|
||||
return false;
|
||||
}
|
||||
|
||||
public final boolean unlocked(){
|
||||
return Vars.data.isUnlocked(this);
|
||||
}
|
||||
|
||||
/** @return whether this content is unlocked, or the player is in a custom game. */
|
||||
public final boolean unlockedCur(){
|
||||
return Vars.data.isUnlocked(this) || !Vars.world.isZone();
|
||||
}
|
||||
|
||||
public final boolean locked(){
|
||||
return !unlocked();
|
||||
}
|
||||
}
|
||||
95
core/src/mindustry/editor/DrawOperation.java
Executable file
95
core/src/mindustry/editor/DrawOperation.java
Executable file
@@ -0,0 +1,95 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import mindustry.annotations.Annotations.Struct;
|
||||
import arc.struct.LongArray;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.gen.TileOp;
|
||||
import mindustry.world.Block;
|
||||
import mindustry.world.Tile;
|
||||
import mindustry.world.blocks.Floor;
|
||||
|
||||
import static mindustry.Vars.content;
|
||||
|
||||
public class DrawOperation{
|
||||
private MapEditor editor;
|
||||
private LongArray array = new LongArray();
|
||||
|
||||
public DrawOperation(MapEditor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
public boolean isEmpty(){
|
||||
return array.isEmpty();
|
||||
}
|
||||
|
||||
public void addOperation(long op){
|
||||
array.add(op);
|
||||
}
|
||||
|
||||
public void undo(){
|
||||
for(int i = array.size - 1; i >= 0; i--){
|
||||
updateTile(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void redo(){
|
||||
for(int i = 0; i < array.size; i++){
|
||||
updateTile(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTile(int i) {
|
||||
long l = array.get(i);
|
||||
array.set(i, TileOp.get(TileOp.x(l), TileOp.y(l), TileOp.type(l), getTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l))));
|
||||
setTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l), TileOp.value(l));
|
||||
}
|
||||
|
||||
short getTile(Tile tile, byte type){
|
||||
if(type == OpType.floor.ordinal()){
|
||||
return tile.floorID();
|
||||
}else if(type == OpType.block.ordinal()){
|
||||
return tile.blockID();
|
||||
}else if(type == OpType.rotation.ordinal()){
|
||||
return tile.rotation();
|
||||
}else if(type == OpType.team.ordinal()){
|
||||
return tile.getTeamID();
|
||||
}else if(type == OpType.overlay.ordinal()){
|
||||
return tile.overlayID();
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid type.");
|
||||
}
|
||||
|
||||
void setTile(Tile tile, byte type, short to){
|
||||
editor.load(() -> {
|
||||
if(type == OpType.floor.ordinal()){
|
||||
tile.setFloor((Floor)content.block(to));
|
||||
}else if(type == OpType.block.ordinal()){
|
||||
Block block = content.block(to);
|
||||
tile.setBlock(block, tile.getTeam(), tile.rotation());
|
||||
}else if(type == OpType.rotation.ordinal()){
|
||||
tile.rotation(to);
|
||||
}else if(type == OpType.team.ordinal()){
|
||||
tile.setTeam(Team.all[to]);
|
||||
}else if(type == OpType.overlay.ordinal()){
|
||||
tile.setOverlayID(to);
|
||||
}
|
||||
});
|
||||
editor.renderer().updatePoint(tile.x, tile.y);
|
||||
}
|
||||
|
||||
@Struct
|
||||
class TileOpStruct{
|
||||
short x;
|
||||
short y;
|
||||
byte type;
|
||||
short value;
|
||||
}
|
||||
|
||||
public enum OpType{
|
||||
floor,
|
||||
block,
|
||||
rotation,
|
||||
team,
|
||||
overlay
|
||||
}
|
||||
}
|
||||
153
core/src/mindustry/editor/EditorTile.java
Normal file
153
core/src/mindustry/editor/EditorTile.java
Normal file
@@ -0,0 +1,153 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import mindustry.content.Blocks;
|
||||
import mindustry.core.GameState.State;
|
||||
import mindustry.editor.DrawOperation.OpType;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.gen.TileOp;
|
||||
import mindustry.world.Block;
|
||||
import mindustry.world.Tile;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.modules.*;
|
||||
|
||||
import static mindustry.Vars.state;
|
||||
import static mindustry.Vars.ui;
|
||||
|
||||
//TODO somehow remove or replace this class with a more flexible solution
|
||||
public class EditorTile extends Tile{
|
||||
|
||||
public EditorTile(int x, int y, int floor, int overlay, int wall){
|
||||
super(x, y, floor, overlay, wall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFloor(Floor type){
|
||||
if(state.is(State.playing)){
|
||||
super.setFloor(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if(type instanceof OverlayFloor){
|
||||
//don't place on liquids
|
||||
if(!floor.isLiquid){
|
||||
setOverlayID(type.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(floor == type && overlayID() == 0) return;
|
||||
if(overlayID() != 0) op(OpType.overlay, overlayID());
|
||||
if(floor != type) op(OpType.floor, floor.id);
|
||||
super.setFloor(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(Block type){
|
||||
if(state.is(State.playing)){
|
||||
super.setBlock(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if(block == type) return;
|
||||
op(OpType.block, block.id);
|
||||
if(rotation != 0) op(OpType.rotation, rotation);
|
||||
if(team != 0) op(OpType.team, team);
|
||||
super.setBlock(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(Block type, Team team, int rotation){
|
||||
if(state.is(State.playing)){
|
||||
super.setBlock(type, team, rotation);
|
||||
return;
|
||||
}
|
||||
|
||||
setBlock(type);
|
||||
setTeam(team);
|
||||
rotation(rotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTeam(Team team){
|
||||
if(state.is(State.playing)){
|
||||
super.setTeam(team);
|
||||
return;
|
||||
}
|
||||
|
||||
if(getTeamID() == team.ordinal()) return;
|
||||
op(OpType.team, getTeamID());
|
||||
super.setTeam(team);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotation(int rotation){
|
||||
if(state.is(State.playing)){
|
||||
super.rotation(rotation);
|
||||
return;
|
||||
}
|
||||
|
||||
if(rotation == rotation()) return;
|
||||
op(OpType.rotation, rotation());
|
||||
super.rotation(rotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOverlay(Block overlay){
|
||||
setOverlayID(overlay.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOverlayID(short overlay){
|
||||
if(state.is(State.playing)){
|
||||
super.setOverlayID(overlay);
|
||||
return;
|
||||
}
|
||||
|
||||
if(floor.isLiquid) return;
|
||||
if(overlayID() == overlay) return;
|
||||
op(OpType.overlay, this.overlay.id);
|
||||
super.setOverlayID(overlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preChanged(){
|
||||
if(state.is(State.playing)){
|
||||
super.preChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
super.setTeam(Team.derelict);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void changed(){
|
||||
if(state.is(State.playing)){
|
||||
super.changed();
|
||||
return;
|
||||
}
|
||||
|
||||
entity = null;
|
||||
|
||||
if(block == null){
|
||||
block = Blocks.air;
|
||||
}
|
||||
|
||||
if(floor == null){
|
||||
floor = (Floor)Blocks.air;
|
||||
}
|
||||
|
||||
Block block = block();
|
||||
|
||||
if(block.hasEntity()){
|
||||
entity = block.newEntity().init(this, false);
|
||||
entity.cons = new ConsumeModule(entity);
|
||||
if(block.hasItems) entity.items = new ItemModule();
|
||||
if(block.hasLiquids) entity.liquids = new LiquidModule();
|
||||
if(block.hasPower) entity.power = new PowerModule();
|
||||
}
|
||||
}
|
||||
|
||||
private void op(OpType type, short value){
|
||||
ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
|
||||
}
|
||||
}
|
||||
247
core/src/mindustry/editor/EditorTool.java
Normal file
247
core/src/mindustry/editor/EditorTool.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.struct.IntArray;
|
||||
import arc.func.*;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.Bresenham2;
|
||||
import arc.util.Structs;
|
||||
import mindustry.Vars;
|
||||
import mindustry.content.Blocks;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.BlockPart;
|
||||
|
||||
public enum EditorTool{
|
||||
zoom,
|
||||
pick{
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
|
||||
|
||||
Tile tile = editor.tile(x, y).link();
|
||||
editor.drawBlock = tile.block() == Blocks.air ? tile.overlay() == Blocks.air ? tile.floor() : tile.overlay() : tile.block();
|
||||
}
|
||||
},
|
||||
line("replace", "orthogonal"){
|
||||
|
||||
@Override
|
||||
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){
|
||||
//straight
|
||||
if(mode == 1){
|
||||
if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){
|
||||
y2 = y1;
|
||||
}else{
|
||||
x2 = x1;
|
||||
}
|
||||
}
|
||||
|
||||
Bresenham2.line(x1, y1, x2, y2, (x, y) -> {
|
||||
if(mode == 0){
|
||||
//replace
|
||||
editor.drawBlocksReplace(x, y);
|
||||
}else{
|
||||
//normal
|
||||
editor.drawBlocks(x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
pencil("replace", "square", "drawteams"){
|
||||
{
|
||||
edit = true;
|
||||
draggable = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
if(mode == -1){
|
||||
//normal mode
|
||||
editor.drawBlocks(x, y);
|
||||
}else if(mode == 0){
|
||||
//replace mode
|
||||
editor.drawBlocksReplace(x, y);
|
||||
}else if(mode == 1){
|
||||
//square mode
|
||||
editor.drawBlocks(x, y, true, tile -> true);
|
||||
}else if(mode == 2){
|
||||
//draw teams
|
||||
editor.drawCircle(x, y, tile -> tile.link().setTeam(editor.drawTeam));
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
eraser("eraseores"){
|
||||
{
|
||||
edit = true;
|
||||
draggable = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
editor.drawCircle(x, y, tile -> {
|
||||
if(mode == -1){
|
||||
//erase block
|
||||
Vars.world.removeBlock(tile);
|
||||
}else if(mode == 0){
|
||||
//erase ore
|
||||
tile.clearOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
fill("replaceall", "fillteams"){
|
||||
{
|
||||
edit = true;
|
||||
}
|
||||
|
||||
IntArray stack = new IntArray();
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
|
||||
Tile tile = editor.tile(x, y);
|
||||
|
||||
if(editor.drawBlock.isMultiblock()){
|
||||
//don't fill multiblocks, thanks
|
||||
pencil.touched(editor, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
//mode 0 or 1, fill everything with the floor/tile or replace it
|
||||
if(mode == 0 || mode == -1){
|
||||
//can't fill parts or multiblocks
|
||||
if(tile.block() instanceof BlockPart || tile.block().isMultiblock()){
|
||||
return;
|
||||
}
|
||||
|
||||
Boolf<Tile> tester;
|
||||
Cons<Tile> setter;
|
||||
|
||||
if(editor.drawBlock.isOverlay()){
|
||||
Block dest = tile.overlay();
|
||||
if(dest == editor.drawBlock) return;
|
||||
tester = t -> t.overlay() == dest;
|
||||
setter = t -> t.setOverlay(editor.drawBlock);
|
||||
}else if(editor.drawBlock.isFloor()){
|
||||
Block dest = tile.floor();
|
||||
if(dest == editor.drawBlock) return;
|
||||
tester = t -> t.floor() == dest;
|
||||
setter = t -> t.setFloorUnder(editor.drawBlock.asFloor());
|
||||
}else{
|
||||
Block dest = tile.block();
|
||||
if(dest == editor.drawBlock) return;
|
||||
tester = t -> t.block() == dest;
|
||||
setter = t -> t.setBlock(editor.drawBlock, editor.drawTeam);
|
||||
}
|
||||
|
||||
//replace only when the mode is 0 using the specified functions
|
||||
fill(editor, 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.link().synthetic()){
|
||||
Team dest = tile.getTeam();
|
||||
if(dest == editor.drawTeam) return;
|
||||
fill(editor, x, y, false, t -> t.getTeamID() == dest.ordinal() && t.link().synthetic(), t -> t.setTeam(editor.drawTeam));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fill(MapEditor editor, int x, int y, boolean replace, Boolf<Tile> tester, Cons<Tile> filler){
|
||||
int width = editor.width(), height = editor.height();
|
||||
|
||||
if(replace){
|
||||
//just do it on everything
|
||||
for(int cx = 0; cx < width; cx++){
|
||||
for(int cy = 0; cy < height; cy++){
|
||||
Tile tile = editor.tile(cx, cy);
|
||||
if(tester.get(tile)){
|
||||
filler.get(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}else{
|
||||
//perform flood fill
|
||||
int x1;
|
||||
|
||||
stack.clear();
|
||||
stack.add(Pos.get(x, y));
|
||||
|
||||
while(stack.size > 0){
|
||||
int popped = stack.pop();
|
||||
x = Pos.x(popped);
|
||||
y = Pos.y(popped);
|
||||
|
||||
x1 = x;
|
||||
while(x1 >= 0 && tester.get(editor.tile(x1, y))) x1--;
|
||||
x1++;
|
||||
boolean spanAbove = false, spanBelow = false;
|
||||
while(x1 < width && tester.get(editor.tile(x1, y))){
|
||||
filler.get(editor.tile(x1, y));
|
||||
|
||||
if(!spanAbove && y > 0 && tester.get(editor.tile(x1, y - 1))){
|
||||
stack.add(Pos.get(x1, y - 1));
|
||||
spanAbove = true;
|
||||
}else if(spanAbove && !tester.get(editor.tile(x1, y - 1))){
|
||||
spanAbove = false;
|
||||
}
|
||||
|
||||
if(!spanBelow && y < height - 1 && tester.get(editor.tile(x1, y + 1))){
|
||||
stack.add(Pos.get(x1, y + 1));
|
||||
spanBelow = true;
|
||||
}else if(spanBelow && y < height - 1 && !tester.get(editor.tile(x1, y + 1))){
|
||||
spanBelow = false;
|
||||
}
|
||||
x1++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
spray("replace"){
|
||||
final double chance = 0.012;
|
||||
|
||||
{
|
||||
edit = true;
|
||||
draggable = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touched(MapEditor editor, int x, int y){
|
||||
|
||||
//floor spray
|
||||
if(editor.drawBlock.isFloor()){
|
||||
editor.drawCircle(x, y, tile -> {
|
||||
if(Mathf.chance(chance)){
|
||||
tile.setFloor(editor.drawBlock.asFloor());
|
||||
}
|
||||
});
|
||||
}else if(mode == 0){ //replace-only mode, doesn't affect air
|
||||
editor.drawBlocks(x, y, tile -> Mathf.chance(chance) && tile.block() != Blocks.air);
|
||||
}else{
|
||||
editor.drawBlocks(x, y, tile -> Mathf.chance(chance));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** All the internal alternate placement modes of this tool. */
|
||||
public final String[] altModes;
|
||||
/** The current alternate placement mode. -1 is the standard mode, no changes.*/
|
||||
public int mode = -1;
|
||||
/** Whether this tool causes canvas changes when touched.*/
|
||||
public boolean edit;
|
||||
/** Whether this tool should be dragged across the canvas when the mouse moves.*/
|
||||
public boolean draggable;
|
||||
|
||||
EditorTool(){
|
||||
this(new String[]{});
|
||||
}
|
||||
|
||||
EditorTool(String... altModes){
|
||||
this.altModes = altModes;
|
||||
}
|
||||
|
||||
public void touched(MapEditor editor, int x, int y){}
|
||||
|
||||
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){}
|
||||
}
|
||||
341
core/src/mindustry/editor/MapEditor.java
Normal file
341
core/src/mindustry/editor/MapEditor.java
Normal file
@@ -0,0 +1,341 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.struct.StringMap;
|
||||
import arc.files.Fi;
|
||||
import arc.func.Cons;
|
||||
import arc.func.Boolf;
|
||||
import arc.graphics.Pixmap;
|
||||
import arc.math.Mathf;
|
||||
import arc.util.Structs;
|
||||
import mindustry.content.Blocks;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.gen.TileOp;
|
||||
import mindustry.io.LegacyMapIO;
|
||||
import mindustry.io.MapIO;
|
||||
import mindustry.maps.Map;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.BlockPart;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class MapEditor{
|
||||
public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20};
|
||||
|
||||
private final Context context = new Context();
|
||||
private StringMap tags = new StringMap();
|
||||
private MapRenderer renderer = new MapRenderer(this);
|
||||
|
||||
private OperationStack stack = new OperationStack();
|
||||
private DrawOperation currentOp;
|
||||
private boolean loading;
|
||||
|
||||
public int brushSize = 1;
|
||||
public int rotation;
|
||||
public Block drawBlock = Blocks.stone;
|
||||
public Team drawTeam = Team.sharded;
|
||||
|
||||
public StringMap getTags(){
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void beginEdit(int width, int height){
|
||||
reset();
|
||||
|
||||
loading = true;
|
||||
createTiles(width, height);
|
||||
renderer.resize(width(), height());
|
||||
loading = false;
|
||||
}
|
||||
|
||||
public void beginEdit(Map map){
|
||||
reset();
|
||||
|
||||
loading = true;
|
||||
tags.putAll(map.tags);
|
||||
if(map.file.parent().parent().name().equals("1127400") && steam){
|
||||
tags.put("steamid", map.file.parent().name());
|
||||
}
|
||||
MapIO.loadMap(map, context);
|
||||
checkLinkedTiles();
|
||||
renderer.resize(width(), height());
|
||||
loading = false;
|
||||
}
|
||||
|
||||
public void beginEdit(Pixmap pixmap){
|
||||
reset();
|
||||
|
||||
createTiles(pixmap.getWidth(), pixmap.getHeight());
|
||||
load(() -> LegacyMapIO.readPixmap(pixmap, tiles()));
|
||||
renderer.resize(width(), height());
|
||||
}
|
||||
|
||||
//adds missing blockparts
|
||||
public void checkLinkedTiles(){
|
||||
Tile[][] tiles = world.getTiles();
|
||||
|
||||
//clear block parts first
|
||||
for(int x = 0; x < width(); x++){
|
||||
for(int y = 0; y < height(); y++){
|
||||
if(tiles[x][y].block() instanceof BlockPart){
|
||||
tiles[x][y].setBlock(Blocks.air);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//set up missing blockparts
|
||||
for(int x = 0; x < width(); x++){
|
||||
for(int y = 0; y < height(); y++){
|
||||
if(tiles[x][y].block().isMultiblock()){
|
||||
world.setBlock(tiles[x][y], tiles[x][y].block(), tiles[x][y].getTeam());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void load(Runnable r){
|
||||
loading = true;
|
||||
r.run();
|
||||
loading = false;
|
||||
}
|
||||
|
||||
/** Creates a 2-D array of EditorTiles with stone as the floor block. */
|
||||
private void createTiles(int width, int height){
|
||||
Tile[][] tiles = world.createTiles(width, height);
|
||||
|
||||
for(int x = 0; x < width; x++){
|
||||
for(int y = 0; y < height; y++){
|
||||
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map createMap(Fi file){
|
||||
return new Map(file, width(), height(), new StringMap(tags), true);
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
clearOp();
|
||||
brushSize = 1;
|
||||
drawBlock = Blocks.stone;
|
||||
tags = new StringMap();
|
||||
}
|
||||
|
||||
public Tile[][] tiles(){
|
||||
return world.getTiles();
|
||||
}
|
||||
|
||||
public Tile tile(int x, int y){
|
||||
return world.rawTile(x, y);
|
||||
}
|
||||
|
||||
public int width(){
|
||||
return world.width();
|
||||
}
|
||||
|
||||
public int height(){
|
||||
return world.height();
|
||||
}
|
||||
|
||||
public void drawBlocksReplace(int x, int y){
|
||||
drawBlocks(x, y, tile -> tile.block() != Blocks.air || drawBlock.isFloor());
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y){
|
||||
drawBlocks(x, y, false, tile -> true);
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y, Boolf<Tile> tester){
|
||||
drawBlocks(x, y, false, tester);
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y, boolean square, 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);
|
||||
|
||||
int offsetx = -(drawBlock.size - 1) / 2;
|
||||
int offsety = -(drawBlock.size - 1) / 2;
|
||||
|
||||
for(int dx = 0; dx < drawBlock.size; dx++){
|
||||
for(int dy = 0; dy < drawBlock.size; dy++){
|
||||
int worldx = dx + offsetx + x;
|
||||
int worldy = dy + offsety + y;
|
||||
|
||||
if(Structs.inBounds(worldx, worldy, width(), height())){
|
||||
Tile tile = tile(worldx, worldy);
|
||||
|
||||
Block block = tile.block();
|
||||
|
||||
//bail out if there's anything blocking the way
|
||||
if(block.isMultiblock() || block instanceof BlockPart){
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.updatePoint(worldx, worldy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
world.setBlock(tile(x, y), drawBlock, drawTeam);
|
||||
}else{
|
||||
boolean isFloor = drawBlock.isFloor() && drawBlock != Blocks.air;
|
||||
|
||||
Cons<Tile> drawer = tile -> {
|
||||
if(!tester.get(tile)) return;
|
||||
|
||||
//remove linked tiles blocking the way
|
||||
if(!isFloor && (tile.isLinked() || tile.block().isMultiblock())){
|
||||
world.removeBlock(tile.link());
|
||||
}
|
||||
|
||||
if(isFloor){
|
||||
tile.setFloor(drawBlock.asFloor());
|
||||
}else{
|
||||
tile.setBlock(drawBlock);
|
||||
if(drawBlock.synthetic()){
|
||||
tile.setTeam(drawTeam);
|
||||
}
|
||||
if(drawBlock.rotate){
|
||||
tile.rotation((byte)rotation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(square){
|
||||
drawSquare(x, y, drawer);
|
||||
}else{
|
||||
drawCircle(x, y, drawer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void drawCircle(int x, int y, Cons<Tile> drawer){
|
||||
for(int rx = -brushSize; rx <= brushSize; rx++){
|
||||
for(int ry = -brushSize; ry <= brushSize; ry++){
|
||||
if(Mathf.dst2(rx, ry) <= (brushSize - 0.5f) * (brushSize - 0.5f)){
|
||||
int wx = x + rx, wy = y + ry;
|
||||
|
||||
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
|
||||
continue;
|
||||
}
|
||||
|
||||
drawer.get(tile(wx, wy));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 wx = x + rx, wy = y + ry;
|
||||
|
||||
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
|
||||
continue;
|
||||
}
|
||||
|
||||
drawer.get(tile(wx, wy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MapRenderer renderer(){
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public void resize(int width, int height){
|
||||
clearOp();
|
||||
|
||||
Tile[][] previous = world.getTiles();
|
||||
int offsetX = -(width - width()) / 2, offsetY = -(height - height()) / 2;
|
||||
loading = true;
|
||||
|
||||
Tile[][] tiles = world.createTiles(width, height);
|
||||
for(int x = 0; x < width; x++){
|
||||
for(int y = 0; y < height; y++){
|
||||
int px = offsetX + x, py = offsetY + y;
|
||||
if(Structs.inBounds(px, py, previous.length, previous[0].length)){
|
||||
tiles[x][y] = previous[px][py];
|
||||
tiles[x][y].x = (short)x;
|
||||
tiles[x][y].y = (short)y;
|
||||
}else{
|
||||
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderer.resize(width, height);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
public void clearOp(){
|
||||
stack.clear();
|
||||
}
|
||||
|
||||
public void undo(){
|
||||
if(stack.canUndo()){
|
||||
stack.undo();
|
||||
}
|
||||
}
|
||||
|
||||
public void redo(){
|
||||
if(stack.canRedo()){
|
||||
stack.redo();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canUndo(){
|
||||
return stack.canUndo();
|
||||
}
|
||||
|
||||
public boolean canRedo(){
|
||||
return stack.canRedo();
|
||||
}
|
||||
|
||||
public void flushOp(){
|
||||
if(currentOp == null || currentOp.isEmpty()) return;
|
||||
stack.add(currentOp);
|
||||
currentOp = null;
|
||||
}
|
||||
|
||||
public void addTileOp(long data){
|
||||
if(loading) return;
|
||||
|
||||
if(currentOp == null) currentOp = new DrawOperation(this);
|
||||
currentOp.addOperation(data);
|
||||
|
||||
renderer.updatePoint(TileOp.x(data), TileOp.y(data));
|
||||
}
|
||||
|
||||
class Context implements WorldContext{
|
||||
@Override
|
||||
public Tile tile(int x, int y){
|
||||
return world.tile(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
world.createTiles(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
|
||||
return (tiles()[x][y] = new EditorTile(x, y, floorID, overlayID, wallID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerating(){
|
||||
return world.isGenerating();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin(){
|
||||
world.beginMapLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(){
|
||||
world.endMapLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
730
core/src/mindustry/editor/MapEditorDialog.java
Normal file
730
core/src/mindustry/editor/MapEditorDialog.java
Normal file
@@ -0,0 +1,730 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.actions.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.Cicon;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
|
||||
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 ScrollPane pane;
|
||||
private FloatingDialog menu;
|
||||
private Rules lastSavedRules;
|
||||
private boolean saved = false;
|
||||
private boolean shownWithMap = false;
|
||||
private Array<Block> blocksOut = new Array<>();
|
||||
|
||||
public MapEditorDialog(){
|
||||
super("");
|
||||
|
||||
background(Styles.black);
|
||||
|
||||
editor = new MapEditor();
|
||||
view = new MapView(editor);
|
||||
infoDialog = new MapInfoDialog(editor);
|
||||
generateDialog = new MapGenerateDialog(editor, true);
|
||||
|
||||
menu = new FloatingDialog("$menu");
|
||||
menu.addCloseButton();
|
||||
|
||||
float swidth = 180f;
|
||||
|
||||
menu.cont.table(t -> {
|
||||
t.defaults().size(swidth, 60f).padBottom(5).padRight(5).padLeft(5);
|
||||
|
||||
t.addImageTextButton("$editor.savemap", Icon.floppy16Small, this::save);
|
||||
|
||||
t.addImageTextButton("$editor.mapinfo", Icon.pencilSmall, () -> {
|
||||
infoDialog.show();
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.row();
|
||||
|
||||
t.addImageTextButton("$editor.generate", Icon.editorSmall, () -> {
|
||||
generateDialog.show(generateDialog::applyToEditor);
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.addImageTextButton("$editor.resize", Icon.resizeSmall, () -> {
|
||||
resizeDialog.show();
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.row();
|
||||
|
||||
t.addImageTextButton("$editor.import", Icon.loadMapSmall, () ->
|
||||
createDialog("$editor.import",
|
||||
"$editor.importmap", "$editor.importmap.description", Icon.loadMap, (Runnable)loadDialog::show,
|
||||
"$editor.importfile", "$editor.importfile.description", Icon.file, (Runnable)() ->
|
||||
platform.showFileChooser(true, mapExtension, file -> ui.loadAnd(() -> {
|
||||
maps.tryCatchMapError(() -> {
|
||||
if(MapIO.isImage(file)){
|
||||
ui.showInfo("$editor.errorimage");
|
||||
}else{
|
||||
editor.beginEdit(MapIO.createMap(file, true));
|
||||
}
|
||||
});
|
||||
})),
|
||||
|
||||
"$editor.importimage", "$editor.importimage.description", Icon.fileImage, (Runnable)() ->
|
||||
platform.showFileChooser(true, "png", file ->
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
Pixmap pixmap = new Pixmap(file);
|
||||
editor.beginEdit(pixmap);
|
||||
pixmap.dispose();
|
||||
}catch(Exception e){
|
||||
ui.showException("$editor.errorload", e);
|
||||
Log.err(e);
|
||||
}
|
||||
})))
|
||||
);
|
||||
|
||||
t.addImageTextButton("$editor.export", Icon.saveMapSmall, () -> {
|
||||
if(!ios){
|
||||
platform.showFileChooser(false, mapExtension, file -> {
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
if(!editor.getTags().containsKey("name")){
|
||||
editor.getTags().put("name", file.nameWithoutExtension());
|
||||
}
|
||||
MapIO.writeMap(file, editor.createMap(file));
|
||||
}catch(Exception e){
|
||||
ui.showException("$editor.errorsave", e);
|
||||
Log.err(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}else{
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
Fi result = Core.files.local(editor.getTags().get("name", "unknown") + "." + mapExtension);
|
||||
MapIO.writeMap(result, editor.createMap(result));
|
||||
platform.shareFile(result);
|
||||
}catch(Exception e){
|
||||
ui.showException("$editor.errorsave", e);
|
||||
Log.err(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
if(steam){
|
||||
menu.cont.addImageTextButton("$editor.publish.workshop", Icon.linkSmall, () -> {
|
||||
Map builtin = maps.all().find(m -> m.name().equals(editor.getTags().get("name", "").trim()));
|
||||
|
||||
if(editor.getTags().containsKey("steamid") && builtin != null && !builtin.custom){
|
||||
platform.viewListingID(editor.getTags().get("steamid"));
|
||||
return;
|
||||
}
|
||||
|
||||
Map map = save();
|
||||
|
||||
if(editor.getTags().containsKey("steamid") && map != null){
|
||||
platform.viewListing(map);
|
||||
return;
|
||||
}
|
||||
|
||||
if(map == null) return;
|
||||
|
||||
if(map.tags.get("description", "").length() < 4){
|
||||
ui.showErrorMessage("$editor.nodescription");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Structs.contains(Gamemode.all, g -> g.valid(map))){
|
||||
ui.showErrorMessage("$map.nospawn");
|
||||
return;
|
||||
}
|
||||
|
||||
platform.publish(map);
|
||||
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.getTags().containsKey("steamid") ? editor.getTags().get("author").equals(player.name) ? "$workshop.listing" : "$view.workshop" : "$editor.publish.workshop"));
|
||||
|
||||
menu.cont.row();
|
||||
}
|
||||
|
||||
menu.cont.addImageTextButton("$editor.ingame", Icon.arrowSmall, this::playtest).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f);
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.addImageTextButton("$quit", Icon.backSmall, () -> {
|
||||
tryExit();
|
||||
menu.hide();
|
||||
}).size(swidth * 2f + 10, 60f);
|
||||
|
||||
resizeDialog = new MapResizeDialog(editor, (x, y) -> {
|
||||
if(!(editor.width() == x && editor.height() == y)){
|
||||
ui.loadAnd(() -> {
|
||||
editor.resize(x, y);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loadDialog = new MapLoadDialog(map -> ui.loadAnd(() -> {
|
||||
try{
|
||||
editor.beginEdit(map);
|
||||
}catch(Exception e){
|
||||
ui.showException("$editor.errorload", e);
|
||||
Log.err(e);
|
||||
}
|
||||
}));
|
||||
|
||||
setFillParent(true);
|
||||
|
||||
clearChildren();
|
||||
margin(0);
|
||||
|
||||
update(() -> {
|
||||
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
|
||||
return;
|
||||
}
|
||||
|
||||
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
|
||||
doInput();
|
||||
}
|
||||
});
|
||||
|
||||
shown(() -> {
|
||||
|
||||
saved = true;
|
||||
if(!Core.settings.getBool("landscape")) platform.beginForceLandscape();
|
||||
editor.clearOp();
|
||||
Core.scene.setScrollFocus(view);
|
||||
if(!shownWithMap){
|
||||
//clear units, rules and other unnecessary stuff
|
||||
logic.reset();
|
||||
state.rules = new Rules();
|
||||
editor.beginEdit(200, 200);
|
||||
}
|
||||
shownWithMap = false;
|
||||
|
||||
Time.runTask(10f, platform::updateRPC);
|
||||
});
|
||||
|
||||
hidden(() -> {
|
||||
editor.clearOp();
|
||||
platform.updateRPC();
|
||||
if(!Core.settings.getBool("landscape")) platform.endForceLandscape();
|
||||
});
|
||||
|
||||
shown(this::build);
|
||||
}
|
||||
|
||||
public void resumeEditing(){
|
||||
state.set(State.menu);
|
||||
shownWithMap = true;
|
||||
show();
|
||||
state.rules = (lastSavedRules == null ? new Rules() : lastSavedRules);
|
||||
lastSavedRules = null;
|
||||
editor.renderer().updateAll();
|
||||
}
|
||||
|
||||
private void playtest(){
|
||||
menu.hide();
|
||||
ui.loadAnd(() -> {
|
||||
lastSavedRules = state.rules;
|
||||
hide();
|
||||
//only reset the player; logic.reset() will clear entities, which we do not want
|
||||
state.teams = new Teams();
|
||||
player.reset();
|
||||
state.rules = Gamemode.editor.apply(lastSavedRules.copy());
|
||||
state.rules.zone = null;
|
||||
world.setMap(new Map(StringMap.of(
|
||||
"name", "Editor Playtesting",
|
||||
"width", editor.width(),
|
||||
"height", editor.height()
|
||||
)));
|
||||
world.endMapLoad();
|
||||
//add entities so they update. is this really needed?
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
Tile tile = world.rawTile(x, y);
|
||||
if(tile.entity != null){
|
||||
tile.entity.add();
|
||||
}
|
||||
}
|
||||
}
|
||||
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
|
||||
player.setDead(false);
|
||||
logic.play();
|
||||
});
|
||||
}
|
||||
|
||||
public @Nullable Map save(){
|
||||
boolean isEditor = state.rules.editor;
|
||||
state.rules.editor = false;
|
||||
String name = editor.getTags().get("name", "").trim();
|
||||
editor.getTags().put("rules", JsonIO.write(state.rules));
|
||||
editor.getTags().remove("width");
|
||||
editor.getTags().remove("height");
|
||||
player.dead = true;
|
||||
|
||||
Map returned = null;
|
||||
|
||||
if(name.isEmpty()){
|
||||
infoDialog.show();
|
||||
Core.app.post(() -> ui.showErrorMessage("$editor.save.noname"));
|
||||
}else{
|
||||
Map map = maps.all().find(m -> m.name().equals(name));
|
||||
if(map != null && !map.custom){
|
||||
handleSaveBuiltin(map);
|
||||
}else{
|
||||
returned = maps.saveMap(editor.getTags());
|
||||
ui.showInfoFade("$editor.saved");
|
||||
}
|
||||
}
|
||||
|
||||
menu.hide();
|
||||
saved = true;
|
||||
state.rules.editor = isEditor;
|
||||
return returned;
|
||||
}
|
||||
|
||||
/** Called when a built-in map save is attempted.*/
|
||||
protected void handleSaveBuiltin(Map map){
|
||||
ui.showErrorMessage("$editor.save.overwrite");
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument format:
|
||||
* 0) button name
|
||||
* 1) description
|
||||
* 2) icon name
|
||||
* 3) listener
|
||||
*/
|
||||
private void createDialog(String title, Object... arguments){
|
||||
FloatingDialog dialog = new FloatingDialog(title);
|
||||
|
||||
float h = 90f;
|
||||
|
||||
dialog.cont.defaults().size(360f, h).padBottom(5).padRight(5).padLeft(5);
|
||||
|
||||
for(int i = 0; i < arguments.length; i += 4){
|
||||
String name = (String)arguments[i];
|
||||
String description = (String)arguments[i + 1];
|
||||
Drawable iconname = (Drawable)arguments[i + 2];
|
||||
Runnable listenable = (Runnable)arguments[i + 3];
|
||||
|
||||
TextButton button = dialog.cont.addButton(name, () -> {
|
||||
listenable.run();
|
||||
dialog.hide();
|
||||
menu.hide();
|
||||
}).left().margin(0).get();
|
||||
|
||||
button.clearChildren();
|
||||
button.addImage(iconname).padLeft(10);
|
||||
button.table(t -> {
|
||||
t.add(name).growX().wrap();
|
||||
t.row();
|
||||
t.add(description).color(Color.gray).growX().wrap();
|
||||
}).growX().pad(10f).padLeft(5);
|
||||
|
||||
button.row();
|
||||
|
||||
dialog.cont.row();
|
||||
}
|
||||
|
||||
dialog.addCloseButton();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog show(){
|
||||
return super.show(Core.scene, Actions.sequence(Actions.alpha(1f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(){
|
||||
super.hide(Actions.sequence(Actions.alpha(0f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
editor.renderer().dispose();
|
||||
}
|
||||
|
||||
public void beginEditMap(Fi file){
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
shownWithMap = true;
|
||||
editor.beginEdit(MapIO.createMap(file, true));
|
||||
show();
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showException("$editor.errorload", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MapView getView(){
|
||||
return view;
|
||||
}
|
||||
|
||||
public MapGenerateDialog getGenerateDialog(){
|
||||
return generateDialog;
|
||||
}
|
||||
|
||||
public void resetSaved(){
|
||||
saved = false;
|
||||
}
|
||||
|
||||
public boolean hasPane(){
|
||||
return Core.scene.getScrollFocus() == pane || Core.scene.getKeyboardFocus() != this;
|
||||
}
|
||||
|
||||
public void build(){
|
||||
float size = 60f;
|
||||
|
||||
clearChildren();
|
||||
table(cont -> {
|
||||
cont.left();
|
||||
|
||||
cont.table(mid -> {
|
||||
mid.top();
|
||||
|
||||
Table tools = new Table().top();
|
||||
|
||||
ButtonGroup<ImageButton> group = new ButtonGroup<>();
|
||||
Table[] lastTable = {null};
|
||||
|
||||
Cons<EditorTool> addTool = tool -> {
|
||||
|
||||
ImageButton button = new ImageButton(Core.atlas.drawable("icon-" + tool.name() + "-small"), Styles.clearTogglei);
|
||||
button.clicked(() -> {
|
||||
view.setTool(tool);
|
||||
if(lastTable[0] != null){
|
||||
lastTable[0].remove();
|
||||
}
|
||||
});
|
||||
button.update(() -> button.setChecked(view.getTool() == tool));
|
||||
group.add(button);
|
||||
|
||||
if(tool.altModes.length > 0){
|
||||
button.clicked(l -> {
|
||||
if(!mobile){
|
||||
//desktop: rightclick
|
||||
l.setButton(KeyCode.MOUSE_RIGHT);
|
||||
}
|
||||
}, e -> {
|
||||
//need to double tap
|
||||
if(mobile && e.getTapCount() < 2){
|
||||
return;
|
||||
}
|
||||
|
||||
if(lastTable[0] != null){
|
||||
lastTable[0].remove();
|
||||
}
|
||||
|
||||
Table table = new Table(Styles.black9);
|
||||
table.defaults().size(300f, 70f);
|
||||
|
||||
for(int i = 0; i < tool.altModes.length; i++){
|
||||
int mode = i;
|
||||
String name = tool.altModes[i];
|
||||
|
||||
table.addButton(b -> {
|
||||
b.left();
|
||||
b.marginLeft(6);
|
||||
b.setStyle(Styles.clearTogglet);
|
||||
b.add(Core.bundle.get("toolmode." + name)).left();
|
||||
b.row();
|
||||
b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.lightGray).left();
|
||||
}, () -> {
|
||||
tool.mode = (tool.mode == mode ? -1 : mode);
|
||||
table.remove();
|
||||
}).update(b -> b.setChecked(tool.mode == mode));
|
||||
table.row();
|
||||
}
|
||||
|
||||
table.update(() -> {
|
||||
Vector2 v = button.localToStageCoordinates(Tmp.v1.setZero());
|
||||
table.setPosition(v.x, v.y, Align.topLeft);
|
||||
if(!isShown()){
|
||||
table.remove();
|
||||
lastTable[0] = null;
|
||||
}
|
||||
});
|
||||
|
||||
table.pack();
|
||||
table.act(Core.graphics.getDeltaTime());
|
||||
|
||||
addChild(table);
|
||||
lastTable[0] = table;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Label mode = new Label("");
|
||||
mode.setColor(Pal.remove);
|
||||
mode.update(() -> mode.setText(tool.mode == -1 ? "" : "M" + (tool.mode + 1) + " "));
|
||||
mode.setAlignment(Align.bottomRight, Align.bottomRight);
|
||||
mode.touchable(Touchable.disabled);
|
||||
|
||||
tools.stack(button, mode);
|
||||
};
|
||||
|
||||
tools.defaults().size(size, size);
|
||||
|
||||
tools.addImageButton(Icon.menuLargeSmall, Styles.cleari, menu::show);
|
||||
|
||||
ImageButton grid = tools.addImageButton(Icon.gridSmall, Styles.clearTogglei, () -> view.setGrid(!view.isGrid())).get();
|
||||
|
||||
addTool.get(EditorTool.zoom);
|
||||
|
||||
tools.row();
|
||||
|
||||
ImageButton undo = tools.addImageButton(Icon.undoSmall, Styles.cleari, editor::undo).get();
|
||||
ImageButton redo = tools.addImageButton(Icon.redoSmall, Styles.cleari, editor::redo).get();
|
||||
|
||||
addTool.get(EditorTool.pick);
|
||||
|
||||
tools.row();
|
||||
|
||||
undo.setDisabled(() -> !editor.canUndo());
|
||||
redo.setDisabled(() -> !editor.canRedo());
|
||||
|
||||
undo.update(() -> undo.getImage().setColor(undo.isDisabled() ? Color.gray : Color.white));
|
||||
redo.update(() -> redo.getImage().setColor(redo.isDisabled() ? Color.gray : Color.white));
|
||||
grid.update(() -> grid.setChecked(view.isGrid()));
|
||||
|
||||
addTool.get(EditorTool.line);
|
||||
addTool.get(EditorTool.pencil);
|
||||
addTool.get(EditorTool.eraser);
|
||||
|
||||
tools.row();
|
||||
|
||||
addTool.get(EditorTool.fill);
|
||||
addTool.get(EditorTool.spray);
|
||||
|
||||
ImageButton rotate = tools.addImageButton(Icon.arrow16Small, Styles.cleari, () -> editor.rotation = (editor.rotation + 1) % 4).get();
|
||||
rotate.getImage().update(() -> {
|
||||
rotate.getImage().setRotation(editor.rotation * 90);
|
||||
rotate.getImage().setOrigin(Align.center);
|
||||
});
|
||||
|
||||
tools.row();
|
||||
|
||||
tools.table(Tex.underline, t -> t.add("$editor.teams"))
|
||||
.colspan(3).height(40).width(size * 3f + 3f).padBottom(3);
|
||||
|
||||
tools.row();
|
||||
|
||||
ButtonGroup<ImageButton> teamgroup = new ButtonGroup<>();
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(Team team : Team.all){
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglePartiali);
|
||||
button.margin(4f);
|
||||
button.getImageCell().grow();
|
||||
button.getStyle().imageUpColor = team.color;
|
||||
button.clicked(() -> editor.drawTeam = team);
|
||||
button.update(() -> button.setChecked(editor.drawTeam == team));
|
||||
teamgroup.add(button);
|
||||
tools.add(button);
|
||||
|
||||
if(i++ % 3 == 2) tools.row();
|
||||
}
|
||||
|
||||
mid.add(tools).top().padBottom(-6);
|
||||
|
||||
mid.row();
|
||||
|
||||
mid.table(Tex.underline, t -> {
|
||||
Slider slider = new Slider(0, MapEditor.brushSizes.length - 1, 1, false);
|
||||
slider.moved(f -> editor.brushSize = MapEditor.brushSizes[(int)(float)f]);
|
||||
for(int j = 0; j < MapEditor.brushSizes.length; j++){
|
||||
if(MapEditor.brushSizes[j] == editor.brushSize){
|
||||
slider.setValue(j);
|
||||
}
|
||||
}
|
||||
|
||||
t.top();
|
||||
t.add("$editor.brush");
|
||||
t.row();
|
||||
t.add(slider).width(size * 3f - 20).padTop(4f);
|
||||
}).padTop(5).growX().top();
|
||||
|
||||
}).margin(0).left().growY();
|
||||
|
||||
|
||||
cont.table(t -> t.add(view).grow()).grow();
|
||||
|
||||
cont.table(this::addBlockSelection).right().growY();
|
||||
|
||||
}).grow();
|
||||
}
|
||||
|
||||
private void doInput(){
|
||||
|
||||
if(Core.input.ctrl()){
|
||||
//alt mode select
|
||||
for(int i = 0; i < view.getTool().altModes.length + 1; i++){
|
||||
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
|
||||
view.getTool().mode = i - 1;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
//tool select
|
||||
for(int i = 0; i < EditorTool.values().length; i++){
|
||||
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
|
||||
view.setTool(EditorTool.values()[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(Core.input.keyTap(KeyCode.ESCAPE)){
|
||||
if(!menu.isShown()){
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.R)){
|
||||
editor.rotation = Mathf.mod(editor.rotation + 1, 4);
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.E)){
|
||||
editor.rotation = Mathf.mod(editor.rotation - 1, 4);
|
||||
}
|
||||
|
||||
//ctrl keys (undo, redo, save)
|
||||
if(Core.input.ctrl()){
|
||||
if(Core.input.keyTap(KeyCode.Z)){
|
||||
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 Rock){
|
||||
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();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.S)){
|
||||
save();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.G)){
|
||||
view.setGrid(!view.isGrid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryExit(){
|
||||
if(!saved){
|
||||
ui.showConfirm("$confirm", "$editor.unsaved", this::hide);
|
||||
}else{
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void addBlockSelection(Table table){
|
||||
Table content = new Table();
|
||||
pane = new ScrollPane(content);
|
||||
pane.setFadeScrollBars(false);
|
||||
pane.setOverscroll(true, false);
|
||||
pane.exited(() -> {
|
||||
if(pane.hasScroll()){
|
||||
Core.scene.setScrollFocus(view);
|
||||
}
|
||||
});
|
||||
ButtonGroup<ImageButton> group = new ButtonGroup<>();
|
||||
|
||||
int i = 0;
|
||||
|
||||
blocksOut.clear();
|
||||
blocksOut.addAll(Vars.content.blocks());
|
||||
blocksOut.sort((b1, b2) -> {
|
||||
int core = -Boolean.compare(b1 instanceof CoreBlock, b2 instanceof CoreBlock);
|
||||
if(core != 0) return core;
|
||||
int synth = Boolean.compare(b1.synthetic(), b2.synthetic());
|
||||
if(synth != 0) return synth;
|
||||
int ore = Boolean.compare(b1 instanceof OverlayFloor, b2 instanceof OverlayFloor);
|
||||
if(ore != 0) return ore;
|
||||
return Integer.compare(b1.id, b2.id);
|
||||
});
|
||||
|
||||
for(Block block : blocksOut){
|
||||
TextureRegion region = block.icon(Cicon.medium);
|
||||
|
||||
if(!Core.atlas.isFound(region)) continue;
|
||||
|
||||
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearTogglei);
|
||||
button.getStyle().imageUp = new TextureRegionDrawable(region);
|
||||
button.clicked(() -> editor.drawBlock = block);
|
||||
button.resizeImage(8 * 4f);
|
||||
button.update(() -> button.setChecked(editor.drawBlock == block));
|
||||
group.add(button);
|
||||
content.add(button).size(50f);
|
||||
|
||||
if(++i % 4 == 0){
|
||||
content.row();
|
||||
}
|
||||
}
|
||||
|
||||
group.getButtons().get(2).setChecked(true);
|
||||
|
||||
table.table(Tex.underline, extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
|
||||
table.row();
|
||||
table.add(pane).growY().fillX();
|
||||
}
|
||||
}
|
||||
444
core/src/mindustry/editor/MapGenerateDialog.java
Normal file
444
core/src/mindustry/editor/MapGenerateDialog.java
Normal file
@@ -0,0 +1,444 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.Pixmap.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.ImageButton.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.maps.filters.*;
|
||||
import mindustry.maps.filters.GenerateFilter.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class MapGenerateDialog extends FloatingDialog{
|
||||
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
|
||||
};
|
||||
private final MapEditor editor;
|
||||
private final boolean applied;
|
||||
|
||||
private Pixmap pixmap;
|
||||
private Texture texture;
|
||||
private GenerateInput input = new GenerateInput();
|
||||
private Array<GenerateFilter> filters = new Array<>();
|
||||
private int scaling = mobile ? 3 : 1;
|
||||
private Table filterTable;
|
||||
|
||||
private AsyncExecutor executor = new AsyncExecutor(1);
|
||||
private AsyncResult<Void> result;
|
||||
private boolean generating;
|
||||
private GenTile returnTile = new GenTile();
|
||||
|
||||
private GenTile[][] buffer1, buffer2;
|
||||
private Cons<Array<GenerateFilter>> applier;
|
||||
private CachedTile ctile = new CachedTile(){
|
||||
//nothing.
|
||||
@Override
|
||||
protected void changed(){
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/** @param applied whether or not to use the applied in-game mode. */
|
||||
public MapGenerateDialog(MapEditor editor, boolean applied){
|
||||
super("$editor.generate");
|
||||
this.editor = editor;
|
||||
this.applied = applied;
|
||||
|
||||
shown(this::setup);
|
||||
addCloseButton();
|
||||
if(applied){
|
||||
buttons.addButton("$editor.apply", () -> {
|
||||
ui.loadAnd(() -> {
|
||||
apply();
|
||||
hide();
|
||||
});
|
||||
}).size(160f, 64f);
|
||||
}else{
|
||||
buttons.addButton("$settings.reset", () -> {
|
||||
filters.set(maps.readFilters(""));
|
||||
rebuildFilters();
|
||||
update();
|
||||
}).size(160f, 64f);
|
||||
}
|
||||
buttons.addButton("$editor.randomize", () -> {
|
||||
for(GenerateFilter filter : filters){
|
||||
filter.randomize();
|
||||
}
|
||||
update();
|
||||
}).size(160f, 64f);
|
||||
|
||||
buttons.addImageTextButton("$add", Icon.add, this::showAdd).height(64f).width(140f);
|
||||
|
||||
if(!applied){
|
||||
hidden(this::apply);
|
||||
}
|
||||
|
||||
onResize(this::rebuildFilters);
|
||||
}
|
||||
|
||||
public void show(Array<GenerateFilter> filters, Cons<Array<GenerateFilter>> applier){
|
||||
this.filters = filters;
|
||||
this.applier = applier;
|
||||
show();
|
||||
}
|
||||
|
||||
public void show(Cons<Array<GenerateFilter>> applier){
|
||||
show(this.filters, applier);
|
||||
}
|
||||
|
||||
/** Applies the specified filters to the editor. */
|
||||
public void applyToEditor(Array<GenerateFilter> filters){
|
||||
//writeback buffer
|
||||
GenTile[][] writeTiles = new GenTile[editor.width()][editor.height()];
|
||||
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
writeTiles[x][y] = new GenTile();
|
||||
}
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
input.begin(filter, editor.width(), editor.height(), editor::tile);
|
||||
//write to buffer
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
Tile tile = editor.tile(x, y);
|
||||
input.apply(x, y, tile.floor(), tile.block(), tile.overlay());
|
||||
filter.apply(input);
|
||||
writeTiles[x][y].set(input.floor, input.block, input.ore, tile.getTeam(), tile.rotation());
|
||||
}
|
||||
}
|
||||
|
||||
editor.load(() -> {
|
||||
//read from buffer back into tiles
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
Tile tile = editor.tile(x, y);
|
||||
GenTile write = writeTiles[x][y];
|
||||
|
||||
tile.rotation(write.rotation);
|
||||
tile.setFloor((Floor)content.block(write.floor));
|
||||
tile.setBlock(content.block(write.block));
|
||||
tile.setTeam(Team.all[write.team]);
|
||||
tile.setOverlay(content.block(write.ore));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//reset undo stack as generation... messes things up
|
||||
editor.load(editor::checkLinkedTiles);
|
||||
editor.renderer().updateAll();
|
||||
editor.clearOp();
|
||||
}
|
||||
|
||||
void setup(){
|
||||
if(pixmap != null){
|
||||
pixmap.dispose();
|
||||
texture.dispose();
|
||||
pixmap = null;
|
||||
texture = null;
|
||||
}
|
||||
|
||||
pixmap = new Pixmap(editor.width() / scaling, editor.height() / scaling, Format.RGBA8888);
|
||||
texture = new Texture(pixmap);
|
||||
|
||||
cont.clear();
|
||||
cont.table(t -> {
|
||||
t.margin(8f);
|
||||
t.stack(new BorderImage(texture){
|
||||
{
|
||||
setScaling(Scaling.fit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
super.draw();
|
||||
for(GenerateFilter filter : filters){
|
||||
filter.draw(this);
|
||||
}
|
||||
}
|
||||
}, new Stack(){{
|
||||
add(new Image(Styles.black8));
|
||||
add(new Image(Icon.refresh, Scaling.none));
|
||||
visible(() -> generating && !updateEditorOnChange);
|
||||
}}).grow().padRight(10);
|
||||
t.pane(p -> filterTable = p.marginRight(6)).update(pane -> {
|
||||
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 v = pane.stageToLocalCoordinates(Core.input.mouse());
|
||||
|
||||
if(v.x >= 0 && v.y >= 0 && v.x <= pane.getWidth() && v.y <= pane.getHeight()){
|
||||
Core.scene.setScrollFocus(pane);
|
||||
}else{
|
||||
Core.scene.setScrollFocus(null);
|
||||
}
|
||||
}).grow().get().setScrollingDisabled(true, false);
|
||||
}).grow();
|
||||
|
||||
buffer1 = create();
|
||||
buffer2 = create();
|
||||
|
||||
update();
|
||||
rebuildFilters();
|
||||
}
|
||||
|
||||
GenTile[][] create(){
|
||||
GenTile[][] out = new GenTile[editor.width() / scaling][editor.height() / scaling];
|
||||
|
||||
for(int x = 0; x < out.length; x++){
|
||||
for(int y = 0; y < out[0].length; y++){
|
||||
out[x][y] = new GenTile();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void rebuildFilters(){
|
||||
int cols = Math.max((int)(Math.max(filterTable.getParent().getWidth(), Core.graphics.getWidth()/2f * 0.9f) / Scl.scl(290f)), 1);
|
||||
filterTable.clearChildren();
|
||||
filterTable.top().left();
|
||||
int i = 0;
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
|
||||
//main container
|
||||
filterTable.table(Tex.button, c -> {
|
||||
//icons to perform actions
|
||||
c.table(t -> {
|
||||
t.top();
|
||||
t.add(filter.name()).padTop(5).color(Pal.accent).growX().left();
|
||||
|
||||
t.row();
|
||||
|
||||
t.table(b -> {
|
||||
ImageButtonStyle style = Styles.cleari;
|
||||
b.defaults().size(50f);
|
||||
b.addImageButton(Icon.refreshSmall, style, () -> {
|
||||
filter.randomize();
|
||||
update();
|
||||
});
|
||||
|
||||
b.addImageButton(Icon.arrowUpSmall, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.max(0, idx - 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
b.addImageButton(Icon.arrowDownSmall, style, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
b.addImageButton(Icon.trashSmall, style, () -> {
|
||||
filters.remove(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
});
|
||||
}).fillX();
|
||||
c.row();
|
||||
//all the options
|
||||
c.table(f -> {
|
||||
f.left().top();
|
||||
for(FilterOption option : filter.options){
|
||||
option.changed = this::update;
|
||||
|
||||
f.table(t -> {
|
||||
t.left();
|
||||
option.build(t);
|
||||
}).growX().left();
|
||||
f.row();
|
||||
}
|
||||
}).grow().left().pad(2).top();
|
||||
}).width(280f).pad(3).top().left().fillY();
|
||||
if(++i % cols == 0){
|
||||
filterTable.row();
|
||||
}
|
||||
}
|
||||
|
||||
if(filters.isEmpty()){
|
||||
filterTable.add("$filters.empty").wrap().width(200f);
|
||||
}
|
||||
}
|
||||
|
||||
void showAdd(){
|
||||
FloatingDialog selection = new FloatingDialog("$add");
|
||||
selection.setFillParent(false);
|
||||
selection.cont.defaults().size(210f, 60f);
|
||||
int i = 0;
|
||||
for(Prov<GenerateFilter> gen : filterTypes){
|
||||
GenerateFilter filter = gen.get();
|
||||
|
||||
if(!applied && filter.buffered) continue;
|
||||
|
||||
selection.cont.addButton(filter.name(), () -> {
|
||||
filters.add(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
});
|
||||
if(++i % 2 == 0) selection.cont.row();
|
||||
}
|
||||
|
||||
selection.cont.addButton("$filter.defaultores", () -> {
|
||||
maps.addDefaultOres(filters);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
});
|
||||
|
||||
selection.addCloseButton();
|
||||
selection.show();
|
||||
}
|
||||
|
||||
GenTile dset(Tile tile){
|
||||
returnTile.set(tile);
|
||||
return returnTile;
|
||||
}
|
||||
|
||||
void apply(){
|
||||
if(result != null){
|
||||
result.get();
|
||||
}
|
||||
|
||||
buffer1 = null;
|
||||
buffer2 = null;
|
||||
generating = false;
|
||||
if(pixmap != null){
|
||||
pixmap.dispose();
|
||||
texture.dispose();
|
||||
pixmap = null;
|
||||
texture = null;
|
||||
}
|
||||
|
||||
applier.get(filters);
|
||||
}
|
||||
|
||||
void update(){
|
||||
|
||||
if(generating){
|
||||
return;
|
||||
}
|
||||
|
||||
Array<GenerateFilter> copy = new Array<>(filters);
|
||||
|
||||
result = executor.submit(() -> {
|
||||
try{
|
||||
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++){
|
||||
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : copy){
|
||||
input.begin(filter, editor.width(), editor.height(), (x, y) -> buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1)][Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)].tile());
|
||||
//read from buffer1 and write to buffer2
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
int x = px * scaling, y = py * scaling;
|
||||
GenTile tile = buffer1[px][py];
|
||||
input.apply(x, y, content.block(tile.floor), content.block(tile.block), content.block(tile.ore));
|
||||
filter.apply(input);
|
||||
buffer2[px][py].set(input.floor, input.block, input.ore, Team.all[tile.team], tile.rotation);
|
||||
}
|
||||
}
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
buffer1[px][py].set(buffer2[px][py]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
int color;
|
||||
//get result from buffer1 if there's filters left, otherwise get from editor directly
|
||||
if(filters.isEmpty()){
|
||||
Tile tile = editor.tile(px * scaling, py * scaling);
|
||||
color = MapIO.colorFor(tile.floor(), tile.block(), tile.overlay(), Team.derelict);
|
||||
}else{
|
||||
GenTile tile = buffer1[px][py];
|
||||
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.derelict);
|
||||
}
|
||||
pixmap.draw(px, pixmap.getHeight() - 1 - py, color);
|
||||
}
|
||||
}
|
||||
|
||||
Core.app.post(() -> {
|
||||
if(pixmap == null || texture == null){
|
||||
return;
|
||||
}
|
||||
texture.draw(pixmap, 0, 0);
|
||||
generating = false;
|
||||
});
|
||||
}catch(Exception e){
|
||||
generating = false;
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private class GenTile{
|
||||
public byte team, rotation;
|
||||
public short block, floor, ore;
|
||||
|
||||
public void set(Block floor, Block wall, Block ore, Team team, int rotation){
|
||||
this.floor = floor.id;
|
||||
this.block = wall.id;
|
||||
this.ore = ore.id;
|
||||
this.team = (byte)team.ordinal();
|
||||
this.rotation = (byte)rotation;
|
||||
}
|
||||
|
||||
public void set(GenTile other){
|
||||
this.floor = other.floor;
|
||||
this.block = other.block;
|
||||
this.ore = other.ore;
|
||||
this.team = other.team;
|
||||
this.rotation = other.rotation;
|
||||
}
|
||||
|
||||
public GenTile set(Tile other){
|
||||
set(other.floor(), other.block(), other.overlay(), other.getTeam(), other.rotation());
|
||||
return this;
|
||||
}
|
||||
|
||||
Tile tile(){
|
||||
ctile.setFloor((Floor)content.block(floor));
|
||||
ctile.setBlock(content.block(block));
|
||||
ctile.setOverlay(content.block(ore));
|
||||
ctile.rotation(rotation);
|
||||
ctile.setTeam(Team.all[team]);
|
||||
return ctile;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
core/src/mindustry/editor/MapInfoDialog.java
Normal file
92
core/src/mindustry/editor/MapInfoDialog.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.scene.ui.*;
|
||||
import mindustry.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
public class MapInfoDialog extends FloatingDialog{
|
||||
private final MapEditor editor;
|
||||
private final WaveInfoDialog waveInfo;
|
||||
private final MapGenerateDialog generate;
|
||||
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
|
||||
|
||||
public MapInfoDialog(MapEditor editor){
|
||||
super("$editor.mapinfo");
|
||||
this.editor = editor;
|
||||
this.waveInfo = new WaveInfoDialog(editor);
|
||||
this.generate = new MapGenerateDialog(editor, false);
|
||||
|
||||
addCloseButton();
|
||||
|
||||
shown(this::setup);
|
||||
}
|
||||
|
||||
private void setup(){
|
||||
cont.clear();
|
||||
|
||||
ObjectMap<String, String> tags = editor.getTags();
|
||||
|
||||
cont.pane(t -> {
|
||||
t.add("$editor.name").padRight(8).left();
|
||||
t.defaults().padTop(15);
|
||||
|
||||
TextField name = t.addField(tags.get("name", ""), text -> {
|
||||
tags.put("name", text);
|
||||
}).size(400, 55f).get();
|
||||
name.setMessageText("$unknown");
|
||||
|
||||
t.row();
|
||||
t.add("$editor.description").padRight(8).left();
|
||||
|
||||
TextArea description = t.addArea(tags.get("description", ""), Styles.areaField, text -> {
|
||||
tags.put("description", text);
|
||||
}).size(400f, 140f).get();
|
||||
|
||||
t.row();
|
||||
t.add("$editor.author").padRight(8).left();
|
||||
|
||||
TextField author = t.addField(tags.get("author", Core.settings.getString("mapAuthor", "")), text -> {
|
||||
tags.put("author", text);
|
||||
Core.settings.put("mapAuthor", text);
|
||||
Core.settings.save();
|
||||
}).size(400, 55f).get();
|
||||
author.setMessageText("$unknown");
|
||||
|
||||
t.row();
|
||||
t.add("$editor.rules").padRight(8).left();
|
||||
t.addButton("$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.addButton("$edit", () -> {
|
||||
waveInfo.show();
|
||||
hide();
|
||||
}).left().width(200f);
|
||||
|
||||
t.row();
|
||||
t.add("$editor.generation").padRight(8).left();
|
||||
t.addButton("$edit", () -> {
|
||||
generate.show(Vars.maps.readFilters(editor.getTags().get("genfilters", "")),
|
||||
filters -> editor.getTags().put("genfilters", JsonIO.write(filters)));
|
||||
hide();
|
||||
}).left().width(200f);
|
||||
|
||||
name.change();
|
||||
description.change();
|
||||
author.change();
|
||||
|
||||
Vars.platform.addDialog(name, 50);
|
||||
Vars.platform.addDialog(author, 50);
|
||||
Vars.platform.addDialog(description, 1000);
|
||||
t.margin(16f);
|
||||
});
|
||||
}
|
||||
}
|
||||
76
core/src/mindustry/editor/MapLoadDialog.java
Normal file
76
core/src/mindustry/editor/MapLoadDialog.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.ui.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.maps;
|
||||
|
||||
public class MapLoadDialog extends FloatingDialog{
|
||||
private Map selected = null;
|
||||
|
||||
public MapLoadDialog(Cons<Map> loader){
|
||||
super("$editor.loadmap");
|
||||
|
||||
shown(this::rebuild);
|
||||
|
||||
TextButton button = new TextButton("$load");
|
||||
button.setDisabled(() -> selected == null);
|
||||
button.clicked(() -> {
|
||||
if(selected != null){
|
||||
loader.get(selected);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.addButton("$cancel", this::hide);
|
||||
buttons.add(button);
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
cont.clear();
|
||||
if(maps.all().size > 0){
|
||||
selected = maps.all().first();
|
||||
}
|
||||
|
||||
ButtonGroup<TextButton> group = new ButtonGroup<>();
|
||||
|
||||
int maxcol = 3;
|
||||
|
||||
int i = 0;
|
||||
|
||||
Table table = new Table();
|
||||
table.defaults().size(200f, 90f).pad(4f);
|
||||
table.margin(10f);
|
||||
|
||||
ScrollPane pane = new ScrollPane(table, Styles.horizontalPane);
|
||||
pane.setFadeScrollBars(false);
|
||||
|
||||
for(Map map : maps.all()){
|
||||
|
||||
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(maps.all().size == 0){
|
||||
table.add("$maps.none").center();
|
||||
}else{
|
||||
cont.add("$editor.loadmap");
|
||||
}
|
||||
|
||||
cont.row();
|
||||
cont.add(pane);
|
||||
}
|
||||
|
||||
}
|
||||
184
core/src/mindustry/editor/MapRenderer.java
Normal file
184
core/src/mindustry/editor/MapRenderer.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.IntSet;
|
||||
import arc.struct.IntSet.IntSetIterator;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.Texture;
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.graphics.g2d.TextureRegion;
|
||||
import arc.math.Mathf;
|
||||
import arc.util.*;
|
||||
import mindustry.content.Blocks;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.graphics.IndexedRenderer;
|
||||
import mindustry.world.Block;
|
||||
import mindustry.world.Tile;
|
||||
import mindustry.world.blocks.BlockPart;
|
||||
|
||||
import static mindustry.Vars.tilesize;
|
||||
|
||||
public class MapRenderer implements Disposable{
|
||||
private static final int chunkSize = 64;
|
||||
private IndexedRenderer[][] chunks;
|
||||
private IntSet updates = new IntSet();
|
||||
private IntSet delayedUpdates = new IntSet();
|
||||
private MapEditor editor;
|
||||
private int width, height;
|
||||
private Texture texture;
|
||||
|
||||
public MapRenderer(MapEditor editor){
|
||||
this.editor = editor;
|
||||
this.texture = Core.atlas.find("clear-editor").getTexture();
|
||||
|
||||
Events.on(ContentReloadEvent.class, e -> {
|
||||
texture = Core.atlas.find("clear-editor").getTexture();
|
||||
});
|
||||
}
|
||||
|
||||
public void resize(int width, int height){
|
||||
if(chunks != null){
|
||||
for(int x = 0; x < chunks.length; x++){
|
||||
for(int y = 0; y < chunks[0].length; y++){
|
||||
chunks[x][y].dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunks = new IndexedRenderer[(int)Math.ceil((float)width / chunkSize)][(int)Math.ceil((float)height / chunkSize)];
|
||||
|
||||
for(int x = 0; x < chunks.length; x++){
|
||||
for(int y = 0; y < chunks[0].length; y++){
|
||||
chunks[x][y] = new IndexedRenderer(chunkSize * chunkSize * 2);
|
||||
}
|
||||
}
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
updateAll();
|
||||
}
|
||||
|
||||
public void draw(float tx, float ty, float tw, float th){
|
||||
Draw.flush();
|
||||
|
||||
IntSetIterator it = updates.iterator();
|
||||
while(it.hasNext){
|
||||
int i = it.next();
|
||||
int x = i % width;
|
||||
int y = i / width;
|
||||
render(x, y);
|
||||
}
|
||||
updates.clear();
|
||||
|
||||
updates.addAll(delayedUpdates);
|
||||
delayedUpdates.clear();
|
||||
|
||||
//????
|
||||
if(chunks == null){
|
||||
return;
|
||||
}
|
||||
|
||||
for(int x = 0; x < chunks.length; x++){
|
||||
for(int y = 0; y < chunks[0].length; y++){
|
||||
IndexedRenderer mesh = chunks[x][y];
|
||||
|
||||
if(mesh == null){
|
||||
continue;
|
||||
}
|
||||
|
||||
mesh.getTransformMatrix().setToTranslation(tx, ty).scale(tw / (width * tilesize), th / (height * tilesize));
|
||||
mesh.setProjectionMatrix(Draw.proj());
|
||||
|
||||
mesh.render(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePoint(int x, int y){
|
||||
updates.add(x + y * width);
|
||||
}
|
||||
|
||||
public void updateAll(){
|
||||
for(int x = 0; x < width; x++){
|
||||
for(int y = 0; y < height; y++){
|
||||
render(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void render(int wx, int wy){
|
||||
int x = wx / chunkSize, y = wy / chunkSize;
|
||||
IndexedRenderer mesh = chunks[x][y];
|
||||
Tile tile = editor.tiles()[wx][wy];
|
||||
|
||||
Team team = tile.getTeam();
|
||||
Block floor = tile.floor();
|
||||
Block wall = tile.block();
|
||||
|
||||
TextureRegion region;
|
||||
|
||||
int idxWall = (wx % chunkSize) + (wy % chunkSize) * chunkSize;
|
||||
int idxDecal = (wx % chunkSize) + (wy % chunkSize) * chunkSize + chunkSize * chunkSize;
|
||||
|
||||
if(wall != Blocks.air && (wall.synthetic() || wall instanceof BlockPart)){
|
||||
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
|
||||
|
||||
if(wall.rotate){
|
||||
mesh.draw(idxWall, region,
|
||||
wx * tilesize + wall.offset(), wy * tilesize + wall.offset(),
|
||||
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl, tile.rotation() * 90 - 90);
|
||||
}else{
|
||||
float width = region.getWidth() * Draw.scl, height = region.getHeight() * Draw.scl;
|
||||
|
||||
mesh.draw(idxWall, region,
|
||||
wx * tilesize + wall.offset() + (tilesize - width) / 2f,
|
||||
wy * tilesize + wall.offset() + (tilesize - height) / 2f,
|
||||
width, height);
|
||||
}
|
||||
}else{
|
||||
region = floor.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, floor.editorVariantRegions().length - 1)];
|
||||
|
||||
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
|
||||
}
|
||||
|
||||
float offsetX = -(wall.size / 3) * tilesize, offsetY = -(wall.size / 3) * tilesize;
|
||||
|
||||
if(wall.update || wall.destructible){
|
||||
mesh.setColor(team.color);
|
||||
region = Core.atlas.find("block-border-editor");
|
||||
}else if(!wall.synthetic() && wall != Blocks.air){
|
||||
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
|
||||
offsetX = tilesize / 2f - region.getWidth() / 2f * Draw.scl;
|
||||
offsetY = tilesize / 2f - region.getHeight() / 2f * Draw.scl;
|
||||
}else if(wall == Blocks.air && tile.overlay() != null){
|
||||
region = tile.overlay().editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
|
||||
}else{
|
||||
region = Core.atlas.find("clear-editor");
|
||||
}
|
||||
|
||||
float width = region.getWidth() * Draw.scl, height = region.getHeight() * Draw.scl;
|
||||
if(!wall.synthetic() && wall != Blocks.air && !wall.isMultiblock()){
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
width = tilesize;
|
||||
height = tilesize;
|
||||
}
|
||||
|
||||
mesh.draw(idxDecal, region, wx * tilesize + offsetX, wy * tilesize + offsetY, width, height);
|
||||
mesh.setColor(Color.white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
if(chunks == null){
|
||||
return;
|
||||
}
|
||||
for(int x = 0; x < chunks.length; x++){
|
||||
for(int y = 0; y < chunks[0].length; y++){
|
||||
if(chunks[x][y] != null){
|
||||
chunks[x][y].dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
core/src/mindustry/editor/MapResizeDialog.java
Normal file
58
core/src/mindustry/editor/MapResizeDialog.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
public class MapResizeDialog extends FloatingDialog{
|
||||
private static final int minSize = 50, maxSize = 500, increment = 50;
|
||||
int width, height;
|
||||
|
||||
public MapResizeDialog(MapEditor editor, Intc2 cons){
|
||||
super("$editor.resizemap");
|
||||
shown(() -> {
|
||||
cont.clear();
|
||||
width = editor.width();
|
||||
height = editor.height();
|
||||
|
||||
Table table = new Table();
|
||||
|
||||
for(boolean w : Mathf.booleans){
|
||||
table.add(w ? "$width" : "$height").padRight(8f);
|
||||
table.defaults().height(60f).padTop(8);
|
||||
table.addButton("<", () -> {
|
||||
if(w)
|
||||
width = move(width, -1);
|
||||
else
|
||||
height = move(height, -1);
|
||||
}).size(60f);
|
||||
|
||||
table.table(Tex.button, t -> t.label(() -> (w ? width : height) + "")).width(200);
|
||||
|
||||
table.addButton(">", () -> {
|
||||
if(w)
|
||||
width = move(width, 1);
|
||||
else
|
||||
height = move(height, 1);
|
||||
}).size(60f);
|
||||
table.row();
|
||||
}
|
||||
cont.row();
|
||||
cont.add(table);
|
||||
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.addButton("$cancel", this::hide);
|
||||
buttons.addButton("$ok", () -> {
|
||||
cons.get(width, height);
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
static int move(int value, int direction){
|
||||
return Mathf.clamp((value / increment + direction) * increment, minSize, maxSize);
|
||||
}
|
||||
}
|
||||
73
core/src/mindustry/editor/MapSaveDialog.java
Normal file
73
core/src/mindustry/editor/MapSaveDialog.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.func.*;
|
||||
import arc.scene.ui.*;
|
||||
import mindustry.*;
|
||||
import mindustry.maps.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.ui;
|
||||
|
||||
public class MapSaveDialog extends FloatingDialog{
|
||||
private TextField field;
|
||||
private Cons<String> listener;
|
||||
|
||||
public MapSaveDialog(Cons<String> cons){
|
||||
super("$editor.savemap");
|
||||
field = new TextField();
|
||||
listener = cons;
|
||||
|
||||
Vars.platform.addDialog(field);
|
||||
|
||||
shown(() -> {
|
||||
cont.clear();
|
||||
cont.label(() -> {
|
||||
Map map = Vars.maps.byName(field.getText());
|
||||
if(map != null){
|
||||
if(map.custom){
|
||||
return "$editor.overwrite";
|
||||
}else{
|
||||
return "$editor.failoverwrite";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}).colspan(2);
|
||||
cont.row();
|
||||
cont.add("$editor.mapname").padRight(14f);
|
||||
cont.add(field).size(220f, 48f);
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f).pad(2f);
|
||||
buttons.addButton("$cancel", this::hide);
|
||||
|
||||
TextButton button = new TextButton("$save");
|
||||
button.clicked(() -> {
|
||||
if(!invalid()){
|
||||
cons.get(field.getText());
|
||||
hide();
|
||||
}
|
||||
});
|
||||
button.setDisabled(this::invalid);
|
||||
buttons.add(button);
|
||||
}
|
||||
|
||||
public void save(){
|
||||
if(!invalid()){
|
||||
listener.get(field.getText());
|
||||
}else{
|
||||
ui.showErrorMessage("$editor.failoverwrite");
|
||||
}
|
||||
}
|
||||
|
||||
public void setFieldText(String text){
|
||||
field.setText(text);
|
||||
}
|
||||
|
||||
private boolean invalid(){
|
||||
if(field.getText().isEmpty()){
|
||||
return true;
|
||||
}
|
||||
Map map = Vars.maps.byName(field.getText());
|
||||
return map != null && !map.custom;
|
||||
}
|
||||
}
|
||||
343
core/src/mindustry/editor/MapView.java
Normal file
343
core/src/mindustry/editor/MapView.java
Normal file
@@ -0,0 +1,343 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.input.GestureDetector;
|
||||
import arc.input.GestureDetector.GestureListener;
|
||||
import arc.input.KeyCode;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.Element;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.TextField;
|
||||
import arc.scene.ui.layout.Scl;
|
||||
import arc.util.*;
|
||||
import mindustry.graphics.Pal;
|
||||
import mindustry.input.Binding;
|
||||
import mindustry.ui.GridImage;
|
||||
|
||||
import static mindustry.Vars.mobile;
|
||||
import static mindustry.Vars.ui;
|
||||
|
||||
public class MapView extends Element implements GestureListener{
|
||||
private MapEditor editor;
|
||||
private EditorTool tool = EditorTool.pencil;
|
||||
private float offsetx, offsety;
|
||||
private float zoom = 1f;
|
||||
private boolean grid = false;
|
||||
private GridImage image = new GridImage(0, 0);
|
||||
private Vector2 vec = new Vector2();
|
||||
private Rectangle rect = new Rectangle();
|
||||
private Vector2[][] brushPolygons = new Vector2[MapEditor.brushSizes.length][0];
|
||||
|
||||
private boolean drawing;
|
||||
private int lastx, lasty;
|
||||
private int startx, starty;
|
||||
private float mousex, mousey;
|
||||
private EditorTool lastTool;
|
||||
|
||||
public MapView(MapEditor editor){
|
||||
this.editor = editor;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Core.input.getInputProcessors().insert(0, new GestureDetector(20, 0.5f, 2, 0.15f, this));
|
||||
touchable(Touchable.enabled);
|
||||
|
||||
Point2 firstTouch = new Point2();
|
||||
|
||||
addListener(new InputListener(){
|
||||
|
||||
@Override
|
||||
public boolean mouseMoved(InputEvent event, float x, float y){
|
||||
mousex = x;
|
||||
mousey = y;
|
||||
requestScroll();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){
|
||||
requestScroll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(pointer != 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
|
||||
return true;
|
||||
}
|
||||
|
||||
if(button == KeyCode.MOUSE_MIDDLE){
|
||||
lastTool = tool;
|
||||
tool = EditorTool.zoom;
|
||||
}
|
||||
|
||||
mousex = x;
|
||||
mousey = y;
|
||||
|
||||
Point2 p = project(x, y);
|
||||
lastx = p.x;
|
||||
lasty = p.y;
|
||||
startx = p.x;
|
||||
starty = p.y;
|
||||
tool.touched(editor, p.x, p.y);
|
||||
firstTouch.set(p);
|
||||
|
||||
if(tool.edit){
|
||||
ui.editor.resetSaved();
|
||||
}
|
||||
|
||||
drawing = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
|
||||
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
|
||||
return;
|
||||
}
|
||||
|
||||
drawing = false;
|
||||
|
||||
Point2 p = project(x, y);
|
||||
|
||||
if(tool == EditorTool.line){
|
||||
ui.editor.resetSaved();
|
||||
tool.touchedLine(editor, startx, starty, p.x, p.y);
|
||||
}
|
||||
|
||||
editor.flushOp();
|
||||
|
||||
if(button == KeyCode.MOUSE_MIDDLE && lastTool != null){
|
||||
tool = lastTool;
|
||||
lastTool = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchDragged(InputEvent event, float x, float y, int pointer){
|
||||
mousex = x;
|
||||
mousey = y;
|
||||
|
||||
Point2 p = project(x, y);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if(tool == EditorTool.line && tool.mode == 1){
|
||||
if(Math.abs(p.x - firstTouch.x) > Math.abs(p.y - firstTouch.y)){
|
||||
lastx = p.x;
|
||||
lasty = firstTouch.y;
|
||||
}else{
|
||||
lastx = firstTouch.x;
|
||||
lasty = p.y;
|
||||
}
|
||||
}else{
|
||||
lastx = p.x;
|
||||
lasty = p.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public EditorTool getTool(){
|
||||
return tool;
|
||||
}
|
||||
|
||||
public void setTool(EditorTool tool){
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
public boolean isGrid(){
|
||||
return grid;
|
||||
}
|
||||
|
||||
public void setGrid(boolean grid){
|
||||
this.grid = grid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void act(float delta){
|
||||
super.act(delta);
|
||||
|
||||
if(Core.scene.getKeyboardFocus() == null || !(Core.scene.getKeyboardFocus() instanceof TextField) && !Core.input.keyDown(KeyCode.CONTROL_LEFT)){
|
||||
float ax = Core.input.axis(Binding.move_x);
|
||||
float ay = Core.input.axis(Binding.move_y);
|
||||
offsetx -= ax * 15f / zoom;
|
||||
offsety -= ay * 15f / zoom;
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(KeyCode.SHIFT_LEFT)){
|
||||
lastTool = tool;
|
||||
tool = EditorTool.pick;
|
||||
}
|
||||
|
||||
if(Core.input.keyRelease(KeyCode.SHIFT_LEFT) && lastTool != null){
|
||||
tool = lastTool;
|
||||
lastTool = null;
|
||||
}
|
||||
|
||||
if(ui.editor.hasPane()) return;
|
||||
|
||||
zoom += Core.input.axis(KeyCode.SCROLL) / 10f * zoom;
|
||||
clampZoom();
|
||||
}
|
||||
|
||||
private void clampZoom(){
|
||||
zoom = Mathf.clamp(zoom, 0.2f, 20f);
|
||||
}
|
||||
|
||||
private Point2 project(float x, float y){
|
||||
float ratio = 1f / ((float)editor.width() / editor.height());
|
||||
float size = Math.min(width, height);
|
||||
float sclwidth = size * zoom;
|
||||
float sclheight = size * zoom * ratio;
|
||||
x = (x - getWidth() / 2 + sclwidth / 2 - offsetx * zoom) / sclwidth * editor.width();
|
||||
y = (y - getHeight() / 2 + sclheight / 2 - offsety * zoom) / sclheight * editor.height();
|
||||
|
||||
if(editor.drawBlock.size % 2 == 0 && tool != EditorTool.eraser){
|
||||
return Tmp.g1.set((int)(x - 0.5f), (int)(y - 0.5f));
|
||||
}else{
|
||||
return Tmp.g1.set((int)x, (int)y);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 unproject(int x, int y){
|
||||
float ratio = 1f / ((float)editor.width() / editor.height());
|
||||
float size = Math.min(width, height);
|
||||
float sclwidth = size * zoom;
|
||||
float sclheight = size * zoom * ratio;
|
||||
float px = ((float)x / editor.width()) * sclwidth + offsetx * zoom - sclwidth / 2 + getWidth() / 2;
|
||||
float py = ((float)(y) / editor.height()) * sclheight
|
||||
+ offsety * zoom - sclheight / 2 + getHeight() / 2;
|
||||
return vec.set(px, py);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
float ratio = 1f / ((float)editor.width() / editor.height());
|
||||
float size = Math.min(width, height);
|
||||
float sclwidth = size * zoom;
|
||||
float sclheight = size * zoom * ratio;
|
||||
float centerx = x + width / 2 + offsetx * zoom;
|
||||
float centery = y + height / 2 + offsety * zoom;
|
||||
|
||||
image.setImageSize(editor.width(), editor.height());
|
||||
|
||||
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
|
||||
return;
|
||||
}
|
||||
|
||||
Draw.color(Pal.remove);
|
||||
Lines.stroke(2f);
|
||||
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
|
||||
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
Draw.reset();
|
||||
|
||||
if(grid){
|
||||
Draw.color(Color.gray);
|
||||
image.setBounds(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
image.draw();
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
for(int i = 0; i < MapEditor.brushSizes.length; i++){
|
||||
if(editor.brushSize == MapEditor.brushSizes[i]){
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float scaling = zoom * Math.min(width, height) / editor.width();
|
||||
|
||||
Draw.color(Pal.accent);
|
||||
Lines.stroke(Scl.scl(2f));
|
||||
|
||||
if((!editor.drawBlock.isMultiblock() || tool == EditorTool.eraser) && tool != EditorTool.fill){
|
||||
if(tool == EditorTool.line && drawing){
|
||||
Vector2 v1 = unproject(startx, starty).add(x, y);
|
||||
float sx = v1.x, sy = v1.y;
|
||||
Vector2 v2 = unproject(lastx, lasty).add(x, y);
|
||||
|
||||
Lines.poly(brushPolygons[index], sx, sy, scaling);
|
||||
Lines.poly(brushPolygons[index], v2.x, v2.y, scaling);
|
||||
}
|
||||
|
||||
if((tool.edit || (tool == EditorTool.line && !drawing)) && (!mobile || drawing)){
|
||||
Point2 p = project(mousex, mousey);
|
||||
Vector2 v = unproject(p.x, p.y).add(x, y);
|
||||
|
||||
//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));
|
||||
}else{
|
||||
Lines.poly(brushPolygons[index], v.x, v.y, scaling);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if((tool.edit || tool == EditorTool.line) && (!mobile || drawing)){
|
||||
Point2 p = project(mousex, mousey);
|
||||
Vector2 v = unproject(p.x, p.y).add(x, y);
|
||||
float offset = (editor.drawBlock.size % 2 == 0 ? scaling / 2f : 0f);
|
||||
Lines.square(
|
||||
v.x + scaling / 2f + offset,
|
||||
v.y + scaling / 2f + offset,
|
||||
scaling * editor.drawBlock.size / 2f);
|
||||
}
|
||||
}
|
||||
|
||||
Draw.color(Pal.accent);
|
||||
Lines.stroke(Scl.scl(3f));
|
||||
Lines.rect(x, y, width, height);
|
||||
Draw.reset();
|
||||
|
||||
ScissorStack.popScissors();
|
||||
}
|
||||
|
||||
private boolean active(){
|
||||
return 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pan(float x, float y, float deltaX, float deltaY){
|
||||
if(!active()) return false;
|
||||
offsetx += deltaX / zoom;
|
||||
offsety += deltaY / zoom;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean zoom(float initialDistance, float distance){
|
||||
if(!active()) return false;
|
||||
float nzoom = distance - initialDistance;
|
||||
zoom += nzoom / 10000f / Scl.scl(1f) * zoom;
|
||||
clampZoom();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pinchStop(){
|
||||
|
||||
}
|
||||
}
|
||||
51
core/src/mindustry/editor/OperationStack.java
Executable file
51
core/src/mindustry/editor/OperationStack.java
Executable file
@@ -0,0 +1,51 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.struct.Array;
|
||||
|
||||
public class OperationStack{
|
||||
private final static int maxSize = 10;
|
||||
private Array<DrawOperation> stack = new Array<>();
|
||||
private int index = 0;
|
||||
|
||||
public OperationStack(){
|
||||
|
||||
}
|
||||
|
||||
public void clear(){
|
||||
stack.clear();
|
||||
index = 0;
|
||||
}
|
||||
|
||||
public void add(DrawOperation action){
|
||||
stack.truncate(stack.size + index);
|
||||
index = 0;
|
||||
stack.add(action);
|
||||
|
||||
if(stack.size > maxSize){
|
||||
stack.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canUndo(){
|
||||
return !(stack.size - 1 + index < 0);
|
||||
}
|
||||
|
||||
public boolean canRedo(){
|
||||
return !(index > -1 || stack.size + index < 0);
|
||||
}
|
||||
|
||||
public void undo(){
|
||||
if(!canUndo()) return;
|
||||
|
||||
stack.get(stack.size - 1 + index).undo();
|
||||
index--;
|
||||
}
|
||||
|
||||
public void redo(){
|
||||
if(!canRedo()) return;
|
||||
|
||||
index++;
|
||||
stack.get(stack.size - 1 + index).redo();
|
||||
|
||||
}
|
||||
}
|
||||
274
core/src/mindustry/editor/WaveInfoDialog.java
Normal file
274
core/src/mindustry/editor/WaveInfoDialog.java
Normal file
@@ -0,0 +1,274 @@
|
||||
package mindustry.editor;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import arc.input.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.event.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.TextField.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.ui.Cicon;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.game.SpawnGroup.never;
|
||||
|
||||
public class WaveInfoDialog extends FloatingDialog{
|
||||
private final static int displayed = 20;
|
||||
private Array<SpawnGroup> groups = new Array<>();
|
||||
|
||||
private Table table, preview;
|
||||
private int start = 0;
|
||||
private UnitType lastType = UnitTypes.dagger;
|
||||
private float updateTimer, updatePeriod = 1f;
|
||||
|
||||
public WaveInfoDialog(MapEditor editor){
|
||||
super("$waves.title");
|
||||
|
||||
shown(this::setup);
|
||||
hidden(() -> {
|
||||
state.rules.spawns = groups;
|
||||
});
|
||||
|
||||
keyDown(key -> {
|
||||
if(key == KeyCode.ESCAPE || key == KeyCode.BACK){
|
||||
Core.app.post(this::hide);
|
||||
}
|
||||
});
|
||||
|
||||
addCloseButton();
|
||||
buttons.addButton("$waves.edit", () -> {
|
||||
FloatingDialog dialog = new FloatingDialog("$waves.edit");
|
||||
dialog.addCloseButton();
|
||||
dialog.setFillParent(false);
|
||||
dialog.cont.defaults().size(210f, 64f);
|
||||
dialog.cont.addButton("$waves.copy", () -> {
|
||||
ui.showInfoFade("$waves.copied");
|
||||
Core.app.setClipboardText(maps.writeWaves(groups));
|
||||
dialog.hide();
|
||||
}).disabled(b -> groups == null);
|
||||
dialog.cont.row();
|
||||
dialog.cont.addButton("$waves.load", () -> {
|
||||
try{
|
||||
groups = maps.readWaves(Core.app.getClipboardText());
|
||||
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.addButton("$settings.reset", () -> ui.showConfirm("$confirm", "$settings.clear.confirm", () -> {
|
||||
groups = JsonIO.copy(defaultWaves.get());
|
||||
buildGroups();
|
||||
dialog.hide();
|
||||
}));
|
||||
dialog.show();
|
||||
}).size(270f, 64f);
|
||||
}
|
||||
|
||||
void setup(){
|
||||
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? defaultWaves.get() : state.rules.spawns);
|
||||
|
||||
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.addButton("$add", () -> {
|
||||
if(groups == null) groups = new Array<>();
|
||||
groups.add(new SpawnGroup(lastType));
|
||||
buildGroups();
|
||||
}).growX().height(70f);
|
||||
}), new Label("$waves.none"){{
|
||||
visible(() -> groups.isEmpty());
|
||||
touchable(Touchable.disabled);
|
||||
setWrap(true);
|
||||
setAlignment(Align.center, Align.center);
|
||||
}}).width(390f).growY();
|
||||
|
||||
cont.table(Tex.clear, m -> {
|
||||
m.add("$waves.preview").color(Color.lightGray).growX().center().get().setAlignment(Align.center, Align.center);
|
||||
m.row();
|
||||
m.addButton("-", () -> {
|
||||
}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
updateTimer += Time.delta();
|
||||
if(updateTimer >= updatePeriod){
|
||||
start = Math.max(start - 1, 0);
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
}
|
||||
}).growX().height(70f);
|
||||
m.row();
|
||||
m.pane(t -> preview = t).grow().get().setScrollingDisabled(true, true);
|
||||
m.row();
|
||||
m.addButton("+", () -> {
|
||||
}).update(t -> {
|
||||
if(t.getClickListener().isPressed()){
|
||||
updateTimer += Time.delta();
|
||||
if(updateTimer >= updatePeriod){
|
||||
start++;
|
||||
updateTimer = 0f;
|
||||
updateWaves();
|
||||
}
|
||||
}
|
||||
}).growX().height(70f);
|
||||
}).growY().width(180f).growY();
|
||||
|
||||
buildGroups();
|
||||
}
|
||||
|
||||
void buildGroups(){
|
||||
table.clear();
|
||||
table.top();
|
||||
table.margin(10f);
|
||||
|
||||
if(groups != null){
|
||||
for(SpawnGroup group : groups){
|
||||
table.table(Tex.button, t -> {
|
||||
t.margin(0).defaults().pad(3).padLeft(5f).growX().left();
|
||||
t.addButton(b -> {
|
||||
b.left();
|
||||
b.addImage(group.type.icon(mindustry.ui.Cicon.medium)).size(32f).padRight(3);
|
||||
b.add(group.type.localizedName).color(Pal.accent);
|
||||
}, () -> showUpdate(group)).pad(-6f).padBottom(0f);
|
||||
|
||||
t.row();
|
||||
t.table(spawns -> {
|
||||
spawns.addField("" + (group.begin + 1), TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePostiveInt(text)){
|
||||
group.begin = Strings.parseInt(text) - 1;
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
spawns.add("$waves.to").padLeft(4).padRight(4);
|
||||
spawns.addField(group.end == never ? "" : (group.end + 1) + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePostiveInt(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.addField(group.spacing + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePostiveInt(text) && Strings.parseInt(text) > 0){
|
||||
group.spacing = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(100f);
|
||||
p.add("$waves.waves").padLeft(4);
|
||||
});
|
||||
|
||||
t.row();
|
||||
t.table(a -> {
|
||||
a.addField(group.unitAmount + "", TextFieldFilter.digitsOnly, text -> {
|
||||
if(Strings.canParsePostiveInt(text)){
|
||||
group.unitAmount = Strings.parseInt(text);
|
||||
updateWaves();
|
||||
}
|
||||
}).width(80f);
|
||||
|
||||
a.add(" + ");
|
||||
a.addField(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.addCheck("$waves.boss", b -> group.effect = (b ? StatusEffects.boss : null)).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss));
|
||||
|
||||
t.row();
|
||||
t.addButton("$waves.remove", () -> {
|
||||
groups.remove(group);
|
||||
table.getCell(t).pad(0f);
|
||||
t.remove();
|
||||
updateWaves();
|
||||
}).growX().pad(-6f).padTop(5);
|
||||
}).width(340f).pad(16);
|
||||
table.row();
|
||||
}
|
||||
}else{
|
||||
table.add("$editor.default");
|
||||
}
|
||||
|
||||
updateWaves();
|
||||
}
|
||||
|
||||
void showUpdate(SpawnGroup group){
|
||||
FloatingDialog dialog = new FloatingDialog("");
|
||||
dialog.setFillParent(true);
|
||||
dialog.cont.pane(p -> {
|
||||
int i = 0;
|
||||
for(UnitType type : content.units()){
|
||||
p.addButton(t -> {
|
||||
t.left();
|
||||
t.addImage(type.icon(mindustry.ui.Cicon.medium)).size(40f).padRight(2f);
|
||||
t.add(type.localizedName);
|
||||
}, () -> {
|
||||
lastType = type;
|
||||
group.type = type;
|
||||
dialog.hide();
|
||||
buildGroups();
|
||||
}).pad(2).margin(12f).fillX();
|
||||
if(++i % 3 == 0) p.row();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
void updateWaves(){
|
||||
preview.clear();
|
||||
preview.top();
|
||||
|
||||
for(int i = start; i < displayed + start; i++){
|
||||
int wave = i;
|
||||
preview.table(Tex.underline, table -> {
|
||||
table.add((wave + 1) + "").color(Pal.accent).center().colspan(2).get().setAlignment(Align.center, Align.center);
|
||||
table.row();
|
||||
|
||||
int[] spawned = new int[Vars.content.getBy(ContentType.unit).size];
|
||||
|
||||
for(SpawnGroup spawn : groups){
|
||||
spawned[spawn.type.id] += spawn.getUnitsSpawned(wave);
|
||||
}
|
||||
|
||||
for(int j = 0; j < spawned.length; j++){
|
||||
if(spawned[j] > 0){
|
||||
UnitType type = content.getByID(ContentType.unit, j);
|
||||
table.addImage(type.icon(Cicon.medium)).size(8f * 4f).padRight(4);
|
||||
table.add(spawned[j] + "x").color(Color.lightGray).padRight(6);
|
||||
table.row();
|
||||
}
|
||||
}
|
||||
|
||||
if(table.getChildren().size == 1){
|
||||
table.add("$none").color(Pal.remove);
|
||||
}
|
||||
}).width(110f).pad(2f);
|
||||
|
||||
preview.row();
|
||||
}
|
||||
}
|
||||
}
|
||||
282
core/src/mindustry/entities/Damage.java
Normal file
282
core/src/mindustry/entities/Damage.java
Normal file
@@ -0,0 +1,282 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.Effects.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Utility class for damaging in an area. */
|
||||
public class Damage{
|
||||
private static Rectangle rect = new Rectangle();
|
||||
private static Rectangle hitrect = new Rectangle();
|
||||
private static Vector2 tr = new Vector2();
|
||||
private static GridBits bits = new GridBits(30, 30);
|
||||
private static IntQueue propagation = new IntQueue();
|
||||
private static IntSet collidedBlocks = new IntSet();
|
||||
|
||||
/** Creates a dynamic explosion based on specified parameters. */
|
||||
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, Color color){
|
||||
for(int i = 0; i < Mathf.clamp(power / 20, 0, 6); i++){
|
||||
int branches = 5 + Mathf.clamp((int)(power / 30), 1, 20);
|
||||
Time.run(i * 2f + Mathf.random(4f), () -> Lightning.create(Team.derelict, Pal.power, 3,
|
||||
x, y, Mathf.random(360f), branches + Mathf.range(2)));
|
||||
}
|
||||
|
||||
for(int i = 0; i < Mathf.clamp(flammability / 4, 0, 30); i++){
|
||||
Time.run(i / 2f, () -> Call.createBullet(Bullets.fireball, Team.derelict, x, y, Mathf.random(360f), 1, 1));
|
||||
}
|
||||
|
||||
int waves = Mathf.clamp((int)(explosiveness / 4), 0, 30);
|
||||
|
||||
for(int i = 0; i < waves; i++){
|
||||
int f = i;
|
||||
Time.run(i * 2f, () -> {
|
||||
Damage.damage(x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f);
|
||||
Effects.effect(Fx.blockExplosionSmoke, x + Mathf.range(radius), y + Mathf.range(radius));
|
||||
});
|
||||
}
|
||||
|
||||
if(explosiveness > 15f){
|
||||
Effects.effect(Fx.shockwave, x, y);
|
||||
}
|
||||
|
||||
if(explosiveness > 30f){
|
||||
Effects.effect(Fx.bigShockwave, x, y);
|
||||
}
|
||||
|
||||
float shake = Math.min(explosiveness / 4f + 3f, 9f);
|
||||
Effects.shake(shake, shake, x, y);
|
||||
Effects.effect(Fx.dynamicExplosion, x, y, radius / 8f);
|
||||
}
|
||||
|
||||
public static void createIncend(float x, float y, float range, int amount){
|
||||
for(int i = 0; i < amount; i++){
|
||||
float cx = x + Mathf.range(range);
|
||||
float cy = y + Mathf.range(range);
|
||||
Tile tile = world.tileWorld(cx, cy);
|
||||
if(tile != null){
|
||||
Fire.create(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length){
|
||||
collideLine(hitter, team, effect, x, y, angle, length, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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){
|
||||
collidedBlocks.clear();
|
||||
tr.trns(angle, length);
|
||||
Intc2 collider = (cx, cy) -> {
|
||||
Tile tile = world.ltile(cx, cy);
|
||||
if(tile != null && !collidedBlocks.contains(tile.pos()) && tile.entity != null && tile.getTeamID() != team.ordinal() && tile.entity.collide(hitter)){
|
||||
tile.entity.collision(hitter);
|
||||
collidedBlocks.add(tile.pos());
|
||||
hitter.getBulletType().hit(hitter, tile.worldx(), tile.worldy());
|
||||
}
|
||||
};
|
||||
|
||||
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
|
||||
collider.get(cx, cy);
|
||||
if(large){
|
||||
for(Point2 p : Geometry.d4){
|
||||
collider.get(cx + p.x, cy + p.y);
|
||||
}
|
||||
}
|
||||
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);
|
||||
Rectangle other = hitrect;
|
||||
other.y -= expand;
|
||||
other.x -= expand;
|
||||
other.width += expand * 2;
|
||||
other.height += expand * 2;
|
||||
|
||||
Vector2 vec = Geometry.raycastRect(x, y, x2, y2, other);
|
||||
|
||||
if(vec != null){
|
||||
Effects.effect(effect, vec.x, vec.y);
|
||||
e.collision(hitter, vec.x, vec.y);
|
||||
hitter.collision(e, vec.x, vec.y);
|
||||
}
|
||||
};
|
||||
|
||||
Units.nearbyEnemies(team, rect, cons);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
entity.hitbox(hitrect);
|
||||
if(!hitrect.overlaps(rect)){
|
||||
return;
|
||||
}
|
||||
entity.damage(damage);
|
||||
acceptor.get(entity);
|
||||
};
|
||||
|
||||
rect.setSize(size * 2).setCenter(x, y);
|
||||
if(team != null){
|
||||
Units.nearbyEnemies(team, rect, cons);
|
||||
}else{
|
||||
Units.nearby(rect, cons);
|
||||
}
|
||||
}
|
||||
|
||||
/** Damages everything in a radius. */
|
||||
public static void damage(float x, float y, float radius, float damage){
|
||||
damage(null, x, y, radius, damage, false);
|
||||
}
|
||||
|
||||
/** 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){
|
||||
damage(team, x, y, radius, damage, false);
|
||||
}
|
||||
|
||||
/** 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){
|
||||
Cons<Unit> cons = entity -> {
|
||||
if(entity.getTeam() == team || entity.dst(x, y) > radius){
|
||||
return;
|
||||
}
|
||||
float amount = calculateDamage(x, y, entity.x, entity.y, radius, damage);
|
||||
entity.damage(amount);
|
||||
//TODO better velocity displacement
|
||||
float dst = tr.set(entity.x - x, entity.y - y).len();
|
||||
entity.velocity().add(tr.setLength((1f - dst / radius) * 2f / entity.mass()));
|
||||
|
||||
if(complete && damage >= 9999999f && entity == player){
|
||||
Events.fire(Trigger.exclusionDeath);
|
||||
}
|
||||
};
|
||||
|
||||
rect.setSize(radius * 2).setCenter(x, y);
|
||||
if(team != null){
|
||||
Units.nearbyEnemies(team, rect, cons);
|
||||
}else{
|
||||
Units.nearby(rect, cons);
|
||||
}
|
||||
|
||||
if(!complete){
|
||||
int trad = (int)(radius / tilesize);
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
if(tile != null){
|
||||
tileDamage(team, tile.x, tile.y, trad, damage);
|
||||
}
|
||||
}else{
|
||||
completeDamage(team, x, y, radius, damage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void tileDamage(Team team, int startx, int starty, int radius, float baseDamage){
|
||||
bits.clear();
|
||||
propagation.clear();
|
||||
int bitOffset = bits.width() / 2;
|
||||
|
||||
propagation.addFirst(PropCell.get((byte)0, (byte)0, (short)baseDamage));
|
||||
//clamp radius to fit bits
|
||||
radius = Math.min(radius, bits.width() / 2);
|
||||
|
||||
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 scaledDamage = (int)(damage * (1f - (float)dst / radius));
|
||||
|
||||
bits.set(bitOffset + x, bitOffset + y);
|
||||
Tile tile = world.ltile(startx + x, starty + y);
|
||||
|
||||
if(scaledDamage <= 0 || tile == null) continue;
|
||||
|
||||
//apply damage to entity if needed
|
||||
if(tile.entity != null && tile.getTeam() != team){
|
||||
int health = (int)tile.entity.health;
|
||||
if(tile.entity.health > 0){
|
||||
tile.entity.damage(scaledDamage);
|
||||
scaledDamage -= health;
|
||||
|
||||
if(scaledDamage <= 0) continue;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.entity != null && (team == null || state.teams.areEnemies(team, tile.getTeam())) && Mathf.dst(dx, dy) <= trad){
|
||||
tile.entity.damage(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);
|
||||
float falloff = 0.4f;
|
||||
float scaled = Mathf.lerp(1f - dist / radius, 1f, falloff);
|
||||
return damage * scaled;
|
||||
}
|
||||
|
||||
@Struct
|
||||
class PropCellStruct{
|
||||
byte x;
|
||||
byte y;
|
||||
short damage;
|
||||
}
|
||||
}
|
||||
168
core/src/mindustry/entities/Effects.java
Normal file
168
core/src/mindustry/entities/Effects.java
Normal file
@@ -0,0 +1,168 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.Core;
|
||||
import arc.struct.Array;
|
||||
import arc.func.Cons;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.Position;
|
||||
import arc.util.pooling.Pools;
|
||||
import mindustry.entities.type.EffectEntity;
|
||||
import mindustry.entities.traits.ScaleTrait;
|
||||
|
||||
public class Effects{
|
||||
private static final EffectContainer container = new EffectContainer();
|
||||
private static Array<Effect> effects = new Array<>();
|
||||
private static ScreenshakeProvider shakeProvider;
|
||||
private static float shakeFalloff = 10000f;
|
||||
private static EffectProvider provider = (effect, color, x, y, rotation, data) -> {
|
||||
EffectEntity entity = Pools.obtain(EffectEntity.class, EffectEntity::new);
|
||||
entity.effect = effect;
|
||||
entity.color = color;
|
||||
entity.rotation = rotation;
|
||||
entity.data = data;
|
||||
entity.set(x, y);
|
||||
entity.add();
|
||||
};
|
||||
|
||||
public static void setEffectProvider(EffectProvider prov){
|
||||
provider = prov;
|
||||
}
|
||||
|
||||
public static void setScreenShakeProvider(ScreenshakeProvider provider){
|
||||
shakeProvider = provider;
|
||||
}
|
||||
|
||||
public static void renderEffect(int id, Effect render, Color color, float life, float rotation, float x, float y, Object data){
|
||||
container.set(id, color, life, render.lifetime, rotation, x, y, data);
|
||||
render.draw.render(container);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public static Effect getEffect(int id){
|
||||
if(id >= effects.size || id < 0)
|
||||
throw new IllegalArgumentException("The effect with ID \"" + id + "\" does not exist!");
|
||||
return effects.get(id);
|
||||
}
|
||||
|
||||
public static Array<Effect> all(){
|
||||
return effects;
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, float x, float y, float rotation){
|
||||
provider.createEffect(effect, Color.white, x, y, rotation, null);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, float x, float y){
|
||||
effect(effect, x, y, 0);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, Color color, float x, float y){
|
||||
provider.createEffect(effect, color, x, y, 0f, null);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, Position loc){
|
||||
provider.createEffect(effect, Color.white, loc.getX(), loc.getY(), 0f, null);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, Color color, float x, float y, float rotation){
|
||||
provider.createEffect(effect, color, x, y, rotation, null);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, Color color, float x, float y, float rotation, Object data){
|
||||
provider.createEffect(effect, color, x, y, rotation, data);
|
||||
}
|
||||
|
||||
public static void effect(Effect effect, float x, float y, float rotation, Object data){
|
||||
provider.createEffect(effect, Color.white, x, y, rotation, data);
|
||||
}
|
||||
|
||||
/** Default value is 1000. Higher numbers mean more powerful shake (less falloff). */
|
||||
public static void setShakeFalloff(float falloff){
|
||||
shakeFalloff = falloff;
|
||||
}
|
||||
|
||||
private static void shake(float intensity, float duration){
|
||||
if(shakeProvider == null) throw new RuntimeException("Screenshake provider is null! Set it first.");
|
||||
shakeProvider.accept(intensity, duration);
|
||||
}
|
||||
|
||||
public static void shake(float intensity, float duration, float x, float y){
|
||||
if(Core.camera == null) return;
|
||||
|
||||
float distance = Core.camera.position.dst(x, y);
|
||||
if(distance < 1) distance = 1;
|
||||
|
||||
shake(Mathf.clamp(1f / (distance * distance / shakeFalloff)) * intensity, duration);
|
||||
}
|
||||
|
||||
public static void shake(float intensity, float duration, Position loc){
|
||||
shake(intensity, duration, loc.getX(), loc.getY());
|
||||
}
|
||||
|
||||
public interface ScreenshakeProvider{
|
||||
void accept(float intensity, float duration);
|
||||
}
|
||||
|
||||
public static class Effect{
|
||||
private static int lastid = 0;
|
||||
public final int id;
|
||||
public final EffectRenderer draw;
|
||||
public final float lifetime;
|
||||
/** Clip size. */
|
||||
public float size;
|
||||
|
||||
public Effect(float life, float clipsize, EffectRenderer draw){
|
||||
this.id = lastid++;
|
||||
this.lifetime = life;
|
||||
this.draw = draw;
|
||||
this.size = clipsize;
|
||||
effects.add(this);
|
||||
}
|
||||
|
||||
public Effect(float life, EffectRenderer draw){
|
||||
this(life, 28f, draw);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EffectContainer implements ScaleTrait{
|
||||
public float x, y, time, lifetime, rotation;
|
||||
public Color color;
|
||||
public int id;
|
||||
public Object data;
|
||||
private EffectContainer innerContainer;
|
||||
|
||||
public void set(int id, Color color, float life, float lifetime, float rotation, float x, float y, Object data){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.color = color;
|
||||
this.time = life;
|
||||
this.lifetime = lifetime;
|
||||
this.id = id;
|
||||
this.rotation = rotation;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void scaled(float lifetime, Cons<EffectContainer> cons){
|
||||
if(innerContainer == null) innerContainer = new EffectContainer();
|
||||
if(time <= lifetime){
|
||||
innerContainer.set(id, color, time, lifetime, rotation, x, y, data);
|
||||
cons.get(innerContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float fin(){
|
||||
return time / lifetime;
|
||||
}
|
||||
}
|
||||
|
||||
public interface EffectProvider{
|
||||
void createEffect(Effect effect, Color color, float x, float y, float rotation, Object data);
|
||||
}
|
||||
|
||||
public interface EffectRenderer{
|
||||
void render(EffectContainer effect);
|
||||
}
|
||||
}
|
||||
33
core/src/mindustry/entities/Entities.java
Executable file
33
core/src/mindustry/entities/Entities.java
Executable file
@@ -0,0 +1,33 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.struct.*;
|
||||
import mindustry.entities.traits.*;
|
||||
|
||||
/** Simple container for managing entity groups.*/
|
||||
public class Entities{
|
||||
private final Array<EntityGroup<?>> groupArray = new Array<>();
|
||||
|
||||
public void clear(){
|
||||
for(EntityGroup group : groupArray){
|
||||
group.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public EntityGroup<?> get(int id){
|
||||
return groupArray.get(id);
|
||||
}
|
||||
|
||||
public Array<EntityGroup<?>> all(){
|
||||
return groupArray;
|
||||
}
|
||||
|
||||
public <T extends Entity> EntityGroup<T> add(Class<T> type){
|
||||
return add(type, true);
|
||||
}
|
||||
|
||||
public <T extends Entity> EntityGroup<T> add(Class<T> type, boolean useTree){
|
||||
EntityGroup<T> group = new EntityGroup<>(groupArray.size, type, useTree);
|
||||
groupArray.add(group);
|
||||
return group;
|
||||
}
|
||||
}
|
||||
236
core/src/mindustry/entities/EntityCollisions.java
Normal file
236
core/src/mindustry/entities/EntityCollisions.java
Normal file
@@ -0,0 +1,236 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.struct.Array;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.entities.traits.Entity;
|
||||
import mindustry.entities.traits.SolidTrait;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.tilesize;
|
||||
import static mindustry.Vars.world;
|
||||
|
||||
public class EntityCollisions{
|
||||
//range for tile collision scanning
|
||||
private static final int r = 1;
|
||||
//move in 1-unit chunks
|
||||
private static final float seg = 1f;
|
||||
|
||||
//tile collisions
|
||||
private Rectangle tmp = new Rectangle();
|
||||
private Vector2 vector = new Vector2();
|
||||
private Vector2 l1 = new Vector2();
|
||||
private Rectangle r1 = new Rectangle();
|
||||
private Rectangle r2 = new Rectangle();
|
||||
|
||||
//entity collisions
|
||||
private Array<SolidTrait> arrOut = new Array<>();
|
||||
|
||||
public void move(SolidTrait entity, float deltax, float deltay){
|
||||
|
||||
boolean movedx = false;
|
||||
|
||||
while(Math.abs(deltax) > 0 || !movedx){
|
||||
movedx = true;
|
||||
moveDelta(entity, Math.min(Math.abs(deltax), seg) * Mathf.sign(deltax), 0, true);
|
||||
|
||||
if(Math.abs(deltax) >= seg){
|
||||
deltax -= seg * Mathf.sign(deltax);
|
||||
}else{
|
||||
deltax = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
boolean movedy = false;
|
||||
|
||||
while(Math.abs(deltay) > 0 || !movedy){
|
||||
movedy = true;
|
||||
moveDelta(entity, 0, Math.min(Math.abs(deltay), seg) * Mathf.sign(deltay), false);
|
||||
|
||||
if(Math.abs(deltay) >= seg){
|
||||
deltay -= seg * Mathf.sign(deltay);
|
||||
}else{
|
||||
deltay = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void moveDelta(SolidTrait entity, float deltax, float deltay, boolean x){
|
||||
|
||||
Rectangle rect = r1;
|
||||
entity.hitboxTile(rect);
|
||||
entity.hitboxTile(r2);
|
||||
rect.x += deltax;
|
||||
rect.y += deltay;
|
||||
|
||||
int tilex = Math.round((rect.x + rect.width / 2) / tilesize), tiley = Math.round((rect.y + rect.height / 2) / tilesize);
|
||||
|
||||
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) && entity.collidesGrid(wx, wy)){
|
||||
tmp.setSize(tilesize).setCenter(wx * tilesize, wy * tilesize);
|
||||
|
||||
if(tmp.overlaps(rect)){
|
||||
Vector2 v = Geometry.overlap(rect, tmp, x);
|
||||
rect.x += v.x;
|
||||
rect.y += v.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity.setX(entity.getX() + rect.x - r2.x);
|
||||
entity.setY(entity.getY() + rect.y - r2.y);
|
||||
}
|
||||
|
||||
public boolean overlapsTile(Rectangle rect){
|
||||
rect.getCenter(vector);
|
||||
int r = 1;
|
||||
|
||||
//assumes tiles are centered
|
||||
int tilex = Math.round(vector.x / tilesize);
|
||||
int tiley = Math.round(vector.y / tilesize);
|
||||
|
||||
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(r2.overlaps(rect)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Entity> void updatePhysics(EntityGroup<T> group){
|
||||
|
||||
QuadTree tree = group.tree();
|
||||
tree.clear();
|
||||
|
||||
for(Entity entity : group.all()){
|
||||
if(entity instanceof SolidTrait){
|
||||
SolidTrait s = (SolidTrait)entity;
|
||||
s.lastPosition().set(s.getX(), s.getY());
|
||||
tree.insert(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean solid(int x, int y){
|
||||
Tile tile = world.tile(x, y);
|
||||
return tile != null && tile.solid();
|
||||
}
|
||||
|
||||
private void checkCollide(Entity entity, Entity other){
|
||||
|
||||
SolidTrait a = (SolidTrait)entity;
|
||||
SolidTrait b = (SolidTrait)other;
|
||||
|
||||
a.hitbox(this.r1);
|
||||
b.hitbox(this.r2);
|
||||
|
||||
r1.x += (a.lastPosition().x - a.getX());
|
||||
r1.y += (a.lastPosition().y - a.getY());
|
||||
r2.x += (b.lastPosition().x - b.getX());
|
||||
r2.y += (b.lastPosition().y - b.getY());
|
||||
|
||||
float vax = a.getX() - a.lastPosition().x;
|
||||
float vay = a.getY() - a.lastPosition().y;
|
||||
float vbx = b.getX() - b.lastPosition().x;
|
||||
float vby = b.getY() - b.lastPosition().y;
|
||||
|
||||
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);
|
||||
if(collide){
|
||||
a.collision(b, l1.x, l1.y);
|
||||
b.collision(a, l1.x, l1.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1,
|
||||
float x2, float y2, float w2, float h2, float vx2, float vy2, Vector2 out){
|
||||
float px = vx1, py = vy1;
|
||||
|
||||
vx1 -= vx2;
|
||||
vy1 -= vy2;
|
||||
|
||||
float xInvEntry, yInvEntry;
|
||||
float xInvExit, yInvExit;
|
||||
|
||||
if(vx1 > 0.0f){
|
||||
xInvEntry = x2 - (x1 + w1);
|
||||
xInvExit = (x2 + w2) - x1;
|
||||
}else{
|
||||
xInvEntry = (x2 + w2) - x1;
|
||||
xInvExit = x2 - (x1 + w1);
|
||||
}
|
||||
|
||||
if(vy1 > 0.0f){
|
||||
yInvEntry = y2 - (y1 + h1);
|
||||
yInvExit = (y2 + h2) - y1;
|
||||
}else{
|
||||
yInvEntry = (y2 + h2) - y1;
|
||||
yInvExit = y2 - (y1 + h1);
|
||||
}
|
||||
|
||||
float xEntry, yEntry;
|
||||
float xExit, yExit;
|
||||
|
||||
xEntry = xInvEntry / vx1;
|
||||
xExit = xInvExit / vx1;
|
||||
|
||||
yEntry = yInvEntry / vy1;
|
||||
yExit = yInvExit / vy1;
|
||||
|
||||
float entryTime = Math.max(xEntry, yEntry);
|
||||
float exitTime = Math.min(xExit, yExit);
|
||||
|
||||
if(entryTime > exitTime || xExit < 0.0f || yExit < 0.0f || xEntry > 1.0f || yEntry > 1.0f){
|
||||
return false;
|
||||
}else{
|
||||
float dx = x1 + w1 / 2f + px * entryTime;
|
||||
float dy = y1 + h1 / 2f + py * entryTime;
|
||||
|
||||
out.set(dx, dy);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void collideGroups(EntityGroup<?> groupa, EntityGroup<?> groupb){
|
||||
|
||||
for(Entity entity : groupa.all()){
|
||||
if(!(entity instanceof SolidTrait))
|
||||
continue;
|
||||
|
||||
SolidTrait solid = (SolidTrait)entity;
|
||||
|
||||
solid.hitbox(r1);
|
||||
r1.x += (solid.lastPosition().x - solid.getX());
|
||||
r1.y += (solid.lastPosition().y - solid.getY());
|
||||
|
||||
solid.hitbox(r2);
|
||||
r2.merge(r1);
|
||||
|
||||
arrOut.clear();
|
||||
groupb.tree().getIntersect(arrOut, r2);
|
||||
|
||||
for(SolidTrait sc : arrOut){
|
||||
sc.hitbox(r1);
|
||||
if(r2.overlaps(r1)){
|
||||
checkCollide(entity, sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
260
core/src/mindustry/entities/EntityGroup.java
Normal file
260
core/src/mindustry/entities/EntityGroup.java
Normal file
@@ -0,0 +1,260 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.entities.traits.*;
|
||||
|
||||
import static mindustry.Vars.collisions;
|
||||
|
||||
/** Represents a group of a certain type of entity.*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class EntityGroup<T extends Entity>{
|
||||
private final boolean useTree;
|
||||
private final int id;
|
||||
private final Class<T> type;
|
||||
private final Array<T> entityArray = new Array<>(false, 32);
|
||||
private final Array<T> entitiesToRemove = new Array<>(false, 32);
|
||||
private final Array<T> entitiesToAdd = new Array<>(false, 32);
|
||||
private final Array<T> intersectArray = new Array<>();
|
||||
private final Rectangle intersectRect = new Rectangle();
|
||||
private IntMap<T> map;
|
||||
private QuadTree tree;
|
||||
private Cons<T> removeListener;
|
||||
private Cons<T> addListener;
|
||||
|
||||
private final Rectangle viewport = new Rectangle();
|
||||
private int count = 0;
|
||||
|
||||
public EntityGroup(int id, Class<T> type, boolean useTree){
|
||||
this.useTree = useTree;
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
|
||||
if(useTree){
|
||||
tree = new QuadTree<>(new Rectangle(0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public void update(){
|
||||
updateEvents();
|
||||
|
||||
if(useTree()){
|
||||
collisions.updatePhysics(this);
|
||||
}
|
||||
|
||||
for(Entity e : all()){
|
||||
e.update();
|
||||
}
|
||||
}
|
||||
|
||||
public int countInBounds(){
|
||||
count = 0;
|
||||
draw(e -> true, e -> count++);
|
||||
return count;
|
||||
}
|
||||
|
||||
public void draw(){
|
||||
draw(e -> true);
|
||||
}
|
||||
|
||||
public void draw(Boolf<T> toDraw){
|
||||
draw(toDraw, t -> ((DrawTrait)t).draw());
|
||||
}
|
||||
|
||||
public void draw(Boolf<T> toDraw, Cons<T> cons){
|
||||
Camera cam = Core.camera;
|
||||
viewport.set(cam.position.x - cam.width / 2, cam.position.y - cam.height / 2, cam.width, cam.height);
|
||||
|
||||
for(Entity e : all()){
|
||||
if(!(e instanceof DrawTrait) || !toDraw.get((T)e) || !e.isAdded()) continue;
|
||||
DrawTrait draw = (DrawTrait)e;
|
||||
|
||||
if(viewport.overlaps(draw.getX() - draw.drawSize()/2f, draw.getY() - draw.drawSize()/2f, draw.drawSize(), draw.drawSize())){
|
||||
cons.get((T)e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean useTree(){
|
||||
return useTree;
|
||||
}
|
||||
|
||||
public void setRemoveListener(Cons<T> removeListener){
|
||||
this.removeListener = removeListener;
|
||||
}
|
||||
|
||||
public void setAddListener(Cons<T> addListener){
|
||||
this.addListener = addListener;
|
||||
}
|
||||
|
||||
public EntityGroup<T> enableMapping(){
|
||||
map = new IntMap<>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean mappingEnabled(){
|
||||
return map != null;
|
||||
}
|
||||
|
||||
public Class<T> getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getID(){
|
||||
return id;
|
||||
}
|
||||
|
||||
public void updateEvents(){
|
||||
|
||||
for(T e : entitiesToAdd){
|
||||
if(e == null)
|
||||
continue;
|
||||
entityArray.add(e);
|
||||
e.added();
|
||||
|
||||
if(map != null){
|
||||
map.put(e.getID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
entitiesToAdd.clear();
|
||||
|
||||
for(T e : entitiesToRemove){
|
||||
entityArray.removeValue(e, true);
|
||||
if(map != null){
|
||||
map.remove(e.getID());
|
||||
}
|
||||
e.removed();
|
||||
}
|
||||
|
||||
entitiesToRemove.clear();
|
||||
}
|
||||
|
||||
public T getByID(int id){
|
||||
if(map == null) throw new RuntimeException("Mapping is not enabled for group " + id + "!");
|
||||
return map.get(id);
|
||||
}
|
||||
|
||||
public void removeByID(int id){
|
||||
if(map == null) throw new RuntimeException("Mapping is not enabled for group " + id + "!");
|
||||
T t = map.get(id);
|
||||
if(t != null){ //remove if present in map already
|
||||
remove(t);
|
||||
}else{ //maybe it's being queued?
|
||||
for(T check : entitiesToAdd){
|
||||
if(check.getID() == id){ //if it is indeed queued, remove it
|
||||
entitiesToAdd.removeValue(check, true);
|
||||
if(removeListener != null){
|
||||
removeListener.get(check);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void intersect(float x, float y, float width, float height, Cons<? super T> out){
|
||||
//don't waste time for empty groups
|
||||
if(isEmpty()) return;
|
||||
tree().getIntersect(out, x, y, width, height);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Array<T> intersect(float x, float y, float width, float height){
|
||||
intersectArray.clear();
|
||||
//don't waste time for empty groups
|
||||
if(isEmpty()) return intersectArray;
|
||||
tree().getIntersect(intersectArray, intersectRect.set(x, y, width, height));
|
||||
return intersectArray;
|
||||
}
|
||||
|
||||
public QuadTree tree(){
|
||||
if(!useTree) throw new RuntimeException("This group does not support quadtrees! Enable quadtrees when creating it.");
|
||||
return tree;
|
||||
}
|
||||
|
||||
/** Resizes the internal quadtree, if it is enabled.*/
|
||||
public void resize(float x, float y, float w, float h){
|
||||
if(useTree){
|
||||
tree = new QuadTree<>(new Rectangle(x, y, w, h));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty(){
|
||||
return entityArray.size == 0;
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return entityArray.size;
|
||||
}
|
||||
|
||||
public int count(Boolf<T> pred){
|
||||
int count = 0;
|
||||
for(int i = 0; i < entityArray.size; i++){
|
||||
if(pred.get(entityArray.get(i))) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void add(T type){
|
||||
if(type == null) throw new RuntimeException("Cannot add a null entity!");
|
||||
if(type.getGroup() != null) return;
|
||||
type.setGroup(this);
|
||||
entitiesToAdd.add(type);
|
||||
|
||||
if(mappingEnabled()){
|
||||
map.put(type.getID(), type);
|
||||
}
|
||||
|
||||
if(addListener != null){
|
||||
addListener.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(T type){
|
||||
if(type == null) throw new RuntimeException("Cannot remove a null entity!");
|
||||
type.setGroup(null);
|
||||
entitiesToRemove.add(type);
|
||||
|
||||
if(removeListener != null){
|
||||
removeListener.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(){
|
||||
for(T entity : entityArray){
|
||||
entity.removed();
|
||||
entity.setGroup(null);
|
||||
}
|
||||
|
||||
for(T entity : entitiesToAdd)
|
||||
entity.setGroup(null);
|
||||
|
||||
for(T entity : entitiesToRemove)
|
||||
entity.setGroup(null);
|
||||
|
||||
entitiesToAdd.clear();
|
||||
entitiesToRemove.clear();
|
||||
entityArray.clear();
|
||||
if(map != null)
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public T find(Boolf<T> pred){
|
||||
|
||||
for(int i = 0; i < entityArray.size; i++){
|
||||
if(pred.get(entityArray.get(i))) return entityArray.get(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the logic-only array for iteration. */
|
||||
public Array<T> all(){
|
||||
return entityArray;
|
||||
}
|
||||
}
|
||||
79
core/src/mindustry/entities/Predict.java
Normal file
79
core/src/mindustry/entities/Predict.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.entities.traits.*;
|
||||
|
||||
/**
|
||||
* Class for predicting shoot angles based on velocities of targets.
|
||||
*/
|
||||
public class Predict{
|
||||
private static Vector2 vec = new Vector2();
|
||||
private static Vector2 vresult = new Vector2();
|
||||
|
||||
/**
|
||||
* Calculates of intercept of a stationary and moving target. Do not call from multiple threads!
|
||||
* @param srcx X of shooter
|
||||
* @param srcy Y of shooter
|
||||
* @param dstx X of target
|
||||
* @param dsty Y of target
|
||||
* @param dstvx X velocity of target (subtract shooter X velocity if needed)
|
||||
* @param dstvy Y velocity of target (subtract shooter Y velocity if needed)
|
||||
* @param v speed of bullet
|
||||
* @return the intercept location
|
||||
*/
|
||||
public static Vector2 intercept(float srcx, float srcy, float dstx, float dsty, float dstvx, float dstvy, float v){
|
||||
dstvx /= Time.delta();
|
||||
dstvy /= Time.delta();
|
||||
float tx = dstx - srcx,
|
||||
ty = dsty - srcy;
|
||||
|
||||
// Get quadratic equation components
|
||||
float a = dstvx * dstvx + dstvy * dstvy - v * v;
|
||||
float b = 2 * (dstvx * tx + dstvy * ty);
|
||||
float c = tx * tx + ty * ty;
|
||||
|
||||
// Solve quadratic
|
||||
Vector2 ts = quad(a, b, c);
|
||||
|
||||
// Find smallest positive solution
|
||||
Vector2 sol = vresult.set(dstx, dsty);
|
||||
if(ts != null){
|
||||
float t0 = ts.x, t1 = ts.y;
|
||||
float t = Math.min(t0, t1);
|
||||
if(t < 0) t = Math.max(t0, t1);
|
||||
if(t > 0){
|
||||
sol.set(dstx + dstvx * t, dsty + dstvy * t);
|
||||
}
|
||||
}
|
||||
|
||||
return sol;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #intercept(float, float, float, float, float, float, float)}.
|
||||
*/
|
||||
public static Vector2 intercept(TargetTrait src, TargetTrait dst, float v){
|
||||
return intercept(src.getX(), src.getY(), dst.getX(), dst.getY(), dst.getTargetVelocityX() - src.getTargetVelocityX()/2f, dst.getTargetVelocityY() - src.getTargetVelocityY()/2f, v);
|
||||
}
|
||||
|
||||
private static Vector2 quad(float a, float b, float c){
|
||||
Vector2 sol = null;
|
||||
if(Math.abs(a) < 1e-6){
|
||||
if(Math.abs(b) < 1e-6){
|
||||
sol = Math.abs(c) < 1e-6 ? vec.set(0, 0) : null;
|
||||
}else{
|
||||
vec.set(-c / b, -c / b);
|
||||
}
|
||||
}else{
|
||||
float disc = b * b - 4 * a * c;
|
||||
if(disc >= 0){
|
||||
disc = Mathf.sqrt(disc);
|
||||
a = 2 * a;
|
||||
sol = vec.set((-b - disc) / a, (-b + disc) / a);
|
||||
}
|
||||
}
|
||||
return sol;
|
||||
}
|
||||
}
|
||||
6
core/src/mindustry/entities/TargetPriority.java
Normal file
6
core/src/mindustry/entities/TargetPriority.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package mindustry.entities;
|
||||
|
||||
public enum TargetPriority{
|
||||
base,
|
||||
turret
|
||||
}
|
||||
226
core/src/mindustry/entities/Units.java
Normal file
226
core/src/mindustry/entities/Units.java
Normal file
@@ -0,0 +1,226 @@
|
||||
package mindustry.entities;
|
||||
|
||||
import arc.struct.EnumSet;
|
||||
import arc.func.Cons;
|
||||
import arc.func.Boolf;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.Geometry;
|
||||
import arc.math.geom.Rectangle;
|
||||
import mindustry.entities.traits.TargetTrait;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Utility class for unit and team interactions.*/
|
||||
public class Units{
|
||||
private static Rectangle hitrect = new Rectangle();
|
||||
private static Unit result;
|
||||
private static float cdist;
|
||||
private static boolean boolResult;
|
||||
|
||||
/** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/
|
||||
public static boolean canInteract(Player player, Tile tile){
|
||||
return player == null || tile == null || tile.interactable(player.getTeam());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a target.
|
||||
* @param target The target to validate
|
||||
* @param team The team of the thing doing tha targeting
|
||||
* @param x The X position of the thing doign the targeting
|
||||
* @param y The Y position of the thing doign the targeting
|
||||
* @param range The maximum distance from the target X/Y the targeter can be for it to be valid
|
||||
* @return whether the target is invalid
|
||||
*/
|
||||
public static boolean invalidateTarget(TargetTrait target, Team team, float x, float y, float range){
|
||||
return target == null || (range != Float.MAX_VALUE && !target.withinDst(x, y, range)) || target.getTeam() == team || !target.isValid();
|
||||
}
|
||||
|
||||
/** See {@link #invalidateTarget(TargetTrait, Team, float, float, float)} */
|
||||
public static boolean invalidateTarget(TargetTrait target, Team team, float x, float y){
|
||||
return invalidateTarget(target, team, x, y, Float.MAX_VALUE);
|
||||
}
|
||||
|
||||
/** See {@link #invalidateTarget(TargetTrait, Team, float, float, float)} */
|
||||
public static boolean invalidateTarget(TargetTrait target, Unit targeter){
|
||||
return invalidateTarget(target, targeter.getTeam(), targeter.x, targeter.y, targeter.getWeapon().bullet.range());
|
||||
}
|
||||
|
||||
/** Returns whether there are any entities on this tile. */
|
||||
public static boolean anyEntities(Tile tile){
|
||||
float size = tile.block().size * tilesize;
|
||||
return anyEntities(tile.drawx() - size/2f, tile.drawy() - size/2f, size, size);
|
||||
}
|
||||
|
||||
public static boolean anyEntities(float x, float y, float width, float height){
|
||||
boolResult = false;
|
||||
|
||||
nearby(x, y, width, height, unit -> {
|
||||
if(boolResult) return;
|
||||
if(!unit.isFlying()){
|
||||
unit.hitbox(hitrect);
|
||||
|
||||
if(hitrect.overlaps(x, y, width, height)){
|
||||
boolResult = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return boolResult;
|
||||
}
|
||||
|
||||
/** Returns the neareset damaged tile. */
|
||||
public static TileEntity findDamagedTile(Team team, float x, float y){
|
||||
Tile tile = Geometry.findClosest(x, y, indexer.getDamaged(team));
|
||||
return tile == null ? null : tile.entity;
|
||||
}
|
||||
|
||||
/** Returns the neareset ally tile in a range. */
|
||||
public static TileEntity findAllyTile(Team team, float x, float y, float range, Boolf<Tile> pred){
|
||||
return indexer.findTile(team, x, y, range, pred);
|
||||
}
|
||||
|
||||
/** Returns the neareset enemy tile in a range. */
|
||||
public static TileEntity findEnemyTile(Team team, float x, float y, float range, Boolf<Tile> pred){
|
||||
if(team == Team.derelict) return null;
|
||||
|
||||
for(Team enemy : state.teams.enemiesOf(team)){
|
||||
TileEntity entity = indexer.findTile(enemy, x, y, range, pred, true);
|
||||
if(entity != null){
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the closest target enemy. First, units are checked, then tile entities. */
|
||||
public static TargetTrait closestTarget(Team team, float x, float y, float range){
|
||||
return closestTarget(team, x, y, range, Unit::isValid);
|
||||
}
|
||||
|
||||
/** Returns the closest target enemy. First, units are checked, then tile entities. */
|
||||
public static TargetTrait closestTarget(Team team, float x, float y, float range, Boolf<Unit> unitPred){
|
||||
return closestTarget(team, x, y, range, unitPred, t -> true);
|
||||
}
|
||||
|
||||
/** Returns the closest target enemy. First, units are checked, then tile entities. */
|
||||
public static TargetTrait closestTarget(Team team, float x, float y, float range, Boolf<Unit> unitPred, Boolf<Tile> tilePred){
|
||||
if(team == Team.derelict) return null;
|
||||
|
||||
Unit unit = closestEnemy(team, x, y, range, unitPred);
|
||||
if(unit != null){
|
||||
return unit;
|
||||
}else{
|
||||
return findEnemyTile(team, x, y, range, tilePred);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the closest enemy of this team. Filter by predicate. */
|
||||
public static Unit closestEnemy(Team team, float x, float y, float range, Boolf<Unit> predicate){
|
||||
if(team == Team.derelict) return null;
|
||||
|
||||
result = null;
|
||||
cdist = 0f;
|
||||
|
||||
nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> {
|
||||
if(e.isDead() || !predicate.get(e)) return;
|
||||
|
||||
float dst2 = Mathf.dst2(e.x, e.y, x, y);
|
||||
if(dst2 < range*range && (result == null || dst2 < cdist)){
|
||||
result = e;
|
||||
cdist = dst2;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the closest ally of this team. Filter by predicate. */
|
||||
public static Unit closest(Team team, float x, float y, float range, Boolf<Unit> predicate){
|
||||
result = null;
|
||||
cdist = 0f;
|
||||
|
||||
nearby(team, x, y, range, e -> {
|
||||
if(!predicate.get(e)) return;
|
||||
|
||||
float dist = Mathf.dst2(e.x, e.y, x, y);
|
||||
if(result == null || dist < cdist){
|
||||
result = e;
|
||||
cdist = dist;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Iterates over all units in a rectangle. */
|
||||
public static void nearby(Team team, float x, float y, float width, float height, Cons<Unit> cons){
|
||||
unitGroups[team.ordinal()].intersect(x, y, width, height, cons);
|
||||
playerGroup.intersect(x, y, width, height, player -> {
|
||||
if(player.getTeam() == team){
|
||||
cons.get(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units in a circle around this position. */
|
||||
public static void nearby(Team team, float x, float y, float radius, Cons<Unit> cons){
|
||||
unitGroups[team.ordinal()].intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
|
||||
if(unit.withinDst(x, y, radius)){
|
||||
cons.get(unit);
|
||||
}
|
||||
});
|
||||
|
||||
playerGroup.intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
|
||||
if(unit.getTeam() == team && unit.withinDst(x, y, radius)){
|
||||
cons.get(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units in a rectangle. */
|
||||
public static void nearby(float x, float y, float width, float height, Cons<Unit> cons){
|
||||
for(Team team : Team.all){
|
||||
unitGroups[team.ordinal()].intersect(x, y, width, height, cons);
|
||||
}
|
||||
|
||||
playerGroup.intersect(x, y, width, height, cons);
|
||||
}
|
||||
|
||||
/** Iterates over all units in a rectangle. */
|
||||
public static void nearby(Rectangle rect, Cons<Unit> cons){
|
||||
nearby(rect.x, rect.y, rect.width, rect.height, cons);
|
||||
}
|
||||
|
||||
/** Iterates over all units that are enemies of this team. */
|
||||
public static void nearbyEnemies(Team team, float x, float y, float width, float height, Cons<Unit> cons){
|
||||
EnumSet<Team> targets = state.teams.enemiesOf(team);
|
||||
|
||||
for(Team other : targets){
|
||||
unitGroups[other.ordinal()].intersect(x, y, width, height, cons);
|
||||
}
|
||||
|
||||
playerGroup.intersect(x, y, width, height, player -> {
|
||||
if(targets.contains(player.getTeam())){
|
||||
cons.get(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units that are enemies of this team. */
|
||||
public static void nearbyEnemies(Team team, Rectangle rect, Cons<Unit> cons){
|
||||
nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons);
|
||||
}
|
||||
|
||||
/** Iterates over all units. */
|
||||
public static void all(Cons<Unit> cons){
|
||||
for(Team team : Team.all){
|
||||
unitGroups[team.ordinal()].all().each(cons);
|
||||
}
|
||||
|
||||
playerGroup.all().each(cons);
|
||||
}
|
||||
|
||||
}
|
||||
49
core/src/mindustry/entities/bullet/ArtilleryBulletType.java
Normal file
49
core/src/mindustry/entities/bullet/ArtilleryBulletType.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.graphics.g2d.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.Effects.*;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.gen.*;
|
||||
|
||||
//TODO scale velocity depending on fslope()
|
||||
public class ArtilleryBulletType extends BasicBulletType{
|
||||
protected Effect trailEffect = Fx.artilleryTrail;
|
||||
|
||||
public ArtilleryBulletType(float speed, float damage, String bulletSprite){
|
||||
super(speed, damage, bulletSprite);
|
||||
collidesTiles = false;
|
||||
collides = false;
|
||||
collidesAir = false;
|
||||
hitShake = 1f;
|
||||
hitSound = Sounds.explosion;
|
||||
}
|
||||
|
||||
public ArtilleryBulletType(){
|
||||
this(1f, 1f, "shell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(mindustry.entities.type.Bullet b){
|
||||
super.update(b);
|
||||
|
||||
if(b.timer.get(0, 3 + b.fslope() * 2f)){
|
||||
Effects.effect(trailEffect, backColor, b.x, b.y, b.fslope() * 4f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float baseScale = 0.7f;
|
||||
float scale = (baseScale + b.fslope() * (1f - baseScale));
|
||||
|
||||
float height = bulletHeight * ((1f - bulletShrink) + bulletShrink * b.fout());
|
||||
|
||||
Draw.color(backColor);
|
||||
Draw.rect(backRegion, b.x, b.y, bulletWidth * scale, height * scale, b.rot() - 90);
|
||||
Draw.color(frontColor);
|
||||
Draw.rect(frontRegion, b.x, b.y, bulletWidth * scale, height * scale, b.rot() - 90);
|
||||
Draw.color();
|
||||
}
|
||||
}
|
||||
46
core/src/mindustry/entities/bullet/BasicBulletType.java
Normal file
46
core/src/mindustry/entities/bullet/BasicBulletType.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.graphics.g2d.TextureRegion;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.graphics.Pal;
|
||||
|
||||
/** An extended BulletType for most ammo-based bullets shot from turrets and units. */
|
||||
public class BasicBulletType extends BulletType{
|
||||
public Color backColor = Pal.bulletYellowBack, frontColor = Pal.bulletYellow;
|
||||
public float bulletWidth = 5f, bulletHeight = 7f;
|
||||
public float bulletShrink = 0.5f;
|
||||
public String bulletSprite;
|
||||
|
||||
public TextureRegion backRegion;
|
||||
public TextureRegion frontRegion;
|
||||
|
||||
public BasicBulletType(float speed, float damage, String bulletSprite){
|
||||
super(speed, damage);
|
||||
this.bulletSprite = bulletSprite;
|
||||
}
|
||||
|
||||
/** For mods. */
|
||||
public BasicBulletType(){
|
||||
this(1f, 1f, "bullet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
backRegion = Core.atlas.find(bulletSprite + "-back");
|
||||
frontRegion = Core.atlas.find(bulletSprite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
float height = bulletHeight * ((1f - bulletShrink) + bulletShrink * b.fout());
|
||||
|
||||
Draw.color(backColor);
|
||||
Draw.rect(backRegion, b.x, b.y, bulletWidth, height, b.rot() - 90);
|
||||
Draw.color(frontColor);
|
||||
Draw.rect(frontRegion, b.x, b.y, bulletWidth, height, b.rot() - 90);
|
||||
Draw.color();
|
||||
}
|
||||
}
|
||||
24
core/src/mindustry/entities/bullet/BombBulletType.java
Normal file
24
core/src/mindustry/entities/bullet/BombBulletType.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import mindustry.gen.*;
|
||||
|
||||
public class BombBulletType extends BasicBulletType{
|
||||
|
||||
public BombBulletType(float damage, float radius, String sprite){
|
||||
super(0.7f, 0, sprite);
|
||||
splashDamageRadius = radius;
|
||||
splashDamage = damage;
|
||||
collidesTiles = false;
|
||||
collides = false;
|
||||
bulletShrink = 0.7f;
|
||||
lifetime = 30f;
|
||||
drag = 0.05f;
|
||||
keepVelocity = false;
|
||||
collidesAir = false;
|
||||
hitSound = Sounds.explosion;
|
||||
}
|
||||
|
||||
public BombBulletType(){
|
||||
this(1f, 1f, "shell");
|
||||
}
|
||||
}
|
||||
165
core/src/mindustry/entities/bullet/BulletType.java
Normal file
165
core/src/mindustry/entities/bullet/BulletType.java
Normal file
@@ -0,0 +1,165 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.audio.*;
|
||||
import arc.math.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.Content;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.Effects.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
public abstract class BulletType extends Content{
|
||||
public float lifetime;
|
||||
public float speed;
|
||||
public float damage;
|
||||
public float hitSize = 4;
|
||||
public float drawSize = 40f;
|
||||
public float drag = 0f;
|
||||
public boolean pierce;
|
||||
public Effect hitEffect, despawnEffect;
|
||||
|
||||
/** Effect created when shooting. */
|
||||
public Effect shootEffect = Fx.shootSmall;
|
||||
/** Extra smoke effect created when shooting. */
|
||||
public Effect smokeEffect = Fx.shootSmallSmoke;
|
||||
/** Sound made when hitting something or getting removed.*/
|
||||
public Sound hitSound = Sounds.none;
|
||||
/** Extra inaccuracy when firing. */
|
||||
public float inaccuracy = 0f;
|
||||
/** How many bullets get created per ammo item/liquid. */
|
||||
public float ammoMultiplier = 2f;
|
||||
/** Multiplied by turret reload speed to get final shoot speed. */
|
||||
public float reloadMultiplier = 1f;
|
||||
/** Recoil from shooter entities. */
|
||||
public float recoil;
|
||||
|
||||
public float splashDamage = 0f;
|
||||
/** Knockback in velocity. */
|
||||
public float knockback;
|
||||
/** Whether this bullet hits tiles. */
|
||||
public boolean hitTiles = true;
|
||||
/** Status effect applied on hit. */
|
||||
public StatusEffect status = StatusEffects.none;
|
||||
/** Intensity of applied status effect in terms of duration. */
|
||||
public float statusDuration = 60 * 10f;
|
||||
/** Whether this bullet type collides with tiles. */
|
||||
public boolean collidesTiles = true;
|
||||
/** Whether this bullet type collides with tiles that are of the same team. */
|
||||
public boolean collidesTeam = false;
|
||||
/** Whether this bullet type collides with air units. */
|
||||
public boolean collidesAir = true;
|
||||
/** Whether this bullet types collides with anything at all. */
|
||||
public boolean collides = true;
|
||||
/** Whether velocity is inherited from the shooter. */
|
||||
public boolean keepVelocity = true;
|
||||
|
||||
//additional effects
|
||||
|
||||
public int fragBullets = 9;
|
||||
public float fragVelocityMin = 0.2f, fragVelocityMax = 1f;
|
||||
public BulletType fragBullet = null;
|
||||
|
||||
/** Use a negative value to disable splash damage. */
|
||||
public float splashDamageRadius = -1f;
|
||||
|
||||
public int incendAmount = 0;
|
||||
public float incendSpread = 8f;
|
||||
public float incendChance = 1f;
|
||||
|
||||
public float homingPower = 0f;
|
||||
public float homingRange = 50f;
|
||||
|
||||
public int lightining;
|
||||
public int lightningLength = 5;
|
||||
|
||||
public float hitShake = 0f;
|
||||
|
||||
public BulletType(float speed, float damage){
|
||||
this.speed = speed;
|
||||
this.damage = damage;
|
||||
lifetime = 40f;
|
||||
hitEffect = Fx.hitBulletSmall;
|
||||
despawnEffect = Fx.hitBulletSmall;
|
||||
}
|
||||
|
||||
/** Returns maximum distance the bullet this bullet type has can travel. */
|
||||
public float range(){
|
||||
return speed * lifetime * (1f - drag);
|
||||
}
|
||||
|
||||
public boolean collides(Bullet bullet, Tile tile){
|
||||
return true;
|
||||
}
|
||||
|
||||
public void hitTile(Bullet b, Tile tile){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b){
|
||||
hit(b, b.x, b.y);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float x, float y){
|
||||
Effects.effect(hitEffect, x, y, b.rot());
|
||||
hitSound.at(b);
|
||||
|
||||
Effects.shake(hitShake, hitShake, b);
|
||||
|
||||
if(fragBullet != null){
|
||||
for(int i = 0; i < fragBullets; i++){
|
||||
float len = Mathf.random(1f, 7f);
|
||||
float a = Mathf.random(360f);
|
||||
Bullet.create(fragBullet, b, x + Angles.trnsx(a, len), y + Angles.trnsy(a, len), a, Mathf.random(fragVelocityMin, fragVelocityMax));
|
||||
}
|
||||
}
|
||||
|
||||
if(Mathf.chance(incendChance)){
|
||||
Damage.createIncend(x, y, incendSpread, incendAmount);
|
||||
}
|
||||
|
||||
if(splashDamageRadius > 0){
|
||||
Damage.damage(b.getTeam(), x, y, splashDamageRadius, splashDamage * b.damageMultiplier());
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
Effects.effect(despawnEffect, b.x, b.y, b.rot());
|
||||
hitSound.at(b);
|
||||
|
||||
if(fragBullet != null || splashDamageRadius > 0){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
for(int i = 0; i < lightining; i++){
|
||||
Lightning.createLighting(Lightning.nextSeed(), b.getTeam(), Pal.surge, damage, b.x, b.y, Mathf.random(360f), lightningLength);
|
||||
}
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
}
|
||||
|
||||
public void init(Bullet b){
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
|
||||
if(homingPower > 0.0001f){
|
||||
TargetTrait target = Units.closestTarget(b.getTeam(), b.x, b.y, homingRange, e -> !e.isFlying() || collidesAir);
|
||||
if(target != null){
|
||||
b.velocity().setAngle(Mathf.slerpDelta(b.velocity().angle(), b.angleTo(target), 0.08f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.bullet;
|
||||
}
|
||||
}
|
||||
46
core/src/mindustry/entities/bullet/FlakBulletType.java
Normal file
46
core/src/mindustry/entities/bullet/FlakBulletType.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.math.geom.Rectangle;
|
||||
import arc.util.Time;
|
||||
import mindustry.content.Fx;
|
||||
import mindustry.entities.Units;
|
||||
import mindustry.entities.type.Bullet;
|
||||
|
||||
public class FlakBulletType extends BasicBulletType{
|
||||
protected static Rectangle rect = new Rectangle();
|
||||
protected float explodeRange = 30f;
|
||||
|
||||
public FlakBulletType(float speed, float damage){
|
||||
super(speed, damage, "shell");
|
||||
splashDamage = 15f;
|
||||
splashDamageRadius = 34f;
|
||||
hitEffect = Fx.flakExplosionBig;
|
||||
bulletWidth = 8f;
|
||||
bulletHeight = 10f;
|
||||
}
|
||||
|
||||
public FlakBulletType(){
|
||||
this(1f, 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
super.update(b);
|
||||
if(b.getData() instanceof Integer) return;
|
||||
|
||||
if(b.timer.get(2, 6)){
|
||||
Units.nearbyEnemies(b.getTeam(), rect.setSize(explodeRange * 2f).setCenter(b.x, b.y), unit -> {
|
||||
if(b.getData() instanceof Float) return;
|
||||
|
||||
if(unit.dst(b) < explodeRange){
|
||||
b.setData(0);
|
||||
Time.run(5f, () -> {
|
||||
if(b.getData() instanceof Integer){
|
||||
b.time(b.lifetime());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
54
core/src/mindustry/entities/bullet/HealBulletType.java
Normal file
54
core/src/mindustry/entities/bullet/HealBulletType.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
|
||||
public class HealBulletType extends BulletType{
|
||||
protected float healPercent = 3f;
|
||||
|
||||
public HealBulletType(float speed, float damage){
|
||||
super(speed, damage);
|
||||
|
||||
shootEffect = Fx.shootHeal;
|
||||
smokeEffect = Fx.hitLaser;
|
||||
hitEffect = Fx.hitLaser;
|
||||
despawnEffect = Fx.hitLaser;
|
||||
collidesTeam = true;
|
||||
}
|
||||
|
||||
public HealBulletType(){
|
||||
this(1f, 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collides(Bullet b, Tile tile){
|
||||
return tile.getTeam() != b.getTeam() || tile.entity.healthf() < 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Pal.heal);
|
||||
Lines.stroke(2f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.rot(), 7f);
|
||||
Draw.color(Color.white);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.rot(), 3f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTile(Bullet b, Tile tile){
|
||||
super.hit(b);
|
||||
tile = tile.link();
|
||||
|
||||
if(tile.entity != null && tile.getTeam() == b.getTeam() && !(tile.block() instanceof BuildBlock)){
|
||||
Effects.effect(Fx.healBlockFull, Pal.heal, tile.drawx(), tile.drawy(), tile.block().size);
|
||||
tile.entity.healBy(healPercent / 100f * tile.entity.maxHealth());
|
||||
}
|
||||
}
|
||||
}
|
||||
81
core/src/mindustry/entities/bullet/LiquidBulletType.java
Normal file
81
core/src/mindustry/entities/bullet/LiquidBulletType.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class LiquidBulletType extends BulletType{
|
||||
public @NonNull Liquid liquid;
|
||||
public float puddleSize = 5f;
|
||||
|
||||
public LiquidBulletType(@Nullable Liquid liquid){
|
||||
super(3.5f, 0);
|
||||
|
||||
if(liquid != null){
|
||||
this.liquid = liquid;
|
||||
this.status = liquid.effect;
|
||||
}
|
||||
|
||||
lifetime = 74f;
|
||||
statusDuration = 90f;
|
||||
despawnEffect = Fx.none;
|
||||
hitEffect = Fx.hitLiquid;
|
||||
smokeEffect = Fx.none;
|
||||
shootEffect = Fx.none;
|
||||
drag = 0.009f;
|
||||
knockback = 0.55f;
|
||||
}
|
||||
|
||||
public LiquidBulletType(){
|
||||
this(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float range(){
|
||||
return speed * lifetime / 2f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
super.update(b);
|
||||
|
||||
if(liquid.canExtinguish()){
|
||||
Tile tile = world.tileWorld(b.x, b.y);
|
||||
if(tile != null && Fire.has(tile.x, tile.y)){
|
||||
Fire.extinguish(tile, 100f);
|
||||
b.remove();
|
||||
hit(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Bullet b){
|
||||
Draw.color(liquid.color, Color.white, b.fout() / 100f);
|
||||
|
||||
Fill.circle(b.x, b.y, 0.5f + b.fout() * 2.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.effect(hitEffect, liquid.color, hitx, hity);
|
||||
Puddle.deposit(world.tileWorld(hitx, hity), liquid, puddleSize);
|
||||
|
||||
if(liquid.temperature <= 0.5f && liquid.flammability < 0.3f){
|
||||
float intensity = 400f;
|
||||
Fire.extinguish(world.tileWorld(hitx, hity), intensity);
|
||||
for(Point2 p : Geometry.d4){
|
||||
Fire.extinguish(world.tileWorld(hitx + p.x * tilesize, hity + p.y * tilesize), intensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
core/src/mindustry/entities/bullet/MassDriverBolt.java
Normal file
107
core/src/mindustry/entities/bullet/MassDriverBolt.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.math.Angles;
|
||||
import arc.math.Mathf;
|
||||
import mindustry.content.Fx;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.graphics.Pal;
|
||||
import mindustry.world.blocks.distribution.MassDriver.DriverBulletData;
|
||||
|
||||
import static mindustry.Vars.content;
|
||||
|
||||
public class MassDriverBolt extends BulletType{
|
||||
|
||||
public MassDriverBolt(){
|
||||
super(5.3f, 50);
|
||||
collidesTiles = false;
|
||||
lifetime = 200f;
|
||||
despawnEffect = Fx.smeltsmoke;
|
||||
hitEffect = Fx.hitBulletBig;
|
||||
drag = 0.005f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(mindustry.entities.type.Bullet b){
|
||||
float w = 11f, h = 13f;
|
||||
|
||||
Draw.color(Pal.bulletYellowBack);
|
||||
Draw.rect("shell-back", b.x, b.y, w, h, b.rot() + 90);
|
||||
|
||||
Draw.color(Pal.bulletYellow);
|
||||
Draw.rect("shell", b.x, b.y, w, h, b.rot() + 90);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(mindustry.entities.type.Bullet b){
|
||||
//data MUST be an instance of DriverBulletData
|
||||
if(!(b.getData() instanceof DriverBulletData)){
|
||||
hit(b);
|
||||
return;
|
||||
}
|
||||
|
||||
float hitDst = 7f;
|
||||
|
||||
DriverBulletData data = (DriverBulletData)b.getData();
|
||||
|
||||
//if the target is dead, just keep flying until the bullet explodes
|
||||
if(data.to.isDead()){
|
||||
return;
|
||||
}
|
||||
|
||||
float baseDst = data.from.dst(data.to);
|
||||
float dst1 = b.dst(data.from);
|
||||
float dst2 = b.dst(data.to);
|
||||
|
||||
boolean intersect = false;
|
||||
|
||||
//bullet has gone past the destination point: but did it intersect it?
|
||||
if(dst1 > baseDst){
|
||||
float angleTo = b.angleTo(data.to);
|
||||
float baseAngle = data.to.angleTo(data.from);
|
||||
|
||||
//if angles are nearby, then yes, it did
|
||||
if(Angles.near(angleTo, baseAngle, 2f)){
|
||||
intersect = true;
|
||||
//snap bullet position back; this is used for low-FPS situations
|
||||
b.set(data.to.x + Angles.trnsx(baseAngle, hitDst), data.to.y + Angles.trnsy(baseAngle, hitDst));
|
||||
}
|
||||
}
|
||||
|
||||
//if on course and it's in range of the target
|
||||
if(Math.abs(dst1 + dst2 - baseDst) < 4f && dst2 <= hitDst){
|
||||
intersect = true;
|
||||
} //else, bullet has gone off course, does not get recieved.
|
||||
|
||||
if(intersect){
|
||||
data.to.handlePayload(b, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void despawned(mindustry.entities.type.Bullet b){
|
||||
super.despawned(b);
|
||||
|
||||
if(!(b.getData() instanceof DriverBulletData)) return;
|
||||
|
||||
DriverBulletData data = (DriverBulletData)b.getData();
|
||||
|
||||
for(int i = 0; i < data.items.length; i++){
|
||||
int amountDropped = Mathf.random(0, data.items[i]);
|
||||
if(amountDropped > 0){
|
||||
float angle = b.rot() + Mathf.range(100f);
|
||||
Effects.effect(Fx.dropItem, Color.white, b.x, b.y, angle, content.item(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
super.hit(b, hitx, hity);
|
||||
despawned(b);
|
||||
}
|
||||
}
|
||||
42
core/src/mindustry/entities/bullet/MissileBulletType.java
Normal file
42
core/src/mindustry/entities/bullet/MissileBulletType.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package mindustry.entities.bullet;
|
||||
|
||||
import arc.graphics.Color;
|
||||
import arc.math.Mathf;
|
||||
import arc.util.Time;
|
||||
import mindustry.content.Fx;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.Pal;
|
||||
|
||||
public class MissileBulletType extends BasicBulletType{
|
||||
protected Color trailColor = Pal.missileYellowBack;
|
||||
|
||||
protected float weaveScale = 0f;
|
||||
protected float weaveMag = -1f;
|
||||
|
||||
public MissileBulletType(float speed, float damage, String bulletSprite){
|
||||
super(speed, damage, bulletSprite);
|
||||
backColor = Pal.missileYellowBack;
|
||||
frontColor = Pal.missileYellow;
|
||||
homingPower = 7f;
|
||||
hitSound = Sounds.explosion;
|
||||
}
|
||||
|
||||
public MissileBulletType(){
|
||||
this(1f, 1f, "missile");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Bullet b){
|
||||
super.update(b);
|
||||
|
||||
if(Mathf.chance(Time.delta() * 0.2)){
|
||||
Effects.effect(Fx.missileTrail, trailColor, b.x, b.y, 2f);
|
||||
}
|
||||
|
||||
if(weaveMag > 0){
|
||||
b.velocity().rotate(Mathf.sin(Time.time() + b.id * 4422, weaveScale, weaveMag) * Time.delta());
|
||||
}
|
||||
}
|
||||
}
|
||||
36
core/src/mindustry/entities/effect/Decal.java
Normal file
36
core/src/mindustry/entities/effect/Decal.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.math.Mathf;
|
||||
import mindustry.entities.EntityGroup;
|
||||
import mindustry.entities.type.TimedEntity;
|
||||
import mindustry.entities.traits.BelowLiquidTrait;
|
||||
import mindustry.entities.traits.DrawTrait;
|
||||
import mindustry.graphics.Pal;
|
||||
|
||||
import static mindustry.Vars.groundEffectGroup;
|
||||
|
||||
/**
|
||||
* Class for creating block rubble on the ground.
|
||||
*/
|
||||
public abstract class Decal extends TimedEntity implements BelowLiquidTrait, DrawTrait{
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return 3600;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
Draw.color(Pal.rubble.r, Pal.rubble.g, Pal.rubble.b, 1f - Mathf.curve(fin(), 0.98f));
|
||||
drawDecal();
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return groundEffectGroup;
|
||||
}
|
||||
|
||||
abstract void drawDecal();
|
||||
}
|
||||
229
core/src/mindustry/entities/effect/Fire.java
Normal file
229
core/src/mindustry/entities/effect/Fire.java
Normal file
@@ -0,0 +1,229 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Fire extends TimedEntity implements SaveTrait, SyncTrait{
|
||||
private static final IntMap<Fire> map = new IntMap<>();
|
||||
private static final float baseLifetime = 1000f, spreadChance = 0.05f, fireballChance = 0.07f;
|
||||
|
||||
private int loadedPosition = -1;
|
||||
private Tile tile;
|
||||
private Block block;
|
||||
private float baseFlammability = -1, puddleFlammability;
|
||||
private float lifetime;
|
||||
|
||||
/** Deserialization use only! */
|
||||
public Fire(){
|
||||
}
|
||||
|
||||
@Remote
|
||||
public static void onRemoveFire(int fid){
|
||||
fireGroup.removeByID(fid);
|
||||
}
|
||||
|
||||
/** Start a fire on the tile. If there already is a file there, refreshes its lifetime. */
|
||||
public static void create(Tile tile){
|
||||
if(net.client() || tile == null) return; //not clientside.
|
||||
|
||||
Fire fire = map.get(tile.pos());
|
||||
|
||||
if(fire == null){
|
||||
fire = new Fire();
|
||||
fire.tile = tile;
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.set(tile.worldx(), tile.worldy());
|
||||
fire.add();
|
||||
map.put(tile.pos(), fire);
|
||||
}else{
|
||||
fire.lifetime = baseLifetime;
|
||||
fire.time = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean has(int x, int y){
|
||||
if(!Structs.inBounds(x, y, world.width(), world.height()) || !map.containsKey(Pos.get(x, y))){
|
||||
return false;
|
||||
}
|
||||
Fire fire = map.get(Pos.get(x, y));
|
||||
return fire.isAdded() && fire.fin() < 1f && fire.tile != null && fire.tile.x == x && fire.tile.y == y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extinguish a fire by shortening its life. If there is no fire here, does nothing.
|
||||
*/
|
||||
public static void extinguish(Tile tile, float intensity){
|
||||
if(tile != null && map.containsKey(tile.pos())){
|
||||
Fire fire = map.get(tile.pos());
|
||||
fire.time += intensity * Time.delta();
|
||||
if(fire.time >= fire.lifetime()){
|
||||
Events.fire(Trigger.fireExtinguish);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeID getTypeID(){
|
||||
return TypeIDs.fire;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte version(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(Mathf.chance(0.1 * Time.delta())){
|
||||
Effects.effect(Fx.fire, x + Mathf.range(4f), y + Mathf.range(4f));
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.05 * Time.delta())){
|
||||
Effects.effect(Fx.fireSmoke, x + Mathf.range(4f), y + Mathf.range(4f));
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.001 * Time.delta())){
|
||||
Sounds.fire.at(this);
|
||||
}
|
||||
|
||||
time = Mathf.clamp(time + Time.delta(), 0, lifetime());
|
||||
map.put(tile.pos(), this);
|
||||
|
||||
if(net.client()){
|
||||
return;
|
||||
}
|
||||
|
||||
if(time >= lifetime() || tile == null){
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
TileEntity entity = tile.link().entity;
|
||||
boolean damage = entity != null;
|
||||
|
||||
float flammability = baseFlammability + puddleFlammability;
|
||||
|
||||
if(!damage && flammability <= 0){
|
||||
time += Time.delta() * 8;
|
||||
}
|
||||
|
||||
if(baseFlammability < 0 || block != tile.block()){
|
||||
baseFlammability = tile.block().getFlammability(tile);
|
||||
block = tile.block();
|
||||
}
|
||||
|
||||
if(damage){
|
||||
lifetime += Mathf.clamp(flammability / 8f, 0f, 0.6f) * Time.delta();
|
||||
}
|
||||
|
||||
if(flammability > 1f && Mathf.chance(spreadChance * Time.delta() * Mathf.clamp(flammability / 5f, 0.3f, 2f))){
|
||||
Point2 p = Geometry.d4[Mathf.random(3)];
|
||||
Tile other = world.tile(tile.x + p.x, tile.y + p.y);
|
||||
create(other);
|
||||
|
||||
if(Mathf.chance(fireballChance * Time.delta() * Mathf.clamp(flammability / 10f))){
|
||||
Call.createBullet(Bullets.fireball, Team.derelict, x, y, Mathf.random(360f), 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.1 * Time.delta())){
|
||||
Puddle p = Puddle.getPuddle(tile);
|
||||
if(p != null){
|
||||
puddleFlammability = p.getFlammability() / 3f;
|
||||
}else{
|
||||
puddleFlammability = 0;
|
||||
}
|
||||
|
||||
if(damage){
|
||||
entity.damage(0.4f);
|
||||
}
|
||||
Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, 3f,
|
||||
unit -> !unit.isFlying() && !unit.isImmune(StatusEffects.burning),
|
||||
unit -> unit.applyEffect(StatusEffects.burning, 60 * 5));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSave(DataOutput stream) throws IOException{
|
||||
stream.writeInt(tile.pos());
|
||||
stream.writeFloat(lifetime);
|
||||
stream.writeFloat(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSave(DataInput stream, byte version) throws IOException{
|
||||
this.loadedPosition = stream.readInt();
|
||||
this.lifetime = stream.readFloat();
|
||||
this.time = stream.readFloat();
|
||||
add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutput data) throws IOException{
|
||||
data.writeInt(tile.pos());
|
||||
data.writeFloat(lifetime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(DataInput data) throws IOException{
|
||||
int pos = data.readInt();
|
||||
this.lifetime = data.readFloat();
|
||||
|
||||
x = Pos.x(pos) * tilesize;
|
||||
y = Pos.y(pos) * tilesize;
|
||||
tile = world.tile(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
loadedPosition = -1;
|
||||
tile = null;
|
||||
baseFlammability = -1;
|
||||
puddleFlammability = 0f;
|
||||
incrementID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
if(loadedPosition != -1){
|
||||
map.put(loadedPosition, this);
|
||||
tile = world.tile(loadedPosition);
|
||||
set(tile.worldx(), tile.worldy());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
if(tile != null){
|
||||
Call.onRemoveFire(id);
|
||||
map.remove(tile.pos());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return fireGroup;
|
||||
}
|
||||
}
|
||||
90
core/src/mindustry/entities/effect/GroundEffectEntity.java
Normal file
90
core/src/mindustry/entities/effect/GroundEffectEntity.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.math.Mathf;
|
||||
import arc.util.Time;
|
||||
import mindustry.Vars;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.entities.Effects.Effect;
|
||||
import mindustry.entities.Effects.EffectRenderer;
|
||||
import mindustry.entities.type.EffectEntity;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
/**
|
||||
* A ground effect contains an effect that is rendered on the ground layer as opposed to the top layer.
|
||||
*/
|
||||
public class GroundEffectEntity extends EffectEntity{
|
||||
private boolean once;
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
GroundEffect effect = (GroundEffect)this.effect;
|
||||
|
||||
if(effect.isStatic){
|
||||
time += Time.delta();
|
||||
|
||||
time = Mathf.clamp(time, 0, effect.staticLife);
|
||||
|
||||
if(!once && time >= lifetime()){
|
||||
once = true;
|
||||
time = 0f;
|
||||
Tile tile = Vars.world.tileWorld(x, y);
|
||||
if(tile != null && tile.floor().isLiquid){
|
||||
remove();
|
||||
}
|
||||
}else if(once && time >= effect.staticLife){
|
||||
remove();
|
||||
}
|
||||
}else{
|
||||
super.update();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
GroundEffect effect = (GroundEffect)this.effect;
|
||||
|
||||
if(once && effect.isStatic)
|
||||
Effects.renderEffect(id, effect, color, lifetime(), rotation, x, y, data);
|
||||
else
|
||||
Effects.renderEffect(id, effect, color, time, rotation, x, y, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
super.reset();
|
||||
once = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An effect that is rendered on the ground layer as opposed to the top layer.
|
||||
*/
|
||||
public static class GroundEffect extends Effect{
|
||||
/**
|
||||
* How long this effect stays on the ground when static.
|
||||
*/
|
||||
public final float staticLife;
|
||||
/**
|
||||
* If true, this effect will stop and lie on the ground for a specific duration,
|
||||
* after its initial lifetime is over.
|
||||
*/
|
||||
public final boolean isStatic;
|
||||
|
||||
public GroundEffect(float life, float staticLife, EffectRenderer draw){
|
||||
super(life, draw);
|
||||
this.staticLife = staticLife;
|
||||
this.isStatic = true;
|
||||
}
|
||||
|
||||
public GroundEffect(boolean isStatic, float life, EffectRenderer draw){
|
||||
super(life, draw);
|
||||
this.staticLife = 0f;
|
||||
this.isStatic = isStatic;
|
||||
}
|
||||
|
||||
public GroundEffect(float life, EffectRenderer draw){
|
||||
super(life, draw);
|
||||
this.staticLife = 0f;
|
||||
this.isStatic = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
119
core/src/mindustry/entities/effect/ItemTransfer.java
Normal file
119
core/src/mindustry/entities/effect/ItemTransfer.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import mindustry.annotations.Annotations.Loc;
|
||||
import mindustry.annotations.Annotations.Remote;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.Interpolation;
|
||||
import arc.math.Mathf;
|
||||
import arc.math.geom.Position;
|
||||
import arc.math.geom.Vector2;
|
||||
import arc.util.Time;
|
||||
import arc.util.pooling.Pools;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.type.TimedEntity;
|
||||
import mindustry.entities.traits.DrawTrait;
|
||||
import mindustry.entities.type.Unit;
|
||||
import mindustry.graphics.Pal;
|
||||
import mindustry.type.Item;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class ItemTransfer extends TimedEntity implements DrawTrait{
|
||||
private Vector2 from = new Vector2();
|
||||
private Vector2 current = new Vector2();
|
||||
private Vector2 tovec = new Vector2();
|
||||
private Item item;
|
||||
private float seed;
|
||||
private Position to;
|
||||
private Runnable done;
|
||||
|
||||
public ItemTransfer(){
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void transferItemEffect(Item item, float x, float y, Unit to){
|
||||
if(to == null) return;
|
||||
create(item, x, y, to, () -> {
|
||||
});
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void transferItemToUnit(Item item, float x, float y, Unit to){
|
||||
if(to == null) return;
|
||||
create(item, x, y, to, () -> to.addItem(item));
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void transferItemTo(Item item, int amount, float x, float y, Tile tile){
|
||||
if(tile == null || tile.entity == null || tile.entity.items == null) return;
|
||||
for(int i = 0; i < Mathf.clamp(amount / 3, 1, 8); i++){
|
||||
Time.run(i * 3, () -> create(item, x, y, tile, () -> {}));
|
||||
}
|
||||
tile.entity.items.add(item, amount);
|
||||
}
|
||||
|
||||
public static void create(Item item, float fromx, float fromy, Position to, Runnable done){
|
||||
ItemTransfer tr = Pools.obtain(ItemTransfer.class, ItemTransfer::new);
|
||||
tr.item = item;
|
||||
tr.from.set(fromx, fromy);
|
||||
tr.to = to;
|
||||
tr.done = done;
|
||||
tr.seed = Mathf.range(1f);
|
||||
tr.add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
super.reset();
|
||||
item = null;
|
||||
to = null;
|
||||
done = null;
|
||||
from.setZero();
|
||||
current.setZero();
|
||||
tovec.setZero();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
if(done != null){
|
||||
done.run();
|
||||
}
|
||||
Pools.free(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(to == null){
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
super.update();
|
||||
current.set(from).interpolate(tovec.set(to.getX(), to.getY()), fin(), Interpolation.pow3);
|
||||
current.add(tovec.set(to.getX(), to.getY()).sub(from).nor().rotate90(1).scl(seed * fslope() * 10f));
|
||||
set(current.x, current.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
Lines.stroke(fslope() * 2f, Pal.accent);
|
||||
|
||||
Lines.circle(x, y, fslope() * 2f);
|
||||
|
||||
Draw.color(item.color);
|
||||
Fill.circle(x, y, fslope() * 1.5f);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return effectGroup;
|
||||
}
|
||||
}
|
||||
160
core/src/mindustry/entities/effect/Lightning.java
Normal file
160
core/src/mindustry/entities/effect/Lightning.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import mindustry.annotations.Annotations.Loc;
|
||||
import mindustry.annotations.Annotations.Remote;
|
||||
import arc.struct.Array;
|
||||
import arc.struct.IntSet;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.pooling.Pools;
|
||||
import mindustry.content.Bullets;
|
||||
import mindustry.entities.EntityGroup;
|
||||
import mindustry.entities.Units;
|
||||
import mindustry.entities.type.Bullet;
|
||||
import mindustry.entities.type.TimedEntity;
|
||||
import mindustry.entities.traits.DrawTrait;
|
||||
import mindustry.entities.traits.TimeTrait;
|
||||
import mindustry.entities.type.Unit;
|
||||
import mindustry.game.Team;
|
||||
import mindustry.gen.Call;
|
||||
import mindustry.graphics.Pal;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Lightning extends TimedEntity implements DrawTrait, TimeTrait{
|
||||
public static final float lifetime = 10f;
|
||||
|
||||
private static final RandomXS128 random = new RandomXS128();
|
||||
private static final Rectangle rect = new Rectangle();
|
||||
private static final Array<Unit> entities = new Array<>();
|
||||
private static final IntSet hit = new IntSet();
|
||||
private static final int maxChain = 8;
|
||||
private static final float hitRange = 30f;
|
||||
private static int lastSeed = 0;
|
||||
|
||||
private Array<Vector2> lines = new Array<>();
|
||||
private Color color = Pal.lancerLaser;
|
||||
|
||||
/** For pooling use only. Do not call directly! */
|
||||
public Lightning(){
|
||||
}
|
||||
|
||||
/** Create a lighting branch at a location. Use Team.none to damage everyone. */
|
||||
public static void create(Team team, Color color, float damage, float x, float y, float targetAngle, int length){
|
||||
Call.createLighting(nextSeed(), team, color, damage, x, y, targetAngle, length);
|
||||
}
|
||||
|
||||
public static int nextSeed(){
|
||||
return lastSeed++;
|
||||
}
|
||||
|
||||
/** Do not invoke! */
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void createLighting(int seed, Team team, Color color, float damage, float x, float y, float rotation, int length){
|
||||
|
||||
Lightning l = Pools.obtain(Lightning.class, Lightning::new);
|
||||
Float dmg = damage;
|
||||
|
||||
l.x = x;
|
||||
l.y = y;
|
||||
l.color = color;
|
||||
l.add();
|
||||
|
||||
random.setSeed(seed);
|
||||
hit.clear();
|
||||
|
||||
boolean[] bhit = {false};
|
||||
|
||||
for(int i = 0; i < length / 2; i++){
|
||||
Bullet.create(Bullets.damageLightning, l, team, x, y, 0f, 1f, 1f, dmg);
|
||||
l.lines.add(new Vector2(x + Mathf.range(3f), y + Mathf.range(3f)));
|
||||
|
||||
if(l.lines.size > 1){
|
||||
bhit[0] = false;
|
||||
Position from = l.lines.get(l.lines.size - 2);
|
||||
Position to = l.lines.get(l.lines.size - 1);
|
||||
world.raycastEach(world.toTile(from.getX()), world.toTile(from.getY()), world.toTile(to.getX()), world.toTile(to.getY()), (wx, wy) -> {
|
||||
|
||||
Tile tile = world.ltile(wx, wy);
|
||||
if(tile != null && tile.block().insulated){
|
||||
bhit[0] = true;
|
||||
//snap it instead of removing
|
||||
l.lines.get(l.lines.size -1).set(wx * tilesize, wy * tilesize);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if(bhit[0]) break;
|
||||
}
|
||||
|
||||
rect.setSize(hitRange).setCenter(x, y);
|
||||
entities.clear();
|
||||
if(hit.size < maxChain){
|
||||
Units.nearbyEnemies(team, rect, u -> {
|
||||
if(!hit.contains(u.getID())){
|
||||
entities.add(u);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Unit furthest = Geometry.findFurthest(x, y, entities);
|
||||
|
||||
if(furthest != null){
|
||||
hit.add(furthest.getID());
|
||||
x = furthest.x;
|
||||
y = furthest.y;
|
||||
}else{
|
||||
rotation += random.range(20f);
|
||||
|
||||
x += Angles.trnsx(rotation, hitRange / 2f);
|
||||
y += Angles.trnsy(rotation, hitRange / 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
super.reset();
|
||||
color = Pal.lancerLaser;
|
||||
lines.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
super.removed();
|
||||
Pools.free(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
Lines.stroke(3f * fout());
|
||||
Draw.color(color, Color.white, fin());
|
||||
Lines.beginLine();
|
||||
|
||||
Lines.linePoint(x, y);
|
||||
for(Position p : lines){
|
||||
Lines.linePoint(p.getX(), p.getY());
|
||||
}
|
||||
Lines.endLine();
|
||||
|
||||
int i = 0;
|
||||
|
||||
for(Position p : lines){
|
||||
Fill.square(p.getX(), p.getY(), (5f - (float)i++ / lines.size * 2f) * fout(), 45);
|
||||
}
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return bulletGroup;
|
||||
}
|
||||
}
|
||||
322
core/src/mindustry/entities/effect/Puddle.java
Normal file
322
core/src/mindustry/entities/effect/Puddle.java
Normal file
@@ -0,0 +1,322 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.pooling.Pool.*;
|
||||
import arc.util.pooling.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Puddle extends SolidEntity implements SaveTrait, Poolable, DrawTrait, SyncTrait{
|
||||
private static final IntMap<Puddle> map = new IntMap<>();
|
||||
private static final float maxLiquid = 70f;
|
||||
private static final int maxGeneration = 2;
|
||||
private static final Color tmp = new Color();
|
||||
private static final Rectangle rect = new Rectangle();
|
||||
private static final Rectangle rect2 = new Rectangle();
|
||||
private static int seeds;
|
||||
|
||||
private int loadedPosition = -1;
|
||||
|
||||
private float updateTime;
|
||||
private float lastRipple;
|
||||
private Tile tile;
|
||||
private Liquid liquid;
|
||||
private float amount, targetAmount;
|
||||
private float accepting;
|
||||
private byte generation;
|
||||
|
||||
/** Deserialization use only! */
|
||||
public Puddle(){
|
||||
}
|
||||
|
||||
/** Deposists a puddle between tile and source. */
|
||||
public static void deposit(Tile tile, Tile source, Liquid liquid, float amount){
|
||||
deposit(tile, source, liquid, amount, 0);
|
||||
}
|
||||
|
||||
/** Deposists a puddle at a tile. */
|
||||
public static void deposit(Tile tile, Liquid liquid, float amount){
|
||||
deposit(tile, tile, liquid, amount, 0);
|
||||
}
|
||||
|
||||
/** Returns the puddle on the specified tile. May return null. */
|
||||
public static Puddle getPuddle(Tile tile){
|
||||
return map.get(tile.pos());
|
||||
}
|
||||
|
||||
private static void deposit(Tile tile, Tile source, Liquid liquid, float amount, int generation){
|
||||
if(tile == null) return;
|
||||
|
||||
if(tile.floor().isLiquid && !canStayOn(liquid, tile.floor().liquidDrop)){
|
||||
reactPuddle(tile.floor().liquidDrop, liquid, amount, tile,
|
||||
(tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
|
||||
if(generation == 0 && p != null && p.lastRipple <= Time.time() - 40f){
|
||||
Effects.effect(Fx.ripple, tile.floor().liquidDrop.color,
|
||||
(tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
|
||||
p.lastRipple = Time.time();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Puddle p = map.get(tile.pos());
|
||||
if(p == null){
|
||||
if(net.client()) return; //not clientside.
|
||||
|
||||
Puddle puddle = Pools.obtain(Puddle.class, Puddle::new);
|
||||
puddle.tile = tile;
|
||||
puddle.liquid = liquid;
|
||||
puddle.amount = amount;
|
||||
puddle.generation = (byte)generation;
|
||||
puddle.set((tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
|
||||
puddle.add();
|
||||
map.put(tile.pos(), puddle);
|
||||
}else if(p.liquid == liquid){
|
||||
p.accepting = Math.max(amount, p.accepting);
|
||||
|
||||
if(generation == 0 && p.lastRipple <= Time.time() - 40f && p.amount >= maxLiquid / 2f){
|
||||
Effects.effect(Fx.ripple, p.liquid.color, (tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
|
||||
p.lastRipple = Time.time();
|
||||
}
|
||||
}else{
|
||||
p.amount += reactPuddle(p.liquid, liquid, amount, p.tile, p.x, p.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the first liquid can 'stay' on the second one.
|
||||
* Currently, the only place where this can happen is oil on water.
|
||||
*/
|
||||
private static boolean canStayOn(Liquid liquid, Liquid other){
|
||||
return liquid == Liquids.oil && other == Liquids.water;
|
||||
}
|
||||
|
||||
/** Reacts two liquids together at a location. */
|
||||
private static float reactPuddle(Liquid dest, Liquid liquid, float amount, Tile tile, float x, float y){
|
||||
if((dest.flammability > 0.3f && liquid.temperature > 0.7f) ||
|
||||
(liquid.flammability > 0.3f && dest.temperature > 0.7f)){ //flammable liquid + hot liquid
|
||||
Fire.create(tile);
|
||||
if(Mathf.chance(0.006 * amount)){
|
||||
Call.createBullet(Bullets.fireball, Team.derelict, x, y, Mathf.random(360f), 1f, 1f);
|
||||
}
|
||||
}else if(dest.temperature > 0.7f && liquid.temperature < 0.55f){ //cold liquid poured onto hot puddle
|
||||
if(Mathf.chance(0.5f * amount)){
|
||||
Effects.effect(Fx.steam, x, y);
|
||||
}
|
||||
return -0.1f * amount;
|
||||
}else if(liquid.temperature > 0.7f && dest.temperature < 0.55f){ //hot liquid poured onto cold puddle
|
||||
if(Mathf.chance(0.8f * amount)){
|
||||
Effects.effect(Fx.steam, x, y);
|
||||
}
|
||||
return -0.4f * amount;
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void onPuddleRemoved(int puddleid){
|
||||
puddleGroup.removeByID(puddleid);
|
||||
}
|
||||
|
||||
public float getFlammability(){
|
||||
return liquid.flammability * amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeID getTypeID(){
|
||||
return TypeIDs.puddle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte version(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitbox(Rectangle rectangle){
|
||||
rectangle.setCenter(x, y).setSize(tilesize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitboxTile(Rectangle rectangle){
|
||||
rectangle.setCenter(x, y).setSize(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
|
||||
//no updating happens clientside
|
||||
if(net.client()){
|
||||
amount = Mathf.lerpDelta(amount, targetAmount, 0.15f);
|
||||
}else{
|
||||
//update code
|
||||
float addSpeed = accepting > 0 ? 3f : 0f;
|
||||
|
||||
amount -= Time.delta() * (1f - liquid.viscosity) / (5f + addSpeed);
|
||||
|
||||
amount += accepting;
|
||||
accepting = 0f;
|
||||
|
||||
if(amount >= maxLiquid / 1.5f && generation < maxGeneration){
|
||||
float deposited = Math.min((amount - maxLiquid / 1.5f) / 4f, 0.3f) * Time.delta();
|
||||
for(Point2 point : Geometry.d4){
|
||||
Tile other = world.tile(tile.x + point.x, tile.y + point.y);
|
||||
if(other != null && other.block() == Blocks.air){
|
||||
deposit(other, tile, liquid, deposited, generation + 1);
|
||||
amount -= deposited / 2f; //tweak to speed up/slow down puddle propagation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
amount = Mathf.clamp(amount, 0, maxLiquid);
|
||||
|
||||
if(amount <= 0f){
|
||||
Call.onPuddleRemoved(getID());
|
||||
}
|
||||
}
|
||||
|
||||
//effects-only code
|
||||
if(amount >= maxLiquid / 2f && updateTime <= 0f){
|
||||
Units.nearby(rect.setSize(Mathf.clamp(amount / (maxLiquid / 1.5f)) * 10f).setCenter(x, y), unit -> {
|
||||
if(unit.isFlying()) return;
|
||||
|
||||
unit.hitbox(rect2);
|
||||
if(!rect.overlaps(rect2)) return;
|
||||
|
||||
unit.applyEffect(liquid.effect, 60 * 2);
|
||||
|
||||
if(unit.velocity().len() > 0.1){
|
||||
Effects.effect(Fx.ripple, liquid.color, unit.x, unit.y);
|
||||
}
|
||||
});
|
||||
|
||||
if(liquid.temperature > 0.7f && (tile.link().entity != null) && Mathf.chance(0.3 * Time.delta())){
|
||||
Fire.create(tile);
|
||||
}
|
||||
|
||||
updateTime = 20f;
|
||||
}
|
||||
|
||||
updateTime -= Time.delta();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
seeds = id;
|
||||
boolean onLiquid = tile.floor().isLiquid;
|
||||
float f = Mathf.clamp(amount / (maxLiquid / 1.5f));
|
||||
float smag = onLiquid ? 0.8f : 0f;
|
||||
float sscl = 20f;
|
||||
|
||||
Draw.color(tmp.set(liquid.color).shiftValue(-0.05f));
|
||||
Fill.circle(x + Mathf.sin(Time.time() + seeds * 532, sscl, smag), y + Mathf.sin(Time.time() + seeds * 53, sscl, smag), f * 8f);
|
||||
Angles.randLenVectors(id, 3, f * 6f, (ex, ey) -> {
|
||||
Fill.circle(x + ex + Mathf.sin(Time.time() + seeds * 532, sscl, smag),
|
||||
y + ey + Mathf.sin(Time.time() + seeds * 53, sscl, smag), f * 5f);
|
||||
seeds++;
|
||||
});
|
||||
Draw.color();
|
||||
|
||||
if(liquid.lightColor.a > 0.001f && f > 0){
|
||||
Color color = liquid.lightColor;
|
||||
float opacity = color.a * f;
|
||||
renderer.lights.add(tile.drawx(), tile.drawy(), 30f * f, color, opacity * 0.8f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drawSize(){
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSave(DataOutput stream) throws IOException{
|
||||
stream.writeInt(tile.pos());
|
||||
stream.writeFloat(x);
|
||||
stream.writeFloat(y);
|
||||
stream.writeByte(liquid.id);
|
||||
stream.writeFloat(amount);
|
||||
stream.writeByte(generation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSave(DataInput stream, byte version) throws IOException{
|
||||
this.loadedPosition = stream.readInt();
|
||||
this.x = stream.readFloat();
|
||||
this.y = stream.readFloat();
|
||||
this.liquid = content.liquid(stream.readByte());
|
||||
this.amount = stream.readFloat();
|
||||
this.generation = stream.readByte();
|
||||
add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
loadedPosition = -1;
|
||||
tile = null;
|
||||
liquid = null;
|
||||
amount = 0;
|
||||
generation = 0;
|
||||
accepting = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
if(loadedPosition != -1){
|
||||
map.put(loadedPosition, this);
|
||||
tile = world.tile(loadedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
if(tile != null){
|
||||
map.remove(tile.pos());
|
||||
}
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutput data) throws IOException{
|
||||
data.writeFloat(x);
|
||||
data.writeFloat(y);
|
||||
data.writeByte(liquid.id);
|
||||
data.writeShort((short)(amount * 4));
|
||||
data.writeInt(tile.pos());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(DataInput data) throws IOException{
|
||||
x = data.readFloat();
|
||||
y = data.readFloat();
|
||||
liquid = content.liquid(data.readByte());
|
||||
targetAmount = data.readShort() / 4f;
|
||||
int pos = data.readInt();
|
||||
tile = world.tile(pos);
|
||||
|
||||
map.put(pos, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return puddleGroup;
|
||||
}
|
||||
}
|
||||
41
core/src/mindustry/entities/effect/RubbleDecal.java
Normal file
41
core/src/mindustry/entities/effect/RubbleDecal.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.graphics.g2d.TextureRegion;
|
||||
import arc.math.Mathf;
|
||||
|
||||
import static mindustry.Vars.headless;
|
||||
|
||||
public class RubbleDecal extends Decal{
|
||||
private TextureRegion region;
|
||||
|
||||
/** Creates a rubble effect at a position. Provide a block size to use. */
|
||||
public static void create(float x, float y, int size){
|
||||
if(headless) return;
|
||||
|
||||
RubbleDecal decal = new RubbleDecal();
|
||||
decal.region = Core.atlas.find("rubble-" + size + "-" + Mathf.randomSeed(decal.id, 0, 1));
|
||||
|
||||
if(!Core.atlas.isFound(decal.region)){
|
||||
return;
|
||||
}
|
||||
|
||||
decal.set(x, y);
|
||||
decal.add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return 8200f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawDecal(){
|
||||
if(!Core.atlas.isFound(region)){
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
Draw.rect(region, x, y, Mathf.randomSeed(id, 0, 4) * 90);
|
||||
}
|
||||
}
|
||||
49
core/src/mindustry/entities/effect/ScorchDecal.java
Normal file
49
core/src/mindustry/entities/effect/ScorchDecal.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package mindustry.entities.effect;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.g2d.Draw;
|
||||
import arc.graphics.g2d.TextureRegion;
|
||||
import arc.math.Angles;
|
||||
import arc.math.Mathf;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.headless;
|
||||
import static mindustry.Vars.world;
|
||||
|
||||
public class ScorchDecal extends Decal{
|
||||
private static final int scorches = 5;
|
||||
private static final TextureRegion[] regions = new TextureRegion[scorches];
|
||||
|
||||
public static void create(float x, float y){
|
||||
if(headless) return;
|
||||
|
||||
if(regions[0] == null || regions[0].getTexture().isDisposed()){
|
||||
for(int i = 0; i < regions.length; i++){
|
||||
regions[i] = Core.atlas.find("scorch" + (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
|
||||
if(tile == null || tile.floor().liquidDrop != null) return;
|
||||
|
||||
ScorchDecal decal = new ScorchDecal();
|
||||
decal.set(x, y);
|
||||
decal.add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawDecal(){
|
||||
for(int i = 0; i < 3; i++){
|
||||
TextureRegion region = regions[Mathf.randomSeed(id - i, 0, scorches - 1)];
|
||||
float rotation = Mathf.randomSeed(id + i, 0, 360);
|
||||
float space = 1.5f + Mathf.randomSeed(id + i + 1, 0, 20) / 10f;
|
||||
Draw.rect(region,
|
||||
x + Angles.trnsx(rotation, space),
|
||||
y + Angles.trnsy(rotation, space) + region.getHeight() / 2f * Draw.scl,
|
||||
region.getWidth() * Draw.scl,
|
||||
region.getHeight() * Draw.scl,
|
||||
region.getWidth() / 2f * Draw.scl, 0, rotation - 90);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
core/src/mindustry/entities/traits/AbsorbTrait.java
Normal file
13
core/src/mindustry/entities/traits/AbsorbTrait.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
public interface AbsorbTrait extends Entity, TeamTrait, DamageTrait{
|
||||
void absorb();
|
||||
|
||||
default boolean canBeAbsorbed(){
|
||||
return true;
|
||||
}
|
||||
|
||||
default float getShieldDamage(){
|
||||
return damage();
|
||||
}
|
||||
}
|
||||
7
core/src/mindustry/entities/traits/BelowLiquidTrait.java
Normal file
7
core/src/mindustry/entities/traits/BelowLiquidTrait.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
/**
|
||||
* A flag interface for marking an effect as appearing below liquids.
|
||||
*/
|
||||
public interface BelowLiquidTrait{
|
||||
}
|
||||
22
core/src/mindustry/entities/traits/BuilderMinerTrait.java
Normal file
22
core/src/mindustry/entities/traits/BuilderMinerTrait.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
/** A class for gracefully merging mining and building traits.*/
|
||||
public interface BuilderMinerTrait extends MinerTrait, BuilderTrait{
|
||||
|
||||
default void updateMechanics(){
|
||||
updateBuilding();
|
||||
|
||||
//mine only when not building
|
||||
if(buildRequest() == null){
|
||||
updateMining();
|
||||
}
|
||||
}
|
||||
|
||||
default void drawMechanics(){
|
||||
if(isBuilding()){
|
||||
drawBuilding();
|
||||
}else{
|
||||
drawMining();
|
||||
}
|
||||
}
|
||||
}
|
||||
394
core/src/mindustry/entities/traits/BuilderTrait.java
Normal file
394
core/src/mindustry/entities/traits/BuilderTrait.java
Normal file
@@ -0,0 +1,394 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.Queue;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.BuildBlock.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.entities.traits.BuilderTrait.BuildDataStatic.*;
|
||||
|
||||
/** Interface for units that build things.*/
|
||||
public interface BuilderTrait extends Entity, TeamTrait{
|
||||
//these are not instance variables!
|
||||
float placeDistance = 220f;
|
||||
float mineDistance = 70f;
|
||||
|
||||
/** Updates building mechanism for this unit.*/
|
||||
default void updateBuilding(){
|
||||
float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : placeDistance;
|
||||
Unit unit = (Unit)this;
|
||||
|
||||
Iterator<BuildRequest> it = buildQueue().iterator();
|
||||
while(it.hasNext()){
|
||||
BuildRequest req = it.next();
|
||||
Tile tile = world.tile(req.x, req.y);
|
||||
if(tile == null || (req.breaking && tile.block() == Blocks.air) || (!req.breaking && (tile.rotation() == req.rotation || !req.block.rotate) && tile.block() == req.block)){
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
TileEntity core = unit.getClosestCore();
|
||||
|
||||
//nothing to build.
|
||||
if(buildRequest() == null) return;
|
||||
|
||||
//find the next build request
|
||||
if(buildQueue().size > 1){
|
||||
int total = 0;
|
||||
BuildRequest req;
|
||||
while((dst((req = buildRequest()).tile()) > finalPlaceDst || shouldSkip(req, core)) && total < buildQueue().size){
|
||||
buildQueue().removeFirst();
|
||||
buildQueue().addLast(req);
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
BuildRequest current = buildRequest();
|
||||
|
||||
if(dst(current.tile()) > finalPlaceDst) return;
|
||||
|
||||
Tile tile = world.tile(current.x, current.y);
|
||||
|
||||
if(!(tile.block() instanceof BuildBlock)){
|
||||
if(!current.initialized && canCreateBlocks() && !current.breaking && Build.validPlace(getTeam(), current.x, current.y, current.block, current.rotation)){
|
||||
Call.beginPlace(getTeam(), current.x, current.y, current.block, current.rotation);
|
||||
}else if(!current.initialized && canCreateBlocks() && current.breaking && Build.validBreak(getTeam(), current.x, current.y)){
|
||||
Call.beginBreak(getTeam(), current.x, current.y);
|
||||
}else{
|
||||
buildQueue().removeFirst();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(tile.entity instanceof BuildEntity && !current.initialized){
|
||||
Core.app.post(() -> Events.fire(new BuildSelectEvent(tile, unit.getTeam(), this, current.breaking)));
|
||||
current.initialized = true;
|
||||
}
|
||||
|
||||
//if there is no core to build with or no build entity, stop building!
|
||||
if((core == null && !state.rules.infiniteResources) || !(tile.entity instanceof BuildEntity)){
|
||||
return;
|
||||
}
|
||||
|
||||
//otherwise, update it.
|
||||
BuildEntity entity = tile.ent();
|
||||
|
||||
if(entity == null){
|
||||
return;
|
||||
}
|
||||
|
||||
if(unit.dst(tile) <= finalPlaceDst){
|
||||
unit.rotation = Mathf.slerpDelta(unit.rotation, unit.angleTo(entity), 0.4f);
|
||||
}
|
||||
|
||||
if(current.breaking){
|
||||
entity.deconstruct(unit, core, 1f / entity.buildCost * Time.delta() * getBuildPower(tile) * state.rules.buildSpeedMultiplier);
|
||||
}else{
|
||||
if(entity.construct(unit, core, 1f / entity.buildCost * Time.delta() * getBuildPower(tile) * state.rules.buildSpeedMultiplier, current.hasConfig)){
|
||||
if(current.hasConfig){
|
||||
Call.onTileConfig(null, tile, current.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current.stuck = Mathf.equal(current.progress, entity.progress);
|
||||
current.progress = entity.progress;
|
||||
}
|
||||
|
||||
/** @return whether this request should be skipped, in favor of the next one. */
|
||||
default boolean shouldSkip(BuildRequest request, @Nullable TileEntity core){
|
||||
//requests that you have at least *started* are considered
|
||||
if(state.rules.infiniteResources || request.breaking || !request.initialized || core == null) return false;
|
||||
return request.stuck && !core.items.has(request.block.requirements);
|
||||
}
|
||||
|
||||
/** Returns the queue for storing build requests. */
|
||||
Queue<BuildRequest> buildQueue();
|
||||
|
||||
/** Build power, can be any float. 1 = builds recipes in normal time, 0 = doesn't build at all. */
|
||||
float getBuildPower(Tile tile);
|
||||
|
||||
/** Whether this type of builder can begin creating new blocks. */
|
||||
default boolean canCreateBlocks(){
|
||||
return true;
|
||||
}
|
||||
|
||||
default void writeBuilding(DataOutput output) throws IOException{
|
||||
BuildRequest request = buildRequest();
|
||||
|
||||
if(request != null && (request.block != null || request.breaking)){
|
||||
output.writeByte(request.breaking ? 1 : 0);
|
||||
output.writeInt(Pos.get(request.x, request.y));
|
||||
output.writeFloat(request.progress);
|
||||
if(!request.breaking){
|
||||
output.writeShort(request.block.id);
|
||||
output.writeByte(request.rotation);
|
||||
}
|
||||
}else{
|
||||
output.writeByte(-1);
|
||||
}
|
||||
}
|
||||
|
||||
default void readBuilding(DataInput input) throws IOException{
|
||||
readBuilding(input, true);
|
||||
}
|
||||
|
||||
default void readBuilding(DataInput input, boolean applyChanges) throws IOException{
|
||||
if(applyChanges) buildQueue().clear();
|
||||
|
||||
byte type = input.readByte();
|
||||
if(type != -1){
|
||||
int position = input.readInt();
|
||||
float progress = input.readFloat();
|
||||
BuildRequest request;
|
||||
|
||||
if(type == 1){ //remove
|
||||
request = new BuildRequest(Pos.x(position), Pos.y(position));
|
||||
}else{ //place
|
||||
short block = input.readShort();
|
||||
byte rotation = input.readByte();
|
||||
request = new BuildRequest(Pos.x(position), Pos.y(position), rotation, content.block(block));
|
||||
}
|
||||
|
||||
request.progress = progress;
|
||||
|
||||
if(applyChanges){
|
||||
buildQueue().addLast(request);
|
||||
}else if(isBuilding()){
|
||||
BuildRequest last = buildRequest();
|
||||
last.progress = progress;
|
||||
if(last.tile() != null && last.tile().entity instanceof BuildEntity){
|
||||
((BuildEntity)last.tile().entity).progress = progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether this builder's place queue contains items. */
|
||||
default boolean isBuilding(){
|
||||
return buildQueue().size != 0;
|
||||
}
|
||||
|
||||
/** Clears the placement queue. */
|
||||
default void clearBuilding(){
|
||||
buildQueue().clear();
|
||||
}
|
||||
|
||||
/** Add another build requests to the tail of the queue, if it doesn't exist there yet. */
|
||||
default void addBuildRequest(BuildRequest place){
|
||||
addBuildRequest(place, true);
|
||||
}
|
||||
|
||||
/** Add another build requests to the queue, if it doesn't exist there yet. */
|
||||
default void addBuildRequest(BuildRequest place, boolean tail){
|
||||
BuildRequest replace = null;
|
||||
for(BuildRequest request : buildQueue()){
|
||||
if(request.x == place.x && request.y == place.y){
|
||||
replace = request;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(replace != null){
|
||||
buildQueue().remove(replace);
|
||||
}
|
||||
Tile tile = world.tile(place.x, place.y);
|
||||
if(tile != null && tile.entity instanceof BuildEntity){
|
||||
place.progress = tile.<BuildEntity>ent().progress;
|
||||
}
|
||||
if(tail){
|
||||
buildQueue().addLast(place);
|
||||
}else{
|
||||
buildQueue().addFirst(place);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the build requests currently active, or the one at the top of the queue.
|
||||
* May return null.
|
||||
*/
|
||||
default @Nullable
|
||||
BuildRequest buildRequest(){
|
||||
return buildQueue().size == 0 ? null : buildQueue().first();
|
||||
}
|
||||
|
||||
//due to iOS weirdness, this is apparently required
|
||||
class BuildDataStatic{
|
||||
static Vector2[] tmptr = new Vector2[]{new Vector2(), new Vector2(), new Vector2(), new Vector2()};
|
||||
}
|
||||
|
||||
/** Draw placement effects for an entity. */
|
||||
default void drawBuilding(){
|
||||
if(!isBuilding()) return;
|
||||
|
||||
Unit unit = (Unit)this;
|
||||
BuildRequest request = buildRequest();
|
||||
Tile tile = world.tile(request.x, request.y);
|
||||
|
||||
if(dst(tile) > placeDistance && !state.isEditor()){
|
||||
return;
|
||||
}
|
||||
|
||||
Lines.stroke(1f, Pal.accent);
|
||||
float focusLen = 3.8f + Mathf.absin(Time.time(), 1.1f, 0.6f);
|
||||
float px = unit.x + Angles.trnsx(unit.rotation, focusLen);
|
||||
float py = unit.y + Angles.trnsy(unit.rotation, focusLen);
|
||||
|
||||
float sz = Vars.tilesize * tile.block().size / 2f;
|
||||
float ang = unit.angleTo(tile);
|
||||
|
||||
tmptr[0].set(tile.drawx() - sz, tile.drawy() - sz);
|
||||
tmptr[1].set(tile.drawx() + sz, tile.drawy() - sz);
|
||||
tmptr[2].set(tile.drawx() - sz, tile.drawy() + sz);
|
||||
tmptr[3].set(tile.drawx() + sz, tile.drawy() + sz);
|
||||
|
||||
Arrays.sort(tmptr, (a, b) -> -Float.compare(Angles.angleDist(Angles.angle(unit.x, unit.y, a.x, a.y), ang),
|
||||
Angles.angleDist(Angles.angle(unit.x, unit.y, b.x, b.y), ang)));
|
||||
|
||||
float x1 = tmptr[0].x, y1 = tmptr[0].y,
|
||||
x3 = tmptr[1].x, y3 = tmptr[1].y;
|
||||
|
||||
Draw.alpha(1f);
|
||||
|
||||
Lines.line(px, py, x1, y1);
|
||||
Lines.line(px, py, x3, y3);
|
||||
|
||||
Fill.circle(px, py, 1.6f + Mathf.absin(Time.time(), 0.8f, 1.5f));
|
||||
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
/** Class for storing build requests. Can be either a place or remove request. */
|
||||
class BuildRequest{
|
||||
/** Position and rotation of this request. */
|
||||
public int x, y, rotation;
|
||||
/** Block being placed. If null, this is a breaking request.*/
|
||||
public @Nullable Block block;
|
||||
/** Whether this is a break request.*/
|
||||
public boolean breaking;
|
||||
/** Whether this request comes with a config int. If yes, any blocks placed with this request will not call playerPlaced.*/
|
||||
public boolean hasConfig;
|
||||
/** Config int. Not used unless hasConfig is true.*/
|
||||
public int config;
|
||||
/** Original position, only used in schematics.*/
|
||||
public int originalX, originalY, originalWidth, originalHeight;
|
||||
|
||||
/** Last progress.*/
|
||||
public float progress;
|
||||
/** Whether construction has started for this request, and other special variables.*/
|
||||
public boolean initialized, worldContext = true, stuck;
|
||||
|
||||
/** Visual scale. Used only for rendering.*/
|
||||
public float animScale = 0f;
|
||||
|
||||
/** This creates a build request. */
|
||||
public BuildRequest(int x, int y, int rotation, Block block){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.block = block;
|
||||
this.breaking = false;
|
||||
}
|
||||
|
||||
/** This creates a remove request. */
|
||||
public BuildRequest(int x, int y){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = -1;
|
||||
this.block = world.tile(x, y).block();
|
||||
this.breaking = true;
|
||||
}
|
||||
|
||||
public BuildRequest(){
|
||||
|
||||
}
|
||||
|
||||
public BuildRequest copy(){
|
||||
BuildRequest copy = new BuildRequest();
|
||||
copy.x = x;
|
||||
copy.y = y;
|
||||
copy.rotation = rotation;
|
||||
copy.block = block;
|
||||
copy.breaking = breaking;
|
||||
copy.hasConfig = hasConfig;
|
||||
copy.config = config;
|
||||
copy.originalX = originalX;
|
||||
copy.originalY = originalY;
|
||||
copy.progress = progress;
|
||||
copy.initialized = initialized;
|
||||
copy.animScale = animScale;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public BuildRequest original(int x, int y, int originalWidth, int originalHeight){
|
||||
originalX = x;
|
||||
originalY = y;
|
||||
this.originalWidth = originalWidth;
|
||||
this.originalHeight = originalHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Rectangle bounds(Rectangle rect){
|
||||
if(breaking){
|
||||
return rect.set(-100f, -100f, 0f, 0f);
|
||||
}else{
|
||||
return block.bounds(x, y, rect);
|
||||
}
|
||||
}
|
||||
|
||||
public BuildRequest set(int x, int y, int rotation, Block block){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.block = block;
|
||||
this.breaking = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float drawx(){
|
||||
return x*tilesize + block.offset();
|
||||
}
|
||||
|
||||
public float drawy(){
|
||||
return y*tilesize + block.offset();
|
||||
}
|
||||
|
||||
public BuildRequest configure(int config){
|
||||
this.config = config;
|
||||
this.hasConfig = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @Nullable Tile tile(){
|
||||
return world.tile(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "BuildRequest{" +
|
||||
"x=" + x +
|
||||
", y=" + y +
|
||||
", rotation=" + rotation +
|
||||
", recipe=" + block +
|
||||
", breaking=" + breaking +
|
||||
", progress=" + progress +
|
||||
", initialized=" + initialized +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
9
core/src/mindustry/entities/traits/DamageTrait.java
Normal file
9
core/src/mindustry/entities/traits/DamageTrait.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
public interface DamageTrait{
|
||||
float damage();
|
||||
|
||||
default void killed(Entity other){
|
||||
|
||||
}
|
||||
}
|
||||
10
core/src/mindustry/entities/traits/DrawTrait.java
Normal file
10
core/src/mindustry/entities/traits/DrawTrait.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
public interface DrawTrait extends Entity{
|
||||
|
||||
default float drawSize(){
|
||||
return 20f;
|
||||
}
|
||||
|
||||
void draw();
|
||||
}
|
||||
42
core/src/mindustry/entities/traits/Entity.java
Normal file
42
core/src/mindustry/entities/traits/Entity.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import mindustry.entities.EntityGroup;
|
||||
|
||||
public interface Entity extends MoveTrait{
|
||||
|
||||
int getID();
|
||||
|
||||
void resetID(int id);
|
||||
|
||||
default void update(){}
|
||||
|
||||
default void removed(){}
|
||||
|
||||
default void added(){}
|
||||
|
||||
EntityGroup targetGroup();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default void add(){
|
||||
if(targetGroup() != null){
|
||||
targetGroup().add(this);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default void remove(){
|
||||
if(getGroup() != null){
|
||||
getGroup().remove(this);
|
||||
}
|
||||
|
||||
setGroup(null);
|
||||
}
|
||||
|
||||
EntityGroup getGroup();
|
||||
|
||||
void setGroup(EntityGroup group);
|
||||
|
||||
default boolean isAdded(){
|
||||
return getGroup() != null;
|
||||
}
|
||||
}
|
||||
52
core/src/mindustry/entities/traits/HealthTrait.java
Normal file
52
core/src/mindustry/entities/traits/HealthTrait.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.Mathf;
|
||||
|
||||
public interface HealthTrait{
|
||||
|
||||
void health(float health);
|
||||
|
||||
float health();
|
||||
|
||||
float maxHealth();
|
||||
|
||||
boolean isDead();
|
||||
|
||||
void setDead(boolean dead);
|
||||
|
||||
default void onHit(SolidTrait entity){
|
||||
}
|
||||
|
||||
default void onDeath(){
|
||||
}
|
||||
|
||||
default boolean damaged(){
|
||||
return health() < maxHealth() - 0.0001f;
|
||||
}
|
||||
|
||||
default void damage(float amount){
|
||||
health(health() - amount);
|
||||
if(health() <= 0 && !isDead()){
|
||||
onDeath();
|
||||
setDead(true);
|
||||
}
|
||||
}
|
||||
|
||||
default void clampHealth(){
|
||||
health(Mathf.clamp(health(), 0, maxHealth()));
|
||||
}
|
||||
|
||||
default float healthf(){
|
||||
return health() / maxHealth();
|
||||
}
|
||||
|
||||
default void healBy(float amount){
|
||||
health(health() + amount);
|
||||
clampHealth();
|
||||
}
|
||||
|
||||
default void heal(){
|
||||
health(maxHealth());
|
||||
setDead(false);
|
||||
}
|
||||
}
|
||||
5
core/src/mindustry/entities/traits/KillerTrait.java
Normal file
5
core/src/mindustry/entities/traits/KillerTrait.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
public interface KillerTrait{
|
||||
void killed(Entity other);
|
||||
}
|
||||
102
core/src/mindustry/entities/traits/MinerTrait.java
Normal file
102
core/src/mindustry/entities/traits/MinerTrait.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.Color;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.util.Time;
|
||||
import mindustry.content.*;
|
||||
import mindustry.entities.Effects;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.gen.Call;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.Item;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public interface MinerTrait extends Entity{
|
||||
|
||||
/** Returns the range at which this miner can mine blocks.*/
|
||||
default float getMiningRange(){
|
||||
return 70f;
|
||||
}
|
||||
|
||||
default boolean isMining(){
|
||||
return getMineTile() != null;
|
||||
}
|
||||
|
||||
/** Returns the tile this builder is currently mining. */
|
||||
Tile getMineTile();
|
||||
|
||||
/** Sets the tile this builder is currently mining. */
|
||||
void setMineTile(Tile tile);
|
||||
|
||||
/** Returns the mining speed of this miner. 1 = standard, 0.5 = half speed, 2 = double speed, etc. */
|
||||
float getMinePower();
|
||||
|
||||
/** Returns whether or not this builder can mine a specific item type. */
|
||||
boolean canMine(Item item);
|
||||
|
||||
default void updateMining(){
|
||||
Unit unit = (Unit)this;
|
||||
Tile tile = getMineTile();
|
||||
TileEntity core = unit.getClosestCore();
|
||||
|
||||
if(tile == null || core == null || tile.block() != Blocks.air || dst(tile.worldx(), tile.worldy()) > getMiningRange()
|
||||
|| tile.drop() == null || !unit.acceptsItem(tile.drop()) || !canMine(tile.drop())){
|
||||
setMineTile(null);
|
||||
}else{
|
||||
Item item = tile.drop();
|
||||
unit.rotation = Mathf.slerpDelta(unit.rotation, unit.angleTo(tile.worldx(), tile.worldy()), 0.4f);
|
||||
|
||||
if(Mathf.chance(Time.delta() * (0.06 - item.hardness * 0.01) * getMinePower())){
|
||||
|
||||
if(unit.dst(core) < mineTransferRange && core.tile.block().acceptStack(item, 1, core.tile, unit) == 1){
|
||||
Call.transferItemTo(item, 1,
|
||||
tile.worldx() + Mathf.range(tilesize / 2f),
|
||||
tile.worldy() + Mathf.range(tilesize / 2f), core.tile);
|
||||
}else if(unit.acceptsItem(item)){
|
||||
Call.transferItemToUnit(item,
|
||||
tile.worldx() + Mathf.range(tilesize / 2f),
|
||||
tile.worldy() + Mathf.range(tilesize / 2f),
|
||||
unit);
|
||||
}
|
||||
}
|
||||
|
||||
if(Mathf.chance(0.06 * Time.delta())){
|
||||
Effects.effect(Fx.pulverizeSmall,
|
||||
tile.worldx() + Mathf.range(tilesize / 2f),
|
||||
tile.worldy() + Mathf.range(tilesize / 2f), 0f, item.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default void drawMining(){
|
||||
Unit unit = (Unit)this;
|
||||
Tile tile = getMineTile();
|
||||
|
||||
if(tile == null) return;
|
||||
|
||||
float focusLen = 4f + Mathf.absin(Time.time(), 1.1f, 0.5f);
|
||||
float swingScl = 12f, swingMag = tilesize / 8f;
|
||||
float flashScl = 0.3f;
|
||||
|
||||
float px = unit.x + Angles.trnsx(unit.rotation, focusLen);
|
||||
float py = unit.y + Angles.trnsy(unit.rotation, focusLen);
|
||||
|
||||
float ex = tile.worldx() + Mathf.sin(Time.time() + 48, swingScl, swingMag);
|
||||
float ey = tile.worldy() + Mathf.sin(Time.time() + 48, swingScl + 2f, swingMag);
|
||||
|
||||
Draw.color(Color.lightGray, Color.white, 1f - flashScl + Mathf.absin(Time.time(), 0.5f, flashScl));
|
||||
|
||||
Drawf.laser(Core.atlas.find("minelaser"), Core.atlas.find("minelaser-end"), px, py, ex, ey, 0.75f);
|
||||
|
||||
if(unit instanceof Player && ((Player)unit).isLocal){
|
||||
Lines.stroke(1f, Pal.accent);
|
||||
Lines.poly(tile.worldx(), tile.worldy(), 4, tilesize / 2f * Mathf.sqrt2, Time.time());
|
||||
}
|
||||
|
||||
Draw.color();
|
||||
}
|
||||
}
|
||||
20
core/src/mindustry/entities/traits/MoveTrait.java
Normal file
20
core/src/mindustry/entities/traits/MoveTrait.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.geom.Position;
|
||||
|
||||
public interface MoveTrait extends Position{
|
||||
|
||||
void setX(float x);
|
||||
|
||||
void setY(float y);
|
||||
|
||||
default void moveBy(float x, float y){
|
||||
setX(getX() + x);
|
||||
setY(getY() + y);
|
||||
}
|
||||
|
||||
default void set(float x, float y){
|
||||
setX(x);
|
||||
setY(y);
|
||||
}
|
||||
}
|
||||
8
core/src/mindustry/entities/traits/SaveTrait.java
Normal file
8
core/src/mindustry/entities/traits/SaveTrait.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
/**
|
||||
* Marks an entity as serializable.
|
||||
*/
|
||||
public interface SaveTrait extends Entity, TypeTrait, Saveable{
|
||||
byte version();
|
||||
}
|
||||
8
core/src/mindustry/entities/traits/Saveable.java
Normal file
8
core/src/mindustry/entities/traits/Saveable.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public interface Saveable{
|
||||
void writeSave(DataOutput stream) throws IOException;
|
||||
void readSave(DataInput stream, byte version) throws IOException;
|
||||
}
|
||||
43
core/src/mindustry/entities/traits/ScaleTrait.java
Normal file
43
core/src/mindustry/entities/traits/ScaleTrait.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.Interpolation;
|
||||
|
||||
public interface ScaleTrait{
|
||||
/** 0 to 1. */
|
||||
float fin();
|
||||
|
||||
/** 1 to 0 */
|
||||
default float fout(){
|
||||
return 1f - fin();
|
||||
}
|
||||
|
||||
/** 1 to 0 */
|
||||
default float fout(Interpolation i){
|
||||
return i.apply(fout());
|
||||
}
|
||||
|
||||
/** 1 to 0, ending at the specified margin */
|
||||
default float fout(float margin){
|
||||
float f = fin();
|
||||
if(f >= 1f - margin){
|
||||
return 1f - (f - (1f - margin)) / margin;
|
||||
}else{
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
/** 0 to 1 **/
|
||||
default float fin(Interpolation i){
|
||||
return i.apply(fin());
|
||||
}
|
||||
|
||||
/** 0 to 1 */
|
||||
default float finpow(){
|
||||
return Interpolation.pow3Out.apply(fin());
|
||||
}
|
||||
|
||||
/** 0 to 1 to 0 */
|
||||
default float fslope(){
|
||||
return (0.5f - Math.abs(fin() - 0.5f)) * 2f;
|
||||
}
|
||||
}
|
||||
13
core/src/mindustry/entities/traits/ShooterTrait.java
Normal file
13
core/src/mindustry/entities/traits/ShooterTrait.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.util.Interval;
|
||||
import mindustry.type.Weapon;
|
||||
|
||||
public interface ShooterTrait extends VelocityTrait, TeamTrait{
|
||||
|
||||
Interval getTimer();
|
||||
|
||||
int getShootTimer(boolean left);
|
||||
|
||||
Weapon getWeapon();
|
||||
}
|
||||
38
core/src/mindustry/entities/traits/SolidTrait.java
Normal file
38
core/src/mindustry/entities/traits/SolidTrait.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
|
||||
import arc.math.geom.*;
|
||||
import arc.math.geom.QuadTree.QuadTreeObject;
|
||||
import mindustry.Vars;
|
||||
|
||||
public interface SolidTrait extends QuadTreeObject, MoveTrait, VelocityTrait, Entity, Position{
|
||||
|
||||
void hitbox(Rectangle rectangle);
|
||||
|
||||
void hitboxTile(Rectangle rectangle);
|
||||
|
||||
Vector2 lastPosition();
|
||||
|
||||
default boolean collidesGrid(int x, int y){
|
||||
return true;
|
||||
}
|
||||
|
||||
default float getDeltaX(){
|
||||
return getX() - lastPosition().x;
|
||||
}
|
||||
|
||||
default float getDeltaY(){
|
||||
return getY() - lastPosition().y;
|
||||
}
|
||||
|
||||
default boolean collides(SolidTrait other){
|
||||
return true;
|
||||
}
|
||||
|
||||
default void collision(SolidTrait other, float x, float y){
|
||||
}
|
||||
|
||||
default void move(float x, float y){
|
||||
Vars.collisions.move(this, x, y);
|
||||
}
|
||||
}
|
||||
18
core/src/mindustry/entities/traits/SpawnerTrait.java
Normal file
18
core/src/mindustry/entities/traits/SpawnerTrait.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.geom.Position;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
public interface SpawnerTrait extends TargetTrait, Position{
|
||||
Tile getTile();
|
||||
|
||||
void updateSpawning(Player unit);
|
||||
|
||||
boolean hasUnit(Unit unit);
|
||||
|
||||
@Override
|
||||
default boolean isValid(){
|
||||
return getTile().entity instanceof SpawnerTrait;
|
||||
}
|
||||
}
|
||||
48
core/src/mindustry/entities/traits/SyncTrait.java
Normal file
48
core/src/mindustry/entities/traits/SyncTrait.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import mindustry.net.Interpolator;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public interface SyncTrait extends Entity, TypeTrait{
|
||||
|
||||
/** Sets the position of this entity and updated the interpolator. */
|
||||
default void setNet(float x, float y){
|
||||
set(x, y);
|
||||
|
||||
if(getInterpolator() != null){
|
||||
getInterpolator().target.set(x, y);
|
||||
getInterpolator().last.set(x, y);
|
||||
getInterpolator().pos.set(0, 0);
|
||||
getInterpolator().updateSpacing = 16;
|
||||
getInterpolator().lastUpdated = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Interpolate entity position only. Override if you need to interpolate rotations or other values. */
|
||||
default void interpolate(){
|
||||
if(getInterpolator() == null){
|
||||
throw new RuntimeException("This entity must have an interpolator to interpolate()!");
|
||||
}
|
||||
|
||||
getInterpolator().update();
|
||||
|
||||
setX(getInterpolator().pos.x);
|
||||
setY(getInterpolator().pos.y);
|
||||
}
|
||||
|
||||
/** Return the interpolator used for smoothing the position. Optional. */
|
||||
default Interpolator getInterpolator(){
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether syncing is enabled for this entity; true by default. */
|
||||
default boolean isSyncing(){
|
||||
return true;
|
||||
}
|
||||
|
||||
//Read and write sync data, usually position
|
||||
void write(DataOutput data) throws IOException;
|
||||
|
||||
void read(DataInput data) throws IOException;
|
||||
}
|
||||
35
core/src/mindustry/entities/traits/TargetTrait.java
Normal file
35
core/src/mindustry/entities/traits/TargetTrait.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.geom.Position;
|
||||
import mindustry.game.Team;
|
||||
|
||||
/**
|
||||
* Base interface for targetable entities.
|
||||
*/
|
||||
public interface TargetTrait extends Position, VelocityTrait{
|
||||
|
||||
boolean isDead();
|
||||
|
||||
Team getTeam();
|
||||
|
||||
default float getTargetVelocityX(){
|
||||
if(this instanceof SolidTrait){
|
||||
return ((SolidTrait)this).getDeltaX();
|
||||
}
|
||||
return velocity().x;
|
||||
}
|
||||
|
||||
default float getTargetVelocityY(){
|
||||
if(this instanceof SolidTrait){
|
||||
return ((SolidTrait)this).getDeltaY();
|
||||
}
|
||||
return velocity().y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this entity is a valid target.
|
||||
*/
|
||||
default boolean isValid(){
|
||||
return !isDead();
|
||||
}
|
||||
}
|
||||
7
core/src/mindustry/entities/traits/TeamTrait.java
Normal file
7
core/src/mindustry/entities/traits/TeamTrait.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import mindustry.game.Team;
|
||||
|
||||
public interface TeamTrait extends Entity{
|
||||
Team getTeam();
|
||||
}
|
||||
23
core/src/mindustry/entities/traits/TimeTrait.java
Normal file
23
core/src/mindustry/entities/traits/TimeTrait.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.Mathf;
|
||||
import arc.util.Time;
|
||||
|
||||
public interface TimeTrait extends ScaleTrait, Entity{
|
||||
|
||||
float lifetime();
|
||||
|
||||
void time(float time);
|
||||
|
||||
float time();
|
||||
|
||||
default void updateTime(){
|
||||
time(Mathf.clamp(time() + Time.delta(), 0, lifetime()));
|
||||
|
||||
if(time() >= lifetime()){
|
||||
remove();
|
||||
}
|
||||
}
|
||||
|
||||
//fin() is not implemented due to compiler issues with iOS/RoboVM
|
||||
}
|
||||
45
core/src/mindustry/entities/traits/TypeTrait.java
Normal file
45
core/src/mindustry/entities/traits/TypeTrait.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import mindustry.type.TypeID;
|
||||
|
||||
public interface TypeTrait{
|
||||
|
||||
TypeID getTypeID();
|
||||
/*
|
||||
int[] lastRegisteredID = {0};
|
||||
Array<Supplier<? extends TypeTrait>> registeredTypes = new Array<>();
|
||||
ObjectIntMap<Class<? extends TypeTrait>> typeToID = new ObjectIntMap<>();
|
||||
|
||||
/**
|
||||
* Register and return a type ID. The supplier should return a fresh instace of that type.
|
||||
|
||||
static <T extends TypeTrait> void registerType(Class<T> type, Supplier<T> supplier){
|
||||
if(typeToID.get(type, -1) != -1){
|
||||
return; //already registered
|
||||
}
|
||||
|
||||
registeredTypes.add(supplier);
|
||||
int result = lastRegisteredID[0];
|
||||
typeToID.put(type, result);
|
||||
lastRegisteredID[0]++;
|
||||
}
|
||||
|
||||
/**Gets a syncable type by ID.
|
||||
static Supplier<? extends TypeTrait> getTypeByID(int id){
|
||||
if(id == -1){
|
||||
throw new IllegalArgumentException("Attempt to retrieve invalid entity type ID! Did you forget to set it in ContentLoader.registerTypes()?");
|
||||
}
|
||||
return registeredTypes.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type ID of this entity used for intstantiation. Should be < BYTE_MAX.
|
||||
* Do not override!
|
||||
|
||||
default int getTypeID(){
|
||||
int id = typeToID.get(getClass(), -1);
|
||||
if(id == -1)
|
||||
throw new RuntimeException("Class of type '" + getClass() + "' is not registered! Did you forget to register it in ContentLoader#registerTypes()?");
|
||||
return id;
|
||||
}*/
|
||||
}
|
||||
36
core/src/mindustry/entities/traits/VelocityTrait.java
Normal file
36
core/src/mindustry/entities/traits/VelocityTrait.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package mindustry.entities.traits;
|
||||
|
||||
import arc.math.geom.Vector2;
|
||||
import arc.util.Time;
|
||||
|
||||
public interface VelocityTrait extends MoveTrait{
|
||||
|
||||
Vector2 velocity();
|
||||
|
||||
default void applyImpulse(float x, float y){
|
||||
velocity().x += x / mass();
|
||||
velocity().y += y / mass();
|
||||
}
|
||||
|
||||
default float maxVelocity(){
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
default float mass(){
|
||||
return 1f;
|
||||
}
|
||||
|
||||
default float drag(){
|
||||
return 0f;
|
||||
}
|
||||
|
||||
default void updateVelocity(){
|
||||
velocity().scl(1f - drag() * Time.delta());
|
||||
|
||||
if(this instanceof SolidTrait){
|
||||
((SolidTrait)this).move(velocity().x * Time.delta(), velocity().y * Time.delta());
|
||||
}else{
|
||||
moveBy(velocity().x * Time.delta(), velocity().y * Time.delta());
|
||||
}
|
||||
}
|
||||
}
|
||||
75
core/src/mindustry/entities/type/BaseEntity.java
Executable file
75
core/src/mindustry/entities/type/BaseEntity.java
Executable file
@@ -0,0 +1,75 @@
|
||||
package mindustry.entities.type;
|
||||
|
||||
import mindustry.*;
|
||||
import mindustry.entities.EntityGroup;
|
||||
import mindustry.entities.traits.Entity;
|
||||
|
||||
public abstract class BaseEntity implements Entity{
|
||||
private static int lastid;
|
||||
/** Do not modify. Used for network operations and mapping. */
|
||||
public int id;
|
||||
public float x, y;
|
||||
protected transient EntityGroup group;
|
||||
|
||||
public BaseEntity(){
|
||||
id = lastid++;
|
||||
}
|
||||
|
||||
public int tileX(){
|
||||
return Vars.world.toTile(x);
|
||||
}
|
||||
|
||||
public int tileY(){
|
||||
return Vars.world.toTile(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID(){
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetID(int id){
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup getGroup(){
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGroup(EntityGroup group){
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getX(){
|
||||
return x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setX(float x){
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getY(){
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setY(float y){
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return getClass() + " " + id;
|
||||
}
|
||||
|
||||
/** Increments this entity's ID. Used for pooled entities.*/
|
||||
public void incrementID(){
|
||||
id = lastid++;
|
||||
}
|
||||
}
|
||||
419
core/src/mindustry/entities/type/BaseUnit.java
Normal file
419
core/src/mindustry/entities/type/BaseUnit.java
Normal file
@@ -0,0 +1,419 @@
|
||||
package mindustry.entities.type;
|
||||
|
||||
import arc.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.type.TypeID;
|
||||
import mindustry.ui.Cicon;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.defense.DeflectorWall.*;
|
||||
import mindustry.world.blocks.units.CommandCenter.*;
|
||||
import mindustry.world.blocks.units.UnitFactory.*;
|
||||
import mindustry.world.meta.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Base class for AI units. */
|
||||
public abstract class BaseUnit extends Unit implements ShooterTrait{
|
||||
protected static int timerIndex = 0;
|
||||
|
||||
protected static final int timerTarget = timerIndex++;
|
||||
protected static final int timerTarget2 = timerIndex++;
|
||||
protected static final int timerShootLeft = timerIndex++;
|
||||
protected static final int timerShootRight = timerIndex++;
|
||||
|
||||
protected boolean loaded;
|
||||
protected UnitType type;
|
||||
protected Interval timer = new Interval(5);
|
||||
protected StateMachine state = new StateMachine();
|
||||
protected TargetTrait target;
|
||||
|
||||
protected int spawner = noSpawner;
|
||||
|
||||
/** internal constructor used for deserialization, DO NOT USE */
|
||||
public BaseUnit(){
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server)
|
||||
public static void onUnitDeath(BaseUnit unit){
|
||||
if(unit == null) return;
|
||||
|
||||
if(net.server() || !net.active()){
|
||||
UnitDrops.dropItems(unit);
|
||||
}
|
||||
|
||||
unit.onSuperDeath();
|
||||
unit.type.deathSound.at(unit);
|
||||
|
||||
//visual only.
|
||||
if(net.client()){
|
||||
Tile tile = world.tile(unit.spawner);
|
||||
if(tile != null){
|
||||
tile.block().unitRemoved(tile, unit);
|
||||
}
|
||||
|
||||
unit.spawner = noSpawner;
|
||||
}
|
||||
|
||||
//must run afterwards so the unit's group is not null when sending the removal packet
|
||||
Core.app.post(unit::remove);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drag(){
|
||||
return type.drag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeID getTypeID(){
|
||||
return type.typeID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHit(SolidTrait entity){
|
||||
if(entity instanceof Bullet && ((Bullet)entity).getOwner() instanceof DeflectorEntity && player != null && getTeam() != player.getTeam()){
|
||||
Core.app.post(() -> {
|
||||
if(isDead()){
|
||||
Events.fire(Trigger.phaseDeflectHit);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
Tile getSpawner(){
|
||||
return world.tile(spawner);
|
||||
}
|
||||
|
||||
public boolean isCommanded(){
|
||||
return indexer.getAllied(team, BlockFlag.comandCenter).size != 0 && indexer.getAllied(team, BlockFlag.comandCenter).first().entity instanceof CommandCenterEntity;
|
||||
}
|
||||
|
||||
public @Nullable UnitCommand getCommand(){
|
||||
if(isCommanded()){
|
||||
return indexer.getAllied(team, BlockFlag.comandCenter).first().<CommandCenterEntity>ent().command;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**Called when a command is recieved from the command center.*/
|
||||
public void onCommand(UnitCommand command){
|
||||
|
||||
}
|
||||
|
||||
/** Initialize the type and team of this unit. Only call once! */
|
||||
public void init(UnitType type, Team team){
|
||||
if(this.type != null) throw new RuntimeException("This unit is already initialized!");
|
||||
|
||||
this.type = type;
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public UnitType getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setSpawner(Tile tile){
|
||||
this.spawner = tile.pos();
|
||||
}
|
||||
|
||||
public void rotate(float angle){
|
||||
rotation = Mathf.slerpDelta(rotation, angle, type.rotatespeed);
|
||||
}
|
||||
|
||||
public boolean targetHasFlag(BlockFlag flag){
|
||||
return (target instanceof TileEntity && ((TileEntity)target).tile.block().flags.contains(flag)) ||
|
||||
(target instanceof Tile && ((Tile)target).block().flags.contains(flag));
|
||||
}
|
||||
|
||||
public void setState(UnitState state){
|
||||
this.state.set(state);
|
||||
}
|
||||
|
||||
public boolean retarget(){
|
||||
return timer.get(timerTarget, 20);
|
||||
}
|
||||
|
||||
/** Only runs when the unit has a target. */
|
||||
public void behavior(){
|
||||
|
||||
}
|
||||
|
||||
public void updateTargeting(){
|
||||
if(target == null || (target instanceof Unit && (target.isDead() || target.getTeam() == team))
|
||||
|| (target instanceof TileEntity && ((TileEntity)target).tile.entity == null)){
|
||||
target = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void targetClosestAllyFlag(BlockFlag flag){
|
||||
Tile target = Geometry.findClosest(x, y, indexer.getAllied(team, flag));
|
||||
if(target != null) this.target = target.entity;
|
||||
}
|
||||
|
||||
public void targetClosestEnemyFlag(BlockFlag flag){
|
||||
Tile target = Geometry.findClosest(x, y, indexer.getEnemy(team, flag));
|
||||
if(target != null) this.target = target.entity;
|
||||
}
|
||||
|
||||
public void targetClosest(){
|
||||
TargetTrait newTarget = Units.closestTarget(team, x, y, Math.max(getWeapon().bullet.range(), type.range), u -> type.targetAir || !u.isFlying());
|
||||
if(newTarget != null){
|
||||
target = newTarget;
|
||||
}
|
||||
}
|
||||
|
||||
public Tile getClosest(BlockFlag flag){
|
||||
return Geometry.findClosest(x, y, indexer.getAllied(team, flag));
|
||||
}
|
||||
|
||||
public Tile getClosestSpawner(){
|
||||
return Geometry.findClosest(x, y, Vars.spawner.getGroundSpawns());
|
||||
}
|
||||
|
||||
public TileEntity getClosestEnemyCore(){
|
||||
for(Team enemy : Vars.state.teams.enemiesOf(team)){
|
||||
Tile tile = Geometry.findClosest(x, y, Vars.state.teams.get(enemy).cores);
|
||||
if(tile != null){
|
||||
return tile.entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public UnitState getStartState(){
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isBoss(){
|
||||
return hasEffect(StatusEffects.boss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDamageMultipler(){
|
||||
return status.getDamageMultiplier() * Vars.state.rules.unitDamageMultiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmune(StatusEffect effect){
|
||||
return type.immunities.contains(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(){
|
||||
return super.isValid() && isAdded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interval getTimer(){
|
||||
return timer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getShootTimer(boolean left){
|
||||
return left ? timerShootLeft : timerShootRight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Weapon getWeapon(){
|
||||
return type.weapon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureRegion getIconRegion(){
|
||||
return type.icon(Cicon.full);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCapacity(){
|
||||
return type.itemCapacity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interpolate(){
|
||||
super.interpolate();
|
||||
|
||||
if(interpolator.values.length > 0){
|
||||
rotation = interpolator.values[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float maxHealth(){
|
||||
return type.health * Vars.state.rules.unitHealthMultiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float mass(){
|
||||
return type.mass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFlying(){
|
||||
return type.flying;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(isDead()){
|
||||
//dead enemies should get immediately removed
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
hitTime -= Time.delta();
|
||||
|
||||
if(net.client()){
|
||||
interpolate();
|
||||
status.update(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isFlying() && (world.tileWorld(x, y) != null && !(world.tileWorld(x, y).block() instanceof BuildBlock) && world.tileWorld(x, y).solid())){
|
||||
kill();
|
||||
}
|
||||
|
||||
avoidOthers();
|
||||
|
||||
if(spawner != noSpawner && (world.tile(spawner) == null || !(world.tile(spawner).entity instanceof UnitFactoryEntity))){
|
||||
kill();
|
||||
}
|
||||
|
||||
updateTargeting();
|
||||
|
||||
state.update();
|
||||
updateVelocityStatus();
|
||||
|
||||
if(target != null) behavior();
|
||||
|
||||
if(!isFlying()){
|
||||
clampPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public float maxVelocity(){
|
||||
return type.maxVelocity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
super.removed();
|
||||
Tile tile = world.tile(spawner);
|
||||
if(tile != null && !net.client()){
|
||||
tile.block().unitRemoved(tile, this);
|
||||
}
|
||||
|
||||
spawner = noSpawner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drawSize(){
|
||||
return type.hitsize * 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(){
|
||||
Call.onUnitDeath(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
state.set(getStartState());
|
||||
|
||||
if(!loaded){
|
||||
health(maxHealth());
|
||||
}
|
||||
|
||||
if(isCommanded()){
|
||||
onCommand(getCommand());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitbox(Rectangle rectangle){
|
||||
rectangle.setSize(type.hitsize).setCenter(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitboxTile(Rectangle rectangle){
|
||||
rectangle.setSize(type.hitsizeTile).setCenter(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return unitGroups[team.ordinal()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte version(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSave(DataOutput stream) throws IOException{
|
||||
super.writeSave(stream);
|
||||
stream.writeByte(type.id);
|
||||
stream.writeInt(spawner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSave(DataInput stream, byte version) throws IOException{
|
||||
super.readSave(stream, version);
|
||||
loaded = true;
|
||||
byte type = stream.readByte();
|
||||
this.spawner = stream.readInt();
|
||||
|
||||
this.type = content.getByID(ContentType.unit, type);
|
||||
add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutput data) throws IOException{
|
||||
super.writeSave(data);
|
||||
data.writeByte(type.id);
|
||||
data.writeInt(spawner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(DataInput data) throws IOException{
|
||||
float lastx = x, lasty = y, lastrot = rotation;
|
||||
|
||||
super.readSave(data, version());
|
||||
|
||||
this.type = content.getByID(ContentType.unit, data.readByte());
|
||||
this.spawner = data.readInt();
|
||||
|
||||
interpolator.read(lastx, lasty, x, y, rotation);
|
||||
rotation = lastrot;
|
||||
x = lastx;
|
||||
y = lasty;
|
||||
}
|
||||
|
||||
public void onSuperDeath(){
|
||||
super.onDeath();
|
||||
}
|
||||
}
|
||||
323
core/src/mindustry/entities/type/Bullet.java
Normal file
323
core/src/mindustry/entities/type/Bullet.java
Normal file
@@ -0,0 +1,323 @@
|
||||
package mindustry.entities.type;
|
||||
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import arc.util.pooling.Pool.*;
|
||||
import arc.util.pooling.*;
|
||||
import mindustry.entities.*;
|
||||
import mindustry.entities.bullet.*;
|
||||
import mindustry.entities.effect.*;
|
||||
import mindustry.entities.traits.*;
|
||||
import mindustry.game.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Bullet extends SolidEntity implements DamageTrait, ScaleTrait, Poolable, DrawTrait, VelocityTrait, TimeTrait, TeamTrait, AbsorbTrait{
|
||||
public Interval timer = new Interval(3);
|
||||
|
||||
private float lifeScl;
|
||||
private Team team;
|
||||
private Object data;
|
||||
private boolean supressCollision, supressOnce, initialized, deflected;
|
||||
|
||||
protected BulletType type;
|
||||
protected Entity owner;
|
||||
protected float time;
|
||||
|
||||
/** Internal use only! */
|
||||
public Bullet(){
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, TeamTrait owner, float x, float y, float angle){
|
||||
return create(type, owner, owner.getTeam(), x, y, angle);
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle){
|
||||
return create(type, owner, team, x, y, angle, 1f);
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl){
|
||||
return create(type, owner, team, x, y, angle, velocityScl, 1f, null);
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl, float lifetimeScl){
|
||||
return create(type, owner, team, x, y, angle, velocityScl, lifetimeScl, null);
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl, float lifetimeScl, Object data){
|
||||
Bullet bullet = Pools.obtain(Bullet.class, Bullet::new);
|
||||
bullet.type = type;
|
||||
bullet.owner = owner;
|
||||
bullet.data = data;
|
||||
|
||||
bullet.velocity.set(0, type.speed).setAngle(angle).scl(velocityScl);
|
||||
if(type.keepVelocity){
|
||||
bullet.velocity.add(owner instanceof VelocityTrait ? ((VelocityTrait)owner).velocity() : Vector2.ZERO);
|
||||
}
|
||||
|
||||
bullet.team = team;
|
||||
bullet.type = type;
|
||||
bullet.lifeScl = lifetimeScl;
|
||||
|
||||
bullet.set(x - bullet.velocity.x * Time.delta(), y - bullet.velocity.y * Time.delta());
|
||||
bullet.add();
|
||||
|
||||
return bullet;
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Bullet parent, float x, float y, float angle){
|
||||
return create(type, parent.owner, parent.team, x, y, angle);
|
||||
}
|
||||
|
||||
public static Bullet create(BulletType type, Bullet parent, float x, float y, float angle, float velocityScl){
|
||||
return create(type, parent.owner, parent.team, x, y, angle, velocityScl);
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void createBullet(BulletType type, Team team, float x, float y, float angle, float velocityScl, float lifetimeScl){
|
||||
create(type, null, team, x, y, angle, velocityScl, lifetimeScl, null);
|
||||
}
|
||||
|
||||
public Entity getOwner(){
|
||||
return owner;
|
||||
}
|
||||
|
||||
public boolean collidesTiles(){
|
||||
return type.collidesTiles;
|
||||
}
|
||||
|
||||
public void deflect(){
|
||||
supressCollision = true;
|
||||
supressOnce = true;
|
||||
deflected = true;
|
||||
}
|
||||
|
||||
public boolean isDeflected(){
|
||||
return deflected;
|
||||
}
|
||||
|
||||
public BulletType getBulletType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
public void resetOwner(Entity entity, Team team){
|
||||
this.owner = entity;
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
public void scaleTime(float add){
|
||||
time += add;
|
||||
}
|
||||
|
||||
public Object getData(){
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(Object data){
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public float damageMultiplier(){
|
||||
if(owner instanceof Unit){
|
||||
return ((Unit)owner).getDamageMultipler();
|
||||
}
|
||||
return 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void killed(Entity other){
|
||||
if(owner instanceof KillerTrait){
|
||||
((KillerTrait)owner).killed(other);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void absorb(){
|
||||
supressCollision = true;
|
||||
remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drawSize(){
|
||||
return type.drawSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float damage(){
|
||||
if(owner instanceof Lightning && data instanceof Float){
|
||||
return (Float)data;
|
||||
}
|
||||
return type.damage * damageMultiplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Team getTeam(){
|
||||
return team;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getShieldDamage(){
|
||||
return Math.max(damage(), type.splashDamage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collides(SolidTrait other){
|
||||
return type.collides && (other != owner && !(other instanceof DamageTrait)) && !supressCollision && !(other instanceof Unit && ((Unit)other).isFlying() && !type.collidesAir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collision(SolidTrait other, float x, float y){
|
||||
if(!type.pierce) remove();
|
||||
type.hit(this, x, y);
|
||||
|
||||
if(other instanceof Unit){
|
||||
Unit unit = (Unit)other;
|
||||
unit.velocity().add(Tmp.v3.set(other.getX(), other.getY()).sub(x, y).setLength(type.knockback / unit.mass()));
|
||||
unit.applyEffect(type.status, type.statusDuration);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
type.update(this);
|
||||
|
||||
x += velocity.x * Time.delta();
|
||||
y += velocity.y * Time.delta();
|
||||
|
||||
velocity.scl(Mathf.clamp(1f - type.drag * Time.delta()));
|
||||
|
||||
time += Time.delta() * 1f / (lifeScl);
|
||||
time = Mathf.clamp(time, 0, type.lifetime);
|
||||
|
||||
if(time >= type.lifetime){
|
||||
if(!supressCollision) type.despawned(this);
|
||||
remove();
|
||||
}
|
||||
|
||||
if(type.hitTiles && collidesTiles() && !supressCollision && initialized){
|
||||
world.raycastEach(world.toTile(lastPosition().x), world.toTile(lastPosition().y), world.toTile(x), world.toTile(y), (x, y) -> {
|
||||
|
||||
Tile tile = world.ltile(x, y);
|
||||
if(tile == null) return false;
|
||||
|
||||
if(tile.entity != null && tile.entity.collide(this) && type.collides(this, tile) && !tile.entity.isDead() && (type.collidesTeam || tile.getTeam() != team)){
|
||||
if(tile.getTeam() != team){
|
||||
tile.entity.collision(this);
|
||||
}
|
||||
|
||||
if(!supressCollision){
|
||||
type.hitTile(this, tile);
|
||||
remove();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if(supressOnce){
|
||||
supressCollision = false;
|
||||
supressOnce = false;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
type = null;
|
||||
owner = null;
|
||||
velocity.setZero();
|
||||
time = 0f;
|
||||
timer.clear();
|
||||
lifeScl = 1f;
|
||||
team = null;
|
||||
data = null;
|
||||
supressCollision = false;
|
||||
supressOnce = false;
|
||||
deflected = false;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitbox(Rectangle rectangle){
|
||||
rectangle.setSize(type.hitSize).setCenter(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitboxTile(Rectangle rectangle){
|
||||
rectangle.setSize(type.hitSize).setCenter(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float lifetime(){
|
||||
return type.lifetime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void time(float time){
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float time(){
|
||||
return time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
Pools.free(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityGroup targetGroup(){
|
||||
return bulletGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
type.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
type.draw(this);
|
||||
renderer.lights.add(x, y, 16f, Pal.powerLight, 0.3f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float fin(){
|
||||
return time / type.lifetime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 velocity(){
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public void velocity(float speed, float angle){
|
||||
velocity.set(0, speed).setAngle(angle);
|
||||
}
|
||||
|
||||
public void limit(float f){
|
||||
velocity.limit(f);
|
||||
}
|
||||
|
||||
/** Sets the bullet's rotation in degrees. */
|
||||
public void rot(float angle){
|
||||
velocity.setAngle(angle);
|
||||
}
|
||||
|
||||
/** @return the bullet's rotation. */
|
||||
public float rot(){
|
||||
float angle = Mathf.atan2(velocity.x, velocity.y) * Mathf.radiansToDegrees;
|
||||
if(angle < 0) angle += 360;
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
47
core/src/mindustry/entities/type/DestructibleEntity.java
Normal file
47
core/src/mindustry/entities/type/DestructibleEntity.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package mindustry.entities.type;
|
||||
|
||||
|
||||
import mindustry.entities.traits.*;
|
||||
|
||||
public abstract class DestructibleEntity extends SolidEntity implements HealthTrait{
|
||||
public transient boolean dead;
|
||||
public float health;
|
||||
|
||||
@Override
|
||||
public boolean collides(SolidTrait other){
|
||||
return other instanceof DamageTrait;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collision(SolidTrait other, float x, float y){
|
||||
if(other instanceof DamageTrait){
|
||||
boolean wasDead = isDead();
|
||||
onHit(other);
|
||||
damage(((DamageTrait)other).damage());
|
||||
if(!wasDead && isDead()){
|
||||
((DamageTrait)other).killed(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void health(float health){
|
||||
this.health = health;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float health(){
|
||||
return health;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead(){
|
||||
return dead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDead(boolean dead){
|
||||
this.dead = dead;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user