ifmapper 1.0.0 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. data/HISTORY.txt +648 -627
  2. data/IFMapper.gemspec +29 -28
  3. data/IFMapper.rbw +31 -31
  4. data/TODO.txt +8 -7
  5. data/bin/IFMapper +31 -31
  6. data/docs/en/index.html +0 -0
  7. data/docs/en/start.html +3 -2
  8. data/docs/en/start.html~ +516 -0
  9. data/docs/es/index.html +0 -0
  10. data/docs/es/start.html +13 -14
  11. data/docs/es/start.html~ +1280 -0
  12. data/docs/images/IFMapper_main.gif +0 -0
  13. data/docs/images/automap.gif +0 -0
  14. data/docs/images/complex_connection.gif +0 -0
  15. data/docs/images/connection.gif +0 -0
  16. data/docs/images/connection_menu.gif +0 -0
  17. data/docs/images/room_description.gif +0 -0
  18. data/docs/images/room_small.gif +0 -0
  19. data/icons/copy.png +0 -0
  20. data/icons/cut.png +0 -0
  21. data/icons/filenew.png +0 -0
  22. data/icons/fileopen.png +0 -0
  23. data/icons/filesave.png +0 -0
  24. data/icons/filesaveas.png +0 -0
  25. data/icons/help.png +0 -0
  26. data/icons/kill.png +0 -0
  27. data/icons/nextpage.png +0 -0
  28. data/icons/paste.png +0 -0
  29. data/icons/prevpage.png +0 -0
  30. data/icons/printicon.png +0 -0
  31. data/icons/redo.png +0 -0
  32. data/icons/room_e.gif +0 -0
  33. data/icons/room_e.xpm +0 -0
  34. data/icons/room_n.gif +0 -0
  35. data/icons/room_n.xpm +0 -0
  36. data/icons/room_ne.gif +0 -0
  37. data/icons/room_ne.xpm +0 -0
  38. data/icons/room_nw.gif +0 -0
  39. data/icons/room_nw.xpm +0 -0
  40. data/icons/room_s.gif +0 -0
  41. data/icons/room_s.xpm +0 -0
  42. data/icons/room_se.gif +0 -0
  43. data/icons/room_se.xpm +0 -0
  44. data/icons/room_sw.gif +0 -0
  45. data/icons/room_sw.xpm +0 -0
  46. data/icons/room_w.gif +0 -0
  47. data/icons/room_w.xpm +0 -0
  48. data/icons/saveas.png +0 -0
  49. data/icons/undo.png +0 -0
  50. data/icons/winapp.png +0 -0
  51. data/icons/zoom.png +0 -0
  52. data/lib/IFMapper/AStar.rb +250 -250
  53. data/lib/IFMapper/Connection.rb +202 -202
  54. data/lib/IFMapper/FXAboutDialogBox.rb +32 -32
  55. data/lib/IFMapper/FXConnection.rb +364 -364
  56. data/lib/IFMapper/FXConnectionDialogBox.rb +124 -124
  57. data/lib/IFMapper/FXDCPostscript.rb +404 -404
  58. data/lib/IFMapper/FXDCPrint.rb +15 -15
  59. data/lib/IFMapper/FXItemList.rb +108 -0
  60. data/lib/IFMapper/FXMap.rb +2147 -2116
  61. data/lib/IFMapper/FXMapColorBox.rb +88 -88
  62. data/lib/IFMapper/FXMapDialogBox.rb +127 -127
  63. data/lib/IFMapper/FXMapFileDialog.rb +34 -34
  64. data/lib/IFMapper/FXMapperSettings.rb +206 -205
  65. data/lib/IFMapper/FXMapperWindow.rb +1592 -1571
  66. data/lib/IFMapper/FXPDFMapExporterOptionsDialogBox.rb +46 -0
  67. data/lib/IFMapper/FXRoom.rb +263 -263
  68. data/lib/IFMapper/FXRoomDialogBox.rb +159 -159
  69. data/lib/IFMapper/FXRoomList.rb +95 -95
  70. data/lib/IFMapper/FXSearchDialogBox.rb +51 -51
  71. data/lib/IFMapper/FXSection.rb +33 -33
  72. data/lib/IFMapper/FXSectionDialogBox.rb +38 -38
  73. data/lib/IFMapper/FXSpline.rb +52 -52
  74. data/lib/IFMapper/FXWarningBox.rb +51 -50
  75. data/lib/IFMapper/GUEReader.rb +445 -445
  76. data/lib/IFMapper/IFMReader.rb +584 -584
  77. data/lib/IFMapper/IFMWriter.rb +245 -227
  78. data/lib/IFMapper/Inform7Writer.rb +579 -573
  79. data/lib/IFMapper/InformReader.rb +478 -478
  80. data/lib/IFMapper/InformWriter.rb +364 -359
  81. data/lib/IFMapper/Map.rb +202 -200
  82. data/lib/IFMapper/MapPrinting.rb +162 -162
  83. data/lib/IFMapper/MapReader.rb +900 -900
  84. data/lib/IFMapper/PDFMapExporter.rb +526 -483
  85. data/lib/IFMapper/Room.rb +153 -151
  86. data/lib/IFMapper/Section.rb +234 -234
  87. data/lib/IFMapper/TADSReader.rb +474 -471
  88. data/lib/IFMapper/TADSWriter.rb +375 -370
  89. data/lib/IFMapper/TranscriptDialogBox.rb +0 -0
  90. data/lib/IFMapper/TranscriptReader.rb +1361 -1359
  91. data/lib/IFMapper/locales/en/Messages.rb +446 -435
  92. data/lib/IFMapper/locales/es/Messages.rb +451 -440
  93. data/lib/IFMapper/locales/es/Messages_iso-8859-1.rb +455 -440
  94. data/lib/IFMapper/locales/es/runme.sh +3 -3
  95. data/maps/A New Life.map b/data/maps/A New → Life.map +0 -0
  96. data/maps/AMFV.map +0 -0
  97. data/maps/AllRoads.map +0 -0
  98. data/maps/Aotearoa.map +0 -0
  99. data/maps/Bronze.map +0 -0
  100. data/maps/Bureaucracy.ifm +0 -0
  101. data/maps/Bureaucracy.map +0 -0
  102. data/maps/CityOfSecrets.map +0 -0
  103. data/maps/DDIV.map +0 -0
  104. data/maps/Following_A_Star.map +0 -0
  105. data/maps/Heated.map +0 -0
  106. data/maps/Heroine.map +0 -0
  107. data/maps/History Repeating.map b/data/maps/History → Repeating.map +0 -0
  108. data/maps/Hollywood_Hijinx.ifm +0 -0
  109. data/maps/Janitor.map +0 -0
  110. data/maps/Jigsaw.ifm +0 -0
  111. data/maps/Jigsaw.map +0 -0
  112. data/maps/LGOP.ifm +0 -0
  113. data/maps/Mercy.ifm +0 -0
  114. data/maps/Ninjas_Fate.map +0 -0
  115. data/maps/Pen_and_Paint.map +0 -0
  116. data/maps/Planetfall.ifm +0 -0
  117. data/maps/Planetfall.map +0 -0
  118. data/maps/Plundered_Hearts.ifm +0 -0
  119. data/maps/QuietEvening.map +0 -0
  120. data/maps/Ralph.ifm +0 -0
  121. data/maps/Reliques_of_Tolti_Alph.map +0 -0
  122. data/maps/Revolution.map +0 -0
  123. data/maps/Robots_of_Dawn.ifm +0 -0
  124. data/maps/SavoirFare.map +0 -0
  125. data/maps/Seastalker.ifm +0 -0
  126. data/maps/Seastalker.map +0 -0
  127. data/maps/Sherlock.ifm +0 -0
  128. data/maps/SoFar.ifm +0 -0
  129. data/maps/Starcross.ifm +0 -0
  130. data/maps/Suspended.ifm +0 -0
  131. data/maps/Tangle.map +0 -0
  132. data/maps/The_Lost_Sheep.map +0 -0
  133. data/maps/Unforgotten.map +0 -0
  134. data/maps/Warbler's Nest.map +0 -0
  135. data/maps/Warbler's_Nest.map +0 -0
  136. data/maps/Westminster_Abbey.map +0 -0
  137. data/maps/WinterWonderland.map +0 -0
  138. data/maps/Wishbringer.ifm +0 -0
  139. data/maps/Wishbringer2.ifm +0 -0
  140. data/maps/Zork1.ifm +0 -0
  141. data/maps/Zork2.ifm +0 -0
  142. data/maps/Zork3.ifm +0 -0
  143. data/maps/Zork_Zero.ifm +0 -0
  144. data/maps/anchor.ifm +0 -0
  145. data/maps/anchor.map +0 -0
  146. data/maps/atrox.ifm +0 -0
  147. data/maps/awaken.ifm +0 -0
  148. data/maps/babel.ifm +0 -0
  149. data/maps/balances.map +0 -0
  150. data/maps/ballerina.map +0 -0
  151. data/maps/bear.map +0 -0
  152. data/maps/bluechairs.map +0 -0
  153. data/maps/break_in.map +0 -0
  154. data/maps/bse.ifm +0 -0
  155. data/maps/building.map +0 -0
  156. data/maps/change.ifm +0 -0
  157. data/maps/christminster.map +0 -0
  158. data/maps/curses.ifm +0 -0
  159. data/maps/curves.ifm +0 -0
  160. data/maps/deadline.map +0 -0
  161. data/maps/delusions.map +0 -0
  162. data/maps/devours.map +0 -0
  163. data/maps/distress.map +0 -0
  164. data/maps/djinni.map +0 -0
  165. data/maps/dreamhold.map +0 -0
  166. data/maps/drift3.map +0 -0
  167. data/maps/eas.map +0 -0
  168. data/maps/eas2.map +0 -0
  169. data/maps/eas3.map +0 -0
  170. data/maps/edifice.ifm +0 -0
  171. data/maps/fallacy.map +0 -0
  172. data/maps/frozen.ifm +0 -0
  173. data/maps/gamlet.map +0 -0
  174. data/maps/glow.ifm +0 -0
  175. data/maps/guilty_bastards.map +0 -0
  176. data/maps/heist.map +0 -0
  177. data/maps/heroes.map +0 -0
  178. data/maps/inhumane.map +0 -0
  179. data/maps/kaged.map +0 -0
  180. data/maps/library.ifm +0 -0
  181. data/maps/lurkinghorror.map +0 -0
  182. data/maps/metamorphoses.map +0 -0
  183. data/maps/mindelec.ifm +0 -0
  184. data/maps/minster.ifm +0 -0
  185. data/maps/mite.map +0 -0
  186. data/maps/moonmist.map +0 -0
  187. data/maps/muldoon_legacy.map +0 -0
  188. data/maps/muse.ifm +0 -0
  189. data/maps/paperchase.ifm +0 -0
  190. data/maps/party.map +0 -0
  191. data/maps/pawn.map +0 -0
  192. data/maps/photograph.map +0 -0
  193. data/maps/pkgirl.map +0 -0
  194. data/maps/pytho.map +0 -0
  195. data/maps/risorgimento.map +0 -0
  196. data/maps/sherbet.map +0 -0
  197. data/maps/simple.map +0 -0
  198. data/maps/slouch.map +0 -0
  199. data/maps/space_st.ifm +0 -0
  200. data/maps/splashdown.map +0 -0
  201. data/maps/spring.map +0 -0
  202. data/maps/squarecircle.map +0 -0
  203. data/maps/stationfall.ifm +0 -0
  204. data/maps/theatre.ifm +0 -0
  205. data/maps/toonesia.ifm +0 -0
  206. data/maps/tortoise.ifm +0 -0
  207. data/maps/trinity.map +0 -0
  208. data/maps/vespers.map +0 -0
  209. data/maps/vgame.ifm +0 -0
  210. data/maps/wasp.map +0 -0
  211. data/maps/weather.ifm +0 -0
  212. data/maps/windhall.ifm +0 -0
  213. data/maps/worlds.map +0 -0
  214. data/maps/xtcontest.map +0 -0
  215. data/maps/zdungeon.map +0 -0
  216. data/maps/zebulon.ifm +0 -0
  217. data/maps/zerosum.map +0 -0
  218. metadata +226 -183
