Merge branch 'master' into GH-v80
This commit is contained in:
@@ -1,42 +1,69 @@
|
||||
package io.anuke.mindustry;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.files.FileHandle;
|
||||
import com.badlogic.gdx.graphics.Pixmap;
|
||||
import com.badlogic.gdx.graphics.Pixmap.Filter;
|
||||
import com.badlogic.gdx.graphics.PixmapIO;
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Log;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.core.*;
|
||||
import io.anuke.mindustry.io.BlockLoader;
|
||||
import io.anuke.mindustry.game.EventType.GameLoadEvent;
|
||||
import io.anuke.mindustry.io.BundleLoader;
|
||||
import io.anuke.ucore.modules.ModuleCore;
|
||||
import io.anuke.ucore.util.Log;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class Mindustry extends ModuleCore {
|
||||
public class Mindustry extends ApplicationCore{
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
debug = Platform.instance.isDebug();
|
||||
@Override
|
||||
public void setup(){
|
||||
Time.setDeltaProvider(() -> {
|
||||
float result = Core.graphics.getDeltaTime() * 60f;
|
||||
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
|
||||
});
|
||||
|
||||
Log.setUseColors(false);
|
||||
BundleLoader.load();
|
||||
BlockLoader.load();
|
||||
Time.mark();
|
||||
|
||||
module(logic = new Logic());
|
||||
module(world = new World());
|
||||
module(control = new Control());
|
||||
module(renderer = new Renderer());
|
||||
module(ui = new UI());
|
||||
module(netServer = new NetServer());
|
||||
module(netClient = new NetClient());
|
||||
module(netCommon = new NetCommon());
|
||||
}
|
||||
Vars.init();
|
||||
|
||||
@Override
|
||||
public void render(){
|
||||
super.render();
|
||||
threads.handleRender();
|
||||
}
|
||||
Log.setUseColors(false);
|
||||
BundleLoader.load();
|
||||
content.load();
|
||||
content.loadColors();
|
||||
|
||||
add(logic = new Logic());
|
||||
add(world = new World());
|
||||
add(control = new Control());
|
||||
add(renderer = new Renderer());
|
||||
add(ui = new UI());
|
||||
add(netServer = new NetServer());
|
||||
add(netClient = new NetClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
super.init();
|
||||
|
||||
Log.info("Time to load [total]: {0}", Time.elapsed());
|
||||
Events.fire(new GameLoadEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
long lastFrameTime = Time.nanos();
|
||||
|
||||
super.update();
|
||||
|
||||
int fpsCap = Core.settings.getInt("fpscap", 125);
|
||||
|
||||
if(fpsCap <= 120){
|
||||
long target = (1000 * 1000000) / fpsCap; //target in nanos
|
||||
long elapsed = Time.timeSinceNanos(lastFrameTime);
|
||||
if(elapsed < target){
|
||||
try{
|
||||
Thread.sleep((target - elapsed) / 1000000, (int)((target - elapsed) % 1000000));
|
||||
}catch(InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,157 +1,222 @@
|
||||
package io.anuke.mindustry;
|
||||
|
||||
import com.badlogic.gdx.Application.ApplicationType;
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.files.FileHandle;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import com.badlogic.gdx.utils.IntMap;
|
||||
import io.anuke.arc.Application.ApplicationType;
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.files.FileHandle;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.util.Structs;
|
||||
import io.anuke.mindustry.core.*;
|
||||
import io.anuke.mindustry.entities.Bullet;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.TileEntity;
|
||||
import io.anuke.mindustry.entities.effect.Shield;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.core.Platform;
|
||||
import io.anuke.mindustry.net.EditLog;
|
||||
import io.anuke.mindustry.net.ClientDebug;
|
||||
import io.anuke.mindustry.net.ServerDebug;
|
||||
import io.anuke.ucore.UCore;
|
||||
import io.anuke.ucore.entities.EffectEntity;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.EntityGroup;
|
||||
import io.anuke.ucore.scene.ui.layout.Unit;
|
||||
import io.anuke.ucore.util.OS;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.bullet.Bullet;
|
||||
import io.anuke.mindustry.entities.effect.Fire;
|
||||
import io.anuke.mindustry.entities.effect.Puddle;
|
||||
import io.anuke.mindustry.entities.impl.EffectEntity;
|
||||
import io.anuke.mindustry.entities.traits.DrawTrait;
|
||||
import io.anuke.mindustry.entities.traits.SyncTrait;
|
||||
import io.anuke.mindustry.entities.type.*;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.gen.Serialization;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.world.blocks.defense.ForceProjector.ShieldEntity;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class Vars{
|
||||
/** Whether to load locales.*/
|
||||
public static boolean loadLocales = true;
|
||||
/** IO buffer size. */
|
||||
public static final int bufferSize = 8192;
|
||||
/** global charset */
|
||||
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 Github API for releases */
|
||||
public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases";
|
||||
/** URL for sending crash reports to */
|
||||
public static final String crashReportURL = "http://mins.us.to/report";
|
||||
/** 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.blue;
|
||||
/** team of the enemy in waves/sectors */
|
||||
public static final Team waveTeam = Team.red;
|
||||
/** 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;
|
||||
/** default size of UI icons.*/
|
||||
public static final int iconsize = 48;
|
||||
/** size of UI icons (small)*/
|
||||
public static final int iconsizesmall = 32;
|
||||
/** 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;
|
||||
/** application data directory, equivalent to {@link io.anuke.arc.Settings#getDataDirectory()} */
|
||||
public static FileHandle dataDirectory;
|
||||
/** data subdirectory used for screenshots */
|
||||
public static FileHandle screenshotDirectory;
|
||||
/** data subdirectory used for custom mmaps */
|
||||
public static FileHandle customMapDirectory;
|
||||
/** tmp subdirectory for map conversion */
|
||||
public static FileHandle tmpDirectory;
|
||||
/** data subdirectory used for saves */
|
||||
public static FileHandle saveDirectory;
|
||||
/** old map file extension, for conversion */
|
||||
public static final String oldMapExtension = "mmap";
|
||||
/** map file extension */
|
||||
public static final String mapExtension = "msav";
|
||||
/** save file extension */
|
||||
public static final String saveExtension = "msav";
|
||||
|
||||
public static final boolean testMobile = false;
|
||||
//shorthand for whether or not this is running on android
|
||||
public static final boolean mobile = (Gdx.app.getType() == ApplicationType.Android) ||
|
||||
Gdx.app.getType() == ApplicationType.iOS || testMobile;
|
||||
public static final boolean ios = Gdx.app.getType() == ApplicationType.iOS;
|
||||
public static final boolean android = Gdx.app.getType() == ApplicationType.Android;
|
||||
//shorthand for whether or not this is running on GWT
|
||||
public static final boolean gwt = (Gdx.app.getType() == ApplicationType.WebGL);
|
||||
//whether to send block state change events to players
|
||||
public static final boolean syncBlockState = false;
|
||||
//how far away from the player blocks can be placed
|
||||
public static final float placerange = 66;
|
||||
//respawn time in frames
|
||||
public static final float respawnduration = 60*4;
|
||||
//time between waves in frames (on normal mode)
|
||||
public static final float wavespace = 60*60*(mobile ? 1 : 1);
|
||||
//waves can last no longer than 3 minutes, otherwise the next one spawns
|
||||
public static final float maxwavespace = 60*60*4f;
|
||||
//advance time the pathfinding starts at
|
||||
public static final float aheadPathfinding = 60*15;
|
||||
//how far away from spawn points the player can't place blocks
|
||||
public static final float enemyspawnspace = 65;
|
||||
//discord group URL
|
||||
public static final String discordURL = "https://discord.gg/BKADYds";
|
||||
/** list of all locales that can be switched to */
|
||||
public static Locale[] locales;
|
||||
|
||||
public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases";
|
||||
public static final String macAppDir = UCore.getProperty("user.home") + "/Library/Application Support/";
|
||||
//directory for user-created map data
|
||||
public static final FileHandle customMapDirectory = gwt ? null : UCore.isAssets() ?
|
||||
Gdx.files.local("../../desktop/mindustry-maps") :
|
||||
OS.isMac ? (Gdx.files.absolute(macAppDir).child("maps/")) :
|
||||
Gdx.files.local("mindustry-maps/");
|
||||
//save file directory
|
||||
public static final FileHandle saveDirectory = gwt ? null : UCore.isAssets() ?
|
||||
Gdx.files.local("../../desktop/mindustry-saves") :
|
||||
OS.isMac ? (Gdx.files.absolute(macAppDir).child("saves/")) :
|
||||
Gdx.files.local("mindustry-saves/");
|
||||
//scale of the font
|
||||
public static float fontscale = Math.max(Unit.dp.scl(1f)/2f, 0.5f);
|
||||
//camera zoom displayed on startup
|
||||
public static final int baseCameraScale = Math.round(Unit.dp.scl(4));
|
||||
//how much the zoom changes every zoom button press (unused?)
|
||||
public static final int zoomScale = Math.round(Unit.dp.scl(1));
|
||||
//if true, player speed will be increased, massive amounts of resources will be given on start, and other debug options will be available
|
||||
public static boolean debug = false;
|
||||
public static boolean debugNet = true;
|
||||
public static boolean console = false;
|
||||
//whether the player can clip through walls
|
||||
public static boolean noclip = false;
|
||||
//whether to draw chunk borders
|
||||
public static boolean debugChunks = false;
|
||||
//whether turrets have infinite ammo (only with debug)
|
||||
public static boolean infiniteAmmo = true;
|
||||
//whether to show paths of enemies
|
||||
public static boolean showPaths = false;
|
||||
//if false, player is always hidden
|
||||
public static boolean showPlayer = true;
|
||||
//whether to hide ui, only on debug
|
||||
public static boolean showUI = true;
|
||||
//whether to show block debug
|
||||
public static boolean showBlockDebug = false;
|
||||
public static ContentLoader content;
|
||||
public static GameState state;
|
||||
public static GlobalData data;
|
||||
public static EntityCollisions collisions;
|
||||
public static DefaultWaves defaultWaves;
|
||||
|
||||
public static boolean headless = false;
|
||||
public static Control control;
|
||||
public static Logic logic;
|
||||
public static Renderer renderer;
|
||||
public static UI ui;
|
||||
public static World world;
|
||||
public static NetServer netServer;
|
||||
public static NetClient netClient;
|
||||
|
||||
public static float controllerMin = 0.25f;
|
||||
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 float baseControllerSpeed = 11f;
|
||||
/** all local players, currently only has one player. may be used for local co-op in the future */
|
||||
public static Player player;
|
||||
|
||||
public static final int saveSlots = 64;
|
||||
//amount of drops that are left when breaking a block
|
||||
public static final float breakDropAmount = 0.5f;
|
||||
|
||||
public static Array<EditLog> currentEditLogs = new Array<>();
|
||||
|
||||
//only if smoothCamera
|
||||
public static boolean snapCamera = true;
|
||||
|
||||
public static final int tilesize = 8;
|
||||
public static void init(){
|
||||
Serialization.init();
|
||||
|
||||
public static final Locale[] locales = {new Locale("en"), new Locale("fr"), new Locale("ru"), new Locale("uk", "UA"), new Locale("pl"),
|
||||
new Locale("de"), new Locale("pt", "BR"), new Locale("ko"), new Locale("in", "ID"), new Locale("ita"), new Locale("es")};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
};
|
||||
Arrays.sort(locales, Structs.comparing(l -> l.getDisplayName(l), String.CASE_INSENSITIVE_ORDER));
|
||||
}
|
||||
|
||||
//server port
|
||||
public static final int port = 6567;
|
||||
public static final int webPort = 6568;
|
||||
Version.init();
|
||||
|
||||
public static final GameState state = new GameState();
|
||||
public static final ThreadHandler threads = new ThreadHandler(Platform.instance.getThreadProvider());
|
||||
content = new ContentLoader();
|
||||
if(!headless){
|
||||
content.setVerbose();
|
||||
}
|
||||
|
||||
public static final ServerDebug serverDebug = new ServerDebug();
|
||||
public static final ClientDebug clientDebug = new ClientDebug();
|
||||
defaultWaves = new DefaultWaves();
|
||||
collisions = new EntityCollisions();
|
||||
|
||||
public static Control control;
|
||||
public static Logic logic;
|
||||
public static Renderer renderer;
|
||||
public static UI ui;
|
||||
public static World world;
|
||||
public static NetCommon netCommon;
|
||||
public static NetServer netServer;
|
||||
public static NetClient netClient;
|
||||
|
||||
public static Player player;
|
||||
playerGroup = Entities.addGroup(Player.class).enableMapping();
|
||||
tileGroup = Entities.addGroup(TileEntity.class, false);
|
||||
bulletGroup = Entities.addGroup(Bullet.class).enableMapping();
|
||||
effectGroup = Entities.addGroup(EffectEntity.class, false);
|
||||
groundEffectGroup = Entities.addGroup(DrawTrait.class, false);
|
||||
puddleGroup = Entities.addGroup(Puddle.class).enableMapping();
|
||||
shieldGroup = Entities.addGroup(ShieldEntity.class, false);
|
||||
fireGroup = Entities.addGroup(Fire.class).enableMapping();
|
||||
unitGroups = new EntityGroup[Team.all.length];
|
||||
|
||||
public static final EntityGroup<Player> playerGroup = Entities.addGroup(Player.class).enableMapping();
|
||||
public static final EntityGroup<Enemy> enemyGroup = Entities.addGroup(Enemy.class).enableMapping();
|
||||
public static final EntityGroup<TileEntity> tileGroup = Entities.addGroup(TileEntity.class, false);
|
||||
public static final EntityGroup<Bullet> bulletGroup = Entities.addGroup(Bullet.class);
|
||||
public static final EntityGroup<Shield> shieldGroup = Entities.addGroup(Shield.class, false);
|
||||
public static final EntityGroup<EffectEntity> effectGroup = Entities.addGroup(EffectEntity.class, false);
|
||||
for(Team team : Team.all){
|
||||
unitGroups[team.ordinal()] = Entities.addGroup(BaseUnit.class).enableMapping();
|
||||
}
|
||||
|
||||
for(EntityGroup<?> group : Entities.getAllGroups()){
|
||||
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;
|
||||
|
||||
Core.settings.setAppName(appName);
|
||||
|
||||
dataDirectory = Core.settings.getDataDirectory();
|
||||
screenshotDirectory = dataDirectory.child("screenshots/");
|
||||
customMapDirectory = dataDirectory.child("maps/");
|
||||
saveDirectory = dataDirectory.child("saves/");
|
||||
tmpDirectory = dataDirectory.child("tmp/");
|
||||
}
|
||||
}
|
||||
|
||||
352
core/src/io/anuke/mindustry/ai/BlockIndexer.java
Normal file
352
core/src/io/anuke/mindustry/ai/BlockIndexer.java
Normal file
@@ -0,0 +1,352 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.arc.collection.*;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.entities.type.TileEntity;
|
||||
import io.anuke.mindustry.game.EventType.TileChangeEvent;
|
||||
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.game.Teams.TeamData;
|
||||
import io.anuke.mindustry.type.Item;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.meta.BlockFlag;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
/** Class used for indexing special target blocks for AI. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BlockIndexer{
|
||||
/** Size of one ore quadrant. */
|
||||
private final static int oreQuadrantSize = 20;
|
||||
/** Size of one structure quadrant. */
|
||||
private final static int structQuadrantSize = 12;
|
||||
|
||||
/** Set of all ores that are being scanned. */
|
||||
private final ObjectSet<Item> scanOres = ObjectSet.with(Item.getAllOres().toArray(Item.class));
|
||||
private final ObjectSet<Item> itemSet = new ObjectSet<>();
|
||||
/** Stores all ore quadtrants on the map. */
|
||||
private ObjectMap<Item, ObjectSet<Tile>> ores;
|
||||
/** 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 -> {
|
||||
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)structQuadrantSize), Mathf.ceil(world.height() / (float)structQuadrantSize));
|
||||
}
|
||||
|
||||
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 * structQuadrantSize, y * structQuadrantSize));
|
||||
}
|
||||
}
|
||||
|
||||
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()){
|
||||
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)){
|
||||
for(Tile tile : getFlagged(enemy)[type.ordinal()]){
|
||||
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, Predicate<Tile> pred){
|
||||
TileEntity closest = null;
|
||||
float dst = 0;
|
||||
|
||||
for(int rx = Math.max((int)((x - range) / tilesize / structQuadrantSize), 0); rx <= (int)((x + range) / tilesize / structQuadrantSize) && rx < quadWidth(); rx++){
|
||||
for(int ry = Math.max((int)((y - range) / tilesize / structQuadrantSize), 0); ry <= (int)((y + range) / tilesize / structQuadrantSize) && ry < quadHeight(); ry++){
|
||||
|
||||
if(!getQuad(team, rx, ry)) continue;
|
||||
|
||||
for(int tx = rx * structQuadrantSize; tx < (rx + 1) * structQuadrantSize && tx < world.width(); tx++){
|
||||
for(int ty = ry * structQuadrantSize; ty < (ry + 1) * structQuadrantSize && ty < world.height(); ty++){
|
||||
Tile other = world.ltile(tx, ty);
|
||||
|
||||
if(other == null) continue;
|
||||
|
||||
if(other.entity == null || other.getTeam() != team || !pred.test(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)){
|
||||
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 #oreQuadrantSize} / 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, world.indexer.getOrePositions(item));
|
||||
|
||||
if(tile == null) return null;
|
||||
|
||||
for(int x = Math.max(0, tile.x - oreQuadrantSize / 2); x < tile.x + oreQuadrantSize / 2 && x < world.width(); x++){
|
||||
for(int y = Math.max(0, tile.y - oreQuadrantSize / 2); y < tile.y + oreQuadrantSize / 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.none){
|
||||
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 / oreQuadrantSize;
|
||||
int quadrantY = tile.y / oreQuadrantSize;
|
||||
itemSet.clear();
|
||||
|
||||
Tile rounded = world.tile(Mathf.clamp(quadrantX * oreQuadrantSize + oreQuadrantSize / 2, 0, world.width() - 1),
|
||||
Mathf.clamp(quadrantY * oreQuadrantSize + oreQuadrantSize / 2, 0, world.height() - 1));
|
||||
|
||||
//find all items that this quadrant contains
|
||||
for(int x = quadrantX * structQuadrantSize; x < world.width() && x < (quadrantX + 1) * structQuadrantSize; x++){
|
||||
for(int y = quadrantY * structQuadrantSize; y < world.height() && y < (quadrantY + 1) * structQuadrantSize; y++){
|
||||
Tile result = world.tile(x, y);
|
||||
if(result == null || result.drop() == null || !scanOres.contains(result.drop())) 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 / structQuadrantSize;
|
||||
int quadrantY = tile.y / structQuadrantSize;
|
||||
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 * structQuadrantSize; x < world.width() && x < (quadrantX + 1) * structQuadrantSize; x++){
|
||||
for(int y = quadrantY * structQuadrantSize; y < world.height() && y < (quadrantY + 1) * structQuadrantSize; 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)structQuadrantSize);
|
||||
}
|
||||
|
||||
private int quadHeight(){
|
||||
return Mathf.ceil(world.height() / (float)structQuadrantSize);
|
||||
}
|
||||
|
||||
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 / oreQuadrantSize);
|
||||
int qy = (y / oreQuadrantSize);
|
||||
|
||||
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 * oreQuadrantSize + oreQuadrantSize / 2, 0, world.width() - 1),
|
||||
Mathf.clamp(qy * oreQuadrantSize + oreQuadrantSize / 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.pfa.Heuristic;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.function.Predicate;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
|
||||
public class Heuristics {
|
||||
/**How many times more it costs to go through a destructible block than an empty block.*/
|
||||
static final float solidMultiplier = 5f;
|
||||
/**How many times more it costs to go through a tile that touches a solid block.*/
|
||||
static final float occludedMultiplier = 5f;
|
||||
|
||||
/**Calculates the fastest path. No priorities, just avoids solid blocks.*/
|
||||
public static class FastestHeuristic implements Heuristic<Tile> {
|
||||
|
||||
@Override
|
||||
public float estimate(Tile node, Tile other){
|
||||
//Get Manhattan distance cost
|
||||
float cost = Math.abs(node.worldx() - other.worldx()) + Math.abs(node.worldy() - other.worldy());
|
||||
|
||||
//If either one of the tiles is a breakable solid block (that is, it's player-made),
|
||||
//increase the cost by the tilesize times the solid block multiplier
|
||||
//Also add the block health, so blocks with more health cost more to traverse
|
||||
if(node.breakable() && node.block().solid) cost += tilesize* solidMultiplier + node.block().health;
|
||||
if(other.breakable() && other.block().solid) cost += tilesize* solidMultiplier + other.block().health;
|
||||
|
||||
//if this block has solid blocks near it, increase the cost, as we don't want enemies hugging walls
|
||||
if(node.occluded) cost += tilesize*occludedMultiplier;
|
||||
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
|
||||
/**Calculates the fastest and most destructive path based on a block predicate.*/
|
||||
public static class DestrutiveHeuristic implements Heuristic<Tile> {
|
||||
/**Should return whether a block if "free", e.g. whether it's an important target*/
|
||||
private final Predicate<Block> frees;
|
||||
|
||||
public DestrutiveHeuristic(Predicate<Block> frees){
|
||||
this.frees = frees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float estimate(Tile node, Tile other){
|
||||
//Get Manhattan distance cost
|
||||
float cost = Math.abs(node.worldx() - other.worldx()) + Math.abs(node.worldy() - other.worldy());
|
||||
|
||||
//If either one of the tiles is a breakable solid block (that is, it's player-made),
|
||||
//increase the cost by the tilesize times the solid block multiplier
|
||||
//Also add the block health, so blocks with more health cost more to traverse
|
||||
if(node.breakable() && node.block().solid) cost += tilesize* solidMultiplier + node.block().health;
|
||||
if(other.breakable() && other.block().solid) cost += tilesize* solidMultiplier + other.block().health;
|
||||
|
||||
//if this block has solid blocks near it, increase the cost, as we don't want enemies hugging walls
|
||||
if(node.occluded) cost += tilesize*occludedMultiplier;
|
||||
|
||||
if(other.getLinked() != null) other = other.getLinked();
|
||||
if(node.getLinked() != null) node = node.getLinked();
|
||||
|
||||
//check if it's free
|
||||
if(frees.test(other.block()) || frees.test(node.block())) cost = 0;
|
||||
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
/**An interface for an indexed graph that doesn't use allocations for connections.*/
|
||||
public interface OptimizedGraph<N>{
|
||||
/**This is used in the same way as getConnections(), but does not use Connection objects.*/
|
||||
N[] connectionsOf(N node);
|
||||
|
||||
/** Returns the unique index of the given node.
|
||||
* @param node the node whose index will be returned
|
||||
* @return the unique index of the given node. */
|
||||
int getIndex (N node);
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.pfa.*;
|
||||
import com.badlogic.gdx.utils.BinaryHeap;
|
||||
import com.badlogic.gdx.utils.IntMap;
|
||||
import com.badlogic.gdx.utils.TimeUtils;
|
||||
|
||||
/**An IndexedAStarPathfinder that uses an OptimizedGraph, and therefore has less allocations.*/
|
||||
public class OptimizedPathFinder<N> implements PathFinder<N> {
|
||||
OptimizedGraph<N> graph;
|
||||
IntMap<NodeRecord<N>> records = new IntMap<>();
|
||||
BinaryHeap<NodeRecord<N>> openList;
|
||||
NodeRecord<N> current;
|
||||
|
||||
/**
|
||||
* The unique ID for each search run. Used to mark nodes.
|
||||
*/
|
||||
private int searchId;
|
||||
|
||||
private static final byte UNVISITED = 0;
|
||||
private static final byte OPEN = 1;
|
||||
private static final byte CLOSED = 2;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public OptimizedPathFinder(OptimizedGraph<N> graph) {
|
||||
this.graph = graph;
|
||||
this.openList = new BinaryHeap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean searchConnectionPath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<Connection<N>> outPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean searchNodePath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<N> outPath) {
|
||||
|
||||
// Perform AStar
|
||||
boolean found = search(startNode, endNode, heuristic);
|
||||
|
||||
if (found) {
|
||||
// Create a path made of nodes
|
||||
generateNodePath(startNode, outPath);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
protected boolean search(N startNode, N endNode, Heuristic<N> heuristic) {
|
||||
|
||||
initSearch(startNode, endNode, heuristic);
|
||||
|
||||
// Iterate through processing each node
|
||||
do {
|
||||
// Retrieve the node with smallest estimated total cost from the open list
|
||||
current = openList.pop();
|
||||
current.category = CLOSED;
|
||||
|
||||
// Terminate if we reached the goal node
|
||||
if (current.node == endNode) return true;
|
||||
|
||||
visitChildren(endNode, heuristic);
|
||||
|
||||
} while (openList.size > 0);
|
||||
|
||||
// We've run out of nodes without finding the goal, so there's no solution
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean search(PathFinderRequest<N> request, long timeToRun) {
|
||||
|
||||
long lastTime = TimeUtils.nanoTime();
|
||||
|
||||
// We have to initialize the search if the status has just changed
|
||||
if (request.statusChanged) {
|
||||
initSearch(request.startNode, request.endNode, request.heuristic);
|
||||
request.statusChanged = false;
|
||||
}
|
||||
|
||||
// Iterate through processing each node
|
||||
do {
|
||||
|
||||
// Check the available time
|
||||
long currentTime = TimeUtils.nanoTime();
|
||||
timeToRun -= currentTime - lastTime;
|
||||
if (timeToRun <= PathFinderQueue.TIME_TOLERANCE) return false;
|
||||
|
||||
// Retrieve the node with smallest estimated total cost from the open list
|
||||
current = openList.pop();
|
||||
current.category = CLOSED;
|
||||
|
||||
// Terminate if we reached the goal node; we've found a path.
|
||||
if (current.node == request.endNode) {
|
||||
request.pathFound = true;
|
||||
|
||||
generateNodePath(request.startNode, request.resultPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Visit current node's children
|
||||
visitChildren(request.endNode, request.heuristic);
|
||||
|
||||
// Store the current time
|
||||
lastTime = currentTime;
|
||||
|
||||
} while (openList.size > 0);
|
||||
|
||||
// The open list is empty and we've not found a path.
|
||||
request.pathFound = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void initSearch(N startNode, N endNode, Heuristic<N> heuristic) {
|
||||
|
||||
// Increment the search id
|
||||
if (++searchId < 0) searchId = 1;
|
||||
|
||||
// Initialize the open list
|
||||
openList.clear();
|
||||
|
||||
// Initialize the record for the start node and add it to the open list
|
||||
NodeRecord<N> startRecord = getNodeRecord(startNode);
|
||||
startRecord.node = startNode;
|
||||
//startRecord.connection = null;
|
||||
startRecord.costSoFar = 0;
|
||||
addToOpenList(startRecord, heuristic.estimate(startNode, endNode));
|
||||
|
||||
current = null;
|
||||
}
|
||||
|
||||
protected void visitChildren(N endNode, Heuristic<N> heuristic) {
|
||||
// Get current node's outgoing connections
|
||||
//Array<Connection<N>> connections = graph.getConnections(current.node);
|
||||
N[] conn = graph.connectionsOf(current.node);
|
||||
|
||||
// Loop through each connection in turn
|
||||
for (int i = 0; i < conn.length; i++) {
|
||||
|
||||
//Connection<N> connection = connections.get(i)
|
||||
|
||||
// Get the cost estimate for the node
|
||||
N node = conn[i];
|
||||
|
||||
if(node == null) continue;
|
||||
|
||||
float addCost = heuristic.estimate(current.node, node);
|
||||
|
||||
float nodeCost = current.costSoFar + addCost;
|
||||
|
||||
float nodeHeuristic;
|
||||
NodeRecord<N> nodeRecord = getNodeRecord(node);
|
||||
if (nodeRecord.category == CLOSED) { // The node is closed
|
||||
|
||||
// If we didn't find a shorter route, skip
|
||||
if (nodeRecord.costSoFar <= nodeCost) continue;
|
||||
|
||||
// We can use the node's old cost values to calculate its heuristic
|
||||
// without calling the possibly expensive heuristic function
|
||||
nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
|
||||
} else if (nodeRecord.category == OPEN) { // The node is open
|
||||
|
||||
// If our route is no better, then skip
|
||||
if (nodeRecord.costSoFar <= nodeCost) continue;
|
||||
|
||||
// Remove it from the open list (it will be re-added with the new cost)
|
||||
openList.remove(nodeRecord);
|
||||
|
||||
// We can use the node's old cost values to calculate its heuristic
|
||||
// without calling the possibly expensive heuristic function
|
||||
nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
|
||||
} else { // the node is unvisited
|
||||
|
||||
// We'll need to calculate the heuristic value using the function,
|
||||
// since we don't have a node record with a previously calculated value
|
||||
nodeHeuristic = heuristic.estimate(node, endNode);
|
||||
}
|
||||
|
||||
// Update node record's cost and connection
|
||||
nodeRecord.costSoFar = nodeCost;
|
||||
nodeRecord.from = current.node; //TODO ???
|
||||
|
||||
// Add it to the open list with the estimated total cost
|
||||
addToOpenList(nodeRecord, nodeCost + nodeHeuristic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void generateNodePath(N startNode, GraphPath<N> outPath) {
|
||||
|
||||
// Work back along the path, accumulating nodes
|
||||
// outPath.clear();
|
||||
while (current.from != null) {
|
||||
outPath.add(current.node);
|
||||
current = records.get(graph.getIndex(current.from));
|
||||
}
|
||||
outPath.add(startNode);
|
||||
|
||||
// Reverse the path
|
||||
outPath.reverse();
|
||||
}
|
||||
|
||||
protected void addToOpenList(NodeRecord<N> nodeRecord, float estimatedTotalCost) {
|
||||
openList.add(nodeRecord, estimatedTotalCost);
|
||||
nodeRecord.category = OPEN;
|
||||
}
|
||||
|
||||
protected NodeRecord<N> getNodeRecord(N node) {
|
||||
if(!records.containsKey(graph.getIndex(node))){
|
||||
NodeRecord<N> record = new NodeRecord<>();
|
||||
record.node = node;
|
||||
record.searchId = searchId;
|
||||
records.put(graph.getIndex(node), record);
|
||||
return record;
|
||||
}else{
|
||||
return records.get(graph.getIndex(node));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This nested class is used to keep track of the information we need for each node during the search.
|
||||
*
|
||||
* @param <N> Type of node
|
||||
* @author davebaol
|
||||
*/
|
||||
static class NodeRecord<N> extends BinaryHeap.Node {
|
||||
/**
|
||||
* The reference to the node.
|
||||
*/
|
||||
N node;
|
||||
N from;
|
||||
|
||||
/**
|
||||
* The incoming connection to the node
|
||||
*/
|
||||
//Connection<N> connection;
|
||||
|
||||
/**
|
||||
* The actual cost from the start node.
|
||||
*/
|
||||
float costSoFar;
|
||||
|
||||
/**
|
||||
* The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #CLOSED}.
|
||||
*/
|
||||
byte category;
|
||||
|
||||
/**
|
||||
* ID of the current search.
|
||||
*/
|
||||
int searchId;
|
||||
|
||||
/**
|
||||
* Creates a {@code NodeRecord}.
|
||||
*/
|
||||
public NodeRecord() {
|
||||
super(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the estimated total cost.
|
||||
*/
|
||||
public float getEstimatedTotalCost() {
|
||||
return getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.pfa.PathFinderRequest;
|
||||
import com.badlogic.gdx.ai.pfa.PathSmoother;
|
||||
import com.badlogic.gdx.math.MathUtils;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.game.SpawnPoint;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.util.Angles;
|
||||
import io.anuke.ucore.util.Log;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class Pathfind{
|
||||
/**Maximum time taken per frame on pathfinding for a single path.*/
|
||||
private static final long maxTime = 1000000 * 5;
|
||||
|
||||
/**Tile graph, for determining conenctions between two tiles*/
|
||||
TileGraph graph = new TileGraph();
|
||||
/**Smoother that removes extra nodes from a path.*/
|
||||
PathSmoother<Tile, Vector2> smoother = new PathSmoother<Tile, Vector2>(new Raycaster());
|
||||
/**temporary vector2 for calculations*/
|
||||
Vector2 vector = new Vector2();
|
||||
|
||||
Vector2 v1 = new Vector2();
|
||||
Vector2 v2 = new Vector2();
|
||||
Vector2 v3 = new Vector2();
|
||||
|
||||
/**Finds the position on the path an enemy should move to.
|
||||
* If the path is not yet calculated, this returns the enemy's position (i. e. "don't move")
|
||||
* @param enemy The enemy to find a path for
|
||||
* @return The position the enemy should move to.*/
|
||||
public Vector2 find(Enemy enemy){
|
||||
//TODO fix -1/-2 node usage
|
||||
if(enemy.node == -1 || enemy.node == -2){
|
||||
findNode(enemy);
|
||||
}
|
||||
|
||||
if(enemy.node == -2){
|
||||
enemy.node = -1;
|
||||
}
|
||||
|
||||
if(enemy.node < 0 || world.getSpawns().get(enemy.lane).pathTiles == null){
|
||||
return vector.set(enemy.x, enemy.y);
|
||||
}
|
||||
|
||||
Tile[] path = world.getSpawns().get(enemy.lane).pathTiles;
|
||||
|
||||
if(enemy.node >= path.length){
|
||||
enemy.node = -1;
|
||||
return vector.set(enemy.x, enemy.y);
|
||||
}
|
||||
|
||||
if(enemy.node <= -1){
|
||||
return vector.set(enemy.x, enemy.y);
|
||||
}
|
||||
|
||||
//TODO documentation on what this does
|
||||
Tile prev = path[enemy.node - 1];
|
||||
|
||||
Tile target = path[enemy.node];
|
||||
|
||||
//a bridge has been broken, re-path
|
||||
if(!world.passable(target.x, target.y)){
|
||||
remakePath();
|
||||
return vector.set(enemy.x, enemy.y);
|
||||
}
|
||||
|
||||
float projectLen = Vector2.dst(prev.worldx(), prev.worldy(), target.worldx(), target.worldy()) / 6f;
|
||||
|
||||
Vector2 projection = projectPoint(prev.worldx(), prev.worldy(),
|
||||
target.worldx(), target.worldy(), enemy.x, enemy.y);
|
||||
|
||||
boolean canProject = true;
|
||||
|
||||
if(projectLen < 8 || !onLine(projection, prev.worldx(), prev.worldy(), target.worldx(), target.worldy())){
|
||||
canProject = false;
|
||||
}else{
|
||||
projection.add(v1.set(projectLen, 0).rotate(Angles.angle(prev.worldx(), prev.worldy(),
|
||||
target.worldx(), target.worldy())));
|
||||
}
|
||||
|
||||
float dst = Vector2.dst(enemy.x, enemy.y, target.worldx(), target.worldy());
|
||||
float nlinedist = enemy.node >= path.length - 1 ? 9999 :
|
||||
pointLineDist(path[enemy.node].worldx(), path[enemy.node].worldy(),
|
||||
path[enemy.node + 1].worldx(), path[enemy.node + 1].worldy(), enemy.x, enemy.y);
|
||||
|
||||
if(dst < 8 || nlinedist < 8){
|
||||
if(enemy.node <= path.length-2)
|
||||
enemy.node ++;
|
||||
|
||||
target = path[enemy.node];
|
||||
}
|
||||
|
||||
if(canProject && projection.dst(enemy.x, enemy.y) < Vector2.dst(target.x, target.y, enemy.x, enemy.y)){
|
||||
vector.set(projection);
|
||||
}else{
|
||||
vector.set(target.worldx(), target.worldy());
|
||||
}
|
||||
|
||||
//near the core, stop
|
||||
if(enemy.node == path.length - 1){
|
||||
vector.set(target.worldx(), target.worldy());
|
||||
}
|
||||
|
||||
return vector;
|
||||
|
||||
}
|
||||
|
||||
/**Re-calculate paths for all enemies. Runs when a path changes while moving.*/
|
||||
private void remakePath(){
|
||||
for(int i = 0; i < enemyGroup.size(); i ++){
|
||||
Enemy enemy = enemyGroup.all().get(i);
|
||||
enemy.node = -1;
|
||||
}
|
||||
|
||||
resetPaths();
|
||||
}
|
||||
|
||||
/**Update the pathfinders and continue calculating the path if it hasn't been calculated yet.
|
||||
* This method is run each frame.*/
|
||||
public void update(){
|
||||
|
||||
//go through each spawnpoint, and if it's not found a path yet, update it
|
||||
for(int i = 0; i < world.getSpawns().size; i ++){
|
||||
SpawnPoint point = world.getSpawns().get(i);
|
||||
if(point.request == null || point.finder == null){
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!point.request.pathFound){
|
||||
try{
|
||||
if(point.finder.search(point.request, maxTime)){
|
||||
smoother.smoothPath(point.path);
|
||||
point.pathTiles = point.path.nodes.toArray(Tile.class);
|
||||
point.finder = null;
|
||||
}
|
||||
}catch (ArrayIndexOutOfBoundsException e){
|
||||
//no path
|
||||
point.request.pathFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//1300-1500ms, usually 1400 unoptimized on Caldera
|
||||
/**Benchmark pathfinding speed. Debugging stuff.*/
|
||||
public void benchmark(){
|
||||
SpawnPoint point = world.getSpawns().first();
|
||||
int amount = 100;
|
||||
|
||||
//warmup
|
||||
for(int i = 0; i < 100; i ++){
|
||||
point.finder.searchNodePath(point.start, world.getCore(), state.difficulty.heuristic, point.path);
|
||||
point.path.clear();
|
||||
}
|
||||
|
||||
Timers.mark();
|
||||
for(int i = 0; i < amount; i ++){
|
||||
point.finder.searchNodePath(point.start, world.getCore(), state.difficulty.heuristic, point.path);
|
||||
point.path.clear();
|
||||
}
|
||||
Log.info("Time elapsed: {0}ms\nAverage MS per path: {1}", Timers.elapsed(), Timers.elapsed()/amount);
|
||||
}
|
||||
|
||||
/**Reset and clear the paths.*/
|
||||
public void resetPaths(){
|
||||
for(int i = 0; i < world.getSpawns().size; i ++){
|
||||
resetPathFor(world.getSpawns().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void resetPathFor(SpawnPoint point){
|
||||
point.finder = new OptimizedPathFinder<>(graph);
|
||||
|
||||
point.path.clear();
|
||||
|
||||
point.pathTiles = null;
|
||||
|
||||
point.request = new PathFinderRequest<>(point.start, world.getCore(), state.difficulty.heuristic, point.path);
|
||||
point.request.statusChanged = true; //IMPORTANT!
|
||||
}
|
||||
|
||||
/**For an enemy that was just loaded from a save, find the node in the path it should be following.*/
|
||||
void findNode(Enemy enemy){
|
||||
if(enemy.lane >= world.getSpawns().size || enemy.lane < 0){
|
||||
enemy.lane = 0;
|
||||
}
|
||||
|
||||
if(world.getSpawns().get(enemy.lane).pathTiles == null){
|
||||
return;
|
||||
}
|
||||
|
||||
Tile[] path = world.getSpawns().get(enemy.lane).pathTiles;
|
||||
|
||||
int closest = findClosest(path, enemy.x, enemy.y);
|
||||
|
||||
closest = Mathf.clamp(closest, 1, path.length-1);
|
||||
if(closest == -1){
|
||||
return;
|
||||
}
|
||||
|
||||
enemy.node = closest;
|
||||
}
|
||||
|
||||
/**Finds the closest tile to a position, in an array of tiles.*/
|
||||
private int findClosest(Tile[] tiles, float x, float y){
|
||||
int cindex = -2;
|
||||
float dst = Float.MAX_VALUE;
|
||||
|
||||
for(int i = 0; i < tiles.length - 1; i ++){
|
||||
Tile tile = tiles[i];
|
||||
Tile next = tiles[i + 1];
|
||||
float d = pointLineDist(tile.worldx(), tile.worldy(), next.worldx(), next.worldy(), x, y);
|
||||
if(d < dst){
|
||||
dst = d;
|
||||
cindex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return cindex + 1;
|
||||
}
|
||||
|
||||
/**Returns whether a point is on a line.*/
|
||||
private boolean onLine(Vector2 vector, float x1, float y1, float x2, float y2){
|
||||
return MathUtils.isEqual(vector.dst(x1, y1) + vector.dst(x2, y2), Vector2.dst(x1, y1, x2, y2), 0.01f);
|
||||
}
|
||||
|
||||
/**Returns distance from a point to a line segment.*/
|
||||
private float pointLineDist(float x, float y, float x2, float y2, float px, float py){
|
||||
float l2 = Vector2.dst2(x, y, x2, y2);
|
||||
float t = Math.max(0, Math.min(1, Vector2.dot(px - x, py - y, x2 - x, y2 - y) / l2));
|
||||
Vector2 projection = v1.set(x, y).add(v2.set(x2, y2).sub(x, y).scl(t)); // Projection falls on the segment
|
||||
return projection.dst(px, py);
|
||||
}
|
||||
|
||||
//TODO documentation
|
||||
private Vector2 projectPoint(float x1, float y1, float x2, float y2, float pointx, float pointy){
|
||||
float px = x2-x1, py = y2-y1, dAB = px*px + py*py;
|
||||
float u = ((pointx - x1) * px + (pointy - y1) * py) / dAB;
|
||||
float x = x1 + u * px, y = y1 + u * py;
|
||||
return v3.set(x, y); //this is D
|
||||
}
|
||||
}
|
||||
211
core/src/io/anuke/mindustry/ai/Pathfinder.java
Normal file
211
core/src/io/anuke/mindustry/ai/Pathfinder.java
Normal file
@@ -0,0 +1,211 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.arc.collection.IntArray;
|
||||
import io.anuke.arc.collection.IntQueue;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.arc.math.geom.Point2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.game.EventType.TileChangeEvent;
|
||||
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.game.Teams.TeamData;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.world.Pos;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.meta.BlockFlag;
|
||||
|
||||
import static io.anuke.mindustry.Vars.state;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class Pathfinder{
|
||||
private static final long maxUpdate = Time.millisToNanos(4);
|
||||
private PathData[] paths;
|
||||
private IntArray blocked = new IntArray();
|
||||
|
||||
public Pathfinder(){
|
||||
Events.on(WorldLoadEvent.class, event -> clear());
|
||||
Events.on(TileChangeEvent.class, event -> {
|
||||
if(Net.client()) return;
|
||||
|
||||
for(Team team : Team.all){
|
||||
TeamData data = state.teams.get(team);
|
||||
if(state.teams.isActive(team) && data.team != event.tile.getTeam()){
|
||||
update(event.tile, data.team);
|
||||
}
|
||||
}
|
||||
|
||||
update(event.tile, event.tile.getTeam());
|
||||
});
|
||||
}
|
||||
|
||||
public void updateSolid(Tile tile){
|
||||
update(tile, tile.getTeam());
|
||||
}
|
||||
|
||||
public void update(){
|
||||
if(Net.client() || paths == null) return;
|
||||
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.isActive(team)){
|
||||
updateFrontier(team, maxUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Tile getTargetTile(Team team, Tile tile){
|
||||
float[][] values = paths[team.ordinal()].weights;
|
||||
|
||||
if(values == null || tile == null) return tile;
|
||||
|
||||
float value = values[tile.x][tile.y];
|
||||
|
||||
Tile target = null;
|
||||
float tl = 0f;
|
||||
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 && (target == 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
|
||||
target = other;
|
||||
tl = values[dx][dy];
|
||||
}
|
||||
}
|
||||
|
||||
if(target == null || tl == Float.MAX_VALUE) return tile;
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
public float getValueforTeam(Team team, int x, int y){
|
||||
return paths == null || paths[team.ordinal()].weights == null || team.ordinal() >= paths.length ? 0 : Structs.inBounds(x, y, paths[team.ordinal()].weights) ? paths[team.ordinal()].weights[x][y] : 0;
|
||||
}
|
||||
|
||||
private boolean passable(Tile tile, Team team){
|
||||
return (!tile.solid()) || (tile.breakable() && (tile.getTeam() != team));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the frontier, increments the search and sets up all flow sources.
|
||||
* This only occurs for active teams.
|
||||
*/
|
||||
private void update(Tile tile, Team team){
|
||||
//make sure team exists
|
||||
if(paths != null && paths[team.ordinal()] != null && paths[team.ordinal()].weights != null){
|
||||
PathData path = paths[team.ordinal()];
|
||||
|
||||
if(path.weights[tile.x][tile.y] <= 0.1f){
|
||||
//this was a previous target
|
||||
path.frontier.clear();
|
||||
}else if(!path.frontier.isEmpty()){
|
||||
return;
|
||||
}
|
||||
|
||||
//impassable tiles have a weight of float.max
|
||||
if(!passable(tile, team)){
|
||||
path.weights[tile.x][tile.y] = Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
//increment search, clear frontier
|
||||
path.search++;
|
||||
path.frontier.clear();
|
||||
path.lastSearchTime = Time.millis();
|
||||
|
||||
//add all targets to the frontier
|
||||
for(Tile other : world.indexer.getEnemy(team, BlockFlag.target)){
|
||||
path.weights[other.x][other.y] = 0;
|
||||
path.searches[other.x][other.y] = (short)path.search;
|
||||
path.frontier.addFirst(other.pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createFor(Team team){
|
||||
PathData path = new PathData();
|
||||
path.weights = new float[world.width()][world.height()];
|
||||
path.searches = new short[world.width()][world.height()];
|
||||
path.search++;
|
||||
path.frontier.ensureCapacity((world.width() + world.height()) * 3);
|
||||
|
||||
paths[team.ordinal()] = path;
|
||||
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
Tile tile = world.tile(x, y);
|
||||
|
||||
if(state.teams.areEnemies(tile.getTeam(), team)
|
||||
&& tile.block().flags.contains(BlockFlag.target)){
|
||||
path.frontier.addFirst(tile.pos());
|
||||
path.weights[x][y] = 0;
|
||||
path.searches[x][y] = (short)path.search;
|
||||
}else{
|
||||
path.weights[x][y] = Float.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateFrontier(team, -1);
|
||||
}
|
||||
|
||||
private void updateFrontier(Team team, long nsToRun){
|
||||
PathData path = paths[team.ordinal()];
|
||||
|
||||
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
|
||||
float 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 < Float.MAX_VALUE){
|
||||
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(other, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clear(){
|
||||
Time.mark();
|
||||
|
||||
paths = new PathData[Team.all.length];
|
||||
blocked.clear();
|
||||
|
||||
for(Team team : Team.all){
|
||||
PathData path = new PathData();
|
||||
paths[team.ordinal()] = path;
|
||||
|
||||
if(state.teams.isActive(team)){
|
||||
createFor(team);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PathData{
|
||||
float[][] weights;
|
||||
short[][] searches;
|
||||
int search = 0;
|
||||
long lastSearchTime;
|
||||
IntQueue frontier = new IntQueue();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.utils.Collision;
|
||||
import com.badlogic.gdx.ai.utils.Ray;
|
||||
import com.badlogic.gdx.ai.utils.RaycastCollisionDetector;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.util.Geometry;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class Raycaster implements RaycastCollisionDetector<Vector2>{
|
||||
private boolean found = false;
|
||||
|
||||
@Override
|
||||
public boolean collides(Ray<Vector2> ray){
|
||||
found = false;
|
||||
|
||||
Geometry.iterateLine(0f, ray.start.x, ray.start.y, ray.end.x, ray.end.y, tilesize, (x, y)->{
|
||||
if(solid(x, y)){
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean findCollision(Collision<Vector2> collision, Ray<Vector2> ray){
|
||||
Vector2 v = vectorCast(ray.start.x, ray.start.y, ray.end.x, ray.end.y);
|
||||
if(v == null) return false;
|
||||
collision.point = v;
|
||||
collision.normal = v.nor();
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2 vectorCast(float x0f, float y0f, float x1f, float y1f){
|
||||
int x0 = (int)x0f;
|
||||
int y0 = (int)y0f;
|
||||
int x1 = (int)x1f;
|
||||
int y1 = (int)y1f;
|
||||
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(solid(x0, y0)){
|
||||
return new Vector2(x0, y0);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean solid(float x, float y){
|
||||
Tile tile = world.tile(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize));
|
||||
|
||||
if(tile == null || tile.solid()) return true;
|
||||
|
||||
for(int i = 0; i < 4; i ++){
|
||||
Tile near = tile.getNearby(i);
|
||||
if(near == null || near.solid()) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.pfa.DefaultGraphPath;
|
||||
import com.badlogic.gdx.ai.pfa.SmoothableGraphPath;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
public class SmoothGraphPath extends DefaultGraphPath<Tile> implements SmoothableGraphPath<Tile, Vector2>{
|
||||
private Vector2 vector = new Vector2();
|
||||
|
||||
@Override
|
||||
public Vector2 getNodePosition(int index){
|
||||
Tile tile = nodes.get(index);
|
||||
return vector.set(tile.worldx(), tile.worldy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swapNodes(int index1, int index2){
|
||||
nodes.swap(index1, index2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void truncatePath(int newLength){
|
||||
nodes.truncate(newLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add (Tile node) {
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
/**Tilegraph that ignores player-made tiles.*/
|
||||
public class TileGraph implements OptimizedGraph<Tile> {
|
||||
private Tile[] tiles = new Tile[4];
|
||||
|
||||
/**Used for the OptimizedPathFinder implementation.*/
|
||||
@Override
|
||||
public Tile[] connectionsOf(Tile node){
|
||||
Tile[] nodes = node.getNearby(tiles);
|
||||
for(int i = 0; i < 4; i ++){
|
||||
if(nodes[i] != null && !nodes[i].passable()){
|
||||
nodes[i] = null;
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndex(Tile node){
|
||||
return node.packedPosition();
|
||||
}
|
||||
}
|
||||
160
core/src/io/anuke/mindustry/ai/WaveSpawner.java
Normal file
160
core/src/io/anuke/mindustry/ai/WaveSpawner.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package io.anuke.mindustry.ai;
|
||||
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.function.PositionConsumer;
|
||||
import io.anuke.arc.math.Angles;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.arc.util.Tmp;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Damage;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.type.BaseUnit;
|
||||
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
|
||||
import io.anuke.mindustry.game.SpawnGroup;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.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.isFlying){
|
||||
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(PositionConsumer 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.accept(spawnX, spawnY);
|
||||
}
|
||||
|
||||
if(state.rules.attackMode && state.teams.isActive(waveTeam)){
|
||||
for(Tile core : state.teams.get(waveTeam).cores){
|
||||
cons.accept(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;
|
||||
}
|
||||
}
|
||||
1748
core/src/io/anuke/mindustry/content/Blocks.java
Normal file
1748
core/src/io/anuke/mindustry/content/Blocks.java
Normal file
File diff suppressed because it is too large
Load Diff
711
core/src/io/anuke/mindustry/content/Bullets.java
Normal file
711
core/src/io/anuke/mindustry/content/Bullets.java
Normal file
@@ -0,0 +1,711 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.arc.util.Tmp;
|
||||
import io.anuke.mindustry.entities.Damage;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.bullet.*;
|
||||
import io.anuke.mindustry.entities.effect.*;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.BuildBlock;
|
||||
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class Bullets implements ContentList{
|
||||
public static BulletType
|
||||
|
||||
//artillery
|
||||
artilleryDense, arilleryPlastic, artilleryPlasticFrag, artilleryHoming, artlleryIncendiary, artilleryExplosive, artilleryUnit,
|
||||
|
||||
//flak
|
||||
flakScrap, flakLead, flakPlastic, flakExplosive, flakSurge,
|
||||
|
||||
//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, frag, eruptorShot,
|
||||
|
||||
//bombs
|
||||
bombExplosive, bombIncendiary, bombOil, explode;
|
||||
|
||||
@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, 7, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
bulletShrink = 1f;
|
||||
lifetime = 15f;
|
||||
backColor = Pal.plastaniumBack;
|
||||
frontColor = Pal.plastaniumFront;
|
||||
despawnEffect = Fx.none;
|
||||
}};
|
||||
|
||||
arilleryPlastic = 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;
|
||||
}};
|
||||
|
||||
artlleryIncendiary = new ArtilleryBulletType(3f, 0, "shell"){{
|
||||
hitEffect = Fx.blastExplosion;
|
||||
knockback = 0.8f;
|
||||
lifetime = 60f;
|
||||
bulletWidth = bulletHeight = 13f;
|
||||
collidesTiles = false;
|
||||
splashDamageRadius = 25f;
|
||||
splashDamage = 30f;
|
||||
incendAmount = 4;
|
||||
incendSpread = 11f;
|
||||
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;
|
||||
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;
|
||||
}};
|
||||
|
||||
flakLead = new FlakBulletType(4.2f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 3f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
bulletWidth = 6f;
|
||||
bulletHeight = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 27f;
|
||||
splashDamageRadius = 15f;
|
||||
}};
|
||||
|
||||
flakScrap = new FlakBulletType(4f, 3){{
|
||||
lifetime = 60f;
|
||||
ammoMultiplier = 3f;
|
||||
shootEffect = Fx.shootSmall;
|
||||
reloadMultiplier = 0.5f;
|
||||
bulletWidth = 6f;
|
||||
bulletHeight = 8f;
|
||||
hitEffect = Fx.flakExplosion;
|
||||
splashDamage = 22f;
|
||||
splashDamageRadius = 24f;
|
||||
}};
|
||||
|
||||
flakPlastic = new FlakBulletType(4f, 6){{
|
||||
splashDamageRadius = 50f;
|
||||
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;
|
||||
}};
|
||||
|
||||
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;
|
||||
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;
|
||||
incendSpread = 10f;
|
||||
incendAmount = 3;
|
||||
}};
|
||||
|
||||
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 = 50f;
|
||||
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;
|
||||
shootEffect = Fx.shootSmall;
|
||||
smokeEffect = Fx.shootSmallSmoke;
|
||||
ammoMultiplier = 1;
|
||||
}};
|
||||
|
||||
standardDense = new BasicBulletType(3.5f, 18, "bullet"){{
|
||||
bulletWidth = 9f;
|
||||
bulletHeight = 12f;
|
||||
reloadMultiplier = 0.6f;
|
||||
ammoMultiplier = 2;
|
||||
}};
|
||||
|
||||
standardThorium = new BasicBulletType(4f, 29, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 13f;
|
||||
shootEffect = Fx.shootBig;
|
||||
smokeEffect = Fx.shootBigSmoke;
|
||||
ammoMultiplier = 2;
|
||||
}};
|
||||
|
||||
standardHoming = new BasicBulletType(3f, 9, "bullet"){{
|
||||
bulletWidth = 7f;
|
||||
bulletHeight = 9f;
|
||||
homingPower = 5f;
|
||||
reloadMultiplier = 1.4f;
|
||||
ammoMultiplier = 3;
|
||||
}};
|
||||
|
||||
standardIncendiary = new BasicBulletType(3.2f, 11, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
frontColor = Pal.lightishOrange;
|
||||
backColor = Pal.lightOrange;
|
||||
incendSpread = 3f;
|
||||
incendAmount = 1;
|
||||
incendChance = 0.3f;
|
||||
inaccuracy = 3f;
|
||||
}};
|
||||
|
||||
standardGlaive = new BasicBulletType(4f, 7.5f, "bullet"){{
|
||||
bulletWidth = 10f;
|
||||
bulletHeight = 12f;
|
||||
frontColor = Color.valueOf("feb380");
|
||||
backColor = Color.valueOf("ea8878");
|
||||
incendSpread = 3f;
|
||||
incendAmount = 1;
|
||||
incendChance = 0.3f;
|
||||
}};
|
||||
|
||||
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;
|
||||
incendSpread = 3f;
|
||||
incendAmount = 2;
|
||||
incendChance = 0.3f;
|
||||
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 BulletType(5.2f, 13){
|
||||
float healPercent = 3f;
|
||||
|
||||
{
|
||||
shootEffect = Fx.shootHeal;
|
||||
smokeEffect = Fx.hitLaser;
|
||||
hitEffect = Fx.hitLaser;
|
||||
despawnEffect = Fx.hitLaser;
|
||||
collidesTeam = true;
|
||||
}
|
||||
|
||||
@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.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());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 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.x, b.y, b.rot(), 30);
|
||||
}
|
||||
};
|
||||
|
||||
arc = new BulletType(0.001f, 25){
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
explode = new BombBulletType(2f, 3f, "clear"){
|
||||
{
|
||||
hitEffect = Fx.pulverize;
|
||||
lifetime = 30f;
|
||||
speed = 1f;
|
||||
splashDamageRadius = 50f;
|
||||
splashDamage = 28f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Bullet b){
|
||||
if(b.getOwner() instanceof Unit){
|
||||
((Unit)b.getOwner()).kill();
|
||||
}
|
||||
b.time(b.lifetime());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
1207
core/src/io/anuke/mindustry/content/Fx.java
Normal file
1207
core/src/io/anuke/mindustry/content/Fx.java
Normal file
File diff suppressed because it is too large
Load Diff
101
core/src/io/anuke/mindustry/content/Items.java
Normal file
101
core/src/io/anuke/mindustry/content/Items.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.type.Item;
|
||||
import io.anuke.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.4f;
|
||||
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.05f;
|
||||
}};
|
||||
|
||||
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;
|
||||
}};
|
||||
}
|
||||
}
|
||||
38
core/src/io/anuke/mindustry/content/Liquids.java
Normal file
38
core/src/io/anuke/mindustry/content/Liquids.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.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;
|
||||
}};
|
||||
|
||||
oil = new Liquid("oil", Color.valueOf("313131")){{
|
||||
viscosity = 0.7f;
|
||||
flammability = 1.2f;
|
||||
explosiveness = 1.2f;
|
||||
heatCapacity = 0.7f;
|
||||
effect = StatusEffects.tarred;
|
||||
}};
|
||||
|
||||
cryofluid = new Liquid("cryofluid", Color.valueOf("6ecdec")){{
|
||||
heatCapacity = 0.9f;
|
||||
temperature = 0.25f;
|
||||
effect = StatusEffects.freezing;
|
||||
}};
|
||||
}
|
||||
}
|
||||
54
core/src/io/anuke/mindustry/content/Loadouts.java
Normal file
54
core/src/io/anuke/mindustry/content/Loadouts.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.type.Loadout;
|
||||
|
||||
public class Loadouts implements ContentList{
|
||||
public static Loadout
|
||||
basicShard,
|
||||
advancedShard,
|
||||
basicFoundation,
|
||||
basicNucleus;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
basicShard = new Loadout(
|
||||
" ### ",
|
||||
" #1# ",
|
||||
" ### ",
|
||||
" ^ ^ ",
|
||||
" ## ## ",
|
||||
" C# C# "
|
||||
);
|
||||
|
||||
advancedShard = new Loadout(
|
||||
" ### ",
|
||||
" #1# ",
|
||||
"#######",
|
||||
"C#^ ^C#",
|
||||
" ## ## ",
|
||||
" C# C# "
|
||||
);
|
||||
|
||||
basicFoundation = new Loadout(
|
||||
" #### ",
|
||||
" #### ",
|
||||
" #2## ",
|
||||
" #### ",
|
||||
" ^^^^ ",
|
||||
" ###### ",
|
||||
" C#C#C# "
|
||||
);
|
||||
|
||||
basicNucleus = new Loadout(
|
||||
" ##### ",
|
||||
" ##### ",
|
||||
" ##3## ",
|
||||
" ##### ",
|
||||
" >#####< ",
|
||||
" ^ ^ ^ ^ ",
|
||||
"#### ####",
|
||||
"C#C# C#C#"
|
||||
);
|
||||
}
|
||||
}
|
||||
374
core/src/io/anuke/mindustry/content/Mechs.java
Normal file
374
core/src/io/anuke/mindustry/content/Mechs.java
Normal file
@@ -0,0 +1,374 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.graphics.Blending;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.Units;
|
||||
import io.anuke.mindustry.entities.bullet.BombBulletType;
|
||||
import io.anuke.mindustry.entities.effect.Lightning;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.graphics.Shaders;
|
||||
import io.anuke.mindustry.type.Mech;
|
||||
import io.anuke.mindustry.type.Weapon;
|
||||
|
||||
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;
|
||||
boostSpeed = 0.95f;
|
||||
buildPower = 1.2f;
|
||||
engineColor = Color.valueOf("ffd37f");
|
||||
health = 250f;
|
||||
|
||||
weapon = new Weapon("blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 14f;
|
||||
roundrobin = 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;
|
||||
roundrobin = true;
|
||||
shots = 2;
|
||||
inaccuracy = 0f;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.lightning;
|
||||
}};
|
||||
}
|
||||
|
||||
@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, 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;
|
||||
roundrobin = false;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBullet;
|
||||
}};
|
||||
}
|
||||
|
||||
@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 = 50;
|
||||
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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
shake = 3f;
|
||||
bullet = Bullets.missileSwarm;
|
||||
}};
|
||||
}
|
||||
|
||||
@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;
|
||||
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 = 0.9f;
|
||||
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;
|
||||
roundrobin = 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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
bullet = Bullets.missileJavelin;
|
||||
}};
|
||||
}
|
||||
|
||||
@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,
|
||||
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;
|
||||
roundrobin = 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;
|
||||
}};
|
||||
}};
|
||||
}
|
||||
|
||||
@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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardGlaive;
|
||||
}};
|
||||
}
|
||||
};
|
||||
|
||||
starter = dart;
|
||||
}
|
||||
}
|
||||
87
core/src/io/anuke/mindustry/content/StatusEffects.java
Normal file
87
core/src/io/anuke/mindustry/content/StatusEffects.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.type.StatusEffect;
|
||||
|
||||
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();
|
||||
|
||||
burning = new StatusEffect(){{
|
||||
damage = 0.04f;
|
||||
effect = Fx.burning;
|
||||
|
||||
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(){{
|
||||
speedMultiplier = 0.6f;
|
||||
armorMultiplier = 0.8f;
|
||||
effect = Fx.freezing;
|
||||
|
||||
opposite(() -> melting, () -> burning);
|
||||
}};
|
||||
|
||||
wet = new StatusEffect(){{
|
||||
speedMultiplier = 0.9f;
|
||||
effect = Fx.wet;
|
||||
|
||||
trans(() -> shocked, ((unit, time, newTime, result) -> unit.damage(15f)));
|
||||
opposite(() -> burning, () -> shocked);
|
||||
}};
|
||||
|
||||
melting = new StatusEffect(){{
|
||||
speedMultiplier = 0.8f;
|
||||
armorMultiplier = 0.8f;
|
||||
damage = 0.3f;
|
||||
effect = Fx.melting;
|
||||
|
||||
trans(() -> tarred, ((unit, time, newTime, result) -> result.set(this, Math.min(time + newTime / 2f, 140f))));
|
||||
opposite(() -> wet, () -> freezing);
|
||||
}};
|
||||
|
||||
tarred = new StatusEffect(){{
|
||||
speedMultiplier = 0.6f;
|
||||
effect = Fx.oily;
|
||||
|
||||
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(){{
|
||||
armorMultiplier = 0.95f;
|
||||
speedMultiplier = 1.15f;
|
||||
damageMultiplier = 1.4f;
|
||||
damage = -0.01f;
|
||||
effect = Fx.overdriven;
|
||||
}};
|
||||
|
||||
shielded = new StatusEffect(){{
|
||||
armorMultiplier = 3f;
|
||||
}};
|
||||
|
||||
boss = new StatusEffect(){{
|
||||
armorMultiplier = 3f;
|
||||
damageMultiplier = 3f;
|
||||
speedMultiplier = 1.1f;
|
||||
}};
|
||||
|
||||
shocked = new StatusEffect();
|
||||
|
||||
//no effects, just small amounts of damage.
|
||||
corroded = new StatusEffect(){{
|
||||
damage = 0.1f;
|
||||
}};
|
||||
}
|
||||
}
|
||||
328
core/src/io/anuke/mindustry/content/TechTree.java
Normal file
328
core/src/io/anuke/mindustry/content/TechTree.java
Normal file
@@ -0,0 +1,328 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.type.ItemStack;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.content.Blocks.*;
|
||||
|
||||
public class TechTree implements ContentList{
|
||||
public static TechNode root;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
root = node(coreShard, () -> {
|
||||
|
||||
node(conveyor, () -> {
|
||||
|
||||
node(junction, () -> {
|
||||
node(itemBridge);
|
||||
node(router, () -> {
|
||||
node(launchPad, () -> {
|
||||
node(launchPadLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(distributor);
|
||||
node(sorter, () -> {
|
||||
node(overflowGate);
|
||||
});
|
||||
node(container, () -> {
|
||||
node(unloader);
|
||||
node(vault, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(titaniumConveyor, () -> {
|
||||
node(phaseConveyor, () -> {
|
||||
node(massDriver, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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(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(rotaryPump, () -> {
|
||||
node(thermalPump, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
node(bridgeConduit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(combustionGenerator, () -> {
|
||||
node(powerNode, () -> {
|
||||
node(powerNodeLarge, () -> {
|
||||
node(surgeTower, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(battery, () -> {
|
||||
node(batteryLarge, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(mender, () -> {
|
||||
node(mendProjector, () -> {
|
||||
node(forceProjector, () -> {
|
||||
node(overdriveProjector, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
node(repairPoint, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(turbineGenerator, () -> {
|
||||
node(thermalGenerator, () -> {
|
||||
node(rtgGenerator, () -> {
|
||||
node(differentialGenerator, () -> {
|
||||
node(thoriumReactor, () -> {
|
||||
node(impactReactor, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(solarPanel, () -> {
|
||||
node(largeSolarPanel, () -> {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
node(draugFactory, () -> {
|
||||
node(spiritFactory, () -> {
|
||||
node(phantomFactory);
|
||||
});
|
||||
|
||||
node(daggerFactory, () -> {
|
||||
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 TechNode node(Block block, Runnable children){
|
||||
ItemStack[] requirements = new ItemStack[block.buildRequirements.length];
|
||||
for(int i = 0; i < requirements.length; i++){
|
||||
requirements[i] = new ItemStack(block.buildRequirements[i].item, block.buildRequirements[i].amount * 5);
|
||||
}
|
||||
|
||||
return new TechNode(block, requirements, children);
|
||||
}
|
||||
|
||||
private TechNode node(Block block){
|
||||
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(Block block, ItemStack[] requirements, Runnable children){
|
||||
if(context != null){
|
||||
context.children.add(this);
|
||||
}
|
||||
|
||||
this.block = block;
|
||||
this.requirements = requirements;
|
||||
|
||||
TechNode last = context;
|
||||
context = this;
|
||||
children.run();
|
||||
context = last;
|
||||
}
|
||||
}
|
||||
}
|
||||
347
core/src/io/anuke/mindustry/content/UnitTypes.java
Normal file
347
core/src/io/anuke/mindustry/content/UnitTypes.java
Normal file
@@ -0,0 +1,347 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.collection.ObjectSet;
|
||||
import io.anuke.mindustry.entities.type.base.*;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.type.UnitType;
|
||||
import io.anuke.mindustry.type.Weapon;
|
||||
|
||||
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", Draug.class, Draug::new){{
|
||||
isFlying = true;
|
||||
drag = 0.01f;
|
||||
speed = 0.3f;
|
||||
maxVelocity = 1.2f;
|
||||
range = 50f;
|
||||
health = 60;
|
||||
minePower = 0.5f;
|
||||
engineSize = 1.8f;
|
||||
engineOffset = 5.7f;
|
||||
weapon = new Weapon("you have incurred my wrath. prepare to die."){{
|
||||
bullet = Bullets.lancerLaser;
|
||||
}};
|
||||
}};
|
||||
|
||||
spirit = new UnitType("spirit", Spirit.class, Spirit::new){{
|
||||
isFlying = true;
|
||||
drag = 0.01f;
|
||||
speed = 0.4f;
|
||||
maxVelocity = 1.6f;
|
||||
range = 50f;
|
||||
health = 60;
|
||||
engineSize = 1.8f;
|
||||
engineOffset = 5.7f;
|
||||
weapon = new Weapon("heal-blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 40f;
|
||||
width = 0.5f;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBullet;
|
||||
}};
|
||||
}};
|
||||
|
||||
phantom = new UnitType("phantom", Phantom.class, Phantom::new){{
|
||||
isFlying = true;
|
||||
drag = 0.01f;
|
||||
mass = 2f;
|
||||
speed = 0.45f;
|
||||
maxVelocity = 1.9f;
|
||||
range = 70f;
|
||||
itemCapacity = 70;
|
||||
health = 220;
|
||||
buildPower = 0.9f;
|
||||
minePower = 1.1f;
|
||||
engineOffset = 6.5f;
|
||||
toMine = ObjectSet.with(Items.lead, Items.copper, Items.titanium);
|
||||
weapon = new Weapon("heal-blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 20f;
|
||||
width = 0.5f;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
recoil = 2f;
|
||||
bullet = Bullets.healBullet;
|
||||
}};
|
||||
}};
|
||||
|
||||
dagger = new UnitType("dagger", Dagger.class, Dagger::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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardCopper;
|
||||
}};
|
||||
}};
|
||||
|
||||
crawler = new UnitType("crawler", Crawler.class, Crawler::new){{
|
||||
maxVelocity = 1.2f;
|
||||
speed = 0.26f;
|
||||
drag = 0.4f;
|
||||
hitsize = 8f;
|
||||
mass = 1.75f;
|
||||
health = 100;
|
||||
weapon = new Weapon("bomber"){{
|
||||
reload = 12f;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.explode;
|
||||
}};
|
||||
}};
|
||||
|
||||
titan = new UnitType("titan", Titan.class, Titan::new){{
|
||||
maxVelocity = 0.8f;
|
||||
speed = 0.18f;
|
||||
drag = 0.4f;
|
||||
mass = 3.5f;
|
||||
hitsize = 9f;
|
||||
rotatespeed = 0.1f;
|
||||
health = 440;
|
||||
immunities.add(StatusEffects.burning);
|
||||
weapon = new Weapon("flamethrower"){{
|
||||
length = 1f;
|
||||
reload = 14f;
|
||||
roundrobin = true;
|
||||
recoil = 1f;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.basicFlame;
|
||||
}};
|
||||
}};
|
||||
|
||||
fortress = new UnitType("fortress", Fortress.class, Fortress::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;
|
||||
roundrobin = true;
|
||||
recoil = 4f;
|
||||
shake = 2f;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.artilleryUnit;
|
||||
}};
|
||||
}};
|
||||
|
||||
eruptor = new UnitType("eruptor", Eruptor.class, Eruptor::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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.eruptorShot;
|
||||
recoil = 1f;
|
||||
width = 7f;
|
||||
}};
|
||||
}};
|
||||
|
||||
chaosArray = new UnitType("chaos-array", Dagger.class, Dagger::new){{
|
||||
maxVelocity = 0.68f;
|
||||
speed = 0.12f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
rotatespeed = 0.06f;
|
||||
health = 4000;
|
||||
weapon = new Weapon("chaos"){{
|
||||
length = 8f;
|
||||
reload = 50f;
|
||||
width = 17f;
|
||||
roundrobin = true;
|
||||
recoil = 3f;
|
||||
shake = 2f;
|
||||
shots = 4;
|
||||
spacing = 4f;
|
||||
shotDelay = 5;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.flakSurge;
|
||||
}};
|
||||
}};
|
||||
|
||||
eradicator = new UnitType("eradicator", Dagger.class, Dagger::new){{
|
||||
maxVelocity = 0.68f;
|
||||
speed = 0.12f;
|
||||
drag = 0.4f;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
rotatespeed = 0.06f;
|
||||
health = 10000;
|
||||
weapon = new Weapon("eradication"){{
|
||||
length = 13f;
|
||||
reload = 30f;
|
||||
width = 22f;
|
||||
roundrobin = true;
|
||||
recoil = 3f;
|
||||
shake = 2f;
|
||||
inaccuracy = 3f;
|
||||
shots = 4;
|
||||
spacing = 0f;
|
||||
shotDelay = 3;
|
||||
ejectEffect = Fx.shellEjectMedium;
|
||||
bullet = Bullets.standardThoriumBig;
|
||||
}};
|
||||
}};
|
||||
|
||||
wraith = new UnitType("wraith", Wraith.class, Wraith::new){{
|
||||
speed = 0.3f;
|
||||
maxVelocity = 1.9f;
|
||||
drag = 0.01f;
|
||||
mass = 1.5f;
|
||||
isFlying = true;
|
||||
health = 75;
|
||||
engineOffset = 5.5f;
|
||||
range = 140f;
|
||||
weapon = new Weapon("chain-blaster"){{
|
||||
length = 1.5f;
|
||||
reload = 28f;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.shellEjectSmall;
|
||||
bullet = Bullets.standardCopper;
|
||||
}};
|
||||
}};
|
||||
|
||||
ghoul = new UnitType("ghoul", Ghoul.class, Ghoul::new){{
|
||||
health = 220;
|
||||
speed = 0.2f;
|
||||
maxVelocity = 1.4f;
|
||||
mass = 3f;
|
||||
drag = 0.01f;
|
||||
isFlying = true;
|
||||
targetAir = false;
|
||||
engineOffset = 7.8f;
|
||||
range = 140f;
|
||||
weapon = new Weapon("bomber"){{
|
||||
length = 0f;
|
||||
width = 2f;
|
||||
reload = 12f;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 1f;
|
||||
inaccuracy = 40f;
|
||||
ignoreRotation = true;
|
||||
bullet = Bullets.bombExplosive;
|
||||
}};
|
||||
}};
|
||||
|
||||
revenant = new UnitType("revenant", Revenant.class, Revenant::new){{
|
||||
health = 1000;
|
||||
mass = 5f;
|
||||
hitsize = 20f;
|
||||
speed = 0.1f;
|
||||
maxVelocity = 1f;
|
||||
drag = 0.01f;
|
||||
range = 80f;
|
||||
shootCone = 40f;
|
||||
isFlying = 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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
bullet = Bullets.missileRevenant;
|
||||
}};
|
||||
}};
|
||||
|
||||
lich = new UnitType("lich", Revenant.class, Revenant::new){{
|
||||
health = 7000;
|
||||
mass = 20f;
|
||||
hitsize = 40f;
|
||||
speed = 0.01f;
|
||||
maxVelocity = 0.6f;
|
||||
drag = 0.02f;
|
||||
range = 80f;
|
||||
shootCone = 20f;
|
||||
isFlying = true;
|
||||
rotateWeapon = true;
|
||||
engineOffset = 21;
|
||||
engineSize = 5.3f;
|
||||
rotatespeed = 0.01f;
|
||||
attackLength = 90f;
|
||||
baseRotateSpeed = 0.04f;
|
||||
weapon = new Weapon("lich-missiles"){{
|
||||
length = 4f;
|
||||
reload = 180f;
|
||||
width = 22f;
|
||||
shots = 22;
|
||||
shootCone = 100f;
|
||||
shotDelay = 2;
|
||||
inaccuracy = 10f;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
velocityRnd = 0.2f;
|
||||
spacing = 1f;
|
||||
bullet = Bullets.missileRevenant;
|
||||
}};
|
||||
}};
|
||||
|
||||
reaper = new UnitType("reaper", Revenant.class, Revenant::new){{
|
||||
health = 13000;
|
||||
mass = 30f;
|
||||
hitsize = 56f;
|
||||
speed = 0.01f;
|
||||
maxVelocity = 0.6f;
|
||||
drag = 0.02f;
|
||||
range = 80f;
|
||||
shootCone = 30f;
|
||||
isFlying = 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;
|
||||
roundrobin = true;
|
||||
ejectEffect = Fx.none;
|
||||
bullet = Bullets.standardDenseBig;
|
||||
}};
|
||||
}};
|
||||
}
|
||||
}
|
||||
177
core/src/io/anuke/mindustry/content/Zones.java
Normal file
177
core/src/io/anuke/mindustry/content/Zones.java
Normal file
@@ -0,0 +1,177 @@
|
||||
package io.anuke.mindustry.content;
|
||||
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.mindustry.game.ContentList;
|
||||
import io.anuke.mindustry.game.SpawnGroup;
|
||||
import io.anuke.mindustry.maps.generators.MapGenerator;
|
||||
import io.anuke.mindustry.maps.generators.MapGenerator.Decoration;
|
||||
import io.anuke.mindustry.maps.zonegen.DesertWastesGenerator;
|
||||
import io.anuke.mindustry.type.*;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
public class Zones implements ContentList{
|
||||
public static Zone
|
||||
groundZero, desertWastes,
|
||||
craters, frozenForest, ruinousShores, stainedMountains, tarFields,
|
||||
saltFlats, overgrowth, infestedIslands,
|
||||
desolateRift, nuclearComplex;
|
||||
|
||||
@Override
|
||||
public void load(){
|
||||
|
||||
groundZero = new Zone("groundZero", new MapGenerator("groundZero", 1)){{
|
||||
baseLaunchCost = ItemStack.with(Items.copper, -100);
|
||||
startingItems = ItemStack.list(Items.copper, 100);
|
||||
alwaysUnlocked = true;
|
||||
conditionWave = 5;
|
||||
launchPeriod = 5;
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.sand};
|
||||
}};
|
||||
|
||||
desertWastes = new Zone("desertWastes", new DesertWastesGenerator(260, 260)){{
|
||||
startingItems = ItemStack.list(Items.copper, 200);
|
||||
conditionWave = 20;
|
||||
launchPeriod = 10;
|
||||
loadout = Loadouts.advancedShard;
|
||||
zoneRequirements = ZoneRequirement.with(groundZero, 20);
|
||||
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.sand};
|
||||
rules = r -> {
|
||||
r.waves = true;
|
||||
r.waveTimer = true;
|
||||
r.launchWaveMultiplier = 3f;
|
||||
r.waveSpacing = 60 * 50f;
|
||||
r.spawns = Array.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;
|
||||
}}
|
||||
);
|
||||
};
|
||||
}};
|
||||
|
||||
saltFlats = new Zone("saltFlats", new MapGenerator("saltFlats")){{
|
||||
baseLaunchCost = ItemStack.with(Items.copper, -100);
|
||||
startingItems = ItemStack.list(Items.copper, 100);
|
||||
alwaysUnlocked = true;
|
||||
conditionWave = 5;
|
||||
launchPeriod = 5;
|
||||
loadout = Loadouts.basicFoundation;
|
||||
zoneRequirements = ZoneRequirement.with(desertWastes, 60);
|
||||
blockRequirements = new Block[]{Blocks.daggerFactory, Blocks.draugFactory};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.sand};
|
||||
}};
|
||||
|
||||
craters = new Zone("craters", new MapGenerator("craters", 1).dist(0).decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.004))){{
|
||||
startingItems = ItemStack.list(Items.copper, 200);
|
||||
conditionWave = 10;
|
||||
zoneRequirements = ZoneRequirement.with(groundZero, 10);
|
||||
blockRequirements = new Block[]{Blocks.router};
|
||||
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.sand, Items.scrap};
|
||||
}};
|
||||
|
||||
frozenForest = new Zone("frozenForest", new MapGenerator("frozenForest", 1)
|
||||
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
baseLaunchCost = ItemStack.with();
|
||||
startingItems = ItemStack.list(Items.copper, 400);
|
||||
conditionWave = 10;
|
||||
zoneRequirements = ZoneRequirement.with(craters, 10);
|
||||
resources = new Item[]{Items.copper, Items.lead, Items.coal};
|
||||
}};
|
||||
|
||||
overgrowth = new Zone("overgrowth", new MapGenerator("overgrowth")){{
|
||||
startingItems = ItemStack.list(Items.copper, 3000, Items.lead, 2000, Items.silicon, 1000, Items.metaglass, 500);
|
||||
conditionWave = 12;
|
||||
launchPeriod = 4;
|
||||
loadout = Loadouts.basicNucleus;
|
||||
zoneRequirements = ZoneRequirement.with(frozenForest, 40);
|
||||
blockRequirements = new Block[]{Blocks.router};
|
||||
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.titanium, Items.sand, Items.thorium, Items.scrap};
|
||||
}};
|
||||
|
||||
ruinousShores = new Zone("ruinousShores", new MapGenerator("ruinousShores", 1).dist(3f, true)){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
baseLaunchCost = ItemStack.with();
|
||||
startingItems = ItemStack.list(Items.copper, 400);
|
||||
conditionWave = 20;
|
||||
launchPeriod = 20;
|
||||
zoneRequirements = ZoneRequirement.with(desertWastes, 20, craters, 15);
|
||||
blockRequirements = new Block[]{Blocks.graphitePress, Blocks.combustionGenerator};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.sand};
|
||||
}};
|
||||
|
||||
stainedMountains = new Zone("stainedMountains", new MapGenerator("stainedMountains", 2)
|
||||
.dist(0f, false)
|
||||
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = ItemStack.list(Items.copper, 400, Items.lead, 100);
|
||||
conditionWave = 10;
|
||||
launchPeriod = 10;
|
||||
zoneRequirements = ZoneRequirement.with(frozenForest, 15);
|
||||
blockRequirements = new Block[]{Blocks.pneumaticDrill};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand};
|
||||
}};
|
||||
|
||||
tarFields = new Zone("tarFields", new MapGenerator("tarFields")
|
||||
.dist(0f, false)
|
||||
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
|
||||
loadout = Loadouts.basicFoundation;
|
||||
startingItems = ItemStack.list(Items.copper, 500, Items.lead, 200);
|
||||
conditionWave = 15;
|
||||
launchPeriod = 10;
|
||||
zoneRequirements = ZoneRequirement.with(ruinousShores, 20);
|
||||
blockRequirements = new Block[]{Blocks.coalCentrifuge};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand};
|
||||
}};
|
||||
|
||||
desolateRift = new Zone("desolateRift", new MapGenerator("desolateRift").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;
|
||||
zoneRequirements = ZoneRequirement.with(tarFields, 20);
|
||||
blockRequirements = new Block[]{Blocks.thermalGenerator};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand, Items.thorium};
|
||||
}};
|
||||
|
||||
nuclearComplex = new Zone("nuclearComplex", new MapGenerator("nuclearProductionComplex", 1)
|
||||
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.01))){{
|
||||
loadout = Loadouts.basicNucleus;
|
||||
baseLaunchCost = ItemStack.with();
|
||||
startingItems = ItemStack.list(Items.copper, 2500, Items.lead, 3000, Items.silicon, 800, Items.metaglass, 400);
|
||||
conditionWave = 30;
|
||||
launchPeriod = 15;
|
||||
zoneRequirements = ZoneRequirement.with(stainedMountains, 20);
|
||||
blockRequirements = new Block[]{Blocks.thermalGenerator};
|
||||
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.thorium, Items.sand};
|
||||
}};
|
||||
}
|
||||
}
|
||||
245
core/src/io/anuke/mindustry/core/ContentLoader.java
Normal file
245
core/src/io/anuke/mindustry/core/ContentLoader.java
Normal file
@@ -0,0 +1,245 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import io.anuke.arc.collection.*;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.Pixmap;
|
||||
import io.anuke.arc.util.Log;
|
||||
import io.anuke.mindustry.content.*;
|
||||
import io.anuke.mindustry.entities.bullet.BulletType;
|
||||
import io.anuke.mindustry.entities.effect.Fire;
|
||||
import io.anuke.mindustry.entities.effect.Puddle;
|
||||
import io.anuke.mindustry.entities.traits.TypeTrait;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.type.*;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.LegacyColorMapper;
|
||||
|
||||
import static io.anuke.arc.Core.files;
|
||||
|
||||
/**
|
||||
* Loads all game content.
|
||||
* Call load() before doing anything with content.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ContentLoader{
|
||||
private boolean loaded = false;
|
||||
private boolean verbose = false;
|
||||
|
||||
private ObjectMap<String, MappableContent>[] contentNameMap = new ObjectMap[ContentType.values().length];
|
||||
private Array<Content>[] contentMap = new Array[ContentType.values().length];
|
||||
private MappableContent[][] temporaryMapper;
|
||||
private ObjectSet<Consumer<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(),
|
||||
|
||||
//these are not really content classes, but this makes initialization easier
|
||||
new LegacyColorMapper(),
|
||||
};
|
||||
|
||||
public void setVerbose(){
|
||||
verbose = true;
|
||||
}
|
||||
|
||||
/** Creates all content types. */
|
||||
public void load(){
|
||||
if(loaded){
|
||||
Log.info("Content already loaded, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
registerTypes();
|
||||
|
||||
for(ContentType type : ContentType.values()){
|
||||
contentMap[type.ordinal()] = new Array<>();
|
||||
contentNameMap[type.ordinal()] = new ObjectMap<>();
|
||||
}
|
||||
|
||||
for(ContentList list : content){
|
||||
list.load();
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
|
||||
for(ContentType type : ContentType.values()){
|
||||
|
||||
for(Content c : contentMap[type.ordinal()]){
|
||||
if(c instanceof MappableContent){
|
||||
String name = ((MappableContent)c).name;
|
||||
if(contentNameMap[type.ordinal()].containsKey(name)){
|
||||
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + name + "')");
|
||||
}
|
||||
contentNameMap[type.ordinal()].put(name, (MappableContent)c);
|
||||
}
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
//set up ID mapping
|
||||
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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(verbose){
|
||||
Log.info("--- CONTENT INFO ---");
|
||||
for(int k = 0; k < contentMap.length; k++){
|
||||
Log.info("[{0}]: loaded {1}", ContentType.values()[k].name(), contentMap[k].size);
|
||||
}
|
||||
Log.info("Total content loaded: {0}", total);
|
||||
Log.info("-------------------");
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
public void initialize(Consumer<Content> callable){
|
||||
initialize(callable, false);
|
||||
}
|
||||
|
||||
/** Initializes all content with the specified function. */
|
||||
public void initialize(Consumer<Content> callable, boolean override){
|
||||
if(initialization.contains(callable) && !override) return;
|
||||
|
||||
for(ContentType type : ContentType.values()){
|
||||
for(Content content : contentMap[type.ordinal()]){
|
||||
callable.accept(content);
|
||||
}
|
||||
}
|
||||
|
||||
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 verbose(boolean verbose){
|
||||
this.verbose = verbose;
|
||||
}
|
||||
|
||||
public void dispose(){
|
||||
//clear all content, currently not needed
|
||||
}
|
||||
|
||||
public void handleContent(Content content){
|
||||
contentMap[content.getContentType().ordinal()].add(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 getByID(type, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers sync IDs for all types of sync entities.
|
||||
* Do not register units here!
|
||||
*/
|
||||
private void registerTypes(){
|
||||
TypeTrait.registerType(Player.class, Player::new);
|
||||
TypeTrait.registerType(Fire.class, Fire::new);
|
||||
TypeTrait.registerType(Puddle.class, Puddle::new);
|
||||
}
|
||||
}
|
||||
@@ -1,410 +1,329 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Input.Buttons;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.GL20;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureAtlas;
|
||||
import io.anuke.arc.input.KeyCode;
|
||||
import io.anuke.arc.scene.ui.Dialog;
|
||||
import io.anuke.arc.scene.ui.TextField;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.game.EventType.*;
|
||||
import io.anuke.mindustry.game.Tutorial;
|
||||
import io.anuke.mindustry.game.UpgradeInventory;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.input.AndroidInput;
|
||||
import io.anuke.mindustry.input.DefaultKeybinds;
|
||||
import io.anuke.mindustry.input.DesktopInput;
|
||||
import io.anuke.mindustry.input.InputHandler;
|
||||
import io.anuke.mindustry.io.Saves;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.input.*;
|
||||
import io.anuke.mindustry.maps.Map;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.resource.Item;
|
||||
import io.anuke.mindustry.resource.Weapon;
|
||||
import io.anuke.mindustry.world.Map;
|
||||
import io.anuke.ucore.UCore;
|
||||
import io.anuke.ucore.core.*;
|
||||
import io.anuke.ucore.core.Inputs.DeviceType;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
import io.anuke.ucore.scene.ui.layout.Unit;
|
||||
import io.anuke.ucore.util.Atlas;
|
||||
import io.anuke.ucore.util.InputProxy;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.mindustry.type.*;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
import static io.anuke.arc.Core.scene;
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
/**Control module.
|
||||
/**
|
||||
* Control module.
|
||||
* Handles all input, saving, keybinds and keybinds.
|
||||
* Should <i>not</i> handle any game-critical state.
|
||||
* This class is not created in the headless server.*/
|
||||
public class Control extends Module{
|
||||
private UpgradeInventory upgrades = new UpgradeInventory();
|
||||
private Tutorial tutorial = new Tutorial();
|
||||
private boolean hiscore = false;
|
||||
* Should <i>not</i> handle any logic-critical state.
|
||||
* This class is not created in the headless server.
|
||||
*/
|
||||
public class Control implements ApplicationListener{
|
||||
public final Saves saves;
|
||||
|
||||
private boolean wasPaused = false;
|
||||
private Interval timer = new Interval(2);
|
||||
private boolean hiscore = false;
|
||||
private boolean wasPaused = false;
|
||||
private InputHandler input;
|
||||
|
||||
private Saves saves;
|
||||
public Control(){
|
||||
IntBuffer buf = BufferUtils.newIntBuffer(1);
|
||||
Core.gl.glGetIntegerv(GL20.GL_MAX_TEXTURE_SIZE, buf);
|
||||
int maxSize = buf.get(0);
|
||||
|
||||
private float respawntime;
|
||||
private InputHandler input;
|
||||
saves = new Saves();
|
||||
data = new GlobalData();
|
||||
|
||||
private InputProxy proxy;
|
||||
private float controlx, controly;
|
||||
private boolean controlling;
|
||||
private Throwable error;
|
||||
Core.input.setCatch(KeyCode.BACK, true);
|
||||
|
||||
public Control(){
|
||||
saves = new Saves();
|
||||
Effects.setShakeFalloff(10000f);
|
||||
|
||||
Inputs.useControllers(!gwt);
|
||||
content.initialize(Content::init);
|
||||
Core.atlas = new TextureAtlas(maxSize < 2048 ? "sprites/sprites_fallback.atlas" : "sprites/sprites.atlas");
|
||||
Draw.scl = 1f / Core.atlas.find("scale_marker").getWidth();
|
||||
content.initialize(Content::load, true);
|
||||
|
||||
Gdx.input.setCatchBackKey(true);
|
||||
data.load();
|
||||
|
||||
if(mobile){
|
||||
input = new AndroidInput();
|
||||
}else{
|
||||
input = new DesktopInput();
|
||||
}
|
||||
Core.settings.setAppName(appName);
|
||||
Core.settings.defaults(
|
||||
"ip", "localhost",
|
||||
"color-0", Color.rgba8888(playerColors[8]),
|
||||
"color-1", Color.rgba8888(playerColors[11]),
|
||||
"color-2", Color.rgba8888(playerColors[13]),
|
||||
"color-3", Color.rgba8888(playerColors[9]),
|
||||
"name", "",
|
||||
"lastBuild", 0
|
||||
);
|
||||
|
||||
proxy = new InputProxy(Gdx.input){
|
||||
@Override
|
||||
public int getY() {
|
||||
return controlling ? (int)controly : input.getY();
|
||||
createPlayer();
|
||||
|
||||
saves.load();
|
||||
|
||||
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.instance::updateRPC);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public int getX() {
|
||||
return controlling ? (int)controlx : input.getX();
|
||||
}
|
||||
Events.on(PlayEvent.class, event -> {
|
||||
player.setTeam(defaultTeam);
|
||||
player.setDead(true);
|
||||
player.add();
|
||||
|
||||
@Override
|
||||
public int getY(int pointer) {
|
||||
return pointer == 0 ? getY() : super.getY(pointer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getX(int pointer) {
|
||||
return pointer == 0 ? getX() : super.getX(pointer);
|
||||
}
|
||||
};
|
||||
|
||||
Inputs.addProcessor(input);
|
||||
|
||||
Effects.setShakeFalloff(10000f);
|
||||
|
||||
Core.atlas = new Atlas("sprites.atlas");
|
||||
|
||||
for(Item item : Item.getAllItems()){
|
||||
item.init();
|
||||
}
|
||||
|
||||
Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
|
||||
"corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
|
||||
"respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
|
||||
"ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
|
||||
|
||||
Sounds.setFalloff(9000f);
|
||||
|
||||
Musics.load("1.mp3", "2.mp3", "3.mp3", "4.mp3", "5.mp3", "6.mp3");
|
||||
|
||||
DefaultKeybinds.load();
|
||||
|
||||
for(int i = 0; i < saveSlots; i ++){
|
||||
Settings.defaults("save-" + i + "-autosave", !gwt);
|
||||
Settings.defaults("save-" + i + "-name", "untitled");
|
||||
Settings.defaults("save-" + i + "-data", "empty");
|
||||
}
|
||||
|
||||
Settings.defaultList(
|
||||
"ip", "localhost",
|
||||
"port", port+"",
|
||||
"name", mobile || gwt ? "player" : UCore.getProperty("user.name"),
|
||||
"servers", "",
|
||||
"color", Color.rgba8888(playerColors[8]),
|
||||
"lastVersion", "3.2",
|
||||
"lastBuild", 0
|
||||
);
|
||||
|
||||
KeyBinds.load();
|
||||
|
||||
for(Map map : world.maps().list()){
|
||||
Settings.defaults("hiscore" + map.name, 0);
|
||||
}
|
||||
|
||||
player = new Player();
|
||||
player.name = Settings.getString("name");
|
||||
player.isAndroid = mobile;
|
||||
player.color.set(Settings.getInt("color"));
|
||||
player.isLocal = true;
|
||||
|
||||
saves.load();
|
||||
|
||||
Events.on(StateChangeEvent.class, (from, to) -> {
|
||||
if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
|
||||
Timers.runTask(5f, Platform.instance::updateRPC);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(PlayEvent.class, () -> {
|
||||
renderer.clearTiles();
|
||||
|
||||
player.set(world.getSpawnX(), world.getSpawnY());
|
||||
|
||||
Core.camera.position.set(player.x, player.y, 0);
|
||||
|
||||
ui.hudfrag.updateItems();
|
||||
|
||||
state.set(State.playing);
|
||||
});
|
||||
|
||||
Events.on(ResetEvent.class, () -> {
|
||||
upgrades.reset();
|
||||
player.weaponLeft = player.weaponRight = Weapon.blaster;
|
||||
|
||||
player.add();
|
||||
player.heal();
|
||||
|
||||
respawntime = -1;
|
||||
hiscore = false;
|
||||
|
||||
ui.hudfrag.updateItems();
|
||||
ui.hudfrag.updateWeapons();
|
||||
ui.hudfrag.fadeRespawn(false);
|
||||
});
|
||||
|
||||
Events.on(WaveEvent.class, () -> {
|
||||
Sounds.play("spawn");
|
||||
|
||||
int last = Settings.getInt("hiscore" + world.getMap().name, 0);
|
||||
|
||||
if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
|
||||
Settings.putInt("hiscore" + world.getMap().name, state.wave);
|
||||
Settings.save();
|
||||
hiscore = true;
|
||||
}
|
||||
|
||||
Platform.instance.updateRPC();
|
||||
});
|
||||
|
||||
Events.on(GameOverEvent.class, () -> {
|
||||
Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
|
||||
Sounds.play("corexplode");
|
||||
for(int i = 0; i < 16; i ++){
|
||||
Timers.run(i*2, ()-> Effects.effect(Fx.explosion, world.getCore().worldx()+Mathf.range(40), world.getCore().worldy()+Mathf.range(40)));
|
||||
}
|
||||
Effects.effect(Fx.coreexplosion, world.getCore().worldx(), world.getCore().worldy());
|
||||
|
||||
ui.restart.show();
|
||||
|
||||
Timers.runTask(30f, () -> state.set(State.menu));
|
||||
});
|
||||
}
|
||||
|
||||
//FIXME figure out what's causing this problem in the first place
|
||||
public void triggerInputUpdate(){
|
||||
Gdx.input = proxy;
|
||||
}
|
||||
|
||||
public void setError(Throwable error){
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public UpgradeInventory upgrades() {
|
||||
return upgrades;
|
||||
}
|
||||
|
||||
public Saves getSaves(){
|
||||
return saves;
|
||||
}
|
||||
|
||||
public boolean showCursor(){
|
||||
return controlling;
|
||||
}
|
||||
|
||||
public InputHandler input(){
|
||||
return input;
|
||||
}
|
||||
|
||||
public void playMap(Map map){
|
||||
ui.loadfrag.show();
|
||||
saves.resetSave();
|
||||
|
||||
Timers.runTask(10, () -> {
|
||||
logic.reset();
|
||||
world.loadMap(map);
|
||||
logic.play();
|
||||
});
|
||||
|
||||
Timers.runTask(18, () -> ui.loadfrag.hide());
|
||||
}
|
||||
|
||||
public boolean isHighScore(){
|
||||
return hiscore;
|
||||
}
|
||||
|
||||
public float getRespawnTime(){
|
||||
return respawntime;
|
||||
}
|
||||
|
||||
public void setRespawnTime(float respawntime){
|
||||
this.respawntime = respawntime;
|
||||
}
|
||||
|
||||
public Tutorial tutorial(){
|
||||
return tutorial;
|
||||
}
|
||||
|
||||
private void checkOldUser(){
|
||||
boolean hasPlayed = false;
|
||||
|
||||
for(Map map : world.maps().getAllMaps()){
|
||||
if(Settings.getInt("hiscore" + map.name) != 0){
|
||||
hasPlayed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(hasPlayed && Settings.getString("lastVersion").equals("3.2")){
|
||||
Timers.runTask(1f, () -> ui.showInfo("$text.changes"));
|
||||
Settings.putString("lastVersion", "3.3");
|
||||
Settings.save();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
Platform.instance.onGameExit();
|
||||
Net.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(){
|
||||
Timers.run(1f, Musics::shuffleAll);
|
||||
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.getClosestCore());
|
||||
}else{
|
||||
//locally, set to player position since respawning occurs immediately
|
||||
Core.camera.position.set(player);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Entities.initPhysics();
|
||||
Entities.collisions().setCollider(tilesize, world::solid);
|
||||
Events.on(ResetEvent.class, event -> {
|
||||
player.reset();
|
||||
|
||||
Platform.instance.updateRPC();
|
||||
hiscore = false;
|
||||
|
||||
checkOldUser();
|
||||
}
|
||||
saves.resetSave();
|
||||
});
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
|
||||
if(error != null){
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
|
||||
Gdx.input = proxy;
|
||||
|
||||
if(Inputs.keyTap("console")){
|
||||
console = !console;
|
||||
}
|
||||
|
||||
if(KeyBinds.getSection("default").device.type == DeviceType.controller){
|
||||
if(Inputs.keyTap("select")){
|
||||
Inputs.getProcessor().touchDown(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
|
||||
Events.on(WaveEvent.class, event -> {
|
||||
if(world.getMap().getHightScore() < state.wave){
|
||||
hiscore = true;
|
||||
world.getMap().setHighScore(state.wave);
|
||||
}
|
||||
});
|
||||
|
||||
if(Inputs.keyRelease("select")){
|
||||
Inputs.getProcessor().touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
|
||||
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){
|
||||
//remove zone save on game over
|
||||
if(saves.getZoneSlot() != null){
|
||||
saves.getZoneSlot().delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
float xa = Inputs.getAxis("cursor_x");
|
||||
float ya = Inputs.getAxis("cursor_y");
|
||||
|
||||
if(Math.abs(xa) > controllerMin || Math.abs(ya) > controllerMin) {
|
||||
float scl = Settings.getInt("sensitivity")/100f * Unit.dp.scl(1f);
|
||||
controlx += xa*baseControllerSpeed*scl;
|
||||
controly -= ya*baseControllerSpeed*scl;
|
||||
controlling = true;
|
||||
|
||||
Gdx.input.setCursorCatched(true);
|
||||
|
||||
Inputs.getProcessor().touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
|
||||
//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.showError(Core.bundle.format("server.error", Strings.parseException(e, true)));
|
||||
Core.app.post(() -> state.set(State.menu));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
controlx = Mathf.clamp(controlx, 0, Gdx.graphics.getWidth());
|
||||
controly = Mathf.clamp(controly, 0, Gdx.graphics.getHeight());
|
||||
Events.on(UnlockEvent.class, e -> ui.hudfrag.showUnlock(e.content));
|
||||
|
||||
if(Gdx.input.getDeltaX() > 1 || Gdx.input.getDeltaY() > 1) {
|
||||
controlling = false;
|
||||
Gdx.input.setCursorCatched(false);
|
||||
}
|
||||
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 -> {
|
||||
ui.hudfrag.showToast(Core.bundle.format("zone.requirement.complete", state.wave, e.zone.localizedName));
|
||||
});
|
||||
|
||||
Events.on(ZoneConfigureCompleteEvent.class, e -> {
|
||||
ui.hudfrag.showToast(Core.bundle.format("zone.config.complete", e.zone.configureWave));
|
||||
});
|
||||
}
|
||||
|
||||
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{
|
||||
controlling = false;
|
||||
Gdx.input.setCursorCatched(false);
|
||||
input = new DesktopInput();
|
||||
}
|
||||
|
||||
if(!controlling){
|
||||
controlx = Gdx.input.getX();
|
||||
controly = Gdx.input.getY();
|
||||
if(!state.is(State.menu)){
|
||||
player.add();
|
||||
}
|
||||
|
||||
Core.input.addProcessor(input);
|
||||
}
|
||||
|
||||
public InputHandler input(){
|
||||
return input;
|
||||
}
|
||||
|
||||
public void playMap(Map map, Rules rules){
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
world.loadMap(map);
|
||||
state.rules = rules;
|
||||
logic.play();
|
||||
});
|
||||
}
|
||||
|
||||
public void playZone(Zone zone){
|
||||
ui.loadAnd(() -> {
|
||||
logic.reset();
|
||||
world.loadGenerator(zone.generator);
|
||||
zone.rules.accept(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);
|
||||
control.saves.zoneSave();
|
||||
logic.play();
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isHighScore(){
|
||||
return hiscore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
content.dispose();
|
||||
Net.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.instance.updateRPC();
|
||||
|
||||
if(!Core.settings.getBool("4.0-warning-2", false)){
|
||||
|
||||
Time.run(5f, () -> {
|
||||
FloatingDialog dialog = new FloatingDialog("VERY IMPORTANT");
|
||||
dialog.buttons.addButton("$ok", () -> {
|
||||
dialog.hide();
|
||||
Core.settings.put("4.0-warning-2", true);
|
||||
Core.settings.save();
|
||||
}).size(100f, 60f);
|
||||
dialog.cont.add("Reminder: The alpha version you are about to play is very unstable, and is [accent]not representative of the final v4 release.[]\n\n " +
|
||||
"\nThere is currently[scarlet] no sound implemented[]; this is intentional.\n" +
|
||||
"All current art and UI is unfinished, and will be changed before release. " +
|
||||
"\n\n[accent]Saves may be corrupted without warning between updates.").wrap().width(400f);
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
saves.update();
|
||||
|
||||
if(state.inventory.isUpdated() && (Timers.get("updateItems", 8) || state.is(State.paused))){
|
||||
ui.hudfrag.updateItems();
|
||||
state.inventory.setUpdated(false);
|
||||
}
|
||||
input.updateController();
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
input.update();
|
||||
//autosave global data if it's modified
|
||||
data.checkSave();
|
||||
|
||||
if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
|
||||
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.items.has(item)){
|
||||
data.unlockContent(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//auto-update rpc every 5 seconds
|
||||
if(timer.get(0, 60 * 5)){
|
||||
Platform.instance.updateRPC();
|
||||
}
|
||||
|
||||
if(Core.input.keyTap(Binding.pause) && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
|
||||
state.set(state.is(State.playing) ? State.paused : State.playing);
|
||||
}
|
||||
}
|
||||
|
||||
if(Inputs.keyTap("menu")){
|
||||
if(state.is(State.paused)){
|
||||
ui.paused.hide();
|
||||
state.set(State.playing);
|
||||
}else if (!ui.restart.isShown()){
|
||||
if(ui.chatfrag.chatOpen()) {
|
||||
ui.chatfrag.hide();
|
||||
}else{
|
||||
ui.paused.show();
|
||||
state.set(State.paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(Core.input.keyTap(Binding.menu) && !ui.restart.isShown()){
|
||||
if(ui.chatfrag.chatOpen()){
|
||||
ui.chatfrag.hide();
|
||||
}else if(!ui.paused.isShown() && !scene.hasDialog()){
|
||||
ui.paused.show();
|
||||
state.set(State.paused);
|
||||
}
|
||||
}
|
||||
|
||||
if(!state.is(State.paused) || Net.active()){
|
||||
Entities.update(effectGroup);
|
||||
if(!mobile && Core.input.keyTap(Binding.screenshot) && !(scene.getKeyboardFocus() instanceof TextField) && !ui.chatfrag.chatOpen()){
|
||||
renderer.takeMapScreenshot();
|
||||
}
|
||||
|
||||
if(respawntime > 0){
|
||||
}else{
|
||||
if(!state.isPaused()){
|
||||
Time.update();
|
||||
}
|
||||
|
||||
respawntime -= Timers.delta();
|
||||
|
||||
if(respawntime <= 0){
|
||||
player.set(world.getSpawnX(), world.getSpawnY());
|
||||
player.heal();
|
||||
player.add();
|
||||
Effects.sound("respawn");
|
||||
ui.hudfrag.fadeRespawn(false);
|
||||
}
|
||||
}
|
||||
|
||||
if(tutorial.active()){
|
||||
tutorial.update();
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if(!state.is(State.paused) || Net.active()){
|
||||
Timers.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!scene.hasDialog() && !(scene.root.getChildren().peek() instanceof Dialog) && Core.input.keyTap(KeyCode.BACK)){
|
||||
Platform.instance.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,63 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import io.anuke.mindustry.game.Difficulty;
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.mindustry.entities.type.BaseUnit;
|
||||
import io.anuke.mindustry.entities.type.base.BaseDrone;
|
||||
import io.anuke.mindustry.game.EventType.StateChangeEvent;
|
||||
import io.anuke.mindustry.game.GameMode;
|
||||
import io.anuke.mindustry.game.Inventory;
|
||||
import io.anuke.ucore.core.Events;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
|
||||
import static io.anuke.mindustry.Vars.unitGroups;
|
||||
import static io.anuke.mindustry.Vars.waveTeam;
|
||||
|
||||
public class GameState{
|
||||
private State state = State.menu;
|
||||
/** 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 final Inventory inventory = new Inventory();
|
||||
public int enemies(){
|
||||
return Net.client() ? enemies : unitGroups[waveTeam.ordinal()].count(b -> !(b instanceof BaseDrone));
|
||||
}
|
||||
|
||||
public int wave = 1;
|
||||
public int lastUpdated = -1;
|
||||
public float wavetime;
|
||||
public float extrawavetime;
|
||||
public int enemies = 0;
|
||||
public boolean gameOver = false;
|
||||
public GameMode mode = GameMode.waves;
|
||||
public Difficulty difficulty = Difficulty.normal;
|
||||
public boolean friendlyFire;
|
||||
|
||||
public void set(State astate){
|
||||
Events.fire(StateChangeEvent.class, state, astate);
|
||||
state = astate;
|
||||
}
|
||||
|
||||
public boolean is(State astate){
|
||||
return state == astate;
|
||||
}
|
||||
public BaseUnit boss(){
|
||||
return unitGroups[waveTeam.ordinal()].find(BaseUnit::isBoss);
|
||||
}
|
||||
|
||||
public State getState(){
|
||||
return state;
|
||||
}
|
||||
|
||||
public enum State{
|
||||
paused, playing, menu
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,167 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.ApplicationListener;
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.arc.collection.ObjectSet.ObjectSetIterator;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.content.*;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.game.EnemySpawn;
|
||||
import io.anuke.mindustry.game.EventType.GameOverEvent;
|
||||
import io.anuke.mindustry.game.EventType.PlayEvent;
|
||||
import io.anuke.mindustry.game.EventType.ResetEvent;
|
||||
import io.anuke.mindustry.game.EventType.WaveEvent;
|
||||
import io.anuke.mindustry.game.SpawnPoint;
|
||||
import io.anuke.mindustry.game.WaveCreator;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.entities.type.TileEntity;
|
||||
import io.anuke.mindustry.game.EventType.*;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.game.Teams.TeamData;
|
||||
import io.anuke.mindustry.gen.BrokenBlock;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.NetEvents;
|
||||
import io.anuke.mindustry.type.Item;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.ProductionBlocks;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Events;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.mindustry.world.blocks.BuildBlock;
|
||||
import io.anuke.mindustry.world.blocks.BuildBlock.BuildEntity;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
/**Logic module.
|
||||
/**
|
||||
* 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 extends Module {
|
||||
private final Array<EnemySpawn> spawns = WaveCreator.getSpawns();
|
||||
public class Logic implements ApplicationListener{
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
Entities.initPhysics();
|
||||
Entities.collisions().setCollider(tilesize, world::solid);
|
||||
public Logic(){
|
||||
Events.on(WaveEvent.class, event -> {
|
||||
if(world.isZone()){
|
||||
world.getZone().updateWave(state.wave);
|
||||
}
|
||||
for (Player p : playerGroup.all()) {
|
||||
p.respawns = state.rules.respawns;
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
if(block instanceof BuildBlock){
|
||||
BuildEntity entity = tile.entity();
|
||||
|
||||
//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());
|
||||
data.brokenBlocks.addFirst(BrokenBlock.get(tile.x, tile.y, tile.rotation(), block.id));
|
||||
});
|
||||
}
|
||||
|
||||
/** 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.wavetime = wavespace * state.difficulty.timeScaling * 2;
|
||||
state.set(State.playing);
|
||||
state.wavetime = state.rules.waveSpacing * 2; //grace period of 2x wave time before game starts
|
||||
Events.fire(new PlayEvent());
|
||||
|
||||
if(state.mode.infiniteResources){
|
||||
state.inventory.fill();
|
||||
//add starting items
|
||||
if(!world.isZone()){
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.isActive(team)){
|
||||
for(Tile core : state.teams.get(team).cores){
|
||||
core.entity.items.add(Items.copper, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Events.fire(PlayEvent.class);
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
state.wave = 1;
|
||||
state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
|
||||
state.wavetime = wavespace * state.difficulty.timeScaling;
|
||||
state.enemies = 0;
|
||||
state.lastUpdated = -1;
|
||||
state.gameOver = false;
|
||||
state.inventory.clearItems();
|
||||
state.wavetime = state.rules.waveSpacing;
|
||||
state.gameOver = state.launched = false;
|
||||
state.teams = new Teams();
|
||||
state.rules = new Rules();
|
||||
state.stats = new Stats();
|
||||
|
||||
Timers.clear();
|
||||
Time.clear();
|
||||
Entities.clear();
|
||||
TileEntity.sleepingEntities = 0;
|
||||
|
||||
Events.fire(ResetEvent.class);
|
||||
Events.fire(new ResetEvent());
|
||||
}
|
||||
|
||||
public void runWave(){
|
||||
world.spawner.spawnEnemies();
|
||||
state.wave++;
|
||||
state.wavetime = world.isZone() && world.getZone().isBossWave(state.wave) ? state.rules.waveSpacing * state.rules.bossWaveMultiplier :
|
||||
world.isZone() && world.getZone().isLaunchWave(state.wave) ? state.rules.waveSpacing * state.rules.launchWaveMultiplier : state.rules.waveSpacing;
|
||||
|
||||
if(state.lastUpdated < state.wave + 1){
|
||||
world.pathfinder().resetPaths();
|
||||
state.lastUpdated = state.wave + 1;
|
||||
}
|
||||
Events.fire(new WaveEvent());
|
||||
}
|
||||
|
||||
for(EnemySpawn spawn : spawns){
|
||||
Array<SpawnPoint> spawns = world.getSpawns();
|
||||
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(int lane = 0; lane < spawns.size; lane ++){
|
||||
int fl = lane;
|
||||
Tile tile = spawns.get(lane).start;
|
||||
int spawnamount = spawn.evaluate(state.wave, lane);
|
||||
|
||||
for(int i = 0; i < spawnamount; i ++){
|
||||
float range = 12f;
|
||||
|
||||
Timers.runTask(i*5f, () -> {
|
||||
|
||||
Enemy enemy = new Enemy(spawn.type);
|
||||
enemy.set(tile.worldx() + Mathf.range(range), tile.worldy() + Mathf.range(range));
|
||||
enemy.lane = fl;
|
||||
enemy.tier = spawn.tier(state.wave, fl);
|
||||
enemy.add();
|
||||
|
||||
Effects.effect(Fx.spawn, enemy);
|
||||
|
||||
state.enemies ++;
|
||||
});
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.get(team).cores.size > 0){
|
||||
if(alive != null){
|
||||
return;
|
||||
}
|
||||
alive = team;
|
||||
}
|
||||
}
|
||||
|
||||
if(alive != null && !state.gameOver){
|
||||
state.gameOver = true;
|
||||
Events.fire(new GameOverEvent(alive));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both)
|
||||
public static void launchZone(){
|
||||
if(!headless){
|
||||
ui.hudfrag.showLaunch();
|
||||
}
|
||||
|
||||
state.wave ++;
|
||||
state.wavetime = wavespace * state.difficulty.timeScaling;
|
||||
state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
|
||||
for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
|
||||
Effects.effect(Fx.launch, tile);
|
||||
}
|
||||
|
||||
Events.fire(WaveEvent.class);
|
||||
Time.runTask(30f, () -> {
|
||||
for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
|
||||
for(Item item : content.items()){
|
||||
data.addItem(item, tile.entity.items.get(item));
|
||||
}
|
||||
world.removeBlock(tile);
|
||||
}
|
||||
state.launched = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both)
|
||||
public static void onGameOver(Team winner){
|
||||
state.stats.wavesLasted = state.wave;
|
||||
ui.restart.show(winner);
|
||||
netClient.setQuiet();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,50 +169,64 @@ public class Logic extends Module {
|
||||
|
||||
if(!state.is(State.menu)){
|
||||
|
||||
if(control != null) control.triggerInputUpdate();
|
||||
if(!state.isPaused()){
|
||||
Time.update();
|
||||
|
||||
if(!state.is(State.paused) || Net.active()){
|
||||
Timers.update();
|
||||
}
|
||||
|
||||
if(!Net.client())
|
||||
world.pathfinder().update();
|
||||
|
||||
if(world.getCore() != null && world.getCore().block() != ProductionBlocks.core && !state.gameOver){
|
||||
state.gameOver = true;
|
||||
if(Net.server()) NetEvents.handleGameOver();
|
||||
Events.fire(GameOverEvent.class);
|
||||
}
|
||||
|
||||
if(!state.is(State.paused) || Net.active()){
|
||||
|
||||
if(!state.mode.disableWaveTimer){
|
||||
|
||||
if(state.enemies <= 0){
|
||||
if(!world.getMap().name.equals("tutorial")) state.wavetime -= Timers.delta();
|
||||
|
||||
if(state.lastUpdated < state.wave + 1 && state.wavetime < aheadPathfinding){ //start updating beforehand
|
||||
world.pathfinder().resetPaths();
|
||||
state.lastUpdated = state.wave + 1;
|
||||
}
|
||||
}else if(!world.getMap().name.equals("tutorial")){
|
||||
state.extrawavetime -= Timers.delta();
|
||||
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.extrawavetime <= 0)){
|
||||
if(!Net.client() && state.wavetime <= 0 && state.rules.waves){
|
||||
runWave();
|
||||
}
|
||||
|
||||
Entities.update(Entities.defaultGroup());
|
||||
Entities.update(bulletGroup);
|
||||
Entities.update(enemyGroup);
|
||||
Entities.update(tileGroup);
|
||||
Entities.update(shieldGroup);
|
||||
if(!headless){
|
||||
Entities.update(effectGroup);
|
||||
Entities.update(groundEffectGroup);
|
||||
}
|
||||
|
||||
if(!state.isEditor()){
|
||||
for(EntityGroup group : unitGroups){
|
||||
Entities.update(group);
|
||||
}
|
||||
|
||||
Entities.update(puddleGroup);
|
||||
Entities.update(shieldGroup);
|
||||
Entities.update(bulletGroup);
|
||||
Entities.update(tileGroup);
|
||||
Entities.update(fireGroup);
|
||||
}else{
|
||||
for(EntityGroup<?> group : unitGroups){
|
||||
group.updateEvents();
|
||||
collisions.updatePhysics(group);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Entities.update(playerGroup);
|
||||
|
||||
Entities.collideGroups(bulletGroup, enemyGroup);
|
||||
Entities.collideGroups(bulletGroup, playerGroup);
|
||||
//effect group only contains item transfers in the headless version, update it!
|
||||
if(headless){
|
||||
Entities.update(effectGroup);
|
||||
}
|
||||
|
||||
if(!state.isEditor()){
|
||||
|
||||
for(EntityGroup group : unitGroups){
|
||||
if(group.isEmpty()) continue;
|
||||
collisions.collideGroups(bulletGroup, group);
|
||||
}
|
||||
|
||||
collisions.collideGroups(bulletGroup, playerGroup);
|
||||
}
|
||||
|
||||
world.pathfinder.update();
|
||||
}
|
||||
|
||||
if(!Net.client() && !world.isInvalidMap() && !state.isEditor()){
|
||||
checkGameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,89 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.utils.IntMap;
|
||||
import com.badlogic.gdx.utils.IntSet;
|
||||
import com.badlogic.gdx.utils.TimeUtils;
|
||||
import io.anuke.annotations.Annotations.*;
|
||||
import io.anuke.arc.ApplicationListener;
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.IntSet;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.math.RandomXS128;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.io.ReusableByteInStream;
|
||||
import io.anuke.arc.util.serialization.Base64Coder;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.Bullet;
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.SyncEntity;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.entities.Entities;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
|
||||
import io.anuke.mindustry.entities.traits.SyncTrait;
|
||||
import io.anuke.mindustry.entities.traits.TypeTrait;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.game.Version;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.gen.RemoteReadClient;
|
||||
import io.anuke.mindustry.net.Administration.TraceInfo;
|
||||
import io.anuke.mindustry.net.*;
|
||||
import io.anuke.mindustry.net.Net.SendMode;
|
||||
import io.anuke.mindustry.net.NetworkIO;
|
||||
import io.anuke.mindustry.net.Packets.*;
|
||||
import io.anuke.mindustry.resource.Item;
|
||||
import io.anuke.mindustry.resource.Upgrade;
|
||||
import io.anuke.mindustry.resource.UpgradeRecipes;
|
||||
import io.anuke.mindustry.resource.Weapon;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Map;
|
||||
import io.anuke.mindustry.world.Placement;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.ProductionBlocks;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.BaseBulletType;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.EntityGroup;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
import io.anuke.ucore.util.Log;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
import io.anuke.mindustry.world.modules.ItemModule;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class NetClient extends Module {
|
||||
private final static float dataTimeout = 60*18; //18 seconds timeout
|
||||
public class NetClient implements ApplicationListener{
|
||||
private final static float dataTimeout = 60 * 18;
|
||||
private final static float playerSyncTime = 2;
|
||||
private final static int maxRequests = 50;
|
||||
public final static float viewScale = 2f;
|
||||
|
||||
private Timer timer = new Timer(5);
|
||||
private Interval timer = new Interval(5);
|
||||
/** Whether the client is currently connecting. */
|
||||
private boolean connecting = false;
|
||||
private boolean kicked = false;
|
||||
private IntSet recieved = new IntSet();
|
||||
private IntMap<Entity> recent = new IntMap<>();
|
||||
private int requests = 0;
|
||||
private float timeoutTime = 0f; //data timeout counter
|
||||
/** If true, no message will be shown on disconnect. */
|
||||
private boolean quiet = 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;
|
||||
|
||||
Net.setClientLoaded(false);
|
||||
recieved.clear();
|
||||
recent.clear();
|
||||
timeoutTime = 0f;
|
||||
connecting = true;
|
||||
kicked = false;
|
||||
reset();
|
||||
|
||||
ui.chatfrag.clearMessages();
|
||||
ui.loadfrag.hide();
|
||||
ui.loadfrag.show("$text.connecting.data");
|
||||
ui.loadfrag.show("$connecting.data");
|
||||
|
||||
Entities.clear();
|
||||
ui.loadfrag.setButton(() -> {
|
||||
ui.loadfrag.hide();
|
||||
connecting = false;
|
||||
quiet = true;
|
||||
Net.disconnect();
|
||||
});
|
||||
|
||||
ConnectPacket c = new ConnectPacket();
|
||||
c.name = player.name;
|
||||
c.android = mobile;
|
||||
c.mobile = mobile;
|
||||
c.versionType = Version.type;
|
||||
c.color = Color.rgba8888(player.color);
|
||||
c.usid = getUsid(packet.addressTCP);
|
||||
c.uuid = Platform.instance.getUUID();
|
||||
|
||||
if(c.uuid == null){
|
||||
ui.showError("$text.invalidid");
|
||||
ui.showError("$invalidid");
|
||||
ui.loadfrag.hide();
|
||||
disconnectQuietly();
|
||||
return;
|
||||
@@ -86,238 +93,207 @@ public class NetClient extends Module {
|
||||
});
|
||||
|
||||
Net.handleClient(Disconnect.class, packet -> {
|
||||
if (kicked) return;
|
||||
|
||||
Timers.runTask(3f, ui.loadfrag::hide);
|
||||
|
||||
state.set(State.menu);
|
||||
|
||||
ui.showError("$text.disconnect");
|
||||
connecting = false;
|
||||
|
||||
logic.reset();
|
||||
Platform.instance.updateRPC();
|
||||
|
||||
if(quiet) return;
|
||||
|
||||
Time.runTask(3f, ui.loadfrag::hide);
|
||||
|
||||
ui.showError("$disconnect");
|
||||
});
|
||||
|
||||
Net.handleClient(WorldData.class, data -> {
|
||||
Net.handleClient(WorldStream.class, data -> {
|
||||
Log.info("Recieved world data: {0} bytes.", data.stream.available());
|
||||
NetworkIO.loadWorld(data.stream);
|
||||
player.set(world.getSpawnX(), world.getSpawnY());
|
||||
NetworkIO.loadWorld(new InflaterInputStream(data.stream));
|
||||
|
||||
finishConnecting();
|
||||
});
|
||||
|
||||
Net.handleClient(CustomMapPacket.class, packet -> {
|
||||
Log.info("Recieved custom map: {0} bytes.", packet.stream.available());
|
||||
|
||||
//custom map is always sent before world data
|
||||
Map map = NetworkIO.loadMap(packet.stream);
|
||||
|
||||
world.maps().setNetworkMap(map);
|
||||
|
||||
MapAckPacket ack = new MapAckPacket();
|
||||
Net.send(ack, SendMode.tcp);
|
||||
Net.handleClient(InvokePacket.class, packet -> {
|
||||
packet.writeBuffer.position(0);
|
||||
RemoteReadClient.readPacket(packet.writeBuffer, packet.type);
|
||||
});
|
||||
}
|
||||
|
||||
Net.handleClient(SyncPacket.class, packet -> {
|
||||
if (connecting) return;
|
||||
int players = 0;
|
||||
int enemies = 0;
|
||||
//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){
|
||||
Vars.ui.chatfrag.addMessage(message, sender);
|
||||
}
|
||||
|
||||
ByteBuffer data = ByteBuffer.wrap(packet.data);
|
||||
long time = data.getLong();
|
||||
if(playersender != null){
|
||||
playersender.lastText = message;
|
||||
playersender.textFadeTime = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
byte groupid = data.get();
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
||||
EntityGroup<?> group = Entities.getGroup(groupid);
|
||||
//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.");
|
||||
}
|
||||
|
||||
while (data.position() < data.capacity()) {
|
||||
int id = data.getInt();
|
||||
//server console logging
|
||||
Log.info("&y{0}: &lb{1}", player.name, message);
|
||||
|
||||
SyncEntity entity = (SyncEntity) group.getByID(id);
|
||||
//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);
|
||||
}
|
||||
|
||||
if(entity instanceof Player) players ++;
|
||||
if(entity instanceof Enemy) enemies ++;
|
||||
private 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;
|
||||
}
|
||||
|
||||
if (entity == null || id == player.id) {
|
||||
if (id != player.id && requests < maxRequests) {
|
||||
EntityRequestPacket req = new EntityRequestPacket();
|
||||
req.id = id;
|
||||
req.group = groupid;
|
||||
Net.send(req, SendMode.udp);
|
||||
requests ++;
|
||||
@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);
|
||||
|
||||
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.both)
|
||||
public static void onInfoMessage(String message){
|
||||
ui.showText("", message);
|
||||
}
|
||||
|
||||
@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.getGroup(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)TypeTrait.getTypeByID(typeID).get(); //create entity from supplier
|
||||
entity.resetID(id);
|
||||
if(!netClient.isEntityUsed(entity.getID())){
|
||||
add = true;
|
||||
}
|
||||
data.position(data.position() + SyncEntity.getWriteSize((Class<? extends SyncEntity>) group.getType()));
|
||||
} else {
|
||||
entity.read(data, time);
|
||||
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.one, priority = PacketPriority.low, unreliable = true)
|
||||
public static void onStateSnapshot(float waveTime, int wave, int enemies, short coreDataLen, byte[] coreData){
|
||||
try{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if(debugNet){
|
||||
clientDebug.setSyncDebug(players, enemies);
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleClient(StateSyncPacket.class, packet -> {
|
||||
|
||||
System.arraycopy(packet.items, 0, state.inventory.getItems(), 0, packet.items.length);
|
||||
|
||||
state.enemies = packet.enemies;
|
||||
state.wavetime = packet.countdown;
|
||||
state.wave = packet.wave;
|
||||
|
||||
ui.hudfrag.updateItems();
|
||||
});
|
||||
|
||||
Net.handleClient(BlockLogRequestPacket.class, packet -> {
|
||||
currentEditLogs = packet.editlogs;
|
||||
});
|
||||
|
||||
Net.handleClient(PlacePacket.class, (packet) -> {
|
||||
Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, Timers.get("placeblocksound", 10));
|
||||
|
||||
if(packet.playerid == player.id){
|
||||
Tile tile = world.tile(packet.x, packet.y);
|
||||
if(tile != null) Block.getByID(packet.block).placed(tile);
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleClient(BreakPacket.class, (packet) ->
|
||||
Placement.breakBlock(packet.x, packet.y, true, Timers.get("breakblocksound", 10)));
|
||||
|
||||
Net.handleClient(EntitySpawnPacket.class, packet -> {
|
||||
EntityGroup group = packet.group;
|
||||
|
||||
//duplicates.
|
||||
if (group.getByID(packet.entity.id) != null ||
|
||||
recieved.contains(packet.entity.id)) return;
|
||||
|
||||
recieved.add(packet.entity.id);
|
||||
recent.put(packet.entity.id, packet.entity);
|
||||
|
||||
packet.entity.add();
|
||||
|
||||
Log.info("Recieved entity {0}", packet.entity.id);
|
||||
});
|
||||
|
||||
Net.handleClient(EnemyDeathPacket.class, packet -> {
|
||||
Enemy enemy = enemyGroup.getByID(packet.id);
|
||||
if (enemy != null){
|
||||
enemy.type.onDeath(enemy, true);
|
||||
}else if(recent.get(packet.id) != null){
|
||||
recent.get(packet.id).remove();
|
||||
}else{
|
||||
Log.err("Got remove for null entity! {0}", packet.id);
|
||||
}
|
||||
recieved.add(packet.id);
|
||||
});
|
||||
|
||||
Net.handleClient(BulletPacket.class, packet -> {
|
||||
//TODO shoot effects for enemies, clientside as well as serverside
|
||||
BulletType type = (BulletType) BaseBulletType.getByID(packet.type);
|
||||
Entity owner = enemyGroup.getByID(packet.owner);
|
||||
new Bullet(type, owner, packet.x, packet.y, packet.angle).add();
|
||||
});
|
||||
|
||||
Net.handleClient(BlockDestroyPacket.class, packet -> {
|
||||
Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
|
||||
if (tile != null && tile.entity != null) {
|
||||
tile.entity.onDeath(true);
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleClient(BlockUpdatePacket.class, packet -> {
|
||||
Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
|
||||
if (tile != null && tile.entity != null) {
|
||||
tile.entity.health = packet.health;
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleClient(DisconnectPacket.class, packet -> {
|
||||
Player player = playerGroup.getByID(packet.playerid);
|
||||
|
||||
if (player != null) {
|
||||
player.remove();
|
||||
}
|
||||
|
||||
Platform.instance.updateRPC();
|
||||
});
|
||||
|
||||
Net.handleClient(KickPacket.class, packet -> {
|
||||
kicked = true;
|
||||
Net.disconnect();
|
||||
state.set(State.menu);
|
||||
if(!packet.reason.quiet) ui.showError("$text.server.kicked." + packet.reason.name());
|
||||
ui.loadfrag.hide();
|
||||
});
|
||||
|
||||
Net.handleClient(GameOverPacket.class, packet -> {
|
||||
if(world.getCore().block() != ProductionBlocks.core &&
|
||||
world.getCore().entity != null){
|
||||
world.getCore().entity.onDeath(true);
|
||||
}
|
||||
kicked = true;
|
||||
ui.restart.show();
|
||||
});
|
||||
|
||||
Net.handleClient(FriendlyFireChangePacket.class, packet -> state.friendlyFire = packet.enabled);
|
||||
|
||||
Net.handleClient(ItemTransferPacket.class, packet -> {
|
||||
Runnable r = () -> {
|
||||
Tile tile = world.tile(packet.position);
|
||||
if (tile == null || tile.entity == null) return;
|
||||
Tile next = tile.getNearby(packet.rotation);
|
||||
tile.entity.items[packet.itemid] --;
|
||||
next.block().handleItem(Item.getByID(packet.itemid), next, tile);
|
||||
};
|
||||
|
||||
threads.run(r);
|
||||
});
|
||||
|
||||
Net.handleClient(ItemSetPacket.class, packet -> {
|
||||
Runnable r = () -> {
|
||||
Tile tile = world.tile(packet.position);
|
||||
if (tile == null || tile.entity == null) return;
|
||||
tile.entity.items[packet.itemid] = packet.amount;
|
||||
};
|
||||
|
||||
threads.run(r);
|
||||
});
|
||||
|
||||
Net.handleClient(ItemOffloadPacket.class, packet -> {
|
||||
Runnable r = () -> {
|
||||
Tile tile = world.tile(packet.position);
|
||||
if (tile == null || tile.entity == null) return;
|
||||
Tile next = tile.getNearby(tile.getRotation());
|
||||
next.block().handleItem(Item.getByID(packet.itemid), next, tile);
|
||||
};
|
||||
|
||||
threads.run(r);
|
||||
});
|
||||
|
||||
Net.handleClient(NetErrorPacket.class, packet -> {
|
||||
ui.showError(packet.message);
|
||||
disconnectQuietly();
|
||||
});
|
||||
|
||||
Net.handleClient(PlayerAdminPacket.class, packet -> {
|
||||
Player player = playerGroup.getByID(packet.id);
|
||||
player.isAdmin = packet.admin;
|
||||
ui.listfrag.rebuild();
|
||||
});
|
||||
|
||||
Net.handleClient(TracePacket.class, packet -> {
|
||||
Player player = playerGroup.getByID(packet.info.playerid);
|
||||
ui.traces.show(player, packet.info);
|
||||
});
|
||||
|
||||
Net.handleClient(UpgradePacket.class, packet -> {
|
||||
Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
|
||||
|
||||
state.inventory.removeItems(UpgradeRecipes.get(weapon));
|
||||
control.upgrades().addWeapon(weapon);
|
||||
ui.hudfrag.updateWeapons();
|
||||
Effects.sound("purchase");
|
||||
});
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -329,12 +305,12 @@ public class NetClient extends Module {
|
||||
}else if(!connecting){
|
||||
Net.disconnect();
|
||||
}else{ //...must be connecting
|
||||
timeoutTime += Timers.delta();
|
||||
timeoutTime += Time.delta();
|
||||
if(timeoutTime > dataTimeout){
|
||||
Log.err("Failed to load data!");
|
||||
ui.loadfrag.hide();
|
||||
kicked = true;
|
||||
ui.showError("$text.disconnect.data");
|
||||
quiet = true;
|
||||
ui.showError("$disconnect.data");
|
||||
Net.disconnect();
|
||||
timeoutTime = 0f;
|
||||
}
|
||||
@@ -351,8 +327,20 @@ public class NetClient extends Module {
|
||||
ui.loadfrag.hide();
|
||||
ui.join.hide();
|
||||
Net.setClientLoaded(true);
|
||||
Timers.runTask(1f, () -> Net.send(new ConnectConfirmPacket(), SendMode.tcp));
|
||||
Timers.runTask(40f, Platform.instance::updateRPC);
|
||||
Core.app.post(Call::connectConfirm);
|
||||
Time.runTask(40f, Platform.instance::updateRPC);
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
Net.setClientLoaded(false);
|
||||
removed.clear();
|
||||
timeoutTime = 0f;
|
||||
connecting = true;
|
||||
quiet = false;
|
||||
lastSent = 0;
|
||||
|
||||
Entities.clear();
|
||||
ui.chatfrag.clearMessages();
|
||||
}
|
||||
|
||||
public void beginConnecting(){
|
||||
@@ -360,31 +348,60 @@ public class NetClient extends Module {
|
||||
}
|
||||
|
||||
public void disconnectQuietly(){
|
||||
kicked = true;
|
||||
quiet = true;
|
||||
Net.disconnect();
|
||||
}
|
||||
|
||||
public void clearRecieved(){
|
||||
recieved.clear();
|
||||
/** 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(){
|
||||
requests = 0;
|
||||
|
||||
if(timer.get(0, playerSyncTime)){
|
||||
BuildRequest[] requests;
|
||||
//limit to 10 to prevent buffer overflows
|
||||
int usedRequests = Math.min(player.buildQueue().size, 10);
|
||||
|
||||
byte[] bytes = new byte[player.getWriteSize() + 8];
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
buffer.putLong(TimeUtils.millis());
|
||||
player.write(buffer);
|
||||
requests = new BuildRequest[usedRequests];
|
||||
for(int i = 0; i < usedRequests; i++){
|
||||
requests[i] = player.buildQueue().get(i);
|
||||
}
|
||||
|
||||
PositionPacket packet = new PositionPacket();
|
||||
packet.data = bytes;
|
||||
Net.send(packet, SendMode.udp);
|
||||
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.chatOpen(),
|
||||
requests,
|
||||
Core.camera.position.x, Core.camera.position.y,
|
||||
Core.camera.width * viewScale, Core.camera.height * viewScale);
|
||||
}
|
||||
|
||||
if(timer.get(1, 60)){
|
||||
Net.updatePing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.Net.SendMode;
|
||||
import io.anuke.mindustry.net.Packets.*;
|
||||
import io.anuke.mindustry.resource.Upgrade;
|
||||
import io.anuke.mindustry.resource.Weapon;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class NetCommon extends Module {
|
||||
|
||||
public NetCommon(){
|
||||
|
||||
Net.handle(ShootPacket.class, (packet) -> {
|
||||
Player player = playerGroup.getByID(packet.playerid);
|
||||
|
||||
Weapon weapon = (Weapon) Upgrade.getByID(packet.weaponid);
|
||||
weapon.shoot(player, packet.x, packet.y, packet.rotation);
|
||||
});
|
||||
|
||||
Net.handle(ChatPacket.class, (packet) -> {
|
||||
ui.chatfrag.addMessage(packet.text, colorizeName(packet.id, packet.name));
|
||||
});
|
||||
|
||||
Net.handle(WeaponSwitchPacket.class, (packet) -> {
|
||||
Player player = playerGroup.getByID(packet.playerid);
|
||||
|
||||
if (player == null) return;
|
||||
|
||||
player.weaponLeft = (Weapon) Upgrade.getByID(packet.left);
|
||||
player.weaponRight = (Weapon) Upgrade.getByID(packet.right);
|
||||
});
|
||||
|
||||
Net.handle(BlockTapPacket.class, (packet) -> {
|
||||
Tile tile = world.tile(packet.position);
|
||||
tile.block().tapped(tile);
|
||||
});
|
||||
|
||||
Net.handle(BlockConfigPacket.class, (packet) -> {
|
||||
Tile tile = world.tile(packet.position);
|
||||
if (tile != null) tile.block().configure(tile, packet.data);
|
||||
});
|
||||
|
||||
Net.handle(PlayerDeathPacket.class, (packet) -> {
|
||||
Player player = playerGroup.getByID(packet.id);
|
||||
if(player == null) return;
|
||||
|
||||
player.doRespawn();
|
||||
});
|
||||
}
|
||||
|
||||
public void sendMessage(String message){
|
||||
ChatPacket packet = new ChatPacket();
|
||||
packet.name = null;
|
||||
packet.text = message;
|
||||
Net.send(packet, SendMode.tcp);
|
||||
if(!headless) ui.chatfrag.addMessage(message, null);
|
||||
}
|
||||
|
||||
public String colorizeName(int id, String name){
|
||||
Player player = playerGroup.getByID(id);
|
||||
if(name == null || player == null) return null;
|
||||
return "[#" + player.color.toString().toUpperCase() + "]" + name;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,71 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.utils.*;
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.ApplicationListener;
|
||||
import io.anuke.arc.Events;
|
||||
import io.anuke.arc.collection.IntMap;
|
||||
import io.anuke.arc.collection.ObjectSet;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.Colors;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.io.*;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.SyncEntity;
|
||||
import io.anuke.mindustry.game.EventType.GameOverEvent;
|
||||
import io.anuke.mindustry.io.Version;
|
||||
import io.anuke.mindustry.entities.Entities;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
|
||||
import io.anuke.mindustry.entities.traits.Entity;
|
||||
import io.anuke.mindustry.entities.traits.SyncTrait;
|
||||
import io.anuke.mindustry.entities.type.Player;
|
||||
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.game.Version;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.gen.RemoteReadServer;
|
||||
import io.anuke.mindustry.net.*;
|
||||
import io.anuke.mindustry.net.Administration.PlayerInfo;
|
||||
import io.anuke.mindustry.net.Net.SendMode;
|
||||
import io.anuke.mindustry.net.Administration.TraceInfo;
|
||||
import io.anuke.mindustry.net.Packets.*;
|
||||
import io.anuke.mindustry.resource.*;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Placement;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.core.Events;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.EntityGroup;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
import io.anuke.ucore.util.Log;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class NetServer extends Module{
|
||||
private final static float serverSyncTime = 4, itemSyncTime = 10, kickDuration = 30 * 1000;
|
||||
|
||||
private final static int timerEntitySync = 0;
|
||||
private final static int timerStateSync = 1;
|
||||
public class NetServer implements ApplicationListener{
|
||||
public final static int maxSnapshotSize = 430;
|
||||
private final static float serverSyncTime = 15, kickDuration = 30 * 1000;
|
||||
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();
|
||||
|
||||
/**Maps connection IDs to players.*/
|
||||
/** Maps connection IDs to players. */
|
||||
private IntMap<Player> connections = new IntMap<>();
|
||||
private ObjectMap<String, ByteArray> weapons = new ObjectMap<>();
|
||||
private boolean closing = false;
|
||||
private Timer timer = new Timer(5);
|
||||
|
||||
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(){
|
||||
|
||||
Events.on(GameOverEvent.class, () -> {
|
||||
weapons.clear();
|
||||
admins.getEditLogs().clear();
|
||||
});
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
if(!headless){
|
||||
connections.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleServer(Connect.class, (id, connect) -> {
|
||||
if(admins.isIPBanned(connect.addressTCP)){
|
||||
@@ -54,28 +73,72 @@ public class NetServer extends Module{
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleServer(Disconnect.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
if(player != null){
|
||||
onDisconnect(player);
|
||||
}
|
||||
connections.remove(id);
|
||||
});
|
||||
|
||||
Net.handleServer(ConnectPacket.class, (id, packet) -> {
|
||||
String uuid = new String(Base64Coder.encode(packet.uuid));
|
||||
String uuid = packet.uuid;
|
||||
|
||||
if(Net.getConnection(id) == null ||
|
||||
admins.isIPBanned(Net.getConnection(id).address)) return;
|
||||
NetConnection connection = Net.getConnection(id);
|
||||
|
||||
if(connection == null ||
|
||||
admins.isIPBanned(connection.address)) return;
|
||||
|
||||
if(connection.hasBegunConnecting){
|
||||
kick(id, KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.hasBegunConnecting = true;
|
||||
|
||||
TraceInfo trace = admins.getTrace(Net.getConnection(id).address);
|
||||
PlayerInfo info = admins.getInfo(uuid);
|
||||
trace.uuid = uuid;
|
||||
trace.android = packet.android;
|
||||
|
||||
connection.mobile = packet.mobile;
|
||||
|
||||
if(admins.isIDBanned(uuid)){
|
||||
kick(id, KickReason.banned);
|
||||
return;
|
||||
}
|
||||
|
||||
if(TimeUtils.millis() - info.lastKicked < kickDuration){
|
||||
if(Time.millis() - info.lastKicked < kickDuration){
|
||||
kick(id, KickReason.recentKick);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, trace.ip);
|
||||
if(packet.versionType == null || ((packet.version == -1 || !packet.versionType.equals(Version.type)) && Version.build != -1 && !admins.allowsCustomClients())){
|
||||
kick(id, KickReason.customClient);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean preventDuplicates = headless && netServer.admins.getStrict();
|
||||
|
||||
if(preventDuplicates){
|
||||
for(Player player : playerGroup.all()){
|
||||
if(player.name.trim().equalsIgnoreCase(packet.name.trim())){
|
||||
kick(id, KickReason.nameInUse);
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.uuid.equals(packet.uuid) || player.usid.equals(packet.usid)){
|
||||
kick(id, KickReason.idInUse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packet.name = fixName(packet.name);
|
||||
|
||||
if(packet.name.trim().length() <= 0){
|
||||
kick(id, KickReason.nameEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, connection.address);
|
||||
|
||||
String ip = Net.getConnection(id).address;
|
||||
|
||||
@@ -87,294 +150,250 @@ public class NetServer extends Module{
|
||||
}
|
||||
|
||||
if(packet.version == -1){
|
||||
trace.modclient = true;
|
||||
connection.modclient = true;
|
||||
}
|
||||
|
||||
Player player = new Player();
|
||||
player.isAdmin = admins.isAdmin(uuid, ip);
|
||||
player.clientid = id;
|
||||
player.isAdmin = admins.isAdmin(uuid, packet.usid);
|
||||
player.con = Net.getConnection(id);
|
||||
player.usid = packet.usid;
|
||||
player.name = packet.name;
|
||||
player.isAndroid = packet.android;
|
||||
player.set(world.getSpawnX(), world.getSpawnY());
|
||||
player.setNet(player.x, player.y);
|
||||
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();
|
||||
kick(id, KickReason.nameEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
//playing in pvp mode automatically assigns players to teams
|
||||
if(state.rules.pvp){
|
||||
player.setTeam(assignTeam(playerGroup.all()));
|
||||
Log.info("Auto-assigned player {0} to team {1}.", player.name, player.getTeam());
|
||||
}
|
||||
|
||||
connections.put(id, player);
|
||||
|
||||
trace.playerid = player.id;
|
||||
|
||||
if(world.getMap().custom){
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
NetworkIO.writeMap(world.getMap(), stream);
|
||||
CustomMapPacket data = new CustomMapPacket();
|
||||
data.stream = new ByteArrayInputStream(stream.toByteArray());
|
||||
Net.sendStream(id, data);
|
||||
|
||||
Log.info("Sending custom map: Packed {0} uncompressed bytes of MAP data.", stream.size());
|
||||
}else{
|
||||
//hack-- simulate the map ack packet recieved to send the world data to the client.
|
||||
Net.handleServerReceived(id, new MapAckPacket());
|
||||
}
|
||||
sendWorldData(player, id);
|
||||
|
||||
Platform.instance.updateRPC();
|
||||
});
|
||||
|
||||
Net.handleServer(MapAckPacket.class, (id, packet) -> {
|
||||
Net.handleServer(InvokePacket.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
NetworkIO.writeWorld(player, weapons.get(admins.getTrace(Net.getConnection(id).address).uuid, new ByteArray()), stream);
|
||||
WorldData data = new WorldData();
|
||||
data.stream = new ByteArrayInputStream(stream.toByteArray());
|
||||
Net.sendStream(id, data);
|
||||
|
||||
Log.info("Packed {0} uncompressed bytes of WORLD data.", stream.size());
|
||||
});
|
||||
|
||||
Net.handleServer(ConnectConfirmPacket.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
|
||||
if (player == null) return;
|
||||
|
||||
player.add();
|
||||
Log.info("&y{0} has connected.", player.name);
|
||||
netCommon.sendMessage("[accent]" + player.name + " has connected.");
|
||||
});
|
||||
|
||||
Net.handleServer(Disconnect.class, (id, packet) -> {
|
||||
Player player = connections.get(packet.id);
|
||||
|
||||
if (player == null) {
|
||||
Log.err("Unknown client has disconnected (ID={0})", id);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info("&y{0} has disconnected.", player.name);
|
||||
netCommon.sendMessage("[accent]" + player.name + " has disconnected.");
|
||||
player.remove();
|
||||
|
||||
DisconnectPacket dc = new DisconnectPacket();
|
||||
dc.playerid = player.id;
|
||||
|
||||
Net.send(dc, SendMode.tcp);
|
||||
|
||||
Platform.instance.updateRPC();
|
||||
admins.save();
|
||||
});
|
||||
|
||||
Net.handleServer(PositionPacket.class, (id, packet) -> {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(packet.data);
|
||||
long time = buffer.getLong();
|
||||
|
||||
Player player = connections.get(id);
|
||||
player.read(buffer, time);
|
||||
});
|
||||
|
||||
Net.handleServer(ShootPacket.class, (id, packet) -> {
|
||||
TraceInfo info = admins.getTrace(Net.getConnection(id).address);
|
||||
Weapon weapon = (Weapon)Upgrade.getByID(packet.weaponid);
|
||||
|
||||
float wtrc = 80;
|
||||
|
||||
if(!Timers.get("fastshoot-" + id + "-" + weapon.id, wtrc)){
|
||||
info.fastShots.getAndIncrement(weapon.id, 0, 1);
|
||||
|
||||
if(info.fastShots.get(weapon.id, 0) > (int)(wtrc / (weapon.getReload() / 2f)) + 30){
|
||||
kick(id, KickReason.fastShoot);
|
||||
}
|
||||
}else{
|
||||
info.fastShots.put(weapon.id, 0);
|
||||
}
|
||||
|
||||
packet.playerid = connections.get(id).id;
|
||||
Net.sendExcept(id, packet, SendMode.udp);
|
||||
});
|
||||
|
||||
Net.handleServer(PlacePacket.class, (id, packet) -> {
|
||||
packet.playerid = connections.get(id).id;
|
||||
|
||||
Block block = Block.getByID(packet.block);
|
||||
|
||||
if(!Placement.validPlace(packet.x, packet.y, block)) return;
|
||||
|
||||
Recipe recipe = Recipes.getByResult(block);
|
||||
|
||||
if(recipe == null) return;
|
||||
|
||||
Tile tile = world.tile(packet.x, packet.y);
|
||||
if(tile.synthetic() && admins.isValidateReplace() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
|
||||
if(Timers.get("break-message-" + id, 120)){
|
||||
sendMessageTo(id, "[scarlet]Anti-grief: you are replacing blocks too quickly. wait until replacing again.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.inventory.removeItems(recipe.requirements);
|
||||
|
||||
Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false);
|
||||
|
||||
admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE);
|
||||
admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block;
|
||||
admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++;
|
||||
admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlockPlaced ++;
|
||||
|
||||
Net.send(packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(BreakPacket.class, (id, packet) -> {
|
||||
packet.playerid = connections.get(id).id;
|
||||
|
||||
if(!Placement.validBreak(packet.x, packet.y)) return;
|
||||
|
||||
Tile tile = world.tile(packet.x, packet.y);
|
||||
|
||||
if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
|
||||
if(Timers.get("break-message-" + id, 120)){
|
||||
sendMessageTo(id, "[scarlet]Anti-grief: you are breaking blocks too quickly. wait until breaking again.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = Placement.breakBlock(packet.x, packet.y, true, false);
|
||||
|
||||
if(block != null) {
|
||||
admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK);
|
||||
admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block;
|
||||
admins.getTrace(Net.getConnection(id).address).totalBlocksBroken++;
|
||||
admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlocksBroken ++;
|
||||
if (block.update || block.destructible)
|
||||
admins.getTrace(Net.getConnection(id).address).structureBlocksBroken++;
|
||||
}
|
||||
|
||||
Net.send(packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(ChatPacket.class, (id, packet) -> {
|
||||
if(!Timers.get("chatFlood" + id, 20)){
|
||||
ChatPacket warn = new ChatPacket();
|
||||
warn.text = "[scarlet]You are sending messages too quickly.";
|
||||
Net.sendTo(id, warn, SendMode.tcp);
|
||||
return;
|
||||
}
|
||||
Player player = connections.get(id);
|
||||
packet.name = player.name;
|
||||
packet.id = player.id;
|
||||
Net.send(packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(UpgradePacket.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
|
||||
Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
|
||||
String uuid = admins.getTrace(Net.getConnection(id).address).uuid;
|
||||
|
||||
if(!state.inventory.hasItems(UpgradeRecipes.get(weapon))){
|
||||
return;
|
||||
}
|
||||
|
||||
if (!weapons.containsKey(uuid)) weapons.put(uuid, new ByteArray());
|
||||
|
||||
if (!weapons.get(uuid).contains(weapon.id)){
|
||||
weapons.get(uuid).add(weapon.id);
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
|
||||
state.inventory.removeItems(UpgradeRecipes.get(weapon));
|
||||
Net.sendTo(id, packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(WeaponSwitchPacket.class, (id, packet) -> {
|
||||
TraceInfo info = admins.getTrace(Net.getConnection(id).address);
|
||||
|
||||
packet.playerid = connections.get(id).id;
|
||||
Net.sendExcept(id, packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(BlockTapPacket.class, (id, packet) -> {
|
||||
Net.sendExcept(id, packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(BlockConfigPacket.class, (id, packet) -> {
|
||||
Net.sendExcept(id, packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(EntityRequestPacket.class, (cid, packet) -> {
|
||||
|
||||
int id = packet.id;
|
||||
int dest = cid;
|
||||
EntityGroup group = Entities.getGroup(packet.group);
|
||||
if(group.getByID(id) != null){
|
||||
EntitySpawnPacket p = new EntitySpawnPacket();
|
||||
p.entity = (SyncEntity)group.getByID(id);
|
||||
p.group = group;
|
||||
Net.sendTo(dest, p, SendMode.tcp);
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleServer(PlayerDeathPacket.class, (id, packet) -> {
|
||||
packet.id = connections.get(id).id;
|
||||
Net.sendExcept(id, packet, SendMode.tcp);
|
||||
});
|
||||
|
||||
Net.handleServer(AdministerRequestPacket.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
|
||||
if(!player.isAdmin){
|
||||
Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
|
||||
player.name, Net.getConnection(player.clientid).address);
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = playerGroup.getByID(packet.id);
|
||||
|
||||
if(other == null || other.isAdmin){
|
||||
Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
|
||||
return;
|
||||
}
|
||||
|
||||
String ip = Net.getConnection(other.clientid).address;
|
||||
|
||||
if(packet.action == AdminAction.ban){
|
||||
admins.banPlayerIP(ip);
|
||||
kick(other.clientid, KickReason.banned);
|
||||
Log.info("&lc{0} has banned {1}.", player.name, other.name);
|
||||
}else if(packet.action == AdminAction.kick){
|
||||
kick(other.clientid, KickReason.kick);
|
||||
Log.info("&lc{0} has kicked {1}.", player.name, other.name);
|
||||
}else if(packet.action == AdminAction.trace){
|
||||
TracePacket trace = new TracePacket();
|
||||
trace.info = admins.getTrace(ip);
|
||||
Net.sendTo(id, trace, SendMode.tcp);
|
||||
Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
|
||||
}
|
||||
});
|
||||
|
||||
Net.handleServer(BlockLogRequestPacket.class, (id, packet) -> {
|
||||
packet.editlogs = admins.getEditLogs().get(packet.x + packet.y * world.width(), new Array<>());
|
||||
Net.sendTo(id, packet, SendMode.udp);
|
||||
});
|
||||
|
||||
Net.handleServer(RollbackRequestPacket.class, (id, packet) -> {
|
||||
Player player = connections.get(id);
|
||||
|
||||
if(!player.isAdmin){
|
||||
Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform a rollback without proper security access.",
|
||||
player.name, Net.getConnection(player.clientid).address);
|
||||
return;
|
||||
}
|
||||
|
||||
admins.rollbackWorld(packet.rollbackTimes);
|
||||
Log.info("&lc{0} has rolled back the world {1} times.", player.name, packet.rollbackTimes);
|
||||
if(player == null) return;
|
||||
RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
|
||||
});
|
||||
}
|
||||
|
||||
public Team assignTeam(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){
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
});
|
||||
}
|
||||
|
||||
public void sendWorldData(Player player, int clientID){
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream def = new FastDeflaterOutputStream(stream);
|
||||
NetworkIO.writeWorld(player, def);
|
||||
WorldStream data = new WorldStream();
|
||||
data.stream = new ByteArrayInputStream(stream.toByteArray());
|
||||
Net.sendStream(clientID, data);
|
||||
|
||||
Log.debug("Packed {0} compressed bytes of world data.", stream.size());
|
||||
}
|
||||
|
||||
public static void onDisconnect(Player player){
|
||||
//singleplayer multiplayer wierdness
|
||||
if(player.con == null){
|
||||
player.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if(player.con.hasConnected){
|
||||
Call.sendMessage("[accent]" + player.name + "[accent] has disconnected.");
|
||||
Call.onPlayerDisconnect(player.id);
|
||||
}
|
||||
player.remove();
|
||||
netServer.connections.remove(player.con.id);
|
||||
Log.info("&lm[{1}] &lc{0} has disconnected.", player.name, player.uuid);
|
||||
}
|
||||
|
||||
private static float compound(float speed, float drag){
|
||||
float total = 0f;
|
||||
for(int i = 0; i < 50; i++){
|
||||
total *= (1f - drag);
|
||||
total += speed;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@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,
|
||||
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.boostSpeed : player.mech.speed;
|
||||
float maxMove = elapsed / 1000f * 60f * Math.min(compound(maxSpeed, player.mech.drag) * 1.25f, player.mech.maxSpeed * 1.1f);
|
||||
|
||||
player.pointerX = pointerX;
|
||||
player.pointerY = pointerY;
|
||||
player.setMineTile(mining);
|
||||
player.isTyping = chatting;
|
||||
player.isBoosting = boosting;
|
||||
player.isShooting = shooting;
|
||||
player.buildQueue().clear();
|
||||
for(BuildRequest req : requests){
|
||||
Tile tile = world.tile(req.x, req.y);
|
||||
if(tile == 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.id, 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);
|
||||
netServer.kick(other.con.id, KickReason.banned);
|
||||
Log.info("&lc{0} has banned {1}.", player.name, other.name);
|
||||
}else if(action == AdminAction.kick){
|
||||
netServer.kick(other.con.id, 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.id, 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);
|
||||
}
|
||||
|
||||
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;
|
||||
reset();
|
||||
ui.loadfrag.show("$text.server.closing");
|
||||
Timers.runTask(5f, () -> {
|
||||
ui.loadfrag.show("$server.closing");
|
||||
Time.runTask(5f, () -> {
|
||||
Net.closeServer();
|
||||
ui.loadfrag.hide();
|
||||
closing = false;
|
||||
@@ -386,9 +405,10 @@ public class NetServer extends Module{
|
||||
}
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
weapons.clear();
|
||||
admins.clearTraces();
|
||||
public void kickAll(KickReason reason){
|
||||
for(NetConnection con : Net.getConnections()){
|
||||
kick(con.id, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void kick(int connection, KickReason reason){
|
||||
@@ -397,107 +417,159 @@ public class NetServer extends Module{
|
||||
Log.err("Cannot kick unknown player!");
|
||||
return;
|
||||
}else{
|
||||
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason);
|
||||
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason.name());
|
||||
}
|
||||
|
||||
if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTrace(con.address).uuid != null){
|
||||
PlayerInfo info = admins.getInfo(admins.getTrace(con.address).uuid);
|
||||
info.timesKicked ++;
|
||||
info.lastKicked = TimeUtils.millis();
|
||||
Player player = connections.get(con.id);
|
||||
|
||||
if(player != null && (reason == KickReason.kick || reason == KickReason.banned) && player.uuid != null){
|
||||
PlayerInfo info = admins.getInfo(player.uuid);
|
||||
info.timesKicked++;
|
||||
info.lastKicked = Time.millis();
|
||||
}
|
||||
|
||||
KickPacket p = new KickPacket();
|
||||
p.reason = reason;
|
||||
Call.onKick(connection, reason);
|
||||
|
||||
con.send(p, SendMode.tcp);
|
||||
Timers.runTask(2f, con::close);
|
||||
Time.runTask(2f, con::close);
|
||||
|
||||
admins.save();
|
||||
}
|
||||
|
||||
void sendMessageTo(int id, String message){
|
||||
ChatPacket packet = new ChatPacket();
|
||||
packet.text = message;
|
||||
Net.sendTo(id, packet, SendMode.tcp);
|
||||
public void writeSnapshot(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.id, 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.getAllGroups()){
|
||||
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()); //write type ID
|
||||
sync.write(dataStream); //write entity
|
||||
|
||||
sent++;
|
||||
|
||||
if(syncStream.size() > maxSnapshotSize){
|
||||
dataStream.close();
|
||||
byte[] syncBytes = syncStream.toByteArray();
|
||||
Call.onEntitySnapshot(player.con.id, (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.id, (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(){
|
||||
|
||||
if(timer.get(timerEntitySync, serverSyncTime)){
|
||||
//scan through all groups with syncable entities
|
||||
for(EntityGroup<?> group : Entities.getAllGroups()) {
|
||||
if(group.size() == 0 || !(group.all().iterator().next() instanceof SyncEntity)) continue;
|
||||
try{
|
||||
|
||||
//get write size for one entity (adding 4, as you need to write the ID as well)
|
||||
int writesize = SyncEntity.getWriteSize((Class<? extends SyncEntity>)group.getType()) + 4;
|
||||
//amount of entities
|
||||
int amount = group.size();
|
||||
//maximum amount of entities per packet
|
||||
int maxsize = 64;
|
||||
//iterate through each player
|
||||
for(int i = 0; i < playerGroup.size(); i++){
|
||||
Player player = playerGroup.all().get(i);
|
||||
if(player.isLocal) continue;
|
||||
|
||||
//current buffer you're writing to
|
||||
ByteBuffer current = null;
|
||||
//number of entities written to this packet/buffer
|
||||
int written = 0;
|
||||
NetConnection connection = player.con;
|
||||
|
||||
//for all the entities...
|
||||
for (int i = 0; i < amount; i++) {
|
||||
//if the buffer is null, create a new one
|
||||
if(current == null){
|
||||
//calculate amount of entities to go into this packet
|
||||
int csize = Math.min(amount-i, maxsize);
|
||||
//create a byte array to write to
|
||||
byte[] bytes = new byte[csize*writesize + 1 + 8];
|
||||
//wrap it for easy writing
|
||||
current = ByteBuffer.wrap(bytes);
|
||||
current.putLong(TimeUtils.millis());
|
||||
//write the group ID so the client knows which group this is
|
||||
current.put((byte)group.getID());
|
||||
}
|
||||
|
||||
SyncEntity entity = (SyncEntity) group.all().get(i);
|
||||
|
||||
//write ID to the buffer
|
||||
current.putInt(entity.id);
|
||||
|
||||
int previous = current.position();
|
||||
//write extra data to the buffer
|
||||
entity.write(current);
|
||||
|
||||
written ++;
|
||||
|
||||
//if the packet is too big now...
|
||||
if(written >= maxsize){
|
||||
//send the packet.
|
||||
SyncPacket packet = new SyncPacket();
|
||||
packet.data = current.array();
|
||||
Net.send(packet, SendMode.udp);
|
||||
|
||||
//reset data, send the next packet
|
||||
current = null;
|
||||
written = 0;
|
||||
}
|
||||
if(connection == null || !connection.isConnected() || !connections.containsKey(connection.id)){
|
||||
//player disconnected, call d/c event
|
||||
onDisconnect(player);
|
||||
return;
|
||||
}
|
||||
|
||||
//make sure to send incomplete packets too
|
||||
if(current != null){
|
||||
SyncPacket packet = new SyncPacket();
|
||||
packet.data = current.array();
|
||||
Net.send(packet, SendMode.udp);
|
||||
}
|
||||
if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
|
||||
|
||||
writeSnapshot(player);
|
||||
}
|
||||
}
|
||||
|
||||
if(timer.get(timerStateSync, itemSyncTime)){
|
||||
StateSyncPacket packet = new StateSyncPacket();
|
||||
packet.items = state.inventory.getItems();
|
||||
packet.countdown = state.wavetime;
|
||||
packet.enemies = state.enemies;
|
||||
packet.wave = state.wave;
|
||||
packet.time = Timers.time();
|
||||
packet.timestamp = TimeUtils.millis();
|
||||
|
||||
Net.send(packet, SendMode.udp);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,91 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.files.FileHandle;
|
||||
import com.badlogic.gdx.utils.Base64Coder;
|
||||
import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
|
||||
import io.anuke.ucore.core.Settings;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.EntityGroup;
|
||||
import io.anuke.ucore.function.Consumer;
|
||||
import io.anuke.ucore.scene.ui.TextField;
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.Input.TextInput;
|
||||
import io.anuke.arc.files.FileHandle;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.math.RandomXS128;
|
||||
import io.anuke.arc.scene.ui.TextField;
|
||||
import io.anuke.arc.util.serialization.Base64Coder;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import static io.anuke.mindustry.Vars.mobile;
|
||||
|
||||
public abstract class Platform {
|
||||
/**Each separate game platform should set this instance to their own implementation.*/
|
||||
public static Platform instance = new Platform() {};
|
||||
public abstract class Platform{
|
||||
/** Each separate game platform should set this instance to their own implementation. */
|
||||
public static Platform instance = new Platform(){
|
||||
};
|
||||
|
||||
/**Format the date using the default date formatter.*/
|
||||
public String format(Date date){return "invalid";}
|
||||
/**Format a number by adding in commas or periods where needed.*/
|
||||
public String format(int number){return "invalid";}
|
||||
/**Show a native error dialog.*/
|
||||
public void showError(String text){}
|
||||
/**Add a text input dialog that should show up after the field is tapped.*/
|
||||
public void addDialog(TextField field){
|
||||
addDialog(field, 16);
|
||||
}
|
||||
/**See addDialog().*/
|
||||
public void addDialog(TextField field, int maxLength){}
|
||||
/**Update discord RPC.*/
|
||||
public void updateRPC(){}
|
||||
/**Called when the game is exited.*/
|
||||
public void onGameExit(){}
|
||||
/**Open donation dialog. Currently android only.*/
|
||||
public void openDonations(){}
|
||||
/**Whether discord RPC is supported.*/
|
||||
public boolean hasDiscord(){return true;}
|
||||
/**Request Android permissions for writing files.*/
|
||||
public void requestWritePerms(){}
|
||||
/**Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().*/
|
||||
public String getLocaleName(Locale locale){
|
||||
return locale.toString();
|
||||
}
|
||||
/**Whether joining games is supported.*/
|
||||
public boolean canJoinGame(){
|
||||
return true;
|
||||
}
|
||||
/**Whether debug mode is enabled.*/
|
||||
public boolean isDebug(){return false;}
|
||||
/**Must be 8 bytes in length.*/
|
||||
public byte[] getUUID(){
|
||||
String uuid = Settings.getString("uuid", "");
|
||||
if(uuid.isEmpty()){
|
||||
byte[] result = new byte[8];
|
||||
new Random().nextBytes(result);
|
||||
uuid = new String(Base64Coder.encode(result));
|
||||
Settings.putString("uuid", uuid);
|
||||
Settings.save();
|
||||
return result;
|
||||
}
|
||||
return Base64Coder.decode(uuid);
|
||||
}
|
||||
/**Only used for iOS or android: open the share menu for a map or save.*/
|
||||
public void shareFile(FileHandle file){}
|
||||
/** Add a text input dialog that should show up after the field is tapped. */
|
||||
public void addDialog(TextField field){
|
||||
addDialog(field, 16);
|
||||
}
|
||||
|
||||
/**Show a file chooser. Desktop only.
|
||||
*
|
||||
/** See addDialog(). */
|
||||
public 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. */
|
||||
public void updateRPC(){
|
||||
}
|
||||
|
||||
/** Whether donating is supported. */
|
||||
public boolean canDonate(){
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Must be a base64 string 8 bytes in length. */
|
||||
public 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. */
|
||||
public void shareFile(FileHandle file){
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a file chooser.
|
||||
* @param text File chooser title text
|
||||
* @param content Type of files to be loaded
|
||||
* @param content Description of the type of files to be loaded
|
||||
* @param cons Selection listener
|
||||
* @param open Whether to open or save files.
|
||||
* @param filetype File extensions to filter.
|
||||
* @param open Whether to open or save files
|
||||
* @param filetype File extension to filter
|
||||
*/
|
||||
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, String filetype){}
|
||||
/**Use the default thread provider from the kryonet module for this.*/
|
||||
public ThreadProvider getThreadProvider(){
|
||||
return new ThreadProvider() {
|
||||
@Override public boolean isOnThread() {return true;}
|
||||
@Override public void sleep(long ms) {}
|
||||
@Override public void start(Runnable run) {}
|
||||
@Override public void stop() {}
|
||||
@Override public void notify(Object object) {}
|
||||
@Override public void wait(Object object) {}
|
||||
@Override public <T extends Entity> void switchContainer(EntityGroup<T> group) {}
|
||||
};
|
||||
}
|
||||
}
|
||||
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, Predicate<String> filetype){
|
||||
}
|
||||
|
||||
/** Hide the app. Android only. */
|
||||
public void hide(){
|
||||
}
|
||||
|
||||
/** Forces the app into landscape mode. Currently Android only. */
|
||||
public void beginForceLandscape(){
|
||||
}
|
||||
|
||||
/** Stops forcing the app into landscape orientation. Currently Android only. */
|
||||
public void endForceLandscape(){
|
||||
}
|
||||
}
|
||||
@@ -1,593 +1,373 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.Colors;
|
||||
import com.badlogic.gdx.graphics.Texture;
|
||||
import com.badlogic.gdx.graphics.Texture.TextureWrap;
|
||||
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
|
||||
import com.badlogic.gdx.math.MathUtils;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import com.badlogic.gdx.utils.FloatArray;
|
||||
import com.badlogic.gdx.utils.Pools;
|
||||
import io.anuke.arc.ApplicationListener;
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.files.FileHandle;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.graphics.*;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.graphics.glutils.FrameBuffer;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.SyncEntity;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.game.SpawnPoint;
|
||||
import io.anuke.mindustry.graphics.BlockRenderer;
|
||||
import io.anuke.mindustry.graphics.Shaders;
|
||||
import io.anuke.mindustry.input.InputHandler;
|
||||
import io.anuke.mindustry.input.PlaceMode;
|
||||
import io.anuke.mindustry.ui.fragments.ToolFragment;
|
||||
import io.anuke.mindustry.world.BlockBar;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Blocks;
|
||||
import io.anuke.mindustry.world.blocks.ProductionBlocks;
|
||||
import io.anuke.ucore.core.*;
|
||||
import io.anuke.ucore.entities.EffectEntity;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.function.Callable;
|
||||
import io.anuke.ucore.graphics.*;
|
||||
import io.anuke.ucore.modules.RendererModule;
|
||||
import io.anuke.ucore.scene.ui.layout.Unit;
|
||||
import io.anuke.ucore.scene.utils.Cursors;
|
||||
import io.anuke.ucore.util.Angles;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Tmp;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.effect.GroundEffectEntity;
|
||||
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
|
||||
import io.anuke.mindustry.entities.impl.EffectEntity;
|
||||
import io.anuke.mindustry.entities.traits.*;
|
||||
import io.anuke.mindustry.entities.type.*;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.graphics.*;
|
||||
import io.anuke.mindustry.world.blocks.defense.ForceProjector.ShieldEntity;
|
||||
|
||||
import static io.anuke.arc.Core.*;
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
import static io.anuke.ucore.core.Core.batch;
|
||||
import static io.anuke.ucore.core.Core.camera;
|
||||
|
||||
public class Renderer extends RendererModule{
|
||||
private final static float shieldHitDuration = 18f;
|
||||
|
||||
public Surface shadowSurface, shieldSurface, indicatorSurface;
|
||||
|
||||
private int targetscale = baseCameraScale;
|
||||
private Texture background = new Texture("sprites/background.png");
|
||||
private FloatArray shieldHits = new FloatArray();
|
||||
private Array<Callable> shieldDraws = new Array<>();
|
||||
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
|
||||
private BlockRenderer blocks = new BlockRenderer();
|
||||
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 Pixelator pixelator = new Pixelator();
|
||||
|
||||
public Renderer() {
|
||||
Lines.setCircleVertices(14);
|
||||
public FrameBuffer shieldBuffer = new FrameBuffer(2, 2);
|
||||
private Color clearColor;
|
||||
private float targetscale = io.anuke.arc.scene.ui.layout.Unit.dp.scl(4);
|
||||
private float camerascale = targetscale;
|
||||
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
|
||||
private float shakeIntensity, shaketime;
|
||||
|
||||
Core.cameraScale = baseCameraScale;
|
||||
Effects.setEffectProvider((name, color, x, y, rotation) -> {
|
||||
if(Settings.getBool("effects")){
|
||||
Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
|
||||
.setCenter(camera.position.x, camera.position.y);
|
||||
Rectangle pos = rect2.setSize(name.size).setCenter(x, y);
|
||||
if(view.overlaps(pos)){
|
||||
new EffectEntity(name, color, rotation).set(x, y).add(effectGroup);
|
||||
}
|
||||
}
|
||||
});
|
||||
public Renderer(){
|
||||
batch = new SpriteBatch(4096);
|
||||
camera = new Camera();
|
||||
Lines.setCircleVertices(20);
|
||||
Shaders.init();
|
||||
|
||||
Cursors.cursorScaling = 3;
|
||||
Cursors.outlineColor = Color.valueOf("444444");
|
||||
Cursors.arrow = Cursors.loadCursor("cursor");
|
||||
Cursors.hand = Cursors.loadCursor("hand");
|
||||
Cursors.ibeam = Cursors.loadCursor("ibar");
|
||||
Effects.setScreenShakeProvider((intensity, duration) -> {
|
||||
shakeIntensity = Math.max(intensity, shakeIntensity);
|
||||
shaketime = Math.max(shaketime, duration);
|
||||
});
|
||||
|
||||
clearColor = Hue.lightness(0.4f);
|
||||
clearColor.a = 1f;
|
||||
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);
|
||||
|
||||
background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
|
||||
}
|
||||
if(view.overlaps(pos)){
|
||||
|
||||
@Override
|
||||
public void init(){
|
||||
pixelate = Settings.getBool("pixelate");
|
||||
int scale = Settings.getBool("pixelate") ? Core.cameraScale : 1;
|
||||
|
||||
shadowSurface = Graphics.createSurface(scale);
|
||||
shieldSurface = Graphics.createSurface(scale);
|
||||
indicatorSurface = Graphics.createSurface(scale);
|
||||
pixelSurface = Graphics.createSurface(scale);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public void setPixelate(boolean pixelate){
|
||||
this.pixelate = pixelate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
|
||||
if(Core.cameraScale != targetscale){
|
||||
float targetzoom = (float) Core.cameraScale / targetscale;
|
||||
camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
|
||||
|
||||
if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
|
||||
camera.zoom = 1f;
|
||||
Graphics.setCameraScale(targetscale);
|
||||
control.input().resetCursor();
|
||||
}
|
||||
}else{
|
||||
camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
|
||||
}
|
||||
|
||||
if(state.is(State.menu)){
|
||||
clearScreen();
|
||||
}else{
|
||||
boolean smoothcam = Settings.getBool("smoothcam");
|
||||
|
||||
if(world.getCore() == null || world.getCore().block() == ProductionBlocks.core){
|
||||
if(!smoothcam){
|
||||
setCamera(player.x, player.y);
|
||||
}else{
|
||||
smoothCamera(player.x, player.y, mobile ? 0.3f : 0.14f);
|
||||
}
|
||||
}else{
|
||||
smoothCamera(world.getCore().worldx(), world.getCore().worldy(), 0.4f);
|
||||
}
|
||||
|
||||
if(Settings.getBool("pixelate"))
|
||||
limitCamera(4f, player.x, player.y);
|
||||
|
||||
float prex = camera.position.x, prey = camera.position.y;
|
||||
updateShake(0.75f);
|
||||
float prevx = camera.position.x, prevy = camera.position.y;
|
||||
clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
|
||||
|
||||
float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
|
||||
|
||||
if(mobile){
|
||||
player.x += camera.position.x - prevx;
|
||||
player.y += camera.position.y - prevy;
|
||||
}
|
||||
|
||||
float lastx = camera.position.x, lasty = camera.position.y;
|
||||
|
||||
if(snapCamera && smoothcam && Settings.getBool("pixelate")){
|
||||
camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
|
||||
}
|
||||
|
||||
if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
|
||||
camera.position.add(0, -0.5f, 0);
|
||||
}
|
||||
|
||||
if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
|
||||
camera.position.add(-0.5f, 0, 0);
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
camera.position.set(lastx - deltax, lasty - deltay, 0);
|
||||
|
||||
if(debug && !ui.chatfrag.chatOpen())
|
||||
record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
camera.update();
|
||||
|
||||
clearScreen(clearColor);
|
||||
|
||||
batch.setProjectionMatrix(camera.combined);
|
||||
|
||||
if(pixelate)
|
||||
Graphics.surface(pixelSurface, false);
|
||||
else
|
||||
batch.begin();
|
||||
|
||||
//clears shield surface
|
||||
Graphics.surface(shieldSurface);
|
||||
Graphics.surface();
|
||||
|
||||
drawPadding();
|
||||
|
||||
blocks.drawFloor();
|
||||
blocks.processBlocks();
|
||||
blocks.drawBlocks(false);
|
||||
|
||||
Graphics.shader(Shaders.outline, false);
|
||||
Entities.draw(enemyGroup);
|
||||
Entities.draw(playerGroup, p -> !p.isAndroid);
|
||||
Graphics.shader();
|
||||
|
||||
Entities.draw(Entities.defaultGroup());
|
||||
|
||||
blocks.drawBlocks(true);
|
||||
|
||||
Graphics.shader(Shaders.outline, false);
|
||||
Entities.draw(playerGroup, p -> p.isAndroid);
|
||||
Graphics.shader();
|
||||
|
||||
Entities.draw(bulletGroup);
|
||||
Entities.draw(effectGroup);
|
||||
|
||||
drawShield();
|
||||
|
||||
drawOverlay();
|
||||
|
||||
if(Settings.getBool("indicators") && showUI){
|
||||
drawEnemyMarkers();
|
||||
}
|
||||
|
||||
if(pixelate)
|
||||
Graphics.flushSurface();
|
||||
|
||||
drawPlayerNames();
|
||||
|
||||
batch.end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height){
|
||||
super.resize(width, height);
|
||||
control.input().resetCursor();
|
||||
camera.position.set(player.x, player.y, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
background.dispose();
|
||||
}
|
||||
|
||||
public void clearTiles(){
|
||||
blocks.clearTiles();
|
||||
}
|
||||
|
||||
void drawPadding(){
|
||||
float vw = world.width() * tilesize;
|
||||
float cw = camera.viewportWidth * camera.zoom;
|
||||
float ch = camera.viewportHeight * camera.zoom;
|
||||
if(vw < cw){
|
||||
batch.draw(background,
|
||||
camera.position.x + vw/2,
|
||||
Mathf.round(camera.position.y - ch/2, tilesize),
|
||||
(cw - vw) /2,
|
||||
ch + tilesize,
|
||||
0, 0,
|
||||
((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
|
||||
|
||||
batch.draw(background,
|
||||
camera.position.x - vw/2,
|
||||
Mathf.round(camera.position.y - ch/2, tilesize),
|
||||
-(cw - vw) /2,
|
||||
ch + tilesize,
|
||||
0, 0,
|
||||
-((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void drawPlayerNames(){
|
||||
GlyphLayout layout = Pools.obtain(GlyphLayout.class);
|
||||
|
||||
Draw.tscl(0.25f/2);
|
||||
for(Player player : playerGroup.all()){
|
||||
if(!player.isLocal && !player.isDead()){
|
||||
layout.setText(Core.font, player.name);
|
||||
Draw.color(0f, 0f, 0f, 0.3f);
|
||||
Draw.rect("blank", player.getDrawPosition().x, player.getDrawPosition().y + 8 - layout.height/2, layout.width + 2, layout.height + 2);
|
||||
Draw.color();
|
||||
Draw.tcolor(player.getColor());
|
||||
Draw.text(player.name, player.getDrawPosition().x, player.getDrawPosition().y + 8);
|
||||
|
||||
if(player.isAdmin){
|
||||
Draw.color(player.getColor());
|
||||
float s = 3f;
|
||||
Draw.rect("icon-admin-small", player.getDrawPosition().x + layout.width/2f + 2 + 1, player.getDrawPosition().y + 7f, s, s);
|
||||
}
|
||||
Draw.reset();
|
||||
}
|
||||
}
|
||||
Pools.free(layout);
|
||||
Draw.tscl(fontscale);
|
||||
clearColor = new Color(0f, 0f, 0f, 1f);
|
||||
}
|
||||
|
||||
void drawEnemyMarkers(){
|
||||
Graphics.surface(indicatorSurface);
|
||||
Draw.color(Color.RED);
|
||||
@Override
|
||||
public void update(){
|
||||
//TODO hack, find source of this bug
|
||||
Color.WHITE.set(1f, 1f, 1f, 1f);
|
||||
|
||||
for(Enemy enemy : enemyGroup.all()) {
|
||||
camerascale = Mathf.lerpDelta(camerascale, targetscale, 0.1f);
|
||||
camera.width = graphics.getWidth() / camerascale;
|
||||
camera.height = graphics.getHeight() / camerascale;
|
||||
|
||||
if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y)
|
||||
.overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))) {
|
||||
continue;
|
||||
}
|
||||
if(state.is(State.menu)){
|
||||
graphics.clear(Color.BLACK);
|
||||
}else{
|
||||
Vector2 position = Tmp.v3.set(player);
|
||||
|
||||
float angle = Angles.angle(camera.position.x, camera.position.y, enemy.x, enemy.y);
|
||||
float tx = Angles.trnsx(angle, Unit.dp.scl(20f));
|
||||
float ty = Angles.trnsy(angle, Unit.dp.scl(20f));
|
||||
Draw.rect("enemyarrow", camera.position.x + tx, camera.position.y + ty, angle);
|
||||
}
|
||||
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(!mobile){
|
||||
camera.position.lerpDelta(position, 0.08f);
|
||||
}
|
||||
|
||||
Draw.color();
|
||||
Draw.alpha(0.4f);
|
||||
Graphics.flushSurface();
|
||||
Draw.color();
|
||||
}
|
||||
updateShake(0.75f);
|
||||
if(pixelator.enabled()){
|
||||
pixelator.drawPixelate();
|
||||
}else{
|
||||
draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawShield(){
|
||||
if(shieldGroup.size() == 0 && shieldDraws.size == 0) return;
|
||||
|
||||
Graphics.surface(renderer.shieldSurface, false);
|
||||
Draw.color(Color.ROYAL);
|
||||
Entities.draw(shieldGroup);
|
||||
for(Callable c : shieldDraws){
|
||||
c.run();
|
||||
}
|
||||
Draw.reset();
|
||||
Graphics.surface();
|
||||
|
||||
for(int i = 0; i < shieldHits.size / 3; i++){
|
||||
float time = shieldHits.get(i * 3 + 2);
|
||||
@Override
|
||||
public void dispose(){
|
||||
minimap.dispose();
|
||||
shieldBuffer.dispose();
|
||||
blocks.dispose();
|
||||
}
|
||||
|
||||
time += Timers.delta() / shieldHitDuration;
|
||||
shieldHits.set(i * 3 + 2, time);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if(time >= 1f){
|
||||
shieldHits.removeRange(i * 3, i * 3 + 2);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
public void draw(){
|
||||
camera.update();
|
||||
|
||||
Texture texture = shieldSurface.texture();
|
||||
Shaders.shield.color.set(Color.SKY);
|
||||
if(Float.isNaN(camera.position.x) || Float.isNaN(camera.position.y)){
|
||||
camera.position.x = player.x;
|
||||
camera.position.y = player.y;
|
||||
}
|
||||
|
||||
Tmp.tr2.setRegion(texture);
|
||||
Shaders.shield.region = Tmp.tr2;
|
||||
Shaders.shield.hits = shieldHits;
|
||||
|
||||
if(Shaders.shield.isFallback){
|
||||
Draw.color(1f, 1f, 1f, 0.3f);
|
||||
Shaders.outline.color = Color.SKY;
|
||||
Shaders.outline.region = Tmp.tr2;
|
||||
}
|
||||
graphics.clear(clearColor);
|
||||
|
||||
Graphics.end();
|
||||
Graphics.shader(Shaders.shield.isFallback ? Shaders.outline : Shaders.shield);
|
||||
Graphics.setScreen();
|
||||
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());
|
||||
}
|
||||
|
||||
Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
|
||||
Draw.proj(camera.projection());
|
||||
|
||||
Graphics.shader();
|
||||
Graphics.end();
|
||||
Graphics.beginCam();
|
||||
|
||||
Draw.color();
|
||||
shieldDraws.clear();
|
||||
}
|
||||
blocks.floor.drawFloor();
|
||||
|
||||
public BlockRenderer getBlocks() {
|
||||
return blocks;
|
||||
}
|
||||
drawAndInterpolate(groundEffectGroup, e -> e instanceof BelowLiquidTrait);
|
||||
drawAndInterpolate(puddleGroup);
|
||||
drawAndInterpolate(groundEffectGroup, e -> !(e instanceof BelowLiquidTrait));
|
||||
|
||||
public void addShieldHit(float x, float y){
|
||||
shieldHits.addAll(x, y, 0f);
|
||||
}
|
||||
blocks.processBlocks();
|
||||
|
||||
public void addShield(Callable call){
|
||||
shieldDraws.add(call);
|
||||
}
|
||||
blocks.drawShadows();
|
||||
Draw.color();
|
||||
|
||||
void drawOverlay(){
|
||||
blocks.floor.beginDraw();
|
||||
blocks.floor.drawLayer(CacheLayer.walls);
|
||||
blocks.floor.endDraw();
|
||||
|
||||
//draw tutorial placement point
|
||||
if(world.getMap().name.equals("tutorial") && control.tutorial().showBlock()){
|
||||
int x = world.getCore().x + control.tutorial().getPlacePoint().x;
|
||||
int y = world.getCore().y + control.tutorial().getPlacePoint().y;
|
||||
int rot = control.tutorial().getPlaceRotation();
|
||||
blocks.drawBlocks(Layer.block);
|
||||
blocks.drawFog();
|
||||
|
||||
Lines.stroke(1f);
|
||||
Draw.color(Color.YELLOW);
|
||||
Lines.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f));
|
||||
Draw.shader(Shaders.blockbuild, true);
|
||||
blocks.drawBlocks(Layer.placement);
|
||||
Draw.shader();
|
||||
|
||||
Draw.color(Color.ORANGE);
|
||||
Lines.stroke(2f);
|
||||
if(rot != -1){
|
||||
Lines.lineAngle(x * tilesize, y * tilesize, rot * 90, 6);
|
||||
}
|
||||
Draw.reset();
|
||||
}
|
||||
blocks.drawBlocks(Layer.overlay);
|
||||
|
||||
//draw config selected block
|
||||
if(ui.configfrag.isShown()){
|
||||
Tile tile = ui.configfrag.getSelectedTile();
|
||||
Draw.color(Colors.get("accent"));
|
||||
Lines.stroke(1f);
|
||||
Lines.square(tile.drawx(), tile.drawy(),
|
||||
tile.block().width * tilesize / 2f + 1f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
int tilex = control.input().getBlockX();
|
||||
int tiley = control.input().getBlockY();
|
||||
|
||||
if(mobile){
|
||||
Vector2 vec = Graphics.world(Gdx.input.getX(0), Gdx.input.getY(0));
|
||||
tilex = Mathf.scl2(vec.x, tilesize);
|
||||
tiley = Mathf.scl2(vec.y, tilesize);
|
||||
}
|
||||
drawGroundShadows();
|
||||
|
||||
InputHandler input = control.input();
|
||||
drawAllTeams(false);
|
||||
|
||||
//draw placement box
|
||||
if((input.recipe != null && state.inventory.hasItems(input.recipe.requirements) && (!ui.hasMouse() || mobile)
|
||||
&& control.input().drawPlace())){
|
||||
blocks.skipLayer(Layer.turret);
|
||||
blocks.drawBlocks(Layer.laser);
|
||||
|
||||
input.placeMode.draw(control.input().getBlockX(), control.input().getBlockY(),
|
||||
control.input().getBlockEndX(), control.input().getBlockEndY());
|
||||
drawFlyerShadows();
|
||||
|
||||
Lines.stroke(1f);
|
||||
Draw.color(Color.SCARLET);
|
||||
for(SpawnPoint spawn : world.getSpawns()){
|
||||
Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace);
|
||||
}
|
||||
drawAllTeams(true);
|
||||
|
||||
if(world.getCore() != null) {
|
||||
Draw.color(Color.LIME);
|
||||
Lines.poly(world.getSpawnX(), world.getSpawnY(), 4, 6f, Timers.time() * 2f);
|
||||
}
|
||||
|
||||
if(input.breakMode == PlaceMode.holdDelete)
|
||||
input.breakMode.draw(tilex, tiley, 0, 0);
|
||||
|
||||
}else if(input.breakMode.delete && control.input().drawPlace()
|
||||
&& (input.recipe == null || !state.inventory.hasItems(input.recipe.requirements))
|
||||
&& (input.placeMode.delete || input.breakMode.both || !mobile)){
|
||||
drawAndInterpolate(bulletGroup);
|
||||
drawAndInterpolate(effectGroup);
|
||||
|
||||
if(input.breakMode == PlaceMode.holdDelete)
|
||||
input.breakMode.draw(tilex, tiley, 0, 0);
|
||||
else
|
||||
input.breakMode.draw(control.input().getBlockX(), control.input().getBlockY(),
|
||||
control.input().getBlockEndX(), control.input().getBlockEndY());
|
||||
}
|
||||
overlays.drawBottom();
|
||||
drawAndInterpolate(playerGroup, p -> true, Player::drawBuildRequests);
|
||||
|
||||
if(ui.toolfrag.confirming){
|
||||
ToolFragment t = ui.toolfrag;
|
||||
PlaceMode.areaDelete.draw(t.px, t.py, t.px2, t.py2);
|
||||
}
|
||||
|
||||
Draw.reset();
|
||||
if(Entities.countInBounds(shieldGroup) > 0){
|
||||
if(settings.getBool("animatedshields")){
|
||||
Draw.flush();
|
||||
shieldBuffer.begin();
|
||||
graphics.clear(Color.CLEAR);
|
||||
Entities.draw(shieldGroup);
|
||||
Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).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{
|
||||
Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).drawSimple());
|
||||
}
|
||||
}
|
||||
|
||||
//draw selected block bars and info
|
||||
if(input.recipe == null && !ui.hasMouse()){
|
||||
Tile tile = world.tileWorld(Graphics.mouseWorld().x, Graphics.mouseWorld().y);
|
||||
overlays.drawTop();
|
||||
|
||||
if(tile != null && tile.block() != Blocks.air){
|
||||
Tile target = tile;
|
||||
if(tile.isLinked())
|
||||
target = tile.getLinked();
|
||||
drawAndInterpolate(playerGroup, p -> !p.isDead() && !p.isLocal, Player::drawName);
|
||||
|
||||
if(showBlockDebug && target.entity != null){
|
||||
Draw.color(Color.RED);
|
||||
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
|
||||
Vector2 v = new Vector2();
|
||||
Draw.color();
|
||||
Draw.flush();
|
||||
}
|
||||
|
||||
private void drawGroundShadows(){
|
||||
Draw.color(0, 0, 0, 0.4f);
|
||||
float rad = 1.6f;
|
||||
|
||||
Consumer<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);
|
||||
};
|
||||
|
||||
Draw.tcolor(Color.YELLOW);
|
||||
Draw.tscl(0.25f);
|
||||
Array<Object> arr = target.block().getDebugInfo(target);
|
||||
StringBuilder result = new StringBuilder();
|
||||
for(int i = 0; i < arr.size/2; i ++){
|
||||
result.append(arr.get(i*2));
|
||||
result.append(": ");
|
||||
result.append(arr.get(i*2 + 1));
|
||||
result.append("\n");
|
||||
}
|
||||
Draw.textc(result.toString(), target.drawx(), target.drawy(), v);
|
||||
Draw.color(0f, 0f, 0f, 0.5f);
|
||||
Fill.rect(target.drawx(), target.drawy(), v.x, v.y);
|
||||
Draw.textc(result.toString(), target.drawx(), target.drawy(), v);
|
||||
Draw.tscl(fontscale);
|
||||
Draw.reset();
|
||||
}
|
||||
for(EntityGroup<? extends BaseUnit> group : unitGroups){
|
||||
if(!group.isEmpty()){
|
||||
drawAndInterpolate(group, unit -> !unit.isDead(), draw::accept);
|
||||
}
|
||||
}
|
||||
|
||||
if(Inputs.keyDown("block_info") && target.block().fullDescription != null){
|
||||
Draw.color(Colors.get("accent"));
|
||||
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
if(Inputs.keyDown("block_logs")){
|
||||
Draw.color(Colors.get("accent"));
|
||||
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
|
||||
Draw.color();
|
||||
}
|
||||
if(!playerGroup.isEmpty()){
|
||||
drawAndInterpolate(playerGroup, unit -> !unit.isDead(), draw::accept);
|
||||
}
|
||||
|
||||
if(target.entity != null) {
|
||||
int bot = 0, top = 0;
|
||||
for (BlockBar bar : target.block().bars) {
|
||||
float offset = Mathf.sign(bar.top) * (target.block().height / 2f * tilesize + 3f + 4f * ((bar.top ? top : bot))) +
|
||||
(bar.top ? -1f : 0f);
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
float value = bar.value.get(target);
|
||||
private void drawFlyerShadows(){
|
||||
float trnsX = -12, trnsY = -13;
|
||||
Draw.color(0, 0, 0, 0.22f);
|
||||
|
||||
if(MathUtils.isEqual(value, -1f)) continue;
|
||||
for(EntityGroup<? extends BaseUnit> group : unitGroups){
|
||||
if(!group.isEmpty()){
|
||||
drawAndInterpolate(group, unit -> unit.isFlying() && !unit.isDead(), baseUnit -> baseUnit.drawShadow(trnsX, trnsY));
|
||||
}
|
||||
}
|
||||
|
||||
drawBar(bar.color, target.drawx(), target.drawy() + offset, value);
|
||||
if(!playerGroup.isEmpty()){
|
||||
drawAndInterpolate(playerGroup, unit -> unit.isFlying() && !unit.isDead(), player -> player.drawShadow(trnsX, trnsY));
|
||||
}
|
||||
|
||||
if (bar.top)
|
||||
top++;
|
||||
else
|
||||
bot++;
|
||||
}
|
||||
}
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
target.block().drawSelect(target);
|
||||
}
|
||||
}
|
||||
|
||||
if((!debug || showUI) && Settings.getBool("healthbars")){
|
||||
private void drawAllTeams(boolean flying){
|
||||
for(Team team : Team.all){
|
||||
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
|
||||
|
||||
//draw entity health bars
|
||||
for(Enemy entity : enemyGroup.all()){
|
||||
drawHealth(entity);
|
||||
}
|
||||
if(group.count(p -> p.isFlying() == flying) +
|
||||
playerGroup.count(p -> p.isFlying() == flying && p.getTeam() == team) == 0 && flying) continue;
|
||||
|
||||
for(Player player : playerGroup.all()){
|
||||
if(!player.isDead() && !player.isAndroid) drawHealth(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawUnder);
|
||||
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team && !p.isDead(), Unit::drawUnder);
|
||||
|
||||
void drawHealth(SyncEntity dest){
|
||||
float x = dest.getDrawPosition().x;
|
||||
float y = dest.getDrawPosition().y;
|
||||
if(dest instanceof Player && snapCamera && Settings.getBool("smoothcam") && Settings.getBool("pixelate")){
|
||||
drawHealth((int) x, (int) y - 7f, dest.health, dest.maxhealth);
|
||||
}else{
|
||||
drawHealth(x, y - 7f, dest.health, dest.maxhealth);
|
||||
}
|
||||
}
|
||||
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawAll);
|
||||
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawAll);
|
||||
blocks.drawTeamBlocks(Layer.turret, team);
|
||||
|
||||
void drawHealth(float x, float y, float health, float maxhealth){
|
||||
drawBar(Color.RED, x, y, health / maxhealth);
|
||||
}
|
||||
|
||||
//TODO optimize!
|
||||
public void drawBar(Color color, float x, float y, float finion){
|
||||
finion = Mathf.clamp(finion);
|
||||
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawOver);
|
||||
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawOver);
|
||||
}
|
||||
}
|
||||
|
||||
if(finion > 0) finion = Mathf.clamp(finion + 0.2f, 0.24f, 1f);
|
||||
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group){
|
||||
drawAndInterpolate(group, t -> true, DrawTrait::draw);
|
||||
}
|
||||
|
||||
float len = 3;
|
||||
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw){
|
||||
drawAndInterpolate(group, toDraw, DrawTrait::draw);
|
||||
}
|
||||
|
||||
float w = (int) (len * 2 * finion) + 0.5f;
|
||||
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw, Consumer<T> drawer){
|
||||
Entities.draw(group, toDraw, drawer);
|
||||
}
|
||||
|
||||
x -= 0.5f;
|
||||
y += 0.5f;
|
||||
public void scaleCamera(float amount){
|
||||
targetscale += amount;
|
||||
clampScale();
|
||||
}
|
||||
|
||||
Lines.stroke(3f);
|
||||
Draw.color(Color.SLATE);
|
||||
Lines.line(x - len + 1, y, x + len + 1.5f, y);
|
||||
Lines.stroke(1f);
|
||||
Draw.color(Color.BLACK);
|
||||
Lines.line(x - len + 1, y, x + len + 0.5f, y);
|
||||
Draw.color(color);
|
||||
if(w >= 1)
|
||||
Lines.line(x - len + 1, y, x - len + w, y);
|
||||
Draw.reset();
|
||||
}
|
||||
public void clampScale(){
|
||||
float s = io.anuke.arc.scene.ui.layout.Unit.dp.scl(1f);
|
||||
targetscale = Mathf.clamp(targetscale, s * 1.5f, Math.round(s * 6));
|
||||
}
|
||||
|
||||
public void setCameraScale(int amount){
|
||||
targetscale = amount;
|
||||
clampScale();
|
||||
//scale up all surfaces in preparation for the zoom
|
||||
if(Settings.getBool("pixelate")){
|
||||
for(Surface surface : Graphics.getSurfaces()){
|
||||
surface.setScale(targetscale);
|
||||
}
|
||||
}
|
||||
}
|
||||
public float getScale(){
|
||||
return targetscale;
|
||||
}
|
||||
|
||||
public void scaleCamera(int amount){
|
||||
setCameraScale(targetscale + amount);
|
||||
}
|
||||
public void setScale(float scl){
|
||||
targetscale = scl;
|
||||
clampScale();
|
||||
}
|
||||
|
||||
public void clampScale(){
|
||||
targetscale = Mathf.clamp(targetscale, Math.round(Unit.dp.scl(2)), Math.round(Unit.dp.scl((5))));
|
||||
}
|
||||
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);
|
||||
FileHandle 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import com.badlogic.gdx.utils.TimeUtils;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.EntityGroup;
|
||||
import io.anuke.ucore.entities.EntityGroup.ArrayContainer;
|
||||
import io.anuke.ucore.util.Log;
|
||||
|
||||
import static io.anuke.mindustry.Vars.control;
|
||||
import static io.anuke.mindustry.Vars.logic;
|
||||
|
||||
public class ThreadHandler {
|
||||
private final Array<Runnable> toRun = new Array<>();
|
||||
private final ThreadProvider impl;
|
||||
private float delta = 1f;
|
||||
private long frame = 0;
|
||||
private float framesSinceUpdate;
|
||||
private boolean enabled;
|
||||
|
||||
private final Object updateLock = new Object();
|
||||
private boolean rendered = true;
|
||||
|
||||
public ThreadHandler(ThreadProvider impl){
|
||||
this.impl = impl;
|
||||
|
||||
Timers.setDeltaProvider(() -> {
|
||||
float result = impl.isOnThread() ? delta : Gdx.graphics.getDeltaTime()*60f;
|
||||
return Math.min(Float.isNaN(result) ? 1f : result, 12f);
|
||||
});
|
||||
}
|
||||
|
||||
public void run(Runnable r){
|
||||
if(enabled) {
|
||||
synchronized (toRun) {
|
||||
toRun.add(r);
|
||||
}
|
||||
}else{
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
|
||||
public int getFPS(){
|
||||
return (int)(60/delta);
|
||||
}
|
||||
|
||||
public long getFrameID(){
|
||||
return frame;
|
||||
}
|
||||
|
||||
public float getFramesSinceUpdate(){
|
||||
return framesSinceUpdate;
|
||||
}
|
||||
|
||||
public void handleRender(){
|
||||
|
||||
if(!enabled) return;
|
||||
|
||||
framesSinceUpdate += Timers.delta();
|
||||
|
||||
synchronized (updateLock) {
|
||||
rendered = true;
|
||||
impl.notify(updateLock);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled){
|
||||
if(enabled){
|
||||
logic.doUpdate = false;
|
||||
for(EntityGroup<?> group : Entities.getAllGroups()){
|
||||
impl.switchContainer(group);
|
||||
}
|
||||
Timers.runTask(2f, () -> {
|
||||
impl.start(this::runLogic);
|
||||
this.enabled = true;
|
||||
});
|
||||
}else{
|
||||
this.enabled = false;
|
||||
impl.stop();
|
||||
for(EntityGroup<?> group : Entities.getAllGroups()){
|
||||
group.setContainer(new ArrayContainer<>());
|
||||
}
|
||||
Timers.runTask(2f, () -> {
|
||||
logic.doUpdate = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled(){
|
||||
return enabled;
|
||||
}
|
||||
|
||||
private void runLogic(){
|
||||
try {
|
||||
while (true) {
|
||||
long time = TimeUtils.millis();
|
||||
|
||||
synchronized (toRun) {
|
||||
for(Runnable r : toRun){
|
||||
r.run();
|
||||
}
|
||||
toRun.clear();
|
||||
}
|
||||
|
||||
logic.update();
|
||||
|
||||
long elapsed = TimeUtils.timeSinceMillis(time);
|
||||
long target = (long) (1000 / 60f);
|
||||
|
||||
delta = Math.max(elapsed, target) / 1000f * 60f;
|
||||
|
||||
if (elapsed < target) {
|
||||
impl.sleep(target - elapsed);
|
||||
}
|
||||
|
||||
synchronized(updateLock) {
|
||||
while(!rendered) {
|
||||
impl.wait(updateLock);
|
||||
}
|
||||
rendered = false;
|
||||
}
|
||||
|
||||
frame ++;
|
||||
framesSinceUpdate = 0;
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Log.info("Stopping logic thread.");
|
||||
} catch (Throwable ex) {
|
||||
control.setError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ThreadProvider {
|
||||
boolean isOnThread();
|
||||
void sleep(long ms) throws InterruptedException;
|
||||
void start(Runnable run);
|
||||
void stop();
|
||||
void wait(Object object) throws InterruptedException;
|
||||
void notify(Object object);
|
||||
<T extends Entity> void switchContainer(EntityGroup<T> group);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,6 @@ public class UI implements ApplicationListener{
|
||||
showTextInput(title, text, textLength < 0 ? 12 : textLength, def, (field, c) -> true, confirmed);
|
||||
}
|
||||
|
||||
|
||||
public void showInfoFade(String info){
|
||||
Table table = new Table();
|
||||
table.setFillParent(true);
|
||||
|
||||
@@ -1,359 +1,523 @@
|
||||
package io.anuke.mindustry.core;
|
||||
|
||||
import com.badlogic.gdx.math.GridPoint2;
|
||||
import com.badlogic.gdx.math.MathUtils;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import io.anuke.mindustry.ai.Pathfind;
|
||||
import io.anuke.mindustry.entities.TileEntity;
|
||||
import io.anuke.mindustry.game.SpawnPoint;
|
||||
import io.anuke.mindustry.io.Maps;
|
||||
import io.anuke.annotations.Annotations.Nullable;
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.collection.IntArray;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.arc.math.geom.Point2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.ai.*;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.entities.Entities;
|
||||
import io.anuke.mindustry.game.EventType.TileChangeEvent;
|
||||
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.io.MapIO;
|
||||
import io.anuke.mindustry.maps.*;
|
||||
import io.anuke.mindustry.maps.generators.Generator;
|
||||
import io.anuke.mindustry.type.Zone;
|
||||
import io.anuke.mindustry.world.*;
|
||||
import io.anuke.mindustry.world.blocks.Blocks;
|
||||
import io.anuke.mindustry.world.blocks.DistributionBlocks;
|
||||
import io.anuke.mindustry.world.blocks.ProductionBlocks;
|
||||
import io.anuke.mindustry.world.blocks.WeaponBlocks;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.modules.Module;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Tmp;
|
||||
import io.anuke.mindustry.world.blocks.BlockPart;
|
||||
|
||||
import static io.anuke.mindustry.Vars.control;
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class World extends Module{
|
||||
private int seed;
|
||||
|
||||
private Map currentMap;
|
||||
private Tile[][] tiles;
|
||||
private Pathfind pathfind = new Pathfind();
|
||||
private Maps maps = new Maps();
|
||||
private Tile core;
|
||||
private Array<SpawnPoint> spawns = new Array<>();
|
||||
public class World implements ApplicationListener{
|
||||
public final Maps maps = new Maps();
|
||||
public final BlockIndexer indexer = new BlockIndexer();
|
||||
public final WaveSpawner spawner = new WaveSpawner();
|
||||
public final Pathfinder pathfinder = new Pathfinder();
|
||||
public final Context context = new Context();
|
||||
|
||||
private Tile[] temptiles = new Tile[4];
|
||||
|
||||
public World(){
|
||||
maps.loadMaps();
|
||||
currentMap = maps.getMap(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
maps.dispose();
|
||||
}
|
||||
private Map currentMap;
|
||||
private Tile[][] tiles;
|
||||
|
||||
public Array<SpawnPoint> getSpawns(){
|
||||
return spawns;
|
||||
}
|
||||
private boolean generating, invalidMap;
|
||||
|
||||
public Tile getCore(){
|
||||
return core;
|
||||
}
|
||||
|
||||
public Maps maps(){
|
||||
return maps;
|
||||
}
|
||||
|
||||
public Pathfind pathfinder(){
|
||||
return pathfind;
|
||||
}
|
||||
public World(){
|
||||
maps.load();
|
||||
}
|
||||
|
||||
public float getSpawnX(){
|
||||
return core.worldx();
|
||||
}
|
||||
@Override
|
||||
public void init(){
|
||||
maps.loadLegacyMaps();
|
||||
}
|
||||
|
||||
public float getSpawnY(){
|
||||
return core.worldy() - tilesize*2;
|
||||
}
|
||||
|
||||
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 boolean blends(Block block, int x, int y){
|
||||
return !floorBlends(x, y-1, block) || !floorBlends(x, y+1, block)
|
||||
|| !floorBlends(x-1, y, block) ||!floorBlends(x+1, y, block);
|
||||
}
|
||||
|
||||
public boolean floorBlends(int x, int y, Block block){
|
||||
Tile tile = tile(x, y);
|
||||
return tile == null || tile.floor().id <= block.id;
|
||||
}
|
||||
|
||||
public Map getMap(){
|
||||
return currentMap;
|
||||
}
|
||||
|
||||
public int width(){
|
||||
return currentMap.getWidth();
|
||||
}
|
||||
|
||||
public int height(){
|
||||
return currentMap.getHeight();
|
||||
}
|
||||
@Override
|
||||
public void dispose(){
|
||||
maps.dispose();
|
||||
}
|
||||
|
||||
public Tile tile(int packed){
|
||||
return tile(packed % width(), packed / width());
|
||||
}
|
||||
|
||||
public Tile tile(int x, int y){
|
||||
if(tiles == null){
|
||||
return null;
|
||||
}
|
||||
if(!Mathf.inBounds(x, y, tiles)) return null;
|
||||
return tiles[x][y];
|
||||
}
|
||||
|
||||
public Tile tileWorld(float x, float y){
|
||||
return tile(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize));
|
||||
}
|
||||
public boolean isInvalidMap(){
|
||||
return invalidMap;
|
||||
}
|
||||
|
||||
public int toTile(float coord){
|
||||
return Mathf.scl2(coord, tilesize);
|
||||
}
|
||||
|
||||
public Tile[][] getTiles(){
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private void createTiles(){
|
||||
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] = new Tile(x, y, Blocks.stone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadMap(Map map){
|
||||
loadMap(map, MathUtils.random(0, 99999));
|
||||
}
|
||||
|
||||
public void loadMap(Map map, int seed){
|
||||
currentMap = map;
|
||||
|
||||
if(tiles != null){
|
||||
clearTileEntities();
|
||||
|
||||
if(tiles.length != map.getWidth() || tiles[0].length != map.getHeight()){
|
||||
tiles = new Tile[map.getWidth()][map.getHeight()];
|
||||
}
|
||||
|
||||
createTiles();
|
||||
}else{
|
||||
tiles = new Tile[map.getWidth()][map.getHeight()];
|
||||
|
||||
createTiles();
|
||||
}
|
||||
|
||||
spawns.clear();
|
||||
|
||||
Entities.resizeTree(0, 0, map.getWidth() * tilesize, map.getHeight() * tilesize);
|
||||
|
||||
this.seed = seed;
|
||||
|
||||
core = WorldGenerator.generate(map.pixmap, tiles, spawns);
|
||||
public boolean solid(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
|
||||
Placement.placeBlock(core.x, core.y, ProductionBlocks.core, 0, false, false);
|
||||
|
||||
if(!map.name.equals("tutorial")){
|
||||
setDefaultBlocks();
|
||||
}else{
|
||||
control.tutorial().setDefaultBlocks(core.x, core.y);
|
||||
}
|
||||
|
||||
pathfind.resetPaths();
|
||||
}
|
||||
|
||||
void setDefaultBlocks(){
|
||||
int x = core.x, y = core.y;
|
||||
int flip = Mathf.sign(!currentMap.flipBase);
|
||||
int fr = currentMap.flipBase ? 2 : 0;
|
||||
|
||||
set(x, y-2*flip, DistributionBlocks.conveyor, 1 + fr);
|
||||
set(x, y-3*flip, DistributionBlocks.conveyor, 1 + fr);
|
||||
|
||||
for(int i = 0; i < 2; i ++){
|
||||
int d = Mathf.sign(i-0.5f);
|
||||
|
||||
set(x+2*d, y-2*flip, ProductionBlocks.stonedrill, d);
|
||||
set(x+2*d, y-1*flip, DistributionBlocks.conveyor, 1 + fr);
|
||||
set(x+2*d, y, DistributionBlocks.conveyor, 1 + fr);
|
||||
set(x+2*d, y+1*flip, WeaponBlocks.doubleturret, 0 + fr);
|
||||
|
||||
set(x+1*d, y-3*flip, DistributionBlocks.conveyor, 2*d);
|
||||
set(x+2*d, y-3*flip, DistributionBlocks.conveyor, 2*d);
|
||||
set(x+2*d, y-4*flip, DistributionBlocks.conveyor, 1 + fr);
|
||||
set(x+2*d, y-5*flip, DistributionBlocks.conveyor, 1 + fr);
|
||||
|
||||
set(x+3*d, y-5*flip, ProductionBlocks.stonedrill, 0 + fr);
|
||||
set(x+3*d, y-4*flip, ProductionBlocks.stonedrill, 0 + fr);
|
||||
set(x+3*d, y-3*flip, ProductionBlocks.stonedrill, 0 + fr);
|
||||
}
|
||||
}
|
||||
|
||||
void set(int x, int y, Block type, int rot){
|
||||
if(!Mathf.inBounds(x, y, tiles)){
|
||||
return;
|
||||
}
|
||||
if(type == ProductionBlocks.stonedrill){
|
||||
tiles[x][y].setFloor(Blocks.stone);
|
||||
}
|
||||
tiles[x][y].setBlock(type, rot);
|
||||
}
|
||||
|
||||
public int getSeed(){
|
||||
return seed;
|
||||
}
|
||||
return tile == null || tile.solid();
|
||||
}
|
||||
|
||||
public void removeBlock(Tile tile){
|
||||
if(!tile.block().isMultiblock() && !tile.isLinked()){
|
||||
tile.setBlock(Blocks.air);
|
||||
}else{
|
||||
Tile target = tile.target();
|
||||
Array<Tile> removals = target.getLinkedTiles();
|
||||
for(Tile toremove : removals){
|
||||
//note that setting a new block automatically unlinks it
|
||||
if(toremove != null) toremove.setBlock(Blocks.air);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TileEntity findTileTarget(float x, float y, Tile tile, float range, boolean damaged){
|
||||
Entity closest = null;
|
||||
float dst = 0;
|
||||
|
||||
int rad = (int)(range/tilesize)+1;
|
||||
int tilex = Mathf.scl2(x, tilesize);
|
||||
int tiley = Mathf.scl2(y, tilesize);
|
||||
|
||||
for(int rx = -rad; rx <= rad; rx ++){
|
||||
for(int ry = -rad; ry <= rad; ry ++){
|
||||
Tile other = tile(rx+tilex, ry+tiley);
|
||||
|
||||
if(other != null && other.getLinked() != null){
|
||||
other = other.getLinked();
|
||||
}
|
||||
|
||||
if(other == null || other.entity == null || (tile != null && other.entity == tile.entity)) continue;
|
||||
|
||||
TileEntity e = other.entity;
|
||||
|
||||
if(damaged && e.health >= e.tile.block().health)
|
||||
continue;
|
||||
|
||||
float ndst = Vector2.dst(x, y, e.x, e.y);
|
||||
if(ndst < range && (closest == null || ndst < dst)){
|
||||
dst = ndst;
|
||||
closest = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
public boolean passable(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
|
||||
return (TileEntity) closest;
|
||||
}
|
||||
return tile != null && tile.passable();
|
||||
}
|
||||
|
||||
/**Raycast, but with world coordinates.*/
|
||||
public GridPoint2 raycastWorld(float x, float y, float x2, float y2){
|
||||
return raycast(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize),
|
||||
Mathf.scl2(x2, tilesize), Mathf.scl2(y2, tilesize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Input is in block coordinates, not world coordinates.
|
||||
* @return null if no collisions found, block position otherwise.*/
|
||||
public GridPoint2 raycast(int x0f, int y0f, int x1, int y1){
|
||||
int x0 = x0f;
|
||||
int y0 = y0f;
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
public boolean wallSolid(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
return tile == null || tile.block().solid;
|
||||
}
|
||||
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
public boolean isAccessible(int x, int y){
|
||||
return !wallSolid(x, y - 1) || !wallSolid(x, y + 1) || !wallSolid(x - 1, y) || !wallSolid(x + 1, y);
|
||||
}
|
||||
|
||||
int err = dx - dy;
|
||||
int e2;
|
||||
while(true){
|
||||
public Map getMap(){
|
||||
return currentMap;
|
||||
}
|
||||
|
||||
if(!passable(x0, y0)){
|
||||
return Tmp.g1.set(x0, y0);
|
||||
}
|
||||
if(x0 == x1 && y0 == y1) break;
|
||||
public void setMap(Map map){
|
||||
this.currentMap = map;
|
||||
}
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x0 = x0 + sx;
|
||||
}
|
||||
public int width(){
|
||||
return tiles == null ? 0 : tiles.length;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y0 = y0 + sy;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public int height(){
|
||||
return tiles == null ? 0 : tiles[0].length;
|
||||
}
|
||||
|
||||
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);
|
||||
public int unitWidth(){
|
||||
return width()*tilesize;
|
||||
}
|
||||
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
public int unitHeight(){
|
||||
return height()*tilesize;
|
||||
}
|
||||
|
||||
int err = dx - dy;
|
||||
int e2;
|
||||
while(true){
|
||||
public @Nullable Tile tile(int pos){
|
||||
return tiles == null ? null : tile(Pos.x(pos), Pos.y(pos));
|
||||
}
|
||||
|
||||
if(cons.accept(x0, y0)) break;
|
||||
if(x0 == x1 && y0 == y1) break;
|
||||
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];
|
||||
}
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x0 = x0 + sx;
|
||||
}
|
||||
public @Nullable Tile ltile(int x, int y){
|
||||
Tile tile = tile(x, y);
|
||||
if(tile == null) return null;
|
||||
return tile.block().linked(tile);
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y0 = y0 + sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
public Tile rawTile(int x, int y){
|
||||
return tiles[x][y];
|
||||
}
|
||||
|
||||
public interface Raycaster{
|
||||
boolean accept(int x, int 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 signal the beginning of loading the map with a custom set of tiles. */
|
||||
public void beginMapLoad(Tile[][] tiles){
|
||||
this.tiles = tiles;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addDarkness(tiles);
|
||||
|
||||
Entities.getAllGroups().each(group -> group.resize(-finalWorldBounds, -finalWorldBounds, tiles.length * tilesize + finalWorldBounds * 2, tiles[0].length * tilesize + finalWorldBounds * 2));
|
||||
|
||||
generating = false;
|
||||
Events.fire(new WorldLoadEvent());
|
||||
}
|
||||
|
||||
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){
|
||||
|
||||
try{
|
||||
MapIO.loadMap(map);
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
if(!headless){
|
||||
ui.showError("$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){
|
||||
ui.showError("$map.nospawn");
|
||||
invalidMap = true;
|
||||
}else if(state.rules.pvp){ //pvp maps need two cores to be valid
|
||||
invalidMap = true;
|
||||
for(Team team : Team.all){
|
||||
if(state.teams.get(team).cores.size != 0 && team != defaultTeam){
|
||||
invalidMap = false;
|
||||
}
|
||||
}
|
||||
if(invalidMap){
|
||||
ui.showError("$map.nospawn.pvp");
|
||||
}
|
||||
}else if(state.rules.attackMode){ //pvp maps need two cores to be valid
|
||||
invalidMap = state.teams.get(waveTeam).cores.isEmpty();
|
||||
if(invalidMap){
|
||||
ui.showError("$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){
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raycast, but with world coordinates.
|
||||
*/
|
||||
public Point2 raycastWorld(float x, float y, float x2, float y2){
|
||||
return raycast(Math.round(x / tilesize), Math.round(y / tilesize),
|
||||
Math.round(x2 / tilesize), Math.round(y2 / tilesize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Input is in block coordinates, not world coordinates.
|
||||
* @return null if no collisions found, block position otherwise.
|
||||
*/
|
||||
public Point2 raycast(int x0f, int y0f, int x1, int y1){
|
||||
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(!passable(x0, y0)){
|
||||
return Tmp.g1.set(x0, y0);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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.block().solid && !tile.block().synthetic() && tile.block().fillsTile){
|
||||
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.block().solid && !tile.block().synthetic()){
|
||||
tiles[x][y].rotation(dark[x][y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* '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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
core/src/io/anuke/mindustry/editor/DrawOperation.java
Executable file
95
core/src/io/anuke/mindustry/editor/DrawOperation.java
Executable file
@@ -0,0 +1,95 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.annotations.Annotations.Struct;
|
||||
import io.anuke.arc.collection.LongArray;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.gen.TileOp;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.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
|
||||
}
|
||||
}
|
||||
147
core/src/io/anuke/mindustry/editor/EditorTile.java
Normal file
147
core/src/io/anuke/mindustry/editor/EditorTile.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.editor.DrawOperation.OpType;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.gen.TileOp;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.*;
|
||||
import io.anuke.mindustry.world.modules.*;
|
||||
|
||||
import static io.anuke.mindustry.Vars.state;
|
||||
import static io.anuke.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 setOverlayID(short overlay){
|
||||
if(state.is(State.playing)){
|
||||
super.setOverlayID(overlay);
|
||||
return;
|
||||
}
|
||||
|
||||
if(overlayID() == overlay) return;
|
||||
op(OpType.overlay, this.overlay);
|
||||
super.setOverlayID(overlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preChanged(){
|
||||
if(state.is(State.playing)){
|
||||
super.preChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
super.setTeam(Team.none);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
241
core/src/io/anuke/mindustry/editor/EditorTool.java
Normal file
241
core/src/io/anuke/mindustry/editor/EditorTool.java
Normal file
@@ -0,0 +1,241 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.collection.IntArray;
|
||||
import io.anuke.arc.function.*;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Bresenham2;
|
||||
import io.anuke.arc.util.Structs;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.world.*;
|
||||
|
||||
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){
|
||||
Predicate<Tile> tester;
|
||||
Consumer<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);
|
||||
}
|
||||
|
||||
//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, Predicate<Tile> tester, Consumer<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.test(tile)){
|
||||
filler.accept(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.test(editor.tile(x1, y))) x1--;
|
||||
x1++;
|
||||
boolean spanAbove = false, spanBelow = false;
|
||||
while(x1 < width && tester.test(editor.tile(x1, y))){
|
||||
filler.accept(editor.tile(x1, y));
|
||||
|
||||
if(!spanAbove && y > 0 && tester.test(editor.tile(x1, y - 1))){
|
||||
stack.add(Pos.get(x1, y - 1));
|
||||
spanAbove = true;
|
||||
}else if(spanAbove && !tester.test(editor.tile(x1, y - 1))){
|
||||
spanAbove = false;
|
||||
}
|
||||
|
||||
if(!spanBelow && y < height - 1 && tester.test(editor.tile(x1, y + 1))){
|
||||
stack.add(Pos.get(x1, y + 1));
|
||||
spanBelow = true;
|
||||
}else if(spanBelow && y < height - 1 && !tester.test(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){}
|
||||
}
|
||||
405
core/src/io/anuke/mindustry/editor/MapEditor.java
Normal file
405
core/src/io/anuke/mindustry/editor/MapEditor.java
Normal file
@@ -0,0 +1,405 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.collection.StringMap;
|
||||
import io.anuke.arc.files.FileHandle;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.graphics.Pixmap;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Structs;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.gen.TileOp;
|
||||
import io.anuke.mindustry.io.LegacyMapIO;
|
||||
import io.anuke.mindustry.io.MapIO;
|
||||
import io.anuke.mindustry.maps.Map;
|
||||
import io.anuke.mindustry.world.*;
|
||||
import io.anuke.mindustry.world.blocks.BlockPart;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
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.blue;
|
||||
|
||||
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);
|
||||
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(FileHandle 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, Predicate<Tile> tester){
|
||||
drawBlocks(x, y, false, tester);
|
||||
}
|
||||
|
||||
public void drawBlocks(int x, int y, boolean square, Predicate<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;
|
||||
|
||||
Consumer<Tile> drawer = tile -> {
|
||||
if(!tester.test(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, Consumer<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.accept(tile(wx, wy));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void drawSquare(int x, int y, Consumer<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.accept(tile(wx, wy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void draw_DEPRECATED(int x, int y, boolean paint, Block drawBlock, double chance){
|
||||
boolean isfloor = drawBlock instanceof Floor && drawBlock != Blocks.air;
|
||||
Tile[][] tiles = world.getTiles();
|
||||
|
||||
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 = tiles[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(tiles[x][y], drawBlock, drawTeam);
|
||||
}else{
|
||||
for(int rx = -brushSize; rx <= brushSize; rx++){
|
||||
for(int ry = -brushSize; ry <= brushSize; ry++){
|
||||
if(Mathf.dst(rx, ry) <= brushSize - 0.5f && (chance >= 0.999 || Mathf.chance(chance))){
|
||||
int wx = x + rx, wy = y + ry;
|
||||
|
||||
if(wx < 0 || wy < 0 || wx >= width() || wy >= height() || (paint && !isfloor && tiles[wx][wy].block() == Blocks.air)){
|
||||
continue;
|
||||
}
|
||||
|
||||
Tile tile = tiles[wx][wy];
|
||||
|
||||
if(!isfloor && (tile.isLinked() || tile.block().isMultiblock())){
|
||||
world.removeBlock(tile.link());
|
||||
}
|
||||
|
||||
if(isfloor){
|
||||
tile.setFloor((Floor)drawBlock);
|
||||
}else{
|
||||
tile.setBlock(drawBlock);
|
||||
if(drawBlock.synthetic()){
|
||||
tile.setTeam(drawTeam);
|
||||
}
|
||||
if(drawBlock.rotate){
|
||||
tile.rotation((byte)rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
658
core/src/io/anuke/mindustry/editor/MapEditorDialog.java
Normal file
658
core/src/io/anuke/mindustry/editor/MapEditorDialog.java
Normal file
@@ -0,0 +1,658 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.collection.StringMap;
|
||||
import io.anuke.arc.files.FileHandle;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.Pixmap;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.arc.input.KeyCode;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.scene.actions.Actions;
|
||||
import io.anuke.arc.scene.event.Touchable;
|
||||
import io.anuke.arc.scene.style.TextureRegionDrawable;
|
||||
import io.anuke.arc.scene.ui.*;
|
||||
import io.anuke.arc.scene.ui.TextButton.TextButtonStyle;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.arc.scene.ui.layout.Unit;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.core.GameState.State;
|
||||
import io.anuke.mindustry.core.Platform;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.io.JsonIO;
|
||||
import io.anuke.mindustry.io.MapIO;
|
||||
import io.anuke.mindustry.maps.Map;
|
||||
import io.anuke.mindustry.ui.dialogs.FileChooser;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Block.Icon;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.OverlayFloor;
|
||||
import io.anuke.mindustry.world.blocks.storage.CoreBlock;
|
||||
|
||||
import static io.anuke.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("", "dialog");
|
||||
|
||||
background("dark");
|
||||
|
||||
editor = new MapEditor();
|
||||
view = new MapView(editor);
|
||||
infoDialog = new MapInfoDialog(editor);
|
||||
generateDialog = new MapGenerateDialog(editor);
|
||||
|
||||
menu = new FloatingDialog("$menu");
|
||||
menu.addCloseButton();
|
||||
|
||||
float isize = iconsize;
|
||||
float swidth = 180f;
|
||||
|
||||
menu.cont.table(t -> {
|
||||
t.defaults().size(swidth, 60f).padBottom(5).padRight(5).padLeft(5);
|
||||
|
||||
t.addImageTextButton("$editor.savemap", "icon-floppy-16", isize, this::save);
|
||||
|
||||
t.addImageTextButton("$editor.mapinfo", "icon-pencil", isize, () -> {
|
||||
infoDialog.show();
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.row();
|
||||
|
||||
t.addImageTextButton("$editor.generate", "icon-editor", isize, () -> {
|
||||
generateDialog.show();
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.addImageTextButton("$editor.resize", "icon-resize", isize, () -> {
|
||||
resizeDialog.show();
|
||||
menu.hide();
|
||||
});
|
||||
|
||||
t.row();
|
||||
|
||||
t.addImageTextButton("$editor.import", "icon-load-map", isize, () ->
|
||||
createDialog("$editor.import",
|
||||
"$editor.importmap", "$editor.importmap.description", "icon-load-map", (Runnable)loadDialog::show,
|
||||
"$editor.importfile", "$editor.importfile.description", "icon-file", (Runnable)() ->
|
||||
Platform.instance.showFileChooser("$editor.loadmap", "Map Files", file -> ui.loadAnd(() -> {
|
||||
world.maps.tryCatchMapError(() -> {
|
||||
if(MapIO.isImage(file)){
|
||||
ui.showInfo("$editor.errorimage");
|
||||
}else if(file.extension().equalsIgnoreCase(oldMapExtension)){
|
||||
editor.beginEdit(world.maps.makeLegacyMap(file));
|
||||
}else{
|
||||
editor.beginEdit(MapIO.createMap(file, true));
|
||||
}
|
||||
});
|
||||
}), true, FileChooser.anyMapFiles),
|
||||
|
||||
"$editor.importimage", "$editor.importimage.description", "icon-file-image", (Runnable)() ->
|
||||
Platform.instance.showFileChooser("$loadimage", "Image Files", file ->
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
Pixmap pixmap = new Pixmap(file);
|
||||
editor.beginEdit(pixmap);
|
||||
pixmap.dispose();
|
||||
}catch(Exception e){
|
||||
ui.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
|
||||
Log.err(e);
|
||||
}
|
||||
}), true, FileChooser.pngFiles))
|
||||
);
|
||||
|
||||
t.addImageTextButton("$editor.export", "icon-save-map", isize, () ->
|
||||
Platform.instance.showFileChooser("$editor.savemap", "Map Files", file -> {
|
||||
file = file.parent().child(file.nameWithoutExtension() + "." + mapExtension);
|
||||
FileHandle result = file;
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
if(!editor.getTags().containsKey("name")){
|
||||
editor.getTags().put("name", result.nameWithoutExtension());
|
||||
}
|
||||
MapIO.writeMap(result, editor.createMap(result));
|
||||
}catch(Exception e){
|
||||
ui.showError(Core.bundle.format("editor.errorsave", Strings.parseException(e, true)));
|
||||
Log.err(e);
|
||||
}
|
||||
});
|
||||
}, false, FileChooser.mapFiles));
|
||||
});
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.addImageTextButton("$editor.ingame", "icon-arrow", isize, this::playtest).padTop(-5).size(swidth * 2f + 10, 60f);
|
||||
|
||||
menu.cont.row();
|
||||
|
||||
menu.cont.addImageTextButton("$quit", "icon-back", isize, () -> {
|
||||
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.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
|
||||
Log.err(e);
|
||||
}
|
||||
}));
|
||||
|
||||
setFillParent(true);
|
||||
|
||||
clearChildren();
|
||||
margin(0);
|
||||
shown(this::build);
|
||||
|
||||
update(() -> {
|
||||
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);
|
||||
}
|
||||
|
||||
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
|
||||
doInput();
|
||||
}
|
||||
});
|
||||
|
||||
shown(() -> {
|
||||
|
||||
saved = true;
|
||||
if(!Core.settings.getBool("landscape")) Platform.instance.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.instance::updateRPC);
|
||||
});
|
||||
|
||||
hidden(() -> {
|
||||
editor.clearOp();
|
||||
Platform.instance.updateRPC();
|
||||
if(!Core.settings.getBool("landscape")) Platform.instance.endForceLandscape();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawBackground(float x, float y){
|
||||
drawDefaultBackground(x, y);
|
||||
}
|
||||
|
||||
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());
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
private void save(){
|
||||
String name = editor.getTags().get("name", "").trim();
|
||||
editor.getTags().put("rules", JsonIO.write(state.rules));
|
||||
player.dead = true;
|
||||
|
||||
if(name.isEmpty()){
|
||||
infoDialog.show();
|
||||
Core.app.post(() -> ui.showError("$editor.save.noname"));
|
||||
}else{
|
||||
Map map = world.maps.all().find(m -> m.name().equals(name));
|
||||
if(map != null && !map.custom){
|
||||
handleSaveBuiltin(map);
|
||||
}else{
|
||||
world.maps.saveMap(editor.getTags());
|
||||
ui.showInfoFade("$editor.saved");
|
||||
}
|
||||
}
|
||||
|
||||
menu.hide();
|
||||
saved = true;
|
||||
}
|
||||
|
||||
/** Called when a built-in map save is attempted.*/
|
||||
protected void handleSaveBuiltin(Map map){
|
||||
ui.showError("$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];
|
||||
String iconname = (String)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).size(iconsize).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(0f), Actions.scaleTo(1f, 1f), Actions.fadeIn(0.3f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose(){
|
||||
editor.renderer().dispose();
|
||||
}
|
||||
|
||||
public void beginEditMap(FileHandle file){
|
||||
ui.loadAnd(() -> {
|
||||
try{
|
||||
shownWithMap = true;
|
||||
editor.beginEdit(MapIO.createMap(file, true));
|
||||
show();
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MapView getView(){
|
||||
return view;
|
||||
}
|
||||
|
||||
public void resetSaved(){
|
||||
saved = false;
|
||||
}
|
||||
|
||||
public boolean hasPane(){
|
||||
return Core.scene.getScrollFocus() == pane || Core.scene.getKeyboardFocus() != this;
|
||||
}
|
||||
|
||||
public void build(){
|
||||
float amount = 10f, baseSize = 60f;
|
||||
|
||||
float size = mobile ? (int)(Math.min(Core.graphics.getHeight(), Core.graphics.getWidth()) / amount / Unit.dp.scl(1f)) :
|
||||
Math.min(Core.graphics.getDisplayMode().height / amount, baseSize);
|
||||
|
||||
clearChildren();
|
||||
table(cont -> {
|
||||
cont.left();
|
||||
|
||||
cont.table(mid -> {
|
||||
mid.top();
|
||||
|
||||
Table tools = new Table().top();
|
||||
|
||||
ButtonGroup<ImageButton> group = new ButtonGroup<>();
|
||||
|
||||
Consumer<EditorTool> addTool = tool -> {
|
||||
Table[] lastTable = {null};
|
||||
|
||||
ImageButton button = new ImageButton("icon-" + tool.name() + "-small", "clear-toggle");
|
||||
button.clicked(() -> {
|
||||
view.setTool(tool);
|
||||
if(lastTable[0] != null){
|
||||
lastTable[0].remove();
|
||||
}
|
||||
});
|
||||
button.resizeImage(iconsizesmall);
|
||||
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("dialogDim");
|
||||
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(Core.scene.skin.get("clear-toggle", TextButtonStyle.class));
|
||||
b.add(Core.bundle.get("toolmode." + name)).left();
|
||||
b.row();
|
||||
b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.LIGHT_GRAY).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-menu-large-small", "clear", iconsizesmall, menu::show);
|
||||
|
||||
ImageButton grid = tools.addImageButton("icon-grid-small", "clear-toggle", iconsizesmall, () -> view.setGrid(!view.isGrid())).get();
|
||||
|
||||
addTool.accept(EditorTool.zoom);
|
||||
|
||||
tools.row();
|
||||
|
||||
ImageButton undo = tools.addImageButton("icon-undo-small", "clear", iconsizesmall, editor::undo).get();
|
||||
ImageButton redo = tools.addImageButton("icon-redo-small", "clear", iconsizesmall, editor::redo).get();
|
||||
|
||||
addTool.accept(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.accept(EditorTool.line);
|
||||
addTool.accept(EditorTool.pencil);
|
||||
addTool.accept(EditorTool.eraser);
|
||||
|
||||
tools.row();
|
||||
|
||||
addTool.accept(EditorTool.fill);
|
||||
addTool.accept(EditorTool.spray);
|
||||
|
||||
ImageButton rotate = tools.addImageButton("icon-arrow-16-small", "clear", iconsizesmall, () -> 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("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("white", "clear-toggle-partial");
|
||||
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("underline", t -> {
|
||||
Slider slider = new Slider(0, MapEditor.brushSizes.length - 1, 1, false);
|
||||
slider.moved(f -> editor.brushSize = MapEditor.brushSizes[(int)(float)f]);
|
||||
|
||||
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
|
||||
//TODO these keycode are unusable, tweak later
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
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(Icon.medium);
|
||||
|
||||
if(!Core.atlas.isFound(region)) continue;
|
||||
|
||||
ImageButton button = new ImageButton("white", "clear-toggle");
|
||||
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("underline", extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
|
||||
table.row();
|
||||
table.add(pane).growY().fillX();
|
||||
}
|
||||
}
|
||||
368
core/src/io/anuke/mindustry/editor/MapGenerateDialog.java
Normal file
368
core/src/io/anuke/mindustry/editor/MapGenerateDialog.java
Normal file
@@ -0,0 +1,368 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.function.Supplier;
|
||||
import io.anuke.arc.graphics.Pixmap;
|
||||
import io.anuke.arc.graphics.Pixmap.Format;
|
||||
import io.anuke.arc.graphics.Texture;
|
||||
import io.anuke.arc.scene.ui.Image;
|
||||
import io.anuke.arc.scene.ui.layout.Stack;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.arc.util.Scaling;
|
||||
import io.anuke.arc.util.async.AsyncExecutor;
|
||||
import io.anuke.arc.util.async.AsyncResult;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.*;
|
||||
import io.anuke.mindustry.editor.generation.GenerateFilter.GenerateInput;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.io.MapIO;
|
||||
import io.anuke.mindustry.ui.BorderImage;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class MapGenerateDialog extends FloatingDialog{
|
||||
private final Supplier<GenerateFilter>[] filterTypes = new Supplier[]{
|
||||
NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new,
|
||||
RiverNoiseFilter::new, OreFilter::new, MedianFilter::new, BlendFilter::new
|
||||
};
|
||||
private final MapEditor editor;
|
||||
|
||||
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;
|
||||
|
||||
public MapGenerateDialog(MapEditor editor){
|
||||
super("$editor.generate");
|
||||
this.editor = editor;
|
||||
|
||||
shown(this::setup);
|
||||
addCloseButton();
|
||||
buttons.addButton("$editor.apply", () -> {
|
||||
ui.loadAnd(() -> {
|
||||
apply();
|
||||
hide();
|
||||
});
|
||||
}).size(160f, 64f);
|
||||
buttons.addButton("$editor.randomize", () -> {
|
||||
for(GenerateFilter filter : filters){
|
||||
filter.randomize();
|
||||
}
|
||||
update();
|
||||
}).size(160f, 64f);
|
||||
|
||||
buttons.addImageTextButton("$add", "icon-add", iconsize, this::showAdd).height(64f).width(140f);
|
||||
}
|
||||
|
||||
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("flat", t -> {
|
||||
t.margin(8f);
|
||||
t.stack(new BorderImage(texture){{
|
||||
setScaling(Scaling.fit);
|
||||
}}, new Stack(){{
|
||||
add(new Image("loadDim"));
|
||||
add(new Image("icon-refresh"){{
|
||||
setScaling(Scaling.none);
|
||||
}});
|
||||
visible(() -> generating && !updateEditorOnChange);
|
||||
}}).size(mobile ? 300f : 400f).padRight(6);
|
||||
t.pane(p -> filterTable = p).width(300f).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(){
|
||||
filterTable.clearChildren();
|
||||
filterTable.top();
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
filterTable.table(t -> {
|
||||
t.add(filter.name()).padTop(5).color(Pal.accent).growX().left();
|
||||
|
||||
t.row();
|
||||
|
||||
t.table(b -> {
|
||||
b.left();
|
||||
b.defaults().size(50f);
|
||||
b.addImageButton("icon-refresh-small", iconsizesmall, () -> {
|
||||
filter.randomize();
|
||||
update();
|
||||
});
|
||||
|
||||
b.addImageButton("icon-arrow-up-small", iconsizesmall, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.max(0, idx - 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
b.addImageButton("icon-arrow-down-small", iconsizesmall, () -> {
|
||||
int idx = filters.indexOf(filter);
|
||||
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
b.addImageButton("icon-trash-small", iconsizesmall, () -> {
|
||||
filters.remove(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
});
|
||||
}).growX();
|
||||
}).fillX();
|
||||
filterTable.row();
|
||||
filterTable.table("underline", f -> {
|
||||
f.left();
|
||||
for(FilterOption option : filter.options){
|
||||
option.changed = this::update;
|
||||
|
||||
f.table(t -> {
|
||||
t.left();
|
||||
option.build(t);
|
||||
}).growX().left();
|
||||
f.row();
|
||||
}
|
||||
}).pad(3).padTop(0).width(280f);
|
||||
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(Supplier<GenerateFilter> gen : filterTypes){
|
||||
GenerateFilter filter = gen.get();
|
||||
selection.cont.addButton(filter.name(), () -> {
|
||||
filters.add(filter);
|
||||
rebuildFilters();
|
||||
update();
|
||||
selection.hide();
|
||||
});
|
||||
if(++i % 2 == 0) selection.cont.row();
|
||||
}
|
||||
|
||||
selection.cont.addButton("Default Ores", () -> {
|
||||
int index = 0;
|
||||
for(Block block : new Block[]{Blocks.oreCopper, Blocks.oreCoal, Blocks.oreLead, Blocks.oreTitanium, Blocks.oreThorium}){
|
||||
OreFilter filter = new OreFilter();
|
||||
filter.threshold += index ++ * 0.02f;
|
||||
filter.ore = block;
|
||||
filters.add(filter);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//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.setFilter(filter, editor.width(), editor.height(), 1, (x, y) -> dset(editor.tile(x, y)));
|
||||
//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.begin(editor, 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 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.setFilter(filter, pixmap.getWidth(), pixmap.getHeight(), scaling, (x, y) -> buffer1[x][y]);
|
||||
//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.begin(editor, 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.none);
|
||||
}else{
|
||||
GenTile tile = buffer1[px][py];
|
||||
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.none);
|
||||
}
|
||||
pixmap.drawPixel(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;
|
||||
});
|
||||
}
|
||||
|
||||
public static class GenTile{
|
||||
public byte team, rotation;
|
||||
public short block, floor, ore;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void set(GenTile other){
|
||||
this.floor = other.floor;
|
||||
this.block = other.block;
|
||||
this.ore = other.ore;
|
||||
this.team = other.team;
|
||||
this.rotation = other.rotation;
|
||||
}
|
||||
|
||||
void set(Tile other){
|
||||
set(other.floor(), other.block(), other.overlay(), other.getTeam(), other.rotation());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
77
core/src/io/anuke/mindustry/editor/MapInfoDialog.java
Normal file
77
core/src/io/anuke/mindustry/editor/MapInfoDialog.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.ObjectMap;
|
||||
import io.anuke.arc.scene.ui.TextArea;
|
||||
import io.anuke.arc.scene.ui.TextField;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.core.Platform;
|
||||
import io.anuke.mindustry.game.Rules;
|
||||
import io.anuke.mindustry.ui.dialogs.CustomRulesDialog;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
|
||||
public class MapInfoDialog extends FloatingDialog{
|
||||
private final MapEditor editor;
|
||||
private final WaveInfoDialog waveInfo;
|
||||
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
|
||||
|
||||
public MapInfoDialog(MapEditor editor){
|
||||
super("$editor.mapinfo");
|
||||
this.editor = editor;
|
||||
this.waveInfo = new WaveInfoDialog(editor);
|
||||
|
||||
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", ""), "textarea", 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())).left().width(200f);
|
||||
|
||||
t.row();
|
||||
t.add("$editor.waves").padRight(8).left();
|
||||
t.addButton("$edit", waveInfo::show).left().width(200f);
|
||||
|
||||
name.change();
|
||||
description.change();
|
||||
author.change();
|
||||
|
||||
Platform.instance.addDialog(name, 50);
|
||||
Platform.instance.addDialog(author, 50);
|
||||
Platform.instance.addDialog(description, 1000);
|
||||
t.margin(16f);
|
||||
});
|
||||
}
|
||||
}
|
||||
77
core/src/io/anuke/mindustry/editor/MapLoadDialog.java
Normal file
77
core/src/io/anuke/mindustry/editor/MapLoadDialog.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.scene.ui.*;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.arc.util.Scaling;
|
||||
import io.anuke.mindustry.maps.Map;
|
||||
import io.anuke.mindustry.ui.BorderImage;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class MapLoadDialog extends FloatingDialog{
|
||||
private Map selected = null;
|
||||
|
||||
public MapLoadDialog(Consumer<Map> loader){
|
||||
super("$editor.loadmap");
|
||||
|
||||
shown(this::rebuild);
|
||||
rebuild();
|
||||
|
||||
TextButton button = new TextButton("$load");
|
||||
button.setDisabled(() -> selected == null);
|
||||
button.clicked(() -> {
|
||||
if(selected != null){
|
||||
loader.accept(selected);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.defaults().size(200f, 50f);
|
||||
buttons.addButton("$cancel", this::hide);
|
||||
buttons.add(button);
|
||||
}
|
||||
|
||||
public void rebuild(){
|
||||
cont.clear();
|
||||
if(world.maps.all().size > 0){
|
||||
selected = world.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, "horizontal");
|
||||
pane.setFadeScrollBars(false);
|
||||
|
||||
for(Map map : world.maps.all()){
|
||||
|
||||
TextButton button = new TextButton(map.name(), "toggle");
|
||||
button.add(new BorderImage(map.texture, 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(world.maps.all().size == 0){
|
||||
table.add("$maps.none").center();
|
||||
}else{
|
||||
cont.add("$editor.loadmap");
|
||||
}
|
||||
|
||||
cont.row();
|
||||
cont.add(pane);
|
||||
}
|
||||
|
||||
}
|
||||
166
core/src/io/anuke/mindustry/editor/MapRenderer.java
Normal file
166
core/src/io/anuke/mindustry/editor/MapRenderer.java
Normal file
@@ -0,0 +1,166 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.IntSet;
|
||||
import io.anuke.arc.collection.IntSet.IntSetIterator;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.Texture;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Disposable;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.graphics.IndexedRenderer;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.BlockPart;
|
||||
|
||||
import static io.anuke.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;
|
||||
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();
|
||||
|
||||
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{
|
||||
mesh.draw(idxWall, region,
|
||||
wx * tilesize + wall.offset() + (tilesize - region.getWidth() * Draw.scl) / 2f,
|
||||
wy * tilesize + wall.offset() + (tilesize - region.getHeight() * Draw.scl) / 2f,
|
||||
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl);
|
||||
}
|
||||
}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");
|
||||
}
|
||||
|
||||
mesh.draw(idxDecal, region,
|
||||
wx * tilesize + offsetX, wy * tilesize + offsetY,
|
||||
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
core/src/io/anuke/mindustry/editor/MapResizeDialog.java
Normal file
57
core/src/io/anuke/mindustry/editor/MapResizeDialog.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.function.IntPositionConsumer;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
|
||||
public class MapResizeDialog extends FloatingDialog{
|
||||
private static final int minSize = 50, maxSize = 500, increment = 50;
|
||||
int width, height;
|
||||
|
||||
public MapResizeDialog(MapEditor editor, IntPositionConsumer 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("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("$editor.resize", () -> {
|
||||
cons.accept(width, height);
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
static int move(int value, int direction){
|
||||
return Mathf.clamp((value / increment + direction) * increment, minSize, maxSize);
|
||||
}
|
||||
}
|
||||
75
core/src/io/anuke/mindustry/editor/MapSaveDialog.java
Normal file
75
core/src/io/anuke/mindustry/editor/MapSaveDialog.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.scene.ui.TextButton;
|
||||
import io.anuke.arc.scene.ui.TextField;
|
||||
import io.anuke.mindustry.core.Platform;
|
||||
import io.anuke.mindustry.maps.Map;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
|
||||
import static io.anuke.mindustry.Vars.ui;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class MapSaveDialog extends FloatingDialog{
|
||||
private TextField field;
|
||||
private Consumer<String> listener;
|
||||
|
||||
public MapSaveDialog(Consumer<String> cons){
|
||||
super("$editor.savemap");
|
||||
field = new TextField();
|
||||
listener = cons;
|
||||
|
||||
Platform.instance.addDialog(field);
|
||||
|
||||
shown(() -> {
|
||||
cont.clear();
|
||||
cont.label(() -> {
|
||||
Map map = world.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.accept(field.getText());
|
||||
hide();
|
||||
}
|
||||
});
|
||||
button.setDisabled(this::invalid);
|
||||
buttons.add(button);
|
||||
}
|
||||
|
||||
public void save(){
|
||||
if(!invalid()){
|
||||
listener.accept(field.getText());
|
||||
}else{
|
||||
ui.showError("$editor.failoverwrite");
|
||||
}
|
||||
}
|
||||
|
||||
public void setFieldText(String text){
|
||||
field.setText(text);
|
||||
}
|
||||
|
||||
private boolean invalid(){
|
||||
if(field.getText().isEmpty()){
|
||||
return true;
|
||||
}
|
||||
Map map = world.maps.byName(field.getText());
|
||||
return map != null && !map.custom;
|
||||
}
|
||||
}
|
||||
344
core/src/io/anuke/mindustry/editor/MapView.java
Normal file
344
core/src/io/anuke/mindustry/editor/MapView.java
Normal file
@@ -0,0 +1,344 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.input.GestureDetector;
|
||||
import io.anuke.arc.input.GestureDetector.GestureListener;
|
||||
import io.anuke.arc.input.KeyCode;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.*;
|
||||
import io.anuke.arc.scene.Element;
|
||||
import io.anuke.arc.scene.event.*;
|
||||
import io.anuke.arc.scene.ui.TextField;
|
||||
import io.anuke.arc.scene.ui.layout.Unit;
|
||||
import io.anuke.arc.util.Tmp;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.input.Binding;
|
||||
import io.anuke.mindustry.ui.GridImage;
|
||||
|
||||
import static io.anuke.mindustry.Vars.mobile;
|
||||
import static io.anuke.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;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@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);
|
||||
if(Core.scene.getKeyboardFocus() != null && isDescendantOf(Core.scene.getKeyboardFocus())){
|
||||
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
}
|
||||
Draw.reset();
|
||||
|
||||
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
|
||||
return;
|
||||
}
|
||||
|
||||
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(Unit.dp.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(Unit.dp.scl(3f));
|
||||
Lines.rect(x, y, width, height);
|
||||
Draw.reset();
|
||||
|
||||
ScissorStack.popScissors();
|
||||
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 / Unit.dp.scl(1f) * zoom;
|
||||
clampZoom();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pinchStop(){
|
||||
|
||||
}
|
||||
}
|
||||
51
core/src/io/anuke/mindustry/editor/OperationStack.java
Executable file
51
core/src/io/anuke/mindustry/editor/OperationStack.java
Executable file
@@ -0,0 +1,51 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.collection.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();
|
||||
|
||||
}
|
||||
}
|
||||
268
core/src/io/anuke/mindustry/editor/WaveInfoDialog.java
Normal file
268
core/src/io/anuke/mindustry/editor/WaveInfoDialog.java
Normal file
@@ -0,0 +1,268 @@
|
||||
package io.anuke.mindustry.editor;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.input.KeyCode;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.scene.event.Touchable;
|
||||
import io.anuke.arc.scene.ui.Label;
|
||||
import io.anuke.arc.scene.ui.TextField.TextFieldFilter;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.content.*;
|
||||
import io.anuke.mindustry.game.*;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.io.JsonIO;
|
||||
import io.anuke.mindustry.type.*;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
import static io.anuke.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.getClipboard().setContents(world.maps.writeWaves(groups));
|
||||
dialog.hide();
|
||||
}).disabled(b -> groups == null);
|
||||
dialog.cont.row();
|
||||
dialog.cont.addButton("$waves.load", () -> {
|
||||
try{
|
||||
groups = world.maps.readWaves(Core.app.getClipboard().getContents());
|
||||
buildGroups();
|
||||
}catch(Exception e){
|
||||
ui.showError("$waves.invalid");
|
||||
}
|
||||
dialog.hide();
|
||||
}).disabled(b -> Core.app.getClipboard().getContents() == null || Core.app.getClipboard().getContents().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("clear", main -> {
|
||||
main.pane(t -> table = t).growX().growY().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("clear", m -> {
|
||||
m.add("$waves.preview").color(Color.LIGHT_GRAY).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, false);
|
||||
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("clear", t -> {
|
||||
t.margin(6f).defaults().pad(2).padLeft(5f).growX().left();
|
||||
t.addButton(b -> {
|
||||
b.left();
|
||||
b.addImage(group.type.iconRegion).size(30f).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.isZero(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(5);
|
||||
table.row();
|
||||
}
|
||||
}else{
|
||||
table.add("$editor.default");
|
||||
}
|
||||
|
||||
updateWaves();
|
||||
}
|
||||
|
||||
void showUpdate(SpawnGroup group){
|
||||
FloatingDialog dialog = new FloatingDialog("");
|
||||
dialog.setFillParent(false);
|
||||
int i = 0;
|
||||
for(UnitType type : content.units()){
|
||||
dialog.cont.addButton(t -> {
|
||||
t.left();
|
||||
t.addImage(type.iconRegion).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) dialog.cont.row();
|
||||
}
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
void updateWaves(){
|
||||
preview.clear();
|
||||
preview.top();
|
||||
|
||||
for(int i = start; i < displayed + start; i++){
|
||||
int wave = i;
|
||||
preview.table("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.iconRegion).size(30f).padRight(4);
|
||||
table.add(spawned[j] + "x").color(Color.LIGHT_GRAY).padRight(6);
|
||||
table.row();
|
||||
}
|
||||
}
|
||||
|
||||
if(table.getChildren().size == 1){
|
||||
table.add("$none").color(Pal.remove);
|
||||
}
|
||||
}).width(110f).pad(2f);
|
||||
|
||||
preview.row();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
|
||||
|
||||
public class BlendFilter extends GenerateFilter{
|
||||
float radius = 2f;
|
||||
Block flooronto = Blocks.stone, floor = Blocks.ice;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 10f),
|
||||
new BlockOption("flooronto", () -> flooronto, b -> flooronto = b, floorsOnly),
|
||||
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
if(in.floor == flooronto) return;
|
||||
|
||||
int rad = (int)radius;
|
||||
boolean found = false;
|
||||
|
||||
outer:
|
||||
for(int x = -rad; x <= rad; x++){
|
||||
for(int y = -rad; y <= rad; y++){
|
||||
if(Mathf.dst2(x, y) > rad*rad) continue;
|
||||
|
||||
if(in.tile(in.x + x, in.y + y).floor == flooronto.id){
|
||||
found = true;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(found){
|
||||
in.floor = floor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.mindustry.Vars.content;
|
||||
|
||||
public class DistortFilter extends GenerateFilter{
|
||||
float scl = 40, mag = 5;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 400f),
|
||||
new SliderOption("mag", () -> mag, f -> mag = f, 0.5f, 100f)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
GenTile tile = in.tile(in.x / (in.scaling) + (noise(in.x, in.y, scl, mag) - mag / 2f) / in.scaling, in.y / (in.scaling) + (noise(in.x, in.y + o, scl, mag) - mag / 2f) / in.scaling);
|
||||
|
||||
in.floor = content.block(tile.floor);
|
||||
if(!content.block(tile.block).synthetic() && !in.block.synthetic()) in.block = content.block(tile.block);
|
||||
if(!((Floor)in.floor).isLiquid) in.ore = content.block(tile.ore);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.function.*;
|
||||
import io.anuke.arc.scene.style.TextureRegionDrawable;
|
||||
import io.anuke.arc.scene.ui.Slider;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Block.Icon;
|
||||
import io.anuke.mindustry.world.blocks.*;
|
||||
|
||||
import static io.anuke.mindustry.Vars.updateEditorOnChange;
|
||||
|
||||
public abstract class FilterOption{
|
||||
public static final Predicate<Block> floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full));
|
||||
public static final Predicate<Block> wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full));
|
||||
public static final Predicate<Block> floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
|
||||
public static final Predicate<Block> wallsOptional = b -> b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full)));
|
||||
public static final Predicate<Block> wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
|
||||
public static final Predicate<Block> oresOnly = b -> b instanceof OverlayFloor && Core.atlas.isFound(b.icon(Icon.full));
|
||||
|
||||
public abstract void build(Table table);
|
||||
|
||||
public Runnable changed = () -> {
|
||||
};
|
||||
|
||||
static class SliderOption extends FilterOption{
|
||||
final String name;
|
||||
final FloatProvider getter;
|
||||
final FloatConsumer setter;
|
||||
final float min, max;
|
||||
|
||||
SliderOption(String name, FloatProvider getter, FloatConsumer setter, float min, float max){
|
||||
this.name = name;
|
||||
this.getter = getter;
|
||||
this.setter = setter;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
table.add("$filter.option." + name);
|
||||
table.row();
|
||||
Slider slider = table.addSlider(min, max, (max - min) / 200f, setter).growX().get();
|
||||
slider.setValue(getter.get());
|
||||
if(updateEditorOnChange){
|
||||
slider.changed(changed);
|
||||
}else{
|
||||
slider.released(changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class BlockOption extends FilterOption{
|
||||
final String name;
|
||||
final Supplier<Block> supplier;
|
||||
final Consumer<Block> consumer;
|
||||
final Predicate<Block> filter;
|
||||
|
||||
BlockOption(String name, Supplier<Block> supplier, Consumer<Block> consumer, Predicate<Block> filter){
|
||||
this.name = name;
|
||||
this.supplier = supplier;
|
||||
this.consumer = consumer;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build(Table table){
|
||||
table.addButton(b -> b.addImage(supplier.get().icon(Icon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable())
|
||||
.setRegion(supplier.get() == Blocks.air ? Core.atlas.find("icon-none") : supplier.get().icon(Icon.small))).size(8 * 3), () -> {
|
||||
FloatingDialog dialog = new FloatingDialog("");
|
||||
dialog.setFillParent(false);
|
||||
int i = 0;
|
||||
for(Block block : Vars.content.blocks()){
|
||||
if(!filter.test(block)) continue;
|
||||
|
||||
dialog.cont.addImage(block == Blocks.air ? Core.atlas.find("icon-none-small") : block.icon(Icon.medium)).size(8 * 4).pad(3).get().clicked(() -> {
|
||||
consumer.accept(block);
|
||||
dialog.hide();
|
||||
changed.run();
|
||||
});
|
||||
if(++i % 10 == 0) dialog.cont.row();
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}).pad(4).margin(12f);
|
||||
|
||||
table.add("$filter.option." + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Pack;
|
||||
import io.anuke.arc.util.noise.RidgedPerlin;
|
||||
import io.anuke.arc.util.noise.Simplex;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.MapEditor;
|
||||
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
public abstract class GenerateFilter{
|
||||
protected transient float o = (float)(Math.random() * 10000000.0);
|
||||
protected transient long seed;
|
||||
protected transient GenerateInput in;
|
||||
|
||||
public FilterOption[] options;
|
||||
|
||||
protected abstract void apply();
|
||||
|
||||
protected float noise(float x, float y, float scl, float mag){
|
||||
return (float)in.noise.octaveNoise2D(1f, 0f, 1f / scl, x + o, y + o) * mag;
|
||||
}
|
||||
|
||||
protected float noise(float x, float y, float scl, float mag, float octaves, float persistence){
|
||||
return (float)in.noise.octaveNoise2D(octaves, persistence, 1f / scl, x + o, y + o) * mag;
|
||||
}
|
||||
|
||||
protected float rnoise(float x, float y, float scl, float mag){
|
||||
return in.pnoise.getValue((int)(x + o), (int)(y + o), 1f / scl) * mag;
|
||||
}
|
||||
|
||||
public void randomize(){
|
||||
seed = Mathf.random(99999999);
|
||||
}
|
||||
|
||||
protected float chance(){
|
||||
return Mathf.randomSeed(Pack.longInt(in.x, in.y + (int)o));
|
||||
}
|
||||
|
||||
public void options(FilterOption... options){
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public String name(){
|
||||
return Core.bundle.get("filter." + getClass().getSimpleName().toLowerCase().replace("filter", ""), getClass().getSimpleName().replace("Filter", ""));
|
||||
}
|
||||
|
||||
public final void apply(GenerateInput in){
|
||||
this.in = in;
|
||||
apply();
|
||||
//remove extra ores on liquids
|
||||
if(((Floor)in.floor).isLiquid){
|
||||
in.ore = Blocks.air;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GenerateInput{
|
||||
public Floor srcfloor;
|
||||
public Block srcblock;
|
||||
public Block srcore;
|
||||
public int x, y, width, height, scaling;
|
||||
|
||||
public MapEditor editor;
|
||||
public Block floor, block, ore;
|
||||
|
||||
Simplex noise = new Simplex();
|
||||
RidgedPerlin pnoise = new RidgedPerlin(0, 1);
|
||||
TileProvider buffer;
|
||||
|
||||
public void begin(MapEditor editor, int x, int y, Block floor, Block block, Block ore){
|
||||
this.editor = editor;
|
||||
this.floor = this.srcfloor = (Floor)floor;
|
||||
this.block = this.srcblock = block;
|
||||
this.ore = srcore = ore;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void setFilter(GenerateFilter filter, int width, int height, int scaling, TileProvider buffer){
|
||||
this.buffer = buffer;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.scaling = scaling;
|
||||
noise.setSeed(filter.seed);
|
||||
pnoise.setSeed((int)(filter.seed + 1));
|
||||
}
|
||||
|
||||
GenTile tile(float x, float y){
|
||||
return buffer.get(Mathf.clamp((int)x, 0, width - 1), Mathf.clamp((int)y, 0, height - 1));
|
||||
}
|
||||
|
||||
public interface TileProvider{
|
||||
GenTile get(int x, int y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.arc.collection.IntArray;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
|
||||
import static io.anuke.mindustry.Vars.content;
|
||||
|
||||
public class MedianFilter extends GenerateFilter{
|
||||
float radius = 2;
|
||||
float percentile = 0.5f;
|
||||
IntArray blocks = new IntArray(), floors = new IntArray();
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 12f),
|
||||
new SliderOption("percentile", () -> percentile, f -> percentile = f, 0f, 1f)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
int rad = (int)radius;
|
||||
blocks.clear();
|
||||
floors.clear();
|
||||
for(int x = -rad; x <= rad; x++){
|
||||
for(int y = -rad; y <= rad; y++){
|
||||
if(Mathf.dst2(x, y) > rad*rad) continue;
|
||||
|
||||
GenTile tile = in.tile(in.x + x, in.y + y);
|
||||
blocks.add(tile.block);
|
||||
floors.add(tile.floor);
|
||||
}
|
||||
}
|
||||
|
||||
floors.sort();
|
||||
blocks.sort();
|
||||
|
||||
int index = Math.min((int)(floors.size * percentile), floors.size - 1);
|
||||
int floor = floors.get(index), block = blocks.get(index);
|
||||
|
||||
in.floor = content.block(floor);
|
||||
if(!content.block(block).synthetic() && !in.block.synthetic()) in.block = content.block(block);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
|
||||
|
||||
public class NoiseFilter extends GenerateFilter{
|
||||
float scl = 40, threshold = 0.5f, octaves = 3f, falloff = 0.5f;
|
||||
Block floor = Blocks.stone, block = Blocks.rocks;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
|
||||
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
|
||||
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
|
||||
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
|
||||
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
|
||||
new BlockOption("wall", () -> block, b -> block = b, wallsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
float noise = noise(in.x, in.y, scl, 1f, octaves, falloff);
|
||||
|
||||
if(noise > threshold){
|
||||
in.floor = floor;
|
||||
if(wallsOnly.test(in.srcblock)) in.block = block;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
core/src/io/anuke/mindustry/editor/generation/OreFilter.java
Normal file
32
core/src/io/anuke/mindustry/editor/generation/OreFilter.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.oresOnly;
|
||||
|
||||
public class OreFilter extends GenerateFilter{
|
||||
public float scl = 50, threshold = 0.72f, octaves = 3f, falloff = 0.4f;
|
||||
public Block ore = Blocks.oreCopper;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
|
||||
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
|
||||
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
|
||||
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
|
||||
new BlockOption("ore", () -> ore, b -> ore = b, oresOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
float noise = noise(in.x, in.y, scl, 1f, octaves, falloff);
|
||||
|
||||
if(noise > threshold){
|
||||
in.ore = ore;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
|
||||
|
||||
public class RiverNoiseFilter extends GenerateFilter{
|
||||
float scl = 40, threshold = 0f, threshold2 = 0.1f;
|
||||
Block floor = Blocks.water, floor2 = Blocks.deepwater, block = Blocks.sandRocks;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
|
||||
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
|
||||
new SliderOption("threshold2", () -> threshold2, f -> threshold2 = f, 0f, 1f),
|
||||
new BlockOption("block", () -> block, b -> block = b, wallsOnly),
|
||||
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
|
||||
new BlockOption("floor2", () -> floor2, b -> floor2 = b, floorsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
float noise = rnoise(in.x, in.y, scl, 1f);
|
||||
|
||||
if(noise >= threshold){
|
||||
in.floor = floor;
|
||||
|
||||
if(in.srcblock.solid){
|
||||
in.block = block;
|
||||
}
|
||||
|
||||
if(noise >= threshold2){
|
||||
in.floor = floor2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.*;
|
||||
|
||||
public class ScatterFilter extends GenerateFilter{
|
||||
float chance = 0.1f;
|
||||
Block flooronto = Blocks.air, floor = Blocks.air, block = Blocks.air;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("chance", () -> chance, f -> chance = f, 0f, 1f),
|
||||
new BlockOption("flooronto", () -> flooronto, b -> flooronto = b, floorsOptional),
|
||||
new BlockOption("floor", () -> floor, b -> floor = b, floorsOptional),
|
||||
new BlockOption("block", () -> block, b -> block = b, wallsOresOptional)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
|
||||
if(block != Blocks.air && (in.srcfloor == flooronto || flooronto == Blocks.air) && in.srcblock == Blocks.air && chance() <= chance){
|
||||
if(!block.isOverlay()){
|
||||
in.block = block;
|
||||
}else{
|
||||
in.ore = block;
|
||||
}
|
||||
}
|
||||
|
||||
if(floor != Blocks.air && (in.srcfloor == flooronto || flooronto == Blocks.air) && chance() <= chance){
|
||||
in.floor = floor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.anuke.mindustry.editor.generation;
|
||||
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
|
||||
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
|
||||
|
||||
public class TerrainFilter extends GenerateFilter{
|
||||
float scl = 40, threshold = 0.9f, octaves = 3f, falloff = 0.5f, magnitude = 1f, circleScl = 2.1f;
|
||||
Block floor = Blocks.stone, block = Blocks.rocks;
|
||||
|
||||
{
|
||||
options(
|
||||
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
|
||||
new SliderOption("mag", () -> magnitude, f -> magnitude = f, 0f, 2f),
|
||||
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
|
||||
new SliderOption("circle-scale", () -> circleScl, f -> circleScl = f, 0f, 3f),
|
||||
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
|
||||
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
|
||||
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
|
||||
new BlockOption("wall", () -> block, b -> block = b, wallsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
float noise = noise(in.x, in.y, scl, magnitude, octaves, falloff) + Mathf.dst((float)in.x / in.editor.width(), (float)in.y / in.editor.height(), 0.5f, 0.5f) * circleScl;
|
||||
|
||||
in.floor = floor;
|
||||
in.ore = Blocks.air;
|
||||
|
||||
if(noise >= threshold){
|
||||
in.block = block;
|
||||
}else{
|
||||
in.block = Blocks.air;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.entities.BulletEntity;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class Bullet extends BulletEntity{
|
||||
public Timer timer = new Timer(3);
|
||||
|
||||
public Bullet(BulletType type, Entity owner, float x, float y, float angle){
|
||||
super(type, owner, angle);
|
||||
set(x, y);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void draw(){
|
||||
//interpolate position linearly at low tick speeds
|
||||
if(SyncEntity.isSmoothing()){
|
||||
x += threads.getFramesSinceUpdate() * velocity.x;
|
||||
y += threads.getFramesSinceUpdate() * velocity.y;
|
||||
|
||||
type.draw(this);
|
||||
|
||||
x -= threads.getFramesSinceUpdate() * velocity.x;
|
||||
y -= threads.getFramesSinceUpdate() * velocity.y;
|
||||
}else{
|
||||
type.draw(this);
|
||||
}
|
||||
}
|
||||
|
||||
public float drawSize(){
|
||||
return 8;
|
||||
}
|
||||
|
||||
public boolean collidesTiles(){
|
||||
return owner instanceof Enemy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
super.update();
|
||||
|
||||
if (collidesTiles()) {
|
||||
world.raycastEach(world.toTile(lastX), world.toTile(lastY), world.toTile(x), world.toTile(y), (x, y) -> {
|
||||
|
||||
Tile tile = world.tile(x, y);
|
||||
if (tile == null) return false;
|
||||
tile = tile.target();
|
||||
|
||||
if (tile.entity != null && tile.entity.collide(this) && !tile.entity.dead) {
|
||||
tile.entity.collision(this);
|
||||
remove();
|
||||
type.hit(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDamage(){
|
||||
return damage == -1 ? type.damage : damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bullet add(){
|
||||
return super.add(bulletGroup);
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import io.anuke.mindustry.entities.effect.DamageArea;
|
||||
import io.anuke.mindustry.entities.effect.EMP;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.BaseBulletType;
|
||||
import io.anuke.ucore.graphics.Lines;
|
||||
import io.anuke.ucore.util.Angles;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.graphics.Fx.*;
|
||||
|
||||
public abstract class BulletType extends BaseBulletType<Bullet>{
|
||||
|
||||
public static final BulletType
|
||||
|
||||
none = new BulletType(0f, 0){
|
||||
public void draw(Bullet b){}
|
||||
},
|
||||
stone = new BulletType(1.5f, 2){
|
||||
public void draw(Bullet b){
|
||||
Draw.colorl(0.64f);
|
||||
Draw.rect("blank", b.x, b.y, 2f, 2f);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
iron = new BulletType(1.7f, 2){
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.GRAY);
|
||||
Draw.rect("bullet", b.x, b.y, b.angle());
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
chain = new BulletType(2f, 8){
|
||||
public void draw(Bullet b){
|
||||
Draw.color(whiteOrange);
|
||||
Draw.rect("chainbullet", b.x, b.y, b.angle());
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
sniper = new BulletType(3f, 25){
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.LIGHT_GRAY);
|
||||
Lines.stroke(1f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), 3f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 4)){
|
||||
Effects.effect(Fx.railsmoke, b.x, b.y);
|
||||
}
|
||||
}
|
||||
},
|
||||
emp = new BulletType(1.6f, 8){
|
||||
{
|
||||
lifetime = 50f;
|
||||
hitsize = 6f;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
float rad = 6f + Mathf.sin(Timers.time(), 5f, 2f);
|
||||
|
||||
Draw.color(Color.SKY);
|
||||
Lines.circle(b.x, b.y, 4f);
|
||||
Draw.rect("circle", b.x, b.y, rad, rad);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 2)){
|
||||
Effects.effect(Fx.empspark, b.x + Mathf.range(2), b.y + Mathf.range(2));
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Timers.run(5f, ()-> new EMP(b.x, b.y, b.getDamage()).add());
|
||||
Effects.effect(Fx.empshockwave, b);
|
||||
Effects.shake(3f, 3f, b);
|
||||
}
|
||||
},
|
||||
//TODO better visuals for shell
|
||||
shell = new BulletType(1.1f, 60){
|
||||
{
|
||||
lifetime = 110f;
|
||||
hitsize = 11f;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
float rad = 8f;
|
||||
Draw.color(Color.ORANGE);
|
||||
Draw.color(Color.GRAY);
|
||||
Draw.rect("circle", b.x, b.y, rad, rad);
|
||||
rad += Mathf.sin(Timers.time(), 3f, 1f);
|
||||
Draw.color(Color.ORANGE);
|
||||
Draw.rect("circle", b.x, b.y, rad/1.7f, rad/1.7f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 7)){
|
||||
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.shake(3f, 3f, b);
|
||||
|
||||
Effects.effect(Fx.shellsmoke, b);
|
||||
Effects.effect(Fx.shellexplosion, b);
|
||||
|
||||
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 25f, (int)(damage * 2f/3f));
|
||||
}
|
||||
},
|
||||
flak = new BulletType(2.9f, 8) {
|
||||
|
||||
public void init(Bullet b) {
|
||||
b.velocity.scl(Mathf.random(0.6f, 1f));
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 7)){
|
||||
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
|
||||
}
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
Draw.color(Color.GRAY);
|
||||
Lines.stroke(3f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
|
||||
Lines.stroke(1.5f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), 5f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity) {
|
||||
Effects.effect(shellsmoke, b);
|
||||
for(int i = 0; i < 3; i ++){
|
||||
Bullet bullet = new Bullet(flakspark, b.owner, hitx, hity, b.angle() + Mathf.range(120f));
|
||||
bullet.add();
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b) {
|
||||
hit(b, b.x, b.y);
|
||||
}
|
||||
},
|
||||
flakspark = new BulletType(2f, 2) {
|
||||
{
|
||||
drag = 0.05f;
|
||||
}
|
||||
|
||||
public void init(Bullet b) {
|
||||
b.velocity.scl(Mathf.random(0.6f, 1f));
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
Draw.color(Color.LIGHT_GRAY, Color.GRAY, b.fin());
|
||||
Lines.stroke(2f - b.fin());
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
titanshell = new BulletType(1.8f, 38){
|
||||
{
|
||||
lifetime = 70f;
|
||||
hitsize = 15f;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
Draw.color(whiteOrange);
|
||||
Draw.rect("titanshell", b.x, b.y, b.angle());
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 4)){
|
||||
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.shake(3f, 3f, b);
|
||||
|
||||
Effects.effect(Fx.shellsmoke, b);
|
||||
Effects.effect(Fx.shockwaveSmall, b);
|
||||
|
||||
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 50f, (int)(damage * 2f/3f));
|
||||
}
|
||||
},
|
||||
yellowshell = new BulletType(1.2f, 20){
|
||||
{
|
||||
lifetime = 60f;
|
||||
hitsize = 11f;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
Draw.color(whiteYellow);
|
||||
Draw.rect("titanshell", b.x, b.y, b.angle());
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 4)){
|
||||
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
|
||||
}
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.shake(3f, 3f, b);
|
||||
|
||||
Effects.effect(Fx.shellsmoke, b);
|
||||
Effects.effect(Fx.shockwaveSmall, b);
|
||||
|
||||
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 25f, (int)(damage * 2f/3f));
|
||||
}
|
||||
},
|
||||
blast = new BulletType(1.1f, 90){
|
||||
{
|
||||
lifetime = 0f;
|
||||
hitsize = 8f;
|
||||
speed = 0f;
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.shake(3f, 3f, b);
|
||||
|
||||
Effects.effect(Fx.blastsmoke, b);
|
||||
Effects.effect(Fx.blastexplosion, b);
|
||||
|
||||
//TODO remove translation() usage
|
||||
Angles.circleVectors(30, 6f, (nx, ny) -> {
|
||||
float ang = Mathf.atan2(nx, ny);
|
||||
Bullet o = new Bullet(blastshot, b.owner, b.x + nx, b.y + ny, ang).add();
|
||||
o.damage = b.damage/9;
|
||||
});
|
||||
}
|
||||
|
||||
public void draw(Bullet b){}
|
||||
},
|
||||
blastshot = new BulletType(1.6f, 6){
|
||||
{
|
||||
lifetime = 7f;
|
||||
}
|
||||
public void draw(Bullet b){}
|
||||
},
|
||||
small = new BulletType(1.5f, 2){
|
||||
public void draw(Bullet b){
|
||||
Draw.color(glowy);
|
||||
Draw.rect("shot", b.x, b.y, b.angle() - 45);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
smallSlow = new BulletType(1.2f, 2){
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.ORANGE);
|
||||
Draw.rect("shot", b.x, b.y, b.angle() - 45);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
purple = new BulletType(1.6f, 2){
|
||||
Color color = new Color(0x8b5ec9ff);
|
||||
|
||||
public void draw(Bullet b){
|
||||
Draw.color(color);
|
||||
Draw.rect("bullet", b.x, b.y, b.angle());
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
flame = new BulletType(0.7f, 5){ //for turrets
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.YELLOW, Color.SCARLET, b.time/lifetime);
|
||||
float size = 6f-b.time/lifetime*5f;
|
||||
Draw.rect("circle", b.x, b.y, size, size);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
plasmaflame = new BulletType(0.8f, 17){
|
||||
{
|
||||
lifetime = 65f;
|
||||
}
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.valueOf("efa66c"), Color.valueOf("72deaf"), b.time/lifetime);
|
||||
float size = 7f-b.time/lifetime*6f;
|
||||
Draw.rect("circle", b.x, b.y, size, size);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
flameshot = new BulletType(0.5f, 3){ //for enemies
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.ORANGE, Color.SCARLET, b.time/lifetime);
|
||||
float size = 6f-b.time/lifetime*5f;
|
||||
Draw.rect("circle", b.x, b.y, size, size);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
shot = new BulletType(2.7f, 5){
|
||||
{
|
||||
lifetime = 40;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
Draw.color(Color.WHITE, lightOrange, b.fout()/2f + 0.25f);
|
||||
Lines.stroke(1.5f);
|
||||
Lines.lineAngle(b.x, b.y, b.angle(), 3f);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
spread = new BulletType(2.4f, 9) {
|
||||
{
|
||||
lifetime = 70;
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
float size = 3f - b.fin()*1f;
|
||||
|
||||
Draw.color(Color.PURPLE, Color.WHITE, 0.8f);
|
||||
Lines.stroke(1f);
|
||||
Lines.circle(b.x, b.y, size);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
cluster = new BulletType(4.5f, 12){
|
||||
{
|
||||
lifetime = 60;
|
||||
drag = 0.05f;
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
Lines.stroke(2f);
|
||||
Draw.color(lightOrange, Color.WHITE, 0.4f);
|
||||
Lines.poly(b.x, b.y, 3, 1.6f, b.angle());
|
||||
Lines.stroke(1f);
|
||||
Draw.color(Color.WHITE, lightOrange, b.fin()/2f);
|
||||
Draw.alpha(b.fin());
|
||||
Lines.spikes(b.x, b.y, 1.5f, 2f, 6);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void despawned(Bullet b){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.shake(1.5f, 1.5f, b);
|
||||
|
||||
Effects.effect(Fx.clusterbomb, b);
|
||||
|
||||
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 35f, damage);
|
||||
}
|
||||
},
|
||||
vulcan = new BulletType(4.5f, 12) {
|
||||
{
|
||||
lifetime = 50;
|
||||
}
|
||||
|
||||
public void init(Bullet b) {
|
||||
Timers.reset(b, "smoke", Mathf.random(4f));
|
||||
}
|
||||
|
||||
public void draw(Bullet b){
|
||||
Draw.color(lightGray);
|
||||
Lines.stroke(1f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void update(Bullet b){
|
||||
if(b.timer.get(0, 4)){
|
||||
Effects.effect(Fx.chainsmoke, b.x, b.y);
|
||||
}
|
||||
}
|
||||
},
|
||||
shockshell = new BulletType(5.5f, 11) {
|
||||
|
||||
{
|
||||
drag = 0.03f;
|
||||
lifetime = 30f;
|
||||
}
|
||||
|
||||
public void init(Bullet b) {
|
||||
b.velocity.scl(Mathf.random(0.5f, 1f));
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
Draw.color(Color.WHITE, Color.ORANGE, b.fin());
|
||||
Lines.stroke(2f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), b.fout()*5f);
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
public void despawned(Bullet b) {
|
||||
hit(b);
|
||||
}
|
||||
|
||||
public void hit(Bullet b, float hitx, float hity) {
|
||||
for(int i = 0; i < 4; i ++){
|
||||
Bullet bullet = new Bullet(scrap, b.owner, b.x, b.y, b.angle() + Mathf.range(80f));
|
||||
bullet.add();
|
||||
}
|
||||
}
|
||||
},
|
||||
scrap = new BulletType(2f, 3) {
|
||||
{
|
||||
drag = 0.06f;
|
||||
lifetime = 30f;
|
||||
}
|
||||
|
||||
public void init(Bullet b) {
|
||||
b.velocity.scl(Mathf.random(0.5f, 1f));
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
Draw.color(Color.WHITE, Color.ORANGE, b.fin());
|
||||
Lines.stroke(1f);
|
||||
Lines.lineAngleCenter(b.x, b.y, b.angle(), b.fout()*4f);
|
||||
Draw.reset();
|
||||
}
|
||||
},
|
||||
beamlaser = new BulletType(0.001f, 38) {
|
||||
float length = 230f;
|
||||
{
|
||||
drawSize = length*2f+20f;
|
||||
lifetime = 15f;
|
||||
}
|
||||
|
||||
public void init(Bullet b) {
|
||||
DamageArea.damageLine(b.owner, Fx.beamhit, b.x, b.y, b.angle(), length, damage);
|
||||
}
|
||||
|
||||
public void draw(Bullet b) {
|
||||
float f = b.fout()*1.5f;
|
||||
|
||||
Draw.color(beam);
|
||||
Draw.rect("circle", b.x, b.y, 6f*f, 6f*f);
|
||||
Lines.stroke(3f * f);
|
||||
Lines.lineAngle(b.x, b.y, b.angle(), length);
|
||||
|
||||
Lines.stroke(2f * f);
|
||||
Lines.lineAngle(b.x, b.y, b.angle(), length + 6f);
|
||||
Lines.stroke(1f * f);
|
||||
Lines.lineAngle(b.x, b.y, b.angle(), length + 12f);
|
||||
|
||||
Draw.color(beamLight);
|
||||
Lines.stroke(1.5f * f);
|
||||
Draw.rect("circle", b.x, b.y, 3f*f, 3f*f);
|
||||
Lines.lineAngle(b.x, b.y, b.angle(), length);
|
||||
}
|
||||
};
|
||||
|
||||
private BulletType(float speed, int damage){
|
||||
this.speed = speed;
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(Bullet b, float hitx, float hity){
|
||||
Effects.effect(Fx.hit, hitx, hity);
|
||||
}
|
||||
}
|
||||
278
core/src/io/anuke/mindustry/entities/Damage.java
Normal file
278
core/src/io/anuke/mindustry/entities/Damage.java
Normal file
@@ -0,0 +1,278 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.annotations.Annotations.Struct;
|
||||
import io.anuke.arc.collection.GridBits;
|
||||
import io.anuke.arc.collection.IntQueue;
|
||||
import io.anuke.arc.function.*;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.*;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.content.Bullets;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Effects.Effect;
|
||||
import io.anuke.mindustry.entities.bullet.Bullet;
|
||||
import io.anuke.mindustry.entities.effect.Fire;
|
||||
import io.anuke.mindustry.entities.effect.Lightning;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.gen.PropCell;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.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();
|
||||
|
||||
/** 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.none, 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, x, y, Mathf.random(360f)));
|
||||
}
|
||||
|
||||
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){
|
||||
tr.trns(angle, length);
|
||||
IntPositionConsumer collider = (cx, cy) -> {
|
||||
Tile tile = world.ltile(cx, cy);
|
||||
if(tile != null && tile.entity != null && tile.getTeamID() != team.ordinal() && tile.entity.collide(hitter)){
|
||||
tile.entity.collision(hitter);
|
||||
hitter.getBulletType().hit(hitter, tile.worldx(), tile.worldy());
|
||||
}
|
||||
};
|
||||
|
||||
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
|
||||
collider.accept(cx, cy);
|
||||
if(large){
|
||||
for(Point2 p : Geometry.d4){
|
||||
collider.accept(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;
|
||||
|
||||
Consumer<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, Predicate<Unit> predicate, Consumer<Unit> acceptor){
|
||||
Consumer<Unit> cons = entity -> {
|
||||
if(!predicate.test(entity)) return;
|
||||
|
||||
entity.hitbox(hitrect);
|
||||
if(!hitrect.overlaps(rect)){
|
||||
return;
|
||||
}
|
||||
entity.damage(damage);
|
||||
acceptor.accept(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){
|
||||
Consumer<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()));
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
166
core/src/io/anuke/mindustry/entities/Effects.java
Normal file
166
core/src/io/anuke/mindustry/entities/Effects.java
Normal file
@@ -0,0 +1,166 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Position;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.entities.impl.EffectEntity;
|
||||
import io.anuke.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 = 1000f;
|
||||
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);
|
||||
}
|
||||
|
||||
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, Consumer<EffectContainer> cons){
|
||||
if(innerContainer == null) innerContainer = new EffectContainer();
|
||||
if(time <= lifetime){
|
||||
innerContainer.set(id, color, time, lifetime, rotation, x, y, data);
|
||||
cons.accept(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);
|
||||
}
|
||||
}
|
||||
89
core/src/io/anuke/mindustry/entities/Entities.java
Executable file
89
core/src/io/anuke/mindustry/entities/Entities.java
Executable file
@@ -0,0 +1,89 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.collection.IntMap;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.graphics.Camera;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.mindustry.entities.traits.DrawTrait;
|
||||
import io.anuke.mindustry.entities.traits.Entity;
|
||||
|
||||
import static io.anuke.mindustry.Vars.collisions;
|
||||
|
||||
public class Entities{
|
||||
private static final Array<EntityGroup<?>> groupArray = new Array<>();
|
||||
private static final IntMap<EntityGroup<?>> groups = new IntMap<>();
|
||||
private static final Rectangle viewport = new Rectangle();
|
||||
private static final boolean clip = true;
|
||||
private static int count = 0;
|
||||
|
||||
public static void clear(){
|
||||
for(EntityGroup group : groupArray){
|
||||
group.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static EntityGroup<?> getGroup(int id){
|
||||
return groups.get(id);
|
||||
}
|
||||
|
||||
public static Array<EntityGroup<?>> getAllGroups(){
|
||||
return groupArray;
|
||||
}
|
||||
|
||||
public static <T extends Entity> EntityGroup<T> addGroup(Class<T> type){
|
||||
return addGroup(type, true);
|
||||
}
|
||||
|
||||
public static <T extends Entity> EntityGroup<T> addGroup(Class<T> type, boolean useTree){
|
||||
EntityGroup<T> group = new EntityGroup<>(type, useTree);
|
||||
groups.put(group.getID(), group);
|
||||
groupArray.add(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public static void update(EntityGroup<?> group){
|
||||
group.updateEvents();
|
||||
|
||||
if(group.useTree()){
|
||||
collisions.updatePhysics(group);
|
||||
}
|
||||
|
||||
for(Entity e : group.all()){
|
||||
e.update();
|
||||
}
|
||||
}
|
||||
|
||||
public static int countInBounds(EntityGroup<?> group){
|
||||
count = 0;
|
||||
draw(group, e -> true, e -> count++);
|
||||
return count;
|
||||
}
|
||||
|
||||
public static void draw(EntityGroup<?> group){
|
||||
draw(group, e -> true);
|
||||
}
|
||||
|
||||
public static <T extends DrawTrait> void draw(EntityGroup<?> group, Predicate<T> toDraw){
|
||||
draw(group, toDraw, DrawTrait::draw);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends DrawTrait> void draw(EntityGroup<?> group, Predicate<T> toDraw, Consumer<T> cons){
|
||||
if(clip){
|
||||
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 : group.all()){
|
||||
if(!(e instanceof DrawTrait) || !toDraw.test((T)e) || !e.isAdded()) continue;
|
||||
DrawTrait draw = (DrawTrait)e;
|
||||
|
||||
if(!clip || viewport.overlaps(draw.getX() - draw.drawSize()/2f, draw.getY() - draw.drawSize()/2f, draw.drawSize(), draw.drawSize())){
|
||||
cons.accept((T)e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
236
core/src/io/anuke/mindustry/entities/EntityCollisions.java
Normal file
236
core/src/io/anuke/mindustry/entities/EntityCollisions.java
Normal file
@@ -0,0 +1,236 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.*;
|
||||
import io.anuke.mindustry.entities.traits.Entity;
|
||||
import io.anuke.mindustry.entities.traits.SolidTrait;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
import static io.anuke.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
202
core/src/io/anuke/mindustry/entities/EntityGroup.java
Normal file
202
core/src/io/anuke/mindustry/entities/EntityGroup.java
Normal file
@@ -0,0 +1,202 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.collection.IntMap;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.math.geom.QuadTree;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.mindustry.entities.traits.Entity;
|
||||
|
||||
public class EntityGroup<T extends Entity>{
|
||||
private static int lastid;
|
||||
private final boolean useTree;
|
||||
private final int id;
|
||||
private final Class<T> type;
|
||||
private final Array<T> entityArray = new Array<>(false, 16);
|
||||
private final Array<T> entitiesToRemove = new Array<>(false, 16);
|
||||
private final Array<T> entitiesToAdd = new Array<>(false, 16);
|
||||
private IntMap<T> map;
|
||||
private QuadTree tree;
|
||||
private Consumer<T> removeListener;
|
||||
private Consumer<T> addListener;
|
||||
|
||||
public EntityGroup(Class<T> type, boolean useTree){
|
||||
this.useTree = useTree;
|
||||
this.id = lastid++;
|
||||
this.type = type;
|
||||
|
||||
if(useTree){
|
||||
tree = new QuadTree<>(new Rectangle(0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean useTree(){
|
||||
return useTree;
|
||||
}
|
||||
|
||||
public void setRemoveListener(Consumer<T> removeListener){
|
||||
this.removeListener = removeListener;
|
||||
}
|
||||
|
||||
public void setAddListener(Consumer<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.accept(check);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void intersect(float x, float y, float width, float height, Consumer<? super T> out){
|
||||
//don't waste time for empty groups
|
||||
if(isEmpty()) return;
|
||||
tree().getIntersect(out, x, y, width, height);
|
||||
}
|
||||
|
||||
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(Predicate<T> pred){
|
||||
int count = 0;
|
||||
for(int i = 0; i < entityArray.size; i++){
|
||||
if(pred.test(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.accept(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.accept(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(){
|
||||
for(T entity : entityArray)
|
||||
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(Predicate<T> pred){
|
||||
|
||||
for(int i = 0; i < entityArray.size; i++){
|
||||
if(pred.test(entityArray.get(i))) return entityArray.get(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the logic-only array for iteration. */
|
||||
public Array<T> all(){
|
||||
return entityArray;
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.graphics.Shaders;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.NetEvents;
|
||||
import io.anuke.mindustry.resource.Mech;
|
||||
import io.anuke.mindustry.resource.Upgrade;
|
||||
import io.anuke.mindustry.resource.Weapon;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Blocks;
|
||||
import io.anuke.ucore.core.*;
|
||||
import io.anuke.ucore.entities.SolidEntity;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.util.Angles;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
import io.anuke.ucore.util.Translator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class Player extends SyncEntity{
|
||||
static final float speed = 1.1f;
|
||||
static final float dashSpeed = 1.8f;
|
||||
|
||||
static final int timerDash = 0;
|
||||
static final int timerShootLeft = 1;
|
||||
static final int timerShootRight = 2;
|
||||
static final int timerRegen = 3;
|
||||
|
||||
public String name = "name";
|
||||
public boolean isAndroid;
|
||||
public boolean isAdmin;
|
||||
public Color color = new Color();
|
||||
|
||||
public Weapon weaponLeft = Weapon.blaster;
|
||||
public Weapon weaponRight = Weapon.blaster;
|
||||
public Mech mech = Mech.standard;
|
||||
|
||||
public float targetAngle = 0f;
|
||||
public boolean dashing = false;
|
||||
|
||||
public int clientid = -1;
|
||||
public boolean isLocal = false;
|
||||
public Timer timer = new Timer(4);
|
||||
|
||||
private Vector2 movement = new Vector2();
|
||||
private Translator tr = new Translator();
|
||||
|
||||
public Player(){
|
||||
hitbox.setSize(5);
|
||||
hitboxTile.setSize(4f);
|
||||
|
||||
maxhealth = 200;
|
||||
heal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void damage(float amount){
|
||||
if(debug || isAndroid) return;
|
||||
|
||||
health -= amount;
|
||||
if(health <= 0 && !dead && isLocal){ //remote players don't die normally
|
||||
onDeath();
|
||||
dead = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collides(SolidEntity other){
|
||||
if(other instanceof Bullet){
|
||||
Bullet b = (Bullet)other;
|
||||
if(!state.friendlyFire && b.owner instanceof Player){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !isDead() && super.collides(other) && !isAndroid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(){
|
||||
dead = true;
|
||||
if(Net.active()){
|
||||
NetEvents.handlePlayerDeath();
|
||||
}
|
||||
|
||||
Effects.effect(Fx.explosion, this);
|
||||
Effects.shake(4f, 5f, this);
|
||||
Effects.sound("die", this);
|
||||
|
||||
control.setRespawnTime(respawnduration);
|
||||
ui.hudfrag.fadeRespawn(true);
|
||||
}
|
||||
|
||||
/**called when a remote player death event is recieved*/
|
||||
public void doRespawn(){
|
||||
dead = true;
|
||||
Effects.effect(Fx.explosion, this);
|
||||
Effects.shake(4f, 5f, this);
|
||||
Effects.sound("die", this);
|
||||
|
||||
Timers.run(respawnduration + 5f, () -> {
|
||||
heal();
|
||||
set(world.getSpawnX(), world.getSpawnY());
|
||||
interpolator.target.set(x, y);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSmooth(){
|
||||
if((debug && (!showPlayer || !showUI)) || (isAndroid && isLocal) || dead) return;
|
||||
boolean snap = snapCamera && Settings.getBool("smoothcam") && Settings.getBool("pixelate") && isLocal;
|
||||
|
||||
String part = isAndroid ? "ship" : "mech";
|
||||
|
||||
Shaders.outline.color.set(getColor());
|
||||
Shaders.outline.lighten = 0f;
|
||||
Shaders.outline.region = Draw.region(part + "-" + mech.name);
|
||||
|
||||
Shaders.outline.apply();
|
||||
|
||||
if(!isAndroid) {
|
||||
for (int i : Mathf.signs) {
|
||||
Weapon weapon = i < 0 ? weaponLeft : weaponRight;
|
||||
tr.trns(angle - 90, 3*i, 2);
|
||||
float w = i > 0 ? -8 : 8;
|
||||
if(snap){
|
||||
Draw.rect(weapon.name + "-equip", (int)x + tr.x, (int)y + tr.y, w, 8, angle - 90);
|
||||
}else{
|
||||
Draw.rect(weapon.name + "-equip", x + tr.x, y + tr.y, w, 8, angle - 90);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(snap){
|
||||
Draw.rect(part + "-" + mech.name, (int)x, (int)y, angle-90);
|
||||
}else{
|
||||
Draw.rect(part + "-" + mech.name, x, y, angle-90);
|
||||
}
|
||||
|
||||
Graphics.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
if(!isLocal || isAndroid){
|
||||
if(isAndroid && isLocal){
|
||||
angle = Mathf.slerpDelta(angle, targetAngle, 0.2f);
|
||||
}
|
||||
if(!isLocal) interpolate();
|
||||
return;
|
||||
}
|
||||
|
||||
if(isDead()) return;
|
||||
|
||||
Tile tile = world.tileWorld(x, y);
|
||||
|
||||
//if player is in solid block
|
||||
if(tile != null && ((tile.floor().liquid && tile.block() == Blocks.air) || tile.solid())) {
|
||||
damage(health + 1); //die instantly
|
||||
}
|
||||
|
||||
if(ui.chatfrag.chatOpen()) return;
|
||||
|
||||
dashing = Inputs.keyDown("dash");
|
||||
|
||||
float speed = dashing ? (debug ? Player.dashSpeed * 5f : Player.dashSpeed) : Player.speed;
|
||||
|
||||
if(health < maxhealth && timer.get(timerRegen, 20))
|
||||
health ++;
|
||||
|
||||
health = Mathf.clamp(health, -1, maxhealth);
|
||||
|
||||
movement.set(0, 0);
|
||||
|
||||
float xa = Inputs.getAxis("move_x");
|
||||
float ya = Inputs.getAxis("move_y");
|
||||
if(Math.abs(xa) < 0.3) xa = 0;
|
||||
if(Math.abs(ya) < 0.3) ya = 0;
|
||||
|
||||
movement.y += ya*speed;
|
||||
movement.x += xa*speed;
|
||||
|
||||
boolean shooting = !Inputs.keyDown("dash") && Inputs.keyDown("shoot") && control.input().recipe == null
|
||||
&& !ui.hasMouse() && !control.input().onConfigurable();
|
||||
|
||||
if(shooting){
|
||||
weaponLeft.update(player, true);
|
||||
weaponRight.update(player, false);
|
||||
}
|
||||
|
||||
if(dashing && timer.get(timerDash, 3) && movement.len() > 0){
|
||||
Effects.effect(Fx.dashsmoke, x + Angles.trnsx(angle + 180f, 3f), y + Angles.trnsy(angle + 180f, 3f));
|
||||
}
|
||||
|
||||
movement.limit(speed);
|
||||
|
||||
if(!noclip){
|
||||
move(movement.x*Timers.delta(), movement.y*Timers.delta());
|
||||
}else{
|
||||
x += movement.x*Timers.delta();
|
||||
y += movement.y*Timers.delta();
|
||||
}
|
||||
|
||||
if(!shooting){
|
||||
if(!movement.isZero())
|
||||
angle = Mathf.slerpDelta(angle, movement.angle(), 0.13f);
|
||||
}else{
|
||||
float angle = Angles.mouseAngle(x, y);
|
||||
this.angle = Mathf.slerpDelta(this.angle, angle, 0.1f);
|
||||
}
|
||||
|
||||
x = Mathf.clamp(x, 0, world.width() * tilesize);
|
||||
y = Mathf.clamp(y, 0, world.height() * tilesize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Player add(){
|
||||
return add(playerGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Player{" + id + ", android=" + isAndroid + ", local=" + isLocal + ", " + x + ", " + y + "}\n";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSpawn(ByteBuffer buffer) {
|
||||
buffer.put((byte)name.getBytes().length);
|
||||
buffer.put(name.getBytes());
|
||||
buffer.put(weaponLeft.id);
|
||||
buffer.put(weaponRight.id);
|
||||
buffer.put(isAndroid ? 1 : (byte)0);
|
||||
buffer.put(isAdmin ? 1 : (byte)0);
|
||||
buffer.putInt(Color.rgba8888(color));
|
||||
buffer.putFloat(x);
|
||||
buffer.putFloat(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSpawn(ByteBuffer buffer) {
|
||||
byte nlength = buffer.get();
|
||||
byte[] n = new byte[nlength];
|
||||
buffer.get(n);
|
||||
name = new String(n);
|
||||
weaponLeft = (Weapon) Upgrade.getByID(buffer.get());
|
||||
weaponRight = (Weapon) Upgrade.getByID(buffer.get());
|
||||
isAndroid = buffer.get() == 1;
|
||||
isAdmin = buffer.get() == 1;
|
||||
color.set(buffer.getInt());
|
||||
x = buffer.getFloat();
|
||||
y = buffer.getFloat();
|
||||
setNet(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer data) {
|
||||
if(Net.client() || isLocal) {
|
||||
data.putFloat(x);
|
||||
data.putFloat(y);
|
||||
}else{
|
||||
data.putFloat(interpolator.target.x);
|
||||
data.putFloat(interpolator.target.y);
|
||||
}
|
||||
data.putFloat(angle);
|
||||
data.putShort((short)health);
|
||||
data.put((byte)(dashing ? 1 : 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer data, long time) {
|
||||
float x = data.getFloat();
|
||||
float y = data.getFloat();
|
||||
float angle = data.getFloat();
|
||||
short health = data.getShort();
|
||||
byte dashing = data.get();
|
||||
|
||||
this.health = health;
|
||||
this.dashing = dashing == 1;
|
||||
|
||||
interpolator.read(this.x, this.y, x, y, angle, time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interpolate() {
|
||||
super.interpolate();
|
||||
|
||||
Interpolator i = interpolator;
|
||||
|
||||
float tx = x + Angles.trnsx(angle + 180f, 4f);
|
||||
float ty = y + Angles.trnsy(angle + 180f, 4f);
|
||||
|
||||
if(isAndroid && i.target.dst(i.last) > 2f && timer.get(timerDash, 1)){
|
||||
Effects.effect(Fx.dashsmoke, tx, ty);
|
||||
}
|
||||
|
||||
if(dashing && !dead && timer.get(timerDash, 3) && i.target.dst(i.last) > 1f){
|
||||
Effects.effect(Fx.dashsmoke, tx, ty);
|
||||
}
|
||||
}
|
||||
|
||||
public Color getColor(){
|
||||
return color;
|
||||
}
|
||||
}
|
||||
81
core/src/io/anuke/mindustry/entities/Predict.java
Normal file
81
core/src/io/anuke/mindustry/entities/Predict.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.entities.traits.TargetTrait;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
public enum StatusEffect{
|
||||
none;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.math.Vector3;
|
||||
import com.badlogic.gdx.utils.ObjectIntMap;
|
||||
import com.badlogic.gdx.utils.TimeUtils;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.DestructibleEntity;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static io.anuke.mindustry.Vars.threads;
|
||||
|
||||
public abstract class SyncEntity extends DestructibleEntity{
|
||||
private static ObjectIntMap<Class<? extends SyncEntity>> writeSizes = new ObjectIntMap<>();
|
||||
|
||||
protected transient Interpolator interpolator = new Interpolator();
|
||||
|
||||
//smoothed position/angle
|
||||
private Vector3 spos = new Vector3();
|
||||
|
||||
public float angle;
|
||||
|
||||
static{
|
||||
setWriteSize(Enemy.class, 4 + 4 + 2 + 2);
|
||||
setWriteSize(Player.class, 4 + 4 + 4 + 2 + 1);
|
||||
}
|
||||
|
||||
public static boolean isSmoothing(){
|
||||
return threads.isEnabled() && threads.getFPS() <= Gdx.graphics.getFramesPerSecond() / 2f;
|
||||
}
|
||||
|
||||
public abstract void writeSpawn(ByteBuffer data);
|
||||
public abstract void readSpawn(ByteBuffer data);
|
||||
|
||||
public abstract void write(ByteBuffer data);
|
||||
public abstract void read(ByteBuffer data, long time);
|
||||
|
||||
public void interpolate(){
|
||||
interpolator.update();
|
||||
|
||||
x = interpolator.pos.x;
|
||||
y = interpolator.pos.y;
|
||||
angle = interpolator.angle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void draw(){
|
||||
final float x = this.x, y = this.y, angle = this.angle;
|
||||
|
||||
//interpolates data at low tick speeds.
|
||||
if(isSmoothing()){
|
||||
if(Vector2.dst(spos.x, spos.y, x, y) > 128){
|
||||
spos.set(x, y, angle);
|
||||
}
|
||||
|
||||
this.x = spos.x = Mathf.lerpDelta(spos.x, x, 0.2f);
|
||||
this.y = spos.y = Mathf.lerpDelta(spos.y, y, 0.2f);
|
||||
this.angle = spos.z = Mathf.slerpDelta(spos.z, angle, 0.3f);
|
||||
}
|
||||
|
||||
drawSmooth();
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = angle;
|
||||
}
|
||||
|
||||
public Vector3 getDrawPosition(){
|
||||
return isSmoothing() ? spos : spos.set(x, y, angle);
|
||||
}
|
||||
|
||||
public void drawSmooth(){}
|
||||
|
||||
public int getWriteSize(){
|
||||
return getWriteSize(getClass());
|
||||
}
|
||||
|
||||
public static int getWriteSize(Class<? extends SyncEntity> type){
|
||||
int i = writeSizes.get(type, -1);
|
||||
if(i == -1) throw new RuntimeException("Write size for class \"" + type + "\" is not defined!");
|
||||
return i;
|
||||
}
|
||||
|
||||
protected static void setWriteSize(Class<? extends SyncEntity> type, int size){
|
||||
writeSizes.put(type, size);
|
||||
}
|
||||
|
||||
public <T extends SyncEntity> T setNet(float x, float y){
|
||||
set(x, y);
|
||||
interpolator.target.set(x, y);
|
||||
interpolator.last.set(x, y);
|
||||
interpolator.spacing = 1f;
|
||||
interpolator.time = 0f;
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
public static class Interpolator {
|
||||
//used for movement
|
||||
public Vector2 target = new Vector2();
|
||||
public Vector2 last = new Vector2();
|
||||
public float targetrot;
|
||||
public float spacing = 1f;
|
||||
public float time;
|
||||
|
||||
//current state
|
||||
public Vector2 pos = new Vector2();
|
||||
public float angle;
|
||||
|
||||
public void read(float cx, float cy, float x, float y, float angle, long sent){
|
||||
targetrot = angle;
|
||||
time = 0f;
|
||||
last.set(cx, cy);
|
||||
target.set(x, y);
|
||||
spacing = Math.min(Math.max(((TimeUtils.timeSinceMillis(sent) / 1000f) * 60f), 4f), 10);
|
||||
}
|
||||
|
||||
public void update(){
|
||||
|
||||
time += 1f / spacing * Math.min(Timers.delta(), 1f);
|
||||
|
||||
time = Mathf.clamp(time, 0, 2f);
|
||||
|
||||
Mathf.lerp2(pos.set(last), target, time);
|
||||
|
||||
angle = Mathf.slerpDelta(angle, targetrot, 0.6f);
|
||||
|
||||
if(target.dst(pos) > 128){
|
||||
pos.set(target);
|
||||
last.set(target);
|
||||
time = 0f;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.NetEvents;
|
||||
import io.anuke.mindustry.resource.Item;
|
||||
import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.types.Wall;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tileGroup;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class TileEntity extends Entity{
|
||||
public Tile tile;
|
||||
public int[] items = new int[Item.getAllItems().size];
|
||||
public Timer timer;
|
||||
public float health;
|
||||
public boolean dead = false;
|
||||
public boolean added;
|
||||
|
||||
/**Sets this tile entity data to this tile, and adds it if necessary.*/
|
||||
public TileEntity init(Tile tile, boolean added){
|
||||
this.tile = tile;
|
||||
this.added = added;
|
||||
x = tile.worldx();
|
||||
y = tile.worldy();
|
||||
|
||||
health = tile.block().health;
|
||||
|
||||
timer = new Timer(tile.block().timers);
|
||||
|
||||
if(added){
|
||||
add();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void write(DataOutputStream stream) throws IOException{
|
||||
|
||||
}
|
||||
|
||||
public void read(DataInputStream stream) throws IOException{
|
||||
|
||||
}
|
||||
|
||||
public void readNetwork(DataInputStream stream, float elapsed) throws IOException{
|
||||
read(stream);
|
||||
}
|
||||
|
||||
public void onDeath(){
|
||||
onDeath(false);
|
||||
}
|
||||
|
||||
public void onDeath(boolean force){
|
||||
if(Net.server()){
|
||||
NetEvents.handleBlockDestroyed(this);
|
||||
}
|
||||
|
||||
if(!Net.active() || Net.server() || force){
|
||||
|
||||
if(!dead) {
|
||||
dead = true;
|
||||
Block block = tile.block();
|
||||
|
||||
block.onDestroyed(tile);
|
||||
|
||||
world.removeBlock(tile);
|
||||
remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void collision(Bullet other){
|
||||
damage(other.getDamage());
|
||||
}
|
||||
|
||||
public void damage(int damage){
|
||||
if(dead) return;
|
||||
|
||||
int amount = tile.block().handleDamage(tile, damage);
|
||||
health -= amount;
|
||||
if(health <= 0) onDeath();
|
||||
|
||||
if(Net.server()){
|
||||
NetEvents.handleBlockDamaged(this);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean collide(Bullet other){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
synchronized (Tile.tileSetLock) {
|
||||
if (health != 0 && health < tile.block().health && !(tile.block() instanceof Wall) &&
|
||||
Mathf.chance(0.009f * Timers.delta() * (1f - health / tile.block().health))) {
|
||||
|
||||
Effects.effect(Fx.smoke, x + Mathf.range(4), y + Mathf.range(4));
|
||||
}
|
||||
|
||||
if (health <= 0) {
|
||||
onDeath();
|
||||
}
|
||||
|
||||
tile.block().update(tile);
|
||||
}
|
||||
}
|
||||
|
||||
public int totalItems(){
|
||||
int sum = 0;
|
||||
for(int i = 0; i < items.length; i ++){
|
||||
sum += items[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public int getItem(Item item){
|
||||
return items[item.id];
|
||||
}
|
||||
|
||||
public boolean hasItem(Item item){
|
||||
return getItem(item) > 0;
|
||||
}
|
||||
|
||||
public boolean hasItem(Item item, int amount){
|
||||
return getItem(item) >= amount;
|
||||
}
|
||||
|
||||
public void addItem(Item item, int amount){
|
||||
items[item.id] += amount;
|
||||
}
|
||||
|
||||
public void removeItem(Item item, int amount){
|
||||
items[item.id] -= amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TileEntity add(){
|
||||
return add(tileGroup);
|
||||
}
|
||||
}
|
||||
221
core/src/io/anuke/mindustry/entities/Units.java
Normal file
221
core/src/io/anuke/mindustry/entities/Units.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package io.anuke.mindustry.entities;
|
||||
|
||||
import io.anuke.arc.collection.EnumSet;
|
||||
import io.anuke.arc.function.Consumer;
|
||||
import io.anuke.arc.function.Predicate;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.mindustry.entities.traits.TargetTrait;
|
||||
import io.anuke.mindustry.entities.type.*;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.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;
|
||||
|
||||
/**
|
||||
* 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, world.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, Predicate<Tile> pred){
|
||||
return world.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, Predicate<Tile> pred){
|
||||
if(team == Team.none) return null;
|
||||
|
||||
for(Team enemy : state.teams.enemiesOf(team)){
|
||||
TileEntity entity = world.indexer.findTile(enemy, x, y, range, pred);
|
||||
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, Predicate<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, Predicate<Unit> unitPred, Predicate<Tile> tilePred){
|
||||
if(team == Team.none) 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, Predicate<Unit> predicate){
|
||||
if(team == Team.none) return null;
|
||||
|
||||
result = null;
|
||||
cdist = 0f;
|
||||
|
||||
nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> {
|
||||
if(e.isDead() || !predicate.test(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, Predicate<Unit> predicate){
|
||||
result = null;
|
||||
cdist = 0f;
|
||||
|
||||
nearby(team, x, y, range, e -> {
|
||||
if(!predicate.test(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, Consumer<Unit> cons){
|
||||
unitGroups[team.ordinal()].intersect(x, y, width, height, cons);
|
||||
playerGroup.intersect(x, y, width, height, player -> {
|
||||
if(player.getTeam() == team){
|
||||
cons.accept(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units in a circle around this position. */
|
||||
public static void nearby(Team team, float x, float y, float radius, Consumer<Unit> cons){
|
||||
unitGroups[team.ordinal()].intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
|
||||
if(unit.withinDst(x, y, radius)){
|
||||
cons.accept(unit);
|
||||
}
|
||||
});
|
||||
|
||||
playerGroup.intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
|
||||
if(unit.getTeam() == team && unit.withinDst(x, y, radius)){
|
||||
cons.accept(unit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units in a rectangle. */
|
||||
public static void nearby(float x, float y, float width, float height, Consumer<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, Consumer<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, Consumer<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.accept(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterates over all units that are enemies of this team. */
|
||||
public static void nearbyEnemies(Team team, Rectangle rect, Consumer<Unit> cons){
|
||||
nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons);
|
||||
}
|
||||
|
||||
/** Iterates over all units. */
|
||||
public static void all(Consumer<Unit> cons){
|
||||
for(Team team : Team.all){
|
||||
unitGroups[team.ordinal()].all().each(cons);
|
||||
}
|
||||
|
||||
playerGroup.all().each(cons);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.Effects.Effect;
|
||||
|
||||
//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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.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;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
319
core/src/io/anuke/mindustry/entities/bullet/Bullet.java
Normal file
319
core/src/io/anuke/mindustry/entities/bullet/Bullet.java
Normal file
@@ -0,0 +1,319 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.arc.util.pooling.Pool.Poolable;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.effect.Lightning;
|
||||
import io.anuke.mindustry.entities.impl.SolidEntity;
|
||||
import io.anuke.mindustry.entities.traits.*;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.mindustry.Vars.bulletGroup;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** Internal use only. */
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void createBullet(BulletType type, float x, float y, float angle){
|
||||
create(type, null, Team.none, x, y, angle);
|
||||
}
|
||||
|
||||
/** ok */
|
||||
@Remote(called = Loc.server, unreliable = true)
|
||||
public static void createBullet(BulletType type, Team team, float x, float y, float angle){
|
||||
create(type, null, team, x, y, angle);
|
||||
}
|
||||
|
||||
public Entity getOwner(){
|
||||
return owner;
|
||||
}
|
||||
|
||||
public boolean collidesTiles(){
|
||||
return type.collidesTiles;
|
||||
}
|
||||
|
||||
public void supress(){
|
||||
supressCollision = true;
|
||||
supressOnce = true;
|
||||
}
|
||||
|
||||
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 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;
|
||||
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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
160
core/src/io/anuke/mindustry/entities/bullet/BulletType.java
Normal file
160
core/src/io/anuke/mindustry/entities/bullet/BulletType.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.math.Angles;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.content.StatusEffects;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.Effects.Effect;
|
||||
import io.anuke.mindustry.entities.effect.Lightning;
|
||||
import io.anuke.mindustry.entities.traits.TargetTrait;
|
||||
import io.anuke.mindustry.game.Content;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.type.ContentType;
|
||||
import io.anuke.mindustry.type.StatusEffect;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
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;
|
||||
/** Extra inaccuracy when firing. */
|
||||
public float inaccuracy = 0f;
|
||||
/** How many bullets get created per ammo item/liquid. */
|
||||
public float ammoMultiplier = 1f;
|
||||
/** 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 * 1f;
|
||||
/** 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());
|
||||
|
||||
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());
|
||||
|
||||
if(fragBullet != null || splashDamageRadius > 0){
|
||||
hit(b);
|
||||
}
|
||||
|
||||
for(int i = 0; i < lightining; i++){
|
||||
Lightning.create(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);
|
||||
if(target != null){
|
||||
b.velocity().setAngle(Mathf.slerpDelta(b.velocity().angle(), b.angleTo(target), 0.08f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentType getContentType(){
|
||||
return ContentType.bullet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.math.geom.Rectangle;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Units;
|
||||
|
||||
public abstract 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;
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.Fill;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.arc.math.geom.Point2;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.effect.Fire;
|
||||
import io.anuke.mindustry.entities.effect.Puddle;
|
||||
import io.anuke.mindustry.type.Liquid;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class LiquidBulletType extends BulletType{
|
||||
Liquid liquid;
|
||||
|
||||
public LiquidBulletType(Liquid liquid){
|
||||
super(3.5f, 0);
|
||||
this.liquid = liquid;
|
||||
|
||||
lifetime = 74f;
|
||||
status = liquid.effect;
|
||||
statusDuration = 90f;
|
||||
despawnEffect = Fx.none;
|
||||
hitEffect = Fx.hitLiquid;
|
||||
smokeEffect = Fx.none;
|
||||
shootEffect = Fx.none;
|
||||
drag = 0.009f;
|
||||
knockback = 0.55f;
|
||||
}
|
||||
|
||||
@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 + Mathf.randomSeedRange(b.id, 0.1f));
|
||||
|
||||
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, 5f);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
core/src/io/anuke/mindustry/entities/bullet/MassDriverBolt.java
Normal file
106
core/src/io/anuke/mindustry/entities/bullet/MassDriverBolt.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.math.Angles;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.world.blocks.distribution.MassDriver.DriverBulletData;
|
||||
|
||||
import static io.anuke.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(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(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(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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.anuke.mindustry.entities.bullet;
|
||||
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.content.Fx;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.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;
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Effects.Effect;
|
||||
import io.anuke.ucore.entities.DestructibleEntity;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.SolidEntity;
|
||||
import io.anuke.ucore.function.Consumer;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Physics;
|
||||
import io.anuke.ucore.util.Translator;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class DamageArea{
|
||||
private static Rectangle rect = new Rectangle();
|
||||
private static Translator tr = new Translator();
|
||||
|
||||
//only for entities, not tiles (yet!)
|
||||
public static void damageLine(Entity owner, Effect effect, float x, float y, float angle, float length, int damage){
|
||||
tr.trns(angle, length);
|
||||
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;
|
||||
|
||||
Consumer<SolidEntity> cons = e -> {
|
||||
if(e == owner || (e instanceof Player && ((Player)e).isAndroid)) return;
|
||||
DestructibleEntity enemy = (DestructibleEntity) e;
|
||||
Rectangle other = enemy.hitbox.getRect(enemy.x, enemy.y);
|
||||
other.y -= expand;
|
||||
other.x -= expand;
|
||||
other.width += expand * 2;
|
||||
other.height += expand * 2;
|
||||
|
||||
Vector2 vec = Physics.raycastRect(x, y, x2, y2, other);
|
||||
|
||||
if (vec != null) {
|
||||
Effects.effect(effect, vec.x, vec.y);
|
||||
enemy.damage(damage);
|
||||
}
|
||||
};
|
||||
|
||||
Entities.getNearby(enemyGroup, rect, cons);
|
||||
if(state.friendlyFire) Entities.getNearby(playerGroup, rect, cons);
|
||||
}
|
||||
|
||||
public static void damageEntities(float x, float y, float radius, int damage){
|
||||
damage(true, x, y, radius, damage);
|
||||
|
||||
for(Player player : playerGroup.all()){
|
||||
if(player.isAndroid) continue;
|
||||
int amount = calculateDamage(x, y, player.x, player.y, radius, damage);
|
||||
player.damage(amount);
|
||||
}
|
||||
}
|
||||
|
||||
public static void damage(boolean enemies, float x, float y, float radius, int damage){
|
||||
Consumer<SolidEntity> cons = entity -> {
|
||||
DestructibleEntity enemy = (DestructibleEntity)entity;
|
||||
if(enemy.distanceTo(x, y) > radius || (entity instanceof Player && ((Player)entity).isAndroid)){
|
||||
return;
|
||||
}
|
||||
int amount = calculateDamage(x, y, enemy.x, enemy.y, radius, damage);
|
||||
enemy.damage(amount);
|
||||
};
|
||||
|
||||
if(enemies){
|
||||
Entities.getNearby(enemyGroup, x, y, radius*2, cons);
|
||||
}else{
|
||||
int trad = (int)(radius / tilesize);
|
||||
for(int dx = -trad; dx <= trad; dx ++){
|
||||
for(int dy= -trad; dy <= trad; dy ++){
|
||||
Tile tile = world.tile(Mathf.scl2(x, tilesize) + dx, Mathf.scl2(y, tilesize) + dy);
|
||||
if(tile != null && tile.entity != null && Vector2.dst(dx, dy, 0, 0) <= trad){
|
||||
int amount = calculateDamage(x, y, tile.worldx(), tile.worldy(), radius, damage);
|
||||
tile.entity.damage(amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Entities.getNearby(playerGroup, x, y, radius*2, cons);
|
||||
}
|
||||
}
|
||||
|
||||
static int calculateDamage(float x, float y, float tx, float ty, float radius, int damage){
|
||||
float dist = Vector2.dst(x, y, tx, ty);
|
||||
float scaled = 1f - dist/radius;
|
||||
return (int)(damage * scaled);
|
||||
}
|
||||
}
|
||||
36
core/src/io/anuke/mindustry/entities/effect/Decal.java
Normal file
36
core/src/io/anuke/mindustry/entities/effect/Decal.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.impl.TimedEntity;
|
||||
import io.anuke.mindustry.entities.traits.BelowLiquidTrait;
|
||||
import io.anuke.mindustry.entities.traits.DrawTrait;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
|
||||
import static io.anuke.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 8200f;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.types.PowerAcceptor;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.entities.TimedEntity;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.graphics.Lines;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Translator;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
import static io.anuke.mindustry.Vars.world;
|
||||
|
||||
public class EMP extends TimedEntity{
|
||||
static final int maxTargets = 8;
|
||||
static Array<Tile> array = new Array<>();
|
||||
static Translator tr = new Translator();
|
||||
|
||||
int radius = 4;
|
||||
int damage = 6;
|
||||
Array<Tile> targets = new Array<>(maxTargets);
|
||||
|
||||
public EMP(float x, float y, int damage){
|
||||
this.damage = damage;
|
||||
set(x, y);
|
||||
|
||||
lifetime = 30f;
|
||||
|
||||
int worldx = Mathf.scl2(x, tilesize);
|
||||
int worldy = Mathf.scl2(y, tilesize);
|
||||
|
||||
array.clear();
|
||||
|
||||
for(int dx = -radius; dx <= radius; dx ++){
|
||||
for(int dy = -radius; dy <= radius; dy ++){
|
||||
if(Vector2.dst(dx, dy, 0, 0) < radius){
|
||||
Tile tile = world.tile(worldx + dx, worldy + dy);
|
||||
|
||||
if(tile != null && tile.block().destructible){
|
||||
array.add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array.shuffle();
|
||||
|
||||
for(int i = 0; i < array.size && i < maxTargets; i ++){
|
||||
Tile tile = array.get(i);
|
||||
targets.add(tile);
|
||||
|
||||
if(tile != null && tile.block() instanceof PowerAcceptor){
|
||||
PowerAcceptor p = (PowerAcceptor)tile.block();
|
||||
p.setPower(tile, 0f);
|
||||
tile.entity.damage((int)(damage*2f)); //extra damage
|
||||
}
|
||||
|
||||
if(tile == null) continue;
|
||||
|
||||
//entity may be null here, after the block is dead!
|
||||
Effects.effect(Fx.empspark, tile.worldx(), tile.worldy());
|
||||
if(tile.entity != null) tile.entity.damage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawOver(){
|
||||
Draw.color(Color.SKY);
|
||||
|
||||
for(int i = 0; i < targets.size; i ++){
|
||||
Tile target = targets.get(i);
|
||||
|
||||
drawLine(target.worldx(), target.worldy());
|
||||
|
||||
float rad = 5f*fout();
|
||||
Draw.rect("circle", target.worldx(), target.worldy(), rad, rad);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 14 - targets.size; i ++){
|
||||
tr.trns(Mathf.randomSeed(i + id*77)*360f, radius * tilesize);
|
||||
drawLine(x + tr.x, y + tr.y);
|
||||
}
|
||||
|
||||
Lines.stroke(fout()*2f);
|
||||
Lines.poly(x, y, 34, radius * tilesize);
|
||||
|
||||
Draw.reset();
|
||||
}
|
||||
|
||||
private void drawLine(float targetx, float targety){
|
||||
int joints = 3;
|
||||
float r = 3f;
|
||||
float lastx = x, lasty = y;
|
||||
|
||||
for(int seg = 0; seg < joints; seg ++){
|
||||
float dx = Mathf.range(r),
|
||||
dy = Mathf.range(r);
|
||||
|
||||
float frac = (seg+1f)/joints;
|
||||
|
||||
float tx = (targetx - x)*frac + x + dx,
|
||||
ty = (targety - y)*frac + y + dy;
|
||||
|
||||
drawLaser(lastx, lasty, tx, ty);
|
||||
|
||||
lastx = tx;
|
||||
lasty = ty;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLaser(float x, float y, float x2, float y2){
|
||||
Lines.stroke(fout() * 2f);
|
||||
Lines.line(x, y, x2, y2);
|
||||
}
|
||||
}
|
||||
216
core/src/io/anuke/mindustry/entities/effect/Fire.java
Normal file
216
core/src/io/anuke/mindustry/entities/effect/Fire.java
Normal file
@@ -0,0 +1,216 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.collection.IntMap;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Geometry;
|
||||
import io.anuke.arc.math.geom.Point2;
|
||||
import io.anuke.arc.util.*;
|
||||
import io.anuke.mindustry.content.*;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.impl.TimedEntity;
|
||||
import io.anuke.mindustry.entities.traits.SaveTrait;
|
||||
import io.anuke.mindustry.entities.traits.SyncTrait;
|
||||
import io.anuke.mindustry.entities.type.TileEntity;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.world.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static io.anuke.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())){
|
||||
map.get(tile.pos()).time += intensity * Time.delta();
|
||||
}
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
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, x, y, Mathf.random(360f));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.mindustry.Vars;
|
||||
import io.anuke.mindustry.entities.Effects;
|
||||
import io.anuke.mindustry.entities.Effects.Effect;
|
||||
import io.anuke.mindustry.entities.Effects.EffectRenderer;
|
||||
import io.anuke.mindustry.entities.impl.EffectEntity;
|
||||
import io.anuke.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
core/src/io/anuke/mindustry/entities/effect/ItemTransfer.java
Normal file
120
core/src/io/anuke/mindustry/entities/effect/ItemTransfer.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.math.Interpolation;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.Position;
|
||||
import io.anuke.arc.math.geom.Vector2;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.impl.TimedEntity;
|
||||
import io.anuke.mindustry.entities.traits.DrawTrait;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
import io.anuke.mindustry.type.Item;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.mindustry.Vars.effectGroup;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
134
core/src/io/anuke/mindustry/entities/effect/Lightning.java
Normal file
134
core/src/io/anuke/mindustry/entities/effect/Lightning.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.collection.Array;
|
||||
import io.anuke.arc.collection.IntSet;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.*;
|
||||
import io.anuke.arc.math.*;
|
||||
import io.anuke.arc.math.geom.*;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.content.Bullets;
|
||||
import io.anuke.mindustry.entities.EntityGroup;
|
||||
import io.anuke.mindustry.entities.Units;
|
||||
import io.anuke.mindustry.entities.bullet.Bullet;
|
||||
import io.anuke.mindustry.entities.impl.TimedEntity;
|
||||
import io.anuke.mindustry.entities.traits.DrawTrait;
|
||||
import io.anuke.mindustry.entities.traits.TimeTrait;
|
||||
import io.anuke.mindustry.entities.type.Unit;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.graphics.Pal;
|
||||
|
||||
import static io.anuke.mindustry.Vars.bulletGroup;
|
||||
|
||||
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<Position> 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(lastSeed++, team, color, damage, x, y, targetAngle, length);
|
||||
}
|
||||
|
||||
/** Do not invoke! */
|
||||
@Remote(called = Loc.server)
|
||||
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();
|
||||
|
||||
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)));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
312
core/src/io/anuke/mindustry/entities/effect/Puddle.java
Normal file
312
core/src/io/anuke/mindustry/entities/effect/Puddle.java
Normal file
@@ -0,0 +1,312 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.annotations.Annotations.Loc;
|
||||
import io.anuke.annotations.Annotations.Remote;
|
||||
import io.anuke.arc.collection.IntMap;
|
||||
import io.anuke.arc.graphics.Color;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.Fill;
|
||||
import io.anuke.arc.math.Angles;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.arc.math.geom.*;
|
||||
import io.anuke.arc.util.Time;
|
||||
import io.anuke.arc.util.pooling.Pool.Poolable;
|
||||
import io.anuke.arc.util.pooling.Pools;
|
||||
import io.anuke.mindustry.content.*;
|
||||
import io.anuke.mindustry.entities.*;
|
||||
import io.anuke.mindustry.entities.impl.SolidEntity;
|
||||
import io.anuke.mindustry.entities.traits.*;
|
||||
import io.anuke.mindustry.gen.Call;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.type.Liquid;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static io.anuke.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, x, y, Mathf.random(360f));
|
||||
}
|
||||
}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 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();
|
||||
}
|
||||
|
||||
@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(){
|
||||
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;
|
||||
}
|
||||
}
|
||||
44
core/src/io/anuke/mindustry/entities/effect/RubbleDecal.java
Normal file
44
core/src/io/anuke/mindustry/entities/effect/RubbleDecal.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.Vars.headless;
|
||||
|
||||
public class RubbleDecal extends Decal{
|
||||
private static final TextureRegion[][] regions = new TextureRegion[16][0];
|
||||
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;
|
||||
|
||||
if(regions[size].length == 0 || regions[size][0].getTexture().isDisposed()){
|
||||
regions[size] = new TextureRegion[2];
|
||||
for(int j = 0; j < 2; j++){
|
||||
regions[size][j] = Core.atlas.find("rubble-" + size + "-" + j);
|
||||
}
|
||||
}
|
||||
|
||||
RubbleDecal decal = new RubbleDecal();
|
||||
decal.region = regions[size][Mathf.clamp(Mathf.randomSeed(decal.id, 0, 1), 0, regions[size].length - 1)];
|
||||
|
||||
if(!Core.atlas.isFound(decal.region)){
|
||||
return;
|
||||
}
|
||||
|
||||
decal.set(x, y);
|
||||
decal.add();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawDecal(){
|
||||
if(!Core.atlas.isFound(region)){
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
Draw.rect(region, x, y, Mathf.randomSeed(id, 0, 4) * 90);
|
||||
}
|
||||
}
|
||||
49
core/src/io/anuke/mindustry/entities/effect/ScorchDecal.java
Normal file
49
core/src/io/anuke/mindustry/entities/effect/ScorchDecal.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import io.anuke.arc.Core;
|
||||
import io.anuke.arc.graphics.g2d.Draw;
|
||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
||||
import io.anuke.arc.math.Angles;
|
||||
import io.anuke.arc.math.Mathf;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
|
||||
import static io.anuke.mindustry.Vars.headless;
|
||||
import static io.anuke.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 < 5; 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import com.badlogic.gdx.math.Interpolation;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.types.defense.ShieldBlock;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.BulletEntity;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.Vars.bulletGroup;
|
||||
import static io.anuke.mindustry.Vars.shieldGroup;
|
||||
|
||||
public class Shield extends Entity{
|
||||
public boolean active;
|
||||
public boolean hitPlayers = false;
|
||||
public float radius = 0f;
|
||||
|
||||
private float uptime = 0f;
|
||||
private final Tile tile;
|
||||
|
||||
public Shield(Tile tile){
|
||||
this.tile = tile;
|
||||
this.x = tile.worldx();
|
||||
this.y = tile.worldy();
|
||||
}
|
||||
|
||||
public float drawSize(){
|
||||
return 150;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
float alpha = 0.1f;
|
||||
Interpolation interp = Interpolation.fade;
|
||||
|
||||
if(active){
|
||||
uptime = interp.apply(uptime, 1f, alpha * Timers.delta());
|
||||
}else{
|
||||
uptime = interp.apply(uptime, 0f, alpha * Timers.delta());
|
||||
if(uptime <= 0.05f)
|
||||
remove();
|
||||
}
|
||||
uptime = Mathf.clamp(uptime);
|
||||
|
||||
if(!(tile.block() instanceof ShieldBlock)){
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
ShieldBlock block = (ShieldBlock)tile.block();
|
||||
|
||||
Entities.getNearby(bulletGroup, x, y, block.shieldRadius * 2*uptime + 10, entity->{
|
||||
BulletEntity bullet = (BulletEntity)entity;
|
||||
if((bullet.owner instanceof Enemy || hitPlayers)){
|
||||
|
||||
float dst = entity.distanceTo(this);
|
||||
|
||||
if(dst < drawRadius()/2f){
|
||||
((ShieldBlock)tile.block()).handleBullet(tile, bullet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(){
|
||||
if(!(tile.block() instanceof ShieldBlock) || radius <= 1f){
|
||||
return;
|
||||
}
|
||||
|
||||
float rad = drawRadius();
|
||||
Draw.rect("circle2", x, y, rad, rad);
|
||||
}
|
||||
|
||||
float drawRadius(){
|
||||
return (radius*2 + Mathf.sin(Timers.time(), 25f, 2f));
|
||||
}
|
||||
|
||||
public void removeDelay(){
|
||||
active = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shield add(){
|
||||
return super.add(shieldGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
active = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
active = false;
|
||||
uptime = 0f;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package io.anuke.mindustry.entities.effect;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import com.badlogic.gdx.utils.ObjectSet;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.SolidEntity;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.graphics.Lines;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
|
||||
import static io.anuke.mindustry.Vars.enemyGroup;
|
||||
|
||||
public class TeslaOrb extends Entity{
|
||||
private Array<Vector2> points = new Array<>();
|
||||
private ObjectSet<Enemy> hit = new ObjectSet<>();
|
||||
private int damage = 0;
|
||||
private float range = 0;
|
||||
private float lifetime = 30f;
|
||||
private float life = 0f;
|
||||
private Vector2 vector = new Vector2();
|
||||
|
||||
public TeslaOrb(float x, float y, float range, int damage){
|
||||
set(x, y);
|
||||
this.damage = damage;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
void shock(){
|
||||
float stopchance = 0.1f;
|
||||
float curx = x, cury = y;
|
||||
float shake = 3f;
|
||||
|
||||
int max = 7;
|
||||
|
||||
while(points.size < max){
|
||||
if(Mathf.chance(stopchance)){
|
||||
break;
|
||||
}
|
||||
|
||||
Array<SolidEntity> enemies = Entities.getNearby(enemyGroup, curx, cury, range*2f);
|
||||
|
||||
synchronized (Entities.entityLock) {
|
||||
|
||||
for (SolidEntity entity : enemies) {
|
||||
if (entity != null && entity.distanceTo(curx, cury) < range && !hit.contains((Enemy) entity)) {
|
||||
hit.add((Enemy) entity);
|
||||
points.add(new Vector2(entity.x + Mathf.range(shake), entity.y + Mathf.range(shake)));
|
||||
damageEnemy((Enemy) entity);
|
||||
curx = entity.x;
|
||||
cury = entity.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(points.size == 0){
|
||||
remove();
|
||||
}
|
||||
}
|
||||
|
||||
void damageEnemy(Enemy enemy){
|
||||
enemy.damage(damage);
|
||||
Effects.effect(Fx.laserhit, enemy.x + Mathf.range(2f), enemy.y + Mathf.range(2f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
life += Timers.delta();
|
||||
|
||||
if(life >= lifetime){
|
||||
remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawOver(){
|
||||
if(points.size == 0) return;
|
||||
|
||||
float range = 1f;
|
||||
|
||||
Vector2 previous = vector.set(x, y);
|
||||
|
||||
for(Vector2 enemy : points){
|
||||
|
||||
|
||||
float x1 = previous.x + Mathf.range(range),
|
||||
y1 = previous.y + Mathf.range(range),
|
||||
x2 = enemy.x + Mathf.range(range),
|
||||
y2 = enemy.y + Mathf.range(range);
|
||||
|
||||
Draw.color(Color.WHITE);
|
||||
Draw.alpha(1f-life/lifetime);
|
||||
|
||||
Lines.stroke(3f - life/lifetime*2f);
|
||||
Lines.line(x1, y1, x2, y2);
|
||||
|
||||
float rad = 7f - life/lifetime*5f;
|
||||
|
||||
Draw.rect("circle", x2, y2, rad, rad);
|
||||
|
||||
if(previous.epsilonEquals(x, y, 0.001f)){
|
||||
Draw.rect("circle", x, y, rad, rad);
|
||||
}
|
||||
|
||||
//Draw.color(Color.WHITE);
|
||||
|
||||
//Draw.stroke(2f - life/lifetime*2f);
|
||||
//Draw.line(x1, y1, x2, y2);
|
||||
|
||||
Draw.reset();
|
||||
|
||||
previous = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
Timers.run(1f, ()->{
|
||||
shock();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drawSize(){
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies;
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import io.anuke.mindustry.entities.Bullet;
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.SyncEntity;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.NetEvents;
|
||||
import io.anuke.ucore.entities.Entity;
|
||||
import io.anuke.ucore.entities.SolidEntity;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Timer;
|
||||
import io.anuke.ucore.util.Translator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static io.anuke.mindustry.Vars.enemyGroup;
|
||||
|
||||
public class Enemy extends SyncEntity {
|
||||
public EnemyType type;
|
||||
|
||||
public Timer timer = new Timer(5);
|
||||
public float idletime = 0f;
|
||||
public int lane;
|
||||
public int node = -1;
|
||||
|
||||
public Enemy spawner;
|
||||
public int spawned;
|
||||
|
||||
public Vector2 velocity = new Vector2();
|
||||
public Vector2 totalMove = new Vector2();
|
||||
public Vector2 tpos = new Vector2(-999, -999);
|
||||
public Entity target;
|
||||
public float hitTime;
|
||||
public int tier = 1;
|
||||
|
||||
public TextureRegion region;
|
||||
public Translator tr = new Translator();
|
||||
|
||||
public Enemy(EnemyType type){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**internal constructor used for deserialization, DO NOT USE*/
|
||||
public Enemy(){}
|
||||
|
||||
@Override
|
||||
public void update(){
|
||||
type.update(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSmooth(){
|
||||
type.draw(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawOver(){
|
||||
type.drawOver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float drawSize(){
|
||||
return 14;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collides(SolidEntity other){
|
||||
return (other instanceof Bullet) && !(((Bullet) other).owner instanceof Enemy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void damage(float amount){
|
||||
super.damage(amount);
|
||||
hitTime = EnemyType.hitDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(){
|
||||
type.onDeath(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(){
|
||||
type.removed(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(){
|
||||
hitbox.setSize(type.hitsize);
|
||||
hitboxTile.setSize(type.hitsizeTile);
|
||||
maxhealth = type.health * tier;
|
||||
region = Draw.region(type.name + "-t" + Mathf.clamp(tier, 1, 3));
|
||||
|
||||
heal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enemy add(){
|
||||
return add(enemyGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSpawn(ByteBuffer buffer) {
|
||||
buffer.put(type.id);
|
||||
buffer.put((byte)lane);
|
||||
buffer.put((byte)tier);
|
||||
buffer.putFloat(x);
|
||||
buffer.putFloat(y);
|
||||
buffer.putShort((short)health);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSpawn(ByteBuffer buffer) {
|
||||
type = EnemyType.getByID(buffer.get());
|
||||
lane = buffer.get();
|
||||
tier = buffer.get();
|
||||
x = buffer.getFloat();
|
||||
y = buffer.getFloat();
|
||||
health = buffer.getShort();
|
||||
setNet(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuffer data) {
|
||||
data.putFloat(x);
|
||||
data.putFloat(y);
|
||||
data.putShort((short)(angle*2));
|
||||
data.putShort((short)health);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(ByteBuffer data, long time) {
|
||||
|
||||
float x = data.getFloat();
|
||||
float y = data.getFloat();
|
||||
short angle = data.getShort();
|
||||
short health = data.getShort();
|
||||
|
||||
this.health = health;
|
||||
|
||||
interpolator.read(this.x, this.y, x, y, angle/2f, time);
|
||||
}
|
||||
|
||||
public void shoot(BulletType bullet){
|
||||
shoot(bullet, 0);
|
||||
}
|
||||
|
||||
public void shoot(BulletType bullet, float rotation){
|
||||
|
||||
if(!(Net.client())) {
|
||||
tr.trns(angle + rotation, type.length);
|
||||
Bullet out = new Bullet(bullet, this, x + tr.x, y + tr.y, this.angle + rotation).add();
|
||||
out.damage = (int) ((bullet.damage * (1 + (tier - 1) * 1f)));
|
||||
type.onShoot(this, bullet, rotation);
|
||||
|
||||
if(Net.server()){
|
||||
NetEvents.handleBullet(bullet, this, x + tr.x, y + tr.y, this.angle + rotation, (short)out.damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.utils.Array;
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.Player;
|
||||
import io.anuke.mindustry.entities.TileEntity;
|
||||
import io.anuke.mindustry.graphics.Fx;
|
||||
import io.anuke.mindustry.graphics.Shaders;
|
||||
import io.anuke.mindustry.net.Net;
|
||||
import io.anuke.mindustry.net.NetEvents;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Blocks;
|
||||
import io.anuke.ucore.core.Effects;
|
||||
import io.anuke.ucore.core.Graphics;
|
||||
import io.anuke.ucore.core.Timers;
|
||||
import io.anuke.ucore.entities.Entities;
|
||||
import io.anuke.ucore.graphics.Draw;
|
||||
import io.anuke.ucore.graphics.Lines;
|
||||
import io.anuke.ucore.util.Mathf;
|
||||
import io.anuke.ucore.util.Strings;
|
||||
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
public class EnemyType {
|
||||
|
||||
//TODO documentation, comments
|
||||
private static byte lastid = 0;
|
||||
private static Array<EnemyType> types = new Array<>();
|
||||
|
||||
public final static Color[] tierColors = {
|
||||
Color.valueOf("ffe451"), Color.valueOf("f48e20"), Color.valueOf("ff6757"),
|
||||
Color.valueOf("ff2d86"), Color.valueOf("cb2dff"), Color.valueOf("362020") };
|
||||
public final static int maxtier = tierColors.length;
|
||||
public final static float maxIdleLife = 60f*2f; //2 seconds idle = death
|
||||
public final static float hitDuration = 5f;
|
||||
|
||||
public final String name;
|
||||
public final byte id;
|
||||
|
||||
protected int timeid;
|
||||
protected int health = 60;
|
||||
protected float hitsize = 5f;
|
||||
protected float hitsizeTile = 4f;
|
||||
protected float speed = 0.4f;
|
||||
protected float reload = 32;
|
||||
protected float range = 60;
|
||||
protected float length = 4;
|
||||
protected float rotatespeed = 0.1f;
|
||||
protected float turretrotatespeed = 0.2f;
|
||||
protected boolean alwaysRotate = false;
|
||||
protected BulletType bullet = BulletType.small;
|
||||
protected String shootsound = "enemyshoot";
|
||||
protected boolean targetCore = false;
|
||||
protected boolean stopNearCore = true;
|
||||
protected boolean targetClient = false;
|
||||
protected float mass = 1f;
|
||||
|
||||
protected final int timerTarget = timeid ++;
|
||||
protected final int timerReload = timeid ++;
|
||||
protected final int timerReset = timeid ++;
|
||||
|
||||
protected final Vector2 shift = new Vector2();
|
||||
protected final Vector2 move = new Vector2();
|
||||
protected final Vector2 calc = new Vector2();
|
||||
|
||||
public EnemyType(String name){
|
||||
this.id = lastid++;
|
||||
this.name = name;
|
||||
types.add(this);
|
||||
}
|
||||
|
||||
public void draw(Enemy enemy){
|
||||
Shaders.outline.color.set(tierColors[enemy.tier - 1]);
|
||||
Shaders.outline.lighten = Mathf.clamp(enemy.hitTime/hitDuration);
|
||||
Shaders.outline.region = enemy.region;
|
||||
|
||||
Shaders.outline.apply();
|
||||
|
||||
Draw.rect(enemy.region, enemy.x, enemy.y, enemy.angle - 90);
|
||||
Draw.color();
|
||||
|
||||
Graphics.flush();
|
||||
|
||||
if(isCalculating(enemy)){
|
||||
Draw.color(Color.SKY);
|
||||
Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f);
|
||||
Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f + 180f);
|
||||
Draw.color();
|
||||
}
|
||||
|
||||
if(showPaths){
|
||||
Draw.tscl(0.25f);
|
||||
Draw.text((int)enemy.idletime + " " + enemy.node + " " + enemy.id + "\n" + Strings.toFixed(enemy.totalMove.x, 2) + ", "
|
||||
+ Strings.toFixed(enemy.totalMove.x, 2), enemy.x, enemy.y);
|
||||
Draw.tscl(fontscale);
|
||||
}
|
||||
|
||||
Shaders.outline.lighten = 0f;
|
||||
}
|
||||
|
||||
public void drawOver(Enemy enemy){ }
|
||||
|
||||
public void update(Enemy enemy){
|
||||
float lastx = enemy.x, lasty = enemy.y;
|
||||
if(enemy.hitTime > 0){
|
||||
enemy.hitTime -= Timers.delta();
|
||||
}
|
||||
|
||||
if(enemy.lane >= world.getSpawns().size || enemy.lane < 0) enemy.lane = 0;
|
||||
|
||||
boolean waiting = enemy.lane >= world.getSpawns().size || enemy.lane < 0
|
||||
|| world.getSpawns().get(enemy.lane).pathTiles == null || enemy.node <= 0;
|
||||
|
||||
move(enemy);
|
||||
|
||||
enemy.velocity.set(enemy.x - lastx, enemy.y - lasty).scl(1f / Timers.delta());
|
||||
enemy.totalMove.add(enemy.velocity);
|
||||
|
||||
float minv = 0.07f;
|
||||
|
||||
if(enemy.timer.get(timerReset, 80)){
|
||||
enemy.totalMove.setZero();
|
||||
}
|
||||
|
||||
if(enemy.velocity.len() < minv && !waiting && enemy.target == null){
|
||||
enemy.idletime += Timers.delta();
|
||||
}else{
|
||||
enemy.idletime = 0;
|
||||
}
|
||||
|
||||
if(enemy.timer.getTime(timerReset) > 50 && enemy.totalMove.len() < 0.2f && !waiting && enemy.target == null){
|
||||
enemy.idletime = 999999f;
|
||||
}
|
||||
|
||||
Tile tile = world.tileWorld(enemy.x, enemy.y);
|
||||
if(tile != null && tile.floor().liquid && tile.block() == Blocks.air){
|
||||
enemy.damage(enemy.health+1); //drown
|
||||
}
|
||||
|
||||
if(Float.isNaN(enemy.angle)){
|
||||
enemy.angle = 0;
|
||||
}
|
||||
|
||||
if(enemy.target == null || alwaysRotate){
|
||||
enemy.angle = Mathf.slerpDelta(enemy.angle, enemy.velocity.angle(), rotatespeed);
|
||||
}else{
|
||||
enemy.angle = Mathf.slerpDelta(enemy.angle, enemy.angleTo(enemy.target), turretrotatespeed);
|
||||
}
|
||||
|
||||
enemy.x = Mathf.clamp(enemy.x, 0, world.width() * tilesize);
|
||||
enemy.y = Mathf.clamp(enemy.y, 0, world.height() * tilesize);
|
||||
}
|
||||
|
||||
public void move(Enemy enemy){
|
||||
if(Net.client()){
|
||||
enemy.interpolate();
|
||||
if(targetClient) updateTargeting(enemy, false);
|
||||
return;
|
||||
}
|
||||
|
||||
float speed = this.speed + 0.04f * enemy.tier;
|
||||
float range = this.range + enemy.tier * 5;
|
||||
|
||||
Tile core = world.getCore();
|
||||
|
||||
if(core == null) return;
|
||||
|
||||
if(enemy.idletime > maxIdleLife && enemy.node > 0){
|
||||
enemy.onDeath();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean nearCore = enemy.distanceTo(core.worldx(), core.worldy()) <= range - 18f && stopNearCore;
|
||||
Vector2 vec;
|
||||
|
||||
if(nearCore){
|
||||
vec = move.setZero();
|
||||
if(targetCore) enemy.target = core.entity;
|
||||
}else{
|
||||
vec = world.pathfinder().find(enemy);
|
||||
vec.sub(enemy.x, enemy.y).limit(speed);
|
||||
}
|
||||
|
||||
shift.setZero();
|
||||
float shiftRange = enemy.hitbox.width + 2f;
|
||||
float avoidRange = shiftRange + 4f;
|
||||
float attractRange = avoidRange + 7f;
|
||||
float avoidSpeed = this.speed/2.7f;
|
||||
|
||||
Entities.getNearby(enemyGroup, enemy.x, enemy.y, range, en -> {
|
||||
Enemy other = (Enemy)en;
|
||||
if(other == enemy) return;
|
||||
float dst = other.distanceTo(enemy);
|
||||
|
||||
if(dst < shiftRange){
|
||||
float scl = Mathf.clamp(1.4f - dst / shiftRange) * mass * 1f/mass;
|
||||
shift.add((enemy.x - other.x) * scl, (enemy.y - other.y) * scl);
|
||||
}else if(dst < avoidRange){
|
||||
calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed);
|
||||
shift.add(calc.scl(1.1f));
|
||||
}else if(dst < attractRange && !nearCore && !isCalculating(enemy)){
|
||||
calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed);
|
||||
shift.add(calc.scl(-1));
|
||||
}
|
||||
});
|
||||
|
||||
shift.limit(1f);
|
||||
vec.add(shift.scl(0.5f));
|
||||
|
||||
enemy.move(vec.x * Timers.delta(), vec.y * Timers.delta());
|
||||
|
||||
updateTargeting(enemy, nearCore);
|
||||
|
||||
behavior(enemy);
|
||||
}
|
||||
|
||||
public void behavior(Enemy enemy){}
|
||||
|
||||
public void updateTargeting(Enemy enemy, boolean nearCore){
|
||||
if(enemy.target != null && enemy.target instanceof TileEntity && ((TileEntity)enemy.target).dead){
|
||||
enemy.target = null;
|
||||
}
|
||||
|
||||
if(enemy.timer.get(timerTarget, 15) && !nearCore){
|
||||
enemy.target = world.findTileTarget(enemy.x, enemy.y, null, range, false);
|
||||
|
||||
//no tile found
|
||||
if(enemy.target == null){
|
||||
enemy.target = Entities.getClosest(playerGroup, enemy.x, enemy.y, range, e -> !((Player)e).isAndroid &&
|
||||
!((Player)e).isDead());
|
||||
}
|
||||
}else if(nearCore){
|
||||
enemy.target = world.getCore().entity;
|
||||
}
|
||||
|
||||
if(enemy.target != null && bullet != null){
|
||||
updateShooting(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateShooting(Enemy enemy){
|
||||
float reload = this.reload / Math.max(enemy.tier / 1.5f, 1f);
|
||||
|
||||
if(enemy.timer.get(timerReload, reload)){
|
||||
shoot(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
public void shoot(Enemy enemy){
|
||||
enemy.shoot(bullet);
|
||||
if(shootsound != null) Effects.sound(shootsound, enemy);
|
||||
}
|
||||
|
||||
public void onShoot(Enemy enemy, BulletType type, float rotation){}
|
||||
|
||||
public void onDeath(Enemy enemy, boolean force){
|
||||
if(Net.server()){
|
||||
NetEvents.handleEnemyDeath(enemy);
|
||||
}
|
||||
|
||||
if(!Net.client() || force) {
|
||||
Effects.effect(Fx.explosion, enemy);
|
||||
Effects.shake(3f, 4f, enemy);
|
||||
Effects.sound("bang2", enemy);
|
||||
enemy.remove();
|
||||
enemy.dead = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void removed(Enemy enemy){
|
||||
if(!enemy.dead){
|
||||
if(enemy.spawner != null){
|
||||
enemy.spawner.spawned --;
|
||||
}else{
|
||||
state.enemies --;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCalculating(Enemy enemy){
|
||||
return enemy.node < 0 && !Net.client();
|
||||
}
|
||||
|
||||
public static EnemyType getByID(byte id){
|
||||
return types.get(id);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies;
|
||||
|
||||
import io.anuke.mindustry.entities.enemies.types.BlastType;
|
||||
import io.anuke.mindustry.entities.enemies.types.EmpType;
|
||||
import io.anuke.mindustry.entities.enemies.types.FastType;
|
||||
import io.anuke.mindustry.entities.enemies.types.FlamerType;
|
||||
import io.anuke.mindustry.entities.enemies.types.FortressType;
|
||||
import io.anuke.mindustry.entities.enemies.types.HealerType;
|
||||
import io.anuke.mindustry.entities.enemies.types.MortarType;
|
||||
import io.anuke.mindustry.entities.enemies.types.RapidType;
|
||||
import io.anuke.mindustry.entities.enemies.types.*;
|
||||
import io.anuke.mindustry.entities.enemies.types.TankType;
|
||||
import io.anuke.mindustry.entities.enemies.types.TargetType;
|
||||
import io.anuke.mindustry.entities.enemies.types.TitanType;
|
||||
|
||||
public class EnemyTypes {
|
||||
public static final EnemyType
|
||||
|
||||
standard = new StandardType(),
|
||||
fast = new FastType(),
|
||||
rapid = new RapidType(),
|
||||
flamer = new FlamerType(),
|
||||
tank = new TankType(),
|
||||
blast = new BlastType(),
|
||||
mortar = new MortarType(),
|
||||
healer = new HealerType(),
|
||||
titan = new TitanType(),
|
||||
emp = new EmpType(),
|
||||
fortress = new FortressType(),
|
||||
target = new TargetType();
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies.types;
|
||||
|
||||
import io.anuke.mindustry.entities.Bullet;
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.TileEntity;
|
||||
import io.anuke.mindustry.entities.enemies.Enemy;
|
||||
import io.anuke.mindustry.entities.enemies.EnemyType;
|
||||
|
||||
import static io.anuke.mindustry.Vars.tilesize;
|
||||
|
||||
public class BlastType extends EnemyType {
|
||||
|
||||
public BlastType() {
|
||||
super("blastenemy");
|
||||
health = 30;
|
||||
speed = 0.8f;
|
||||
bullet = null;
|
||||
turretrotatespeed = 0f;
|
||||
mass = 0.8f;
|
||||
stopNearCore = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void behavior(Enemy enemy){
|
||||
|
||||
float range = 10f;
|
||||
float ox = 0, oy = 0;
|
||||
|
||||
if(enemy.target instanceof TileEntity){
|
||||
TileEntity e = (TileEntity)enemy.target;
|
||||
range = (e.tile.block().width * tilesize) /2f + 8f;
|
||||
ox = e.tile.block().getPlaceOffset().x;
|
||||
oy = e.tile.block().getPlaceOffset().y;
|
||||
}
|
||||
|
||||
if(enemy.target != null && enemy.target.distanceTo(enemy.x - ox, enemy.y - oy) < range){
|
||||
explode(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(Enemy enemy, boolean force){
|
||||
if(force) explode(enemy);
|
||||
super.onDeath(enemy, force);
|
||||
}
|
||||
|
||||
void explode(Enemy enemy){
|
||||
Bullet b = new Bullet(BulletType.blast, enemy, enemy.x, enemy.y, 0).add();
|
||||
b.damage = BulletType.blast.damage + (enemy.tier-1) * 40;
|
||||
enemy.damage(999);
|
||||
enemy.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies.types;
|
||||
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.enemies.EnemyType;
|
||||
|
||||
public class EmpType extends EnemyType {
|
||||
|
||||
public EmpType() {
|
||||
super("empenemy");
|
||||
|
||||
speed = 0.3f;
|
||||
reload = 70;
|
||||
health = 210;
|
||||
range = 80f;
|
||||
bullet = BulletType.emp;
|
||||
turretrotatespeed = 0.1f;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies.types;
|
||||
|
||||
import io.anuke.mindustry.entities.enemies.EnemyType;
|
||||
|
||||
public class FastType extends EnemyType {
|
||||
|
||||
public FastType() {
|
||||
super("fastenemy");
|
||||
|
||||
speed = 0.73f;
|
||||
reload = 20;
|
||||
mass = 0.2f;
|
||||
|
||||
health = 50;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package io.anuke.mindustry.entities.enemies.types;
|
||||
|
||||
import io.anuke.mindustry.entities.BulletType;
|
||||
import io.anuke.mindustry.entities.enemies.EnemyType;
|
||||
|
||||
public class FlamerType extends EnemyType {
|
||||
|
||||
public FlamerType() {
|
||||
super("flamerenemy");
|
||||
|
||||
speed = 0.35f;
|
||||
health = 150;
|
||||
reload = 6;
|
||||
bullet = BulletType.flameshot;
|
||||
shootsound = "flame";
|
||||
mass = 1.5f;
|
||||
range = 40;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user