| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- 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;
- }
- }
|