diff --git a/annotations/src/main/java/io/anuke/annotations/Annotations.java b/annotations/src/main/java/io/anuke/annotations/Annotations.java index 1d5b62ed46..d964f85401 100644 --- a/annotations/src/main/java/io/anuke/annotations/Annotations.java +++ b/annotations/src/main/java/io/anuke/annotations/Annotations.java @@ -25,7 +25,7 @@ public class Annotations{ } /** Indicates that a method return or field cannot be null.*/ - @Target({ElementType.METHOD, ElementType.FIELD}) + @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface NonNull{ diff --git a/core/assets-raw/sprites/blocks/environment/snow1.png b/core/assets-raw/sprites/blocks/environment/snow1.png index 39f893df24..b33b1194c0 100644 Binary files a/core/assets-raw/sprites/blocks/environment/snow1.png and b/core/assets-raw/sprites/blocks/environment/snow1.png differ diff --git a/core/assets-raw/sprites/blocks/environment/snow2.png b/core/assets-raw/sprites/blocks/environment/snow2.png index 3d1a76608d..d4d28874a5 100644 Binary files a/core/assets-raw/sprites/blocks/environment/snow2.png and b/core/assets-raw/sprites/blocks/environment/snow2.png differ diff --git a/core/assets-raw/sprites/blocks/environment/snow3.png b/core/assets-raw/sprites/blocks/environment/snow3.png index c52f98cdae..b1eeb60dd2 100644 Binary files a/core/assets-raw/sprites/blocks/environment/snow3.png and b/core/assets-raw/sprites/blocks/environment/snow3.png differ diff --git a/core/assets/music/game7.ogg b/core/assets/music/game7.ogg index 58d0eb7db9..6a1011657c 100644 Binary files a/core/assets/music/game7.ogg and b/core/assets/music/game7.ogg differ diff --git a/core/src/io/anuke/mindustry/content/Blocks.java b/core/src/io/anuke/mindustry/content/Blocks.java index 2cdeb76990..0527aafed4 100644 --- a/core/src/io/anuke/mindustry/content/Blocks.java +++ b/core/src/io/anuke/mindustry/content/Blocks.java @@ -1448,7 +1448,7 @@ public class Blocks implements ContentList{ size = 2; range = 150f; - reload = 30f; + reload = 38f; restitution = 0.03f; ammoEjectBack = 3f; cooldown = 0.03f; diff --git a/core/src/io/anuke/mindustry/core/Control.java b/core/src/io/anuke/mindustry/core/Control.java index 10a22a92d8..ec1fc6043a 100644 --- a/core/src/io/anuke/mindustry/core/Control.java +++ b/core/src/io/anuke/mindustry/core/Control.java @@ -358,18 +358,7 @@ public class Control implements ApplicationListener{ //autosave global data if it's modified data.checkSave(); - if(state.is(State.menu)){ - if(ui.deploy.isShown()){ - music.play(Musics.launch); - }else if(ui.editor.isShown()){ - music.play(Musics.editor); - }else{ - music.play(Musics.menu); - } - }else{ - //TODO game music - music.silence(); - } + music.update(); if(!state.is(State.menu)){ input.update(); diff --git a/core/src/io/anuke/mindustry/game/MusicControl.java b/core/src/io/anuke/mindustry/game/MusicControl.java index 7b9fdfab7d..c9e0147f5c 100644 --- a/core/src/io/anuke/mindustry/game/MusicControl.java +++ b/core/src/io/anuke/mindustry/game/MusicControl.java @@ -3,44 +3,162 @@ package io.anuke.mindustry.game; import io.anuke.annotations.Annotations.*; import io.anuke.arc.*; import io.anuke.arc.audio.*; +import io.anuke.arc.collection.*; import io.anuke.arc.math.*; import io.anuke.arc.util.*; +import io.anuke.mindustry.core.GameState.*; +import io.anuke.mindustry.game.EventType.*; +import io.anuke.mindustry.gen.*; + +import static io.anuke.mindustry.Vars.*; /** Controls playback of multiple music tracks.*/ public class MusicControl{ - private static final float finTime = 120f, foutTime = 120f; + private static final float finTime = 120f, foutTime = 120f, musicInterval = 60 * 60 * 3f, musicChance = 0.25f, musicWaveChance = 0.2f; + + /** normal, ambient music, plays at any time */ + public final Array ambientMusic = Array.with(Musics.game1, Musics.game3, Musics.game4, Musics.game6); + /** darker music, used in times of conflict */ + public final Array darkMusic = Array.with(Musics.game2, Musics.game5, Musics.game7); + /** all music, both dark and ambient */ + public final Array allMusic = Array.withArrays(ambientMusic, darkMusic); + + private Music lastRandomPlayed; + private Interval timer = new Interval(); private @Nullable Music current; private float fade; + private boolean silenced; - public void play(@Nullable Music music){ - if(current != null){ - current.setVolume(fade * Core.settings.getInt("musicvol") / 100f); - } + public MusicControl(){ + Events.on(WaveEvent.class, e -> Time.run(60f * 10f, () -> { + if(Mathf.chance(musicWaveChance)){ + playRandom(); + } + })); + } - if(current == null && music != null){ - current = music; - current.setLooping(true); - current.setVolume(fade = 0f); - current.play(); - }else if(current == music && music != null){ - fade = Mathf.clamp(fade + Time.delta()/finTime); - }else if(current != null){ - fade = Mathf.clamp(fade - Time.delta()/foutTime); + /** Update and play the right music track.*/ + public void update(){ + if(state.is(State.menu)){ + if(ui.deploy.isShown()){ + play(Musics.launch); + }else if(ui.editor.isShown()){ + play(Musics.editor); + }else{ + play(Musics.menu); + } + }else if(state.rules.editor){ + play(Musics.editor); + }else{ + //this just fades out the last track to make way for ingame music + silence(); - if(fade <= 0.01f){ - current.stop(); - current = null; - if(music != null){ - current = music; - current.setVolume(fade = 0f); - current.setLooping(true); - current.play(); + //play music at intervals + if(timer.get(musicInterval)){ + //chance to play it per interval + if(Mathf.chance(musicChance)){ + playRandom(); } } } } - public void silence(){ + /** 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; + } + + if(state.enemies() > 25){ + //many enemies -> dark + return true; + } + + //it may be dark based on wave + if(Mathf.chance((float)(Math.log10((state.wave - 17f)/19f) + 1) / 4f)){ + return true; + } + + return false; + } + + /** 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(@NonNull Music music){ + if(current != 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); } } diff --git a/core/src/io/anuke/mindustry/ui/dialogs/LoadoutDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/LoadoutDialog.java index 64a588de1d..eb05a7502f 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/LoadoutDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/LoadoutDialog.java @@ -1,12 +1,14 @@ package io.anuke.mindustry.ui.dialogs; -import io.anuke.arc.collection.Array; -import io.anuke.arc.function.Predicate; -import io.anuke.arc.function.Supplier; -import io.anuke.arc.scene.ui.TextButton; +import io.anuke.arc.*; +import io.anuke.arc.collection.*; +import io.anuke.arc.function.*; +import io.anuke.arc.input.*; +import io.anuke.arc.scene.ui.*; +import io.anuke.arc.scene.ui.layout.*; import io.anuke.mindustry.type.*; -import static io.anuke.mindustry.Vars.content; +import static io.anuke.mindustry.Vars.*; public class LoadoutDialog extends FloatingDialog{ private Runnable hider; @@ -14,24 +16,58 @@ public class LoadoutDialog extends FloatingDialog{ private Runnable resetter; private Runnable updater; private Predicate filter; + private Table items; private int capacity; public LoadoutDialog(){ super("$configure"); - setFillParent(false); - addCloseButton(); + setFillParent(true); + + keyDown(key -> { + if(key == KeyCode.ESCAPE || key == KeyCode.BACK){ + Core.app.post(this::hide); + } + }); + + cont.add(items = new Table()).left(); + shown(this::setup); hidden(() -> { if(hider != null){ hider.run(); } }); - buttons.row(); - buttons.addButton("$settings.reset", () -> { + + cont.row(); + + cont.addButton("$add", () -> { + FloatingDialog dialog = new FloatingDialog(""); + dialog.setFillParent(false); + for(Item item : content.items().select(item -> filter.test(item) && item.type == ItemType.material && supplier.get().find(stack -> stack.item == item) == null)){ + TextButton button = dialog.cont.addButton("", "clear", () -> { + dialog.hide(); + supplier.get().add(new ItemStack(item, 0)); + updater.run(); + setup(); + }).size(300f, 36f).get(); + button.clearChildren(); + button.left(); + button.addImage(item.icon(Item.Icon.medium)).size(8 * 3).pad(4); + button.add(item.localizedName); + dialog.cont.row(); + } + dialog.show(); + }).size(100f, 40).left().disabled(b -> !content.items().contains(item -> filter.test(item) && !supplier.get().contains(stack -> stack.item == item))); + + cont.row(); + cont.addButton("$settings.reset", () -> { resetter.run(); updater.run(); setup(); }).size(210f, 64f); + + cont.row(); + cont.addImageTextButton("$back", "icon-arrow-left", iconsize, this::hide).size(210f, 64f); } public void show(int capacity, Supplier> supplier, Runnable reseter, Runnable updater, Runnable hider, Predicate filter){ @@ -45,50 +81,31 @@ public class LoadoutDialog extends FloatingDialog{ } void setup(){ - cont.clear(); + items.clearChildren(); + items.left(); float bsize = 40f; int step = 50; for(ItemStack stack : supplier.get()){ - cont.addButton("x", "clear-partial", () -> { + items.addButton("x", "clear-partial", () -> { supplier.get().remove(stack); updater.run(); setup(); }).size(bsize); - cont.addButton("-", "clear-partial", () -> { + items.addButton("-", "clear-partial", () -> { stack.amount = Math.max(stack.amount - step, 0); updater.run(); }).size(bsize); - cont.addButton("+", "clear-partial", () -> { + items.addButton("+", "clear-partial", () -> { stack.amount = Math.min(stack.amount + step, capacity); updater.run(); }).size(bsize); - cont.addImage(stack.item.icon(Item.Icon.medium)).size(8 * 3).padRight(4); - cont.label(() -> stack.amount + "").left(); + items.addImage(stack.item.icon(Item.Icon.medium)).size(8 * 3).padRight(4).padLeft(4); + items.label(() -> stack.amount + "").left(); - cont.row(); + items.row(); } - - cont.addButton("$add", () -> { - FloatingDialog dialog = new FloatingDialog(""); - dialog.setFillParent(false); - for(Item item : content.items().select(item -> filter.test(item) && item.type == ItemType.material && supplier.get().find(stack -> stack.item == item) == null)){ - TextButton button = dialog.cont.addButton("", "clear", () -> { - supplier.get().add(new ItemStack(item, 0)); - updater.run(); - setup(); - dialog.hide(); - }).size(300f, 36f).get(); - button.clearChildren(); - button.left(); - button.addImage(item.icon(Item.Icon.medium)).size(8 * 3).pad(4); - button.add(item.localizedName); - dialog.cont.row(); - } - dialog.show(); - }).colspan(4).size(100f, bsize).left().disabled(b -> !content.items().contains(item -> filter.test(item) && !supplier.get().contains(stack -> stack.item == item))); - pack(); } } diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 6e49684e8f..8eeeba9e1c 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -147,12 +147,7 @@ public class ServerControl implements ApplicationListener{ Array maps = world.maps.customMaps().size == 0 ? world.maps.defaultMaps() : world.maps.customMaps(); Map previous = world.getMap(); - Map map = previous; - if(maps.size > 1){ - while(map == previous) map = maps.random(); - }else if(!previous.custom && !world.maps.customMaps().isEmpty()){ - map = maps.first(); - } + Map map = maps.random(previous); Call.onInfoMessage((state.rules.pvp ? "[YELLOW]The " + event.winner.name() + " team is victorious![]" : "[SCARLET]Game over![]") @@ -162,9 +157,7 @@ public class ServerControl implements ApplicationListener{ info("Selected next map to be {0}.", map.name()); - Map fmap = map; - - play(true, () -> world.loadMap(fmap)); + play(true, () -> world.loadMap(map)); } }else{ netServer.kickAll(KickReason.gameover);