├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── readme_files
└── demo.gif
├── samples
├── decorating
│ ├── Project.xml
│ └── source
│ │ ├── AssetPaths.hx
│ │ ├── Main.hx
│ │ └── PlayState.hx
└── demo
│ ├── Project.xml
│ ├── assets
│ └── sounds
│ │ ├── beep1.mp3
│ │ ├── beep1.ogg
│ │ ├── beep1.wav
│ │ ├── beep2.mp3
│ │ ├── beep2.ogg
│ │ └── beep2.wav
│ └── source
│ ├── AssetPaths.hx
│ ├── Main.hx
│ └── PlayState.hx
├── test
├── TestMain.hx
├── TestSuite.hx
├── TextboxTest.hx
├── project.xml
└── wrappers
│ └── Textbox.hx
└── textbox
├── CommandValues.hx
├── Settings.hx
├── Text.hx
├── TextEffectArray.hx
├── TextPool.hx
├── Textbox.hx
├── TextboxLine.hx
└── effects
├── ColorEffect.hx
├── IEffect.hx
├── RainbowEffect.hx
├── RotatingEffect.hx
└── WaveEffect.hx
/.gitignore:
--------------------------------------------------------------------------------
1 | export/
2 | .vscode/
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 | ### Changed
5 | - Clean and long method split iteration.
6 | ## 0.0.5 - 2018-12-14
7 | ### Added
8 | - Added MP3 samples for demo sample for flash target.
9 | ### Changed
10 | - Changed the internal character structure for an ADT enumeration. Nothing should visually change. It's a part of cleaning process.
11 | - Renamed an internal class.
12 | - Removed a few seemingly useless setters in textbox.
13 | - Removed usage of FlxTypedGroup in TextboxLine. Didn't serve a sensible goal in the end.
14 | - Removed a few unit test-related cruft.
15 | ### Fixed
16 | - Fixed demos not compiling on static.
17 | - Fixed super constructor call in Textbox, blocking Flash target to compile.
18 | ## 0.0.4 - 2018-12-04
19 | ### Added
20 | ### Changed
21 | - Switched from `FlxTween`'s tween type variables to `FlxTweenType` enumeration, avoiding deprecation messages on Haxeflixel 4.5.1+.
22 | - Added a few haxedef defines to avoid compiling useless chunks of Haxeflixel in tests.
23 | ### Fixed
24 | - (static platforms) Parsing non-hexadecimal characters wouldn't trigger a parsing error fallback as `parseInt` returns 0 and not `null`.
25 | - Added a few additonal unit tests around invalid character code parsing.
26 | ## 0.0.3 - 2018-04-19
27 | ### Added
28 | - (html5) Added `characterSpacingHack` in `Settings` to replace the magic cookie used for fixing character spacing.
29 | ### Changed
30 | - Readme cleaned.
31 | ### Fixed
32 | - Compilation failure when targetting html5
33 | - Demo sample's cursor would jump on html5 due to spaces having no height.
34 | ## 0.0.2 - 2018-04-17
35 | ### Changed
36 | - Textbox's `advanceCharacter` function has been cleaned.
37 | - General codestyle cleaning.
38 | - Factorized the demo sample to make callback plugin class examples.
39 | ### Fixed
40 | - Newline in the textbox's last line would make the textbox perpetually stuck on the newline.
41 | - Using isSpace instead of a comparison against the space character
42 | ## 0.0.1 - 2018-04-17
43 | ### Added
44 | - Initial versioning.
45 | - Added a sample (decorating)
46 | ### Changed
47 | - Textbox callback members are now callback arrays to handle multiple callbacks.
48 | - Sample folder structure and name (`sample -> samples`)
49 | - The first sample is now in `samples/demo`
50 | - Changed demo sample to use the new callback type.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Florian Dormont
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A Haxeflixel unnamed textbox library
2 | **NOTE** : Even though the API shouldn't change a lot for now, this project is still a bit in WIP.
3 |
4 | 
5 |
6 | ## Features
7 | - "Character-per-character"-style textbox for [Haxeflixel](https://haxeflixel.com/).
8 | - Correct word-wrapping : a being written word won't jump from a line to another if it's too big for the current line
9 | - Per-character effects : allows you to have dynamic text.
10 | - Extendable and customizable : There is a way to add new effects to your textbox, change the font, color, size, etc...
11 |
12 | ## Usage
13 |
14 | ### Known issues or quirks
15 | - On HTML5 we have to deal with an incorrect dimension report for space characters, thus letter spacing and space size are incorrect. There is a character spacing hack setting to make up for it, `characterSpacingHack` in the `Settings` class.
16 |
17 | ### Installation
18 | Include the library's root folder as classpath in your project node. Something like (as 2018-04-16)
19 | ```xml
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | Now you should be ready and able to require classes such as `textbox.Textbox`.
28 |
29 | ### Textbox functions
30 | - `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.
31 | - `bring ()` : activates the textbox and its process, making it appear in-game.
32 | - `dismiss ()` : deactivates the textbox and makes it disappear in-game
33 | - `setText (String)` : parses the string's content and prepares itself for the next use.
34 | - `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)
35 |
36 | ### Callbacks
37 | **NOTE** : This is under WIP as it might change between versions.
38 |
39 | #### Why callbacks?
40 | 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.
41 |
42 | A few callback ideas (chaining boxes, sound-per-character, text character) are shown in the given sample project, in the form of simple callbacks or *plugin* classes.
43 |
44 | ### List of available callbacks
45 | - `statusChangeCallbacks (Status)` : if the textbox's state changes, this callback is called. Here a list of the expected behavior to be notifed of:
46 | + `FULL` : the textbox is full. Coupled with `continueWriting` you can make stuff like waiting a button press to resume writing.
47 | + `DONE` : the textbox finished writing its content. You can `dismiss` it or set it with new text.
48 | - `characterDisplayCallbacks (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...
49 |
50 | Those textbox callback facilities are just callback arrays, to add or remove your own callback, just use `push`, `pop`, `remove`, etc... on those members. There is no control from the textbox's part, so take care of which callbacks you're manipulating.
51 |
52 | ### Settings object
53 | (Check for textbox/Settings.hx to see what kind of parametters you can override.)
54 | ```haxe
55 | font:String // Font location, default = HXFlixel's default font
56 | fontSize:Int // default = 12
57 | textFieldWidth:Float // default = 240px
58 | color:FlxColor // default = White
59 | numLines:Int // How many lines the textbox will display, default = 3
60 | charactersPerSecond:Int // default = 24
61 | ```
62 |
63 | ## Text effects
64 | This textbox allows for per-character effects such as (but not limited to) rotating characters or making them wave, coloring text or making an animated rainbow. Those effects are enabled and disabled by in-text code sequences that are small and human-writable. The system that links effects and the textbox is also user-editable to add new effects for your own projects.
65 |
66 | ## Code sequences and effects.
67 | Having a textbox is fine. Having a textbox that can make letters change their color or move easily is better. To apply effects easily, this library looks for code sequences while parsing to determine which effect and how to trigger it. A code sequence is composed of an arobas (`@`) and 3 or 7 hexadecimal numbers determining which effect to enable or disable and its parameters. It looks like `@00105DEFF` or `@050`. The first sequence turns on the effect n° 00 with as arguments the values (5, 222, 255) while the second disables the effect n°05.
68 |
69 | ### Enabling an effect
70 | ```
71 | ┌ Enable this effect
72 | ▼
73 | @MM1AABBCC
74 | ▲│ ┃ ┃ ┃
75 | └┼ Sequence start ()
76 | │ ┃ ┃ ┃
77 | └ Effect n° 0xMM
78 | ┃ ┃ ┃
79 | ┃ ┃ ┃
80 | ┗ Argument 1 : 0xAA
81 | ┃ ┃
82 | ┗ Argument 2 : 0xBB
83 | ┃
84 | ┗ Argument 3 : 0xCC
85 | ```
86 |
87 | ### Disabling an effect
88 | ```
89 | ┌ Disable this effect
90 | ▼
91 | @MM0
92 | ▲│
93 | └┼ Sequence start ()
94 | │
95 | └ Effect n° 0xMM
96 | ```
97 |
98 | ### Usage example
99 | ```haxe
100 | var textbox:Textbox = new Textbox(...);
101 | textbox`.setText("
102 | I'm enabling effect n°00 with arguments (0x00, 0x00, 0x00) : @001000000
103 | I'm disabling effect n°05 @050
104 | I'm enabling multiple effects : @0010A0C0E@0510DCCDE...
105 | It even works inside wo@001000000rds, even if it's a bit unreadable...
106 | ");
107 | ```
108 |
109 | ### Create new effects
110 | **NOTE** : This is under WIP as it might change between versions.
111 |
112 | 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.
113 |
114 | #### Effect interface
115 | - `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.)
116 | - `update(Float)` : the good old classic update function, called by the textbox on a `FlxState.update()` tick or manual update.
117 | - `apply(Float)` : called on a character's own update function to update the character's look if needed.
118 | - `setActive(Bool)/isActive():Bool` : **(WIP)** implement those to correctly manage the effect's activated's state. A simple get/set is enough.
119 |
120 | ### Testing
121 | There is a `test` folder containing tests using [munit]. Run `lime test ` inside the `test` folder to test on the given platform.
122 |
123 | ### Samples
124 | There are currently a few sample varying in complexity to show a few ways to use the textbox library.
125 | - Demo, which shows a few of the features the textbox has to offer through two textboxes
126 | - Decorating, which shows a textbox with tweening, press waiting status and a linked background. Closer to a simple ingame textbox visual.
127 |
128 | **NOTE** : Currently, test on JS/HTML5 platform is broken as munit fails to find a `haxe:trace` element. This is currently being looked on to see how to fix it.
129 |
130 | ## Roadmap
131 | 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 :
132 | - [X] Change the callback types into arrays or a class that acts a bit like C#'s delegates.
133 | - [X] On JS, there is a quirk on how to calculate a space's width and a magic cookie is used instead. Maybe make this variable part of the settings class.
134 | - [X] Implement ~~helper~~ example classes such as a status icon or a "Press a button to continue" helper class.
135 | Note : Due to various kind of interactions with a textbox, I prefer making small examples as there is no canonical way to make them, only ways that works with your own projects.
136 | - Add more effects
137 | - [ ] Add more examples
138 | + [ ] For tween-based character, factorize tween selection code?
139 | - [ ] Document the code as well as this file?
140 | - [ ] Unit testing *(in progress)*
141 | + [ ] Fix broken unit tests on JS or swap to haxe's bundled unit test library.
142 |
143 | [munit]: https://github.com/massiveinteractive/MassiveUnit
--------------------------------------------------------------------------------
/readme_files/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/readme_files/demo.gif
--------------------------------------------------------------------------------
/samples/decorating/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/samples/decorating/source/AssetPaths.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | @:build(flixel.system.FlxAssets.buildFileReferences("assets", true))
4 | class AssetPaths {}
--------------------------------------------------------------------------------
/samples/decorating/source/Main.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxGame;
4 | import openfl.display.Sprite;
5 |
6 | class Main extends Sprite
7 | {
8 | public function new()
9 | {
10 | super();
11 | addChild(new FlxGame(0, 0, PlayState));
12 | }
13 | }
--------------------------------------------------------------------------------
/samples/decorating/source/PlayState.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxG;
4 | import flixel.FlxSprite;
5 | import flixel.FlxState;
6 | import flixel.text.FlxText;
7 | import flixel.util.FlxColor;
8 | import flixel.tweens.FlxTween;
9 | import flixel.tweens.FlxEase;
10 | import flixel.system.FlxAssets;
11 | import flixel.system.FlxSound;
12 |
13 | import textbox.Textbox;
14 | import textbox.Settings;
15 |
16 | class PlayState extends FlxState
17 | {
18 | var tboxBackground:FlxSprite;
19 | var tbox:Textbox;
20 | var waitingForUserInput:Bool = false;
21 | var textboxIsFinished:Bool = false;
22 | var waitingToRestart:Bool = false;
23 | var pressSpaceIndication:FlxText;
24 |
25 | function showIndication(text:String)
26 | {
27 | pressSpaceIndication.text = text;
28 | pressSpaceIndication.visible = true;
29 | }
30 |
31 | override public function create():Void
32 | {
33 |
34 | var settingsTbox:Settings = new Settings
35 | (
36 | FlxAssets.FONT_DEFAULT,
37 | 16,
38 | 320,
39 | FlxColor.WHITE
40 | );
41 | tbox = new Textbox((FlxG.width - 320)/2,30, settingsTbox);
42 | tbox.setText("This is a demo showing how to make the textbox wait for user input. Useful when you're having too much text and not a lot of space, right? :D");
43 |
44 | tbox.statusChangeCallbacks.push(function (newStatus:textbox.Status):Void
45 | {
46 | if (newStatus == textbox.Status.FULL)
47 | {
48 | waitingForUserInput = true;
49 | showIndication("Press [SPACE] to continue.");
50 | }
51 | else if(newStatus == textbox.Status.DONE)
52 | {
53 | waitingForUserInput = true;
54 | showIndication("Press [SPACE] to end.");
55 | textboxIsFinished = true;
56 | }
57 | });
58 |
59 | pressSpaceIndication = new FlxText(tbox.x, 160, 0, "", 14);
60 | pressSpaceIndication.color = FlxColor.CYAN;
61 | pressSpaceIndication.visible = false;
62 |
63 |
64 | tboxBackground = new FlxSprite(tbox.x - 4, tbox.y - 4);
65 | tboxBackground.makeGraphic(Std.int(settingsTbox.textFieldWidth + 8), 78, FlxColor.BROWN);
66 | tboxBackground.alpha = 0;
67 |
68 | FlxTween.tween(tboxBackground, {alpha: 1}, 0.4,
69 | {
70 | ease: FlxEase.circOut,
71 | onComplete: function(_)
72 | {
73 | tbox.bring();
74 | }
75 | });
76 |
77 | add(tboxBackground);
78 | add(tbox);
79 | add(pressSpaceIndication);
80 |
81 |
82 | super.create();
83 | }
84 |
85 | override public function update(elapsed:Float):Void
86 | {
87 | super.update(elapsed);
88 | if(FlxG.keys.justPressed.SPACE)
89 | {
90 | if (waitingForUserInput)
91 | {
92 | waitingForUserInput = false;
93 | if(textboxIsFinished)
94 | {
95 | tbox.dismiss();
96 | FlxTween.tween(tboxBackground, {alpha: 0}, 0.4,
97 | {
98 | ease: FlxEase.circOut,
99 | onComplete : function(_)
100 | {
101 | showIndication("Press [SPACE] to restart.");
102 | waitingToRestart = true;
103 | }
104 | });
105 | }
106 | else
107 | {
108 | tbox.continueWriting();
109 | }
110 | pressSpaceIndication.visible = false;
111 | }
112 | else if (waitingToRestart)
113 | {
114 | FlxG.resetState();
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/samples/demo/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep1.mp3
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep1.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep1.ogg
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep1.wav
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep2.mp3
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep2.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep2.ogg
--------------------------------------------------------------------------------
/samples/demo/assets/sounds/beep2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eiyeron/Textbox/0c1f42cec4b10f68825f51941a4d8579e510900e/samples/demo/assets/sounds/beep2.wav
--------------------------------------------------------------------------------
/samples/demo/source/AssetPaths.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | @:build(flixel.system.FlxAssets.buildFileReferences("assets", true))
4 | class AssetPaths {}
--------------------------------------------------------------------------------
/samples/demo/source/Main.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxGame;
4 | import openfl.display.Sprite;
5 |
6 | class Main extends Sprite
7 | {
8 | public function new()
9 | {
10 | super();
11 | addChild(new FlxGame(0, 0, PlayState));
12 | }
13 | }
--------------------------------------------------------------------------------
/samples/demo/source/PlayState.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxG;
4 | import flixel.FlxSprite;
5 | import flixel.FlxState;
6 | import flixel.util.FlxColor;
7 | import flixel.tweens.FlxTween;
8 | import flixel.tweens.FlxTween.FlxTweenType;
9 | import flixel.tweens.FlxEase;
10 | import flixel.system.FlxAssets;
11 | import flixel.system.FlxSound;
12 |
13 | import textbox.Textbox;
14 | import textbox.Settings;
15 |
16 | // Those two demo classes are both an example of how you could make classes to
17 | // modularize plugin features on the textbox and a structure idea to
18 | // eventually make a helper class and/or interface to do so.
19 |
20 | class DemoTextCursor extends FlxSprite
21 | {
22 | public override function new(X:Float, Y:Float)
23 | {
24 | super(X, Y);
25 | makeGraphic(8, 4);
26 |
27 | ownCharacterCallback = function(character:textbox.Text)
28 | {
29 | characterCallbackInternal(character);
30 | };
31 | }
32 |
33 |
34 | public function attachToTextbox(textbox:Textbox)
35 | {
36 | textbox.characterDisplayCallbacks.push(ownCharacterCallback);
37 | }
38 |
39 | public function detachFromTextbox(textbox:Textbox)
40 | {
41 | textbox.characterDisplayCallbacks.remove(ownCharacterCallback);
42 | }
43 |
44 | private function characterCallbackInternal(character:textbox.Text)
45 | {
46 | x = character.x + character.width + 2;
47 |
48 | // I noted an issue : the character height is 0 if targetting javascript.
49 | if (character.text != " ")
50 | {
51 | y = character.y + character.height - 4;
52 | }
53 | color = character.color;
54 | }
55 |
56 | private var ownCharacterCallback:textbox.Text->Void = null;
57 | }
58 |
59 | class DemoPlaySoundOnCharacter
60 | {
61 | public function new(soundAsset:flixel.system.FlxAssets.FlxSoundAsset)
62 | {
63 | sound = new FlxSound();
64 | sound.loadEmbedded(soundAsset);
65 | ownCharacterCallback = function(character:textbox.Text)
66 | {
67 | sound.play(true);
68 | };
69 | }
70 |
71 | public function attachToTextbox(textbox:Textbox)
72 | {
73 | textbox.characterDisplayCallbacks.push(ownCharacterCallback);
74 | }
75 |
76 | public function detachFromTextbox(textbox:Textbox)
77 | {
78 | textbox.characterDisplayCallbacks.remove(ownCharacterCallback);
79 | }
80 |
81 | private var sound:FlxSound;
82 | private var ownCharacterCallback:textbox.Text->Void = null;
83 | }
84 |
85 | class PlayState extends FlxState
86 | {
87 |
88 | var tbox:Textbox;
89 | var tbox2:Textbox;
90 | var cursor:DemoTextCursor;
91 | var cursorTween:FlxTween;
92 | var beep1:DemoPlaySoundOnCharacter;
93 | var beep2:DemoPlaySoundOnCharacter;
94 | override public function create():Void
95 | {
96 |
97 | cursor = new DemoTextCursor(0, 0);
98 | #if flash
99 | beep1 = new DemoPlaySoundOnCharacter(AssetPaths.beep1__mp3);
100 | beep2 = new DemoPlaySoundOnCharacter(AssetPaths.beep2__mp3);
101 | #else
102 | beep1 = new DemoPlaySoundOnCharacter(AssetPaths.beep1__ogg);
103 | beep2 = new DemoPlaySoundOnCharacter(AssetPaths.beep2__ogg);
104 | #end
105 | var settingsTbox:Settings = new Settings(
106 | FlxAssets.FONT_DEFAULT,
107 | 16,
108 | 320,
109 | FlxColor.WHITE
110 | );
111 | tbox = new Textbox(200,30, settingsTbox);
112 | tbox.setText("Hello World!@011001500 How goes? @010@001FF0000Color test!@000 This is a good old textbox test.");
113 | cursor.attachToTextbox(tbox);
114 | beep1.attachToTextbox(tbox);
115 | tbox.bring();
116 |
117 | var settingsTbox2:Settings = new Settings
118 | (
119 | FlxAssets.FONT_DEFAULT,
120 | 12,
121 | 400,
122 | FlxColor.YELLOW,
123 | 30,
124 | 4
125 | );
126 | tbox2 = new Textbox(30,150, settingsTbox2);
127 | tbox2.setText("This is @021014010another@020 textbox, to show how the settings variables can change the result. Speed, size or color and @031023820more with the effects@030! Note that there is a fully working text wrap! :D");
128 | beep2.attachToTextbox(tbox2);
129 | tbox2.statusChangeCallbacks.push(function(s:textbox.Status):Void
130 | {
131 | if (s == textbox.Status.DONE)
132 | {
133 | cursorTween = FlxTween.color(cursor, 0.5, cursor.color, FlxColor.TRANSPARENT,
134 | {
135 | type: FlxTweenType.PINGPONG,
136 | ease: FlxEase.cubeInOut
137 | }
138 | );
139 | }
140 | });
141 | add(cursor);
142 |
143 |
144 | tbox.statusChangeCallbacks.push(function (newStatus:textbox.Status):Void
145 | {
146 | if (newStatus == textbox.Status.FULL)
147 | {
148 | tbox.continueWriting();
149 | }
150 | else if(newStatus == textbox.Status.DONE)
151 | {
152 | add(tbox2);
153 | cursor.detachFromTextbox(tbox);
154 | cursor.attachToTextbox(tbox2);
155 | tbox2.bring();
156 | }
157 | });
158 | add(tbox);
159 |
160 | super.create();
161 | }
162 |
163 | override public function update(elapsed:Float):Void
164 | {
165 | super.update(elapsed);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/test/TestMain.hx:
--------------------------------------------------------------------------------
1 | import massive.munit.client.PrintClient;
2 | import massive.munit.client.RichPrintClient;
3 | import massive.munit.client.HTTPClient;
4 | import massive.munit.client.JUnitReportClient;
5 | import massive.munit.client.SummaryReportClient;
6 | import massive.munit.TestRunner;
7 | import openfl.Lib;
8 | import flixel.FlxGame;
9 |
10 | /**
11 | * Auto generated Test Application.
12 | * Refer to munit command line tool for more information (haxelib run munit)
13 | */
14 | class TestMain
15 | {
16 | static function main()
17 | {
18 | // Setting back trace() for html5
19 | new TestMain();
20 | }
21 |
22 | public function new()
23 | {
24 | // OpenFL/Flixel init
25 | Lib.current.stage.addChild(new FlxGame(800, 600, null, 1, 60, 60, true));
26 |
27 | var suites = new Array>();
28 | suites.push(TestSuite);
29 |
30 | #if MCOVER
31 | var client = new mcover.coverage.munit.client.MCoverPrintClient();
32 | var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient());
33 | #else
34 | var client = new RichPrintClient();
35 | var httpClient = new HTTPClient(new SummaryReportClient());
36 | #end
37 |
38 | var runner:TestRunner = new TestRunner(client);
39 | runner.addResultClient(httpClient);
40 |
41 | runner.completionHandler = completionHandler;
42 |
43 | runner.run(suites);
44 | }
45 |
46 | /**
47 | * updates the background color and closes the current browser
48 | * for flash and html targets (useful for continous integration servers)
49 | */
50 | function completionHandler(successful:Bool)
51 | {
52 | try
53 | {
54 | #if flash
55 | flash.external.ExternalInterface.call("testResult", successful);
56 | #elseif js
57 | js.Lib.eval("testResult(" + successful + ");");
58 | #elseif (neko || cpp || java || cs || python || php || hl)
59 | Sys.exit(0);
60 | #end
61 | }
62 | // if run from outside browser can get error which we can ignore
63 | catch (e:Dynamic) {}
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/test/TestSuite.hx:
--------------------------------------------------------------------------------
1 | import massive.munit.TestSuite;
2 |
3 | /**
4 | * Auto generated Test Suite for MassiveUnit.
5 | * Refer to munit command line tool for more information (haxelib run munit)
6 | */
7 | class TestSuite extends massive.munit.TestSuite
8 | {
9 | public function new()
10 | {
11 | super();
12 |
13 | add(TextboxTest);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/TextboxTest.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import massive.munit.util.Timer;
4 | import massive.munit.Assert;
5 | import massive.munit.async.AsyncFactory;
6 |
7 | import haxe.Utf8;
8 | import textbox.CommandValues;
9 | import textbox.Textbox.CharacterType;
10 | import textbox.Settings;
11 | import wrappers.Textbox;
12 |
13 | /**
14 | * Auto generated ExampleTest for MassiveUnit.
15 | * This is an example test class can be used as a template for writing normal and async tests
16 | * Refer to munit command line tool for more information (haxelib run munit)
17 | */
18 | class TextboxTest
19 | {
20 | private var tbox:Textbox;
21 | public function new()
22 | {
23 | }
24 |
25 | @Before
26 | public function setup()
27 | {
28 | tbox = new Textbox(0, 0, new Settings());
29 | }
30 |
31 | @Test
32 | public function testSimpleParsing()
33 | {
34 | var text = "Hello world";
35 | tbox.setText(text);
36 | var characters = tbox.getCharacters();
37 |
38 | Assert.areEqual(Utf8.length(text), characters.length);
39 |
40 | matchTextAgainstCharacters(text, characters);
41 | }
42 |
43 | @Test
44 | public function testEnableSequenceParsing()
45 | {
46 | var text = "@501134256";
47 | tbox.setText(text);
48 | var characters = tbox.getCharacters();
49 |
50 | Assert.areEqual(1, characters.length);
51 | var expectedSequence:CommandValues =
52 | {
53 | command: 0x50,
54 | activated: true,
55 | arg1: 0x13,
56 | arg2: 0x42,
57 | arg3: 0x56,
58 | };
59 |
60 | matchSequences(expectedSequence, characters[0]);
61 | }
62 |
63 | @Test
64 | public function testDisableSequenceParsing()
65 | {
66 | var text = "@050";
67 | tbox.setText(text);
68 | var characters = tbox.getCharacters();
69 |
70 | Assert.areEqual(1, characters.length);
71 | var expectedSequence:CommandValues =
72 | {
73 | command: 5,
74 | activated: false,
75 | arg1: 0,
76 | arg2: 0,
77 | arg3: 0,
78 | };
79 | matchSequences(expectedSequence, characters[0]);
80 | }
81 |
82 | @Test
83 | public function testParsingInText()
84 | {
85 | var prefix = "Hello ";
86 | var suffix = "world!";
87 | var code = "@001123456";
88 |
89 | var prefixLength = Utf8.length(prefix);
90 | var suffixLength = Utf8.length(suffix);
91 |
92 | var text = prefix + code + suffix;
93 | tbox.setText(text);
94 | var characters = tbox.getCharacters();
95 |
96 | Assert.areEqual(prefixLength + suffixLength + 1, characters.length, "Length should match the prefix's plus the suffix's plus the character sequence");
97 |
98 | matchTextAgainstCharacters(prefix, characters.slice(0, prefixLength));
99 |
100 | matchSequences
101 | (
102 | {
103 | command: 0,
104 | activated: true,
105 | arg1: 0x12,
106 | arg2: 0x34,
107 | arg3: 0x56,
108 | },
109 | characters[prefixLength]
110 | );
111 |
112 | matchTextAgainstCharacters(suffix, characters.slice(prefixLength + 1, characters.length));
113 | }
114 |
115 | @Test
116 | public function testInvalidSequenceParsing()
117 | {
118 | // As now, the parser only breaks at the end of a sequence part.
119 | // Here, it stops after collecting the two characters of the command's index
120 | // Hence the string resulting length is not taking both in count
121 | // TODO : Fix parsing to add the collected values back into `characters` on failure.
122 | // This is slightly non trivial? What to expect?
123 | // Error case examples:
124 | // @GG 1 00 11 22 => Stop and add nothing into the chain? Stop and add the @ into the chain?
125 | // @00 D 00 11 22 => Stop and add "00 D" into the chain?
126 | // ...
127 | // This also expects that the spacing and all are processed. This'll require an O(n) allocations of memory as the backtrack
128 | // string goes higher and higher.
129 |
130 | // Invalid first part
131 | {
132 | var text = "Hello@__0001122";
133 | var expectedText = "Hello0001122";
134 | tbox.setText(text);
135 | var characters = tbox.getCharacters();
136 |
137 | Assert.areEqual(Utf8.length(expectedText), characters.length);
138 |
139 | matchTextAgainstCharacters(expectedText, characters);
140 | }
141 |
142 | // Invalid second part
143 | {
144 | var text = "Hello@00_001122";
145 | var expectedText = "Hello001122";
146 | tbox.setText(text);
147 | var characters = tbox.getCharacters();
148 |
149 | Assert.areEqual(Utf8.length(expectedText), characters.length);
150 |
151 | matchTextAgainstCharacters(expectedText, characters);
152 | }
153 |
154 | // Invalid third part
155 | {
156 | var text = "Hello@001__1122";
157 | var expectedText = "Hello1122";
158 | tbox.setText(text);
159 | var characters = tbox.getCharacters();
160 |
161 | Assert.areEqual(Utf8.length(expectedText), characters.length);
162 |
163 | matchTextAgainstCharacters(expectedText, characters);
164 | }
165 |
166 | // Invalid fourth part
167 | {
168 | var text = "Hello@00100__22";
169 | var expectedText = "Hello22";
170 | tbox.setText(text);
171 | var characters = tbox.getCharacters();
172 |
173 | Assert.areEqual(Utf8.length(expectedText), characters.length);
174 |
175 | matchTextAgainstCharacters(expectedText, characters);
176 | }
177 |
178 | // Invalid fifth part
179 | {
180 | var text = "Hello@0010011__";
181 | var expectedText = "Hello";
182 | tbox.setText(text);
183 | var characters = tbox.getCharacters();
184 |
185 | Assert.areEqual(Utf8.length(expectedText), characters.length);
186 |
187 | matchTextAgainstCharacters(expectedText, characters);
188 | }
189 |
190 | }
191 |
192 | // Small helpers
193 | function matchSequences(expected:CommandValues, actual:CharacterType)
194 | {
195 | switch actual {
196 | case CharacterType.Character(_):
197 | Assert.fail("Expected a code sequence but got a character sequence.");
198 | case CharacterType.Command(command):
199 | Assert.areEqual(expected.command, command.command, haxe.CallStack.toString(haxe.CallStack.callStack()));
200 | Assert.areEqual(expected.activated, command.activated);
201 | Assert.areEqual(expected.arg1, command.arg1);
202 | Assert.areEqual(expected.arg2, command.arg2);
203 | Assert.areEqual(expected.arg3, command.arg3);
204 | }
205 | }
206 |
207 | function matchTextAgainstCharacters(expectedText:String, characters:Array)
208 | {
209 | for (i in 0...Utf8.length(expectedText))
210 | {
211 | switch characters[i] {
212 | case CharacterType.Command(command):
213 | Assert.fail("Expected a character sequence but got a code sequence");
214 | case CharacterType.Character(character):
215 | Assert.areEqual(Utf8.sub(expectedText, i, 1), character);
216 | }
217 | }
218 | }
219 | }
--------------------------------------------------------------------------------
/test/project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/wrappers/Textbox.hx:
--------------------------------------------------------------------------------
1 | package wrappers;
2 |
3 | import textbox.Textbox.CharacterType;
4 |
5 | class Textbox extends textbox.Textbox
6 | {
7 | public function getCharacters():Array
8 | {
9 | return characters;
10 | }
11 | }
--------------------------------------------------------------------------------
/textbox/CommandValues.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 |
3 | typedef CommandValues =
4 | {
5 | command:Int,
6 | activated:Bool,
7 | arg1:Int,
8 | arg2:Int,
9 | arg3:Int
10 | };
--------------------------------------------------------------------------------
/textbox/Settings.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 |
3 | import flixel.util.FlxColor;
4 | import flixel.system.FlxAssets;
5 |
6 | @:structInit
7 | class Settings
8 | {
9 | public var font:String;
10 | public var fontSize:Int;
11 | public var textFieldWidth:Float;
12 | public var color: FlxColor;
13 | public var numLines:Int;
14 | public var charactersPerSecond:Float;
15 | /**
16 | * This should only be used in html5 target as determining the space character'size
17 | * is broken only on this target.
18 | */
19 | public var characterSpacingHack:Float;
20 |
21 | public function new(
22 | font:String = null,
23 | fontSize:Int = 12,
24 | textFieldWidth:Float = 240,
25 | color:FlxColor = FlxColor.WHITE,
26 | numLines:Int = 3,
27 | charactersPerSecond:Float = 24,
28 | // This default value has been empirically chosen to work with only the fonts
29 | // I used, feel free to use another value to match your font's needs
30 | characterSpacingHack:Float = 2
31 | )
32 | {
33 | this.font = font == null ? FlxAssets.FONT_DEFAULT : font;
34 | this.fontSize = fontSize;
35 | this.textFieldWidth = textFieldWidth;
36 | this.color = color;
37 | this.numLines = numLines;
38 | this.charactersPerSecond = charactersPerSecond;
39 | this.characterSpacingHack = characterSpacingHack;
40 | }
41 | }
--------------------------------------------------------------------------------
/textbox/Text.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 | import flixel.text.FlxText;
3 | import textbox.effects.IEffect;
4 |
5 | class Text extends FlxText {
6 | public var effects:Array;
7 |
8 | public function new(X:Float = 0, Y:Float = 0, FieldWidth:Float = 0, ?text:String, Size:Int = 8, EmbeddedFont:Bool = true)
9 | {
10 | super(X, Y, FieldWidth, text, Size, EmbeddedFont);
11 | effects = [];
12 | for (effect in TextEffectArray.effectClasses)
13 | {
14 | effects.push(Type.createInstance(effect, []));
15 | }
16 | }
17 |
18 | public function clear()
19 | {
20 | this.offset.set(0,0);
21 | }
22 |
23 | override public function update(elapsed:Float)
24 | {
25 | for (effect in effects)
26 | {
27 | if (effect.isActive())
28 | {
29 | effect.update(elapsed);
30 | effect.apply(this);
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/textbox/TextEffectArray.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 | import textbox.effects.*;
3 |
4 | /**
5 | * Contains an array of used effects. Their index will be their effect index, so an effect token @00[...]
6 | * will interact with the first class in the list.
7 | * Feel free to edit this class here and somewhere else too add your own effects.
8 | */
9 | class TextEffectArray
10 | {
11 | public static var effectClasses:Array> =
12 | [
13 | ColorEffect, // 00
14 | RainbowEffect, // 01
15 | RotatingEffect, // 02
16 | WaveEffect, // 03
17 | // ...
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/textbox/TextPool.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 |
3 | import flixel.util.FlxDestroyUtil;
4 |
5 | class TextPool implements IFlxPool
6 | {
7 |
8 | public function new()
9 | {
10 | }
11 |
12 | public function get():Text
13 | {
14 | if (_count == 0)
15 | {
16 | return new Text();
17 | }
18 | var c:Text = _pool[--_count];
19 | c.clear();
20 | return c;
21 | }
22 |
23 | public function put(obj:Text):Void
24 | {
25 | // we don't want to have the same object in the accessible pool twice (ok to have multiple in the inaccessible zone)
26 | if (obj != null)
27 | {
28 | var i:Int = _pool.indexOf(obj);
29 | // if the object's spot in the pool was overwritten, or if it's at or past _count (in the inaccessible zone)
30 | if (i == -1 || i >= _count)
31 | {
32 | // Make the character invisible and not updated instead of destroying it.
33 | obj.kill();
34 | _pool[_count++] = obj;
35 | }
36 | }
37 | }
38 |
39 | public function putUnsafe(obj:Text):Void
40 | {
41 | if (obj != null)
42 | {
43 | // Make the character invisible and not updated instead of destroying it.
44 | obj.kill();
45 | _pool[_count++] = obj;
46 | }
47 | }
48 |
49 | public function preAllocate(numObjects:Int):Void
50 | {
51 | while (numObjects-- > 0)
52 | {
53 | _pool[_count++] = new Text();
54 | }
55 | }
56 |
57 | public function clear():Array
58 | {
59 | _count = 0;
60 | var oldPool = _pool;
61 | _pool = [];
62 | return oldPool;
63 | }
64 |
65 | private inline function get_length():Int
66 | {
67 | return _count;
68 | }
69 |
70 | public var length(get, never):Int;
71 |
72 | private var _pool:Array = [];
73 |
74 | /**
75 | * Objects aren't actually removed from the array in order to improve performance.
76 | * _count keeps track of the valid, accessible pool objects.
77 | */
78 | private var _count:Int = 0;
79 | }
80 |
81 | interface IFlxPooled extends IFlxDestroyable
82 | {
83 | public function put():Void;
84 | private var _inPool:Bool;
85 | }
86 |
87 | interface IFlxPool
88 | {
89 | public function preAllocate(numObjects:Int):Void;
90 | public function clear():Array;
91 | }
92 |
--------------------------------------------------------------------------------
/textbox/Textbox.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 |
3 | import textbox.CommandValues;
4 | import textbox.Text;
5 | import textbox.TextboxLine;
6 | import textbox.TextPool;
7 | import flixel.group.FlxGroup;
8 | import flixel.group.FlxSpriteGroup;
9 | import flixel.util.typeLimit.OneOfTwo;
10 | import haxe.Utf8;
11 |
12 | using StringTools;
13 |
14 | enum Status
15 | {
16 | EMPTY;
17 | WRITING;
18 | PAUSED;
19 | FULL;
20 | DONE;
21 | }
22 |
23 | enum CharacterType
24 | {
25 | Character(character:String);
26 | Command(command:CommandValues);
27 | }
28 |
29 | // Callback typedefs
30 |
31 | // To be called when the textbox's status changes
32 | typedef StatusChangeCallback = Status -> Void;
33 | // To be called when a character is shown (for sound callbacks, timing or other).
34 | typedef CharacterDisplayCallback = Text -> Void;
35 |
36 | /**
37 | * The holy mother of textboxes.
38 | * Accepts text with tokens to enable and disable per-character effects.
39 | * Accept a settings structure to customize the basic behavior (such as font options or text speed.)
40 | * The token can take two shapes
41 | * - @XX0 => disable effect 0xXX
42 | * - @XX1AABBCC => set effect 0xXX with args 0xAA, 0xBB 0xCC
43 | */
44 | class Textbox extends FlxSpriteGroup {
45 |
46 | private static inline var hexadecimalCharacters = "0123456789abcdefABCDEF";
47 |
48 | public function new(X:Float, Y:Float, settings:Settings)
49 | {
50 | super(X, Y);
51 | this.settings = settings;
52 |
53 | status = DONE;
54 |
55 | currentCharacterIndex = 0;
56 | currentLineIndex = 0;
57 | timerBeforeNewCharacter = 0;
58 |
59 | willResume = false;
60 |
61 |
62 | // Sub structure allocation.
63 | characters = [];
64 |
65 | characterPool = new TextPool();
66 | lines = [for(i in 0...settings.numLines) new TextBoxLine()];
67 |
68 | // Those ones can only be set when the lines are created, else we crash.
69 | visible = false;
70 | active = false;
71 |
72 | effects = [];
73 | for (i in 0 ... TextEffectArray.effectClasses.length)
74 | {
75 | effects.push({
76 | command:i,
77 | activated:false,
78 | arg1:0,
79 | arg2:0,
80 | arg3:0
81 | });
82 | }
83 |
84 | }
85 |
86 |
87 | public override function update(elapsed:Float)
88 | {
89 | // If asked to continue after getting a full state
90 | if(willResume)
91 | {
92 | if(status == FULL)
93 | {
94 | moveTextUp();
95 | }
96 | status = WRITING;
97 | willResume = false;
98 | }
99 | else if(status != PAUSED && status != DONE)
100 | {
101 | timerBeforeNewCharacter += elapsed;
102 | while(timerBeforeNewCharacter > timePerCharacter)
103 | {
104 | if(status == WRITING)
105 | {
106 | advanceCharacter();
107 | }
108 | timerBeforeNewCharacter -= timePerCharacter;
109 | }
110 | }
111 | super.update(elapsed);
112 | }
113 |
114 | /**
115 | * The textbox will be invisible and disables itselfs from the scene.
116 | */
117 | public function dismiss()
118 | {
119 | if(!visible)
120 | {
121 | return;
122 | }
123 | visible = false;
124 | active = false;
125 | }
126 |
127 | /**
128 | * When called, the textbox will come on and activates itself into the scene.
129 | * It also starts writing the parsed text, so it should be called after `setText`.
130 | */
131 | public function bring()
132 | {
133 | startWriting();
134 | visible = true;
135 | active = true;
136 | }
137 |
138 | /**
139 | * Clears the current text lines and parsed sequences and parse the given text
140 | * to set it up as the new sequence.
141 | * @param text - New text to set
142 | */
143 | public function setText(text:String)
144 | {
145 | for(line in lines)
146 | {
147 | // Puts back every used character into the pool.
148 | for(character in line.dispose())
149 | {
150 | remove(character);
151 | characterPool.put(character);
152 | }
153 | }
154 |
155 | prepareString(text);
156 | // Ready.
157 | status = EMPTY;
158 |
159 | }
160 |
161 | /**
162 | * This function sets the initial state to start writing.
163 | */
164 | public function startWriting()
165 | {
166 | currentCharacterIndex = 0;
167 | currentLineIndex = 0;
168 | timerBeforeNewCharacter = 0;
169 | resetTextEffects();
170 | status = WRITING;
171 | }
172 |
173 | /**
174 | * This functions asks the textbox to continue writing if it got full.
175 | * This is manually done to allow multiple behaviors like automatic scrolling
176 | * or waiting for user input.
177 | */
178 | public function continueWriting()
179 | {
180 | if(status == PAUSED || status == FULL)
181 | {
182 | willResume = true;
183 | }
184 | }
185 |
186 | /**
187 | * Resets the available effects' state/parameters.
188 | */
189 | function resetTextEffects()
190 | {
191 | for (effect in effects)
192 | {
193 | effect =
194 | {
195 | command:0,
196 | activated:false,
197 | arg1:0,
198 | arg2:0,
199 | arg3:0
200 | };
201 | }
202 | }
203 |
204 | /**
205 | * Parses collected sequence part and dispatches it to the command depending on the step the parsing is on.
206 | */
207 | function processSequenceCodePart(currentHexString:String, commandParsingStep:Int, resultingCommand:CommandValues)
208 | {
209 | // Basic sanity check : make sure that the parsed characters are alright
210 | // First character
211 |
212 | if (hexadecimalCharacters.indexOf(Utf8.sub(currentHexString, 2, 1)) == -1)
213 | {
214 | return false;
215 | }
216 | // Second character (proccessed after to avoid going out of bounds for the toggle flag)
217 | if (currentHexString.length == 4 && hexadecimalCharacters.indexOf(Utf8.sub(currentHexString, 3, 1)) == -1)
218 | {
219 | return false;
220 | }
221 | // If we parsed a pair, just put it in the correct variable.
222 | var value:Null = Std.parseInt(currentHexString);
223 | // Bad parsing
224 | if(value == null)
225 | {
226 | return false;
227 | }
228 | switch(commandParsingStep)
229 | {
230 | // Code | @IIvAABBCC
231 | // Index | 12 4 6 8
232 | case 1:
233 | resultingCommand.command = value;
234 | case 2:
235 | resultingCommand.activated = value != 0;
236 | case 4:
237 | resultingCommand.arg1 = value;
238 | case 6:
239 | resultingCommand.arg2 = value;
240 | case 8:
241 | resultingCommand.arg3 = value;
242 | }
243 | return true;
244 | }
245 |
246 | /**
247 | * Parses the set string and fill the character array with the possible characters and commands.
248 | * TODO : this could be moved into a static function with a context class to help splitting code.
249 | */
250 | function prepareString(text:String)
251 | {
252 | characters = [];
253 | var isParsingACommand = false;
254 | var command:CommandValues =
255 | {
256 | command: 0,
257 | activated:false,
258 | arg1: 0,
259 | arg2: 0,
260 | arg3: 0
261 | };
262 | var currentHexString:String = "0x";
263 | var commandParsingStep:Int = 0;
264 |
265 | for(i in 0...Utf8.length(text))
266 | {
267 | var currentCharacter = Utf8.sub(text, i, 1);
268 | // If we're still parsing a command code
269 | if(isParsingACommand)
270 | {
271 | // Quick shortcut to check if the code is @@ to put @ in the text, just interrupt the parsing.
272 | if(Utf8.compare(currentCharacter, '@') == 0 && commandParsingStep == 0)
273 | {
274 | isParsingACommand = false;
275 | characters.push(CharacterType.Character(currentCharacter));
276 | continue;
277 | }
278 |
279 | // Letting spaces split sequence codes to help people write and proof-read them.
280 | if (currentCharacter.isSpace(0))
281 | {
282 | continue;
283 | }
284 |
285 | // Continue parsing the hex code.
286 | currentHexString += currentCharacter;
287 | if((commandParsingStep == 2 && currentHexString.length == 3) || currentHexString.length == 4)
288 | {
289 | isParsingACommand = processSequenceCodePart(currentHexString, commandParsingStep, command);
290 | currentHexString = "0x";
291 | if (!isParsingACommand)
292 | {
293 | continue;
294 | }
295 | }
296 | // And stop it if we had enough characters.
297 | if(commandParsingStep == 8 || (commandParsingStep == 2 && !command.activated))
298 | {
299 | isParsingACommand = false;
300 | characters.push(CharacterType.Command(command));
301 | // Use a new command.
302 | command =
303 | {
304 | command: 0,
305 | activated:false,
306 | arg1: 0,
307 | arg2: 0,
308 | arg3: 0
309 | };
310 | }
311 | else
312 | {
313 | // Go forward in the process
314 | commandParsingStep++;
315 | }
316 | }
317 | else
318 | {
319 | // Go into the hex code system if requested.
320 | if(Utf8.compare(currentCharacter, '@') == 0)
321 | {
322 | isParsingACommand = true;
323 | commandParsingStep = 0;
324 | currentHexString = "0x";
325 | command.command = 0;
326 | command.activated = false;
327 | command.arg1 = 0;
328 | command.arg2 = 0;
329 | command.arg3 = 0;
330 | }
331 | else
332 | {
333 | characters.push(CharacterType.Character(currentCharacter));
334 | }
335 | }
336 | }
337 | // Decided that the system wouldn't add a partial command code at the end of a text entry.
338 | }
339 |
340 | function getNextWord(currentCharacterIndex:Int):String
341 | {
342 | // We have to build a string containing the next characters to calculate the size of the line.
343 | var wordBuilder:StringBuf = new StringBuf();
344 | wordBuilder.add(" ");
345 | var index:Int = currentCharacterIndex+1;
346 | // So, while we're finding non-invisible characters
347 | while(index < characters.length)
348 | {
349 | var forward_character = characters[index];
350 | switch characters[index]
351 | {
352 | case CharacterType.Command(command):
353 | index++;
354 | continue;
355 | case CharacterType.Character(forward_char):
356 | if(!forward_char.isSpace(0))
357 | wordBuilder.add(forward_char);
358 | else
359 | break;
360 | }
361 |
362 | index++;
363 | }
364 | return wordBuilder.toString();
365 | }
366 |
367 | function appendNewCharacterToTextbox(characterToAdd:String)
368 | {
369 | // Get a new character from the pool
370 | var newCharacter:Text = characterPool.get();
371 | // Preparing it for the default style.
372 | newCharacter.autoSize = true;
373 | newCharacter.font = settings.font;
374 | newCharacter.size = settings.fontSize;
375 | newCharacter.text = characterToAdd;
376 | newCharacter.color = settings.color;
377 | newCharacter.y = currentLineIndex * newCharacter.height;
378 | newCharacter.x = lines[currentLineIndex].textWidth;
379 | for (effect in effects)
380 | {
381 | var characterEffect = newCharacter.effects[effect.command];
382 | characterEffect.reset(effect.arg1,effect.arg2,effect.arg3, 0);
383 | characterEffect.setActive(effect.activated);
384 | if (effect.activated)
385 | {
386 | characterEffect.apply(newCharacter);
387 | }
388 | }
389 |
390 | // This line is only for the opacity tweens to work.
391 | newCharacter.alpha = alpha;
392 | // Raaaaaise from the deeead.
393 | newCharacter.revive();
394 | // Put it in the line and go forward
395 | lines[currentLineIndex].push(newCharacter, settings.characterSpacingHack);
396 | add(newCharacter);
397 | for (callback in characterDisplayCallbacks)
398 | {
399 | callback(newCharacter);
400 | }
401 | }
402 |
403 | function goToNextLine()
404 | {
405 | if(currentLineIndex < settings.numLines-1)
406 | {
407 | currentLineIndex++;
408 | }
409 | else
410 | {
411 | status = FULL;
412 | }
413 |
414 | }
415 |
416 | /**
417 | * This function is called when the textbox has to write down a new character.
418 | * Does *a bit* of process (like word wrapping or newline support) but makes
419 | * stuff work.
420 | */
421 | function advanceCharacter()
422 | {
423 | // Just avoid an access exception.
424 | if(currentCharacterIndex >= characters.length)
425 | {
426 | status = DONE;
427 | return;
428 | }
429 | // TODO : this process could eventually be split into functions.
430 | switch characters[currentCharacterIndex]
431 | {
432 |
433 | case Character(currentCharacterChar):
434 | {
435 | // inline line returns support
436 | // must be first because \n is space
437 | if (currentCharacterChar == '\n')
438 | {
439 | goToNextLine();
440 | // If we're placing a newline in the last textbox's line,
441 | // make the textbox advance by one character else it'd
442 | // be perpetually stuck on the newline.
443 | if (status == FULL)
444 | {
445 | currentCharacterIndex++;
446 | }
447 | return;
448 | }
449 | // If current character is a space, let's calculate how long the next word will be.
450 | else if (currentCharacterChar.isSpace(0))
451 | {
452 | var nextWord:String = getNextWord(currentCharacterIndex);
453 | // TODO : please don't make words too long.
454 | // SO, if we're going over the limit, just go to the next line.
455 | if(lines[currentLineIndex].projectWidth(nextWord) > settings.textFieldWidth)
456 | {
457 | goToNextLine();
458 | // As we wrapped the line on this character, let's skip it.
459 | currentCharacterIndex++;
460 | return;
461 | }
462 | }
463 |
464 | // Character-wrap. Shouldn't be really useful but it's still a guard.
465 | else if(lines[currentLineIndex].projectWidth(currentCharacterChar) > settings.textFieldWidth)
466 | {
467 | goToNextLine();
468 | if (status == FULL)
469 | {
470 | return;
471 | }
472 | }
473 |
474 | // Now that the text flow control is past us, let's add the new character to the textbox.
475 | appendNewCharacterToTextbox(currentCharacterChar);
476 | currentCharacterIndex++;
477 |
478 | }
479 | case Command(command):
480 | {
481 | effects[command.command] = command;
482 |
483 | currentCharacterIndex++;
484 | timerBeforeNewCharacter += timePerCharacter;
485 | }
486 | }
487 | }
488 |
489 | // THis function is only called when having to continue spitting out characters after going FULL
490 | function moveTextUp()
491 | {
492 | // Clearing the first line and putting its characters in the pool.
493 | var charactersToDispose = lines[0].dispose();
494 | for(character in charactersToDispose)
495 | {
496 | remove(character);
497 | characterPool.put(character);
498 | }
499 | // Moving the text one line upwards.
500 | for(i in 1...settings.numLines)
501 | {
502 | var charactersToGive = lines[i].dispose();
503 | for(character in charactersToGive)
504 | {
505 | character.y -= character.height;
506 | }
507 | lines[i-1].take(charactersToGive);
508 | }
509 | currentLineIndex = settings.numLines - 1;
510 | }
511 |
512 | // callbacks
513 | public var characterDisplayCallbacks(default, null): Array = [];
514 | public var statusChangeCallbacks(default, null): Array = [];
515 |
516 | // Variable members
517 | var status(default, set):Status;
518 |
519 | var settings:Settings;
520 |
521 |
522 | // internal
523 | // The character array. Calculated from a sent string, it contains the list of characters to show or commands to execute.
524 | var characters:Array;
525 | var timePerCharacter(get, never):Float;
526 | // The position among the whole character array.
527 | var currentCharacterIndex:Int;
528 | // The timer before adding a new character
529 | var timerBeforeNewCharacter:Float;
530 |
531 |
532 | // A TextPool to easily manage the ever-changing characters with a minimal amount of allocation.
533 | var characterPool:TextPool;
534 |
535 | // The line array, contains the data structures to store and manage the characters.
536 | var lines:Array;
537 | // The text line's index.
538 | var currentLineIndex:Int;
539 | // Just a small internal boolean to notice when a FULL textbox can continue.
540 | var willResume:Bool;
541 |
542 | // Textbox things. Kinds of act like a pen.
543 | // Stores the current color of the text.
544 | var effects:Array;
545 |
546 |
547 | // getter/setters
548 | public function get_timePerCharacter():Float
549 | {
550 | return 1./settings.charactersPerSecond;
551 | }
552 |
553 | // Small helper to determine if the status change callbacks must be called.
554 | function set_status(status:Status)
555 | {
556 | var previousStatus = this.status;
557 | this.status = status;
558 | if(status != previousStatus)
559 | {
560 | for (callback in statusChangeCallbacks)
561 | {
562 | callback(status);
563 | }
564 | }
565 | return status;
566 | }
567 | }
--------------------------------------------------------------------------------
/textbox/TextboxLine.hx:
--------------------------------------------------------------------------------
1 | package textbox;
2 |
3 | import flixel.text.FlxText;
4 |
5 | using StringTools;
6 | class TextBoxLine
7 | {
8 | public function new()
9 | {
10 | characters = new Array();
11 | textWidth = 0;
12 | innerText = new FlxText();
13 | innerText.text = "";
14 | }
15 |
16 | // Creates a tempoary FlxText to calculate the future length of the current string with a suffix.
17 | public function projectWidth(string_to_append:String):Float
18 | {
19 | var testString:FlxText = new FlxText();
20 | testString.font = innerText.font;
21 | testString.size = innerText.size;
22 | testString.text = innerText.text + string_to_append;
23 | return testString.textField.width;
24 | }
25 |
26 | // Accepts a new character and updates its logic values like width.
27 | public function push(character:Text, characterSpacingHack:Float):Void
28 | {
29 | // Regerenate the FlxText.
30 | if(characters.length == 0)
31 | {
32 | innerText.text = "";
33 | innerText.font = character.font;
34 | innerText.size = character.size;
35 | }
36 | characters.push(character);
37 | innerText.text += character.text;
38 | #if js
39 | // Legnth calculation wouldn't work properly if I haven't done this.
40 | if(character.text.isSpace(0))
41 | textWidth += character.width + characterSpacingHack;
42 | else
43 | textWidth = innerText.textField.textWidth;
44 | #else
45 | textWidth = innerText.textField.textWidth;
46 | #end
47 | }
48 |
49 | // Releases its characters to pass along or put them back into pool.
50 | public function dispose():Array
51 | {
52 | textWidth = 0;
53 | var c = characters;
54 | characters = new Array();
55 | innerText.text = "";
56 | return c;
57 | }
58 |
59 | // Takes ownership of the characters and recalculates its metrics.
60 | public function take(characters:Array):Void
61 | {
62 | this.characters = characters;
63 | innerText.text = "";
64 | for(character in characters)
65 | {
66 | innerText.text += character.text;
67 | }
68 | textWidth = innerText.width;
69 | }
70 |
71 | public var characters:Array;
72 | public var textWidth(default, null):Float;
73 | private var innerText:FlxText;
74 | }
75 |
--------------------------------------------------------------------------------
/textbox/effects/ColorEffect.hx:
--------------------------------------------------------------------------------
1 | package textbox.effects;
2 |
3 | import flixel.util.FlxColor;
4 |
5 | class ColorEffect implements IEffect
6 | {
7 | private var active:Bool;
8 | public function new()
9 | {
10 | color = new FlxColor(FlxColor.WHITE);
11 | active = false;
12 | }
13 |
14 | public function reset(arg1:Int, arg2:Int, arg3:Int, nthCharacter:Int):Void
15 | {
16 | color.red = arg1;
17 | color.green = arg2;
18 | color.blue = arg3;
19 | }
20 |
21 | public function update(elapsed:Float):Void
22 | {
23 | }
24 |
25 | public function apply(text:Text):Void
26 | {
27 | text.color = color;
28 | }
29 |
30 | public function setActive(active:Bool):Void
31 | {
32 | this.active = active;
33 | }
34 |
35 | public function isActive():Bool
36 | {
37 | return active;
38 | }
39 |
40 | var color:FlxColor;
41 | }
--------------------------------------------------------------------------------
/textbox/effects/IEffect.hx:
--------------------------------------------------------------------------------
1 | package textbox.effects;
2 | import textbox.Text;
3 |
4 | interface IEffect
5 | {
6 | public function reset(arg1:Int, arg2:Int, arg3:Int, nthCharacter:Int):Void;
7 | public function update(elapsed:Float):Void;
8 | public function apply(text:Text):Void;
9 | // To be called to offset per character
10 | public function setActive(active:Bool):Void;
11 | public function isActive():Bool;
12 | }
--------------------------------------------------------------------------------
/textbox/effects/RainbowEffect.hx:
--------------------------------------------------------------------------------
1 | package textbox.effects;
2 |
3 | import flixel.util.FlxColor;
4 |
5 | class RainbowEffect implements IEffect
6 | {
7 | public function new()
8 | {
9 | hue = 0;
10 | hueSpeed = 0;
11 | }
12 |
13 | public function reset(_startingHue:Int, _hueSpeed:Int, arg3:Int, nthCharacter:Int):Void
14 | {
15 | hue = _startingHue;
16 | hueSpeed = _hueSpeed*15;
17 | }
18 |
19 | public function update(elapsed:Float):Void
20 | {
21 | hue = (hue + hueSpeed * elapsed) % 360.;
22 | }
23 |
24 | public function apply(text:Text):Void
25 | {
26 | text.color = FlxColor.fromHSL(hue, 0.5, 0.5);
27 | }
28 |
29 | public function setActive(active:Bool):Void
30 | {
31 | this.active = active;
32 | }
33 |
34 |
35 | public function isActive():Bool
36 | {
37 | return active;
38 | }
39 |
40 | private var active:Bool = false;
41 | private var hue:Float;
42 | private var hueSpeed:Float;
43 | }
--------------------------------------------------------------------------------
/textbox/effects/RotatingEffect.hx:
--------------------------------------------------------------------------------
1 | package textbox.effects;
2 |
3 | import flixel.tweens.FlxTween;
4 | import flixel.tweens.FlxTween.FlxTweenType;
5 | import flixel.tweens.FlxEase;
6 | import textbox.Text;
7 |
8 | class RotatingEffect implements IEffect
9 | {
10 |
11 | public function new()
12 | {
13 | }
14 |
15 | /**
16 | * @param easingKind - Bound between 0 and 2. Defaulting in linear if out bounds.
17 | * @param angle - Bound between 0 and 255, converted into a max rotation between 0 and 180°
18 | * @param duration - Bound between 0 and 255, converted into a duration between 200ms and 5s.
19 | * @param nthCharacter - Unused
20 | */
21 | public function reset(easingKind:Int, angle:Int, duration:Int, nthCharacter:Int):Void
22 | {
23 | var selectedEase:EaseFunction;
24 | switch (easingKind)
25 | {
26 | case 0:
27 | selectedEase = FlxEase.linear;
28 |
29 | case 1:
30 | selectedEase = FlxEase.cubeOut;
31 |
32 | case 2:
33 | selectedEase = FlxEase.bounceOut;
34 |
35 | // Add more if you feel like so.
36 | default:
37 | selectedEase = FlxEase.linear;
38 | }
39 |
40 | var mappedAngle:Float = angle/255. * 180;
41 | var mappedDuration:Float = (duration*(10-0.2))/255. + 0.2;
42 |
43 | rotationAngle = -mappedAngle;
44 |
45 | tween = FlxTween.tween
46 | (
47 | this,
48 | {rotationAngle: mappedAngle},
49 | mappedDuration,
50 | {
51 | type: FlxTweenType.PINGPONG,
52 | ease: selectedEase
53 | }
54 | );
55 | }
56 |
57 | public function update(elapsed:Float):Void
58 | {
59 | }
60 |
61 | public function apply(text:Text):Void
62 | {
63 | text.angle = rotationAngle;
64 | }
65 |
66 | public function setActive(active:Bool):Void
67 | {
68 | this.active = active;
69 | }
70 |
71 | public function isActive():Bool
72 | {
73 | return active;
74 | }
75 |
76 | private var tween:FlxTween;
77 | private var rotationAngle:Float;
78 | private var active:Bool = false;
79 | }
--------------------------------------------------------------------------------
/textbox/effects/WaveEffect.hx:
--------------------------------------------------------------------------------
1 | package textbox.effects;
2 |
3 | import flixel.tweens.FlxTween;
4 | import flixel.tweens.FlxTween.FlxTweenType;
5 | import flixel.tweens.FlxEase;
6 | import textbox.Text;
7 |
8 | class WaveEffect implements IEffect
9 | {
10 |
11 | public function new()
12 | {
13 | }
14 |
15 | /**
16 | * @param easingKind - Bound between 0 and 2. Defaulting in linear if out bounds.
17 | * @param height - Bound between 0 and 255, converted into a max offset between 0 and 30px.
18 | * @param duration - Bound between 0 and 255, converted into a duration between 200ms and 5s.
19 | * @param nthCharacter - Unused
20 | */
21 | public function reset(easingKind:Int, height:Int, speed:Int, nthCharacter:Int):Void
22 | {
23 | var selectedEase:EaseFunction;
24 | switch (easingKind)
25 | {
26 | case 0:
27 | selectedEase = FlxEase.linear;
28 |
29 | case 1:
30 | selectedEase = FlxEase.cubeOut;
31 |
32 | case 2:
33 | selectedEase = FlxEase.bounceOut;
34 |
35 | // Add more if you feel like so.
36 | default:
37 | selectedEase = FlxEase.linear;
38 | }
39 |
40 | var mappedHeight:Float = height/255. * 30;
41 | var mappedDuration:Float = (speed*(10-0.2))/255. + 0.2;
42 |
43 | offsetY = -mappedHeight;
44 |
45 | tween = FlxTween.tween
46 | (
47 | this,
48 | {offsetY: mappedHeight},
49 | mappedDuration,
50 | {
51 | type: FlxTweenType.PINGPONG,
52 | ease: selectedEase
53 | }
54 | );
55 | }
56 |
57 | public function update(elapsed:Float):Void
58 | {
59 | }
60 |
61 | public function apply(text:Text):Void
62 | {
63 | text.offset.y = offsetY;
64 | }
65 |
66 | public function setActive(active:Bool):Void
67 | {
68 | this.active = active;
69 | }
70 |
71 | public function isActive():Bool
72 | {
73 | return active;
74 | }
75 |
76 | private var tween:FlxTween;
77 | private var offsetY:Float;
78 | private var active:Bool = false;
79 | }
--------------------------------------------------------------------------------