it is done

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

View File

@@ -0,0 +1,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;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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