1 /* 2 Copyright © 2020, Luna Nielsen 3 Distributed under the 2-Clause BSD License, see LICENSE file. 4 5 Authors: Luna Nielsen 6 */ 7 module engine.vn.render.dialg; 8 import engine; 9 import std.conv; 10 import std.string : split; 11 import std.random; 12 13 private struct textInstr { 14 dstring instr; 15 dstring value; 16 } 17 18 private textInstr parseInstr(dstring txt) { 19 textInstr instr; 20 instr.instr = txt[0..2]; 21 instr.value = txt[2..$]; 22 return instr; 23 } 24 25 /** 26 Renderer for visual novel dialogue, supports slow-typing 27 */ 28 class DialogueRenderer { 29 private: 30 enum DEFAULT_TIMEOUT = 0.045; 31 32 dstring current; 33 size_t rendOffset = 0; 34 35 double accum = 0; 36 double timeout = DEFAULT_TIMEOUT; 37 38 double sleep = 0; 39 40 bool requestHide = false; 41 42 public: 43 44 /** 45 Resets text speed 46 */ 47 void resetSpeed() { 48 timeout = DEFAULT_TIMEOUT; 49 } 50 51 /** 52 Gets the current text buffer being rendered 53 */ 54 dstring currentTextBuffer() { 55 return current; 56 } 57 58 /** 59 Whether the text requested for the dialogue to be hidden 60 */ 61 bool isHidingRequested() { 62 return requestHide; 63 } 64 65 /** 66 Skip to end of dialogue 67 */ 68 void skip() { 69 if (!done) { 70 rendOffset = current.length; 71 resetSpeed(); 72 } 73 } 74 75 /** 76 Gets whether the renderer is empty 77 */ 78 bool empty() { 79 return current.length == 0; 80 } 81 82 /** 83 Gets whether this line is done 84 */ 85 bool done() { 86 return rendOffset == current.length; 87 } 88 89 /** 90 Push text for rendering 91 */ 92 void pushText(dstring text) { 93 current = text; 94 rendOffset = 0; 95 accum = 0; 96 } 97 98 void update() { 99 100 101 // Don't try to uupdate empty text 102 if (empty) return; 103 104 // Accumulator 105 accum += 1*deltaTime; 106 107 // Handle sleeping 108 if (sleep > 0) { 109 if (accum >= sleep) { 110 sleep = 0; 111 accum = 0; 112 while(!done && current[rendOffset] != ']') rendOffset++; 113 rendOffset++; 114 } 115 else return; 116 } 117 118 // If we're past the end for some reason, fix it 119 120 // Handle slowtyping text 121 if (accum >= timeout) { 122 accum -= timeout; 123 if (!done) { 124 if (current[rendOffset] == '\n') rendOffset++; 125 126 size_t i = rendOffset; 127 128 // Parse time changes 129 while (i < current.length-3 && current[i] == '[' && current[i+1] == '&') { 130 i += 2; 131 132 while(i < current.length && current[i] != ']') i++; 133 i++; 134 135 // Get the instruction 136 textInstr instr = parseInstr(current[rendOffset+2..i-1]); 137 switch(instr.instr) { 138 case "tm"d: 139 if (instr.value == "clear") { 140 timeout = DEFAULT_TIMEOUT; 141 break; 142 } 143 timeout = parse!double(instr.value); 144 break; 145 case "sl"d: 146 sleep = parse!double(instr.value); 147 return; 148 case "rh"d: 149 requestHide = true; 150 break; 151 case "rs"d: 152 requestHide = false; 153 break; 154 default: break; 155 } 156 } 157 158 // Extra increment 159 if (i < current.length) i++; 160 rendOffset = i; 161 } 162 } 163 } 164 165 void draw(vec2 at, int size = 48) { 166 167 // Don't try to render empty text 168 if (empty) return; 169 170 // Always flush drawing when done 171 scope(exit) GameFont.flush(); 172 173 // Setup 174 GameFont.changeSize(size); 175 vec2 metrics = GameFont.getMetrics(); 176 at += vec2(metrics.x/2, metrics.y/2);; 177 vec2 cursor = at; 178 int shake = 0; 179 int wave = 0; 180 int waveSpeed = 5; 181 bool waveRot = false; 182 vec4 color = vec4(1, 1, 1, 1); 183 for(int i = 0; i < rendOffset; i++) { 184 185 // Values for this iteration 186 dchar c = current[i]; 187 vec2 advance = GameFont.advance(c); 188 189 // Parse mode changes 190 if (i < current.length-3) { 191 while (current[i] == '[' && current[i+1] == '&') { 192 i += 2; 193 194 // Go till end of instruction 195 size_t startOffset = i; 196 while(i != current.length-1 && current[i] != ']') i++; 197 i++; 198 199 // Get the instruction 200 textInstr instr = parseInstr(current[startOffset..i-1]); 201 switch(instr.instr) { 202 case "cl"d: 203 204 // Handle clear 205 if (instr.value == "clear") { 206 color = vec4(1, 1, 1, 1); 207 break; 208 } 209 210 // Try to figure out color 211 dstring[] vals = instr.value.split(','); 212 if (vals.length != 3) break; 213 214 // Parse colors 215 color.r = parse!float(vals[0]); 216 color.g = parse!float(vals[1]); 217 color.b = parse!float(vals[2]); 218 break; 219 220 case "wv"d: 221 // Handle clear 222 if (instr.value == "clear") { 223 wave = 0; 224 break; 225 } 226 227 shake = 0; 228 wave = parse!int(instr.value); 229 break; 230 231 case "wr"d: 232 waveRot = parse!bool(instr.value); 233 break; 234 235 case "ws"d: 236 // Handle clear 237 if (instr.value == "clear") { 238 waveSpeed = 5; 239 break; 240 } 241 242 waveSpeed = parse!int(instr.value); 243 break; 244 245 case "sh"d: 246 // Handle clear 247 if (instr.value == "clear") { 248 shake = 0; 249 break; 250 } 251 252 wave = 0; 253 shake = parse!int(instr.value); 254 break; 255 256 default: break; 257 } 258 259 if (i < current.length) { 260 c = current[i]; 261 advance = GameFont.advance(c); 262 } else return; 263 } 264 } 265 266 // Handle newlines 267 if (c == '\n') { 268 cursor.x = at.x; 269 cursor.y += metrics.y; 270 continue; 271 } 272 273 // Skip all the parsing stuff for whitespace 274 if (c == ' ') { 275 cursor.x += advance.x; 276 continue; 277 } 278 279 vec2 lCursor = cursor; 280 float rot = 0; 281 282 if (wave > 0) { 283 cursor.y += sin((currTime()+cast(float)i)*waveSpeed)*wave; 284 if (waveRot) rot += sin((currTime()+cast(float)i)*waveSpeed)*(0.015*wave); 285 } 286 if (shake > 0) { 287 cursor.x += ((uniform01()*2)-1)*shake; 288 cursor.y += ((uniform01()*2)-1)*shake; 289 } 290 291 292 // Draw font 293 GameFont.draw(c, cursor, vec2(advance.x/2, metrics.y/2), rot, color); 294 cursor = lCursor; 295 cursor.x += advance.x; 296 } 297 } 298 }