-
Notifications
You must be signed in to change notification settings - Fork 365
/
TextBox.py
276 lines (227 loc) · 9.72 KB
/
TextBox.py
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
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import Messages
# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
# characters on a line reach this value.
LINE_WIDTH = 1801800
# Attempting to display more lines in a single text box will cause additional lines to bleed past the bottom of the box.
LINES_PER_BOX = 4
# Attempting to display more characters in a single text box will cause buffer overflows. First, visual artifacts will
# appear in lower areas of the text box. Eventually, the text box will become uncloseable.
MAX_CHARACTERS_PER_BOX = 200
LINE_BREAK = '&'
BOX_BREAK = '^'
def lineWrap(text):
boxes = text.split(BOX_BREAK)
boxesWithWrappedLines = []
for box in boxes:
forcedLines = box.split(LINE_BREAK)
lines = [line.strip() for forcedLine in forcedLines for line in _wrapLines(forcedLine)]
wrapped = LINE_BREAK.join(lines)
if len(lines) > LINES_PER_BOX:
print('Wrapped text exceeds maximum lines per text box. Original text:\n' + box)
# Subtracting line count so that line breaks aren't counted as characters
if len(wrapped) - (len(lines) - 1) > MAX_CHARACTERS_PER_BOX:
print('Text length exceeds maximum characters per text box. Original text:\n' + box)
boxesWithWrappedLines.append(wrapped)
return BOX_BREAK.join(boxesWithWrappedLines)
def _wrapLines(text):
lines = []
currentLine = []
currentWidth = 0
for word in text.split(' '):
currentLinePlusWord = currentLine.copy()
currentLinePlusWord.append(word)
currentLinePlusWordWidth = _calculateWidth(currentLinePlusWord)
if (currentLinePlusWordWidth <= LINE_WIDTH):
currentLine = currentLinePlusWord
currentWidth = currentLinePlusWordWidth
else:
lines.append(' '.join(currentLine))
currentLine = [word]
currentWidth = _calculateWidth(currentLine)
lines.append(' '.join(currentLine))
return lines
def _calculateWidth(words):
wordsWidth = 0
for word in words:
index = 0
while index < len(word):
character = word[index]
index += 1
if ord(character) in Messages.CONTROL_CODES:
index += Messages.CONTROL_CODES[ord(character)][1]
else:
wordsWidth += _getCharacterWidth(character)
spacesWidth = _getCharacterWidth(' ') * (len(words) - 1)
return wordsWidth + spacesWidth
def _getCharacterWidth(character):
try:
return characterTable[character]
except KeyError:
# Control character for color settings; does not affect the width
if character == '#':
return 0
# Control character for displaying the player's name; assume greater than average width
elif character == '@':
return characterTable['M'] * 8
# A sane default with the most common character width
else :
return characterTable[' ']
# Tediously measured by filling a full line of a gossip stone's text box with one character until it is reasonably full
# (with a right margin) and counting how many characters fit. OoT does not appear to use any kerning, but, if it does,
# it will only make the characters more space-efficient, so this is an underestimate of the number of letters per line,
# at worst. This ensures that we will never bleed text out of the text box while line wrapping.
# Larger numbers in the denominator mean more of that character fits on a line; conversely, larger values in this table
# mean the character is wider and can't fit as many on one line.
characterTable = {
'a': 51480, # LINE_WIDTH / 35
'b': 51480, # LINE_WIDTH / 35
'c': 51480, # LINE_WIDTH / 35
'd': 51480, # LINE_WIDTH / 35
'e': 51480, # LINE_WIDTH / 35
'f': 34650, # LINE_WIDTH / 52
'g': 51480, # LINE_WIDTH / 35
'h': 51480, # LINE_WIDTH / 35
'i': 25740, # LINE_WIDTH / 70
'j': 34650, # LINE_WIDTH / 52
'k': 51480, # LINE_WIDTH / 35
'l': 25740, # LINE_WIDTH / 70
'm': 81900, # LINE_WIDTH / 22
'n': 51480, # LINE_WIDTH / 35
'o': 51480, # LINE_WIDTH / 35
'p': 51480, # LINE_WIDTH / 35
'q': 51480, # LINE_WIDTH / 35
'r': 42900, # LINE_WIDTH / 42
's': 51480, # LINE_WIDTH / 35
't': 42900, # LINE_WIDTH / 42
'u': 51480, # LINE_WIDTH / 35
'v': 51480, # LINE_WIDTH / 35
'w': 81900, # LINE_WIDTH / 22
'x': 51480, # LINE_WIDTH / 35
'y': 51480, # LINE_WIDTH / 35
'z': 51480, # LINE_WIDTH / 35
'A': 81900, # LINE_WIDTH / 22
'B': 51480, # LINE_WIDTH / 35
'C': 72072, # LINE_WIDTH / 25
'D': 72072, # LINE_WIDTH / 25
'E': 51480, # LINE_WIDTH / 35
'F': 51480, # LINE_WIDTH / 35
'G': 81900, # LINE_WIDTH / 22
'H': 60060, # LINE_WIDTH / 30
'I': 25740, # LINE_WIDTH / 70
'J': 51480, # LINE_WIDTH / 35
'K': 60060, # LINE_WIDTH / 30
'L': 51480, # LINE_WIDTH / 35
'M': 81900, # LINE_WIDTH / 22
'N': 72072, # LINE_WIDTH / 25
'O': 81900, # LINE_WIDTH / 22
'P': 51480, # LINE_WIDTH / 35
'Q': 81900, # LINE_WIDTH / 22
'R': 60060, # LINE_WIDTH / 30
'S': 60060, # LINE_WIDTH / 30
'T': 51480, # LINE_WIDTH / 35
'U': 60060, # LINE_WIDTH / 30
'V': 72072, # LINE_WIDTH / 25
'W': 100100, # LINE_WIDTH / 18
'X': 72072, # LINE_WIDTH / 25
'Y': 60060, # LINE_WIDTH / 30
'Z': 60060, # LINE_WIDTH / 30
' ': 51480, # LINE_WIDTH / 35
'1': 25740, # LINE_WIDTH / 70
'2': 51480, # LINE_WIDTH / 35
'3': 51480, # LINE_WIDTH / 35
'4': 60060, # LINE_WIDTH / 30
'5': 51480, # LINE_WIDTH / 35
'6': 51480, # LINE_WIDTH / 35
'7': 51480, # LINE_WIDTH / 35
'8': 51480, # LINE_WIDTH / 35
'9': 51480, # LINE_WIDTH / 35
'0': 60060, # LINE_WIDTH / 30
'!': 51480, # LINE_WIDTH / 35
'?': 72072, # LINE_WIDTH / 25
'\'': 17325, # LINE_WIDTH / 104
'"': 34650, # LINE_WIDTH / 52
'.': 25740, # LINE_WIDTH / 70
',': 25740, # LINE_WIDTH / 70
'/': 51480, # LINE_WIDTH / 35
'-': 34650, # LINE_WIDTH / 52
'_': 51480, # LINE_WIDTH / 35
'(': 42900, # LINE_WIDTH / 42
')': 42900, # LINE_WIDTH / 42
'$': 51480 # LINE_WIDTH / 35
}
# To run tests, enter the following into a python3 REPL:
# >>> from TextBox import test_lineWrapTests
# >>> test_lineWrapTests()
def test_lineWrapTests():
test_wrapSimpleLine()
test_honorForcedLineWraps()
test_honorBoxBreaks()
test_honorControlCharacters()
test_honorPlayerName()
test_maintainMultipleForcedBreaks()
test_trimWhitespace()
test_supportLongWords()
def test_wrapSimpleLine():
words = 'Hello World! Hello World! Hello World!'
expected = 'Hello World! Hello World! Hello&World!'
result = lineWrap(words)
if result != expected:
print('"Wrap Simple Line" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Wrap Simple Line" test passed!')
def test_honorForcedLineWraps():
words = 'Hello World! Hello World!&Hello World! Hello World! Hello World!'
expected = 'Hello World! Hello World!&Hello World! Hello World! Hello&World!'
result = lineWrap(words)
if result != expected:
print('"Honor Forced Line Wraps" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Honor Forced Line Wraps" test passed!')
def test_honorBoxBreaks():
words = 'Hello World! Hello World!^Hello World! Hello World! Hello World!'
expected = 'Hello World! Hello World!^Hello World! Hello World! Hello&World!'
result = lineWrap(words)
if result != expected:
print('"Honor Box Breaks" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Honor Box Breaks" test passed!')
def test_honorControlCharacters():
words = 'Hello World! #Hello# World! Hello World!'
expected = 'Hello World! #Hello# World! Hello&World!'
result = lineWrap(words)
if result != expected:
print('"Honor Control Characters" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Honor Control Characters" test passed!')
def test_honorPlayerName():
words = 'Hello @! Hello World! Hello World!'
expected = 'Hello @! Hello World!&Hello World!'
result = lineWrap(words)
if result != expected:
print('"Honor Player Name" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Honor Player Name" test passed!')
def test_maintainMultipleForcedBreaks():
words = 'Hello World!&&&Hello World!'
expected = 'Hello World!&&&Hello World!'
result = lineWrap(words)
if result != expected:
print('"Maintain Multiple Forced Breaks" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Maintain Multiple Forced Breaks" test passed!')
def test_trimWhitespace():
words = 'Hello World! & Hello World!'
expected = 'Hello World!&Hello World!'
result = lineWrap(words)
if result != expected:
print('"Trim Whitespace" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Trim Whitespace" test passed!')
def test_supportLongWords():
words = 'Hello World! WWWWWWWWWWWWWWWWWWWW Hello World!'
expected = 'Hello World!&WWWWWWWWWWWWWWWWWWWW&Hello World!'
result = lineWrap(words)
if result != expected:
print('"Support Long Words" test failed: Got ' + result + ', wanted ' + expected)
else:
print('"Support Long Words" test passed!')