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