it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View 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();
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}
};
}
}

File diff suppressed because it is too large Load Diff

View 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;
}};
}
}

View 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);
}};
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}};
}
}

View 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);
}
}
}

View 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);
}
}

View 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;
}};
}};
}
}

View 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};
}};*/
}
}

View 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);
}
}

View 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();
}
}
}
}

View 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);
}
}

View 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
}
}

View 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();
}
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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(){
}
}

View 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);
}
}

View 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 + "";
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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;
}
}

View 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();
}

View 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();
}

View 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;
}
}

View 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();
}
}

View 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
}
}

View 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));
}
}

View 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){}
}

View 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();
}
}
}

View 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();
}
}

View 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;
}
}
}

View 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);
});
}
}

View 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);
}
}

View 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();
}
}
}
}
}

View 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);
}
}

View 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;
}
}

View 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(){
}
}

View 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();
}
}

View 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();
}
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}
}
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,6 @@
package mindustry.entities;
public enum TargetPriority{
base,
turret
}

View 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);
}
}

View 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();
}
}

View 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();
}
}

View 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");
}
}

View 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;
}
}

View 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());
}
});
}
});
}
}
}

View 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());
}
}
}

View 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);
}
}
}
}

View 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);
}
}

View 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());
}
}
}

View 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();
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}
}

View 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();
}
}

View File

@@ -0,0 +1,7 @@
package mindustry.entities.traits;
/**
* A flag interface for marking an effect as appearing below liquids.
*/
public interface BelowLiquidTrait{
}

View 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();
}
}
}

View 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 +
'}';
}
}
}

View File

@@ -0,0 +1,9 @@
package mindustry.entities.traits;
public interface DamageTrait{
float damage();
default void killed(Entity other){
}
}

View File

@@ -0,0 +1,10 @@
package mindustry.entities.traits;
public interface DrawTrait extends Entity{
default float drawSize(){
return 20f;
}
void draw();
}

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,5 @@
package mindustry.entities.traits;
public interface KillerTrait{
void killed(Entity other);
}

View 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();
}
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
package mindustry.entities.traits;
/**
* Marks an entity as serializable.
*/
public interface SaveTrait extends Entity, TypeTrait, Saveable{
byte version();
}

View 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;
}

View 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;
}
}

View 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();
}

View 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);
}
}

View 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;
}
}

View 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;
}

View 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();
}
}

View File

@@ -0,0 +1,7 @@
package mindustry.entities.traits;
import mindustry.game.Team;
public interface TeamTrait extends Entity{
Team getTeam();
}

View 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
}

View 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;
}*/
}

View 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());
}
}
}

View 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++;
}
}

View 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();
}
}

View 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;
}
}

View 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