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 }