diff --git a/assets/data/scripts/pixel.hx b/assets/data/scripts/pixel.hx index 22c4e611..b3d134c5 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/FunkinSprite.hx b/source/funkin/backend/FunkinSprite.hx index 00373e7b..4f0ecac9 100644 --- a/source/funkin/backend/FunkinSprite.hx +++ b/source/funkin/backend/FunkinSprite.hx @@ -75,24 +75,34 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I moves = false; } - public static function copyFrom(source:FunkinSprite) + /** + * Gets the graphics and copies other properties from another sprite (Works both for `FlxSprite` and `FunkinSprite`!). + */ + public static function copyFrom(source:FlxSprite):FunkinSprite { var spr = new FunkinSprite(); + var casted:FunkinSprite = null; + if (source is FunkinSprite) + casted = cast source; + @:privateAccess { spr.setPosition(source.x, source.y); spr.frames = source.frames; - if (source.animateAtlas != null && source.atlasPath != null) - spr.loadSprite(source.atlasPath); + if (casted != null && casted.animateAtlas != null && casted.atlasPath != null) + spr.loadSprite(casted.atlasPath); spr.animation.copyFrom(source.animation); spr.visible = source.visible; spr.alpha = source.alpha; spr.antialiasing = source.antialiasing; spr.scale.set(source.scale.x, source.scale.y); spr.scrollFactor.set(source.scrollFactor.x, source.scrollFactor.y); - spr.skew.set(source.skew.x, source.skew.y); - spr.transformMatrix = source.transformMatrix; - spr.matrixExposed = source.matrixExposed; - spr.animOffsets = source.animOffsets.copy(); + + if (casted != null) { + spr.skew.set(casted.skew.x, casted.skew.y); + spr.transformMatrix = casted.transformMatrix; + spr.matrixExposed = casted.matrixExposed; + spr.animOffsets = casted.animOffsets.copy(); + } } return spr; } diff --git a/source/funkin/backend/assets/AssetsLibraryList.hx b/source/funkin/backend/assets/AssetsLibraryList.hx index d7ff2737..b235638a 100644 --- a/source/funkin/backend/assets/AssetsLibraryList.hx +++ b/source/funkin/backend/assets/AssetsLibraryList.hx @@ -63,7 +63,7 @@ class AssetsLibraryList extends AssetLibrary { if (l is IModsAssetLibrary) { var lib = cast(l, IModsAssetLibrary); for(e in lib.getFiles(folder)) - content.push(e); + content.pushOnce(e); } #end } diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx index 93686c0c..6a27c38e 100644 --- a/source/funkin/backend/assets/Paths.hx +++ b/source/funkin/backend/assets/Paths.hx @@ -194,6 +194,29 @@ class Paths return tempFramesCache[key] = loadFrames(assetsPath ? key : Paths.image(key, library, true, ext), false, null, false, ext); } + /** + * 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/HScript.hx b/source/funkin/backend/scripting/HScript.hx index 1727750d..5cbedded 100644 --- a/source/funkin/backend/scripting/HScript.hx +++ b/source/funkin/backend/scripting/HScript.hx @@ -128,6 +128,10 @@ class HScript extends Script { interp.execute(expr); call("new", []); } + + #if GLOBAL_SCRIPT + funkin.backend.scripting.GlobalScript.call("onScriptSetup", [this, "hscript"]); + #end } public override function reload() { diff --git a/source/funkin/backend/scripting/events/healthicon/HealthIconChangeEvent.hx b/source/funkin/backend/scripting/events/healthicon/HealthIconChangeEvent.hx index dbb4cb68..95ab6a8d 100644 --- a/source/funkin/backend/scripting/events/healthicon/HealthIconChangeEvent.hx +++ b/source/funkin/backend/scripting/events/healthicon/HealthIconChangeEvent.hx @@ -1,10 +1,12 @@ package funkin.backend.scripting.events.healthicon; +import flixel.util.typeLimit.OneOfTwo; + final class HealthIconChangeEvent extends CancellableEvent { /** - * Amount + * Animation State */ - public var amount:Int; + public var anim:OneOfTwo; /** * The health icon diff --git a/source/funkin/backend/scripting/events/note/NoteHitEvent.hx b/source/funkin/backend/scripting/events/note/NoteHitEvent.hx index 5614b75c..c9f8c4c9 100644 --- a/source/funkin/backend/scripting/events/note/NoteHitEvent.hx +++ b/source/funkin/backend/scripting/events/note/NoteHitEvent.hx @@ -1,6 +1,7 @@ package funkin.backend.scripting.events.note; import funkin.game.Character; +import funkin.game.HealthIcon; import funkin.game.Note; final class NoteHitEvent extends CancellableEvent { @@ -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/backend/shaders/FunkinShader.hx b/source/funkin/backend/shaders/FunkinShader.hx index fb3f9201..281616b0 100644 --- a/source/funkin/backend/shaders/FunkinShader.hx +++ b/source/funkin/backend/shaders/FunkinShader.hx @@ -1,5 +1,6 @@ package funkin.backend.shaders; +import flixel.graphics.FlxGraphic; import flixel.system.FlxAssets.FlxShader; import flixel.util.FlxSignal.FlxTypedSignal; import haxe.Exception; @@ -555,34 +556,44 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { return val; } - var field:Dynamic = Reflect.field(data, name); - var cl:String = Type.getClassName(Type.getClass(field)); + var field = Reflect.field(data, name); + var cl = Type.getClassName(Type.getClass(field)); + var isNotNull = val != null; // cant do "field is ShaderInput" for some reason if (cl.startsWith("openfl.display.ShaderParameter")) { if (field.__length <= 1) { - if ((val is Array)) - return field.value = val; // that means we wait for a single number, instead of an array - if (field.__isInt && !(val is Int)) + if (field.__isInt && isNotNull && !(val is Int)) { throw new ShaderTypeException(name, Type.getClass(val), 'Int'); - else if (field.__isBool && !(val is Bool)) + return null; + } else + if (field.__isBool && isNotNull && !(val is Bool)) { throw new ShaderTypeException(name, Type.getClass(val), 'Bool'); - else if (field.__isFloat && !(val is Float)) + return null; + } else + if (field.__isFloat && isNotNull && !(val is Float)) { throw new ShaderTypeException(name, Type.getClass(val), 'Float'); - - return field.value = [val]; + return null; + } + return field.value = isNotNull ? [val] : null; } else { - if (!(val is Array)) + if (isNotNull && !(val is Array)) { throw new ShaderTypeException(name, Type.getClass(val), Array); - + return null; + } return field.value = val; } } else if (cl.startsWith("openfl.display.ShaderInput")) { // shader input!! - if (!(val is BitmapData)) { + var bitmap:BitmapData; + if (!isNotNull) bitmap = null; + else if (val is BitmapData) bitmap = val; + else if (val is FlxGraphic) bitmap = val.bitmap; + else { throw new ShaderTypeException(name, Type.getClass(val), BitmapData); + return null; } - field.input = cast val; + field.input = bitmap; } return val; diff --git a/source/funkin/editors/character/CharacterInfoScreen.hx b/source/funkin/editors/character/CharacterInfoScreen.hx index 599474cc..42905838 100644 --- a/source/funkin/editors/character/CharacterInfoScreen.hx +++ b/source/funkin/editors/character/CharacterInfoScreen.hx @@ -3,6 +3,7 @@ package funkin.editors.character; import flixel.math.FlxPoint; import funkin.editors.extra.PropertyButton; import funkin.game.Character; +import funkin.game.HealthIcon; import haxe.xml.Access; class CharacterInfoScreen extends UISubstateWindow { @@ -10,7 +11,7 @@ class CharacterInfoScreen extends UISubstateWindow { 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,22 +148,14 @@ 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/' + Flags.DEFAULT_HEALTH_ICON); - - 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); + var size = Std.int(150 * 0.5); + iconSprite.setUnstretchedGraphicSize(size, size, true); iconSprite.updateHitbox(); - iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2)); + iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2)); + iconSprite.scrollFactor.set(1, 1); } function saveCharacterInfo() { diff --git a/source/funkin/editors/charter/ChartCreationScreen.hx b/source/funkin/editors/charter/ChartCreationScreen.hx index b41e147e..87bd3c93 100644 --- a/source/funkin/editors/charter/ChartCreationScreen.hx +++ b/source/funkin/editors/charter/ChartCreationScreen.hx @@ -319,7 +319,8 @@ class CompactCharacterButton extends UIButton { autoAlpha = false; charIcon = new HealthIcon(funkin.game.Character.getIconFromCharName(char)); - charIcon.scale.set(0.2, 0.2); + var size = Std.int(150 * 0.2); + charIcon.setUnstretchedGraphicSize(size, size, true); charIcon.updateHitbox(); charIcon.setPosition(10, bHeight/2 - charIcon.height / 2); charIcon.scrollFactor.set(1,1); @@ -331,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/" + Flags.DEFAULT_HEALTH_ICON); - charIcon.loadGraphic(image, true, 150, 150); + charIcon.setIcon(char); + charIcon.setUnstretchedGraphicSize(size, size, true); 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 8dd5f937..a2337627 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; @@ -19,7 +20,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; @@ -124,22 +125,14 @@ 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/' + Flags.DEFAULT_HEALTH_ICON); - - 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); + var size = Std.int(150 * 0.5); + iconSprite.setUnstretchedGraphicSize(size, size, true); iconSprite.updateHitbox(); - iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2)); + iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2)); + iconSprite.scrollFactor.set(1, 1); } public function saveMeta() { diff --git a/source/funkin/editors/charter/CharterStrumline.hx b/source/funkin/editors/charter/CharterStrumline.hx index c7b332e3..afe96ad1 100644 --- a/source/funkin/editors/charter/CharterStrumline.hx +++ b/source/funkin/editors/charter/CharterStrumline.hx @@ -43,20 +43,7 @@ class CharterStrumline extends UISprite { if(strumLine.visible == null) strumLine.visible = true; - var icons = strumLine.characters != null ? strumLine.characters : []; - healthIcons = new FlxSpriteGroup(x, y); - - for (i=>icon in icons) { - var healthIcon = new HealthIcon(Character.getIconFromCharName(icon)); - healthIcon.scale.x = healthIcon.scale.y = 0.6 - (icons.length / 20); - healthIcon.updateHitbox(); - healthIcon.x = FlxMath.lerp(0, icons.length * 20, (icons.length-1 != 0 ? i / (icons.length-1) : 0)); - healthIcon.y = draggable ? 29 : 7; - healthIcon.alpha = strumLine.visible ? 1 : 0.4; - healthIcons.add(healthIcon); - } - members.push(healthIcons); draggingSprite = new UISprite(); @@ -70,8 +57,7 @@ class CharterStrumline extends UISprite { button = new CharterStrumlineOptions(this); members.push(button); - vocals = strumLine.vocalsSuffix.length > 0 ? FlxG.sound.load(Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, strumLine.vocalsSuffix)) : new FlxSound(); - vocals.group = FlxG.sound.defaultMusicGroup; + updateInfo(); selectedWaveform = -1; } @@ -106,7 +92,9 @@ 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); + var size = Std.int(150 * newScale); + healthIcon.setUnstretchedGraphicSize(size, size, true); 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 35730dc8..ba2534b7 100644 --- a/source/funkin/editors/charter/CharterStrumlineScreen.hx +++ b/source/funkin/editors/charter/CharterStrumlineScreen.hx @@ -192,7 +192,8 @@ class CharacterButton extends UIButton { super(x, y, "", null, 250, 54); charIcon = new HealthIcon(Character.getIconFromCharName(char)); - charIcon.scale.set(0.3, 0.3); + var size = Std.int(150 * 0.3); + charIcon.setUnstretchedGraphicSize(size, size, true); charIcon.updateHitbox(); charIcon.setPosition(x + 10, bHeight/2 - charIcon.height / 2); charIcon.scrollFactor.set(1,1); @@ -204,11 +205,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/" + Flags.DEFAULT_HEALTH_ICON); - charIcon.loadGraphic(image, true, 150, 150); + charIcon.setIcon(char); + charIcon.setUnstretchedGraphicSize(size, size, true); 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 5bb1a895..33cc4a73 100644 --- a/source/funkin/editors/charter/SongCreationScreen.hx +++ b/source/funkin/editors/charter/SongCreationScreen.hx @@ -8,6 +8,7 @@ import funkin.backend.chart.FNFLegacyParser.SwagSong; import funkin.backend.chart.PsychParser; import funkin.backend.chart.VSliceParser; import funkin.backend.utils.ZipUtil; +import funkin.game.HealthIcon; import haxe.Json; import haxe.io.Bytes; import haxe.io.BytesInput; @@ -33,7 +34,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; @@ -328,22 +329,14 @@ class SongCreationScreen extends UISubstateWindow { } function updateIcon(icon:String) { - if (iconSprite == null) menuDataGroup.add(iconSprite = new FlxSprite()); + if (iconSprite == null) menuDataGroup.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/' + Flags.DEFAULT_HEALTH_ICON); - - 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); + var size = Std.int(150 * 0.5); + iconSprite.setUnstretchedGraphicSize(size, size, true); iconSprite.updateHitbox(); - iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2)); + iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2)); + iconSprite.scrollFactor.set(1, 1); } function saveSongInfo() { diff --git a/source/funkin/game/HealthIcon.hx b/source/funkin/game/HealthIcon.hx index 5d2c8225..558ade61 100644 --- a/source/funkin/game/HealthIcon.hx +++ b/source/funkin/game/HealthIcon.hx @@ -1,12 +1,32 @@ package funkin.game; +import flixel.graphics.FlxGraphic; +import flixel.math.FlxPoint; +import flixel.util.typeLimit.OneOfTwo; +import funkin.backend.scripting.events.healthicon.HealthIconChangeEvent; -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,31 +40,60 @@ 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 + * Current animation state */ - public var curAnimState:Int = -1; + 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 ]; } @@ -58,50 +107,338 @@ 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/' + Flags.DEFAULT_HEALTH_ICON); - - 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 = Flags.BOP_ICON_SCALE; + 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 = Flags.ICON_LERP; + 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(newIconPath)); + else { + var iconAsset:FlxGraphic = FlxG.bitmap.add(Paths.image(iconPath)); + var assetW:Float = iconAsset.width; + var assetH:Float = iconAsset.height; + + iconAmt = Math.round(assetW / assetH); // Just in case the icon is in a weird aspect ratio + iconSize = Math.floor(assetW / iconAmt); + if (iconSize * iconAmt > assetW) { + iconSize = Math.floor(assetW / iconAmt); + iconAmt = Math.floor(assetW / iconSize); + } + + 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"); + + var offsetX:Float = 0; + var offsetY:Float = 0; + if (node.exists("offsetX")) + offsetX = Std.parseFloat(node.get("offsetX")).getDefault(0); + else if (node.exists("offsetx")) + offsetX = Std.parseFloat(node.get("offsetx")).getDefault(0); + + if (node.exists("offsetY")) + offsetY = Std.parseFloat(node.get("offsetY")).getDefault(0); + else if (node.exists("offsety")) + offsetY = Std.parseFloat(node.get("offsety")).getDefault(0); + + addOffset(animName, offsetX, offsetY); + + 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; + scale.set(defaultScale, defaultScale); + updateHitbox(); + } + + 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 (i >= 0 && curAnimState != i) { - var event = EventManager.get(funkin.backend.scripting.events.healthicon.HealthIconChangeEvent).recycle(i, this); + if (animation.curAnim != null || (this.animated && animateAtlas != null)) { + var data = getIconAnim(health); + var localAnimState = data.animState; + + if (data.isValid && curAnimState != localAnimState) { + var event = EventManager.get(HealthIconChangeEvent).recycle(localAnimState, this); funkin.backend.scripting.GlobalScript.event("onHealthIconAnimChange", event); - if (!event.cancelled) - animation.curAnim.curFrame = event.amount; + if (!event.cancelled) { + if (this.animated) { + var transAnim = 'from-$curAnimState-to-${event.anim}'; + playAnim(hasAnim(transAnim) ? transAnim : event.anim); + } else { + if(animation.curAnim != null) + animation.curAnim.curFrame = event.anim; + } + } - curAnimState = event.amount; + curAnimState = localAnimState; } } } + + 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 e9c0044f..a593a23e 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -646,10 +646,11 @@ class PlayState extends MusicBeatState // case "": // ADD YOUR HARDCODED SCRIPTS HERE! default: - var scriptsFolders:Array = ['songs/${SONG.meta.name.toLowerCase()}/scripts', 'data/charts/', 'songs/']; + var normal = 'songs/${SONG.meta.name.toLowerCase()}/scripts'; + var scriptsFolders:Array = [normal, normal + '/$difficulty/', 'data/charts/', 'songs/']; - for(folder in scriptsFolders) { - for(file in Paths.getFolderContent(folder, true, fromMods ? MODS : BOTH)) { + for (folder in scriptsFolders) { + for (file in Paths.getFolderContent(folder, true, fromMods ? MODS : BOTH)) { if (folder == 'data/charts/') Logs.warn('data/charts/ is deprecated and will be removed in the future. Please move script $file to songs/', DARKYELLOW, "PlayState"); @@ -848,7 +849,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); @@ -1266,18 +1268,19 @@ class PlayState extends MusicBeatState ])); } - function updateIconPositions() { + dynamic function updateIconPositions() { var iconOffset = Flags.ICON_OFFSET; + 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() { @@ -1323,15 +1326,13 @@ class PlayState extends MusicBeatState }*/ } - if (doIconBop) { - var iconLerp = Flags.ICON_LERP; - iconP1.scale.set(lerp(iconP1.scale.x, 1, iconLerp), lerp(iconP1.scale.y, 1, iconLerp)); - iconP2.scale.set(lerp(iconP2.scale.x, 1, iconLerp), lerp(iconP2.scale.y, 1, iconLerp)); + if (doIconBop) + for (icon in [iconP1, iconP2]) + if (icon.updateBump != null) + icon.updateBump(); - iconP1.updateHitbox(); - iconP2.updateHitbox(); - } - updateIconPositions(); + if (updateIconPositions != null) + updateIconPositions(); if (startingSong) { @@ -1794,9 +1795,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); @@ -1959,14 +1960,9 @@ class PlayState extends MusicBeatState } if (doIconBop) - { - var iconScale = Flags.BOP_ICON_SCALE; - iconP1.scale.set(iconScale, iconScale); - iconP2.scale.set(iconScale, iconScale); - - iconP1.updateHitbox(); - iconP2.updateHitbox(); - } + for (icon in [iconP1, iconP2]) + if (icon.bump != null) + icon.bump(); scripts.call("beatHit", [curBeat]); } diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index 3cf0f0b5..e914ea55 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -139,6 +139,8 @@ class FreeplayState extends MusicBeatState var icon:HealthIcon = new HealthIcon(songs[i].icon); icon.sprTracker = songText; + icon.setUnstretchedGraphicSize(150, 150, true); + icon.updateHitbox(); // using a FlxGroup is too much fuss! iconArray.push(icon); diff --git a/source/funkin/options/type/IconOption.hx b/source/funkin/options/type/IconOption.hx index aa4ad3c2..b61c5c67 100644 --- a/source/funkin/options/type/IconOption.hx +++ b/source/funkin/options/type/IconOption.hx @@ -14,6 +14,10 @@ class IconOption extends TextOption { iconSpr = new HealthIcon(icon, false); iconSpr.setPosition(90 - iconSpr.width, (__text.height - iconSpr.height) / 2); + iconSpr.setUnstretchedGraphicSize(150, 150, true); + iconSpr.updateHitbox(); + iconSpr.sprTracker = __text; + iconSpr.sprTrackerAlignment = LEFT; add(iconSpr); } } \ No newline at end of file diff --git a/source/funkin/options/type/NewOption.hx b/source/funkin/options/type/NewOption.hx index 6804608b..6102ca60 100644 --- a/source/funkin/options/type/NewOption.hx +++ b/source/funkin/options/type/NewOption.hx @@ -1,7 +1,6 @@ package funkin.options.type; import flixel.util.FlxColor; -import funkin.game.HealthIcon; /** * Option type that has a new button, and is green.