-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathqudobject_wiki.py
438 lines (390 loc) · 17.8 KB
/
qudobject_wiki.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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
import os
import re
from typing import Union
from qbe.config import config
from hagadias.helpers import strip_oldstyle_qud_colors, strip_newstyle_qud_colors
from hagadias.qudobject_props import QudObjectProps
from qbe.helpers import displayname_to_wiki
IMAGE_OVERRIDES = config['Templates']['Image overrides']
def escape_ampersands(text: str):
"""Convert & to & for use in wiki template."""
return re.sub('&', '&', text)
class QudObjectWiki(QudObjectProps):
"""Represents a Caves of Qud game object to the wiki interface.
Inherits from QudObjectProps which provides all the derived information about the object.
Passes requests to its QudObjectProps superclass and translates the results for templates,
with HTML escaped characters, etc."""
def wiki_template(self, gamever) -> str:
"""Return the fully wikified template representing this object and add a category.
gamever is a string giving the version of Caves of Qud."""
fields = config['Templates']['Fields']
flavor = self.wiki_template_type()
before_title = '{{Qud text|'
after_title = '}}'
template = '{{' + f'{flavor}\n'
template += "| title = " + before_title + self.title + after_title + "\n"
for field in fields:
if field == 'title':
continue
if flavor == 'Corpse':
if field == 'hunger':
continue
if field == 'image':
if not (self.is_specified('part_Render_Tile')
or self.is_specified('part_Render_TileColor')
or self.is_specified('part_Render_ColorString')
or self.is_specified('part_Render_DetailColor')):
continue # uses default corpse tile/colors, so don't add image field
attrib = getattr(self, field)
if attrib is not None:
# do some final cleanup before sending to template
if field == 'renderstr':
# } character messes with mediawiki template rendering
attrib = attrib.replace('}', '}')
# replace Booleans with wiki-compatible 'yes' and 'no'
if isinstance(attrib, bool):
attrib = 'yes' if attrib else 'no'
elif isinstance(attrib, list):
attrib = ', '.join(attrib)
template += f'| {field} = {attrib}\n'
category = self.wiki_category()
if category:
template += f'| categories = {category}\n'
if gamever != 'unknown':
template += f'| gameversion = {gamever}\n'
template += '}}\n'
return template
def wiki_template_type(self) -> str:
"""Determine which template to use for the wiki."""
flavor = "Item"
not_corpses = ('Albino Ape Pelt',
'Crystal of Eve',
'Black Puma Haunch',
'Arsplice Seed',
'Albino Ape Heart',
'Ogre Ape Heart')
other_foods = ['Black Puma Haunch', 'Flowers', 'Flower']
if (self.part_Physics_Takeable == "false" or self.part_Physics_Takeable == "False") and \
self.part_Gas is None and not self.inherits_from('MeleeWeapon') and \
not self.inherits_from('NaturalWeapon') and \
not self.is_specified('part_MeleeWeapon') and \
not self.inherits_from('MissileWeapon') and \
not self.is_specified('part_MissileWeapon'):
flavor = "Character"
elif self.inherits_from('Food') or self.name in other_foods:
flavor = "Food"
elif (self.inherits_from('RobotLimb') or self.inherits_from('Corpse')) and \
(self.name not in not_corpses):
flavor = "Corpse"
return flavor
def wiki_category(self) -> str:
"""Determine what configured wiki category this object belongs in."""
cat = None
for config_cat, names in config['Wiki']['Categories'].items():
for name in names:
if self.inherits_from(name):
cat = config_cat
return cat
def wiki_namespace(self) -> str:
ns = None
for config_ns, names in config['Wiki']['Article Namespaces'].items():
for name in names:
if self.inherits_from(name):
ns = config_ns
return ns
def is_wiki_eligible(self) -> bool:
"""Return whether this object should be included in the wiki."""
if self.name == 'Argyve\'s Data Disk Encoded':
return True # special case because of '['
if self.name == 'DefaultFist':
return True # special case because this is the player's fist
if self.tag_BaseObject:
if self.name in ['ScrapCape', 'CatacombWall']:
return True # special cases, not sure why they're marked as BaseObjects
return False
if self.displayname == '' or '[' in self.displayname:
return False
eligible = True # equal to initial +Object in config.yml
for entry in config['Wiki']['Article eligibility categories']:
if entry.startswith('*') and self.inherits_from(entry[1:]):
eligible = True
elif entry.startswith('/') and self.inherits_from(entry[1:]):
eligible = False
elif entry.startswith('+') and self.name == entry[1:]:
eligible = True
elif entry.startswith('-') and self.name == entry[1:]:
eligible = False
return eligible
# PROPERTIES
# The following properties are implemented to make wiki formatting far simpler.
# Sorted alphabetically. All return types should be strings or None.
@property
def ammodamagetypes(self) -> Union[str, None]:
"""Damage attributes associated with the projectile (</br> delimited)."""
types = super().ammodamagetypes
if types is not None:
return '</br>'.join(types)
@property
def butcheredinto(self) -> Union[str, None]:
"""What a corpse item can be butchered into."""
outcomes = super().butcheredinto
if outcomes is not None:
result = ''
total_weight = 0
for outcome in outcomes:
total_weight += outcome['Weight']
for outcome in outcomes:
chance_num = 100.0 * outcome['Weight'] / total_weight
chance = ("%.1f" % chance_num).rstrip('0').rstrip('.')
result += f'{{{{corpse pop table|population={self.name}|object={{{{ID to name|' \
+ f'{outcome["Object"]}}}}}|id={outcome["Object"]}|num={outcome["Number"]}|' \
+ f'weight={outcome["Weight"]}|chance={chance}}}}}'
return result
@property
def colorstr(self) -> Union[str, None]:
"""The Qud color code associated with the RenderString."""
colorstr = super().colorstr
if colorstr is not None:
return escape_ampersands(colorstr)
@property
def commerce(self) -> Union[float, int, None]:
"""Remove trailing decimal points on values."""
value = super().commerce
if value is not None:
if self.part_Physics_Takeable is not None and self.part_Physics_Takeable == 'false':
if value == 0.01:
# ignore for non-takeable objects that inherit 0.01 from PhysicalObject
return None
return value if 0 < value < 1 else int(value)
@property
def cookeffect(self) -> Union[str, None]:
"""The possible cooking effects of an item."""
effect = super().cookeffect
if effect is not None:
return ','.join(f'{val}' for val in effect)
@property
def desc(self) -> Union[str, None]:
"""The short description of the object, with color codes included (ampersands escaped)."""
text = super().desc
if text is not None:
text = displayname_to_wiki(text)
return text
@property
def displayname(self) -> Union[str, None]:
"""The display name of the object, with color codes removed. Used in QBE UI"""
if self.name in config['Wiki']['Displayname overrides']:
dname = config['Wiki']['Displayname overrides'][self.name]
dname = strip_oldstyle_qud_colors(dname)
dname = strip_newstyle_qud_colors(dname)
else:
dname = super().displayname
return dname
@property
def dynamictable(self) -> Union[str, None]:
"""What dynamic tables the object is a member of."""
tables = super().dynamictable
if tables is not None:
return ' </br>'.join(f'{{{{Dynamic object|{table}|{self.name}}}}}' for table in tables)
@property
def eatdesc(self) -> Union[str, None]:
"""The text when you eat this item."""
text = super().eatdesc
if text is not None:
text = displayname_to_wiki(text)
return text
@property
def extra(self) -> Union[str, None]:
"""Any other features that do not have an associated variable."""
fields = []
if self.featureweightinfo == 'no': # put weight in extrainfo if it's not featured
fields.append(('weight', self.weight))
extrafields = config['Templates']['ExtraFields']
for field in extrafields:
attrib = getattr(self, field)
if attrib is not None:
# convert Booleans to wiki-compatible 'yes' and 'no'
if isinstance(attrib, bool):
attrib = 'yes' if attrib else 'no'
fields.append((field, attrib))
if len(fields) > 0:
text = ' | '.join(f'{field} = {attrib}' for field, attrib in fields)
return f'{{{{Extra info|{text}}}}}'
@property
def faction(self) -> Union[str, None]:
"""The factions this creature has loyalty to, formatted for the wiki."""
# <part Name="Brain" Wanders="false" Factions="Joppa-100,Barathrumites-100" />
factions = super().faction
if factions is not None:
template = ''
for faction, value in factions:
if template != '':
template += '</br>'
template += f'{{{{creature faction|{{{{FactionID to name|{faction}}}}}|{value}}}}}'
return template
@property
def featureweightinfo(self) -> Union[str, None]:
"""'no' if the weight should be shown as extra data. 'yes' if the weight should be
featured near the top of the wiki infobox. Weight is featured only for takeable objects
(i.e. items). For other things it plays a much less prominent role only for explosion
calculations, getting stuck in webs, etc. """
w = self.weight
if w is not None:
if self.part_Physics_Takeable is None or self.part_Physics_Takeable == 'true':
return 'yes'
else:
return 'no'
@property
def gasemitted(self) -> Union[str, None]:
"""The gas emitted by the weapon (typically missile weapon 'pumps')."""
gas = super().gasemitted
if gas is not None:
return f'{{{{ID to name|{gas}}}}}'
@property
def gif(self) -> Union[str, None]:
"""The gif image filename. On the wiki, this is used only by single-tile images with a
GIF animation. For multi-tile images, this field will be ignored in favor of
overrideimages."""
if self.has_gif_tile():
path = self.image
if path is not None and path != 'none':
return os.path.splitext(path)[0] + ' animated.gif'
@property
def image(self) -> Union[str, None]:
"""The image filename for the object's primary image. May be specified in our config.
If the object has additional alternate images, their filenames will be derived from
this one."""
if self.name in IMAGE_OVERRIDES:
return IMAGE_OVERRIDES[self.name]
elif self.has_tile():
name = self.displayname
name = re.sub(r"[^a-zA-Z\d ]", '', name)
name = name.casefold() + '.png'
return name
else:
return 'none'
@property
def inventory(self) -> Union[str, None]:
"""The inventory of a character.
Retrieves a list of tuples of strings (name, count, equipped, chance)
and renders to a template."""
inv = super().inventory
if inv is not None:
template = ''
for name, count, equipped, chance in inv:
template += f"{{{{inventory|" \
f"{name}|{count}|{equipped}|{chance}}}}}"
return template
@property
def liquidburst(self) -> Union[str, None]:
liquid = super().liquidburst
if liquid is not None:
return '{{ID to name|' + liquid + '}}'
@property
def mods(self) -> Union[str, None]:
"""Mods that are attached to the current item.
Retrieves a list of tuples of strings (modid, tier) and renders to a template.
"""
mods = super().mods
if mods is not None:
return ' </br>'.join(f'{{{{ModID to name|{mod}|{tier}}}}}' for mod, tier in mods)
@property
def movespeedbonus(self) -> Union[str, None]:
"""The movespeed bonus of an item, prefixed with a + if positive."""
bonus = super().movespeedbonus
if bonus is not None:
return '+' + str(bonus) if bonus > 0 else str(bonus)
@property
def mutations(self) -> Union[str, None]:
"""The mutations the creature has along with their level."""
mutations = super().mutations
if mutations is not None:
templates = []
ego = self.attribute_helper_avg("Ego")
for mutation, level in mutations:
mutation_entry = \
f'{{{{creature mutation|{{{{MutationID to name|{mutation}}}}}|{level}'
if ego is not None:
mutation_entry += f'|{ego}'
mutation_entry += '}}'
templates.append(mutation_entry)
return ' </br>'.join(templates)
@property
def oneat(self) -> Union[str, None]:
"""Effects granted when the object is eaten."""
effects = super().oneat
if effects is not None:
return ' </br>'.join(f'{{{{OnEat ID to name|{effect}}}}}' for effect in effects)
@property
def overrideimages(self) -> Union[str, None]:
"""A full list of images for this object, expressed as individual {{altimage}} templates
for each image (and, if applicable, for that image's corresponding GIF). This property is
returned for any object that has more than one tile variant. On the wiki, this property
is used to replace the single image that is usually shown alone for an object with an
image carousel that can rotate through all of the images for this object. """
if self.number_of_tiles() > 1:
metadata = self.tiles_and_metadata()[1]
val = '{{altimage start}}'
for meta in metadata:
val += '{{altimage'
val += f' | {meta.filename}'
if meta.is_animated():
val += f' | gif = {meta.gif_filename}'
if meta.type is not None:
val += f' | type = {meta.type}'
val += '}}'
val += '{{altimage end}}'
return val
@property
def renderstr(self) -> Union[str, None]:
"""The character used to render this object in ASCII mode."""
render = super().renderstr
if render == '}':
render = '}'
return render
@property
def reputationbonus(self) -> Union[str, None]:
"""The faction rep bonuses granted by this object."""
reps = super().reputationbonus
if reps is not None:
return ''.join(f'{{{{reputation bonus|{{{{FactionID to name|'
f'{faction}}}}}|{value}}}}}' for faction, value in reps)
@property
def skills(self) -> Union[str, None]:
"""A creature's learned skills/powers."""
skills = super().skills
if skills is not None:
return ' </br>'.join(f'{{{{SkillID to name|{skill}}}}}' for skill in skills)
@property
def title(self) -> Union[str, None]:
"""The display name of the item, with ampersands escaped."""
if self.name in config['Wiki']['Displayname overrides']:
title = config['Wiki']['Displayname overrides'][self.name]
else:
title = super().title
if title is not None:
title = displayname_to_wiki(title)
return escape_ampersands(title)
@property
def unidentifiedname(self) -> Union[str, None]:
"""The name of the object when unidentified, such as 'weird artifact'."""
name = super().unidentifiedname
if name is not None:
return displayname_to_wiki(name)
@property
def unidentifiedaltname(self) -> Union[str, None]:
"""The name of the object when partially identified, such as 'backpack'."""
altname = super().unidentifiedaltname
if altname is not None:
return displayname_to_wiki(altname)
@property
def uniquechara(self) -> Union[str, None]:
"""Whether this is a unique character, for wiki purposes."""
if self.inherits_from('Creature') or self.inherits_from('ActivePlant'):
if self.name in config['Wiki']['Categories']['Unique Characters']:
return 'yes'
@property
def weaponskill(self) -> Union[str, None]:
"""The skill that is used to wield this object as a weapon."""
skill = super().weaponskill
if skill is not None:
return f'{{{{SkillID to name|{skill}}}}}'