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';