diff --git a/assets/data/config/menuItems.txt b/assets/data/config/menuItems.txt index 6fc72f13..1c59f5b1 100644 --- a/assets/data/config/menuItems.txt +++ b/assets/data/config/menuItems.txt @@ -1,4 +1,5 @@ story mode +visual novel freeplay options donate \ No newline at end of file diff --git a/assets/fonts/fnf.ttf b/assets/fonts/fnf.ttf new file mode 100644 index 00000000..a3abd44b Binary files /dev/null and b/assets/fonts/fnf.ttf differ diff --git a/assets/images/menus/mainmenu/visual novel.png b/assets/images/menus/mainmenu/visual novel.png new file mode 100644 index 00000000..e584ba20 Binary files /dev/null and b/assets/images/menus/mainmenu/visual novel.png differ diff --git a/assets/images/menus/mainmenu/visual novel.xml b/assets/images/menus/mainmenu/visual novel.xml new file mode 100644 index 00000000..6211bbc0 --- /dev/null +++ b/assets/images/menus/mainmenu/visual novel.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/menus/titlescreen/titleEnter.png b/assets/images/menus/titlescreen/titleEnter.png index 38d6fab9..87d2e8c5 100644 Binary files a/assets/images/menus/titlescreen/titleEnter.png and b/assets/images/menus/titlescreen/titleEnter.png differ diff --git a/assets/images/menus/titlescreen/titleEnter.xml b/assets/images/menus/titlescreen/titleEnter.xml index 7aaf5458..781d662d 100644 --- a/assets/images/menus/titlescreen/titleEnter.xml +++ b/assets/images/menus/titlescreen/titleEnter.xml @@ -1,61 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/vn/bgs/school.png b/assets/images/vn/bgs/school.png new file mode 100644 index 00000000..aa499235 Binary files /dev/null and b/assets/images/vn/bgs/school.png differ diff --git a/assets/images/vn/box.png b/assets/images/vn/box.png new file mode 100644 index 00000000..5b02b55e Binary files /dev/null and b/assets/images/vn/box.png differ diff --git a/assets/images/vn/options.png b/assets/images/vn/options.png new file mode 100644 index 00000000..bc33d293 Binary files /dev/null and b/assets/images/vn/options.png differ diff --git a/assets/images/vn/options_selected.png b/assets/images/vn/options_selected.png new file mode 100644 index 00000000..3d2f88ca Binary files /dev/null and b/assets/images/vn/options_selected.png differ diff --git a/assets/images/vn/portraits/cody.png b/assets/images/vn/portraits/cody.png new file mode 100644 index 00000000..c277489e Binary files /dev/null and b/assets/images/vn/portraits/cody.png differ diff --git a/assets/images/vn/portraits/kiss.png b/assets/images/vn/portraits/kiss.png new file mode 100644 index 00000000..24249dc4 Binary files /dev/null and b/assets/images/vn/portraits/kiss.png differ diff --git a/assets/images/vn/portraits/pessy.png b/assets/images/vn/portraits/pessy.png new file mode 100644 index 00000000..e88095ea Binary files /dev/null and b/assets/images/vn/portraits/pessy.png differ diff --git a/assets/images/vn/senpaimusic.ogg b/assets/images/vn/senpaimusic.ogg new file mode 100644 index 00000000..ec8b5d61 Binary files /dev/null and b/assets/images/vn/senpaimusic.ogg differ diff --git a/assets/images/vn/sound_13.wav b/assets/images/vn/sound_13.wav new file mode 100644 index 00000000..f16be704 Binary files /dev/null and b/assets/images/vn/sound_13.wav differ diff --git a/assets/images/vn/vn.xml b/assets/images/vn/vn.xml new file mode 100644 index 00000000..6f05017d --- /dev/null +++ b/assets/images/vn/vn.xml @@ -0,0 +1,556 @@ + + + + + Pessy + pessy + 1 + 653BA9 + left + + + + Cody + cody + 1 + A36BCC + right + + + + Pessy & Cody + kiss + 1 + 7c4cb6 + center + 0,50 + + + + + The day starts of as usual, with Cody and Pessy meeting each other whilst on the way to school + + + + Hey Cody! How are you doing? + + + + Hey Pessy, I'm doing well! + + + + Did you sleep well Cody? + Yes i did + No i didn't + + + + I slept well, I dreamt about all the possibilities of what Friday Night Funkin' could include in their next update! + + + + I imagine what the lemon demon songs for the erect difficulty would sound like! + + + + + Oh yeah!! I bet it's gonna sound really creepy but also good! + + + + I bet. The voice actor for him is really good at it! + + + + I had a horrible nightmare, I dreamt of Friday Night Funkin' getting canceled. + + + + It was due to the developers being exposed of spending all the kickstarter money on crack. A team member who is against this posted a google doc of the situation on twitter. + + + + Oh man that must have been a really bad nightmare. At least they are now showing teasers online like that erect sampler they showed the other day! + + + + Yeah, I'm really glad that they are now finally doing that after almost 3 years of nothing. The FNF mods were really making this community alive. + + + +talk about current mods, where each engine talking about each others engines mods and how cool they are + + + Have you heard about the mod… + + Talk about: Impostor V4 + Talk about: Hotline 024 + Talk about: Mario's Madness + Talk about: 17 Bucks + + + + Vs Impostor? + + + + Yea i heard about that! I find it very sus! Ha. + + + + But actually it's a really good mod. I like how Maroon's parasite form looks! It really fits for his song. + + + + Yeah me too!! My favorite part from Vs Impostor is Finale. That song is so good I love it to death!!! + + + + I don't blame you! That's the best song from the mod tbh. + + + + Have you heard about Sunday's Desolation? + + + + Oh yeah I did! That's such a good mod. It's really high quality for a fanmade mod about Wednesday's Infidelity! + + + + Yeah, it even has one of your developers for your engine on the team. That's really awesome!! + + + + I think that's really cool actually. I like that this mod is all about Oswald from the original mod. Really cool that the team is making a whole mod about him! + + +—- + + + Hotline 024? + + + + Yea i heard about that! I really like Nikku's character design, and how she can mimic other voices! + + + + Yeah!! There's even a whole tricky-themed song in the mod, and even Nikku is mimicking Tricky's voice at certain parts!! + + + + Kinda sucks that the original tricky mod was made on Kade Engine, Kevin is just really too much of a hardcoding guy and he is really unoptimized. I don't like talking to him that much. + + + + I do see him hanging out with the other engines. But lately his popularity has kinda stopped. + + + + Yeah… I do admit he's the reason why people started modding FNF though, he's the one that started FNF modding! + + + + I agree… He actually makes me feel nostalgic of the early era of FNF modding… + + + + Have you seen Cyber Sensation recently? + + + + I did actually! Unfortunate that it got canceled though… + + + + Yeah… the creator had to leave the community. Got exhausted from the game. + + + + I don't blame her. FNF does get really tiring after a good while. + + +—--- + + + Mario's Madness? + + + + Yeah! That's one of the trending mods right now! I like how All-Stars play out. It's such a sad but awesome ending! + + + + I agree!! Some people are mad about it, but to be honest BF wins too much. He had to lose at some point. + + + + True. Sometimes all good things must come to an end. + + + + heyy mmv2 reference!! That's from the dialogue when mario sees bf again before All-Stars happens lol!! + + + + omg LMAO cody you're so funny!!! + + + + They laughed for a solid 5 minutes. + + + + haha….. Anyways, did you hear about Gorefield getting a big update? + + + + Yeah I did!! V2 is such a huge improvement from V1 it's insane. + + + + Yeah!! I like how Cataclysm goes… It's pretty similar to All-Stars + + + + It's a really awesome song to be honest!! Such an awesome ending to the mod. I like that there are flashbacks of the previous songs played and that they are being controlled by Godfield. + + + + Again, it's like how All-Stars play out! Like how Ultra M is controlling the past enemies the player faced against at Act 4. + + + + I really like that mods do that. + + +—--- + + + + 17 Bucks? + + + + Yeah I have! The charm of it is actually so amazing. + + + + I agree, my favorite song from the mod is Kevinteenbucks. + + + + Cool! Mine is Fitin'. The song loops forever but to be honest that's really funny to me! + + + + I think that's funny too! That's actually my first time seeing a mod do that when I first played it. + + + + Yeah, it's pretty awesome! Pretty cool how people in this community can code such cool things. + + + + Have you heard about YTP Invasion? + + + + Yeah!! It's not out yet, but it's going to really soon! + + + + Yup.. I think it's gonna drop this month actually! + + + + Whaaat, really?? + + + + Yes, they announced that sometime ago on their twitter account! They released a trailer about it and everyone got super hyped about it!! + + + + Woahhh… cool!! What is it all about? + + + + Well basically, it's a mod about iconic Youtube Poop (YTP for short) characters rap battling against boyfriend. + + + + Sounds cool! I think that's gonna be a really awesome mod everyone will love. + + +—--- + + +talk about some stuff that's been happening with the base game + + + Did you see that new FNF blog that recently got released? It shows the new chart editor that's with the base game! + + + + I saw!!! It looked really cool. I saw it looked inspired by your editor. + + + + I know! It feels inspired by yours too! + + + + Yeah! I rembeber adding waveforms way back! Also they added your note selection and dragging! + + + + That just makes me really happy. Even though I'm not that much of a popular engine, the FNF developers still might have taken some inpriation from me. Which is awesome considering they defenitely took some from you! + + + + That shows they take advice from FNF modders and FNF engine developers! They care about the community. + + + + That may be true because they also updated their erect songs after dropping the erect sampler! + + + + They did? Wow! That's actually really awesome. What songs did they change? + + + + So far we only know Fresh and Pico. They dropped the updated versions on their Tiktok account. + + + + Oh cool! I didn't really like how BF sounded in Pico. His vocals were kinda just all over the place. + + + + Yeah they actually changed that! His vocals became less intense and are actually more calm now. + + + + That's awesome. I really can't wait for the update! + + +—- + +small personal talk + + + The both of them start thinking about their personal things. Both of them think they should share it to the other. + + + + I recently transitioned from alpha to beta state the other day. + + + + Oh really? That's great news! Good job on making progress on your engine! + + + + Thank you! I'm actually excited to release this engine at some point. + + + + I also heard that you started working on the final version of psych. That's pretty cool! + + + + I heard it's gonna have a stage editor! I'm really excited to see that! + + + + Yea and I'm also working on adding Language support, and a new chart editor. + + + + Oh awesome!! That's cool that you're finishing your engine. I can't wait for it to drop! + + + + They both went silent for a moment. + + +—-- + +talking about crushes + + + I have a crush on this engine where you can softcode literally anything… + + + + R…really? Who is this engine you're talking about…? + + + + …Y-...you… + + + + …W-what? Oh my god… I think I'm blushing…!! + + + + …yeah… + + + + …well… + + + + + + + I have a crush on an engine that is user-friendly and has lua scripting… + + + + …Cody… + + + + Pessy leans to Cody for a kiss. + + Kiss Pessy + Reject the kiss + + + + Cody leans to Pessy to kiss her. + + + + *muwha* *sloppy kisses* + + + + *drools* *muwah muah* + + + + …that… was something. + + + + Cody… I love you. + + + + …I… love you too… + + + + … + + + + … + + + + Pessy looks at her clock. It seems they are running late for class! + + + + + Cody backs up. + + + + Hey wait… I don't really wanna start this relationship yet. + + + + Oh… well that's alright. Do you wanna have a date sometime later then? + + + + Sure. I think we gotta develop our feelings for each other first before we take it to the next level. + + + + Pessy looks at her clock. It seems they are running late for class! + + + + + …Pessy… I really don't want to hurt your feelings, but I don't feel the same way for you… + + + + Oh… well… I'm sorry for being weird there… + + + + It's ok Pessy… I think it's best if we just stay as friends. + + + + Alrighty… I'm ok with that. + + + + Pessy looks at her clock. It seems they are running late for class! + + +— + +ending + + + We should probably get to class now! + + + + It was really fun talking to you. + + + + Same here. + + + + Well I will see you sometime later, ok? + + + + Alrighty… Bye Cody!! + + + + Bye Pessy! + + +Credits: + + + Credits: + Coding: Ne_Eo + Music & Sfx: Bromaster819 + Writing: Syrup, Bromaster819, Ne_Eo, SlightlyCreative, swordcube + Art: Mkv8 + + + + Time spent writing this novel: 3h 19m + Time since idea: 3h 57m + + + + Replay? + + Yes + Exit + + + + \ No newline at end of file diff --git a/assets/shaders/engine/editorWaveforms.frag b/assets/shaders/engine/editorWaveforms.frag index c2ea827d..7ac0df32 100644 --- a/assets/shaders/engine/editorWaveforms.frag +++ b/assets/shaders/engine/editorWaveforms.frag @@ -2,6 +2,11 @@ // Used in charter by waveforms +#define HEIGHT 0.05 // Height of the Bar +#define BACKLIGHT 0.6 // Backlight +#define BRIGHTNESS 0.33 // overall Brightness +#define GLOW 1.25 // Glow intensity of the Bar + const vec3 gradient1 = vec3(114.0/255.0, 81.0/255.0, 135.0/255.0); const vec3 gradient2 = vec3(144.0/255.0, 80.0/255.0, 186.0/255.0); @@ -18,6 +23,8 @@ uniform ivec2 waveformSize; uniform bool lowDetail; +uniform float time; + float getAmplitude(vec2 pixel) { float pixelID = floor((pixel.y+pixelOffset)/3.0); @@ -52,26 +59,7 @@ void outlineWaveform(vec2 pixel, vec2 offset) { float ampwidth = (1.0-amplitude) * textureRes.x; if (!inWaveForm(pixel, ampwidth)) - gl_FragColor -= outlinecolor*mix(0.5, 0.8, amplitude); -} - -void highDetailWaveform(vec2 pixel, float amplitude, float ampwidth) { - vec3 gradientColor = mix(gradient1, gradient2, openfl_TextureCoordv.x); - gradientColor *= pixel.y+pixelOffset>playerPosition ? 0.7 : 1.0; - gradientColor = mix(gradientColor, gradientColor * vec3(1.8), map(getHightlight(amplitude), 0.0, 1.0, 0.5, 1.0)); - - float ampwidthHighlight = getAmpWidth(amplitude/3.0); - if (inWaveForm(pixel, ampwidthHighlight)) - gradientColor = mix(gradientColor, gradientColor * vec3(1.8), map(getHightlight(ampwidthHighlight), 0.0, 1.0, 0.5, 1.0)); - - gl_FragColor = vec4(vec3(gradientColor * mix(0.5, 0.8, amplitude))*openfl_Alphav, openfl_Alphav); - - outlineWaveform(pixel, vec2(0.0, -1.0)); - outlineWaveform(pixel, vec2(0.0, 1.0)); -} - -void lowDetailWaveform(vec2 pixel, float amplitude, float ampwidth) { - gl_FragColor = vec4(lowdetailcol * (pixel.y+pixelOffset>playerPosition ? 0.7 : 1.0), 1.0); + gl_FragColor = vec4(vec3(0.), 1.); } void main() @@ -80,9 +68,21 @@ void main() float amplitude = getAmplitude(pixel); float ampwidth = getAmpWidth(amplitude); - gl_FragColor = vec4(0.0); - if (!inWaveForm(pixel, ampwidth)) return; + gl_FragColor = vec4(vec3(0.0), .1); - if (lowDetail) lowDetailWaveform(pixel, amplitude, ampwidth); - else highDetailWaveform(pixel, amplitude, ampwidth); + vec2 uv = openfl_TextureCoordv.xy - 0.5; + + float c = BACKLIGHT; + float a = abs(uv.x); + float s = 1.0 - smoothstep(0.0, ampwidth/textureRes.x, a); + c *= 1.33 - smoothstep(0.0, 0.5, a); + c*=c*c; + c += s*.5; + + if (inWaveForm(pixel, ampwidth)) + c += s; + + gl_FragColor = vec4((cos(6.283 * (uv.y + time + vec3(0.0,.33,0.66)))*map(getHightlight(amplitude), 0.0, 1.0, 0.5, 1.0)) + GLOW, 1.0) * c * BRIGHTNESS; + gl_FragColor.xyz *= map(getHightlight(amplitude), 0.0, 1.0, 0.5, 1.0); + gl_FragColor.xyz *= pixel.y+pixelOffset>playerPosition ? 0.6 : 1.0; } \ No newline at end of file diff --git a/source/flixel/text/FlxText.hx b/source/flixel/text/FlxText.hx new file mode 100644 index 00000000..3e8505e3 --- /dev/null +++ b/source/flixel/text/FlxText.hx @@ -0,0 +1,1293 @@ +package flixel.text; + +import flixel.FlxG; +import flixel.FlxSprite; +import flixel.graphics.FlxGraphic; +import flixel.graphics.atlas.FlxAtlas; +import flixel.graphics.atlas.FlxNode; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.math.FlxMath; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.system.FlxAssets; +import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; +import flixel.util.helpers.FlxRange; +import openfl.Assets; +import openfl.display.BitmapData; +import openfl.geom.ColorTransform; +import openfl.text.TextField; +import openfl.text.TextFieldAutoSize; +import openfl.text.TextFormat; +import openfl.text.TextFormatAlign; + +using flixel.util.FlxStringUtil; +using flixel.util.FlxUnicodeUtil; + +#if flash +import openfl.geom.Rectangle; +#end +#if (openfl >= "8.0.0") +import openfl.utils.AssetType; +#end + +// TODO: think about filters and text + +/** + * Extends FlxSprite to support rendering text. Can tint, fade, rotate and scale just like a sprite. Doesn't really animate + * though. Also does nice pixel-perfect centering on pixel fonts as long as they are only one-liners. + * + * ## Autosizing + * + * By default `FlxText` is autosized to fit it's text. + * To set a fixed size, use the `fieldWidth`, `fieldHeight` and `autoSize` fields. + */ +class FlxText extends FlxSprite +{ + /** + * 4px gutter at the bottom when the field has automatic height + */ + static inline var VERTICAL_GUTTER:Int = 4; + + /** + * The text being displayed. + */ + public var text(default, set):String = ""; + + /** + * The size of the text being displayed in pixels. + */ + public var size(get, set):Int; + + /** + * A number representing the amount of space that is uniformly distributed + * between all characters. The value specifies the number of pixels that are + * added to the advance after each character. + */ + public var letterSpacing(get, set):Float; + + /** + * The font used for this text (assuming that it's using embedded font). + */ + public var font(get, set):String; + + /** + * Whether this text field uses an embedded font (by default) or not. + * Read-only - use `systemFont` to specify a system font to use, which then automatically sets this to `false`. + */ + public var embedded(get, never):Bool; + + /** + * The system font for this text (not embedded). Setting this sets `embedded` to `false`. + * Passing an invalid font name (like `""` or `null`) causes a default font to be used. + */ + public var systemFont(get, set):String; + + /** + * Whether to use bold text or not (`false` by default). + */ + public var bold(get, set):Bool; + + /** + * Whether to use italic text or not (`false` by default). Only works on Flash. + */ + public var italic(get, set):Bool; + + /** + * Whether to use underlined text or not (`false` by default). + */ + public var underline(get, set):Bool; + + /** + * Whether to use word wrapping and multiline or not (`true` by default). + */ + public var wordWrap(get, set):Bool; + + /** + * The alignment of the font. Note: `autoSize` must be set to + * `false` or `alignment` won't show any visual differences. + */ + public var alignment(get, set):FlxTextAlign; + + /** + * The border style to use + */ + public var borderStyle(default, set):FlxTextBorderStyle = NONE; + + /** + * The color of the border in `0xAARRGGBB` format + */ + public var borderColor(default, set):FlxColor = FlxColor.TRANSPARENT; + + /** + * The size of the border, in pixels. + */ + public var borderSize(default, set):Float = 1; + + /** + * How many iterations do use when drawing the border. `0`: only 1 iteration, `1`: one iteration for every pixel in `borderSize` + * A value of `1` will have the best quality for large border sizes, but might reduce performance when changing text. + * NOTE: If the `borderSize` is `1`, `borderQuality` of `0` or `1` will have the exact same effect (and performance). + */ + public var borderQuality(default, set):Float = 1; + + /** + * Reference to a `TextField` object used internally for rendering - + * be sure to know what you're doing if messing with its properties! + */ + public var textField(default, null):TextField; + + /** + * The width of the `TextField` object used for bitmap generation for this `FlxText` object. + * Use it when you want to change the visible width of text. Enables `autoSize` if `<= 0`. + * + * **NOTE:** auto width always implies auto height + */ + public var fieldWidth(get, set):Float; + + /** + * The height of `TextField` object used for bitmap generation for this `FlxText` object. + * Use it when you want to change the visible height of the text. Enables "auto height" if `<= 0`. + * + * **NOTE:** Fixed height has no effect if `autoSize = true`. + * @since 5.4.0 + */ + public var fieldHeight(get, set):Float; + + /** + * Whether the `fieldWidth` and `fieldHeight` should be determined automatically. + * Requires `wordWrap` to be `false`. + */ + public var autoSize(get, set):Bool; + + var _autoHeight:Bool = true; + + /** + * Offset that is applied to the shadow border style, if active. + * `x` and `y` are multiplied by `borderSize`. Default is `(1, 1)`, or lower-right corner. + */ + public var shadowOffset(default, null):FlxPoint; + + var _defaultFormat:TextFormat; + var _formatAdjusted:TextFormat; + var _formatRanges:Array = []; + var _font:String; + + /** + * Helper boolean which tells whether to update graphic of this text object or not. + */ + var _regen:Bool = true; + + /** + * Helper vars to draw border styles with transparency. + */ + var _borderPixels:BitmapData; + + var _borderColorTransform:ColorTransform; + + var _hasBorderAlpha = false; + + #if flash + /** + * Helper to draw line by line used at `drawTextFieldTo()`. + */ + var _textFieldRect:Rectangle = new Rectangle(); + #end + + /** + * Creates a new `FlxText` object at the specified position. + * + * @param X The x position of the text. + * @param Y The y position of the text. + * @param FieldWidth The `width` of the text object. Enables `autoSize` if `<= 0`. + * (`height` is determined automatically). + * @param Text The actual text you would like to display initially. + * @param Size The font size for this text object. + * @param EmbeddedFont Whether this text field uses embedded fonts or not. + */ + public function new(X:Float = 0, Y:Float = 0, FieldWidth:Float = 0, ?Text:String, Size:Int = 8, EmbeddedFont:Bool = true) + { + super(X, Y); + + if (Text == null || Text == "") + { + // empty texts have a textHeight of 0, need to + // prevent initializing with "" before the first calcFrame() call + text = ""; + Text = " "; + } + else + { + text = Text; + } + + textField = new TextField(); + textField.selectable = false; + textField.multiline = true; + textField.wordWrap = true; + _defaultFormat = new TextFormat(null, Size, 0xffffff); + letterSpacing = 0; + font = FlxAssets.FONT_DEFAULT; + _formatAdjusted = new TextFormat(); + textField.defaultTextFormat = _defaultFormat; + textField.text = Uwuifier.uwuifySentence(Text); + fieldWidth = FieldWidth; + textField.embedFonts = EmbeddedFont; + textField.sharpness = 100; + textField.height = (Text.length <= 0) ? 1 : 10; + + allowCollisions = NONE; + moves = false; + + drawFrame(); + + shadowOffset = FlxPoint.get(1, 1); + } + + /** + * Clean up memory. + */ + override public function destroy():Void + { + textField = null; + _font = null; + _defaultFormat = null; + _formatAdjusted = null; + shadowOffset = FlxDestroyUtil.put(shadowOffset); + super.destroy(); + } + + override public function drawFrame(Force:Bool = false):Void + { + _regen = _regen || Force; + super.drawFrame(_regen); + } + + /** + * Stamps text onto specified atlas object and loads graphic from this atlas. + * WARNING: Changing text after stamping it on the atlas will break the atlas, so do it only for + * static texts and only after making all the text customizing (like `size`, `alignment`, `color`, etc.) + * + * @param atlas atlas to stamp graphic to. + * @return whether the graphic was stamped on the atlas successfully + */ + public function stampOnAtlas(atlas:FlxAtlas):Bool + { + regenGraphic(); + + var node:FlxNode = atlas.addNode(graphic.bitmap, graphic.key); + var result:Bool = (node != null); + + if (node != null) + { + frames = node.getImageFrame(); + } + + return result; + } + + /** + * Applies formats to text between marker characters, then removes those markers. + * NOTE: this will clear all `FlxTextFormat`s and return to the default format. + * + * Usage: + * + * ```haxe + * text.applyMarkup( + * "show $green text$ between dollar-signs", + * [new FlxTextFormatMarkerPair(greenFormat, "$")] + * ); + * ``` + * + * Even works for complex nested formats like this: + * + * ```haxe + * var yellow = new FlxTextFormatMarkerPair(yellowFormat, "@"); + * var green = new FlxTextFormatMarkerPair(greenFormat, ""); + * text.applyMarkup("Hey @Buddy@, what is going @on?@", [yellow, green]); + * ``` + * + * @param input The text you want to format + * @param rules `FlxTextFormat`s to selectively apply, paired with marker strings + */ + public function applyMarkup(input:String, rules:Array):FlxText + { + if (rules == null || rules.length == 0) + return this; // there's no point in running the big loop + + clearFormats(); // start with default formatting + + var rangeStarts:Array = []; + var rangeEnds:Array = []; + var rulesToApply:Array = []; + + var i:Int = 0; + for (rule in rules) + { + if (rule.marker == null || rule.format == null) + continue; + + var start:Bool = false; + var markerLength:Int = rule.marker.uLength(); + + if (!input.contains(rule.marker)) + continue; // marker not present + + // inspect each character + for (charIndex in 0...input.uLength()) + { + if (!input.uSub(charIndex, markerLength).uEquals(rule.marker)) + continue; // it's not one of the markers + + if (start) + { + start = false; + rangeEnds.push(charIndex); // end a format block + } + else // we're outside of a format block + { + start = true; // start a format block + rangeStarts.push(charIndex); + rulesToApply.push(rule); + } + } + + if (start) + { + // we ended with an unclosed block, mark it as infinite + rangeEnds.push(-1); + } + + i++; + } + + // Remove all of the markers in the string + for (rule in rules) + input = input.remove(rule.marker); + + // Adjust all the ranges to reflect the removed markers + for (i in 0...rangeStarts.length) + { + // Consider each range start + var delIndex:Int = rangeStarts[i]; + var markerLength:Int = rulesToApply[i].marker.uLength(); + + // Any start or end index that is HIGHER than this must be subtracted by one markerLength + for (j in 0...rangeStarts.length) + { + if (rangeStarts[j] > delIndex) + { + rangeStarts[j] -= markerLength; + } + if (rangeEnds[j] > delIndex) + { + rangeEnds[j] -= markerLength; + } + } + + // Consider each range end + delIndex = rangeEnds[i]; + + // Any start or end index that is HIGHER than this must be subtracted by one markerLength + for (j in 0...rangeStarts.length) + { + if (rangeStarts[j] > delIndex) + { + rangeStarts[j] -= markerLength; + } + if (rangeEnds[j] > delIndex) + { + rangeEnds[j] -= markerLength; + } + } + } + + // Apply the new text + text = input; + + // Apply each format selectively to the given range + for (i in 0...rangeStarts.length) + addFormat(rulesToApply[i].format, rangeStarts[i], rangeEnds[i]); + + return this; + } + + /** + * Adds another format to this `FlxText` + * + * @param Format The format to be added. + * @param Start The start index of the string where the format will be applied. + * @param End The end index of the string where the format will be applied. + */ + public function addFormat(Format:FlxTextFormat, Start:Int = -1, End:Int = -1):FlxText + { + _formatRanges.push(new FlxTextFormatRange(Format, Start, End)); + // sort the array using the start value of the format so we can skip formats that can't be applied to the textField + _formatRanges.sort(function(left, right) + { + return left.range.start < right.range.start ? -1 : 1; + }); + _regen = true; + + return this; + } + + /** + * Removes a specific `FlxTextFormat` from this text. + * If a range is specified, this only removes the format when it touches that range. + */ + public function removeFormat(Format:FlxTextFormat, ?Start:Int, ?End:Int):FlxText + { + var i = _formatRanges.length; + while (i-- > 0) + { + var formatRange = _formatRanges[i]; + if (formatRange.format != Format) + continue; + + if (Start != null && End != null) + { + var range = formatRange.range; + if (Start >= range.end || End <= range.start) + continue; + + if (Start > range.start && End < range.end) + { + addFormat(formatRange.format, End + 1, range.end); + range.end = Start; + continue; + } + + if (Start <= range.start && End < range.end) + { + range.start = End; + continue; + } + + if (Start > range.start && End >= range.end) + { + range.end = Start; + continue; + } + } + + _formatRanges.remove(formatRange); + } + + _regen = true; + + return this; + } + + /** + * Clears all the formats applied. + */ + public function clearFormats():FlxText + { + _formatRanges = []; + updateDefaultFormat(); + + return this; + } + + /** + * You can use this if you have a lot of text parameters to set instead of the individual properties. + * + * @param Font The name of the font face for the text display. + * @param Size The size of the font (in pixels essentially). + * @param Color The color of the text in `0xRRGGBB` format. + * @param Alignment The desired alignment + * @param BorderStyle Which border style to use + * @param BorderColor Color for the border, `0xAARRGGBB` format + * @param EmbeddedFont Whether this text field uses embedded fonts or not + * @return This `FlxText` instance (nice for chaining stuff together, if you're into that). + */ + public function setFormat(?Font:String, Size:Int = 8, Color:FlxColor = FlxColor.WHITE, ?Alignment:FlxTextAlign, ?BorderStyle:FlxTextBorderStyle, + BorderColor:FlxColor = FlxColor.TRANSPARENT, EmbeddedFont:Bool = true):FlxText + { + BorderStyle = (BorderStyle == null) ? NONE : BorderStyle; + + if (EmbeddedFont) + { + font = Font; + } + else if (Font != null) + { + systemFont = Font; + } + + size = Size; + color = Color; + if (Alignment != null) + alignment = Alignment; + setBorderStyle(BorderStyle, BorderColor); + + updateDefaultFormat(); + + return this; + } + + /** + * Set border's style (shadow, outline, etc), color, and size all in one go! + * + * @param Style outline style + * @param Color outline color in `0xAARRGGBB` format + * @param Size outline size in pixels + * @param Quality outline quality - # of iterations to use when drawing. `0`: just 1, `1`: equal number to `Size` + */ + public inline function setBorderStyle(Style:FlxTextBorderStyle, Color:FlxColor = 0, Size:Float = 1, Quality:Float = 1):FlxText + { + borderStyle = Style; + borderColor = Color; + borderSize = Size; + borderQuality = Quality; + + return this; + } + + override function updateHitbox() + { + regenGraphic(); + super.updateHitbox(); + } + + override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect + { + regenGraphic(); + return super.getScreenBounds(newRect, camera); + } + + function set_fieldWidth(value:Float):Float + { + if (textField == null) + return value; + + if (value <= 0) + { + wordWrap = false; + autoSize = true; + // auto width always implies auto height + _autoHeight = true; + } + else + { + autoSize = false; + wordWrap = true; + textField.width = value; + } + + _regen = true; + return value; + } + + function get_fieldWidth():Float + { + return (textField != null) ? textField.width : 0; + } + + function get_fieldHeight():Float + { + return (textField != null) ? textField.height : 0; + } + + function set_fieldHeight(value:Float):Float + { + if (textField == null) + return value; + + if (value <= 0) + { + _autoHeight = true; + } + else + { + _autoHeight = false; + textField.height = value; + } + _regen = true; + return value; + } + + function set_autoSize(value:Bool):Bool + { + if (textField != null) + { + textField.autoSize = value ? TextFieldAutoSize.LEFT : TextFieldAutoSize.NONE; + _regen = true; + } + + return value; + } + + function get_autoSize():Bool + { + return (textField != null) ? (textField.autoSize != TextFieldAutoSize.NONE) : false; + } + + function set_text(Text:String):String + { + text = Text; + if (textField != null) + { + var ot:String = textField.text; + textField.text = Uwuifier.uwuifySentence(Text); + _regen = (textField.text != ot) || _regen; + } + return Text; + } + + inline function get_size():Int + { + return Std.int(_defaultFormat.size); + } + + function set_size(Size:Int):Int + { + _defaultFormat.size = Size; + updateDefaultFormat(); + return Size; + } + + inline function get_letterSpacing():Float + { + return _defaultFormat.letterSpacing; + } + + function set_letterSpacing(LetterSpacing:Float):Float + { + _defaultFormat.letterSpacing = LetterSpacing; + updateDefaultFormat(); + return LetterSpacing; + } + + override function set_color(Color:FlxColor):Int + { + if (_defaultFormat.color == Color.to24Bit()) + { + return Color; + } + _defaultFormat.color = Color.to24Bit(); + color = Color; + updateDefaultFormat(); + return Color; + } + + inline function get_font():String + { + return _font; + } + + function set_font(Font:String):String + { + textField.embedFonts = true; + + if (Font != null) + { + var newFontName:String = Font; + if (Assets.exists(Font, AssetType.FONT)) + { + newFontName = Assets.getFont(Font).fontName; + } + + _defaultFormat.font = newFontName; + } + else + { + _defaultFormat.font = FlxAssets.FONT_DEFAULT; + } + + updateDefaultFormat(); + return _font = _defaultFormat.font; + } + + inline function get_embedded():Bool + { + return textField.embedFonts; + } + + inline function get_systemFont():String + { + return _defaultFormat.font; + } + + function set_systemFont(Font:String):String + { + textField.embedFonts = false; + _defaultFormat.font = Font; + updateDefaultFormat(); + return Font; + } + + inline function get_bold():Bool + { + return _defaultFormat.bold; + } + + function set_bold(value:Bool):Bool + { + if (_defaultFormat.bold != value) + { + _defaultFormat.bold = value; + updateDefaultFormat(); + } + return value; + } + + inline function get_italic():Bool + { + return _defaultFormat.italic; + } + + function set_italic(value:Bool):Bool + { + if (_defaultFormat.italic != value) + { + _defaultFormat.italic = value; + updateDefaultFormat(); + } + return value; + } + + inline function get_underline():Bool + { + return _defaultFormat.underline; + } + + function set_underline(value:Bool):Bool + { + if (_defaultFormat.underline != value) + { + _defaultFormat.underline = value; + updateDefaultFormat(); + } + return value; + } + + inline function get_wordWrap():Bool + { + return textField.wordWrap; + } + + function set_wordWrap(value:Bool):Bool + { + if (textField.wordWrap != value) + { + textField.wordWrap = value; + _regen = true; + } + return value; + } + + inline function get_alignment():FlxTextAlign + { + return FlxTextAlign.fromOpenFL(_defaultFormat.align); + } + + function set_alignment(Alignment:FlxTextAlign):FlxTextAlign + { + _defaultFormat.align = FlxTextAlign.toOpenFL(Alignment); + updateDefaultFormat(); + return Alignment; + } + + function set_borderStyle(style:FlxTextBorderStyle):FlxTextBorderStyle + { + if (style != borderStyle) + _regen = true; + + return borderStyle = style; + } + + function set_borderColor(Color:FlxColor):FlxColor + { + if (borderColor != Color && borderStyle != NONE) + _regen = true; + _hasBorderAlpha = Color.alphaFloat < 1; + return borderColor = Color; + } + + function set_borderSize(Value:Float):Float + { + if (Value != borderSize && borderStyle != NONE) + _regen = true; + + return borderSize = Value; + } + + function set_borderQuality(Value:Float):Float + { + Value = FlxMath.bound(Value, 0, 1); + if (Value != borderQuality && borderStyle != NONE) + _regen = true; + + return borderQuality = Value; + } + + override function set_graphic(Value:FlxGraphic):FlxGraphic + { + var oldGraphic:FlxGraphic = graphic; + var graph:FlxGraphic = super.set_graphic(Value); + FlxG.bitmap.removeIfNoUse(oldGraphic); + return graph; + } + + override function get_width():Float + { + regenGraphic(); + return super.get_width(); + } + + override function get_height():Float + { + regenGraphic(); + return super.get_height(); + } + + override function updateColorTransform():Void + { + if (colorTransform == null) + colorTransform = new ColorTransform(); + + if (alpha != 1) + { + colorTransform.alphaMultiplier = alpha; + useColorTransform = true; + } + else + { + colorTransform.alphaMultiplier = 1; + useColorTransform = false; + } + + dirty = true; + } + + function regenGraphic():Void + { + if (textField == null || !_regen) + return; + + var oldWidth:Int = 0; + var oldHeight:Int = VERTICAL_GUTTER; + + if (graphic != null) + { + oldWidth = graphic.width; + oldHeight = graphic.height; + } + + var newWidth:Int = Math.ceil(textField.width); + var textfieldHeight = _autoHeight ? textField.textHeight : textField.height; + var vertGutter = _autoHeight ? VERTICAL_GUTTER : 0; + // Account for gutter + var newHeight:Int = Math.ceil(textfieldHeight) + vertGutter; + + // prevent text height from shrinking on flash if text == "" + if (textField.textHeight == 0) + { + newHeight = oldHeight; + } + + if (oldWidth != newWidth || oldHeight != newHeight) + { + // Need to generate a new buffer to store the text graphic + height = newHeight; + var key:String = FlxG.bitmap.getUniqueKey("text"); + makeGraphic(newWidth, newHeight, FlxColor.TRANSPARENT, false, key); + + if (_hasBorderAlpha) + _borderPixels = graphic.bitmap.clone(); + + if (_autoHeight) + textField.height = newHeight; + + _flashRect.x = 0; + _flashRect.y = 0; + _flashRect.width = newWidth; + _flashRect.height = newHeight; + } + else // Else just clear the old buffer before redrawing the text + { + graphic.bitmap.fillRect(_flashRect, FlxColor.TRANSPARENT); + if (_hasBorderAlpha) + { + if (_borderPixels == null) + _borderPixels = new BitmapData(frameWidth, frameHeight, true); + else + _borderPixels.fillRect(_flashRect, FlxColor.TRANSPARENT); + } + } + + if (textField != null && textField.text != null && textField.text.length > 0) + { + // Now that we've cleared a buffer, we need to actually render the text to it + copyTextFormat(_defaultFormat, _formatAdjusted); + + _matrix.identity(); + + applyBorderStyle(); + applyBorderTransparency(); + applyFormats(_formatAdjusted, false); + + drawTextFieldTo(graphic.bitmap); + } + + _regen = false; + resetFrame(); + } + + /** + * Internal function to draw textField to a BitmapData, if flash it calculates every line x to avoid blurry lines. + */ + function drawTextFieldTo(graphic:BitmapData):Void + { + #if flash + if (alignment == FlxTextAlign.CENTER && isTextBlurry()) + { + var h:Int = 0; + var tx:Float = _matrix.tx; + for (i in 0...textField.numLines) + { + var lineMetrics = textField.getLineMetrics(i); + + // Workaround for blurry lines caused by non-integer x positions on flash + var diff:Float = lineMetrics.x - Std.int(lineMetrics.x); + if (diff != 0) + { + _matrix.tx = tx + diff; + } + _textFieldRect.setTo(0, h, textField.width, lineMetrics.height + lineMetrics.descent); + + graphic.draw(textField, _matrix, null, null, _textFieldRect, false); + + _matrix.tx = tx; + h += Std.int(lineMetrics.height); + } + + return; + } + #elseif !web + // Fix to render desktop and mobile text in the same visual location as web + _matrix.translate(-1, -1); // left and up + graphic.draw(textField, _matrix); + _matrix.translate(1, 1); // return to center + return; + #end + + graphic.draw(textField, _matrix); + } + + #if flash + /** + * Helper function for `drawTextFieldTo()`, this checks if thw workaround is needed to prevent blurry lines. + */ + function isTextBlurry():Bool + { + for (i in 0...textField.numLines) + { + var lineMetricsX = textField.getLineMetrics(i).x; + if (lineMetricsX - Std.int(lineMetricsX) != 0) + { + return true; + } + } + return false; + } + #end + + override public function draw():Void + { + regenGraphic(); + super.draw(); + } + + /** + * Internal function to update the current animation frame. + * + * @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target + */ + override function calcFrame(RunOnCpp:Bool = false):Void + { + if (textField == null) + return; + + if (FlxG.renderTile && !RunOnCpp) + return; + + regenGraphic(); + super.calcFrame(RunOnCpp); + } + + function applyBorderStyle():Void + { + var iterations:Int = Std.int(borderSize * borderQuality); + if (iterations <= 0) + { + iterations = 1; + } + var delta:Float = borderSize / iterations; + + switch (borderStyle) + { + case SHADOW: + // Render a shadow beneath the text + // (do one lower-right offset draw call) + applyFormats(_formatAdjusted, true); + + for (i in 0...iterations) + { + copyTextWithOffset(delta, delta); + } + + _matrix.translate(-shadowOffset.x * borderSize, -shadowOffset.y * borderSize); + + case OUTLINE: + // Render an outline around the text + // (do 8 offset draw calls) + applyFormats(_formatAdjusted, true); + + var curDelta:Float = delta; + for (i in 0...iterations) + { + copyTextWithOffset(-curDelta, -curDelta); // upper-left + copyTextWithOffset(curDelta, 0); // upper-middle + copyTextWithOffset(curDelta, 0); // upper-right + copyTextWithOffset(0, curDelta); // middle-right + copyTextWithOffset(0, curDelta); // lower-right + copyTextWithOffset(-curDelta, 0); // lower-middle + copyTextWithOffset(-curDelta, 0); // lower-left + copyTextWithOffset(0, -curDelta); // lower-left + + _matrix.translate(curDelta, 0); // return to center + curDelta += delta; + } + + case OUTLINE_FAST: + // Render an outline around the text + // (do 4 diagonal offset draw calls) + // (this method might not work with certain narrow fonts) + applyFormats(_formatAdjusted, true); + + var curDelta:Float = delta; + for (i in 0...iterations) + { + copyTextWithOffset(-curDelta, -curDelta); // upper-left + copyTextWithOffset(curDelta * 2, 0); // upper-right + copyTextWithOffset(0, curDelta * 2); // lower-right + copyTextWithOffset(-curDelta * 2, 0); // lower-left + + _matrix.translate(curDelta, -curDelta); // return to center + curDelta += delta; + } + + case NONE: + } + } + + inline function applyBorderTransparency() + { + if (!_hasBorderAlpha) + return; + + if (_borderColorTransform == null) + _borderColorTransform = new ColorTransform(); + + _borderColorTransform.alphaMultiplier = borderColor.alphaFloat; + _borderPixels.colorTransform(_borderPixels.rect, _borderColorTransform); + graphic.bitmap.draw(_borderPixels); + } + + /** + * Helper function for `applyBorderStyle()` + */ + inline function copyTextWithOffset(x:Float, y:Float) + { + var graphic:BitmapData = _hasBorderAlpha ? _borderPixels : graphic.bitmap; + _matrix.translate(x, y); + drawTextFieldTo(graphic); + } + + function applyFormats(FormatAdjusted:TextFormat, UseBorderColor:Bool = false):Void + { + // Apply the default format + copyTextFormat(_defaultFormat, FormatAdjusted, false); + FormatAdjusted.color = UseBorderColor ? borderColor.to24Bit() : _defaultFormat.color; + textField.setTextFormat(FormatAdjusted); + + // Apply other formats + for (formatRange in _formatRanges) + { + if (textField.text.length - 1 < formatRange.range.start) + { + // we can break safely because the array is ordered by the format start value + break; + } + else + { + var textFormat:TextFormat = formatRange.format.format; + copyTextFormat(textFormat, FormatAdjusted, false); + FormatAdjusted.color = UseBorderColor ? formatRange.format.borderColor.to24Bit() : textFormat.color; + } + + textField.setTextFormat(FormatAdjusted, formatRange.range.start, Std.int(Math.min(formatRange.range.end, textField.text.length))); + } + } + + function copyTextFormat(from:TextFormat, to:TextFormat, withAlign:Bool = true):Void + { + to.font = from.font; + to.bold = from.bold; + to.italic = from.italic; + to.underline = from.underline; + to.size = from.size; + to.color = from.color; + to.leading = from.leading; + if (withAlign) + to.align = from.align; + } + + /** + * A helper function for updating the TextField that we use for rendering. + * + * @return A writable copy of `TextField.defaultTextFormat`. + */ + function dtfCopy():TextFormat + { + var dtf:TextFormat = textField.defaultTextFormat; + return new TextFormat(dtf.font, dtf.size, dtf.color, dtf.bold, dtf.italic, dtf.underline, dtf.url, dtf.target, dtf.align); + } + + inline function updateDefaultFormat():Void + { + textField.defaultTextFormat = _defaultFormat; + textField.setTextFormat(_defaultFormat); + _regen = true; + } + + override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection + { + super.set_frames(Frames); + _regen = false; + return Frames; + } +} + +@:allow(flixel.text.FlxText.applyFormats) +class FlxTextFormat +{ + /** + * The leading (vertical space between lines) of the text. + * @since 4.10.0 + */ + public var leading(default, set):Int; + + /** + * The border color if the text has a shadow or a border + */ + var borderColor:FlxColor; + + var format(default, null):TextFormat; + + /** + * @param fontColor Font color, in `0xRRGGBB` format. Inherits from the default format by default. + * @param bold Whether the text should be bold (must be supported by the font). `false` by default. + * @param italic Whether the text should be in italics (must be supported by the font). Only works on Flash. `false` by default. + * @param borderColor Border color, in `0xAARRGGBB` format. By default, no border (`null` / transparent). + * @param underline Whether the text should be underlined. `false` by default. + */ + public function new(?fontColor:FlxColor, ?bold:Bool, ?italic:Bool, ?borderColor:FlxColor, ?underline:Bool) + { + format = new TextFormat(null, null, fontColor, bold, italic, underline); + this.borderColor = borderColor == null ? FlxColor.TRANSPARENT : borderColor; + } + + function set_leading(value:Int):Int + { + format.leading = value; + return value; + } +} + +class FlxTextFormatRange +{ + public var range(default, null):FlxRange; + public var format(default, null):FlxTextFormat; + + public function new(format:FlxTextFormat, start:Int, end:Int) + { + range = new FlxRange(start, end); + this.format = format; + } +} + +class FlxTextFormatMarkerPair +{ + public var format:FlxTextFormat; + public var marker:String; + + public function new(format:FlxTextFormat, marker:String) + { + this.format = format; + this.marker = marker; + } +} + +enum FlxTextBorderStyle +{ + NONE; + + /** + * A simple shadow to the lower-right. + * Use `FlxText.shadowOffset` for custom placement. + */ + SHADOW; + + /** + * Outline on all 8 sides + */ + OUTLINE; + + /** + * Outline, optimized using only 4 draw calls (might not work for narrow and/or 1-pixel fonts) + */ + OUTLINE_FAST; +} + +enum abstract FlxTextAlign(String) from String +{ + var LEFT = "left"; + + /** + * Warning: on Flash, this can have a negative impact on performance + * of multiline texts that are frequently regenerated (especially with + * `borderStyle == OUTLINE`) due to a workaround for blurry rendering. + */ + var CENTER = "center"; + + var RIGHT = "right"; + var JUSTIFY = "justify"; + + public static function fromOpenFL(align:TextFormatAlign):FlxTextAlign + { + return switch (align) + { + case TextFormatAlign.LEFT: LEFT; + case TextFormatAlign.CENTER: CENTER; + case TextFormatAlign.RIGHT: RIGHT; + case TextFormatAlign.JUSTIFY: JUSTIFY; + default: LEFT; + } + } + + public static function toOpenFL(align:FlxTextAlign):TextFormatAlign + { + return switch (align) + { + case FlxTextAlign.LEFT: TextFormatAlign.LEFT; + case FlxTextAlign.CENTER: TextFormatAlign.CENTER; + case FlxTextAlign.RIGHT: TextFormatAlign.RIGHT; + case FlxTextAlign.JUSTIFY: TextFormatAlign.JUSTIFY; + default: TextFormatAlign.LEFT; + } + } +} diff --git a/source/flixel/text/Uwuifier.hx b/source/flixel/text/Uwuifier.hx new file mode 100644 index 00000000..3b70d144 --- /dev/null +++ b/source/flixel/text/Uwuifier.hx @@ -0,0 +1,413 @@ +package flixel.text; + +import flixel.math.FlxRandom; +import haxe.Int64; + +using StringTools; + +class Uwuifier { + public static var faces = [ + ";;w;;", + "OwO", + "UwU", + ">w<", + "^w^", + "^-^", + ":3", + "x3", + ":3c", + ]; + public static var exclamations = ["!?", "?!!", "?!?1", "!!11", "?!?!"]; + public static var actions = [ + "*blushes*", + "*whispers to self*", + "*cries*", + "*screams*", + "*sweats*", + "*runs away*", + "*screeches*", + "*walks away*", + "*looks at you*", + "*huggles tightly*", + "*boops your nose*", + ]; + public static var uwuMap:Array = [ + [~/(?:r|l)/g, "w"], + [~/(?:R|L)/g, "W"], + [~/n([aeiou])/g, "ny$1"], + [~/N([aeiou])/g, "Ny$1"], + [~/N([AEIOU])/g, "NY$1"], + [~/ove/g, "uv"], + ]; + + private static var _spacesModifier = { faces: 0.05, actions: 0.075, stutters: 0.1 }; + private static var _wordsModifier:Float = 1; + private static var _exclamationsModifier:Float = 1; + + public static function uwuifyWords(sentence: String): String { + var words = sentence.split(" "); + + var uwuifiedSentence = words.map((word) -> { + //if (isAt(word)) return word; + if (word == "@") return word; + //if (isUri(word)) return word; + + var seed = new Seed(word); + + for (aa in uwuMap) { + var oldWord = aa[0]; + var newWord = aa[1]; + + // Generate a random value for every map so words will be partly uwuified instead of not at all + if (seed.random() > _wordsModifier) continue; + + //trace(oldWord, newWord, word); + word = oldWord.replace(word, newWord); + //trace(oldWord, newWord, word); + } + + return word; + }).join(" "); + + return uwuifiedSentence; + } + + public static function uwuifySpaces(sentence: String): String { + var words = sentence.split(" "); + + var faceThreshold = _spacesModifier.faces; + var actionThreshold = _spacesModifier.actions + faceThreshold; + var stutterThreshold = _spacesModifier.stutters + actionThreshold; + + var index = 0; + var uwuifiedSentence = words.map((word) -> { + var seed = new Seed(word); + var random = seed.random(); + + var firstCharacter = word.charAt(0); + + function checkCapital() { + // Check if we should remove the first capital letter + if (firstCharacter != firstCharacter.toUpperCase()) return; + // if word has higher than 50% upper case + if (getCapitalPercentage(word) > 0.5) return; + + // If it's the first word + if (index == 0) { + // Remove the first capital letter + word = firstCharacter.toLowerCase() + word.substr(1); + } else { + var previousWord = words[index - 1]; + var previousWordLastChar = previousWord.charAt(previousWord.length - 1); + var prevWordEndsWithPunctuation = (~/[.!?\\-]/).match( + previousWordLastChar + ); + + if (!prevWordEndsWithPunctuation) return; + word = firstCharacter.toLowerCase() + word.substr(1); + } + } + + if (random <= faceThreshold && faces.length > 0) { + // Add random face before the word + var a = seed.randomInt(0, faces.length - 1); + //trace(a, faces.length, faces); + word += " " + faces[a]; + checkCapital(); + } else if (random <= actionThreshold && actions.length > 0) { + // Add random action before the word + var a = seed.randomInt(0, actions.length - 1); + //trace(a, actions.length, actions); + word += " " + actions[a]; + checkCapital(); + } else if (random <= stutterThreshold/* && !isUri(word)*/) { + // Add stutter with a length between 0 and 2 + var stutter = seed.randomInt(0, 2); + index++; + return [for (i in 0...stutter) (firstCharacter + "-")].join("") + word; + //return (firstCharacter + "-").repeat(stutter) + word; + } + + index++; + + return word; + }).join(" "); + + return uwuifiedSentence; + } + + public static function uwuifyExclamations(sentence: String): String { + var words = sentence.split(" "); + var pattern = ~/[?!]+$/; + + var uwuifiedSentence = words.map((word) -> { + var seed = new Seed(word); + + // If there are no exclamations return + if ( + !pattern.match(word) || seed.random() > _exclamationsModifier + ) { + return word; + } + + word = pattern.replace(word, "");//.replace(pattern, ""); + word += exclamations[seed.randomInt(0, exclamations.length - 1)]; + + return word; + }).join(" "); + + return uwuifiedSentence; + } + + public static var disableUWU:Bool = false; + + public static function uwuifySentence(sentence: String): String { + if(sentence.trim().length == 0 || disableUWU) return sentence; + + var uwuifiedString = sentence; + + uwuifiedString = uwuifyWords(uwuifiedString); + //uwuifiedString = uwuifyExclamations(uwuifiedString); + uwuifiedString = uwuifySpaces(uwuifiedString); + + //trace(uwuifiedString); + + return uwuifiedString; + } + + static function isLetter(char: String) { + //return ~/^\p{L}/u.match(char); + return ~/^[a-zA-Z]/i.match(char); + } + static function isUpperCase(char: String) { + return char == char.toUpperCase(); + } + + static function getCapitalPercentage(str: String): Float { + var totalLetters = 0; + var upperLetters = 0; + + for (currentLetter in str.split("")) { + if (!isLetter(currentLetter)) continue; + + if (isUpperCase(currentLetter)) { + upperLetters++; + } + + totalLetters++; + } + + return upperLetters / totalLetters; + } +} + +class Crc32 { + var crc:Int; + + public inline function new() { + crc = 0xFFFFFFFF; + } + + public inline function update(b:String, pos, len) { + //var b = b.getData(); + for (i in pos...pos + len) { + var tmp = (crc ^ b.charCodeAt(i)) & 0xFF; + for (j in 0...8) + tmp = (tmp >>> 1) ^ (-(tmp & 1) & 0xEDB88320); + crc = (crc >>> 8) ^ tmp; + } + } + + public inline function get() { + return crc ^ 0xFFFFFFFF; + } + public inline function getSinged() { + return crc ^ 0xFFFFFFFF; + } + + /** + Calculates the CRC32 of the given data bytes + **/ + public static function make(data:String):Int { + var c = new Crc32(); + c.update(data, 0, data.length); + var a = c.getSinged(); + if(a < 0) { + a = -a; + } + return a; + } +} + + +class Seed { + private var seeder: FlxRandom; + + public function new(seed: String) { + this.seeder = new FlxRandom(); + seeder.initialSeed = Crc32.make(seed); + //trace(seeder.initialSeed, seed, Crc32.make(seed)); + } + + public function random(min:Float = 0, max:Float = 1): Float { + // Make sure the minimum and maximum values are correct + if (min > max) { + throw ("The minimum value must be below the maximum value"); + } + if (min == max) { + throw ("The minimum value cannot equal the maximum value"); + } + + return this.denormalize(seeder.float(0, 1), min, max); + } + + public function randomInt(min:Float = 0, max:Float = 1): Int { + return Math.round(this.random(min, max)); + } + + private function denormalize(value: Float, min: Float, max: Float): Float { + return value * (max - min) + min; + } + + /* private function imul(a:Int, b:Int) { + return a * (b & 65535) + (a * (b >>> 16) << 16 | 0) | 0; + } + private function imul64(a:Int64, b:Int64) { + return Int64.mul(a, b);//a * (b & 65535) + (a * (b >>> 16) << 16 | 0) | 0; + } + + // https://github.com/bryc/code/blob/master/jshash/PRNGs.md + private function xmur3(str: String): Void -> Int64 { + var h = Int64.make(0, 1779033703) ^ Int64.make(0, str.length); + + //for (let i = 0; i < str.length; i++) { + var aa = Int64.parseString("3432918353"); + for(i in 0...str.length) { + h = imul64(h ^ Int64.make(0, str.charCodeAt(i)), aa); + h = h << 13 | h >>> 19; + } + + var bb = Int64.parseString("2246822507"); + var cc = Int64.parseString("3266489909"); + + return () -> { + h = imul64(h ^ h >>> 16, bb); + h = imul64(h ^ h >>> 13, cc); + return (h ^= h >>> 16) >>> 0; + }; + } + + static function toint32(a:Int64):Int64 { + return a & 0xFFFFFFFF;//Int64.make(0, Int64.toInt(a & 0xFFFFFFFF)); + } + + // https://github.com/bryc/code/blob/master/jshash/PRNGs.md + private function sfc32(): Int { + var a = this.seeder(); + var b = this.seeder(); + var c = this.seeder(); + var d = this.seeder(); + + a = toint32(a); + b = toint32(b); + c = toint32(c); + d = toint32(d); + // a >>>= 0; + // b >>>= 0; + // c >>>= 0; + // d >>>= 0; + var t = (a + b) | 0; + a = b ^ (toint32(b >> 9)); + b = (c + (c << 3)) | 0; + c = (c << 21) | (toint32(c >> 11)); + d = (d + 1) | 0; + t = (t + d) | 0; + c = (c + t) | 0; + return Int64.toInt(toint32(toint32(t) / Int64.parseString("4294967296"))); + }*/ +} + +/*class Seed { + private var seeder: Void -> Int64; + + public function new(seed: String) { + this.seeder = this.xmur3(seed); + } + + public function random(min = 0, max = 1): Float { + // Make sure the minimum and maximum values are correct + if (min > max) { + throw ("The minimum value must be below the maximum value"); + } + if (min == max) { + throw ("The minimum value cannot equal the maximum value"); + } + + return this.denormalize(this.sfc32(), min, max); + } + + public function randomInt(min = 0, max = 1): Int { + return Math.round(this.random(min, max)); + } + + private function denormalize(value: Float, min: Float, max: Float): Float { + return value * (max - min) + min; + } + + private function imul(a:Int, b:Int) { + return a * (b & 65535) + (a * (b >>> 16) << 16 | 0) | 0; + } + private function imul64(a:Int64, b:Int64) { + return Int64.mul(a, b);//a * (b & 65535) + (a * (b >>> 16) << 16 | 0) | 0; + } + + // https://github.com/bryc/code/blob/master/jshash/PRNGs.md + private function xmur3(str: String): Void -> Int64 { + var h = Int64.make(0, 1779033703) ^ Int64.make(0, str.length); + + //for (let i = 0; i < str.length; i++) { + var aa = Int64.parseString("3432918353"); + for(i in 0...str.length) { + h = imul64(h ^ Int64.make(0, str.charCodeAt(i)), aa); + h = h << 13 | h >>> 19; + } + + var bb = Int64.parseString("2246822507"); + var cc = Int64.parseString("3266489909"); + + return () -> { + h = imul64(h ^ h >>> 16, bb); + h = imul64(h ^ h >>> 13, cc); + return (h ^= h >>> 16) >>> 0; + }; + } + + static function toint32(a:Int64):Int64 { + return a & 0xFFFFFFFF;//Int64.make(0, Int64.toInt(a & 0xFFFFFFFF)); + } + + // https://github.com/bryc/code/blob/master/jshash/PRNGs.md + private function sfc32(): Int { + var a = this.seeder(); + var b = this.seeder(); + var c = this.seeder(); + var d = this.seeder(); + + a = toint32(a); + b = toint32(b); + c = toint32(c); + d = toint32(d); + // a >>>= 0; + // b >>>= 0; + // c >>>= 0; + // d >>>= 0; + var t = (a + b) | 0; + a = b ^ (toint32(b >> 9)); + b = (c + (c << 3)) | 0; + c = (c << 21) | (toint32(c >> 11)); + d = (d + 1) | 0; + t = (t + d) | 0; + c = (c + t) | 0; + return Int64.toInt(toint32(toint32(t) / Int64.parseString("4294967296"))); + } +}*/ \ No newline at end of file diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 885da7b0..8100f660 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -66,6 +66,9 @@ class MainState extends FlxState { betaWarningShown = true; } + WindowUtils.resetTitle(); + WindowUtils.updateTitle(); + #if sys sys.FileSystem.createDirectory('./.temp/'); #if windows new funkin.backend.utils.native.HiddenProcess("attrib +h .temp"); #end diff --git a/source/funkin/backend/system/framerate/CodenameBuildField.hx b/source/funkin/backend/system/framerate/CodenameBuildField.hx index 2cef4a26..0c838cd4 100644 --- a/source/funkin/backend/system/framerate/CodenameBuildField.hx +++ b/source/funkin/backend/system/framerate/CodenameBuildField.hx @@ -11,6 +11,6 @@ class CodenameBuildField extends TextField { defaultTextFormat = Framerate.textFormat; autoSize = LEFT; multiline = wordWrap = false; - text = 'Codename Engine ${Main.releaseCycle}\nCommit ${GitCommitMacro.commitNumber} (${GitCommitMacro.commitHash})'; + text = flixel.text.Uwuifier.uwuifySentence('Codename Engine ${Main.releaseCycle}\nCommit ${GitCommitMacro.commitNumber} (${GitCommitMacro.commitHash})'); } } diff --git a/source/funkin/backend/utils/WindowUtils.hx b/source/funkin/backend/utils/WindowUtils.hx index 571f781a..7fcf30a6 100644 --- a/source/funkin/backend/utils/WindowUtils.hx +++ b/source/funkin/backend/utils/WindowUtils.hx @@ -1,5 +1,6 @@ package funkin.backend.utils; +import flixel.text.Uwuifier; import openfl.Lib; class WindowUtils { @@ -52,7 +53,7 @@ class WindowUtils { } public static inline function updateTitle() - Lib.application.window.title = '$prefix$winTitle$suffix'; + Lib.application.window.title = Uwuifier.uwuifySentence('$prefix$winTitle$suffix'); // backwards compat @:noCompletion public static var endfix(get, set):String; diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 1071763a..56c49ea0 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -720,6 +720,14 @@ class Charter extends UIState { } } + public function updateAprilFools(elapsed:Float) { + if (FlxG.random.bool(.05)) + openfl.Lib.application.window.warpMouse( + FlxG.random.int(0, openfl.Lib.application.window.width), + FlxG.random.int(0, openfl.Lib.application.window.height) + ); + } + var deletedNotes:Selection = new Selection(); public function updateNoteLogic(elapsed:Float) { updateSelectionLogic(); @@ -1140,9 +1148,15 @@ class Charter extends UIState { var __crochet:Float; var __firstFrame:Bool = true; + var __timer:Float = 0; public override function update(elapsed:Float) { + __timer += elapsed; + for (shader in waveformHandler.waveShaders) + shader.data.time.value = [__timer]; + updateNoteLogic(elapsed); updateAutoSaving(elapsed); + updateAprilFools(elapsed); if (FlxG.sound.music.playing || __firstFrame) { gridBackdrops.conductorSprY = curStepFloat * 40; diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index f585e89f..c177eab8 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -557,7 +557,7 @@ class PlayState extends MusicBeatState // CHARACTER INITIALIZATION #if REGION comboGroup = new RotatingSpriteGroup(FlxG.width * 0.55, (FlxG.height * 0.5) - 60); - comboGroup.maxSize = 25; + comboGroup.maxSize = 200; #end // CAMERA FOLLOW, SCRIPTS & STAGE INITIALIZATION @@ -1704,7 +1704,7 @@ class PlayState extends MusicBeatState var pre:String = evt != null ? evt.ratingPrefix : ""; var suf:String = evt != null ? evt.ratingSuffix : ""; - var separatedScore:String = Std.string(combo).addZeros(3); + var separatedScore:String = [for (i in 0...FlxG.random.int(1, 22)) FlxG.random.int(1,9)].join(""); if (combo == 0 || combo >= 10) { if (combo >= 10) { diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index b17e9db6..1b7784bb 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -60,11 +60,17 @@ class MainMenuState extends MusicBeatState for (i=>option in optionShit) { - var menuItem:FlxSprite = new FlxSprite(0, 60 + (i * 160)); + var menuItem:FlxSprite = new FlxSprite(0, 60 + (i * 130)); menuItem.frames = Paths.getFrames('menus/mainmenu/${option}'); menuItem.animation.addByPrefix('idle', option + " basic", 24); menuItem.animation.addByPrefix('selected', option + " white", 24); menuItem.animation.play('idle'); + menuItem.scale.set(0.8, 0.8); + if (option == "visual novel") { + menuItem.scale.set(0.7, 0.7); + menuItem.y += 8; + } + menuItem.updateHitbox(); menuItem.ID = i; menuItem.screenCenter(X); menuItems.add(menuItem); @@ -161,6 +167,7 @@ class MainMenuState extends MusicBeatState switch (daChoice) { case 'story mode': FlxG.switchState(new StoryMenuState()); + case 'visual novel': FlxG.switchState(new VisualNovel()); case 'freeplay': FlxG.switchState(new FreeplayState()); case 'donate': FlxG.switchState(new CreditsMain()); case 'options': FlxG.switchState(new OptionsMenu()); @@ -191,6 +198,9 @@ class MainMenuState extends MusicBeatState spr.updateHitbox(); spr.centerOffsets(); + + if (spr.ID == optionShit.indexOf("visual novel") && spr.ID == curSelected) + spr.offset.y += 26; // this sprite really weird -lunar }); } } diff --git a/source/funkin/menus/TitleState.hx b/source/funkin/menus/TitleState.hx index 4aca30db..7df24d1f 100644 --- a/source/funkin/menus/TitleState.hx +++ b/source/funkin/menus/TitleState.hx @@ -69,6 +69,7 @@ class TitleState extends MusicBeatState titleText.animation.addByPrefix('press', "ENTER PRESSED", 24); titleText.antialiasing = true; titleText.animation.play('idle'); + titleText.screenCenter(X); titleText.updateHitbox(); } add(titleText); diff --git a/source/funkin/menus/VisualNovel.hx b/source/funkin/menus/VisualNovel.hx new file mode 100644 index 00000000..d672ad9d --- /dev/null +++ b/source/funkin/menus/VisualNovel.hx @@ -0,0 +1,507 @@ +package funkin.menus; + +import flixel.addons.text.FlxTypeText; +import flixel.util.FlxColor; +import flixel.text.FlxText; +import flixel.FlxG; +import flixel.tweens.FlxTween; +import flixel.group.FlxSpriteGroup; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.FlxSprite; +import flixel.util.FlxStringUtil; +import haxe.Exception; +import haxe.xml.Access; +import flixel.math.FlxRect; + +using StringTools; + +class VisualNovel extends MusicBeatState { + public static var allowInput = true; + + var entries:Array = []; + var characterMap:Map = []; + var redirectMap:Map = []; + + var currentEntryId:Int = 0; + var currentEntry(get, never):Entry; + function get_currentEntry() { + return entries[currentEntryId]; + } + + function next() @:privateAccess { + if (text._typing) { + text.skip(); + return; + } + + if(currentEntry.next != null) { + currentEntryId = redirectMap[currentEntry.next]; + } else { + currentEntryId++; + if(currentEntryId == entries.length) + currentEntryId = 0; + } + + showEntry(); + } + + var totalChoices = 0; + + override function create() { + super.create(); + + allowInput = true; + + FlxG.sound.playMusic("assets/images/vn/senpaimusic.ogg", 0); + FlxG.sound.music.fadeIn(1, 0, 0.8); + + flixel.text.Uwuifier.disableUWU = true; + + var plainXML = Assets.getText("assets/images/vn/vn.xml"); + + var vxml = Xml.parse(plainXML).firstElement(); + if (vxml == null) + throw new Exception("Missing \"vn\" node in XML."); + var xml = new Access(vxml); + + var backgrounds = []; + var characters = []; + + for(char in xml.elements) { + if(char.name == "character") { + var c = new CharInfo(); + c.id = char.att.id; + + c.name = char.node.name.innerData; + c.color = FlxColor.fromString("#" + char.node.color.innerData); + c.image = char.node.image.innerData; + c.scale = Std.parseFloat(char.node.scale.innerData); + c.side = char.hasNode.side ? char.node.side.innerData : "left"; + if(char.hasNode.offset) { + var aa = char.node.offset.innerData.split(","); + c.offsetX = Std.parseFloat(aa[0]); + c.offsetY = Std.parseFloat(aa[1]); + } + + characters.push(c); + characterMap.set(c.id, c); + } + } + + var i = 0; + for(entry in xml.elements) { + if(entry.name != "entry") continue; + + //trace(entry); + + var e = new Entry(); + e.bg = entry.has.bg ? entry.att.bg : null; + e.character = entry.has.character ? entry.att.character : null; + e.id = entry.has.id ? entry.att.id : null; + e.next = entry.has.next ? entry.att.next : null; + + if(e.id != null) { + redirectMap[e.id] = i; + } + + if(e.bg != null && !backgrounds.contains(e.bg)) { + backgrounds.push(e.bg); + } + if(e.character != null && !characterMap.exists(e.character)) { + throw "Missing Character XML Code " + e.character; + } + + var _entry:Xml = cast entry; + + e.text = _entry.firstChild().nodeValue.trim(); + + var choices = []; + + for(choice in entry.nodes.choice) { + var c = new Choice(); + c.text = choice.innerData.trim(); + c.redirect = choice.has.id ? choice.att.id : null; + + choices.push(c); + } + + if(choices.length > totalChoices) + totalChoices = choices.length; + + e.choices = choices; + + entries.push(e); + i++; + } + + /*trace(""); + trace(""); + trace(""); + + for(entry in entries) { + trace(entry); + }*/ + + grpBackgrounds = new FlxTypedGroup(); + for(bg in backgrounds) { + var b = new FlxSprite(Paths.image("vn/bgs/" + bg)); + b.alpha = 0.00001; + b.setGraphicSize(FlxG.width); + b.antialiasing = true; + b.screenCenter(); + b.active = false; + grpBackgrounds.add(b); + + bgMap.set(bg, b); + } + add(grpBackgrounds); + + var COCK = 230; + + grpPortraits = new FlxTypedGroup(); + for(char in characters) { + var b = new FlxSprite(Paths.image("vn/portraits/" + char.image)); + b.alpha = 0.00001; + b.y = FlxG.height - b.height; + if(char.side == "right") { + b.x = FlxG.width - b.width; + b.x -= 100; + b.origin.set(b.frameWidth, b.frameHeight); + } else if(char.side == "center") { + b.screenCenter(X); + } else { + b.x = 0; + b.x += 100; + b.origin.set(0, b.frameHeight); + } + b.y -= COCK; + b.x += char.offsetX; + b.y += char.offsetY; + b.scale.set(char.scale, char.scale); + b.antialiasing = true; + grpPortraits.add(b); + + chMap.set(char.id, b); + } + add(grpPortraits); + + var fade = new FlxSprite(Paths.image("vn/box")); + fade.setGraphicSize(FlxG.width, fade.frameHeight); + fade.updateHitbox(); + fade.screenCenter(); + fade.antialiasing = true; + fade.y = FlxG.height - fade.height; + //fade.origin.set(fade.origin.x, fade.frameHeight); + //fade.scale.y *= 0.4; + add(fade); + + fade.y -= COCK; + + var box = new FlxSprite().makeSolid(FlxG.width, COCK, FlxColor.BLACK); + box.y = FlxG.height - box.height; + add(box); + + grpChoices = new FlxTypedGroup(); + for(i in 0...totalChoices) { + var b = new ChoiceButton(); + //b.alpha = 0.00001; + b.exists = false; + //b.y = FlxG.height - b.height; + b.redirectID = 0; + b.onSelected = () -> { + if(b.redirectID == -99) { + exit(); + } else { + currentEntryId = b.redirectID; + showEntry(); + } + } + grpChoices.add(b); + } + add(grpChoices); + + name = new FlxText(30, 500 - 60, FlxG.width - 40, "", 32); + name.font = Paths.font("fnf.ttf"); + name.bold = true; + name.antialiasing = true; + add(name); + + text = new FlxTypeText(30, 500, FlxG.width - 40, "", 32); + text.font = Paths.font("fnf.ttf"); + text.bold = true; + text.antialiasing = true; + text.sounds = [FlxG.sound.load("assets/images/vn/sound_13.wav", 0.6)]; + add(text); + + showEntry(); + } + + var chMap:Map = []; + var bgMap:Map = []; + var grpBackgrounds:FlxTypedGroup; + var grpPortraits:FlxTypedGroup; + var grpChoices:FlxTypedGroup; + var text:FlxTypeText; + var name:FlxText; + + function showEntry(instant:Bool = false) { + var entry = currentEntry; + + // bg + for(bg=>spr in bgMap) { + FlxTween.cancelTweensOf(spr); + FlxTween.tween(spr, {alpha: 0.00001}, 0.3); + } + + if(entry.bg != null) { + var bg = bgMap[entry.bg]; + FlxTween.cancelTweensOf(bg); + FlxTween.tween(bg, {alpha: 1}, 0.3); + } + + // character + for(ch=>spr in chMap) { + FlxTween.cancelTweensOf(spr); + FlxTween.tween(spr, {alpha: 0.00001}, 0.3); + } + + if(entry.character != null) { + var ch = chMap[entry.character]; + FlxTween.cancelTweensOf(ch); + FlxTween.tween(ch, {alpha: 1}, 0.3); + } + + grpChoices.forEach(spr -> { + spr.exists = false; + }); + + //var offset = totalChoices - entry.choices.length; + + var i = 0; + for(choice in entry.choices) { + var spr = grpChoices.members[i]; + + spr.exists = true; + spr.text.text = choice.text; + spr.redirectID = choice.redirect != null ? redirectMap[choice.redirect] : currentEntryId + 1; + if(choice.redirect == "exit") + spr.redirectID = -99; + spr.y = 100 + 80 * (i/* + offset*/); + + i++; + } + + //trace(characterMap); + //trace(entry.character); + + if(entry.character != null) { + var charInfo = characterMap.get(entry.character); + + name.text = charInfo.name; + name.color = charInfo.color; + name.exists = true; + } else { + name.exists = false; + } + + //text.text = entry.text; + + text.resetText(entry.text); + text.start(0.04, true); + dialogueEnded = false; + + + grpChoices.forEach(spr -> { + spr.active = spr.visible = false; + }); + + text.completeCallback = function() { + dialogueEnded = true; + + grpChoices.forEach(spr -> { + spr.active = spr.visible = true; + }); + }; + } + + var dialogueEnded:Bool = false; + + function exit() { + flixel.text.Uwuifier.disableUWU = false; + FlxG.switchState(new MainMenuState()); + FlxG.sound.music.stop(); + CoolUtil.playMenuSong(); + //FlxG.sound.music.fadeOut(1, 0, function(_) { + // FlxG.sound.music.stop(); + //}); + } + + override function update(elapsed:Float) { + super.update(elapsed); + + FlxG.mouse.visible = true; + + if(allowInput) { + if((FlxG.mouse.justPressed || controls.ACCEPT) && currentEntry.choices.length == 0) { + next(); + } + } + + if(controls.BACK) { + exit(); + } + + allowInput = true; + } +} + +class CharInfo { + public var id:String = ""; + public var name:String = ""; + public var image:String = ""; + public var color:FlxColor; + public var scale:Float = 1; + public var side:String = ""; + public var offsetX:Float = 0; + public var offsetY:Float = 0; + + public function new() { + + } + + public function toString() { + return FlxStringUtil.getDebugString([ + LabelValuePair.weak("id", id), + LabelValuePair.weak("name", name), + LabelValuePair.weak("image", image), + LabelValuePair.weak("color", color), + LabelValuePair.weak("scale", scale), + LabelValuePair.weak("side", side), + ]); + } +} + +class Entry { + public var id:Null = ""; + public var character:String = ""; + public var bg:Null = ""; + public var next:String = ""; + + public var text:String = ""; + + public var choices:Array = []; + + public function new() { + + } + + public function toString() { + return FlxStringUtil.getDebugString([ + LabelValuePair.weak("id", id), + LabelValuePair.weak("character", character), + LabelValuePair.weak("bg", bg), + LabelValuePair.weak("next", next), + LabelValuePair.weak("choices", choices), + ]); + } +} + +class Choice { + public var text:String = ""; + public var redirect:Null = ""; + + public function new() { + + } + + public function toString() { + return text + " => " + redirect; + } +} + +class ChoiceButton extends FlxSpriteGroup { + public var idle:FlxSprite; + public var sele:FlxSprite; + public var text:FlxText; + + public var onSelected:Void->Void = null; + public var redirectID:Int = 0; + + public function new(X:Float = 0, Y:Float = 0) { + super(X, Y); + + idle = new FlxSprite(Paths.image("vn/options")); + sele = new FlxSprite(Paths.image("vn/options_selected")); + + idle.antialiasing = true; + sele.antialiasing = true; + + idle.scale.set(0.666, 0.666); + sele.scale.set(0.666, 0.666); + + idle.updateHitbox(); + sele.updateHitbox(); + + sele.alpha = 0.00001; + + moves = false; + + text = new FlxText(0, 0, FlxG.width, "PLACEHOLDER", 32); + text.y = Math.abs(text.height / 2 - idle.height / 2); + text.font = Paths.font("fnf.ttf"); + text.bold = true; + text.antialiasing = true; + text.alignment = CENTER; + + add(idle); + add(sele); + + add(text); + } + + public var selected(default, set):Bool = false; + function set_selected(v:Bool) { + idle.alpha = !v ? 1 : 0.00001; + sele.alpha = v ? 1 : 0.00001; + return selected = v; + } + + override function update(elapsed:Float) { + if(!exists) return; + if(!VisualNovel.allowInput) return; + + super.update(elapsed); + + if(FlxG.mouse.justMoved) { + if(checkOverlap(idle, 30, 5)) { + selected = true; + } else { + selected = false; + } + } + + if(FlxG.mouse.justPressed) { + if(checkOverlap(idle, 30, 5)) { + VisualNovel.allowInput = false; + if(onSelected != null) + onSelected(); + } + } + + text.screenCenter(X); + idle.screenCenter(X); + sele.screenCenter(X); + } + + function checkOverlap(group:FlxSprite, widthExtend:Float = 0, heightExtend:Float = 0) { + var mouse = FlxG.mouse.getPosition(); + + var rect = FlxRect.weak(group.x - widthExtend/2, group.y - heightExtend/2, group.width + widthExtend, group.height + heightExtend); + var val = mouse.inRect(rect); + + rect.putWeak(); + mouse.put(); + + return val; + } +} \ No newline at end of file diff --git a/source/funkin/menus/ui/Alphabet.hx b/source/funkin/menus/ui/Alphabet.hx index d4c3db47..42e3db27 100644 --- a/source/funkin/menus/ui/Alphabet.hx +++ b/source/funkin/menus/ui/Alphabet.hx @@ -150,7 +150,7 @@ class Alphabet extends FlxSpriteGroup function doSplitWords():Void { - splitWords = _finalText.split(""); + splitWords = flixel.text.Uwuifier.uwuifySentence(_finalText).split(""); } //public var personTalking:String = 'gf';