it is done
This commit is contained in:
191
core/src/mindustry/game/DefaultWaves.java
Normal file
191
core/src/mindustry/game/DefaultWaves.java
Normal file
@@ -0,0 +1,191 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.struct.Array;
|
||||
import mindustry.content.*;
|
||||
import mindustry.type.ItemStack;
|
||||
|
||||
public class DefaultWaves{
|
||||
private Array<SpawnGroup> spawns;
|
||||
|
||||
public Array<SpawnGroup> get(){
|
||||
if(spawns == null && UnitTypes.dagger != null){
|
||||
spawns = Array.with(
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
end = 10;
|
||||
unitScaling = 2f;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.crawler){{
|
||||
begin = 4;
|
||||
end = 13;
|
||||
unitAmount = 2;
|
||||
unitScaling = 1.5f;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.wraith){{
|
||||
begin = 12;
|
||||
end = 16;
|
||||
unitScaling = 1f;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 11;
|
||||
unitScaling = 1.7f;
|
||||
spacing = 2;
|
||||
max = 4;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.titan){{
|
||||
begin = 7;
|
||||
spacing = 3;
|
||||
unitScaling = 2;
|
||||
|
||||
end = 30;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 8;
|
||||
unitScaling = 1;
|
||||
unitAmount = 4;
|
||||
spacing = 2;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.titan){{
|
||||
begin = 28;
|
||||
spacing = 3;
|
||||
unitScaling = 1;
|
||||
end = 40;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.titan){{
|
||||
begin = 45;
|
||||
spacing = 3;
|
||||
unitScaling = 2;
|
||||
effect = StatusEffects.overdrive;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.titan){{
|
||||
begin = 120;
|
||||
spacing = 2;
|
||||
unitScaling = 3;
|
||||
unitAmount = 5;
|
||||
effect = StatusEffects.overdrive;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.wraith){{
|
||||
begin = 16;
|
||||
unitScaling = 1;
|
||||
spacing = 2;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 82;
|
||||
spacing = 3;
|
||||
unitAmount = 4;
|
||||
unitScaling = 3;
|
||||
effect = StatusEffects.overdrive;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 41;
|
||||
spacing = 5;
|
||||
unitAmount = 1;
|
||||
unitScaling = 3;
|
||||
effect = StatusEffects.shielded;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.fortress){{
|
||||
begin = 40;
|
||||
spacing = 5;
|
||||
unitAmount = 2;
|
||||
unitScaling = 2;
|
||||
max = 20;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 35;
|
||||
spacing = 3;
|
||||
unitAmount = 4;
|
||||
effect = StatusEffects.overdrive;
|
||||
items = new ItemStack(Items.blastCompound, 60);
|
||||
end = 60;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.dagger){{
|
||||
begin = 42;
|
||||
spacing = 3;
|
||||
unitAmount = 4;
|
||||
effect = StatusEffects.overdrive;
|
||||
items = new ItemStack(Items.pyratite, 100);
|
||||
end = 130;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.ghoul){{
|
||||
begin = 40;
|
||||
unitAmount = 2;
|
||||
spacing = 2;
|
||||
unitScaling = 2;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.wraith){{
|
||||
begin = 50;
|
||||
unitAmount = 4;
|
||||
unitScaling = 3;
|
||||
spacing = 5;
|
||||
effect = StatusEffects.overdrive;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.revenant){{
|
||||
begin = 50;
|
||||
unitAmount = 2;
|
||||
unitScaling = 3;
|
||||
spacing = 5;
|
||||
max = 16;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.ghoul){{
|
||||
begin = 53;
|
||||
unitAmount = 2;
|
||||
unitScaling = 3;
|
||||
spacing = 4;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.eruptor){{
|
||||
begin = 31;
|
||||
unitAmount = 4;
|
||||
unitScaling = 1;
|
||||
spacing = 3;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.chaosArray){{
|
||||
begin = 41;
|
||||
unitAmount = 1;
|
||||
unitScaling = 1;
|
||||
spacing = 30;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.eradicator){{
|
||||
begin = 81;
|
||||
unitAmount = 1;
|
||||
unitScaling = 1;
|
||||
spacing = 40;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.lich){{
|
||||
begin = 131;
|
||||
unitAmount = 1;
|
||||
unitScaling = 1;
|
||||
spacing = 40;
|
||||
}},
|
||||
|
||||
new SpawnGroup(UnitTypes.ghoul){{
|
||||
begin = 90;
|
||||
unitAmount = 2;
|
||||
unitScaling = 3;
|
||||
spacing = 4;
|
||||
}}
|
||||
);
|
||||
}
|
||||
return spawns == null ? new Array<>() : spawns;
|
||||
}
|
||||
}
|
||||
28
core/src/mindustry/game/Difficulty.java
Normal file
28
core/src/mindustry/game/Difficulty.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.Core;
|
||||
|
||||
/** Presets for time between waves. Currently unused.*/
|
||||
public enum Difficulty{
|
||||
easy(1.4f),
|
||||
normal(1f),
|
||||
hard(0.5f),
|
||||
insane(0.25f);
|
||||
|
||||
/** Multiplier of the time between waves. */
|
||||
public final float waveTime;
|
||||
|
||||
private String value;
|
||||
|
||||
Difficulty(float waveTime){
|
||||
this.waveTime = waveTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
if(value == null){
|
||||
value = Core.bundle.get("setting.difficulty." + name());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
393
core/src/mindustry/game/EventType.java
Normal file
393
core/src/mindustry/game/EventType.java
Normal file
@@ -0,0 +1,393 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.core.GameState.State;
|
||||
import mindustry.ctype.UnlockableContent;
|
||||
import mindustry.entities.traits.BuilderTrait;
|
||||
import mindustry.entities.type.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.Tile;
|
||||
|
||||
public class EventType{
|
||||
|
||||
//events that occur very often
|
||||
public enum Trigger{
|
||||
shock,
|
||||
phaseDeflectHit,
|
||||
impactPower,
|
||||
thoriumReactorOverheat,
|
||||
itemLaunch,
|
||||
fireExtinguish,
|
||||
newGame,
|
||||
tutorialComplete,
|
||||
flameAmmo,
|
||||
turretCool,
|
||||
enablePixelation,
|
||||
drown,
|
||||
exclusionDeath,
|
||||
suicideBomb,
|
||||
openWiki,
|
||||
teamCoreDamage
|
||||
}
|
||||
|
||||
public static class WinEvent{}
|
||||
|
||||
public static class LoseEvent{}
|
||||
|
||||
public static class LaunchEvent{}
|
||||
|
||||
public static class LaunchItemEvent{
|
||||
public final ItemStack stack;
|
||||
|
||||
public LaunchItemEvent(Item item, int amount){
|
||||
this.stack = new ItemStack(item, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MapMakeEvent{}
|
||||
|
||||
public static class MapPublishEvent{}
|
||||
|
||||
public static class CommandIssueEvent{
|
||||
public final Tile tile;
|
||||
public final UnitCommand command;
|
||||
|
||||
public CommandIssueEvent(Tile tile, UnitCommand command){
|
||||
this.tile = tile;
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerChatEvent{
|
||||
public final Player player;
|
||||
public final String message;
|
||||
|
||||
public PlayerChatEvent(Player player, String message){
|
||||
this.player = player;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a zone's requirements are met. */
|
||||
public static class ZoneRequireCompleteEvent{
|
||||
public final Zone zoneMet, zoneForMet;
|
||||
public final Objective objective;
|
||||
|
||||
public ZoneRequireCompleteEvent(Zone zoneMet, Zone zoneForMet, Objective objective){
|
||||
this.zoneMet = zoneMet;
|
||||
this.zoneForMet = zoneForMet;
|
||||
this.objective = objective;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a zone's requirements are met. */
|
||||
public static class ZoneConfigureCompleteEvent{
|
||||
public final Zone zone;
|
||||
|
||||
public ZoneConfigureCompleteEvent(Zone zone){
|
||||
this.zone = zone;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the client game is first loaded. */
|
||||
public static class ClientLoadEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class ServerLoadEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class ContentReloadEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class DisposeEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class PlayEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class ResetEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class WaveEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called when the player places a line, mobile or desktop.*/
|
||||
public static class LineConfirmEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called when a turret recieves ammo, but only when the tutorial is active! */
|
||||
public static class TurretAmmoDeliverEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called when a core recieves ammo, but only when the tutorial is active! */
|
||||
public static class CoreItemDeliverEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called when the player opens info for a specific block.*/
|
||||
public static class BlockInfoEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called when the player withdraws items from a block. */
|
||||
public static class WithdrawEvent{
|
||||
public final Tile tile;
|
||||
public final Player player;
|
||||
public final Item item;
|
||||
public final int amount;
|
||||
|
||||
public WithdrawEvent(Tile tile, Player player, Item item, int amount){
|
||||
this.tile = tile;
|
||||
this.player = player;
|
||||
this.item = item;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a player deposits items to a block.*/
|
||||
public static class DepositEvent{
|
||||
public final Tile tile;
|
||||
public final Player player;
|
||||
public final Item item;
|
||||
public final int amount;
|
||||
|
||||
public DepositEvent(Tile tile, Player player, Item item, int amount){
|
||||
this.tile = tile;
|
||||
this.player = player;
|
||||
this.item = item;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the player taps a block. */
|
||||
public static class TapEvent{
|
||||
public final Tile tile;
|
||||
public final Player player;
|
||||
|
||||
public TapEvent(Tile tile, Player player){
|
||||
this.tile = tile;
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the player sets a specific block. */
|
||||
public static class TapConfigEvent{
|
||||
public final Tile tile;
|
||||
public final Player player;
|
||||
public final int value;
|
||||
|
||||
public TapConfigEvent(Tile tile, Player player, int value){
|
||||
this.tile = tile;
|
||||
this.player = player;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameOverEvent{
|
||||
public final Team winner;
|
||||
|
||||
public GameOverEvent(Team winner){
|
||||
this.winner = winner;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a game begins and the world is loaded. */
|
||||
public static class WorldLoadEvent{
|
||||
|
||||
}
|
||||
|
||||
/** Called from the logic thread. Do not access graphics here! */
|
||||
public static class TileChangeEvent{
|
||||
public final Tile tile;
|
||||
|
||||
public TileChangeEvent(Tile tile){
|
||||
this.tile = tile;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StateChangeEvent{
|
||||
public final State from, to;
|
||||
|
||||
public StateChangeEvent(State from, State to){
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnlockEvent{
|
||||
public final UnlockableContent content;
|
||||
|
||||
public UnlockEvent(UnlockableContent content){
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResearchEvent{
|
||||
public final UnlockableContent content;
|
||||
|
||||
public ResearchEvent(UnlockableContent content){
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when block building begins by placing down the BuildBlock.
|
||||
* The tile's block will nearly always be a BuildBlock.
|
||||
*/
|
||||
public static class BlockBuildBeginEvent{
|
||||
public final Tile tile;
|
||||
public final Team team;
|
||||
public final boolean breaking;
|
||||
|
||||
public BlockBuildBeginEvent(Tile tile, Team team, boolean breaking){
|
||||
this.tile = tile;
|
||||
this.team = team;
|
||||
this.breaking = breaking;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BlockBuildEndEvent{
|
||||
public final Tile tile;
|
||||
public final Team team;
|
||||
public final @Nullable
|
||||
Player player;
|
||||
public final boolean breaking;
|
||||
|
||||
public BlockBuildEndEvent(Tile tile, @Nullable Player player, Team team, boolean breaking){
|
||||
this.tile = tile;
|
||||
this.team = team;
|
||||
this.player = player;
|
||||
this.breaking = breaking;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player or drone begins building something.
|
||||
* This does not necessarily happen when a new BuildBlock is created.
|
||||
*/
|
||||
public static class BuildSelectEvent{
|
||||
public final Tile tile;
|
||||
public final Team team;
|
||||
public final BuilderTrait builder;
|
||||
public final boolean breaking;
|
||||
|
||||
public BuildSelectEvent(Tile tile, Team team, BuilderTrait builder, boolean breaking){
|
||||
this.tile = tile;
|
||||
this.team = team;
|
||||
this.builder = builder;
|
||||
this.breaking = breaking;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called right before a block is destroyed.
|
||||
* The tile entity of the tile in this event cannot be null when this happens.*/
|
||||
public static class BlockDestroyEvent{
|
||||
public final Tile tile;
|
||||
|
||||
public BlockDestroyEvent(Tile tile){
|
||||
this.tile = tile;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnitDestroyEvent{
|
||||
public final Unit unit;
|
||||
|
||||
public UnitDestroyEvent(Unit unit){
|
||||
this.unit = unit;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnitCreateEvent{
|
||||
public final BaseUnit unit;
|
||||
|
||||
public UnitCreateEvent(BaseUnit unit){
|
||||
this.unit = unit;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResizeEvent{
|
||||
|
||||
}
|
||||
|
||||
public static class MechChangeEvent{
|
||||
public final Player player;
|
||||
public final Mech mech;
|
||||
|
||||
public MechChangeEvent(Player player, Mech mech){
|
||||
this.player = player;
|
||||
this.mech = mech;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called after connecting; when a player recieves world data and is ready to play.*/
|
||||
public static class PlayerJoin{
|
||||
public final Player player;
|
||||
|
||||
public PlayerJoin(Player player){
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a player connects, but has not joined the game yet.*/
|
||||
public static class PlayerConnect{
|
||||
public final Player player;
|
||||
|
||||
public PlayerConnect(Player player){
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerLeave{
|
||||
public final Player player;
|
||||
|
||||
public PlayerLeave(Player player){
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerBanEvent{
|
||||
public final Player player;
|
||||
|
||||
public PlayerBanEvent(Player player){
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerUnbanEvent{
|
||||
public final Player player;
|
||||
|
||||
public PlayerUnbanEvent(Player player){
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerIpBanEvent{
|
||||
public final String ip;
|
||||
|
||||
|
||||
public PlayerIpBanEvent(String ip){
|
||||
this.ip = ip;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerIpUnbanEvent{
|
||||
public final String ip;
|
||||
|
||||
|
||||
public PlayerIpUnbanEvent(String ip){
|
||||
this.ip = ip;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
104
core/src/mindustry/game/Gamemode.java
Normal file
104
core/src/mindustry/game/Gamemode.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.func.*;
|
||||
import mindustry.maps.*;
|
||||
|
||||
import static mindustry.Vars.waveTeam;
|
||||
|
||||
/** Defines preset rule sets. */
|
||||
public enum Gamemode{
|
||||
survival(rules -> {
|
||||
rules.waveTimer = true;
|
||||
rules.waves = true;
|
||||
rules.unitDrops = true;
|
||||
}, map -> map.spawns > 0),
|
||||
sandbox(rules -> {
|
||||
rules.infiniteResources = true;
|
||||
rules.waves = true;
|
||||
rules.waveTimer = false;
|
||||
rules.respawnTime = 0f;
|
||||
}),
|
||||
attack(rules -> {
|
||||
rules.unitDrops = true;
|
||||
rules.attackMode = true;
|
||||
}, map -> map.teams.contains(waveTeam.ordinal())),
|
||||
pvp(rules -> {
|
||||
rules.pvp = true;
|
||||
rules.enemyCoreBuildRadius = 600f;
|
||||
rules.respawnTime = 60 * 10;
|
||||
rules.buildCostMultiplier = 1f;
|
||||
rules.buildSpeedMultiplier = 1f;
|
||||
rules.playerDamageMultiplier = 0.33f;
|
||||
rules.playerHealthMultiplier = 0.5f;
|
||||
rules.unitBuildSpeedMultiplier = 2f;
|
||||
rules.unitHealthMultiplier = 3f;
|
||||
rules.attackMode = true;
|
||||
}, map -> map.teams.size > 1),
|
||||
editor(true, rules -> {
|
||||
rules.infiniteResources = true;
|
||||
rules.editor = true;
|
||||
rules.waves = false;
|
||||
rules.enemyCoreBuildRadius = 0f;
|
||||
rules.waveTimer = false;
|
||||
rules.respawnTime = 0f;
|
||||
});
|
||||
|
||||
private final Cons<Rules> rules;
|
||||
private final Boolf<Map> validator;
|
||||
|
||||
public final boolean hidden;
|
||||
public final static Gamemode[] all = values();
|
||||
|
||||
Gamemode(Cons<Rules> rules){
|
||||
this(false, rules);
|
||||
}
|
||||
|
||||
Gamemode(boolean hidden, Cons<Rules> rules){
|
||||
this(hidden, rules, m -> true);
|
||||
}
|
||||
|
||||
Gamemode(Cons<Rules> rules, Boolf<Map> validator){
|
||||
this(false, rules, validator);
|
||||
}
|
||||
|
||||
Gamemode(boolean hidden, Cons<Rules> rules, Boolf<Map> validator){
|
||||
this.rules = rules;
|
||||
this.hidden = hidden;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
public static Gamemode bestFit(Rules rules){
|
||||
if(rules.pvp){
|
||||
return pvp;
|
||||
}else if(rules.editor){
|
||||
return editor;
|
||||
}else if(rules.attackMode){
|
||||
return attack;
|
||||
}else if(rules.infiniteResources){
|
||||
return sandbox;
|
||||
}else{
|
||||
return survival;
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies this preset to this ruleset. */
|
||||
public Rules apply(Rules in){
|
||||
rules.get(in);
|
||||
return in;
|
||||
}
|
||||
|
||||
/** @return whether this mode can be played on the specified map. */
|
||||
public boolean valid(Map map){
|
||||
return validator.get(map);
|
||||
}
|
||||
|
||||
public String description(){
|
||||
return Core.bundle.get("mode." + name() + ".description");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return Core.bundle.get("mode." + name() + ".name");
|
||||
}
|
||||
}
|
||||
184
core/src/mindustry/game/GlobalData.java
Normal file
184
core/src/mindustry/game/GlobalData.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.util.io.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Stores player unlocks. Clientside only. */
|
||||
public class GlobalData{
|
||||
private ObjectMap<ContentType, ObjectSet<String>> unlocked = new ObjectMap<>();
|
||||
private ObjectIntMap<Item> items = new ObjectIntMap<>();
|
||||
private boolean modified;
|
||||
|
||||
public GlobalData(){
|
||||
Core.settings.setSerializer(ContentType.class, (stream, t) -> stream.writeInt(t.ordinal()), stream -> ContentType.values()[stream.readInt()]);
|
||||
Core.settings.setSerializer(Item.class, (stream, t) -> stream.writeUTF(t.name), stream -> content.getByName(ContentType.item, stream.readUTF()));
|
||||
|
||||
Core.settings.setSerializer(ItemStack.class, (stream, t) -> {
|
||||
stream.writeUTF(t.item.name);
|
||||
stream.writeInt(t.amount);
|
||||
}, stream -> {
|
||||
String name = stream.readUTF();
|
||||
int amount = stream.readInt();
|
||||
return new ItemStack(content.getByName(ContentType.item, name), amount);
|
||||
});
|
||||
}
|
||||
|
||||
public void exportData(Fi file) throws IOException{
|
||||
Array<Fi> files = new Array<>();
|
||||
files.add(Core.settings.getSettingsFile());
|
||||
files.addAll(customMapDirectory.list());
|
||||
files.addAll(saveDirectory.list());
|
||||
files.addAll(screenshotDirectory.list());
|
||||
files.addAll(modDirectory.list());
|
||||
files.addAll(schematicDirectory.list());
|
||||
String base = Core.settings.getDataDirectory().path();
|
||||
|
||||
try(OutputStream fos = file.write(false, 2048); ZipOutputStream zos = new ZipOutputStream(fos)){
|
||||
for(Fi add : files){
|
||||
if(add.isDirectory()) continue;
|
||||
zos.putNextEntry(new ZipEntry(add.path().substring(base.length())));
|
||||
Streams.copyStream(add.read(), zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void importData(Fi file){
|
||||
Fi dest = Core.files.local("zipdata.zip");
|
||||
file.copyTo(dest);
|
||||
Fi zipped = new ZipFi(dest);
|
||||
|
||||
Fi base = Core.settings.getDataDirectory();
|
||||
if(!zipped.child("settings.bin").exists()){
|
||||
throw new IllegalArgumentException("Not valid save data.");
|
||||
}
|
||||
|
||||
//purge existing tmp data, keep everything else
|
||||
tmpDirectory.deleteDirectory();
|
||||
|
||||
zipped.walk(f -> f.copyTo(base.child(f.path())));
|
||||
dest.delete();
|
||||
}
|
||||
|
||||
public void modified(){
|
||||
modified = true;
|
||||
}
|
||||
|
||||
public int getItem(Item item){
|
||||
return items.get(item, 0);
|
||||
}
|
||||
|
||||
public void addItem(Item item, int amount){
|
||||
if(amount > 0){
|
||||
unlockContent(item);
|
||||
}
|
||||
modified = true;
|
||||
items.getAndIncrement(item, 0, amount);
|
||||
state.stats.itemsDelivered.getAndIncrement(item, 0, amount);
|
||||
}
|
||||
|
||||
public boolean hasItems(Array<ItemStack> stacks){
|
||||
return !stacks.contains(s -> items.get(s.item, 0) < s.amount);
|
||||
}
|
||||
|
||||
public boolean hasItems(ItemStack[] stacks){
|
||||
for(ItemStack stack : stacks){
|
||||
if(!has(stack.item, stack.amount)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void removeItems(ItemStack[] stacks){
|
||||
for(ItemStack stack : stacks){
|
||||
items.getAndIncrement(stack.item, 0, -stack.amount);
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
|
||||
public void removeItems(Array<ItemStack> stacks){
|
||||
for(ItemStack stack : stacks){
|
||||
items.getAndIncrement(stack.item, 0, -stack.amount);
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
|
||||
public boolean has(Item item, int amount){
|
||||
return items.get(item, 0) >= amount;
|
||||
}
|
||||
|
||||
public ObjectIntMap<Item> items(){
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Returns whether or not this piece of content is unlocked yet. */
|
||||
public boolean isUnlocked(UnlockableContent content){
|
||||
return content.alwaysUnlocked() || unlocked.getOr(content.getContentType(), ObjectSet::new).contains(content.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this piece of content 'unlocked', if possible.
|
||||
* If this piece of content is already unlocked, nothing changes.
|
||||
* Results are not saved until you call {@link #save()}.
|
||||
*/
|
||||
public void unlockContent(UnlockableContent content){
|
||||
if(content.alwaysUnlocked()) return;
|
||||
|
||||
//fire unlock event so other classes can use it
|
||||
if(unlocked.getOr(content.getContentType(), ObjectSet::new).add(content.name)){
|
||||
modified = true;
|
||||
content.onUnlock();
|
||||
Events.fire(new UnlockEvent(content));
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all unlocked content. Automatically saves. */
|
||||
public void reset(){
|
||||
save();
|
||||
}
|
||||
|
||||
public void checkSave(){
|
||||
if(modified){
|
||||
save();
|
||||
modified = false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void load(){
|
||||
items.clear();
|
||||
unlocked = Core.settings.getObject("unlocks", ObjectMap.class, ObjectMap::new);
|
||||
for(Item item : Vars.content.items()){
|
||||
items.put(item, Core.settings.getInt("item-" + item.name, 0));
|
||||
}
|
||||
|
||||
//set up default values
|
||||
if(!Core.settings.has("item-" + Items.copper.name)){
|
||||
addItem(Items.copper, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(){
|
||||
Core.settings.putObject("unlocks", unlocked);
|
||||
for(Item item : Vars.content.items()){
|
||||
Core.settings.put("item-" + item.name, items.get(item, 0));
|
||||
}
|
||||
Core.settings.save();
|
||||
}
|
||||
|
||||
}
|
||||
61
core/src/mindustry/game/LoopControl.java
Normal file
61
core/src/mindustry/game/LoopControl.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.audio.*;
|
||||
import arc.struct.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import mindustry.*;
|
||||
|
||||
public class LoopControl{
|
||||
private ObjectMap<Sound, SoundData> sounds = new ObjectMap<>();
|
||||
|
||||
public void play(Sound sound, Position pos, float volume){
|
||||
if(Vars.headless) return;
|
||||
|
||||
float baseVol = sound.calcFalloff(pos.getX(), pos.getY());
|
||||
float vol = baseVol * volume;
|
||||
|
||||
SoundData data = sounds.getOr(sound, SoundData::new);
|
||||
data.volume += vol;
|
||||
data.volume = Mathf.clamp(data.volume, 0f, 1f);
|
||||
data.total += baseVol;
|
||||
data.sum.add(pos.getX() * baseVol, pos.getY() * baseVol);
|
||||
}
|
||||
|
||||
public void update(){
|
||||
float avol = Core.settings.getInt("ambientvol", 100) / 100f;
|
||||
|
||||
sounds.each((sound, data) -> {
|
||||
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.2f);
|
||||
|
||||
boolean play = data.curVolume > 0.01f;
|
||||
float pan = Mathf.zero(data.total, 0.0001f) ? 0f : sound.calcPan(data.sum.x / data.total, data.sum.y / data.total);
|
||||
if(data.soundID <= 0){
|
||||
if(play){
|
||||
data.soundID = sound.loop(data.curVolume, 1f, pan);
|
||||
}
|
||||
}else{
|
||||
if(data.curVolume <= 0.01f){
|
||||
sound.stop();
|
||||
data.soundID = -1;
|
||||
return;
|
||||
}
|
||||
sound.setPan(data.soundID, pan, data.curVolume);
|
||||
}
|
||||
|
||||
data.volume = 0f;
|
||||
data.total = 0f;
|
||||
data.sum.setZero();
|
||||
});
|
||||
}
|
||||
|
||||
private class SoundData{
|
||||
float volume;
|
||||
float total;
|
||||
Vector2 sum = new Vector2();
|
||||
|
||||
int soundID;
|
||||
float curVolume;
|
||||
}
|
||||
}
|
||||
169
core/src/mindustry/game/MusicControl.java
Normal file
169
core/src/mindustry/game/MusicControl.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.audio.*;
|
||||
import arc.struct.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Controls playback of multiple music tracks.*/
|
||||
public class MusicControl{
|
||||
private static final float finTime = 120f, foutTime = 120f, musicInterval = 60 * 60 * 3f, musicChance = 0.6f, musicWaveChance = 0.5f;
|
||||
|
||||
/** normal, ambient music, plays at any time */
|
||||
public Array<Music> ambientMusic = Array.with();
|
||||
/** darker music, used in times of conflict */
|
||||
public Array<Music> darkMusic = Array.with();
|
||||
private Music lastRandomPlayed;
|
||||
private Interval timer = new Interval();
|
||||
private @Nullable Music current;
|
||||
private float fade;
|
||||
private boolean silenced;
|
||||
|
||||
public MusicControl(){
|
||||
Events.on(ClientLoadEvent.class, e -> reload());
|
||||
|
||||
//only run music 10 seconds after a wave spawns
|
||||
Events.on(WaveEvent.class, e -> Time.run(60f * 10f, () -> {
|
||||
if(Mathf.chance(musicWaveChance)){
|
||||
playRandom();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void reload(){
|
||||
current = null;
|
||||
fade = 0f;
|
||||
ambientMusic = Array.with(Musics.game1, Musics.game3, Musics.game4, Musics.game6);
|
||||
darkMusic = Array.with(Musics.game2, Musics.game5, Musics.game7);
|
||||
}
|
||||
|
||||
/** Update and play the right music track.*/
|
||||
public void update(){
|
||||
if(state.is(State.menu)){
|
||||
silenced = false;
|
||||
if(ui.deploy.isShown()){
|
||||
play(Musics.launch);
|
||||
}else if(ui.editor.isShown()){
|
||||
play(Musics.editor);
|
||||
}else{
|
||||
play(Musics.menu);
|
||||
}
|
||||
}else if(state.rules.editor){
|
||||
silenced = false;
|
||||
play(Musics.editor);
|
||||
}else{
|
||||
//this just fades out the last track to make way for ingame music
|
||||
silence();
|
||||
|
||||
//play music at intervals
|
||||
if(timer.get(musicInterval)){
|
||||
//chance to play it per interval
|
||||
if(Mathf.chance(musicChance)){
|
||||
playRandom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Plays a random track.*/
|
||||
private void playRandom(){
|
||||
if(isDark()){
|
||||
playOnce(darkMusic.random(lastRandomPlayed));
|
||||
}else{
|
||||
playOnce(ambientMusic.random(lastRandomPlayed));
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to play dark music.*/
|
||||
private boolean isDark(){
|
||||
if(!state.teams.get(player.getTeam()).cores.isEmpty() && state.teams.get(player.getTeam()).cores.first().entity.healthf() < 0.85f){
|
||||
//core damaged -> dark
|
||||
return true;
|
||||
}
|
||||
|
||||
//it may be dark based on wave
|
||||
if(Mathf.chance((float)(Math.log10((state.wave - 17f)/19f) + 1) / 4f)){
|
||||
return true;
|
||||
}
|
||||
|
||||
//dark based on enemies
|
||||
return Mathf.chance(state.enemies() / 70f + 0.1f);
|
||||
}
|
||||
|
||||
/** Plays and fades in a music track. This must be called every frame.
|
||||
* If something is already playing, fades out that track and fades in this new music.*/
|
||||
private void play(@Nullable Music music){
|
||||
//update volume of current track
|
||||
if(current != null){
|
||||
current.setVolume(fade * Core.settings.getInt("musicvol") / 100f);
|
||||
}
|
||||
|
||||
//do not update once the track has faded out completely, just stop
|
||||
if(silenced){
|
||||
return;
|
||||
}
|
||||
|
||||
if(current == null && music != null){
|
||||
//begin playing in a new track
|
||||
current = music;
|
||||
current.setLooping(true);
|
||||
current.setVolume(fade = 0f);
|
||||
current.play();
|
||||
silenced = false;
|
||||
}else if(current == music && music != null){
|
||||
//fade in the playing track
|
||||
fade = Mathf.clamp(fade + Time.delta()/finTime);
|
||||
}else if(current != null){
|
||||
//fade out the current track
|
||||
fade = Mathf.clamp(fade - Time.delta()/foutTime);
|
||||
|
||||
if(fade <= 0.01f){
|
||||
//stop current track when it hits 0 volume
|
||||
current.stop();
|
||||
current = null;
|
||||
silenced = true;
|
||||
if(music != null){
|
||||
//play newly scheduled track
|
||||
current = music;
|
||||
current.setVolume(fade = 0f);
|
||||
current.setLooping(true);
|
||||
current.play();
|
||||
silenced = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Plays a music track once and only once. If something is already playing, does nothing.*/
|
||||
private void playOnce(Music music){
|
||||
if(current != null || music == null) return; //do not interrupt already-playing tracks
|
||||
|
||||
//save last random track played to prevent duplicates
|
||||
lastRandomPlayed = music;
|
||||
|
||||
//set fade to 1 and play it, stopping the current when it's done
|
||||
fade = 1f;
|
||||
current = music;
|
||||
current.setVolume(1f);
|
||||
current.setLooping(false);
|
||||
current.setCompletionListener(m -> {
|
||||
if(current == m){
|
||||
current = null;
|
||||
fade = 0f;
|
||||
}
|
||||
});
|
||||
current.play();
|
||||
}
|
||||
|
||||
/** Fades out the current track, unless it has already been silenced. */
|
||||
private void silence(){
|
||||
play(null);
|
||||
}
|
||||
}
|
||||
27
core/src/mindustry/game/Objective.java
Normal file
27
core/src/mindustry/game/Objective.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.game.Objectives.*;
|
||||
import mindustry.type.*;
|
||||
|
||||
/** Defines a specific objective for a game. */
|
||||
public interface Objective{
|
||||
|
||||
/** @return whether this objective is met. */
|
||||
boolean complete();
|
||||
|
||||
/** @return the string displayed when this objective is completed, in imperative form.
|
||||
* e.g. when the objective is 'complete 10 waves', this would display "complete 10 waves".
|
||||
* If this objective should not be displayed, should return null.*/
|
||||
@Nullable String display();
|
||||
|
||||
/** Build a display for this zone requirement.*/
|
||||
default void build(Table table){
|
||||
|
||||
}
|
||||
|
||||
default Zone zone(){
|
||||
return this instanceof ZoneObjective ? ((ZoneObjective)this).zone : null;
|
||||
}
|
||||
}
|
||||
96
core/src/mindustry/game/Objectives.java
Normal file
96
core/src/mindustry/game/Objectives.java
Normal file
@@ -0,0 +1,96 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
/** Holds objective classes. */
|
||||
public class Objectives{
|
||||
|
||||
//TODO
|
||||
public static class Wave implements Objective{
|
||||
public int wave;
|
||||
|
||||
public Wave(int wave){
|
||||
this.wave = wave;
|
||||
}
|
||||
|
||||
protected Wave(){}
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
//TODO
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Unlock implements Objective{
|
||||
public @NonNull Block block;
|
||||
|
||||
public Unlock(Block block){
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
protected Unlock(){}
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return block.unlocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
return Core.bundle.format("requirement.unlock", block.localizedName);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ZoneWave extends ZoneObjective{
|
||||
public int wave;
|
||||
|
||||
public ZoneWave(Zone zone, int wave){
|
||||
this.zone = zone;
|
||||
this.wave = wave;
|
||||
}
|
||||
|
||||
protected ZoneWave(){}
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return zone.bestWave() >= wave;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
return Core.bundle.format("requirement.wave", wave, zone.localizedName);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Launched extends ZoneObjective{
|
||||
|
||||
public Launched(Zone zone){
|
||||
this.zone = zone;
|
||||
}
|
||||
|
||||
protected Launched(){}
|
||||
|
||||
@Override
|
||||
public boolean complete(){
|
||||
return zone.hasLaunched();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String display(){
|
||||
return Core.bundle.format("requirement.core", zone.localizedName);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class ZoneObjective implements Objective{
|
||||
public @NonNull Zone zone;
|
||||
}
|
||||
}
|
||||
91
core/src/mindustry/game/Rules.java
Normal file
91
core/src/mindustry/game/Rules.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package mindustry.game;
|
||||
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import arc.struct.*;
|
||||
import arc.graphics.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
/**
|
||||
* Defines current rules on how the game should function.
|
||||
* Does not store game state, just configuration.
|
||||
*/
|
||||
@Serialize
|
||||
public class Rules{
|
||||
/** Whether the player has infinite resources. */
|
||||
public boolean infiniteResources;
|
||||
/** Whether the waves come automatically on a timer. If not, waves come when the play button is pressed. */
|
||||
public boolean waveTimer = true;
|
||||
/** Whether waves are spawnable at all. */
|
||||
public boolean waves;
|
||||
/** Whether the enemy AI has infinite resources in most of their buildings and turrets. */
|
||||
public boolean enemyCheat;
|
||||
/** Whether the game objective is PvP. Note that this enables automatic hosting. */
|
||||
public boolean pvp;
|
||||
/** Whether enemy units drop random items on death. */
|
||||
public boolean unitDrops = true;
|
||||
/** Whether reactors can explode and damage other blocks. */
|
||||
public boolean reactorExplosions = true;
|
||||
/** How fast unit pads build units. */
|
||||
public float unitBuildSpeedMultiplier = 1f;
|
||||
/** How much health units start with. */
|
||||
public float unitHealthMultiplier = 1f;
|
||||
/** How much health players start with. */
|
||||
public float playerHealthMultiplier = 1f;
|
||||
/** How much damage player mechs deal. */
|
||||
public float playerDamageMultiplier = 1f;
|
||||
/** How much damage any other units deal. */
|
||||
public float unitDamageMultiplier = 1f;
|
||||
/** Multiplier for buildings for the player. */
|
||||
public float buildCostMultiplier = 1f;
|
||||
/** Multiplier for building speed. */
|
||||
public float buildSpeedMultiplier = 1f;
|
||||
/** No-build zone around enemy core radius. */
|
||||
public float enemyCoreBuildRadius = 400f;
|
||||
/** Radius around enemy wave drop zones.*/
|
||||
public float dropZoneRadius = 300f;
|
||||
/** Player respawn time in ticks. */
|
||||
public float respawnTime = 60 * 4;
|
||||
/** Time between waves in ticks. */
|
||||
public float waveSpacing = 60 * 60 * 2;
|
||||
/** How many times longer a boss wave takes. */
|
||||
public float bossWaveMultiplier = 3f;
|
||||
/** How many times longer a launch wave takes. */
|
||||
public float launchWaveMultiplier = 2f;
|
||||
/** Zone for saves that have them.*/
|
||||
public Zone zone;
|
||||
/** Spawn layout. */
|
||||
public Array<SpawnGroup> spawns = new Array<>();
|
||||
/** Determines if there should be limited respawns. */
|
||||
public boolean limitedRespawns = false;
|
||||
/** How many times player can respawn during one wave. */
|
||||
public int respawns = 5;
|
||||
/** Hold wave timer until all enemies are destroyed. */
|
||||
public boolean waitForWaveToEnd = false;
|
||||
/** Determinates if gamemode is attack mode */
|
||||
public boolean attackMode = false;
|
||||
/** Whether this is the editor gamemode. */
|
||||
public boolean editor = false;
|
||||
/** Whether the tutorial is enabled. False by default. */
|
||||
public boolean tutorial = false;
|
||||
/** Starting items put in cores */
|
||||
public Array<ItemStack> loadout = Array.with(ItemStack.with(Items.copper, 100));
|
||||
/** Blocks that cannot be placed. */
|
||||
public ObjectSet<Block> bannedBlocks = new ObjectSet<>();
|
||||
/** Whether everything is dark. Enables lights. Experimental. */
|
||||
public boolean lighting = false;
|
||||
/** Ambient light color, used when lighting is enabled. */
|
||||
public Color ambientLight = new Color(0.01f, 0.01f, 0.04f, 0.99f);
|
||||
|
||||
/** Copies this ruleset exactly. Not very efficient at all, do not use often. */
|
||||
public Rules copy(){
|
||||
return JsonIO.copy(this);
|
||||
}
|
||||
|
||||
/** Returns the gamemode that best fits these rules.*/
|
||||
public Gamemode mode(){
|
||||
return Gamemode.bestFit(this);
|
||||
}
|
||||
}
|
||||
324
core/src/mindustry/game/Saves.java
Normal file
324
core/src/mindustry/game/Saves.java
Normal file
@@ -0,0 +1,324 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.graphics.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.*;
|
||||
import mindustry.core.GameState.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.io.SaveIO.*;
|
||||
import mindustry.maps.Map;
|
||||
import mindustry.type.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Saves{
|
||||
private Array<SaveSlot> saves = new Array<>();
|
||||
private SaveSlot current;
|
||||
private AsyncExecutor previewExecutor = new AsyncExecutor(1);
|
||||
private boolean saving;
|
||||
private float time;
|
||||
private Fi zoneFile;
|
||||
|
||||
private long totalPlaytime;
|
||||
private long lastTimestamp;
|
||||
|
||||
public Saves(){
|
||||
Core.assets.setLoader(Texture.class, ".spreview", new SavePreviewLoader());
|
||||
|
||||
Events.on(StateChangeEvent.class, event -> {
|
||||
if(event.to == State.menu){
|
||||
totalPlaytime = 0;
|
||||
lastTimestamp = 0;
|
||||
current = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void load(){
|
||||
saves.clear();
|
||||
zoneFile = saveDirectory.child("-1.msav");
|
||||
|
||||
for(Fi file : saveDirectory.list()){
|
||||
if(!file.name().contains("backup") && SaveIO.isSaveValid(file)){
|
||||
SaveSlot slot = new SaveSlot(file);
|
||||
saves.add(slot);
|
||||
slot.meta = SaveIO.getMeta(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SaveSlot getCurrent(){
|
||||
return current;
|
||||
}
|
||||
|
||||
public void update(){
|
||||
SaveSlot current = this.current;
|
||||
|
||||
if(current != null && !state.is(State.menu)
|
||||
&& !(state.isPaused() && Core.scene.hasDialog())){
|
||||
if(lastTimestamp != 0){
|
||||
totalPlaytime += Time.timeSinceMillis(lastTimestamp);
|
||||
}
|
||||
lastTimestamp = Time.millis();
|
||||
}
|
||||
|
||||
if(!state.is(State.menu) && !state.gameOver && current != null && current.isAutosave() && !state.rules.tutorial){
|
||||
time += Time.delta();
|
||||
if(time > Core.settings.getInt("saveinterval") * 60){
|
||||
saving = true;
|
||||
|
||||
Time.runTask(2f, () -> {
|
||||
try{
|
||||
current.save();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
saving = false;
|
||||
});
|
||||
|
||||
time = 0;
|
||||
}
|
||||
}else{
|
||||
time = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long getTotalPlaytime(){
|
||||
return totalPlaytime;
|
||||
}
|
||||
|
||||
public void resetSave(){
|
||||
current = null;
|
||||
}
|
||||
|
||||
public boolean isSaving(){
|
||||
return saving;
|
||||
}
|
||||
|
||||
public void zoneSave(){
|
||||
SaveSlot slot = new SaveSlot(zoneFile);
|
||||
slot.setName("zone");
|
||||
saves.remove(s -> s.file.equals(zoneFile));
|
||||
saves.add(slot);
|
||||
slot.save();
|
||||
}
|
||||
|
||||
public SaveSlot addSave(String name){
|
||||
SaveSlot slot = new SaveSlot(getNextSlotFile());
|
||||
slot.setName(name);
|
||||
saves.add(slot);
|
||||
slot.save();
|
||||
return slot;
|
||||
}
|
||||
|
||||
public SaveSlot importSave(Fi file) throws IOException{
|
||||
SaveSlot slot = new SaveSlot(getNextSlotFile());
|
||||
slot.importFile(file);
|
||||
slot.setName(file.nameWithoutExtension());
|
||||
saves.add(slot);
|
||||
slot.meta = SaveIO.getMeta(slot.file);
|
||||
current = slot;
|
||||
return slot;
|
||||
}
|
||||
|
||||
public SaveSlot getZoneSlot(){
|
||||
SaveSlot slot = getSaveSlots().find(s -> s.file.equals(zoneFile));
|
||||
return slot == null || slot.getZone() == null ? null : slot;
|
||||
}
|
||||
|
||||
public Fi getNextSlotFile(){
|
||||
int i = 0;
|
||||
Fi file;
|
||||
while((file = saveDirectory.child(i + "." + saveExtension)).exists()){
|
||||
i ++;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
public Array<SaveSlot> getSaveSlots(){
|
||||
return saves;
|
||||
}
|
||||
|
||||
public class SaveSlot{
|
||||
//public final int index;
|
||||
public final Fi file;
|
||||
boolean requestedPreview;
|
||||
SaveMeta meta;
|
||||
|
||||
public SaveSlot(Fi file){
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public void load() throws SaveException{
|
||||
try{
|
||||
SaveIO.load(file);
|
||||
meta = SaveIO.getMeta(file);
|
||||
current = this;
|
||||
totalPlaytime = meta.timePlayed;
|
||||
savePreview();
|
||||
}catch(Exception e){
|
||||
throw new SaveException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(){
|
||||
long time = totalPlaytime;
|
||||
long prev = totalPlaytime;
|
||||
totalPlaytime = time;
|
||||
|
||||
SaveIO.save(file);
|
||||
meta = SaveIO.getMeta(file);
|
||||
if(!state.is(State.menu)){
|
||||
current = this;
|
||||
}
|
||||
|
||||
totalPlaytime = prev;
|
||||
savePreview();
|
||||
}
|
||||
|
||||
private void savePreview(){
|
||||
if(Core.assets.isLoaded(loadPreviewFile().path())){
|
||||
Core.assets.unload(loadPreviewFile().path());
|
||||
}
|
||||
previewExecutor.submit(() -> {
|
||||
try{
|
||||
previewFile().writePNG(renderer.minimap.getPixmap());
|
||||
requestedPreview = false;
|
||||
}catch(Throwable t){
|
||||
t.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Texture previewTexture(){
|
||||
if(!previewFile().exists()){
|
||||
return null;
|
||||
}else if(Core.assets.isLoaded(loadPreviewFile().path())){
|
||||
return Core.assets.get(loadPreviewFile().path());
|
||||
}else if(!requestedPreview){
|
||||
Core.assets.load(new AssetDescriptor<>(loadPreviewFile(), Texture.class));
|
||||
requestedPreview = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String index(){
|
||||
return file.nameWithoutExtension();
|
||||
}
|
||||
|
||||
private Fi previewFile(){
|
||||
return mapPreviewDirectory.child("save_slot_" + index() + ".png");
|
||||
}
|
||||
|
||||
private Fi loadPreviewFile(){
|
||||
return previewFile().sibling(previewFile().name() + ".spreview");
|
||||
}
|
||||
|
||||
public boolean isHidden(){
|
||||
return getZone() != null;
|
||||
}
|
||||
|
||||
public String getPlayTime(){
|
||||
return Strings.formatMillis(current == this ? totalPlaytime : meta.timePlayed);
|
||||
}
|
||||
|
||||
public long getTimestamp(){
|
||||
return meta.timestamp;
|
||||
}
|
||||
|
||||
public String getDate(){
|
||||
return SimpleDateFormat.getDateTimeInstance().format(new Date(meta.timestamp));
|
||||
}
|
||||
|
||||
public Map getMap(){
|
||||
return meta.map;
|
||||
}
|
||||
|
||||
public void cautiousLoad(Runnable run){
|
||||
Array<String> mods = Array.with(getMods());
|
||||
mods.removeAll(Vars.mods.getModStrings());
|
||||
|
||||
if(!mods.isEmpty()){
|
||||
ui.showConfirm("$warning", Core.bundle.format("mod.missing", mods.toString("\n")), run);
|
||||
}else{
|
||||
run.run();
|
||||
}
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return Core.settings.getString("save-" + index() + "-name", "untitled");
|
||||
}
|
||||
|
||||
public void setName(String name){
|
||||
Core.settings.put("save-" + index() + "-name", name);
|
||||
Core.settings.save();
|
||||
}
|
||||
|
||||
public String[] getMods(){
|
||||
return meta.mods;
|
||||
}
|
||||
|
||||
public Zone getZone(){
|
||||
return meta == null || meta.rules == null ? null : meta.rules.zone;
|
||||
}
|
||||
|
||||
public Gamemode mode(){
|
||||
return Gamemode.bestFit(meta.rules);
|
||||
}
|
||||
|
||||
public int getBuild(){
|
||||
return meta.build;
|
||||
}
|
||||
|
||||
public int getWave(){
|
||||
return meta.wave;
|
||||
}
|
||||
|
||||
public boolean isAutosave(){
|
||||
return Core.settings.getBool("save-" + index() + "-autosave", true);
|
||||
}
|
||||
|
||||
public void setAutosave(boolean save){
|
||||
Core.settings.put("save-" + index() + "-autosave", save);
|
||||
Core.settings.save();
|
||||
}
|
||||
|
||||
public void importFile(Fi from) throws IOException{
|
||||
try{
|
||||
from.copyTo(file);
|
||||
}catch(Exception e){
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void exportFile(Fi to) throws IOException{
|
||||
try{
|
||||
file.copyTo(to);
|
||||
}catch(Exception e){
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(){
|
||||
file.delete();
|
||||
saves.removeValue(this, true);
|
||||
if(this == current){
|
||||
current = null;
|
||||
}
|
||||
|
||||
if(Core.assets.isLoaded(loadPreviewFile().path())){
|
||||
Core.assets.unload(loadPreviewFile().path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
core/src/mindustry/game/Schematic.java
Normal file
130
core/src/mindustry/game/Schematic.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.struct.*;
|
||||
import arc.struct.IntIntMap.*;
|
||||
import arc.files.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import mindustry.*;
|
||||
import mindustry.mod.Mods.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class Schematic implements Publishable, Comparable<Schematic>{
|
||||
public final Array<Stile> tiles;
|
||||
public StringMap tags;
|
||||
public int width, height;
|
||||
public @Nullable
|
||||
Fi file;
|
||||
/** Associated mod. If null, no mod is associated with this schematic. */
|
||||
public @Nullable LoadedMod mod;
|
||||
|
||||
public Schematic(Array<Stile> tiles, @NonNull StringMap tags, int width, int height){
|
||||
this.tiles = tiles;
|
||||
this.tags = tags;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Array<ItemStack> requirements(){
|
||||
IntIntMap amounts = new IntIntMap();
|
||||
|
||||
tiles.each(t -> {
|
||||
for(ItemStack stack : t.block.requirements){
|
||||
amounts.getAndIncrement(stack.item.id, 0, stack.amount);
|
||||
}
|
||||
});
|
||||
Array<ItemStack> stacks = new Array<>();
|
||||
for(Entry ent : amounts.entries()){
|
||||
stacks.add(new ItemStack(Vars.content.item(ent.key), ent.value));
|
||||
}
|
||||
stacks.sort();
|
||||
return stacks;
|
||||
}
|
||||
|
||||
public boolean hasCore(){
|
||||
return tiles.contains(s -> s.block instanceof CoreBlock);
|
||||
}
|
||||
|
||||
public @NonNull CoreBlock findCore(){
|
||||
CoreBlock block = (CoreBlock)tiles.find(s -> s.block instanceof CoreBlock).block;
|
||||
if(block == null) throw new IllegalArgumentException("Schematic is missing a core!");
|
||||
return block;
|
||||
}
|
||||
|
||||
public String name(){
|
||||
return tags.get("name", "unknown");
|
||||
}
|
||||
|
||||
public void save(){
|
||||
schematics.saveChanges(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSteamID(){
|
||||
return tags.get("steamid");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSteamID(String id){
|
||||
tags.put("steamid", id);
|
||||
save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSteamID(){
|
||||
tags.remove("steamid");
|
||||
save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String steamTitle(){
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String steamDescription(){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String steamTag(){
|
||||
return "schematic";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fi createSteamFolder(String id){
|
||||
Fi directory = tmpDirectory.child("schematic_" + id).child("schematic." + schematicExtension);
|
||||
file.copyTo(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fi createSteamPreview(String id){
|
||||
Fi preview = tmpDirectory.child("schematic_preview_" + id + ".png");
|
||||
schematics.savePreview(this, preview);
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Schematic schematic){
|
||||
return name().compareTo(schematic.name());
|
||||
}
|
||||
|
||||
public static class Stile{
|
||||
public @NonNull Block block;
|
||||
public short x, y;
|
||||
public int config;
|
||||
public byte rotation;
|
||||
|
||||
public Stile(Block block, int x, int y, int config, byte rotation){
|
||||
this.block = block;
|
||||
this.x = (short)x;
|
||||
this.y = (short)y;
|
||||
this.config = config;
|
||||
this.rotation = rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
468
core/src/mindustry/game/Schematics.java
Normal file
468
core/src/mindustry/game/Schematics.java
Normal file
@@ -0,0 +1,468 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.assets.*;
|
||||
import arc.struct.*;
|
||||
import arc.files.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.gl.*;
|
||||
import arc.util.*;
|
||||
import arc.util.ArcAnnotate.*;
|
||||
import arc.util.io.Streams.*;
|
||||
import arc.util.serialization.*;
|
||||
import mindustry.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.traits.BuilderTrait.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.game.Schematic.*;
|
||||
import mindustry.input.*;
|
||||
import mindustry.input.Placement.*;
|
||||
import mindustry.world.*;
|
||||
import mindustry.world.blocks.*;
|
||||
import mindustry.world.blocks.production.*;
|
||||
import mindustry.world.blocks.storage.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Handles schematics.*/
|
||||
public class Schematics implements Loadable{
|
||||
public static final String base64Header = "bXNjaAB";
|
||||
|
||||
private static final byte[] header = {'m', 's', 'c', 'h'};
|
||||
private static final byte version = 0;
|
||||
|
||||
private static final int padding = 2;
|
||||
private static final int maxPreviewsMobile = 32;
|
||||
private static final int resolution = 32;
|
||||
|
||||
private OptimizedByteArrayOutputStream out = new OptimizedByteArrayOutputStream(1024);
|
||||
private Array<Schematic> all = new Array<>();
|
||||
private OrderedMap<Schematic, FrameBuffer> previews = new OrderedMap<>();
|
||||
private FrameBuffer shadowBuffer;
|
||||
private long lastClearTime;
|
||||
|
||||
public Schematics(){
|
||||
Events.on(DisposeEvent.class, e -> {
|
||||
previews.each((schem, m) -> m.dispose());
|
||||
previews.clear();
|
||||
shadowBuffer.dispose();
|
||||
});
|
||||
|
||||
Events.on(ContentReloadEvent.class, event -> {
|
||||
previews.each((schem, m) -> m.dispose());
|
||||
previews.clear();
|
||||
load();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSync(){
|
||||
load();
|
||||
}
|
||||
|
||||
/** Load all schematics in the folder immediately.*/
|
||||
public void load(){
|
||||
all.clear();
|
||||
|
||||
for(Fi file : schematicDirectory.list()){
|
||||
loadFile(file);
|
||||
}
|
||||
|
||||
platform.getWorkshopContent(Schematic.class).each(this::loadFile);
|
||||
|
||||
//mod-specific schematics, cannot be removed
|
||||
mods.listFiles("schematics", (mod, file) -> {
|
||||
Schematic s = loadFile(file);
|
||||
if(s != null){
|
||||
s.mod = mod;
|
||||
}
|
||||
});
|
||||
|
||||
all.sort();
|
||||
|
||||
if(shadowBuffer == null){
|
||||
Core.app.post(() -> shadowBuffer = new FrameBuffer(maxSchematicSize + padding + 8, maxSchematicSize + padding + 8));
|
||||
}
|
||||
}
|
||||
|
||||
public void overwrite(Schematic target, Schematic newSchematic){
|
||||
if(previews.containsKey(target)){
|
||||
previews.get(target).dispose();
|
||||
previews.remove(target);
|
||||
}
|
||||
|
||||
target.tiles.clear();
|
||||
target.tiles.addAll(newSchematic.tiles);
|
||||
target.width = newSchematic.width;
|
||||
target.height = newSchematic.height;
|
||||
newSchematic.tags.putAll(target.tags);
|
||||
newSchematic.file = target.file;
|
||||
|
||||
try{
|
||||
write(newSchematic, target.file);
|
||||
}catch(Exception e){
|
||||
Log.err(e);
|
||||
ui.showException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Schematic loadFile(Fi file){
|
||||
if(!file.extension().equals(schematicExtension)) return null;
|
||||
|
||||
try{
|
||||
Schematic s = read(file);
|
||||
all.add(s);
|
||||
|
||||
//external file from workshop
|
||||
if(!s.file.parent().equals(schematicDirectory)){
|
||||
s.tags.put("steamid", s.file.parent().name());
|
||||
}
|
||||
|
||||
return s;
|
||||
}catch(IOException e){
|
||||
Log.err(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Array<Schematic> all(){
|
||||
return all;
|
||||
}
|
||||
|
||||
public void saveChanges(Schematic s){
|
||||
if(s.file != null){
|
||||
try{
|
||||
write(s, s.file);
|
||||
}catch(Exception e){
|
||||
ui.showException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void savePreview(Schematic schematic, Fi file){
|
||||
FrameBuffer buffer = getBuffer(schematic);
|
||||
Draw.flush();
|
||||
buffer.begin();
|
||||
Pixmap pixmap = ScreenUtils.getFrameBufferPixmap(0, 0, buffer.getWidth(), buffer.getHeight());
|
||||
file.writePNG(pixmap);
|
||||
buffer.end();
|
||||
}
|
||||
|
||||
public Texture getPreview(Schematic schematic){
|
||||
return getBuffer(schematic).getTexture();
|
||||
}
|
||||
|
||||
public boolean hasPreview(Schematic schematic){
|
||||
return previews.containsKey(schematic);
|
||||
}
|
||||
|
||||
public FrameBuffer getBuffer(Schematic schematic){
|
||||
//dispose unneeded previews to prevent memory outage errors.
|
||||
//only runs every 2 seconds
|
||||
if(mobile && Time.timeSinceMillis(lastClearTime) > 1000 * 2 && previews.size > maxPreviewsMobile){
|
||||
Array<Schematic> keys = previews.orderedKeys().copy();
|
||||
for(int i = 0; i < previews.size - maxPreviewsMobile; i++){
|
||||
//dispose and remove unneeded previews
|
||||
previews.get(keys.get(i)).dispose();
|
||||
previews.remove(keys.get(i));
|
||||
}
|
||||
//update last clear time
|
||||
lastClearTime = Time.millis();
|
||||
}
|
||||
|
||||
if(!previews.containsKey(schematic)){
|
||||
Draw.blend();
|
||||
Draw.reset();
|
||||
Tmp.m1.set(Draw.proj());
|
||||
Tmp.m2.set(Draw.trans());
|
||||
FrameBuffer buffer = new FrameBuffer((schematic.width + padding) * resolution, (schematic.height + padding) * resolution);
|
||||
|
||||
shadowBuffer.beginDraw(Color.clear);
|
||||
|
||||
Draw.trans().idt();
|
||||
Draw.proj().setOrtho(0, 0, shadowBuffer.getWidth(), shadowBuffer.getHeight());
|
||||
|
||||
Draw.color();
|
||||
schematic.tiles.each(t -> {
|
||||
int size = t.block.size;
|
||||
int offsetx = -(size - 1) / 2;
|
||||
int offsety = -(size - 1) / 2;
|
||||
for(int dx = 0; dx < size; dx++){
|
||||
for(int dy = 0; dy < size; dy++){
|
||||
int wx = t.x + dx + offsetx;
|
||||
int wy = t.y + dy + offsety;
|
||||
Fill.square(padding/2f + wx + 0.5f, padding/2f + wy + 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shadowBuffer.endDraw();
|
||||
|
||||
buffer.beginDraw(Color.clear);
|
||||
|
||||
Draw.proj().setOrtho(0, buffer.getHeight(), buffer.getWidth(), -buffer.getHeight());
|
||||
|
||||
Tmp.tr1.set(shadowBuffer.getTexture(), 0, 0, schematic.width + padding, schematic.height + padding);
|
||||
Draw.color(0f, 0f, 0f, 1f);
|
||||
Draw.rect(Tmp.tr1, buffer.getWidth()/2f, buffer.getHeight()/2f, buffer.getWidth(), -buffer.getHeight());
|
||||
Draw.color();
|
||||
|
||||
Array<BuildRequest> requests = schematic.tiles.map(t -> new BuildRequest(t.x, t.y, t.rotation, t.block).configure(t.config));
|
||||
|
||||
Draw.flush();
|
||||
//scale each request to fit schematic
|
||||
Draw.trans().scale(resolution / tilesize, resolution / tilesize).translate(tilesize*1.5f, tilesize*1.5f);
|
||||
|
||||
//draw requests
|
||||
requests.each(req -> {
|
||||
req.animScale = 1f;
|
||||
req.worldContext = false;
|
||||
req.block.drawRequestRegion(req, requests::each);
|
||||
});
|
||||
|
||||
requests.each(req -> req.block.drawRequestConfigTop(req, requests::each));
|
||||
|
||||
Draw.flush();
|
||||
Draw.trans().idt();
|
||||
|
||||
buffer.endDraw();
|
||||
|
||||
Draw.proj(Tmp.m1);
|
||||
Draw.trans(Tmp.m2);
|
||||
|
||||
previews.put(schematic, buffer);
|
||||
}
|
||||
|
||||
return previews.get(schematic);
|
||||
}
|
||||
|
||||
/** Creates an array of build requests from a schematic's data, centered on the provided x+y coordinates. */
|
||||
public Array<BuildRequest> toRequests(Schematic schem, int x, int y){
|
||||
return schem.tiles.map(t -> new BuildRequest(t.x + x - schem.width/2, t.y + y - schem.height/2, t.rotation, t.block).original(t.x, t.y, schem.width, schem.height).configure(t.config))
|
||||
.removeAll(s -> !s.block.isVisible() || !s.block.unlockedCur());
|
||||
}
|
||||
|
||||
public void placeLoadout(Schematic schem, int x, int y){
|
||||
Stile coreTile = schem.tiles.find(s -> s.block instanceof CoreBlock);
|
||||
int ox = x - coreTile.x, oy = y - coreTile.y;
|
||||
schem.tiles.each(st -> {
|
||||
Tile tile = world.tile(st.x + ox, st.y + oy);
|
||||
if(tile == null) return;
|
||||
|
||||
world.setBlock(tile, st.block, defaultTeam);
|
||||
tile.rotation(st.rotation);
|
||||
if(st.block.posConfig){
|
||||
tile.configureAny(Pos.get(tile.x - st.x + Pos.x(st.config), tile.y - st.y + Pos.y(st.config)));
|
||||
}else{
|
||||
tile.configureAny(st.config);
|
||||
}
|
||||
|
||||
if(st.block instanceof Drill){
|
||||
tile.getLinkedTiles(t -> t.setOverlay(Blocks.oreCopper));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds a schematic to the list, also copying it into the files.*/
|
||||
public void add(Schematic schematic){
|
||||
all.add(schematic);
|
||||
try{
|
||||
Fi file = schematicDirectory.child(Time.millis() + "." + schematicExtension);
|
||||
write(schematic, file);
|
||||
schematic.file = file;
|
||||
}catch(Exception e){
|
||||
ui.showException(e);
|
||||
Log.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(Schematic s){
|
||||
all.remove(s);
|
||||
if(s.file != null){
|
||||
s.file.delete();
|
||||
}
|
||||
|
||||
if(previews.containsKey(s)){
|
||||
previews.get(s).dispose();
|
||||
previews.remove(s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a schematic from a world selection. */
|
||||
public Schematic create(int x, int y, int x2, int y2){
|
||||
NormalizeResult result = Placement.normalizeArea(x, y, x2, y2, 0, false, maxSchematicSize);
|
||||
x = result.x;
|
||||
y = result.y;
|
||||
x2 = result.x2;
|
||||
y2 = result.y2;
|
||||
|
||||
int ox = x, oy = y, ox2 = x2, oy2 = y2;
|
||||
|
||||
Array<Stile> tiles = new Array<>();
|
||||
|
||||
int minx = x2, miny = y2, maxx = x, maxy = y;
|
||||
boolean found = false;
|
||||
for(int cx = x; cx <= x2; cx++){
|
||||
for(int cy = y; cy <= y2; cy++){
|
||||
Tile linked = world.ltile(cx, cy);
|
||||
|
||||
if(linked != null && linked.entity != null && linked.entity.block.isVisible() && !(linked.block() instanceof BuildBlock)){
|
||||
int top = linked.block().size/2;
|
||||
int bot = linked.block().size % 2 == 1 ? -linked.block().size/2 : -(linked.block().size - 1)/2;
|
||||
minx = Math.min(linked.x + bot, minx);
|
||||
miny = Math.min(linked.y + bot, miny);
|
||||
maxx = Math.max(linked.x + top, maxx);
|
||||
maxy = Math.max(linked.y + top, maxy);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(found){
|
||||
x = minx;
|
||||
y = miny;
|
||||
x2 = maxx;
|
||||
y2 = maxy;
|
||||
}else{
|
||||
return new Schematic(new Array<>(), new StringMap(), 1, 1);
|
||||
}
|
||||
|
||||
int width = x2 - x + 1, height = y2 - y + 1;
|
||||
int offsetX = -x, offsetY = -y;
|
||||
IntSet counted = new IntSet();
|
||||
for(int cx = ox; cx <= ox2; cx++){
|
||||
for(int cy = oy; cy <= oy2; cy++){
|
||||
Tile tile = world.ltile(cx, cy);
|
||||
|
||||
if(tile != null && tile.entity != null && !counted.contains(tile.pos()) && !(tile.block() instanceof BuildBlock) && tile.entity.block.isVisible()){
|
||||
int config = tile.entity.config();
|
||||
if(tile.block().posConfig){
|
||||
config = Pos.get(Pos.x(config) + offsetX, Pos.y(config) + offsetY);
|
||||
}
|
||||
|
||||
tiles.add(new Stile(tile.block(), tile.x + offsetX, tile.y + offsetY, config, tile.rotation()));
|
||||
counted.add(tile.pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Schematic(tiles, new StringMap(), width, height);
|
||||
}
|
||||
|
||||
/** Converts a schematic to base64. Note that the result of this will always start with 'bXNjaAB'.*/
|
||||
public String writeBase64(Schematic schematic){
|
||||
try{
|
||||
out.reset();
|
||||
write(schematic, out);
|
||||
return new String(Base64Coder.encode(out.getBuffer(), out.size()));
|
||||
}catch(IOException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
//region IO methods
|
||||
|
||||
/** Loads a schematic from base64. May throw an exception. */
|
||||
public static Schematic readBase64(String schematic) throws IOException{
|
||||
return read(new ByteArrayInputStream(Base64Coder.decode(schematic)));
|
||||
}
|
||||
|
||||
public static Schematic read(Fi file) throws IOException{
|
||||
Schematic s = read(new DataInputStream(file.read(1024)));
|
||||
if(!s.tags.containsKey("name")){
|
||||
s.tags.put("name", file.nameWithoutExtension());
|
||||
}
|
||||
s.file = file;
|
||||
return s;
|
||||
}
|
||||
|
||||
public static Schematic read(InputStream input) throws IOException{
|
||||
for(byte b : header){
|
||||
if(input.read() != b){
|
||||
throw new IOException("Not a schematic file (missing header).");
|
||||
}
|
||||
}
|
||||
|
||||
int ver;
|
||||
if((ver = input.read()) != version){
|
||||
throw new IOException("Unknown version: " + ver);
|
||||
}
|
||||
|
||||
try(DataInputStream stream = new DataInputStream(new InflaterInputStream(input))){
|
||||
short width = stream.readShort(), height = stream.readShort();
|
||||
|
||||
StringMap map = new StringMap();
|
||||
byte tags = stream.readByte();
|
||||
for(int i = 0; i < tags; i++){
|
||||
map.put(stream.readUTF(), stream.readUTF());
|
||||
}
|
||||
|
||||
IntMap<Block> blocks = new IntMap<>();
|
||||
byte length = stream.readByte();
|
||||
for(int i = 0; i < length; i++){
|
||||
Block block = Vars.content.getByName(ContentType.block, stream.readUTF());
|
||||
blocks.put(i, block == null ? Blocks.air : block);
|
||||
}
|
||||
|
||||
int total = stream.readInt();
|
||||
Array<Stile> tiles = new Array<>(total);
|
||||
for(int i = 0; i < total; i++){
|
||||
Block block = blocks.get(stream.readByte());
|
||||
int position = stream.readInt();
|
||||
int config = stream.readInt();
|
||||
byte rotation = stream.readByte();
|
||||
if(block != Blocks.air){
|
||||
tiles.add(new Stile(block, Pos.x(position), Pos.y(position), config, rotation));
|
||||
}
|
||||
}
|
||||
|
||||
return new Schematic(tiles, map, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
public static void write(Schematic schematic, Fi file) throws IOException{
|
||||
write(schematic, file.write(false, 1024));
|
||||
}
|
||||
|
||||
public static void write(Schematic schematic, OutputStream output) throws IOException{
|
||||
output.write(header);
|
||||
output.write(version);
|
||||
|
||||
try(DataOutputStream stream = new DataOutputStream(new DeflaterOutputStream(output))){
|
||||
|
||||
stream.writeShort(schematic.width);
|
||||
stream.writeShort(schematic.height);
|
||||
|
||||
stream.writeByte(schematic.tags.size);
|
||||
for(ObjectMap.Entry<String, String> e : schematic.tags.entries()){
|
||||
stream.writeUTF(e.key);
|
||||
stream.writeUTF(e.value);
|
||||
}
|
||||
|
||||
OrderedSet<Block> blocks = new OrderedSet<>();
|
||||
schematic.tiles.each(t -> blocks.add(t.block));
|
||||
|
||||
//create dictionary
|
||||
stream.writeByte(blocks.size);
|
||||
for(int i = 0; i < blocks.size; i++){
|
||||
stream.writeUTF(blocks.orderedItems().get(i).name);
|
||||
}
|
||||
|
||||
stream.writeInt(schematic.tiles.size);
|
||||
//write each tile
|
||||
for(Stile tile : schematic.tiles){
|
||||
stream.writeByte(blocks.orderedItems().indexOf(tile.block));
|
||||
stream.writeInt(Pos.get(tile.x, tile.y));
|
||||
stream.writeInt(tile.config);
|
||||
stream.writeByte(tile.rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
50
core/src/mindustry/game/SoundLoop.java
Normal file
50
core/src/mindustry/game/SoundLoop.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.audio.*;
|
||||
import arc.math.*;
|
||||
import arc.util.*;
|
||||
|
||||
/** A simple class for playing a looping sound at a position.*/
|
||||
public class SoundLoop{
|
||||
private static final float fadeSpeed = 0.05f;
|
||||
|
||||
private final Sound sound;
|
||||
private int id = -1;
|
||||
private float volume, baseVolume;
|
||||
|
||||
public SoundLoop(Sound sound, float baseVolume){
|
||||
this.sound = sound;
|
||||
this.baseVolume = baseVolume;
|
||||
}
|
||||
|
||||
public void update(float x, float y, boolean play){
|
||||
if(baseVolume < 0) return;
|
||||
|
||||
if(id < 0){
|
||||
if(play){
|
||||
id = sound.loop(sound.calcVolume(x, y) * volume * baseVolume, 1f, sound.calcPan(x, y));
|
||||
}
|
||||
}else{
|
||||
//fade the sound in or out
|
||||
if(play){
|
||||
volume = Mathf.clamp(volume + fadeSpeed * Time.delta());
|
||||
}else{
|
||||
volume = Mathf.clamp(volume - fadeSpeed * Time.delta());
|
||||
if(volume <= 0.001f){
|
||||
sound.stop(id);
|
||||
id = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
sound.setPan(id, sound.calcPan(x, y), sound.calcVolume(x, y) * volume * baseVolume);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(){
|
||||
if(id != -1){
|
||||
sound.stop(id);
|
||||
id = -1;
|
||||
volume = baseVolume = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
core/src/mindustry/game/SpawnGroup.java
Normal file
113
core/src/mindustry/game/SpawnGroup.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.util.serialization.Json;
|
||||
import arc.util.serialization.Json.Serializable;
|
||||
import arc.util.serialization.JsonValue;
|
||||
import mindustry.content.*;
|
||||
import mindustry.ctype.ContentType;
|
||||
import mindustry.entities.type.BaseUnit;
|
||||
import mindustry.type.*;
|
||||
|
||||
import static mindustry.Vars.content;
|
||||
|
||||
/**
|
||||
* A spawn group defines spawn information for a specific type of unit, with optional extra information like
|
||||
* weapon equipped, ammo used, and status effects.
|
||||
* Each spawn group can have multiple sub-groups spawned in different areas of the map.
|
||||
*/
|
||||
public class SpawnGroup implements Serializable{
|
||||
public static final int never = Integer.MAX_VALUE;
|
||||
|
||||
/** The unit type spawned */
|
||||
public UnitType type;
|
||||
/** When this spawn should end */
|
||||
public int end = never;
|
||||
/** When this spawn should start */
|
||||
public int begin;
|
||||
/** The spacing, in waves, of spawns. For example, 2 = spawns every other wave */
|
||||
public int spacing = 1;
|
||||
/** Maximum amount of units that spawn */
|
||||
public int max = 100;
|
||||
/** How many waves need to pass before the amount of units spawned increases by 1 */
|
||||
public float unitScaling = never;
|
||||
/** Amount of enemies spawned initially, with no scaling */
|
||||
public int unitAmount = 1;
|
||||
/** Status effect applied to the spawned unit. Null to disable. */
|
||||
public StatusEffect effect;
|
||||
/** Items this unit spawns with. Null to disable. */
|
||||
public ItemStack items;
|
||||
|
||||
public SpawnGroup(UnitType type){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public SpawnGroup(){
|
||||
//serialization use only
|
||||
}
|
||||
|
||||
/** Returns the amount of units spawned on a specific wave. */
|
||||
public int getUnitsSpawned(int wave){
|
||||
if(wave < begin || wave > end || (wave - begin) % spacing != 0){
|
||||
return 0;
|
||||
}
|
||||
return Math.min(unitAmount + (int)(((wave - begin) / spacing) / unitScaling), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unit, and assigns correct values based on this group's data.
|
||||
* This method does not add() the unit.
|
||||
*/
|
||||
public BaseUnit createUnit(Team team){
|
||||
BaseUnit unit = type.create(team);
|
||||
|
||||
if(effect != null){
|
||||
unit.applyEffect(effect, 999999f);
|
||||
}
|
||||
|
||||
if(items != null){
|
||||
unit.addItem(items.item, items.amount);
|
||||
}
|
||||
|
||||
return unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(Json json){
|
||||
json.writeValue("type", type.name);
|
||||
if(begin != 0) json.writeValue("begin", begin);
|
||||
if(end != never) json.writeValue("end", end);
|
||||
if(spacing != 1) json.writeValue("spacing", spacing);
|
||||
//if(max != 40) json.writeValue("max", max);
|
||||
if(unitScaling != never) json.writeValue("scaling", unitScaling);
|
||||
if(unitAmount != 1) json.writeValue("amount", unitAmount);
|
||||
if(effect != null) json.writeValue("effect", effect.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(Json json, JsonValue data){
|
||||
type = content.getByName(ContentType.unit, data.getString("type", "dagger"));
|
||||
if(type == null) type = UnitTypes.dagger;
|
||||
begin = data.getInt("begin", 0);
|
||||
end = data.getInt("end", never);
|
||||
spacing = data.getInt("spacing", 1);
|
||||
//max = data.getInt("max", 40);
|
||||
unitScaling = data.getFloat("scaling", never);
|
||||
unitAmount = data.getInt("amount", 1);
|
||||
effect = content.getByID(ContentType.status, data.getInt("effect", -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "SpawnGroup{" +
|
||||
"type=" + type +
|
||||
", end=" + end +
|
||||
", begin=" + begin +
|
||||
", spacing=" + spacing +
|
||||
", max=" + max +
|
||||
", unitScaling=" + unitScaling +
|
||||
", unitAmount=" + unitAmount +
|
||||
", effect=" + effect +
|
||||
", items=" + items +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
72
core/src/mindustry/game/Stats.java
Normal file
72
core/src/mindustry/game/Stats.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package mindustry.game;
|
||||
|
||||
import mindustry.annotations.Annotations.Serialize;
|
||||
import arc.struct.Array;
|
||||
import arc.struct.ObjectIntMap;
|
||||
import arc.math.Mathf;
|
||||
import mindustry.type.*;
|
||||
|
||||
@Serialize
|
||||
public class Stats{
|
||||
/** Items delivered to global resoure counter. Zones only. */
|
||||
public ObjectIntMap<Item> itemsDelivered = new ObjectIntMap<>();
|
||||
/** Enemy (red team) units destroyed. */
|
||||
public int enemyUnitsDestroyed;
|
||||
/** Total waves lasted. */
|
||||
public int wavesLasted;
|
||||
/** Total (ms) time lasted in this save/zone. */
|
||||
public long timeLasted;
|
||||
/** Friendly buildings fully built. */
|
||||
public int buildingsBuilt;
|
||||
/** Friendly buildings fully deconstructed. */
|
||||
public int buildingsDeconstructed;
|
||||
/** Friendly buildings destroyed. */
|
||||
public int buildingsDestroyed;
|
||||
|
||||
public RankResult calculateRank(Zone zone, boolean launched){
|
||||
float score = 0;
|
||||
|
||||
if(launched && zone.getRules().attackMode){
|
||||
score += 3f;
|
||||
}else if(wavesLasted >= zone.conditionWave){
|
||||
//each new launch period adds onto the rank 'points'
|
||||
score += (float)((wavesLasted - zone.conditionWave) / zone.launchPeriod + 1) * 1.2f;
|
||||
}
|
||||
|
||||
int capacity = zone.loadout.findCore().itemCapacity;
|
||||
|
||||
//weigh used fractions
|
||||
float frac = 0f;
|
||||
Array<Item> obtainable = Array.with(zone.resources).select(i -> i.type == ItemType.material);
|
||||
for(Item item : obtainable){
|
||||
frac += Mathf.clamp((float)itemsDelivered.get(item, 0) / capacity) / (float)obtainable.size;
|
||||
}
|
||||
|
||||
score += frac * 1.6f;
|
||||
|
||||
if(!launched){
|
||||
score *= 0.5f;
|
||||
}
|
||||
|
||||
int rankIndex = Mathf.clamp((int)(score), 0, Rank.values().length - 1);
|
||||
Rank rank = Rank.values()[rankIndex];
|
||||
String sign = Math.abs((rankIndex + 0.5f) - score) < 0.2f || rank.name().contains("S") ? "" : (rankIndex + 0.5f) < score ? "-" : "+";
|
||||
|
||||
return new RankResult(rank, sign);
|
||||
}
|
||||
|
||||
public static class RankResult{
|
||||
public final Rank rank;
|
||||
/** + or - */
|
||||
public final String modifier;
|
||||
|
||||
public RankResult(Rank rank, String modifier){
|
||||
this.rank = rank;
|
||||
this.modifier = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Rank{
|
||||
F, D, C, B, A, S, SS
|
||||
}
|
||||
}
|
||||
27
core/src/mindustry/game/Team.java
Normal file
27
core/src/mindustry/game/Team.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.Core;
|
||||
import arc.graphics.Color;
|
||||
import mindustry.graphics.*;
|
||||
|
||||
public enum Team{
|
||||
derelict(Color.valueOf("4d4e58")),
|
||||
sharded(Pal.accent),
|
||||
crux(Color.valueOf("e82d2d")),
|
||||
green(Color.valueOf("4dd98b")),
|
||||
purple(Color.valueOf("9a4bdf")),
|
||||
blue(Color.royal.cpy());
|
||||
|
||||
public final static Team[] all = values();
|
||||
public final Color color;
|
||||
public final int intColor;
|
||||
|
||||
Team(Color color){
|
||||
this.color = color;
|
||||
intColor = Color.rgba8888(color);
|
||||
}
|
||||
|
||||
public String localized(){
|
||||
return Core.bundle.get("team." + name() + ".name");
|
||||
}
|
||||
}
|
||||
76
core/src/mindustry/game/Teams.java
Normal file
76
core/src/mindustry/game/Teams.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.struct.*;
|
||||
import mindustry.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
/** Class for various team-based utilities. */
|
||||
public class Teams{
|
||||
private TeamData[] map = new TeamData[Team.all.length];
|
||||
|
||||
/**
|
||||
* Register a team.
|
||||
* @param team The team type enum.
|
||||
* @param enemies The array of enemies of this team. Any team not in this array is considered neutral.
|
||||
*/
|
||||
public void add(Team team, Team... enemies){
|
||||
map[team.ordinal()] = new TeamData(team, EnumSet.of(enemies));
|
||||
}
|
||||
|
||||
/** Returns team data by type. */
|
||||
public TeamData get(Team team){
|
||||
if(map[team.ordinal()] == null){
|
||||
add(team, Array.with(Team.all).select(t -> t != team).toArray(Team.class));
|
||||
}
|
||||
return map[team.ordinal()];
|
||||
}
|
||||
|
||||
/** Returns whether a team is active, e.g. whether it has any cores remaining. */
|
||||
public boolean isActive(Team team){
|
||||
//the enemy wave team is always active
|
||||
return team == Vars.waveTeam || get(team).cores.size > 0;
|
||||
}
|
||||
|
||||
/** Returns a set of all teams that are enemies of this team. */
|
||||
public EnumSet<Team> enemiesOf(Team team){
|
||||
return get(team).enemies;
|
||||
}
|
||||
|
||||
/** Returns whether {@param other} is an enemy of {@param #team}. */
|
||||
public boolean areEnemies(Team team, Team other){
|
||||
return enemiesOf(team).contains(other);
|
||||
}
|
||||
|
||||
/** Allocates a new array with the active teams.
|
||||
* Never call in the main game loop.*/
|
||||
public Array<TeamData> getActive(){
|
||||
return Array.select(map, t -> t != null);
|
||||
}
|
||||
|
||||
public static class TeamData{
|
||||
public final ObjectSet<Tile> cores = new ObjectSet<>();
|
||||
public final EnumSet<Team> enemies;
|
||||
public final Team team;
|
||||
public Queue<BrokenBlock> brokenBlocks = new Queue<>();
|
||||
|
||||
public TeamData(Team team, EnumSet<Team> enemies){
|
||||
this.team = team;
|
||||
this.enemies = enemies;
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a block made by this team that was destroyed somewhere on the map.
|
||||
* This does not include deconstructed blocks.*/
|
||||
public static class BrokenBlock{
|
||||
public final short x, y, rotation, block;
|
||||
public final int config;
|
||||
|
||||
public BrokenBlock(short x, short y, short rotation, short block, int config){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.block = block;
|
||||
this.config = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
308
core/src/mindustry/game/Tutorial.java
Normal file
308
core/src/mindustry/game/Tutorial.java
Normal file
@@ -0,0 +1,308 @@
|
||||
package mindustry.game;
|
||||
|
||||
import arc.*;
|
||||
import arc.struct.*;
|
||||
import arc.func.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
import arc.scene.*;
|
||||
import arc.scene.ui.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.type.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
/** Handles tutorial state. */
|
||||
public class Tutorial{
|
||||
private static final int mineCopper = 18;
|
||||
private static final int blocksToBreak = 3, blockOffset = -6;
|
||||
|
||||
private ObjectSet<String> events = new ObjectSet<>();
|
||||
private ObjectIntMap<Block> blocksPlaced = new ObjectIntMap<>();
|
||||
private int sentence;
|
||||
public TutorialStage stage = TutorialStage.values()[0];
|
||||
|
||||
public Tutorial(){
|
||||
Events.on(BlockBuildEndEvent.class, event -> {
|
||||
if(!event.breaking){
|
||||
blocksPlaced.getAndIncrement(event.tile.block(), 0, 1);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(LineConfirmEvent.class, event -> events.add("lineconfirm"));
|
||||
Events.on(TurretAmmoDeliverEvent.class, event -> events.add("ammo"));
|
||||
Events.on(CoreItemDeliverEvent.class, event -> events.add("coreitem"));
|
||||
Events.on(BlockInfoEvent.class, event -> events.add("blockinfo"));
|
||||
Events.on(DepositEvent.class, event -> events.add("deposit"));
|
||||
Events.on(WithdrawEvent.class, event -> events.add("withdraw"));
|
||||
|
||||
Events.on(ClientLoadEvent.class, e -> {
|
||||
for(TutorialStage stage : TutorialStage.values()){
|
||||
stage.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** update tutorial state, transition if needed */
|
||||
public void update(){
|
||||
if(stage.done.get() && !canNext()){
|
||||
next();
|
||||
}else{
|
||||
stage.update();
|
||||
}
|
||||
}
|
||||
|
||||
/** draw UI overlay */
|
||||
public void draw(){
|
||||
if(!Core.scene.hasDialog()){
|
||||
stage.draw();
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets tutorial state. */
|
||||
public void reset(){
|
||||
stage = TutorialStage.values()[0];
|
||||
stage.begin();
|
||||
blocksPlaced.clear();
|
||||
events.clear();
|
||||
sentence = 0;
|
||||
}
|
||||
|
||||
/** Goes on to the next tutorial step. */
|
||||
public void next(){
|
||||
stage = TutorialStage.values()[Mathf.clamp(stage.ordinal() + 1, 0, TutorialStage.values().length)];
|
||||
stage.begin();
|
||||
blocksPlaced.clear();
|
||||
events.clear();
|
||||
sentence = 0;
|
||||
}
|
||||
|
||||
public boolean canNext(){
|
||||
return sentence + 1 < stage.sentences.size;
|
||||
}
|
||||
|
||||
public void nextSentence(){
|
||||
if(canNext()){
|
||||
sentence ++;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canPrev(){
|
||||
return sentence > 0;
|
||||
}
|
||||
|
||||
public void prevSentence(){
|
||||
if(canPrev()){
|
||||
sentence --;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TutorialStage{
|
||||
intro(
|
||||
line -> Strings.format(line, item(Items.copper), mineCopper),
|
||||
() -> item(Items.copper) >= mineCopper
|
||||
),
|
||||
drill(() -> placed(Blocks.mechanicalDrill, 1)){
|
||||
void draw(){
|
||||
outline("category-production");
|
||||
outline("block-mechanical-drill");
|
||||
outline("confirmplace");
|
||||
}
|
||||
},
|
||||
blockinfo(() -> event("blockinfo")){
|
||||
void draw(){
|
||||
outline("category-production");
|
||||
outline("block-mechanical-drill");
|
||||
outline("blockinfo");
|
||||
}
|
||||
},
|
||||
conveyor(() -> placed(Blocks.conveyor, 2) && event("lineconfirm") && event("coreitem")){
|
||||
void draw(){
|
||||
outline("category-distribution");
|
||||
outline("block-conveyor");
|
||||
}
|
||||
},
|
||||
turret(() -> placed(Blocks.duo, 1)){
|
||||
void draw(){
|
||||
outline("category-turret");
|
||||
outline("block-duo");
|
||||
}
|
||||
},
|
||||
drillturret(() -> event("ammo")),
|
||||
pause(() -> state.isPaused()){
|
||||
void draw(){
|
||||
if(mobile){
|
||||
outline("pause");
|
||||
}
|
||||
}
|
||||
},
|
||||
unpause(() -> !state.isPaused()){
|
||||
void draw(){
|
||||
if(mobile){
|
||||
outline("pause");
|
||||
}
|
||||
}
|
||||
},
|
||||
breaking(TutorialStage::blocksBroken){
|
||||
void begin(){
|
||||
placeBlocks();
|
||||
}
|
||||
|
||||
void draw(){
|
||||
if(mobile){
|
||||
outline("breakmode");
|
||||
}
|
||||
}
|
||||
},
|
||||
withdraw(() -> event("withdraw")){
|
||||
void begin(){
|
||||
state.teams.get(defaultTeam).cores.first().entity.items.add(Items.copper, 10);
|
||||
}
|
||||
},
|
||||
deposit(() -> event("deposit")),
|
||||
waves(() -> state.wave > 2 && state.enemies() <= 0 && !spawner.isSpawning()){
|
||||
void begin(){
|
||||
state.rules.waveTimer = true;
|
||||
logic.runWave();
|
||||
}
|
||||
|
||||
void update(){
|
||||
if(state.wave > 2){
|
||||
state.rules.waveTimer = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
launch(() -> false){
|
||||
void begin(){
|
||||
state.rules.waveTimer = false;
|
||||
state.wave = 5;
|
||||
|
||||
//end tutorial, never show it again
|
||||
Events.fire(Trigger.tutorialComplete);
|
||||
Core.settings.put("playedtutorial", true);
|
||||
Core.settings.save();
|
||||
}
|
||||
|
||||
void draw(){
|
||||
outline("waves");
|
||||
}
|
||||
},;
|
||||
|
||||
protected String line = "";
|
||||
protected final Func<String, String> text;
|
||||
protected Array<String> sentences;
|
||||
protected final Boolp done;
|
||||
|
||||
TutorialStage(Func<String, String> text, Boolp done){
|
||||
this.text = text;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
TutorialStage(Boolp done){
|
||||
this(line -> line, done);
|
||||
}
|
||||
|
||||
/** displayed tutorial stage text.*/
|
||||
public String text(){
|
||||
if(sentences == null){
|
||||
load();
|
||||
}
|
||||
String line = sentences.get(control.tutorial.sentence);
|
||||
return line.contains("{") ? text.get(line) : line;
|
||||
}
|
||||
|
||||
void load(){
|
||||
this.line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name();
|
||||
this.sentences = Array.select(Core.bundle.get(line).split("\n"), s -> !s.isEmpty());
|
||||
}
|
||||
|
||||
/** called every frame when this stage is active.*/
|
||||
void update(){
|
||||
|
||||
}
|
||||
|
||||
/** called when a stage begins.*/
|
||||
void begin(){
|
||||
|
||||
}
|
||||
|
||||
/** called when a stage needs to draw itself, usually over highlighted UI elements. */
|
||||
void draw(){
|
||||
|
||||
}
|
||||
|
||||
//utility
|
||||
|
||||
static void placeBlocks(){
|
||||
Tile core = state.teams.get(defaultTeam).cores.first();
|
||||
for(int i = 0; i < blocksToBreak; i++){
|
||||
world.removeBlock(world.ltile(core.x + blockOffset, core.y + i));
|
||||
world.tile(core.x + blockOffset, core.y + i).setBlock(Blocks.scrapWall, defaultTeam);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean blocksBroken(){
|
||||
Tile core = state.teams.get(defaultTeam).cores.first();
|
||||
|
||||
for(int i = 0; i < blocksToBreak; i++){
|
||||
if(world.tile(core.x + blockOffset, core.y + i).block() == Blocks.scrapWall){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean event(String name){
|
||||
return control.tutorial.events.contains(name);
|
||||
}
|
||||
|
||||
static boolean placed(Block block, int amount){
|
||||
return placed(block) >= amount;
|
||||
}
|
||||
|
||||
static int placed(Block block){
|
||||
return control.tutorial.blocksPlaced.get(block, 0);
|
||||
}
|
||||
|
||||
static int item(Item item){
|
||||
return state.teams.get(defaultTeam).cores.isEmpty() ? 0 : state.teams.get(defaultTeam).cores.first().entity.items.get(item);
|
||||
}
|
||||
|
||||
static boolean toggled(String name){
|
||||
Element element = Core.scene.findVisible(name);
|
||||
if(element instanceof Button){
|
||||
return ((Button)element).isChecked();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void outline(String name){
|
||||
Element element = Core.scene.findVisible(name);
|
||||
if(element != null && !toggled(name)){
|
||||
element.localToStageCoordinates(Tmp.v1.setZero());
|
||||
float sin = Mathf.sin(11f, Scl.scl(4f));
|
||||
Lines.stroke(Scl.scl(7f), Pal.place);
|
||||
Lines.rect(Tmp.v1.x - sin, Tmp.v1.y - sin, element.getWidth() + sin*2, element.getHeight() + sin*2);
|
||||
|
||||
float size = Math.max(element.getWidth(), element.getHeight()) + Mathf.absin(11f/2f, Scl.scl(18f));
|
||||
float angle = Angles.angle(Core.graphics.getWidth()/2f, Core.graphics.getHeight()/2f, Tmp.v1.x + element.getWidth()/2f, Tmp.v1.y + element.getHeight()/2f);
|
||||
Tmp.v2.trns(angle + 180f, size*1.4f);
|
||||
float fs = Scl.scl(40f);
|
||||
float fs2 = Scl.scl(56f);
|
||||
|
||||
Draw.color(Pal.gray);
|
||||
Drawf.tri(Tmp.v1.x + element.getWidth()/2f + Tmp.v2.x, Tmp.v1.y + element.getHeight()/2f + Tmp.v2.y, fs2, fs2, angle);
|
||||
Draw.color(Pal.place);
|
||||
Tmp.v2.setLength(Tmp.v2.len() - Scl.scl(4));
|
||||
Drawf.tri(Tmp.v1.x + element.getWidth()/2f + Tmp.v2.x, Tmp.v1.y + element.getHeight()/2f + Tmp.v2.y, fs, fs, angle);
|
||||
Draw.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user