diff --git a/assets/data/scripts/pixel.hx b/assets/data/scripts/pixel.hx
index 8f1887c8..1109f2dc 100644
--- a/assets/data/scripts/pixel.hx
+++ b/assets/data/scripts/pixel.hx
@@ -97,9 +97,6 @@ function postCreate() {
defaultCamZoom /= daPixelZoom;
}
- iconP1.antialiasing = false;
- iconP2.antialiasing = false;
-
if (enablePixelGameOver) {
gameOverSong = "pixel/gameOver";
lossSFX = "pixel/gameOverSFX";
diff --git a/assets/images/icons/bf-old.png b/assets/images/icons/bf-old/icon.png
similarity index 100%
rename from assets/images/icons/bf-old.png
rename to assets/images/icons/bf-old/icon.png
diff --git a/assets/images/icons/bf-pixel/data.xml b/assets/images/icons/bf-pixel/data.xml
new file mode 100644
index 00000000..57653cad
--- /dev/null
+++ b/assets/images/icons/bf-pixel/data.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/assets/images/icons/bf-pixel.png b/assets/images/icons/bf-pixel/icon.png
similarity index 100%
rename from assets/images/icons/bf-pixel.png
rename to assets/images/icons/bf-pixel/icon.png
diff --git a/assets/images/icons/bf.png b/assets/images/icons/bf/icon.png
similarity index 100%
rename from assets/images/icons/bf.png
rename to assets/images/icons/bf/icon.png
diff --git a/assets/images/icons/dad.png b/assets/images/icons/dad/icon.png
similarity index 100%
rename from assets/images/icons/dad.png
rename to assets/images/icons/dad/icon.png
diff --git a/assets/images/icons/face.png b/assets/images/icons/face/icon.png
similarity index 100%
rename from assets/images/icons/face.png
rename to assets/images/icons/face/icon.png
diff --git a/assets/images/icons/gf-pixel/data.xml b/assets/images/icons/gf-pixel/data.xml
new file mode 100644
index 00000000..57653cad
--- /dev/null
+++ b/assets/images/icons/gf-pixel/data.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/assets/images/icons/gf-pixel.png b/assets/images/icons/gf-pixel/icon.png
similarity index 100%
rename from assets/images/icons/gf-pixel.png
rename to assets/images/icons/gf-pixel/icon.png
diff --git a/assets/images/icons/gf.png b/assets/images/icons/gf/icon.png
similarity index 100%
rename from assets/images/icons/gf.png
rename to assets/images/icons/gf/icon.png
diff --git a/assets/images/icons/mom.png b/assets/images/icons/mom/icon.png
similarity index 100%
rename from assets/images/icons/mom.png
rename to assets/images/icons/mom/icon.png
diff --git a/assets/images/icons/monster.png b/assets/images/icons/monster/icon.png
similarity index 100%
rename from assets/images/icons/monster.png
rename to assets/images/icons/monster/icon.png
diff --git a/assets/images/icons/parents-christmas.png b/assets/images/icons/parents-christmas/icon.png
similarity index 100%
rename from assets/images/icons/parents-christmas.png
rename to assets/images/icons/parents-christmas/icon.png
diff --git a/assets/images/icons/pico.png b/assets/images/icons/pico/icon.png
similarity index 100%
rename from assets/images/icons/pico.png
rename to assets/images/icons/pico/icon.png
diff --git a/assets/images/icons/senpai/data.xml b/assets/images/icons/senpai/data.xml
new file mode 100644
index 00000000..57653cad
--- /dev/null
+++ b/assets/images/icons/senpai/data.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/assets/images/icons/senpai.png b/assets/images/icons/senpai/icon.png
similarity index 100%
rename from assets/images/icons/senpai.png
rename to assets/images/icons/senpai/icon.png
diff --git a/assets/images/icons/spirit/data.xml b/assets/images/icons/spirit/data.xml
new file mode 100644
index 00000000..57653cad
--- /dev/null
+++ b/assets/images/icons/spirit/data.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/assets/images/icons/spirit.png b/assets/images/icons/spirit/icon.png
similarity index 100%
rename from assets/images/icons/spirit.png
rename to assets/images/icons/spirit/icon.png
diff --git a/assets/images/icons/spooky.png b/assets/images/icons/spooky/icon.png
similarity index 100%
rename from assets/images/icons/spooky.png
rename to assets/images/icons/spooky/icon.png
diff --git a/assets/images/icons/tankman.png b/assets/images/icons/tankman/icon.png
similarity index 100%
rename from assets/images/icons/tankman.png
rename to assets/images/icons/tankman/icon.png
diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx
index 9486aed6..a29d3909 100644
--- a/source/funkin/backend/assets/Paths.hx
+++ b/source/funkin/backend/assets/Paths.hx
@@ -200,6 +200,29 @@ class Paths
return tempFramesCache[key] = loadFrames(assetsPath ? key : Paths.image(key, library, true));
}
+ /**
+ * Checks if the images needed for using getFrames() exist.
+ * @param key Path to the image
+ * @param checkAtlas Whenever to check for the Animation.json file (used in FlxAnimate)
+ * @param assetsPath Whenever to use the raw path or to pass it through Paths.image()
+ * @param library (Additional) library to load the frames from.
+ * @return True if the images exist, false otherwise.
+ **/
+ public static function framesExists(key:String, checkAtlas:Bool = false, checkMulti:Bool = true, assetsPath:Bool = false, ?library:String) {
+ var path = assetsPath ? key : Paths.image(key, library, true);
+ var noExt = Path.withoutExtension(path);
+ if(checkAtlas && Assets.exists('$noExt/Animation.json'))
+ return true;
+ if(checkMulti && Assets.exists('$noExt/1.png'))
+ return true;
+ if(Assets.exists('$noExt.xml'))
+ return true;
+ if(Assets.exists('$noExt.txt'))
+ return true;
+ if(Assets.exists('$noExt.json'))
+ return true;
+ return false;
+ }
/**
* Loads frames from a specific image path. Supports Sparrow Atlases, Packer Atlases, and multiple spritesheets.
diff --git a/source/funkin/backend/scripting/events/NoteHitEvent.hx b/source/funkin/backend/scripting/events/NoteHitEvent.hx
index a4595fd1..750e5f52 100644
--- a/source/funkin/backend/scripting/events/NoteHitEvent.hx
+++ b/source/funkin/backend/scripting/events/NoteHitEvent.hx
@@ -2,6 +2,7 @@ package funkin.backend.scripting.events;
import funkin.game.Note;
import funkin.game.Character;
+import funkin.game.HealthIcon;
final class NoteHitEvent extends CancellableEvent {
@:dox(hide) public var animCancelled:Bool = false;
@@ -113,6 +114,10 @@ final class NoteHitEvent extends CancellableEvent {
* Whenever the animation should be forced to play (if it's null it will be forced based on the sprite's data xml, if it has one).
*/
public var forceAnim:Null = true;
+ /**
+ * The attached healthIcon used distinction for icons amongst others
+ */
+ public var healthIcon:HealthIcon;
/**
* Prevents the default sing animation from being played.
diff --git a/source/funkin/editors/character/CharacterInfoScreen.hx b/source/funkin/editors/character/CharacterInfoScreen.hx
index 1d7f313b..7e457183 100644
--- a/source/funkin/editors/character/CharacterInfoScreen.hx
+++ b/source/funkin/editors/character/CharacterInfoScreen.hx
@@ -4,13 +4,14 @@ import flixel.math.FlxPoint;
import haxe.xml.Access;
import funkin.game.Character;
import funkin.editors.extra.PropertyButton;
+import funkin.game.HealthIcon;
class CharacterInfoScreen extends UISubstateWindow {
public var character:Character;
public var spriteTextBox:UITextBox;
public var iconTextBox:UITextBox;
- public var iconSprite:FlxSprite;
+ public var iconSprite:HealthIcon;
public var gameOverCharTextBox:UITextBox;
public var antialiasingCheckbox:UICheckbox;
public var flipXCheckbox:UICheckbox;
@@ -147,20 +148,10 @@ class CharacterInfoScreen extends UISubstateWindow {
}
function updateIcon(icon:String) {
- if (iconSprite == null) add(iconSprite = new FlxSprite());
+ if (iconSprite == null) add(iconSprite = new HealthIcon());
- if (iconSprite.animation.exists(icon)) return;
- @:privateAccess iconSprite.animation.clearAnimations();
-
- var path:String = Paths.image('icons/$icon');
- if (!Assets.exists(path)) path = Paths.image('icons/face');
-
- iconSprite.loadGraphic(path, true, 150, 150);
- iconSprite.animation.add(icon, [0], 0, false);
- iconSprite.antialiasing = true;
- iconSprite.animation.play(icon);
-
- iconSprite.scale.set(0.5, 0.5);
+ iconSprite.setIcon(icon);
+ iconSprite.scale.set(iconSprite.defaultScale * 0.5, iconSprite.defaultScale * 0.5);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
}
diff --git a/source/funkin/editors/charter/ChartCreationScreen.hx b/source/funkin/editors/charter/ChartCreationScreen.hx
index c89835f9..21e925b5 100644
--- a/source/funkin/editors/charter/ChartCreationScreen.hx
+++ b/source/funkin/editors/charter/ChartCreationScreen.hx
@@ -320,7 +320,7 @@ class CompactCharacterButton extends UIButton {
autoAlpha = false;
charIcon = new HealthIcon(funkin.game.Character.getIconFromCharName(char));
- charIcon.scale.set(0.2, 0.2);
+ charIcon.scale.set(charIcon.defaultScale * 0.2, charIcon.defaultScale * 0.2);
charIcon.updateHitbox();
charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
charIcon.scrollFactor.set(1,1);
@@ -332,11 +332,10 @@ class CompactCharacterButton extends UIButton {
textBox.antialiasing = true;
textBox.onChange = function(char:String) {
char = funkin.game.Character.getIconFromCharName(char);
- var image = Paths.image("icons/" + char);
- if(!Assets.exists(image))
- image = Paths.image("icons/face");
- charIcon.loadGraphic(image, true, 150, 150);
+ charIcon.setIcon(char);
+ charIcon.scale.set(charIcon.defaultScale * 0.2, charIcon.defaultScale * 0.2);
charIcon.updateHitbox();
+ charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
}
deleteButton = new UIButton(textBox.x + 115 + 16, bHeight/2 - (32/2), "", function () {
diff --git a/source/funkin/editors/charter/CharterMetaDataScreen.hx b/source/funkin/editors/charter/CharterMetaDataScreen.hx
index 4179dfc7..c7fdc49b 100644
--- a/source/funkin/editors/charter/CharterMetaDataScreen.hx
+++ b/source/funkin/editors/charter/CharterMetaDataScreen.hx
@@ -3,6 +3,7 @@ package funkin.editors.charter;
import flixel.math.FlxPoint;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.editors.extra.PropertyButton;
+import funkin.game.HealthIcon;
using StringTools;
@@ -20,7 +21,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
- public var iconSprite:FlxSprite;
+ public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var colorWheel:UIColorwheel;
@@ -130,20 +131,10 @@ class CharterMetaDataScreen extends UISubstateWindow {
}
function updateIcon(icon:String) {
- if (iconSprite == null) add(iconSprite = new FlxSprite());
+ if (iconSprite == null) add(iconSprite = new HealthIcon());
- if (iconSprite.animation.exists(icon)) return;
- @:privateAccess iconSprite.animation.clearAnimations();
-
- var path:String = Paths.image('icons/$icon');
- if (!Assets.exists(path)) path = Paths.image('icons/face');
-
- iconSprite.loadGraphic(path, true, 150, 150);
- iconSprite.animation.add(icon, [0], 0, false);
- iconSprite.antialiasing = true;
- iconSprite.animation.play(icon);
-
- iconSprite.scale.set(0.5, 0.5);
+ iconSprite.setIcon(icon);
+ iconSprite.scale.set(iconSprite.defaultScale * 0.5, iconSprite.defaultScale * 0.5);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
}
diff --git a/source/funkin/editors/charter/CharterStrumline.hx b/source/funkin/editors/charter/CharterStrumline.hx
index 3850c2fa..23da1e9a 100644
--- a/source/funkin/editors/charter/CharterStrumline.hx
+++ b/source/funkin/editors/charter/CharterStrumline.hx
@@ -49,7 +49,8 @@ class CharterStrumline extends UISprite {
for (i=>icon in icons) {
var healthIcon = new HealthIcon(Character.getIconFromCharName(icon));
- healthIcon.scale.x = healthIcon.scale.y = 0.6 - (icons.length / 20);
+ var newScale = 0.6 - (icons.length / 20);
+ healthIcon.scale.x = healthIcon.scale.y = healthIcon.defaultScale * newScale;
healthIcon.updateHitbox();
healthIcon.x = FlxMath.lerp(0, icons.length * 20, (icons.length-1 != 0 ? i / (icons.length-1) : 0));
healthIcon.y = draggable ? 29 : 7;
@@ -106,7 +107,8 @@ class CharterStrumline extends UISprite {
for (i=>icon in icons) {
var healthIcon = new HealthIcon(Character.getIconFromCharName(icon));
- healthIcon.scale.x = healthIcon.scale.y = 0.6 - (icons.length / 20);
+ var newScale = 0.6 - (icons.length / 20);
+ healthIcon.scale.x = healthIcon.scale.y = healthIcon.defaultScale * newScale;
healthIcon.updateHitbox();
healthIcon.x = FlxMath.lerp(0, icons.length * 20, (icons.length-1 != 0 ? i / (icons.length-1) : 0));
healthIcon.y = draggable ? 14 : 7;
diff --git a/source/funkin/editors/charter/CharterStrumlineScreen.hx b/source/funkin/editors/charter/CharterStrumlineScreen.hx
index d9298430..915a1862 100644
--- a/source/funkin/editors/charter/CharterStrumlineScreen.hx
+++ b/source/funkin/editors/charter/CharterStrumlineScreen.hx
@@ -194,7 +194,7 @@ class CharacterButton extends UIButton {
super(x, y, "", null, 250, 54);
charIcon = new HealthIcon(Character.getIconFromCharName(char));
- charIcon.scale.set(0.3, 0.3);
+ charIcon.scale.set(charIcon.defaultScale * 0.3, charIcon.defaultScale * 0.3);
charIcon.updateHitbox();
charIcon.setPosition(x + 10, bHeight/2 - charIcon.height / 2);
charIcon.scrollFactor.set(1,1);
@@ -206,11 +206,10 @@ class CharacterButton extends UIButton {
textBox.antialiasing = true;
textBox.onChange = function(char:String) {
char = Character.getIconFromCharName(char);
- var image = Paths.image("icons/" + char);
- if(!Assets.exists(image))
- image = Paths.image("icons/face");
- charIcon.loadGraphic(image, true, 150, 150);
+ charIcon.setIcon(char);
+ charIcon.scale.set(charIcon.defaultScale * 0.3, charIcon.defaultScale * 0.3);
charIcon.updateHitbox();
+ charIcon.setPosition(x + 10, bHeight/2 - charIcon.height / 2);
}
deleteButton = new UIButton(textBox.x + 115 + 16, bHeight/2 - (32/2), "", function () {
diff --git a/source/funkin/editors/charter/SongCreationScreen.hx b/source/funkin/editors/charter/SongCreationScreen.hx
index fdd9cf1e..c134da5a 100644
--- a/source/funkin/editors/charter/SongCreationScreen.hx
+++ b/source/funkin/editors/charter/SongCreationScreen.hx
@@ -5,6 +5,7 @@ import flixel.text.FlxText.FlxTextFormatMarkerPair;
import flixel.group.FlxGroup;
import funkin.backend.chart.ChartData.ChartMetaData;
import haxe.io.Bytes;
+import funkin.game.HealthIcon;
typedef SongCreationData = {
var meta:ChartMetaData;
@@ -25,7 +26,7 @@ class SongCreationScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
- public var iconSprite:FlxSprite;
+ public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var colorWheel:UIColorwheel;
@@ -231,20 +232,10 @@ class SongCreationScreen extends UISubstateWindow {
}
function updateIcon(icon:String) {
- if (iconSprite == null) menuDataGroup.add(iconSprite = new FlxSprite());
+ if (iconSprite == null) add(iconSprite = new HealthIcon());
- if (iconSprite.animation.exists(icon)) return;
- @:privateAccess iconSprite.animation.clearAnimations();
-
- var path:String = Paths.image('icons/$icon');
- if (!Assets.exists(path)) path = Paths.image('icons/face');
-
- iconSprite.loadGraphic(path, true, 150, 150);
- iconSprite.animation.add(icon, [0], 0, false);
- iconSprite.antialiasing = true;
- iconSprite.animation.play(icon);
-
- iconSprite.scale.set(0.5, 0.5);
+ iconSprite.setIcon(icon);
+ iconSprite.scale.set(iconSprite.defaultScale * 0.5, iconSprite.defaultScale * 0.5);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
}
diff --git a/source/funkin/game/HealthIcon.hx b/source/funkin/game/HealthIcon.hx
index 70f5024b..7900ca3b 100644
--- a/source/funkin/game/HealthIcon.hx
+++ b/source/funkin/game/HealthIcon.hx
@@ -1,12 +1,31 @@
package funkin.game;
+import flixel.graphics.FlxGraphic;
+import flixel.math.FlxPoint;
+import flixel.util.typeLimit.OneOfTwo;
-class HealthIcon extends FlxSprite
+class HealthIcon extends FunkinSprite
{
/**
- * Used for FreeplayState! If you use it elsewhere, prob gonna annoying
+ * Attaches the icon to a sprite, following it's position
*/
public var sprTracker:FlxSprite;
+ /**
+ * Where to place the icon in relation to the sprite
+ *
+ * LEFT: Left of the sprite
+ *
+ * CENTER: Center of the sprite
+ *
+ * RIGHT: Right of the sprite
+ */
+ public var sprTrackerAlignment:TrackerAlignment = RIGHT;
+ /**
+ * Offset of the icon in relation to the sprite
+ *
+ * By default it is set to (10, -30) and is intended to be used with Alphabet
+ */
+ public var sprTrackerOffset:FlxPoint = new FlxPoint(10, -30);
/**
* The currently showing icon
@@ -20,30 +39,64 @@ class HealthIcon extends FlxSprite
/**
* Health steps in this format:
- * Min Percentage => Frame Index
+ *
+ * Min Percentage => Frame Index / Animation Name
*/
- public var healthSteps:Map = null;
+ public var healthSteps:Map> = null;
+
+ /**
+ * Current animation state
+ */
+ public var curAnimState:OneOfTwo = -1;
+
+ /**
+ * The Default Scale For The Icon
+ *
+ * This is what scale the icon should return to when its bump animation is finished
+ */
+ public var defaultScale:Float = 1;
+
+ /**
+ * Whenever or not the icon is animated or not
+ */
+ public var animated:Bool = false;
+
+ /**
+ * XML Animated Icon data
+ *
+ * `null` if the icon is not animated or its invalid
+ */
+ public var xmlData:Xml;
+
+ /**
+ * Extra offsets to add when updating the hitbox
+ */
+ public var extraOffsets:FlxPoint = FlxPoint.get();
/**
* Helper for HScript who can't make maps
- * @param steps Something like this: `[[0, 1], [20, 0]]`
+ *
+ * THIS IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE
+ *
+ * Please set the healthSteps directly in the script instead, hscript does support maps
+ *
+ * @param steps Something like this: `[[0, 1], [20, 0]]` or `[[0, "losing"], [20, "neutral"]]` for animated icons
*/
- public function setHealthSteps(steps:Array>) { // helper for hscript that can't do maps
+ @:dox(hide) @:noCompletion @:deprecated("use healthSteps instead")
+ public function setHealthSteps(steps:Array>>):Void { // helper for hscript that can't do maps
if (steps == null) return;
healthSteps = [];
- for(s in steps)
+ for (s in steps)
if (s.length > 1)
healthSteps[s[0]] = s[1];
- var am = 0;
- for(k=>e in healthSteps) am++;
- if (am <= 0) healthSteps = [
- 0 => 1, // losing icon
- 20 => 0, // normal icon
+ if (Lambda.count(healthSteps) <= 0) healthSteps = [
+ 0 => (this.animated ? "losing" : 1), // losing icon
+ 20 => (this.animated ? "neutral" : 0), // normal icon
];
}
- public function new(char:String = 'bf', isPlayer:Bool = false)
+ public function new(?char:String, isPlayer:Bool = false)
{
super();
health = 0.5;
@@ -53,44 +106,316 @@ class HealthIcon extends FlxSprite
scrollFactor.set();
}
- public function setIcon(char:String, width:Int = 150, height:Int = 150) {
- if(curCharacter != char || this.width != width || this.height != height) {
- curCharacter = char;
- var path = Paths.image('icons/$char');
- if (!Assets.exists(path)) path = Paths.image('icons/face');
-
- loadGraphic(path, true, width, height);
-
- animation.add(char, [for(i in 0...frames.frames.length) i], 0, false, isPlayer);
- antialiasing = true;
- animation.play(char);
-
- healthSteps = [
- 0 => 1, // losing icon
- 20 => 0, // normal icon
- ];
-
- if (frames.frames.length >= 3)
- healthSteps[80] = 2; // winning icon
- }
+ /**
+ * Called every beat, and causes the icon to become bigger
+ **/
+ public dynamic function bump():Void {
+ var iconScale = 1.2;
+ scale.set(defaultScale * iconScale, defaultScale * iconScale);
+ updateHitbox();
}
- override function update(elapsed:Float)
- {
+ /**
+ * Called every frame and causes the icon to become smaller
+ **/
+ public dynamic function updateBump():Void {
+ var iconLerp = 0.33;
+ scale.set(CoolUtil.fpsLerp(scale.x, defaultScale, iconLerp), CoolUtil.fpsLerp(scale.y, defaultScale, iconLerp));
+ updateHitbox();
+ }
+
+ /**
+ * Sets the icon to the specified character
+ * @param char Character to set the icon to
+ * @param allowAnimated Whenever the icon can be animated
+ **/
+ public function setIcon(char:String, allowAnimated:Bool = true):Void {
+ if (curCharacter == char) return;
+
+ var oldIconPath = 'icons/$char';
+ var newIconPath = 'icons/$char/icon';
+
+ if (!Assets.exists(Paths.image(oldIconPath)) && !Assets.exists(Paths.image(newIconPath))) {
+ char = 'face';
+ oldIconPath = 'icons/$char';
+ newIconPath = 'icons/$char/icon';
+ }
+ curCharacter = char;
+
+ var iconPath = Assets.exists(Paths.image(oldIconPath)) ? oldIconPath : newIconPath;
+ var iconXmlPath = Paths.getPath('images/icons/$char/data.xml');
+ var iconFoundData = Assets.exists(iconXmlPath);
+
+ try {
+ xmlData = iconFoundData ? Xml.parse(Assets.getText(iconXmlPath)) : null;
+ } catch(e) {
+ Logs.trace('Error while parsing icon data for $char: ${e.message}', ERROR);
+ xmlData = null;
+ }
+ var xmlValid = xmlData != null && (xmlData = xmlData.firstElement()) != null;
+
+ this.animated = allowAnimated && iconFoundData && xmlValid && Paths.framesExists(Paths.image(newIconPath), true, true, true);
+
+ var iconAmt:Int = 0;
+ var iconSize:Int = 0;
+ var iconIsPlayer = xmlValid ? xmlData.get("facing").getDefault("right").toLowerCase() == "left" : false;
+
+ animateAtlas = null; // reset
+ if (this.animated)
+ loadSprite(Paths.image(iconPath));
+ else {
+ var iconAsset:FlxGraphic = FlxG.bitmap.add(Paths.image(iconPath));
+ var assetW:Float = iconAsset.width;
+ var assetH:Float = iconAsset.height;
+
+ iconAmt = Math.floor(assetW / assetH);
+ iconSize = Math.floor(assetW / iconAmt);
+
+ loadGraphic(iconAsset, true, Std.int(Math.min(iconSize, assetW)), Std.int(Math.min(iconSize, assetH)));
+
+ setGraphicSize(150);
+ updateHitbox();
+
+ animation.add(char, [for(i in 0...iconAmt) i], 0, false, isPlayer != iconIsPlayer);
+ animation.play(char);
+ }
+
+ if(!animation.onFinishEnd.has(animFinishCallback))
+ animation.onFinishEnd.add(animFinishCallback);
+ if(animateAtlas != null && !animateAtlas.anim.onFinishEnd.has(animFinishCallback))
+ animateAtlas.anim.onFinishEnd.add(animFinishCallback);
+
+ var parsedSteps:Map = [];
+
+ antialiasing = true;
+ if (xmlValid) {
+ if (xmlData.exists("antialiasing"))
+ antialiasing = xmlData.get("antialiasing").toLowerCase() == "true";
+ if (xmlData.exists("offsetX"))
+ extraOffsets.x = Std.parseFloat(xmlData.get("offsetX")).getDefault(0);
+ if (xmlData.exists("offsetY"))
+ extraOffsets.y = Std.parseFloat(xmlData.get("offsetY")).getDefault(0);
+
+ for (node in xmlData.elements())
+ switch(node.nodeName) {
+ case "transition":
+ if (this.animated == false) {
+ Logs.trace('Icon ${char} data is not allowed when not animated', WARNING);
+ continue;
+ }
+ if (!node.exists("anim")) {
+ Logs.trace('Icon ${char} data is missing anim', WARNING);
+ continue;
+ }
+ if (!node.exists("to")) {
+ Logs.trace('Icon ${char} data is missing to', WARNING);
+ continue;
+ }
+ if (!node.exists("from")) {
+ Logs.trace('Icon ${char} data is missing from', WARNING);
+ continue;
+ }
+
+ var animName = 'from-${node.get("from")}-to-${node.get("to")}';
+ if (node.exists("offsetX") || node.exists("offsetY"))
+ addOffset(animName, Std.parseFloat(node.get("offsetX")).getDefault(0), Std.parseFloat(node.get("offsetY")).getDefault(0));
+ addAnim(animName, node.get("anim"), Std.parseInt(node.get("fps")).getDefault(24), false); // don't allow looping for transitions
+ if (animateAtlas == null && animation.exists(animName))
+ animation.getByName(animName).flipX = isPlayer != iconIsPlayer;
+ case "anim":
+ if (this.animated == false) {
+ Logs.trace('Icon ${char} data is not allowed when not animated', WARNING);
+ continue;
+ }
+ if (!node.exists("name")) {
+ Logs.trace('Icon ${char} data is missing name', WARNING);
+ continue;
+ }
+ if (!node.exists("anim")) {
+ Logs.trace('Icon ${char} data is missing anim', WARNING);
+ continue;
+ }
+
+ var animName = node.get("name");
+
+ if (node.exists("offsetX") || node.exists("offsetY"))
+ addOffset(animName, Std.parseFloat(node.get("offsetX")).getDefault(0), Std.parseFloat(node.get("offsetY")).getDefault(0));
+ addAnim(animName, node.get("anim"), Std.parseInt(node.get("fps")).getDefault(24), node.get("looped").getDefault("true").toLowerCase() == "true");
+ if (animateAtlas == null && animation.exists(animName))
+ animation.getByName(animName).flipX = isPlayer != iconIsPlayer;
+ case "step":
+ if (!node.exists("percent")) {
+ Logs.trace('Icon ${char} data is missing percent', WARNING);
+ continue;
+ }
+ if (!node.exists("name")) {
+ Logs.trace('Icon ${char} data is missing name', WARNING);
+ continue;
+ }
+
+ parsedSteps.set(Std.parseInt(node.get("percent")).getDefault(0), node.get("name"));
+ }
+ }
+
+ if (Lambda.count(parsedSteps) > 0) {
+ healthSteps = parsedSteps;
+ } else {
+ var hasLosing = this.animated ? hasAnim("losing") : iconAmt >= 2;
+ var hasWinning = this.animated ? hasAnim("winning") : iconAmt >= 3;
+
+ if (hasLosing) {
+ healthSteps = [
+ 0 => (this.animated ? "losing" : 1), // losing icon
+ 20 => (this.animated ? "neutral" : 0), // normal icon
+ ];
+ } else {
+ healthSteps = [
+ 0 => (this.animated ? "neutral" : 0), // normal icon
+ ];
+ }
+ if (hasWinning)
+ healthSteps.set(80, this.animated ? "winning" : 2); // winning icon
+ }
+ var data = getIconAnim(health);
+ if (data.isValid) {
+ if (this.animated)
+ playAnim(data.animState);
+ else
+ animation.curAnim.curFrame = data.animState;
+ curAnimState = data.animState;
+ }
+
+ if (animateAtlas != null) {
+ @:bypassAccessor
+ frameWidth = 150;
+ @:bypassAccessor
+ frameHeight = 150;
+ extraOffsets.x -= frameWidth / 2;
+ extraOffsets.y -= frameHeight / 2;
+ updateHitbox();
+ } else {
+ setGraphicSize(150);
+ updateHitbox();
+ }
+
+ defaultScale = (xmlValid && xmlData.exists("scale")) ? Std.parseFloat(xmlData.get("scale")).getDefault(scale.x) : scale.x;
+ }
+
+ var normalizedNames = ["neutral", "losing", "winning"];
+ private function normalizeAnim(anim:OneOfTwo):OneOfTwo {
+ if(this.animated) {
+ if (anim is Int) {
+ var _:Int = cast anim;
+ if(_ >= 0 && _ < normalizedNames.length)
+ anim = normalizedNames[anim];
+ }
+ } else {
+ if (anim is String) {
+ var _ = normalizedNames.indexOf(cast anim);
+ if(_ >= 0)
+ anim = _;
+ }
+ }
+ return anim;
+ }
+
+ /**
+ * Gets the animation data for a specific health percentage
+ * @param health Health percentage
+ * @return Animation data (-1 if invalid)
+ */
+ public dynamic function getIconAnim(health:Float):IconAnimData {
+ var i:OneOfTwo = -1;
+ var oldKey:Int = -1;
+ var isValid = false;
+ for (k=>icon in healthSteps) if (k > oldKey && k <= health * 100) {
+ oldKey = k;
+ i = icon;
+ isValid = true;
+ }
+
+ i = normalizeAnim(i);
+
+ return new IconAnimData(i, isValid);
+ }
+
+ override function update(elapsed:Float):Void {
super.update(elapsed);
- if (sprTracker != null)
- setPosition(sprTracker.x + sprTracker.width + 10, sprTracker.y - 30);
+ if (sprTracker != null) {
+ setPosition(sprTracker.x + (switch sprTrackerAlignment {
+ case LEFT: -width;
+ case CENTER: (sprTracker.width - width) / 2;
+ case RIGHT: sprTracker.width;
+ }) + sprTrackerOffset.x, sprTracker.y + sprTrackerOffset.y);
+ }
- if (animation.curAnim != null) {
- var i:Int = -1;
- var oldKey:Int = -1;
- for (k=>icon in healthSteps) if (k > oldKey && k <= health * 100) {
- oldKey = k;
- i = icon;
+ if (animation.curAnim != null || (this.animated && animateAtlas != null)) {
+ var data = getIconAnim(health);
+ var localAnimState = data.animState;
+
+ if (data.isValid && curAnimState != localAnimState) {
+ if (this.animated) {
+ var transAnim = 'from-$curAnimState-to-$localAnimState';
+ playAnim(hasAnim(transAnim) ? transAnim : localAnimState);
+ } else {
+ if(animation.curAnim != null)
+ animation.curAnim.curFrame = localAnimState;
+ }
+
+ curAnimState = localAnimState;
}
-
- if (i >= 0) animation.curAnim.curFrame = i;
}
}
+
+ function animFinishCallback(anim:String):Void {
+ if (this.animated)
+ if (anim.startsWith("from-"))
+ playAnim(anim.substr(anim.lastIndexOf('-') + 1));
+ }
+
+ override function updateHitbox():Void {
+ super.updateHitbox();
+ offset += extraOffsets;
+ }
}
+
+@:dox(hide)
+class IconAnimData {
+ public var animState:OneOfTwo;
+ public var isValid:Bool;
+
+ public function new(animState:OneOfTwo, isValid:Bool) {
+ this.animState = animState;
+ this.isValid = isValid;
+ }
+
+ public function toString():String {
+ return '$animState (Valid: $isValid)';
+ }
+}
+
+/**
+ * Used for `funkin.game.HealthIcon.sprTrackerAlignment`.
+ * This determines the position of the icon in relation to the sprite tracker.
+**/
+enum abstract TrackerAlignment(Int) {
+ /**
+ * Left of the sprite tracker
+ *
+ * Mathematically: `tracker.x - icon.width`
+ */
+ var LEFT = 0;
+ /**
+ * Center of the sprite tracker
+ *
+ * Mathematically: `tracker.x + (tracker.width - icon.width) / 2`
+ */
+ var CENTER = 1;
+ /**
+ * Right of the sprite tracker
+ *
+ * Mathematically: `tracker.x + tracker.width`
+ */
+ var RIGHT = 2;
+}
\ No newline at end of file
diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx
index 65d5b0ac..58ea592d 100644
--- a/source/funkin/game/PlayState.hx
+++ b/source/funkin/game/PlayState.hx
@@ -808,7 +808,8 @@ class PlayState extends MusicBeatState
// Make icons appear in the correct spot during cutscenes
healthBar.update(0);
- updateIconPositions();
+ if (updateIconPositions != null)
+ updateIconPositions();
__updateNote_event = EventManager.get(NoteUpdateEvent);
@@ -1210,18 +1211,19 @@ class PlayState extends MusicBeatState
]));
}
- function updateIconPositions() {
+ dynamic function updateIconPositions() {
var iconOffset:Int = 26;
+ var healthBarPercent = healthBar.percent;
- var center:Float = healthBar.x + healthBar.width * FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0);
+ var center:Float = healthBar.x + healthBar.width * FlxMath.remapToRange(healthBarPercent, 0, 100, 1, 0);
iconP1.x = center - iconOffset;
iconP2.x = center - (iconP2.width - iconOffset);
health = FlxMath.bound(health, 0, maxHealth);
- iconP1.health = healthBar.percent / 100;
- iconP2.health = 1 - (healthBar.percent / 100);
+ iconP1.health = healthBarPercent / 100;
+ iconP2.health = 1 - (healthBarPercent / 100);
}
function updateRatingStuff() {
@@ -1267,14 +1269,13 @@ class PlayState extends MusicBeatState
}
}
- if (doIconBop) {
- iconP1.scale.set(lerp(iconP1.scale.x, 1, 0.33), lerp(iconP1.scale.y, 1, 0.33));
- iconP2.scale.set(lerp(iconP2.scale.x, 1, 0.33), lerp(iconP2.scale.y, 1, 0.33));
+ if (doIconBop)
+ for (icon in [iconP1, iconP2])
+ if (icon.updateBump != null)
+ icon.updateBump();
- iconP1.updateHitbox();
- iconP2.updateHitbox();
- }
- updateIconPositions();
+ if (updateIconPositions != null)
+ updateIconPositions();
if (startingSong)
{
@@ -1684,9 +1685,9 @@ class PlayState extends MusicBeatState
var event:NoteHitEvent;
if (strumLine != null && !strumLine.cpu)
- event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick");
+ event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick", 0.5, true, 0.7, true, true, iconP1);
else
- event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false);
+ event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false, 0.5, true, 0.7, true, true, iconP2);
event.deleteNote = !note.isSustainNote; // work around, to allow sustain notes to be deleted
event = scripts.event(strumLine != null && !strumLine.cpu ? "onPlayerHit" : "onDadHit", event);
strumLine.onHit.dispatch(event);
@@ -1846,14 +1847,10 @@ class PlayState extends MusicBeatState
camHUD.zoom += 0.03 * camZoomingStrength;
}
- if (doIconBop)
- {
- iconP1.scale.set(1.2, 1.2);
- iconP2.scale.set(1.2, 1.2);
-
- iconP1.updateHitbox();
- iconP2.updateHitbox();
- }
+ if (doIconBop)
+ for (icon in [iconP1, iconP2])
+ if (icon.bump != null)
+ icon.bump();
scripts.call("beatHit", [curBeat]);
}