diff --git a/Project.xml b/Project.xml
index d7ee924f5..73b384ee0 100644
--- a/Project.xml
+++ b/Project.xml
@@ -135,6 +135,14 @@
+
+
+
+
diff --git a/hmm.json b/hmm.json
index c8aa048ce..f14ebdfe8 100644
--- a/hmm.json
+++ b/hmm.json
@@ -21,13 +21,6 @@
"ref": "157eaf3",
"url": "https://github.com/MasterEric/flixel-addons"
},
- {
- "name": "flixel-addons",
- "type": "git",
- "dir": null,
- "ref": "dev",
- "url": "https://github.com/MasterEric/flixel-addons"
- },
{
"name": "flixel-ui",
"type": "haxelib",
@@ -100,8 +93,8 @@
"name": "openfl",
"type": "git",
"dir": null,
- "ref": "4f999ac",
- "url": "https://github.com/openfl/openfl"
+ "ref": "3fd5763",
+ "url": "https://github.com/MasterEric/openfl/"
},
{
"name": "polymod",
diff --git a/source/funkin/audio/FlxAudioGroup.hx b/source/funkin/audio/FlxAudioGroup.hx
new file mode 100644
index 000000000..182af3dea
--- /dev/null
+++ b/source/funkin/audio/FlxAudioGroup.hx
@@ -0,0 +1,206 @@
+package funkin.audio;
+
+import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.system.FlxSound;
+
+/**
+ * A group of FlxSounds which can be controlled as a whole.
+ *
+ * Add sounds to the group using `add()`, and then control them
+ * as a whole using the properties and methods of this class.
+ *
+ * It is assumed that all the sounds will play at the same time,
+ * and have the same duration.
+ */
+class FlxAudioGroup extends FlxTypedGroup
+{
+ /**
+ * The position in time of the sounds in the group.
+ * Measured in milliseconds.
+ */
+ public var time(get, set):Float;
+
+ function get_time():Float
+ {
+ if (getFirstAlive() != null)
+ return getFirstAlive().time;
+ else
+ return 0;
+ }
+
+ function set_time(time:Float):Float
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ // account for different offsets per sound?
+ sound.time = time;
+ });
+
+ return time;
+ }
+
+ /**
+ * The volume of the sounds in the group.
+ */
+ public var volume(get, set):Float;
+
+ function get_volume():Float
+ {
+ if (getFirstAlive() != null)
+ return getFirstAlive().volume;
+ else
+ return 1.0;
+ }
+
+ function set_volume(volume:Float):Float
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.volume = volume;
+ });
+
+ return volume;
+ }
+
+ /**
+ * The pitch of the sounds in the group, as a multiplier of 1.0x.
+ * `2.0` would play the audio twice as fast with a higher pitch,
+ * and `0.5` would play the audio at half speed with a lower pitch.
+ */
+ public var pitch(get, set):Float;
+
+ function get_pitch():Float
+ {
+ #if FLX_PITCH
+ if (getFirstAlive() != null)
+ return getFirstAlive().pitch;
+ else
+ #end
+ return 1;
+ }
+
+ function set_pitch(val:Float):Float
+ {
+ #if FLX_PITCH
+ trace('Setting audio pitch to ' + val);
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.pitch = val;
+ });
+ #end
+ return val;
+ }
+
+ /**
+ * Whether members of the group should be destroyed when they finish playing.
+ */
+ public var autoDestroyMembers(default, set):Bool = false;
+
+ function set_autoDestroyMembers(value:Bool):Bool
+ {
+ autoDestroyMembers = value;
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.autoDestroy = value;
+ });
+ return value;
+ }
+
+ /**
+ * Add a sound to the group.
+ */
+ public override function add(sound:FlxSound):FlxSound
+ {
+ var result:FlxSound = super.add(sound);
+
+ if (result == null)
+ return;
+
+ // Apply parameters to the new sound.
+ result.autoDestroy = this.autoDestroyMembers;
+ result.pitch = this.pitch;
+ result.volume = this.volume;
+
+ // We have to play, then pause the sound to set the time,
+ // else the sound will restart immediately when played.
+ result.play(true, 0.0);
+ result.pause();
+ result.time = this.time;
+ }
+
+ /**
+ * Pause all the sounds in the group.
+ */
+ public function pause()
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.pause();
+ });
+ }
+
+ /**
+ * Play all the sounds in the group.
+ */
+ public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.play(forceRestart, startTime, endTime);
+ });
+ }
+
+ /**
+ * Resume all the sounds in the group.
+ */
+ public function resume()
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.resume();
+ });
+ }
+
+ /**
+ * Stop all the sounds in the group.
+ */
+ public function stop()
+ {
+ forEachAlive(function(sound:FlxSound)
+ {
+ sound.stop();
+ });
+ }
+
+ public override function clear():Void {
+ this.stop();
+
+ super.clear();
+ }
+
+ /**
+ * Calculates the deviation of the sounds in the group from the target time.
+ *
+ * @param targetTime The time to compare the sounds to.
+ * If null, the current time of the first sound in the group is used.
+ * @return The largest deviation of the sounds in the group from the target time.
+ */
+ public function calcDeviation(?targetTime:Float):Float
+ {
+ var deviation:Float = 0;
+
+ forEachAlive(function(sound:FlxSound)
+ {
+ if (targetTime == null)
+ targetTime = sound.time;
+ else
+ {
+ var diff:Float = sound.time - targetTime;
+ if (Math.abs(diff) > Math.abs(deviation))
+ deviation = diff;
+ }
+ });
+
+ return deviation;
+ }
+}
diff --git a/source/funkin/audio/VocalGroup.hx b/source/funkin/audio/VocalGroup.hx
new file mode 100644
index 000000000..2dce0d4c0
--- /dev/null
+++ b/source/funkin/audio/VocalGroup.hx
@@ -0,0 +1,119 @@
+package funkin.audio;
+
+import flixel.system.FlxSound;
+
+/**
+ * An audio group that allows for specific control of vocal tracks.
+ */
+class VocalGroup extends FlxAudioGroup
+{
+ /**
+ * The player's vocal track.
+ */
+ var playerVocals:FlxSound;
+
+ /**
+ * The opponent's vocal track.
+ */
+ var opponentVocals:FlxSound;
+
+ /**
+ * The volume of the player's vocal track.
+ * Nore that this value is multiplied by the overall volume of the group.
+ */
+ public var playerVolume(default, set):Float;
+
+ function set_playerVolume(value:Float):Float
+ {
+ playerVolume = value;
+ if (playerVocals != null)
+ {
+ // Make sure volume is capped at 1.0.
+ playerVocals.volume = Math.min(playerVolume * this.volume, 1.0);
+ }
+ return playerVolume;
+ }
+
+ /**
+ * The volume of the opponent's vocal track.
+ * Nore that this value is multiplied by the overall volume of the group.
+ */
+ public var opponentVolume(default, set):Float;
+
+ function set_opponentVolume(value:Float):Float
+ {
+ opponentVolume = value;
+ if (opponentVocals != null)
+ {
+ // Make sure volume is capped at 1.0.
+ opponentVocals.volume = opponentVolume * this.volume;
+ }
+ return opponentVolume;
+ }
+
+ /**
+ * Sets up the player's vocal track.
+ * Stops and removes the existing player track if one exists.
+ */
+ public function setPlayerVocals(sound:FlxSound):FlxSound
+ {
+ if (playerVocals != null)
+ {
+ playerVocals.stop();
+ remove(playerVocals);
+ playerVocals = null;
+ }
+
+ playerVocals = add(sound);
+ playerVocals.volume = this.playerVolume * this.volume;
+
+ return playerVocals;
+ }
+
+ /**
+ * Sets up the opponent's vocal track.
+ * Stops and removes the existing player track if one exists.
+ */
+ public function setOpponentVocals(sound:FlxSound):FlxSound
+ {
+ if (opponentVocals != null)
+ {
+ opponentVocals.stop();
+ remove(opponentVocals);
+ opponentVocals = null;
+ }
+
+ opponentVocals = add(sound);
+ opponentVocals.volume = this.opponentVolume * this.volume;
+
+ return opponentVocals;
+ }
+
+ /**
+ * In this extension of FlxAudioGroup, there is a separate overall volume
+ * which affects all the members of the group.
+ */
+ var _volume = 1.0;
+
+ override function get_volume():Float
+ {
+ return _volume;
+ }
+
+ override function set_volume(value:Float):Float
+ {
+ _volume = super.set_volume(value);
+
+ if (playerVocals != null)
+ {
+ playerVocals.volume = playerVolume * _volume;
+ }
+
+ if (opponentVocals != null)
+ {
+ opponentVocals.volume = opponentVolume * _volume;
+ }
+
+ return _volume;
+ }
+}
diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx
index 4c4d8caaa..9f2f61b38 100644
--- a/source/funkin/play/HealthIcon.hx
+++ b/source/funkin/play/HealthIcon.hx
@@ -196,17 +196,11 @@ class HealthIcon extends FlxSprite
// Make the health icons bump (the update function causes them to lerp back down).
if (this.width > this.height)
{
- var targetSize = Std.int(CoolUtil.coolLerp(this.width + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
- targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
-
- setGraphicSize(targetSize, 0);
+ setGraphicSize(this.width + (HEALTH_ICON_SIZE * this.size.x * 0.2), 0);
}
else
{
- var targetSize = Std.int(CoolUtil.coolLerp(this.height + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
- targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
-
- setGraphicSize(0, targetSize);
+ setGraphicSize(0, this.height + (HEALTH_ICON_SIZE * this.size.y * 0.2));
}
this.updateHitbox();
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d349d9cdb..e26ed09d7 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -278,6 +278,26 @@ class ChartEditorState extends HaxeUIState
return isViewDownscroll;
}
+ /**
+ * Whether hitsounds are enabled for at least one character.
+ */
+ var hitsoundsEnabled(get, null):Bool;
+
+ function get_hitsoundsEnabled():Bool
+ {
+ return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent;
+ }
+
+ /**
+ * Whether hitsounds are enabled for the player.
+ */
+ var hitsoundsEnabledPlayer:Bool = true;
+
+ /**
+ * Whether hitsounds are enabled for the opponent.
+ */
+ var hitsoundsEnabledOpponent:Bool = true;
+
/**
* Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI.
* If so, ignore mouse events underneath.
@@ -1063,13 +1083,25 @@ class ChartEditorState extends HaxeUIState
});
setUISelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
+ addUIChangeListener('menubarItemPlayerHitsounds', (event:UIEvent) ->
+ {
+ hitsoundsEnabledPlayer = event.value;
+ });
+ setUISelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
+
+ addUIChangeListener('menubarItemOpponentHitsounds', (event:UIEvent) ->
+ {
+ hitsoundsEnabledOpponent = event.value;
+ });
+ setUISelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
+
var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) ->
{
var volume:Float = event.value / 100.0;
if (audioInstTrack != null)
audioInstTrack.volume = volume;
- instVolumeLabel.text = 'Instrumental - ${event.value}%';
+ instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
});
var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label);
@@ -1078,7 +1110,7 @@ class ChartEditorState extends HaxeUIState
var volume:Float = event.value / 100.0;
if (audioVocalTrackGroup != null)
audioVocalTrackGroup.volume = volume;
- vocalsVolumeLabel.text = 'Vocals - ${event.value}%';
+ vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
});
var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label);
@@ -1091,7 +1123,7 @@ class ChartEditorState extends HaxeUIState
if (audioVocalTrackGroup != null)
audioVocalTrackGroup.pitch = pitch;
#end
- playbackSpeedLabel.text = 'Playback Speed - ${pitch}x';
+ playbackSpeedLabel.text = 'Playback Speed - ${Std.int(event.value * 100) / 100}x';
});
addUIChangeListener('menubarItemToggleToolboxTools', (event:UIEvent) ->
@@ -2265,7 +2297,9 @@ class ChartEditorState extends HaxeUIState
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
var oldStepTime = Conductor.currentStepTime;
+ var oldSongPosition = Conductor.songPosition;
Conductor.update(audioInstTrack.time);
+ handleHitsounds(oldSongPosition, Conductor.songPosition);
// Resync vocals.
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
audioVocalTrackGroup.time = audioInstTrack.time;
@@ -2279,8 +2313,9 @@ class ChartEditorState extends HaxeUIState
else
{
// Else, move the entire view.
-
+ var oldSongPosition = Conductor.songPosition;
Conductor.update(audioInstTrack.time);
+ handleHitsounds(oldSongPosition, Conductor.songPosition);
// Resync vocals.
if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
audioVocalTrackGroup.time = audioInstTrack.time;
@@ -2302,6 +2337,36 @@ class ChartEditorState extends HaxeUIState
}
}
+ /**
+ * Handle the playback of hitsounds.
+ */
+ function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void {
+ if (!hitsoundsEnabled)
+ return;
+
+ // Assume notes are sorted by time.
+ for (noteData in currentSongChartNoteData) {
+ if (noteData.time < oldSongPosition)
+ // Note is in the past.
+ continue;
+
+ if (noteData.time >= newSongPosition)
+ // Note is in the future.
+ return;
+
+ // Note was just hit.
+ switch (noteData.getStrumlineIndex()) {
+ case 0: // Player
+ if (hitsoundsEnabledPlayer)
+ playSound(Paths.sound('funnyNoise/funnyNoise-09'));
+ case 1: // Opponent
+ if (hitsoundsEnabledOpponent)
+ playSound(Paths.sound('funnyNoise/funnyNoise-010'));
+ }
+ }
+ }
+
+
function startAudioPlayback()
{
if (audioInstTrack != null)