ソースを参照

First shippable version?

Eiyeron Fulmincendii 7 年 前
コミット
86ed2bf97c

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+export/
+.vscode/

+ 119 - 0
README.md

@@ -0,0 +1,119 @@
+# A Haxeflixel unnamed textbox library
+
+![Demo](readme_files/demo.gif)
+
+## Features
+- "Character-per-character"-style textbox for [Haxeflixel](https://haxeflixel.com/).
+- Correct word-wrapping : a being written word won't jump from a line to another if it's too big for the current line
+- Per-character effects : allows you to have dynamic text.
+- Extendable and customizable : There is a way to add new effects to your textbox, change the font, color, size, etc...
+
+
+## Usage
+
+### Installation
+Include the library's root folder as classpath in your project node. Something like (as 2018-04-16)
+```xml
+<project>
+  <!-- Here goes some of your configuration -->
+  <classpath name="path to the library's folder." />
+  <!-- Here goes some of your configuration -->
+```
+
+Now you should be ready and able to require classes such as `textbox.Textbox`.
+
+### Textbox functions
+- `new (Float, Float, Settings)` : Creates a textbox where top-left corner is placed in (X,Y). Additional settings are stored in the given Settings object.
+- `bring ()` : activates the textbox and its process, making it appear in-game.
+- `dismiss ()` : deactivates the textbox and makes it disappear in-game
+- `setText (String)` : parses the string's content and prepares itself for the next use.
+- `continueWriting ()` : if the textbox is full, this function acknowledges it and asks the textbox to continue writing text (by moving up the text and writing in the newly empty last line)
+
+### Callbacks
+**NOTE** : This is under WIP as it might change between versions.
+
+#### Why callbacks?
+Callbacks are useful as they allow you to extend the textbox without touching its logic. It allowed me to extract the textbox's logic from my now-dead project and make it easy to add features over it, features like effects, triggering a sound per character added to the box or change how it deals with the siutation when the textbox is full.
+
+A few callback ideas (chaining boxes, sound-per-character, text character) are shown in the given sample project.
+
+- `statusChangeCallback (Status)` : if the textbox's state changes, this callback is called. Here a list of the expected behavior to be notifed of:
+  + `FULL` : the textbox is full. Coupled with `continueWriting` you can make stuff like waiting a button press to resume writing.
+  + `DONE` : the textbox finished writing its content. You can `dismiss` it or set it with new text.
+- `characterDisplayCallback (textbox.Text)` : This callback is added each time a character is added to the box. Use this if you need features like an audio sample played for every character, a text cursor following the input, etc etc...
+
+### Settings object
+typedef Settings = {
+    font:String,
+    fontSize: Int,
+    textFieldWidth:Float,
+    color: FlxColor,
+    ?numLines:Int, // Default is currently 3
+    ?charactersPerSecond:Float, // Default is currently 24
+};
+
+## Text effects
+This textbox allows for per-character effects such as (but not limited to) coloring text or making an animated rainbow. Those effects are enabled and disabled by in-text code sequences that are small and human-writable.
+
+## Code sequences and effects.
+
+### Usage example
+
+```haxe
+var textbox:Textbox = new Textbox(...);
+textbox`.setText("
+I'm enabling effect n°00 with arguments (0x00, 0x00, 0x00) : @001000000
+I'm disabling effect n°05  @050
+I'm enabling multiple effects : @0010A0C0E@0510DCCDE...
+It even works inside wo@001000000rds, even if it's a bit unreadable...
+");
+```
+
+### Enabling an effect
+```
+    @MM1AABBCC
+    ▲│  │ │ │
+    └┼Sequence start ()
+     │  │ │ │
+     └ Effect n° 0xMM
+        │ │ │
+        │ │ │
+        └ Argument 1 : 0xAA
+          │ │
+          └ Argument 2 : 0xAA
+            │
+            └ Argument 3 : 0xAA
+```
+
+### Disabling an effect
+
+```
+    @MM0
+    ▲│
+    └┼Sequence start ()
+     │
+     └ Effect n° 0xMM
+```
+
+Note : The `0` or `1` between the effect index and the first argument indicates the textbox parser to disable or enable said effect
+
+### Create new effects
+**NOTE** : This is under WIP as it might change between versions.
+
+To add an effect to the effect list, you have to create a class implementing `IEffect` and add it to `TextEffectArray`'s `effectClasses` variables. Two effects currently are already implemented : coloring some text and an animated rainbow effect. The effect's position index in the array will be it's code sequence's ID.
+
+#### Effect interface
+- `reset(Int, Int, Int, nthCharacter:Int):Void` : called when the effect is enabled on a character. (It's named reset as an effect can be set multiple times.)
+- `update(Float)` : the good old classic update function, called by the textbox on a `FlxState.update()` tick or manual update.
+- `apply(Float)` : called on a character's own update function to update the character's look if needed.
+- `setActive(Bool)/isActive():Bool` : **(WIP)** implement those to correctly manage the effect's activated's state. A simple get/set is enough.
+
+## Roadmap
+The library's pretty much functionnal and gives the barebones features as now (the current effects comes from my dead project as freebies). Here's a non-exhaustive list of what could be added or changed to make the users' life easier :
+- [ ] Change the callback types into arrays or a class that acts a bit like C#'s delegates.
+- [ ] On JS, there is a quirk on how to calculate a space's width and a custom value is set instead. Maybe make this variable part of the settings class.
+- [ ] Implement helper classes such as a status icon or a "Press a button to continue" helper class.
+- [ ] Add more effects
+- [ ] Add more examples
+- [ ] Document the code as well as this file?
+- [ ] Unit testing. (As of now I've been using my own project and the sample as testing content but formalized unit testing would be nice to have)

BIN
readme_files/demo.gif


+ 83 - 0
sample/Project.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<project>
+	<!-- _________________________ Application Settings _________________________ -->
+
+	<app title="FlxProject" file="FlxProject" main="Main" version="0.0.1" company="HaxeFlixel" />
+
+	<!--The flixel preloader is not accurate in Chrome. You can use it regularly if you embed the swf into a html file
+		or you can set the actual size of your file manually at "FlxPreloaderBase-onUpdate-bytesTotal"-->
+	<app preloader="flixel.system.FlxPreloader" />
+
+	<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
+	<set name="SWF_VERSION" value="11.8" />
+
+	<!-- ____________________________ Window Settings ___________________________ -->
+
+	<!--These window settings apply to all targets-->
+	<window width="640" height="480" fps="60" background="#000000" hardware="true" vsync="false" />
+
+	<!--HTML5-specific-->
+	<window if="html5" resizable="false" />
+
+	<!--Desktop-specific-->
+	<window if="desktop" orientation="landscape" fullscreen="false" resizable="true" />
+
+	<!--Mobile-specific-->
+	<window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" />
+
+	<!-- _____________________________ Path Settings ____________________________ -->
+
+	<set name="BUILD_DIR" value="export" />
+	<classpath name="source" />
+	<classpath name=".." />
+	<assets path="assets" />
+
+	<!-- _______________________________ Libraries ______________________________ -->
+
+	<haxelib name="flixel" />
+
+	<!--In case you want to use the addons package-->
+	<haxelib name="flixel-addons" />
+
+	<!--In case you want to use the ui package-->
+	<!--<haxelib name="flixel-ui" />-->
+
+	<!--In case you want to use nape with flixel-->
+	<!--<haxelib name="nape" />-->
+
+	<!-- ______________________________ Haxedefines _____________________________ -->
+
+	<!--Enable the Flixel core recording system-->
+	<!--<haxedef name="FLX_RECORD" />-->
+
+	<!--Disable the right and middle mouse buttons-->
+	<!--<haxedef name="FLX_NO_MOUSE_ADVANCED" />-->
+
+	<!--Disable the native cursor API on Flash-->
+	<!--<haxedef name="FLX_NO_NATIVE_CURSOR" />-->
+
+	<!--Optimise inputs, be careful you will get null errors if you don't use conditionals in your game-->
+	<haxedef name="FLX_NO_MOUSE" if="mobile" />
+	<haxedef name="FLX_NO_KEYBOARD" if="mobile" />
+	<haxedef name="FLX_NO_TOUCH" if="desktop" />
+	<!--<haxedef name="FLX_NO_GAMEPAD" />-->
+
+	<!--Disable the Flixel core sound tray-->
+	<!--<haxedef name="FLX_NO_SOUND_TRAY" />-->
+
+	<!--Disable the Flixel sound management code-->
+	<!--<haxedef name="FLX_NO_SOUND_SYSTEM" />-->
+
+	<!--Disable the Flixel core focus lost screen-->
+	<!--<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />-->
+
+	<!--Disable the Flixel core debugger. Automatically gets set whenever you compile in release mode!-->
+	<haxedef name="FLX_NO_DEBUG" unless="debug" />
+
+	<!--Enable this for Nape release builds for a serious peformance improvement-->
+	<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
+
+	<!-- _________________________________ Custom _______________________________ -->
+
+	<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
+</project>

+ 0 - 0
sample/assets/data/data-goes-here.txt


+ 0 - 0
sample/assets/images/images-go-here.txt


+ 0 - 0
sample/assets/music/music-goes-here.txt


BIN
sample/assets/sounds/beep1.ogg


BIN
sample/assets/sounds/beep1.wav


BIN
sample/assets/sounds/beep2.ogg


BIN
sample/assets/sounds/beep2.wav


+ 0 - 0
sample/assets/sounds/sounds-go-here.txt


+ 4 - 0
sample/source/AssetPaths.hx

@@ -0,0 +1,4 @@
+package;
+
+@:build(flixel.system.FlxAssets.buildFileReferences("assets", true))
+class AssetPaths {}

+ 13 - 0
sample/source/Main.hx

@@ -0,0 +1,13 @@
+package;
+
+import flixel.FlxGame;
+import openfl.display.Sprite;
+
+class Main extends Sprite
+{
+	public function new()
+	{
+		super();
+		addChild(new FlxGame(0, 0, PlayState));
+	}
+}

+ 102 - 0
sample/source/PlayState.hx

@@ -0,0 +1,102 @@
+package;
+
+import flixel.FlxG;
+import flixel.FlxSprite;
+import flixel.FlxState;
+import flixel.util.FlxColor;
+import flixel.tweens.FlxTween;
+import flixel.tweens.FlxEase;
+import flixel.system.FlxAssets;
+import flixel.system.FlxSound;
+
+import textbox.Textbox;
+import textbox.Settings;
+
+class PlayState extends FlxState
+{
+
+	var tbox:Textbox;
+	var tbox2:Textbox;
+	var cursor:FlxSprite;
+	var cursorTween:FlxTween;
+	var beep1:FlxSound;
+	var beep2:FlxSound;
+	override public function create():Void
+	{
+
+		cursor = new FlxSprite(0, 0);
+		cursor.makeGraphic(8, 4);
+		beep1 = new FlxSound();
+		beep1.loadEmbedded(AssetPaths.beep1__ogg);
+		beep2 = new FlxSound();
+		beep2.loadEmbedded(AssetPaths.beep2__ogg);
+		var settingsTbox:Settings =
+		{
+			font: FlxAssets.FONT_DEFAULT,
+			fontSize: 16,
+			textFieldWidth: 320,
+			color: FlxColor.WHITE
+		};
+		tbox = new Textbox(200,30, settingsTbox);
+		tbox.setText("Hello World!@011001500 How goes? @010@001FF0000Color test!@000 This is a good old textbox test.");
+		tbox.characterDisplayCallback = function(t:textbox.Text):Void
+		{
+			cursor.x = t.x + t.width + 2;
+			cursor.y = t.y + t.height - 4;
+			cursor.color = t.color;
+			beep1.play(true);
+		};
+		tbox.bring();
+
+		var settingsTbox2:Settings =
+		{
+			font: FlxAssets.FONT_DEFAULT,
+			fontSize: 10,
+			textFieldWidth: 400,
+			charactersPerSecond: 30,
+			color: FlxColor.YELLOW
+		};
+		tbox2 = new Textbox(30,150, settingsTbox2);
+		tbox2.setText("This is another textbox, to show how the settings variables can change the result. Speed, size or color and more with the effects! Note that there is a fully working text wrap! :D");
+		tbox2.characterDisplayCallback = function(t:textbox.Text):Void
+		{
+			cursor.x = t.x + t.width + 2;
+			cursor.y = t.y + t.height - 4;
+			cursor.color = t.color;
+			beep2.play(true);
+		};
+		tbox2.statusChangeCallback = function(s:textbox.Status):Void
+		{
+			if (s == textbox.Status.DONE)
+			{
+				cursorTween = FlxTween.color(cursor, 0.5, cursor.color, FlxColor.TRANSPARENT, {
+					type: FlxTween.PINGPONG,
+					ease: FlxEase.cubeInOut
+				});
+			}
+		};
+		add(cursor);
+
+
+		tbox.statusChangeCallback = function (newStatus:textbox.Status):Void
+		{
+			if (newStatus == textbox.Status.FULL)
+			{
+				tbox.continueWriting();
+			}
+			else if(newStatus == textbox.Status.DONE)
+			{
+				add(tbox2);
+				tbox2.bring();
+			}
+		};
+		add(tbox);
+
+		super.create();
+	}
+
+	override public function update(elapsed:Float):Void
+	{
+		super.update(elapsed);
+	}
+}

+ 10 - 0
textbox/CommandValues.hx

@@ -0,0 +1,10 @@
+package textbox;
+
+typedef CommandValues =
+{
+	command:Int,
+    activated:Bool,
+	arg1:Int,
+	arg2:Int,
+	arg3:Int
+};

+ 33 - 0
textbox/Settings.hx

@@ -0,0 +1,33 @@
+package textbox;
+
+import flixel.util.FlxColor;
+import flixel.system.FlxAssets;
+
+@:structInit
+class Settings
+{
+    public var font:String;
+    public var fontSize:Int;
+    public var textFieldWidth:Float;
+    public var color: FlxColor;
+    public var numLines:Int;
+    public var charactersPerSecond:Float;
+
+    public function new(
+        font:String = null,
+        fontSize:Int = 12,
+        textFieldWidth:Float = 240,
+        color:FlxColor = null,
+        numLines:Int = 3,
+        charactersPerSecond:Float = 24
+    )
+    {
+        this.font = font == null ? FlxAssets.FONT_DEFAULT : font;
+        this.color = color == null ? FlxColor.WHITE : color;
+
+        this.fontSize = fontSize;
+        this.textFieldWidth = textFieldWidth;
+        this.numLines = numLines;
+        this.charactersPerSecond = charactersPerSecond;
+    }
+}

+ 34 - 0
textbox/Text.hx

@@ -0,0 +1,34 @@
+package textbox;
+import flixel.text.FlxText;
+import textbox.effects.IEffect;
+
+class Text extends FlxText {
+	public var effects:Array<IEffect>;
+
+	public override function new(X:Float = 0, Y:Float = 0, FieldWidth:Float = 0, ?text:String, Size:Int = 8, EmbeddedFont:Bool = true)
+	{
+		super(X, Y, FieldWidth, text, EmbeddedFont);
+		effects = [];
+		for (effect in TextEffectArray.effectClasses)
+		{
+			effects.push(Type.createInstance(effect, []));
+		}
+	}
+
+	public function clear()
+	{
+		this.offset.set(0,0);
+	}
+
+	override public function update(elapsed:Float)
+	{
+		for (effect in effects)
+		{
+            if (effect.isActive())
+            {
+                effect.update(elapsed);
+                effect.apply(this);
+            }
+		}
+	}
+}

+ 16 - 0
textbox/TextEffectArray.hx

@@ -0,0 +1,16 @@
+package textbox;
+import textbox.effects.*;
+
+/**
+ *  Contains an array of used effects. Their index will be their effect index, so an effect token @00[...]
+ *  will interact with the first class in the list.
+ */
+class TextEffectArray
+{
+    public static var effectClasses:Array<Class<IEffect>> =
+    [
+        ColorEffect,        // 00
+        RainbowEffect       // 01
+        // ...
+    ];
+}

+ 90 - 0
textbox/TextPool.hx

@@ -0,0 +1,90 @@
+package textbox;
+
+import flixel.util.FlxDestroyUtil;
+
+class TextPool implements IFlxPool<Text>
+{
+	public var length(get, never):Int;
+
+	private var _pool:Array<Text> = [];
+
+	/**
+	 * Objects aren't actually removed from the array in order to improve performance.
+	 * _count keeps track of the valid, accessible pool objects.
+	 */
+	private var _count:Int = 0;
+
+	public function new()
+	{
+	}
+
+	public function get():Text
+	{
+		if (_count == 0)
+		{
+			return new Text();
+		}
+		var c:Text = _pool[--_count];
+		c.clear();
+		return c;
+	}
+
+	public function put(obj:Text):Void
+	{
+		// we don't want to have the same object in the accessible pool twice (ok to have multiple in the inaccessible zone)
+		if (obj != null)
+		{
+			var i:Int = _pool.indexOf(obj);
+			// if the object's spot in the pool was overwritten, or if it's at or past _count (in the inaccessible zone)
+			if (i == -1 || i >= _count)
+			{
+				// Make the character invisible and not updated instead of destroying the shit out of it.
+				obj.kill();
+				_pool[_count++] = obj;
+			}
+		}
+	}
+
+	public function putUnsafe(obj:Text):Void
+	{
+		if (obj != null)
+		{
+			// Make the character invisible and not updated instead of destroying the shit out of it.
+			obj.kill();
+			_pool[_count++] = obj;
+		}
+	}
+
+	public function preAllocate(numObjects:Int):Void
+	{
+		while (numObjects-- > 0)
+		{
+			_pool[_count++] = new Text();
+		}
+	}
+
+	public function clear():Array<Text>
+	{
+		_count = 0;
+		var oldPool = _pool;
+		_pool = [];
+		return oldPool;
+	}
+
+	private inline function get_length():Int
+	{
+		return _count;
+	}
+}
+
+interface IFlxPooled extends IFlxDestroyable
+{
+	public function put():Void;
+	private var _inPool:Bool;
+}
+
+interface IFlxPool<T:IFlxDestroyable>
+{
+	public function preAllocate(numObjects:Int):Void;
+	public function clear():Array<Text>;
+}

+ 487 - 0
textbox/Textbox.hx

@@ -0,0 +1,487 @@
+package textbox;
+
+import textbox.CommandValues;
+import textbox.Text;
+import textbox.TextboxLine;
+import textbox.TextPool;
+import flixel.group.FlxGroup;
+import flixel.group.FlxSpriteGroup;
+import flixel.util.typeLimit.OneOfTwo;
+import haxe.Utf8;
+
+using StringTools;
+
+enum Status
+{
+    EMPTY;
+    WRITING;
+    PAUSED;
+    FULL;
+    DONE;
+}
+
+typedef TextboxCharacter = OneOfTwo<String, CommandValues>;
+
+// Callback typedefs
+
+// To be called when the textbox's status changes
+typedef StatusChangeCallback = Status -> Void;
+// To be called when a character is shown (for sound callbacks, timing or other).
+typedef CharacterDisplayCallback = Text -> Void;
+
+/**
+ * 	The holy mother of the textboxes.
+ *  Accepts text with tokens to enable and disable per-character effects.
+ *  Accept a settings structure to customize the basic behavior (such as font options or text speed.)
+ * 	The token can take two shapes
+ *	- @XX0 => disable effect 0xXX
+ *	- @XX1AABBCC => set effect 0xXX with args 0xAA, 0xBB 0xCC
+ */
+class Textbox extends FlxSpriteGroup {
+
+	public function new(X:Float, Y:Float, settings:Settings)
+	{
+		super(X, Y);
+		this.settings = settings;
+
+		status = DONE;
+
+		currentCharacterIndex = 0;
+		currentLineIndex = 0;
+		timerBeforeNewCharacter = 0;
+
+		willResume = false;
+
+
+		// Sub structure allocation.
+		characters = [];
+
+		characterPool = new TextPool();
+		lines = [for(i in 0...settings.numLines) new TextBoxLine()];
+
+		// Those ones can only be set when the lines are created, else we crash.
+		visible = false;
+		active = false;
+
+		effects = [];
+		for (i in 0 ... TextEffectArray.effectClasses.length)
+		{
+			effects.push({
+				command:i,
+                activated:false,
+				arg1:0,
+				arg2:0,
+				arg3:0
+			});
+		}
+
+	}
+
+
+	public override function update(elapsed:Float)
+	{
+		// If asked to continue after getting a full state
+		if(willResume)
+		{
+			if(status == FULL)
+				moveTextUp();
+			status = WRITING;
+			willResume = false;
+		}
+        else if(!(status == PAUSED || status == DONE))
+        {
+            // Nothing to do here
+
+            timerBeforeNewCharacter += elapsed;
+            while(timerBeforeNewCharacter > timePerCharacter)
+            {
+                if(status == WRITING)
+                {
+                    advanceCharacter();
+                }
+                timerBeforeNewCharacter -= timePerCharacter;
+            }
+        }
+		super.update(elapsed);
+	}
+
+	// When called, the textbox will go poof and disables itselfs from the scene.
+	public function dismiss()
+	{
+		if(!visible)
+		{
+			return;
+		}
+		// TODO : add a tween hook.
+		visible = false;
+		active = false;
+	}
+
+	// When called, the textbox will come on and activates itself into the scene.
+	public function bring()
+	{
+
+		// TODO : add a tween hook.
+		startWriting();
+		visible = true;
+		active = true;
+	}
+
+	// Sets a new string to the textbox and clears the shown characters.
+	public function setText(text:String)
+	{
+		for(line in lines)
+		{
+			// Puts back every used character into the pool.
+			for(character in line.dispose())
+			{
+				remove(character);
+				characterPool.put(character);
+			}
+		}
+
+		prepareString(text);
+		// Ready.
+		status = EMPTY;
+
+	}
+
+	// When called, this functions sets back some variables back for starting typing again.
+	public function startWriting()
+	{
+		currentCharacterIndex = 0;
+		currentLineIndex = 0;
+		timerBeforeNewCharacter = 0;
+		resetTextEffects();
+		status = WRITING;
+	}
+
+	// Small function to ask for the rest of the text when FULL.
+	public function continueWriting()
+	{
+		if(status == PAUSED || status == FULL)
+		{
+			willResume = true;
+		}
+	}
+
+	function resetTextEffects()
+	{
+		for (effect in effects)
+		{
+			effect =
+            {
+				command:0,
+                activated:false,
+				arg1:0,
+				arg2:0,
+				arg3:0
+			};
+		}
+	}
+
+	//  Parses the set string and fill the character array with the possible characters and commands
+	function prepareString(text:String)
+	{
+		characters = [];
+		var is_in_command = false;
+		var command:CommandValues =
+		{
+			command: 0,
+            activated:false,
+			arg1: 0,
+			arg2: 0,
+			arg3: 0
+		};
+		var current_hex:String = "0x";
+		var in_command_step:Int = 0;
+
+		for(i in 0...Utf8.length(text))
+		{
+			var char_code = Utf8.charCodeAt(text, i);
+			var current_character = Utf8.sub(text, i, 1);
+			// If we're still parsing a command code
+			if(is_in_command)
+			{
+				// Quick shortcut to check if the code is @@ to put @ in the text, just interrupt the parsing.
+				if(current_character == "@" && in_command_step == 0)
+				{
+					is_in_command = false;
+					characters.push(current_character);
+					continue;
+				}
+                // Spacing
+				// TODO : find a better way to determine if it's a space character than only that character
+                if (current_character == " ")
+                {
+                    continue;
+                }
+				// Continue parsing the hex code.
+				current_hex += current_character;
+				if((in_command_step == 2 && current_hex.length == 3) || current_hex.length == 4)
+				{
+					// If we parsed a pair, just put it in the correct variable.
+					var value:Null<Int> =  Std.parseInt(current_hex);
+					// Bad parsing
+					if(value == null)
+					{
+						is_in_command = false;
+						continue;
+					}
+					switch(in_command_step)
+					{
+
+						case 1:
+						command.command = value;
+                        case 2:
+                        command.activated = value != 0;
+						case 4:
+						command.arg1 = value;
+						case 6:
+						command.arg2 = value;
+						case 8:
+						command.arg3 = value;
+					}
+					current_hex = "0x";
+				}
+				// Go forward in the process
+				in_command_step++;
+				// And stop it if we had enough characters.
+				if(in_command_step == 9 || (in_command_step == 3 && !command.activated))
+				{
+					is_in_command = false;
+					characters.push(command);
+                    command =
+                    {
+                        command: 0,
+                        activated:false,
+                        arg1: 0,
+                        arg2: 0,
+                        arg3: 0
+                    };
+				}
+			}
+			else
+			{
+				// Go into the hex code system if requested.
+				if(char_code == '@'.charCodeAt(0))
+				{
+					is_in_command = true;
+					in_command_step = 0;
+					current_hex = "0x";
+					command.command = 0;
+					command.activated = false;
+					command.arg1 = 0;
+					command.arg2 = 0;
+					command.arg3 = 0;
+				}
+				else{
+					characters.push(current_character);
+				}
+			}
+		}
+		// Decided that the system wouldn't add a partial command code at the end of a text entry.
+	}
+
+	// This one is a helluva function but it does everything you need.
+	function advanceCharacter()
+	{
+		// Just avoid an access exception.
+		if(currentCharacterIndex >= characters.length)
+		{
+			status = DONE;
+			return;
+		}
+		var current_character = characters[currentCharacterIndex];
+
+		// If space, pre-calculate next word's length to word wrap.
+		if(!(Std.is(current_character, String) && (!cast(current_character, String).isSpace(0) || cast(current_character, String) == '\n'))){
+			// We have to build a string containing the next characters to calculate the size of the line.
+			var word:String = " ";
+			var index:Int = currentCharacterIndex+1;
+			// So, while we're finding non-invisible characters
+			while(index < characters.length)
+			{
+				var forward_character = characters[index];
+				if(!Std.is(forward_character, String))
+				{
+					index++;
+					continue;
+				}
+				var forward_char:String = cast(characters[index], String);
+				if(!forward_char.isSpace(0))
+					word += forward_char;
+				else
+					break;
+				index++;
+			}
+			// TODO : please don't make bigass words
+			// SO, if we're going over the limit, just go to the next line.
+			if(lines[currentLineIndex].projectWidth(word) > settings.textFieldWidth)
+			{
+				currentCharacterIndex++;
+				if(currentLineIndex < settings.numLines-1){
+					currentLineIndex++;
+				}
+				else {
+					status = FULL;
+				}
+				return;
+			}
+		}
+		else if(Std.is(current_character, String)){
+			// Now, character wrap should be useless but let's keep it.
+			if(cast(current_character, String) == '\n' || lines[currentLineIndex].projectWidth(current_character) > settings.textFieldWidth)
+			{
+				if(currentLineIndex < settings.numLines-1){
+					currentLineIndex++;
+				}
+				else {
+					status = FULL;
+					return;
+				}
+			}
+		}
+
+		if(Std.is(current_character, String))
+		{
+			var char:String = cast(current_character, String);
+			// Get a new character from the pool
+			var new_character:Text = characterPool.get();
+			// Preparing it for the default style.
+			new_character.autoSize = true;
+			new_character.font = settings.font;
+			new_character.size = settings.fontSize;
+			new_character.text = char;
+			new_character.color = settings.color;
+			new_character.y = currentLineIndex * new_character.height;
+			new_character.x = lines[currentLineIndex].text_width;
+			for (effect in effects)
+			{
+				var characterEffect = new_character.effects[effect.command];
+				characterEffect.reset(effect.arg1,effect.arg2,effect.arg3, 0);
+                characterEffect.setActive(effect.activated);
+				characterEffect.apply(new_character);
+			}
+
+			// This line is only for the opacity tweens to work.
+			new_character.alpha = alpha;
+			// Raaaaaise from the deeead.
+			new_character.revive();
+			// Put it in the line and go forward
+			lines[currentLineIndex].push(new_character);
+			add(new_character);
+            if(characterDisplayCallback != null)
+            {
+                characterDisplayCallback(new_character);
+            }
+			currentCharacterIndex++;
+		}
+		else
+		{
+			var command:CommandValues = cast current_character;
+			effects[command.command] = command;
+
+			currentCharacterIndex++;
+			timerBeforeNewCharacter += timePerCharacter;
+		}
+	}
+
+	// THis function is only called when having to continue spitting out characters after going FULL
+	function moveTextUp()
+	{
+		// Clearing the first line and putting its characters in the pool.
+		var characters_to_dispose = lines[0].dispose();
+		for(character in characters_to_dispose)
+		{
+			remove(character);
+			characterPool.put(character);
+		}
+		// Moving the text one line upwrads.
+		for(i in 1...settings.numLines)
+		{
+			var characters_to_pass:FlxTypedGroup<Text> = lines[i].dispose();
+			for(character in characters_to_pass)
+            {
+				character.y -= character.height;
+			}
+			lines[i-1].take(characters_to_pass);
+		}
+		currentLineIndex = settings.numLines - 1;
+	}
+
+    // callbacks
+    public var characterDisplayCallback: CharacterDisplayCallback;
+    public var statusChangeCallback: StatusChangeCallback;
+
+	// Variable members
+	var status(default, set):Status;
+
+	var settings:Settings;
+
+
+	// internal
+	// The character array. Calculated from a sent string, it contains the list of characters to show or commands to execute.
+	var characters:Array<TextboxCharacter>;
+	var timePerCharacter(get, never):Float;
+	// The position among the whole character array.
+	var currentCharacterIndex:Int;
+	// The timer before adding a new character
+	var timerBeforeNewCharacter:Float;
+
+
+	// A TextPool to easily manage the ever-changing characters with a minimal amount of allocation.
+	var characterPool:TextPool;
+
+	// The line array, contains the data structures to store and manage the characters.
+	var lines:Array<TextBoxLine>;
+	// The text line's index.
+	var currentLineIndex:Int;
+	// Just a small internal boolean to notice when a FULL textbox can continue.
+	var willResume:Bool;
+
+	// Textbox things. Kinds of act like a pen.
+	// Stores the current color of the text.
+	var effects:Array<CommandValues>;
+
+
+    // getter/setters
+ 	public function get_timePerCharacter():Float
+	{
+		return 1./settings.charactersPerSecond;
+	}
+
+	// Small helper to manage the status_icon.
+	function set_status(status:Status)
+	{
+        var previousStatus = this.status;
+		this.status = status;
+        if (status != previousStatus && statusChangeCallback != null)
+        {
+            statusChangeCallback(status);
+        }
+		return status;
+	}
+
+	public override function set_alpha(Alpha:Float):Float
+    {
+		for(line in lines)
+			line.characters.forEach(function(s){s.set_alpha(Alpha);});
+		return super.set_alpha(Alpha);
+	}
+
+// Why do I need to do this...
+	public override function set_visible(Visible:Bool):Bool
+    {
+		visible = Visible;
+		group.set_visible(true);
+		return visible;
+	}
+
+	public override function set_active(Active:Bool):Bool
+    {
+		active = Active;
+		group.set_active(true);
+		return active;
+	}
+}

+ 74 - 0
textbox/TextboxLine.hx

@@ -0,0 +1,74 @@
+package textbox;
+
+import flixel.group.FlxGroup;
+import flixel.text.FlxText;
+
+class TextBoxLine {
+	public var characters:FlxTypedGroup<Text>;
+	public var text_width(default, null):Float;
+	var inner_text:FlxText;
+
+	public function new()
+	{
+		characters = new FlxTypedGroup<Text>();
+		text_width = 0;
+		inner_text = new FlxText();
+		inner_text.text = "";
+	}
+
+	// Creates a tempoary FlxText to calculate the future length of the current string with a suffix.
+	public function projectWidth(string_to_append:String):Float
+	{
+		var test_string:FlxText = new FlxText();
+		test_string.font = inner_text.font;
+		test_string.size = inner_text.size;
+		test_string.text = inner_text.text + string_to_append;
+		return test_string.textField.width;
+	}
+
+	// Accepts a new character and updates its logic values like width.
+	public function push(character:Text):Void
+	{
+		// Regerenate the FlxText.
+		if(characters.length == 0)
+		{
+			inner_text.text = "";
+			inner_text.font = character.font;
+			inner_text.size = character.size;
+		}
+		characters.add(character);
+		inner_text.text += character.text;
+		#if js
+		// Legnth calculation wouldn't work properly if I haven't done this.
+		if(character.text == " ")
+			// TODO : pass this magic cookie as a setting
+			text_width += character.width+2;
+		else
+			text_width = inner_text.textField.textWidth;
+		#else
+		text_width = inner_text.textField.textWidth;
+		#end
+	}
+
+	// Releases its characters to pass along or put them back into pool.
+	public function dispose():FlxTypedGroup<Text>
+	{
+		text_width = 0;
+		var c = characters;
+		characters = new FlxTypedGroup<Text>();
+		inner_text.text = "";
+		return c;
+	}
+
+	// Takes ownership of the characters and recalculates its metrics.
+	public function take(characters:FlxTypedGroup<Text>):Void
+	{
+		this.characters = characters;
+		inner_text.text = "";
+		for(character in characters)
+		{
+			inner_text.text += character.text;
+		}
+		text_width = inner_text.width;
+	}
+}

+ 44 - 0
textbox/effects/ColorEffect.hx

@@ -0,0 +1,44 @@
+package textbox.effects;
+
+import flixel.util.FlxColor;
+
+class ColorEffect implements IEffect
+{
+    private var active:Bool;
+    public function new()
+    {
+        color = new FlxColor(FlxColor.WHITE);
+        active = false;
+    }
+
+    public function reset(arg1:Int, arg2:Int, arg3:Int, nthCharacter:Int):Void
+    {
+        color.red = arg1;
+        color.green = arg2;
+        color.blue = arg3;
+    }
+
+    public function setActive(active:Bool):Void
+    {
+        this.active = active;
+    }
+
+
+    public function update(elapsed:Float):Void
+    {
+    }
+
+    public function apply(text:Text):Void
+    {
+        if(!isActive())
+            return;
+            text.color = color;
+    }
+
+    public function isActive():Bool
+    {
+        return active;
+    }
+
+    var color:FlxColor;
+}

+ 12 - 0
textbox/effects/IEffect.hx

@@ -0,0 +1,12 @@
+package textbox.effects;
+import textbox.Text;
+
+interface IEffect
+{
+    public function reset(arg1:Int, arg2:Int, arg3:Int, nthCharacter:Int):Void;
+    public function update(elapsed:Float):Void;
+    public function apply(text:Text):Void;
+    // To be called to offset per character
+    public function setActive(active:Bool):Void;
+    public function isActive():Bool;
+}

+ 52 - 0
textbox/effects/RainbowEffect.hx

@@ -0,0 +1,52 @@
+package textbox.effects;
+
+import flixel.util.FlxColor;
+
+class RainbowEffect implements IEffect
+{
+    private var active:Bool;
+
+    public function new()
+    {
+        hue = 0;
+        hueSpeed = 0;
+    }
+
+    public function reset(_startingHue:Int, _hueSpeed:Int, arg3:Int, nthCharacter:Int):Void
+    {
+        hue = _startingHue;
+        hueSpeed = _hueSpeed*15;
+    }
+
+    public function update(elapsed:Float):Void
+    {
+        if (!isActive())
+            return;
+
+        hue = (hue + hueSpeed * elapsed) % 360.;
+    }
+
+    public function apply(text:Text):Void
+    {
+        if (!isActive())
+            return;
+
+
+        text.color = FlxColor.fromHSL(hue, 0.5, 0.5);
+    }
+
+    public function setActive(active:Bool):Void
+    {
+        this.active = active;
+    }
+
+
+    public function isActive():Bool
+    {
+        return active;
+    }
+
+    var enabled:Bool;
+    var hue:Float;
+    var hueSpeed:Float;
+}