@@ -1,573 +1,579 @@
1
-
2
-
3
- class Inform7Writer
4
-
5
-
6
- DIRECTIONS = [
7
- 'North',
8
- 'Northeast',
9
- 'East',
10
- 'Southeast',
11
- 'South',
12
- 'Southwest',
13
- 'West',
14
- 'Northwest',
15
- ]
16
-
17
- OTHERDIRS = [
18
- '',
19
- 'Above',
20
- 'Below',
21
- 'Inside',
22
- 'Outside',
23
- ]
24
-
25
- IGNORE_WORDS = [
26
- 'a', 'the', 'and', 'of', 'your', 'to', 'out', 'in'
27
- ]
28
-
29
- IGNORED_ARTICLES = /^(?:#{IGNORE_WORDS.join('|')})$/
30
-
31
-
32
-
33
- KEYWORDS = [
34
- 'include',
35
- 'has',
36
- 'with',
37
- 'is',
38
- 'in',
39
- 'container',
40
- 'to',
41
- 'and',
42
- 'side',
43
- '\s*,\s*',
44
- '\s*\(\s*',
45
- ] + DIRECTIONS + OTHERDIRS[1..-1]
46
-
47
- INVALID_KEYWORD = /\b(?:#{KEYWORDS.join('|')})\b/i
48
-
49
-
50
- LOCATION_NAMES = [
51
- 'door',
52
- 'include',
53
- 'room',
54
- 'has',
55
- 'with',
56
- 'is',
57
- 'container',
58
- ] + DIRECTIONS
59
- INVALID_LOCATION_NAME = /^(?:#{LOCATION_NAMES.join('|')})$/i
60
-
61
-
62
- #
63
- # Some common animals in adventure games
64
- #
65
- ANIMALS = [
66
- 'horse',
67
- 'donkey',
68
- 'mule',
69
- 'goat',
70
- # lizards
71
- 'lizard',
72
- 'snake',
73
- 'turtle',
74
- # fishes
75
- 'fish',
76
- 'dolphin',
77
- 'whale',
78
- 'tortoise',
79
- # dogs
80
- 'dog',
81
- 'wolf',
82
- # cats
83
- 'cat',
84
- 'leopard',
85
- 'tiger',
86
- 'lion',
87
- # fantastic
88
- 'grue',
89
- # birds
90
- 'bird',
91
- 'pigeon',
92
- 'peacock',
93
- 'eagle',
94
- ]
95
-
96
- IS_ANIMAL = /\b(?:#{ANIMALS.join('|')})\b/i
97
-
98
- MALE_PEOPLE = [
99
- 'boy',
100
- 'man',
101
- 'sir',
102
- ]
103
- IS_MALE_PERSON = /\b(?:#{MALE_PEOPLE.join('|')})\b/i
104
-
105
- FEMALE_PEOPLE = [
106
- 'girl',
107
- 'woman',
108
- 'lady',
109
- 'actress',
110
- ]
111
- IS_FEMALE_PERSON = /\b(?:#{FEMALE_PEOPLE.join('|')})\b/i
112
-
113
- PEOPLE = MALE_PEOPLE + FEMALE_PEOPLE + [
114
- 'child',
115
- 'attendant',
116
- 'doctor',
117
- 'actor',
118
- 'character',
119
- 'engineer',
120
- 'bum',
121
- 'nerd',
122
- 'ghost',
123
- ]
124
-
125
- IS_PERSON = /\b(?:#{PEOPLE.join('|')})\b/i
126
-
127
- #
128
- # Some common container types
129
- #
130
- CONTAINERS = [
131
- 'box',
132
- 'cup',
133
- 'crate',
134
- 'tin',
135
- 'can',
136
- 'chest',
137
- 'wardrobe',
138
- 'jacket',
139
- 'suit',
140
- 'trophy case',
141
- 'coffin',
142
- 'briefcase',
143
- 'suitcase',
144
- 'bag',
145
- 'flask',
146
- ]
147
- IS_CONTAINER = /\b(?:#{CONTAINERS.join('|')})\b/i
148
-
149
- #
150
- # Some common lit types
151
- #
152
- UNLIT_TYPES = [
153
- 'lantern',
154
- 'torch',
155
- ]
156
- IS_LIGHT = /\b(?:#{UNLIT_TYPES.join('|')})\b/i
157
-
158
- #
159
- # Some common wearable types
160
- #
161
- CLOTHES = [
162
- 'cape',
163
- 'glove',
164
- 'jeans',
165
- 'trousers',
166
- 'hat',
167
- 'gown',
168
- 'backpack',
169
- 'shirt',
170
- 'jacket',
171
- 'suit',
172
- 'ring',
173
- 'bracelet',
174
- 'amulet',
175
- 'locket',
176
- ]
177
- IS_WEARABLE = /\b(?:#{CLOTHES.join('|')})\b/i
178
-
179
- #
180
- # Some common supporter types
181
- #
182
- SUPPORTERS = [
183
- 'table',
184
- 'shelf',
185
- ]
186
- IS_SUPPORTER = /\b(?:#{SUPPORTERS.join('|')})\b/i
187
-
188
- #
189
- # Some common edible types
190
- #
191
- FOOD = [
192
- 'bread',
193
- 'cake',
194
- # drinks
195
- 'water',
196
- 'soda',
197
- 'beer',
198
- 'beverage',
199
- 'potion',
200
- # fruits
201
- 'fruit',
202
- 'apple',
203
- 'orange',
204
- 'banana',
205
- 'almond',
206
- 'nut',
207
- ]
208
- IS_EDIBLE = /\b(?:#{FOOD.join('|')})\b/i
209
-
210
- def new_tag(elem, str)
211
- tag = str.dup
212
-
213
- # Take text from Unicode utf-8 to iso-8859-1
214
- utf = Iconv.new( 'iso-8859-1', 'utf-8' )
215
- tag = utf.iconv( tag )
216
-
217
- # Remove redundant spaces
218
- tag.sub!(/^\s/, '')
219
- tag.sub!(/\s$/, '')
220
-
221
- # Invalid tag characters, replaced with _
222
- tag.gsub!(/[\s"\\\#\,\.:;!\?\n\(\)]+/,'_')
223
- tag.sub!(/^([\d]+)_?(.*)/, '\2\1') # No numbers allowed at start of tag
224
-
225
- tag.gsub!(/__/, '_')
226
-
227
- # tag cannot be repeated and cannot be keyword (Doorway, Room, etc)
228
- # In those cases, we add a number to the tag name.
229
- idx = 0
230
- if @tags.values.include?(tag) or tag =~ INVALID_LOCATION_NAME
231
- idx = 1
232
- end
233
-
234
-
235
-
236
- if idx > 0
237
- root = tag.dup
238
- tag = "#{root}#{idx}"
239
- while @tags.values.include?(tag)
240
- tag = "#{root}#{idx}"
241
- idx += 1
242
- end
243
- end
244
-
245
-
246
- if elem.kind_of?(String)
247
- @tags[tag] = tag
248
- else
249
- @tags[elem] = tag
250
- end
251
- return tag
252
- end
253
-
254
- def get_tag(elem, name)
255
- return @tags[elem] if @tags[elem]
256
- tag = name
257
- if tag =~ INVALID_KEYWORD or @tags.values.include?(tag)
258
- return new_tag(elem, name)
259
- else
260
- @tags[elem] = tag
261
- end
262
- return tag
263
- end
264
-
265
- def get_room_tag(elem)
266
- return @tags[elem] if @tags[elem]
267
- tag = elem.name
268
- if tag =~ INVALID_KEYWORD or @tags.values.include?(tag)
269
- tag = new_tag(elem, elem.name)
270
- else
271
- @tags[elem] = tag
272
- end
273
- return tag
274
- end
275
-
276
- def get_door_name(e)
277
- dirA = e.roomA.exits.index(e)
278
- dirB = e.roomB.exits.rindex(e)
279
- name = DIRECTIONS[dirA].downcase + "-" + DIRECTIONS[dirB].downcase
280
- name << " door"
281
- end
282
-
283
- def get_door_tag(e)
284
- name = get_door_name(e)
285
- name.upcase
286
- get_tag(e, name)
287
- end
288
-
289
- def wrap_text(text, width = 75, indent = 78 - width)
290
- return 'UNDER CONSTRUCTION.' if not text or text == ''
291
- str = inform_quote( text )
292
-
293
- if str.size > width
294
- r = ''
295
- while str
296
- if str.size >= width
297
- idx = str.rindex(/[ -]/, width)
298
- idx = str.size unless idx
299
- else
300
- idx = str.size
301
- end
302
- r << str[0..idx]
303
- str = str[idx+1..-1]
304
- r << "\n" << ' ' * indent if str
305
- end
306
- return r
307
- else
308
- return str
309
- end
310
- end
311
-
312
-
313
- #
314
- # Take a text and quote it for inform's double-quote text areas.
315
- #
316
- def inform_quote(text)
317
- str = text.dup
318
-
319
- # Take text from Unicode utf-8 to iso-8859-1
320
- utf = Iconv.new( 'iso-8859-1', 'utf-8' )
321
- str = utf.iconv( str )
322
-
323
- # Quote special characters
324
- # str.gsub!(/@/, '@@64')
325
- str.gsub!(/"/, '\'')
326
- # str.gsub!(/~/, '@@126')
327
- # str.gsub!(/\\/, '@@92')
328
- # str.gsub!(/\^/, '@@94')
329
- return str
330
- end
331
-
332
-
333
- def objects(r)
334
- room = get_room_tag(r)
335
- objs = r.objects.split("\n")
336
- objs.each { |o|
337
-
338
- tag = get_tag(o, o)
339
-
340
- names = o.dup
341
- names.gsub!(/"/, '') # remove any quotes
342
- names.gsub!(/\b\w\b/, '') # remove single letter words
343
-
344
- name = names
345
- names = name.split(' ')
346
-
347
- article = 'a '
348
- if name =~ /ves$/ or name =~ /s$/
349
- article = 'some '
350
- elsif name =~ /^[aeiou]/
351
- article = 'an '
352
- end
353
-
354
-
355
- # If name begins with uppercase, assume it is an NPC
356
- type = 'a thing'
357
-
358
- if name =~ IS_ANIMAL
359
- type = 'an animal'
360
- elsif name =~ IS_PERSON
361
- article = 'a '
362
- type = 'a person'
363
- if name =~ IS_MALE_PERSON
364
- type = 'a male person'
365
- elsif name =~ IS_FEMALE_PERSON
366
- type = 'a female person'
367
- end
368
- elsif name =~ /[A-Z]/
369
- if name !~ /'/ # possesive, like Michael's wallet
370
- # if too many words, probably a book's title
371
- if names.size <= 3
372
- article = ''
373
- if name =~ /^(?:Miss|Mrs)/
374
- type = 'a woman'
375
- else
376
- type = 'a person'
377
- end
378
- end
379
- end
380
- end
381
-
382
-
383
- props = ''
384
-
385
- if tag != name
386
- props << "\n The printed name of #{tag} is \"#{article}#{name}\"."
387
- else
388
- tag = article + tag
389
- end
390
-
391
- if name =~ IS_LIGHT
392
- props << "\n It is not lit."
393
- end
394
-
395
- if name =~ IS_CONTAINER
396
- props << "\n It is a container."
397
- end
398
-
399
- if name =~ IS_SUPPORTER
400
- props << "\n It is a supporter."
401
- end
402
-
403
- if name =~ IS_WEARABLE
404
- props << "\n It is wearable."
405
- end
406
-
407
- if name =~ IS_EDIBLE
408
- props << "\n It is edible."
409
- end
410
-
411
- @f.print <<"EOF"
412
-
413
- In #{room}, there is #{type} called #{tag}. #{props}
414
- The description of #{tag} is \"UNDER CONSTRUCTION.\"
415
- EOF
416
- if names.size > 1
417
- names.each { |n|
418
- next if n =~ IGNORED_ARTICLES
419
- @f.puts " Understand \"#{n}\" as #{tag}."
420
- }
421
- end
422
- }
423
-
424
-
425
- end
426
-
427
- def door(e)
428
- name = get_door_name(e)
429
- tag = get_tag(e, name)
430
- roomA = get_room_tag(e.roomA)
431
- roomB = get_room_tag(e.roomB)
432
- dirA = e.roomA.exits.index(e)
433
- dirB = e.roomB.exits.rindex(e)
434
- dirA = DIRECTIONS[dirA].downcase
435
- dirB = DIRECTIONS[dirB].downcase
436
-
437
- props = ''
438
- if e.type == Connection::LOCKED_DOOR
439
- props = "It is locked."
440
- elsif e.type == Connection::CLOSED_DOOR
441
- props = "It is closed."
442
- end
443
-
444
- found_in = 'It is '
445
- if e.dir == Connection::BOTH
446
- found_in << "#{dirA} of #{roomA} and #{dirB} of #{roomB}"
447
- elsif e.dir == Connection::AtoB
448
- found_in << "#{dirA} of #{roomA}. Through it is #{roomB}"
449
- elsif e.dir == Connection::BtoA
450
- found_in << "#{dirB} of #{roomB}. Through it is #{roomA}"
451
- end
452
-
453
- @f.puts <<"EOF"
454
-
455
- The #{tag} is a door. #{props}
456
- The printed name of #{tag} is "a #{name}".
457
- Understand "#{name}" as #{tag}.
458
- Understand "door" as #{tag}.
459
- #{found_in}.
460
- EOF
461
-
462
- end
463
-
464
-
465
- def room(r)
466
- tag = get_room_tag(r)
467
- name = r.name
468
-
469
- prop = ''
470
- if r.darkness
471
- prop << 'dark '
472
- end
473
-
474
- @f.print "\n#{tag} is a #{prop}room."
475
-
476
- if name != tag
477
- name = inform_quote(name)
478
- @f.print " The printed name of #{tag} is \"#{name}\"."
479
- end
480
-
481
- @f.puts
482
- @f.puts " \"#{wrap_text(r.desc)}\""
483
-
484
- # Now, handle exits...
485
- r.exits.each_with_index { |e, dir|
486
- next if (not e) or e.stub? or e.type == Connection::SPECIAL
487
- if e.roomB == r
488
- next if e.dir == Connection::AtoB
489
- text = e.exitBtext
490
- b = e.roomA
491
- else
492
- next if e.dir == Connection::BtoA
493
- text = e.exitAtext
494
- b = e.roomB
495
- end
496
- @f.print ' '
497
- if text == 0
498
- @f.print "#{DIRECTIONS[dir]} is "
499
- else
500
- @f.print "#{OTHERDIRS[text]} is "
501
- end
502
- if e.type == Connection::CLOSED_DOOR or
503
- e.type == Connection::LOCKED_DOOR
504
- @f.print get_door_tag(e)
505
- else
506
- @f.print get_room_tag(b)
507
- end
508
- @f.puts '.'
509
- }
510
- objects(r)
511
- end
512
-
513
- def section(sect, idx)
514
- name = sect.name
515
- name = 'Unnamed' if name.to_s == ''
516
-
517
- @f.puts
518
- @f.puts "Section #{idx+1} - #{name}"
519
- @f.puts
520
-
521
- @f.puts
522
- @f.puts "Part 1 - Room Descriptions"
523
- @f.puts
524
- sect.rooms.each { |r| room(r) }
525
- @f.puts
526
- @f.puts
527
-
528
- @f.puts
529
- @f.puts "Part 2 - Doors"
530
- @f.puts
531
- sect.connections.each { |e|
532
- next if (e.type != Connection::LOCKED_DOOR and
533
- e.type != Connection::CLOSED_DOOR)
534
- door(e)
535
- }
536
- end
537
-
538
- def start
539
- @f = File.open("#@root.inform", "w")
540
- story = @map.name
541
- story = 'Untitled' if story == ''
542
- today = Date.today
543
- serial = today.strftime("%y%m%d")
544
-
545
- @f.puts
546
- @f.puts "\"#{story}\" by \"#{@map.creator}\""
547
- @f.puts
548
- @f.puts <<"EOF"
549
- The story genre is "Unknown". The release number is 1.
550
- The story headline is "An Interactive Fiction".
551
- The story description is "".
552
- The story creation year is #{today.year}.
553
-
554
-
555
- Use full-length room descriptions.
556
-
557
- EOF
558
-
559
- @map.sections.each_with_index { |sect, idx|
560
- section(sect, idx)
561
- }
562
- @f.close
563
- end
564
-
565
- def initialize(map, fileroot)
566
- @tags = {}
567
- @root = fileroot
568
- @base = File.basename(@root)
569
- @map = map
570
-
571
- start
572
- end
573
- end
1
+
2
+
3
+ class Inform7Writer
4
+
5
+
6
+ DIRECTIONS = [
7
+ 'North',
8
+ 'Northeast',
9
+ 'East',
10
+ 'Southeast',
11
+ 'South',
12
+ 'Southwest',
13
+ 'West',
14
+ 'Northwest',
15
+ ]
16
+
17
+ OTHERDIRS = [
18
+ '',
19
+ 'Above',
20
+ 'Below',
21
+ 'Inside',
22
+ 'Outside',
23
+ ]
24
+
25
+ IGNORE_WORDS = [
26
+ 'a', 'the', 'and', 'of', 'your', 'to', 'out', 'in'
27
+ ]
28
+
29
+ IGNORED_ARTICLES = /^(?:#{IGNORE_WORDS.join('|')})$/
30
+
31
+
32
+
33
+ KEYWORDS = [
34
+ 'include',
35
+ 'has',
36
+ 'with',
37
+ 'is',
38
+ 'in',
39
+ 'container',
40
+ 'to',
41
+ 'and',
42
+ 'side',
43
+ '\s*,\s*',
44
+ '\s*\(\s*',
45
+ ] + DIRECTIONS + OTHERDIRS[1..-1]
46
+
47
+ INVALID_KEYWORD = /\b(?:#{KEYWORDS.join('|')})\b/i
48
+
49
+
50
+ LOCATION_NAMES = [
51
+ 'door',
52
+ 'include',
53
+ 'room',
54
+ 'has',
55
+ 'with',
56
+ 'is',
57
+ 'container',
58
+ ] + DIRECTIONS
59
+ INVALID_LOCATION_NAME = /^(?:#{LOCATION_NAMES.join('|')})$/i
60
+
61
+
62
+ #
63
+ # Some common animals in adventure games
64
+ #
65
+ ANIMALS = [
66
+ 'horse',
67
+ 'donkey',
68
+ 'mule',
69
+ 'goat',
70
+ # lizards
71
+ 'lizard',
72
+ 'snake',
73
+ 'turtle',
74
+ # fishes
75
+ 'fish',
76
+ 'dolphin',
77
+ 'whale',
78
+ 'tortoise',
79
+ # dogs
80
+ 'dog',
81
+ 'wolf',
82
+ # cats
83
+ 'cat',
84
+ 'leopard',
85
+ 'tiger',
86
+ 'lion',
87
+ # fantastic
88
+ 'grue',
89
+ # birds
90
+ 'bird',
91
+ 'pigeon',
92
+ 'peacock',
93
+ 'eagle',
94
+ ]
95
+
96
+ IS_ANIMAL = /\b(?:#{ANIMALS.join('|')})\b/i
97
+
98
+ MALE_PEOPLE = [
99
+ 'boy',
100
+ 'man',
101
+ 'sir',
102
+ ]
103
+ IS_MALE_PERSON = /\b(?:#{MALE_PEOPLE.join('|')})\b/i
104
+
105
+ FEMALE_PEOPLE = [
106
+ 'girl',
107
+ 'woman',
108
+ 'lady',
109
+ 'actress',
110
+ ]
111
+ IS_FEMALE_PERSON = /\b(?:#{FEMALE_PEOPLE.join('|')})\b/i
112
+
113
+ PEOPLE = MALE_PEOPLE + FEMALE_PEOPLE + [
114
+ 'child',
115
+ 'attendant',
116
+ 'doctor',
117
+ 'actor',
118
+ 'character',
119
+ 'engineer',
120
+ 'bum',
121
+ 'nerd',
122
+ 'ghost',
123
+ ]
124
+
125
+ IS_PERSON = /\b(?:#{PEOPLE.join('|')})\b/i
126
+
127
+ #
128
+ # Some common container types
129
+ #
130
+ CONTAINERS = [
131
+ 'box',
132
+ 'cup',
133
+ 'crate',
134
+ 'tin',
135
+ 'can',
136
+ 'chest',
137
+ 'wardrobe',
138
+ 'jacket',
139
+ 'suit',
140
+ 'trophy case',
141
+ 'coffin',
142
+ 'briefcase',
143
+ 'suitcase',
144
+ 'bag',
145
+ 'flask',
146
+ ]
147
+ IS_CONTAINER = /\b(?:#{CONTAINERS.join('|')})\b/i
148
+
149
+ #
150
+ # Some common lit types
151
+ #
152
+ UNLIT_TYPES = [
153
+ 'lantern',
154
+ 'torch',
155
+ ]
156
+ IS_LIGHT = /\b(?:#{UNLIT_TYPES.join('|')})\b/i
157
+
158
+ #
159
+ # Some common wearable types
160
+ #
161
+ CLOTHES = [
162
+ 'cape',
163
+ 'glove',
164
+ 'jeans',
165
+ 'trousers',
166
+ 'hat',
167
+ 'gown',
168
+ 'backpack',
169
+ 'shirt',
170
+ 'jacket',
171
+ 'suit',
172
+ 'ring',
173
+ 'bracelet',
174
+ 'amulet',
175
+ 'locket',
176
+ ]
177
+ IS_WEARABLE = /\b(?:#{CLOTHES.join('|')})\b/i
178
+
179
+ #
180
+ # Some common supporter types
181
+ #
182
+ SUPPORTERS = [
183
+ 'table',
184
+ 'shelf',
185
+ ]
186
+ IS_SUPPORTER = /\b(?:#{SUPPORTERS.join('|')})\b/i
187
+
188
+ #
189
+ # Some common edible types
190
+ #
191
+ FOOD = [
192
+ 'bread',
193
+ 'cake',
194
+ # drinks
195
+ 'water',
196
+ 'soda',
197
+ 'beer',
198
+ 'beverage',
199
+ 'potion',
200
+ # fruits
201
+ 'fruit',
202
+ 'apple',
203
+ 'orange',
204
+ 'banana',
205
+ 'almond',
206
+ 'nut',
207
+ ]
208
+ IS_EDIBLE = /\b(?:#{FOOD.join('|')})\b/i
209
+
210
+ def new_tag(elem, str)
211
+ tag = str.dup
212
+
213
+ # Take text from Unicode utf-8 to iso-8859-1
214
+ utf = Iconv.new( 'iso-8859-1', 'utf-8' )
215
+ tag = utf.iconv( tag )
216
+
217
+ # Remove redundant spaces
218
+ tag.sub!(/^\s/, '')
219
+ tag.sub!(/\s$/, '')
220
+
221
+ # Invalid tag characters, replaced with _
222
+ tag.gsub!(/[\s"\\\#\,\.:;!\?\n\(\)]+/,'_')
223
+ tag.sub!(/^([\d]+)_?(.*)/, '\2\1') # No numbers allowed at start of tag
224
+
225
+ tag.gsub!(/__/, '_')
226
+
227
+ # tag cannot be repeated and cannot be keyword (Doorway, Room, etc)
228
+ # In those cases, we add a number to the tag name.
229
+ idx = 0
230
+ if @tags.values.include?(tag) or tag =~ INVALID_LOCATION_NAME
231
+ idx = 1
232
+ end
233
+
234
+
235
+
236
+ if idx > 0
237
+ root = tag.dup
238
+ tag = "#{root}#{idx}"
239
+ while @tags.values.include?(tag)
240
+ tag = "#{root}#{idx}"
241
+ idx += 1
242
+ end
243
+ end
244
+
245
+
246
+ if elem.kind_of?(String)
247
+ @tags[tag] = tag
248
+ else
249
+ @tags[elem] = tag
250
+ end
251
+ return tag
252
+ end
253
+
254
+ def get_tag(elem, name)
255
+ return @tags[elem] if @tags[elem]
256
+ tag = name
257
+ if tag =~ INVALID_KEYWORD or @tags.values.include?(tag)
258
+ return new_tag(elem, name)
259
+ else
260
+ @tags[elem] = tag
261
+ end
262
+ return tag
263
+ end
264
+
265
+ def get_room_tag(elem)
266
+ return @tags[elem] if @tags[elem]
267
+ tag = elem.name
268
+ if tag =~ INVALID_KEYWORD or @tags.values.include?(tag)
269
+ tag = new_tag(elem, elem.name)
270
+ else
271
+ @tags[elem] = tag
272
+ end
273
+ return tag
274
+ end
275
+
276
+ def get_door_name(e)
277
+ dirA = e.roomA.exits.index(e)
278
+ dirB = e.roomB.exits.rindex(e)
279
+ name = DIRECTIONS[dirA].downcase + "-" + DIRECTIONS[dirB].downcase
280
+ name << " door"
281
+ end
282
+
283
+ def get_door_tag(e)
284
+ name = get_door_name(e)
285
+ name.upcase
286
+ get_tag(e, name)
287
+ end
288
+
289
+ def wrap_text(text, width = 75, indent = 78 - width)
290
+ return 'UNDER CONSTRUCTION.' if not text or text == ''
291
+ str = inform_quote( text )
292
+
293
+ if str.size > width
294
+ r = ''
295
+ while str
296
+ if str.size >= width
297
+ idx = str.rindex(/[ -]/, width)
298
+ idx = str.size unless idx
299
+ else
300
+ idx = str.size
301
+ end
302
+ r << str[0..idx]
303
+ str = str[idx+1..-1]
304
+ r << "\n" << ' ' * indent if str
305
+ end
306
+ return r
307
+ else
308
+ return str
309
+ end
310
+ end
311
+
312
+
313
+ #
314
+ # Take a text and quote it for inform's double-quote text areas.
315
+ #
316
+ def inform_quote(text)
317
+ str = text.dup
318
+
319
+ # Take text from Unicode utf-8 to iso-8859-1
320
+ utf = Iconv.new( 'iso-8859-1', 'utf-8' )
321
+ str = utf.iconv( str )
322
+
323
+ # Quote special characters
324
+ # str.gsub!(/@/, '@@64')
325
+ str.gsub!(/"/, '\'')
326
+ # str.gsub!(/~/, '@@126')
327
+ # str.gsub!(/\\/, '@@92')
328
+ # str.gsub!(/\^/, '@@94')
329
+ return str
330
+ end
331
+
332
+
333
+ def objects(r)
334
+ room = get_room_tag(r)
335
+ objs = r.objects.split("\n")
336
+ objs.each { |o|
337
+
338
+ tag = get_tag(o, o)
339
+
340
+ names = o.dup
341
+ names.gsub!(/"/, '') # remove any quotes
342
+ names.gsub!(/\b\w\b/, '') # remove single letter words
343
+
344
+ name = names
345
+ names = name.split(' ')
346
+
347
+ article = 'a '
348
+ if name =~ /ves$/ or name =~ /s$/
349
+ article = 'some '
350
+ elsif name =~ /^[aeiou]/
351
+ article = 'an '
352
+ end
353
+
354
+
355
+ # If name begins with uppercase, assume it is an NPC
356
+ type = 'a thing'
357
+
358
+ if name =~ IS_ANIMAL
359
+ type = 'an animal'
360
+ elsif name =~ IS_PERSON
361
+ article = 'a '
362
+ type = 'a person'
363
+ if name =~ IS_MALE_PERSON
364
+ type = 'a male person'
365
+ elsif name =~ IS_FEMALE_PERSON
366
+ type = 'a female person'
367
+ end
368
+ elsif name =~ /[A-Z]/
369
+ if name !~ /'/ # possesive, like Michael's wallet
370
+ # if too many words, probably a book's title
371
+ if names.size <= 3
372
+ article = ''
373
+ if name =~ /^(?:Miss|Mrs)/
374
+ type = 'a woman'
375
+ else
376
+ type = 'a person'
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+
383
+ props = ''
384
+
385
+ if tag != name
386
+ props << "\n The printed name of #{tag} is \"#{article}#{name}\"."
387
+ else
388
+ tag = article + tag
389
+ end
390
+
391
+ if name =~ IS_LIGHT
392
+ props << "\n It is not lit."
393
+ end
394
+
395
+ if name =~ IS_CONTAINER
396
+ props << "\n It is a container."
397
+ end
398
+
399
+ if name =~ IS_SUPPORTER
400
+ props << "\n It is a supporter."
401
+ end
402
+
403
+ if name =~ IS_WEARABLE
404
+ props << "\n It is wearable."
405
+ end
406
+
407
+ if name =~ IS_EDIBLE
408
+ props << "\n It is edible."
409
+ end
410
+
411
+ @f.print <<"EOF"
412
+
413
+ In #{room}, there is #{type} called #{tag}. #{props}
414
+ The description of #{tag} is \"UNDER CONSTRUCTION.\"
415
+ EOF
416
+ if names.size > 1
417
+ names.each { |n|
418
+ next if n =~ IGNORED_ARTICLES
419
+ @f.puts " Understand \"#{n}\" as #{tag}."
420
+ }
421
+ end
422
+ }
423
+
424
+
425
+ end
426
+
427
+ def door(e)
428
+ name = get_door_name(e)
429
+ tag = get_tag(e, name)
430
+ roomA = get_room_tag(e.roomA)
431
+ roomB = get_room_tag(e.roomB)
432
+ dirA = e.roomA.exits.index(e)
433
+ dirB = e.roomB.exits.rindex(e)
434
+ dirA = DIRECTIONS[dirA].downcase
435
+ dirB = DIRECTIONS[dirB].downcase
436
+
437
+ props = ''
438
+ if e.type == Connection::LOCKED_DOOR
439
+ props = "It is locked."
440
+ elsif e.type == Connection::CLOSED_DOOR
441
+ props = "It is closed."
442
+ end
443
+
444
+ found_in = 'It is '
445
+ if e.dir == Connection::BOTH
446
+ found_in << "#{dirA} of #{roomA} and #{dirB} of #{roomB}"
447
+ elsif e.dir == Connection::AtoB
448
+ found_in << "#{dirA} of #{roomA}. Through it is #{roomB}"
449
+ elsif e.dir == Connection::BtoA
450
+ found_in << "#{dirB} of #{roomB}. Through it is #{roomA}"
451
+ end
452
+
453
+ @f.puts <<"EOF"
454
+
455
+ The #{tag} is a door. #{props}
456
+ The printed name of #{tag} is "a #{name}".
457
+ Understand "#{name}" as #{tag}.
458
+ Understand "door" as #{tag}.
459
+ #{found_in}.
460
+ EOF
461
+
462
+ end
463
+
464
+
465
+ def room(r)
466
+ tag = get_room_tag(r)
467
+ name = r.name
468
+
469
+ prop = ''
470
+ if r.darkness
471
+ prop << 'dark '
472
+ end
473
+
474
+ @f.print "\n#{tag} is a #{prop}room."
475
+
476
+ if name != tag
477
+ name = inform_quote(name)
478
+ @f.print " The printed name of #{tag} is \"#{name}\"."
479
+ end
480
+
481
+ @f.puts
482
+ @f.puts " \"#{wrap_text(r.desc)}\""
483
+
484
+ # Now, handle exits...
485
+ r.exits.each_with_index { |e, dir|
486
+ next if (not e) or e.stub? or e.type == Connection::SPECIAL
487
+ if e.loop?
488
+ text = e.exitAtext
489
+ b = e.roomB
490
+ else
491
+ if e.roomB == r
492
+ next if e.dir == Connection::AtoB
493
+ text = e.exitBtext
494
+ b = e.roomA
495
+ else
496
+ next if e.dir == Connection::BtoA
497
+ text = e.exitAtext
498
+ b = e.roomB
499
+ end
500
+ end
501
+ @f.print ' '
502
+ if text == 0
503
+ @f.print "#{DIRECTIONS[dir]} is "
504
+ else
505
+ @f.print "#{OTHERDIRS[text]} is "
506
+ end
507
+
508
+ if e.type == Connection::CLOSED_DOOR or
509
+ e.type == Connection::LOCKED_DOOR
510
+ @f.print get_door_tag(e)
511
+ else
512
+ @f.print get_room_tag(b)
513
+ end
514
+ @f.puts '.'
515
+ }
516
+ objects(r)
517
+ end
518
+
519
+ def section(sect, idx)
520
+ name = sect.name
521
+ name = 'Unnamed' if name.to_s == ''
522
+
523
+ @f.puts
524
+ @f.puts "Section #{idx+1} - #{name}"
525
+ @f.puts
526
+
527
+ @f.puts
528
+ @f.puts "Part 1 - Room Descriptions"
529
+ @f.puts
530
+ sect.rooms.each { |r| room(r) }
531
+ @f.puts
532
+ @f.puts
533
+
534
+ @f.puts
535
+ @f.puts "Part 2 - Doors"
536
+ @f.puts
537
+ sect.connections.each { |e|
538
+ next if (e.type != Connection::LOCKED_DOOR and
539
+ e.type != Connection::CLOSED_DOOR)
540
+ door(e)
541
+ }
542
+ end
543
+
544
+ def start
545
+ @f = File.open("#@root.inform", "w")
546
+ story = @map.name
547
+ story = 'Untitled' if story == ''
548
+ today = Date.today
549
+ serial = today.strftime("%y%m%d")
550
+
551
+ @f.puts
552
+ @f.puts "\"#{story}\" by \"#{@map.creator}\""
553
+ @f.puts
554
+ @f.puts <<"EOF"
555
+ The story genre is "Unknown". The release number is 1.
556
+ The story headline is "An Interactive Fiction".
557
+ The story description is "".
558
+ The story creation year is #{today.year}.
559
+
560
+
561
+ Use full-length room descriptions.
562
+
563
+ EOF
564
+
565
+ @map.sections.each_with_index { |sect, idx|
566
+ section(sect, idx)
567
+ }
568
+ @f.close
569
+ end
570
+
571
+ def initialize(map, fileroot)
572
+ @tags = {}
573
+ @root = fileroot
574
+ @base = File.basename(@root)
575
+ @map = map
576
+
577
+ start
578
+ end
579
+ end