ifmapper 0.9.8 → 0.9.9

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.
data/HISTORY.txt CHANGED
@@ -1,3 +1,10 @@
1
+ v0.9.9 Improvements:
2
+ - It is now possible to export maps in Inform 7 new (beta) syntax,
3
+ albeit the English grammar is still very limited.
4
+ Just name your map with extension ".inform" instead of ".inf"
5
+ when saving it or exporting it.
6
+ - Bug fixed wrap_text() in all exporters.
7
+
1
8
  v0.9.8 Bug fixes:
2
9
  - TADS Exporter was incorrectly recognizing locations with "room" in
3
10
  them as keywords and adding numbers to their tags. Fixed.
@@ -2014,7 +2014,20 @@ class FXMap < Map
2014
2014
  #
2015
2015
  # Export map as a set of Inform source code files
2016
2016
  #
2017
- def export_inform(file)
2017
+ def export_inform7(file)
2018
+ require 'IFMapper/Inform7Writer'
2019
+ file.sub!(/.inform$/, '')
2020
+ Inform7Writer.new(self, file)
2021
+ end
2022
+
2023
+ #
2024
+ # Export map as a set of Inform source code files
2025
+ #
2026
+ def export_inform(file, version = 6)
2027
+ if file =~ /\.inform$/ or version > 6
2028
+ return export_inform7(file)
2029
+ end
2030
+
2018
2031
  require 'IFMapper/InformWriter'
2019
2032
  file.sub!(/(-\d+)?\.inf/, '')
2020
2033
  InformWriter.new(self, file)
@@ -2032,7 +2045,7 @@ class FXMap < Map
2032
2045
  end
2033
2046
 
2034
2047
  case file
2035
- when /\.inf$/
2048
+ when /\.inform$/, /\.inf$/
2036
2049
  export_inform(file)
2037
2050
  when /\.ifm$/
2038
2051
  export_ifm(file)
@@ -9,7 +9,7 @@ end
9
9
  def no_fox
10
10
  $stderr.puts "Please install the FXRuby (FOX) library v1.2 or later."
11
11
  if $rubygems
12
- $stderr.puts "You can usually do so if you do 'gem install fxruby'."
12
+ $stderr.puts "You can usually do so if you do 'gem install -r fxruby'."
13
13
  end
14
14
  exit(1)
15
15
  end
@@ -17,7 +17,7 @@ end
17
17
  def get_fox
18
18
  ##### ARRRGH!!!! Why does Lyle keep changing the fxruby name on each
19
19
  ##### release!
20
- foxes = [ 'fox14', 'fox12', 'fox' ]
20
+ foxes = [ 'fox16', 'fox14', 'fox12', 'fox' ]
21
21
  foxes.each { |fox|
22
22
  begin
23
23
  require "#{fox}"
@@ -128,6 +128,9 @@ class FXMapperWindow < FXMainWindow
128
128
  map.start_automap
129
129
  end
130
130
 
131
+ #
132
+ # Properties of the automapper callback
133
+ #
131
134
  def automap_properties_cb(sender, sel, ptr)
132
135
  map = current_map
133
136
  return if not map or not map.automap
@@ -143,6 +146,10 @@ class FXMapperWindow < FXMainWindow
143
146
  map.stop_automap
144
147
  end
145
148
 
149
+
150
+ #
151
+ # Callback to Open File
152
+ #
146
153
  def open_cb(sender, sel, ptr)
147
154
  file = FXMapFileDialog.new(self, "Load New Map").filename
148
155
  return if file == ''
@@ -216,10 +223,16 @@ class FXMapperWindow < FXMainWindow
216
223
  status "Loaded '#{file}'."
217
224
  end
218
225
 
226
+ #
227
+ # Write a message to the status bar
228
+ #
219
229
  def status(msg)
220
230
  @statusbar.statusLine.text = msg
221
231
  end
222
232
 
233
+ #
234
+ # Returns current active map or nil if no maps
235
+ #
223
236
  def current_map
224
237
  window = @mdiclient.activeChild
225
238
  return nil unless window
@@ -230,18 +243,27 @@ class FXMapperWindow < FXMainWindow
230
243
  return nil
231
244
  end
232
245
 
246
+ #
247
+ # Callback for Save
248
+ #
233
249
  def save_cb(sender, sel, ptr)
234
250
  map = current_map
235
251
  return unless map
236
252
  map.save
237
253
  end
238
254
 
255
+ #
256
+ # Callback for Save As
257
+ #
239
258
  def save_as_cb(sender, sel, ptr)
240
259
  map = current_map
241
260
  return unless map
242
261
  map.save_as
243
262
  end
244
263
 
264
+ #
265
+ # Callback used to create new map
266
+ #
245
267
  def new_map_cb(*args)
246
268
  m = new_map
247
269
  m.window.create
@@ -275,7 +297,9 @@ class FXMapperWindow < FXMainWindow
275
297
  return map
276
298
  end
277
299
 
300
+ #
278
301
  # Load the named PNG icon from a file
302
+ #
279
303
  def load_icon(filename)
280
304
  begin
281
305
  filename = File.join("icons", filename) + ".png"
@@ -358,7 +382,7 @@ class FXMapperWindow < FXMainWindow
358
382
 
359
383
  d = FXMapFileDialog.new(self, "Save Map as Inform Files",
360
384
  [
361
- "Inform Source Code (*.inf)"
385
+ "Inform Source Code (*.inf,*.inform)",
362
386
  ])
363
387
  map.export_inform( d.filename ) if d.filename != ''
364
388
  end
@@ -51,7 +51,7 @@ class FXRoomList < FXDialogBox
51
51
  end
52
52
 
53
53
  rooms.each { |r|
54
- item = "#{r[0]}\t#{r[1].name}"
54
+ item = "#{r[0] + 1}\t#{r[1].name}"
55
55
  @box.appendItem(item, nil, nil, r)
56
56
  }
57
57
 
@@ -0,0 +1,496 @@
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'
27
+ ]
28
+
29
+ IGNORED_ARTICLES = /^(?:#{IGNORE_WORDS.join('|')})$/
30
+
31
+
32
+
33
+ KEYWORDS = [
34
+ 'include',
35
+ 'has',
36
+ 'with',
37
+ 'is',
38
+ 'in',
39
+ 'inside',
40
+ ]
41
+ INVALID_KEYWORD = /\b(?:#{KEYWORDS.join('|')})\b/i
42
+
43
+
44
+ LOCATION_NAMES = [
45
+ 'door',
46
+ 'include',
47
+ 'room',
48
+ 'has',
49
+ 'with',
50
+ 'is',
51
+ 'container',
52
+ ] + DIRECTIONS
53
+ INVALID_LOCATION_NAME = /^(?:#{LOCATION_NAMES.join('|')})$/i
54
+
55
+
56
+ #
57
+ # Some common animals in adventure games
58
+ #
59
+ ANIMALS = [
60
+ 'horse',
61
+ 'donkey',
62
+ 'mule',
63
+ 'goat',
64
+ # lizards
65
+ 'lizard',
66
+ 'snake',
67
+ 'turtle',
68
+ # fishes
69
+ 'fish',
70
+ 'dolphin',
71
+ 'whale',
72
+ 'tortoise',
73
+ # dogs
74
+ 'dog',
75
+ 'wolf',
76
+ # cats
77
+ 'cat',
78
+ 'leopard',
79
+ 'tiger',
80
+ 'lion',
81
+ # fantastic
82
+ 'grue',
83
+ # birds
84
+ 'bird',
85
+ 'pigeon',
86
+ 'peacock',
87
+ 'eagle',
88
+ ]
89
+
90
+ IS_ANIMAL = /\b(?:#{ANIMALS.join('|')})\b/i
91
+
92
+ PEOPLE = [
93
+ 'child',
94
+ 'girl',
95
+ 'woman',
96
+ 'lady',
97
+ 'boy',
98
+ 'man',
99
+ 'attendant',
100
+ 'doctor',
101
+ 'engineer',
102
+ 'bum',
103
+ 'nerd',
104
+ 'ghost',
105
+ ]
106
+
107
+ IS_PERSON = /\b(?:#{PEOPLE.join('|')})\b/i
108
+
109
+ #
110
+ # Some common container types
111
+ #
112
+ CONTAINERS = [
113
+ 'box',
114
+ 'cup',
115
+ 'crate',
116
+ 'tin',
117
+ 'can',
118
+ 'chest',
119
+ 'wardrobe',
120
+ 'trophy case',
121
+ 'coffin',
122
+ 'briefcase',
123
+ 'suitcase',
124
+ 'bag',
125
+ ]
126
+ IS_CONTAINER = /\b(?:#{CONTAINERS.join('|')})\b/i
127
+
128
+ #
129
+ # Some common wearable types
130
+ #
131
+ CLOTHES = [
132
+ 'cape',
133
+ 'glove',
134
+ 'jeans',
135
+ 'trousers',
136
+ 'hat',
137
+ 'gown',
138
+ 'backpack',
139
+ 'shirt',
140
+ 'ring',
141
+ 'bracelet',
142
+ 'amulet',
143
+ 'locket',
144
+ ]
145
+ IS_WEARABLE = /\b(?:#{CLOTHES.join('|')})\b/i
146
+
147
+ #
148
+ # Some common supporter types
149
+ #
150
+ SUPPORTERS = [
151
+ 'table',
152
+ 'shelf',
153
+ ]
154
+ IS_SUPPORTER = /\b(?:#{SUPPORTERS.join('|')})\b/i
155
+
156
+ #
157
+ # Some common edible types
158
+ #
159
+ FOOD = [
160
+ 'bread',
161
+ 'cake',
162
+ # drinks
163
+ 'water',
164
+ 'soda',
165
+ 'beer',
166
+ 'beverage',
167
+ 'potion',
168
+ # fruits
169
+ 'fruit',
170
+ 'apple',
171
+ 'orange',
172
+ 'banana',
173
+ 'almond',
174
+ 'nut',
175
+ ]
176
+ IS_EDIBLE = /\b(?:#{FOOD.join('|')})\b/i
177
+
178
+ def new_tag(elem, str)
179
+ tag = str.dup
180
+
181
+ # Remove redundant spaces
182
+ tag.sub!(/^\s/, '')
183
+ tag.sub!(/\s$/, '')
184
+
185
+ # Invalid tag characters, replaced with _
186
+ tag.gsub!(/[\s"\\\#\,\.:;!\?\n\(\)]+/,'_')
187
+ tag.sub!(/^([\d]+)_?(.*)/, '\2\1') # No numbers allowed at start of tag
188
+
189
+ tag.gsub!(/__/, '_')
190
+
191
+ tag.upcase! # All tags are uppercase
192
+
193
+ # tag cannot be repeated and cannot be keyword (Doorway, Room, etc)
194
+ # In those cases, we add a number to the tag name.
195
+ idx = 0
196
+ if @tags.values.include?(tag) or tag =~ INVALID_LOCATION_NAME
197
+ idx = 1
198
+ end
199
+
200
+
201
+
202
+ if idx > 0
203
+ root = tag.dup
204
+ tag = "#{root}#{idx}"
205
+ while @tags.values.include?(tag)
206
+ tag = "#{root}#{idx}"
207
+ idx += 1
208
+ end
209
+ end
210
+
211
+
212
+ if elem.kind_of?(String)
213
+ @tags[tag] = tag
214
+ else
215
+ @tags[elem] = tag
216
+ end
217
+ return tag
218
+ end
219
+
220
+ def get_tag(elem, name = elem.name)
221
+ return @tags[elem] if @tags[elem]
222
+ return new_tag(elem, name)
223
+ end
224
+
225
+ def get_door_name(e)
226
+ dirA = e.roomA.exits.index(e)
227
+ dirB = e.roomB.exits.rindex(e)
228
+ name = DIRECTIONS[dirA].downcase + "-" + DIRECTIONS[dirB].downcase
229
+ name << " door"
230
+ end
231
+
232
+ def get_door_tag(e)
233
+ get_tag(e, get_door_name(e))
234
+ end
235
+
236
+ def wrap_text(text, width = 75, indent = 78 - width)
237
+ return 'UNDER CONSTRUCTION' if not text or text == ''
238
+ str = inform_quote( text.dup )
239
+
240
+ if str.size > width
241
+ r = ''
242
+ while str
243
+ idx = str.rindex(/[ -]/, width)
244
+ idx = str.size unless idx
245
+ r << str[0..idx]
246
+ str = str[idx+1..-1]
247
+ r << "\n" << ' ' * indent if str
248
+ end
249
+ return r
250
+ else
251
+ return str
252
+ end
253
+ end
254
+
255
+
256
+ #
257
+ # Take a text and quote it for inform's double-quote text areas.
258
+ #
259
+ def inform_quote(text)
260
+ str = text.dup
261
+ # Quote special characters
262
+ # str.gsub!(/@/, '@@64')
263
+ str.gsub!(/"/, '\'')
264
+ # str.gsub!(/~/, '@@126')
265
+ # str.gsub!(/\\/, '@@92')
266
+ # str.gsub!(/\^/, '@@94')
267
+ return str
268
+ end
269
+
270
+
271
+ def objects(r)
272
+ room = get_tag(r)
273
+ objs = r.objects.split("\n")
274
+ objs.each { |o|
275
+
276
+ tag = new_tag(o, o)
277
+
278
+ names = o.dup
279
+ names.gsub!(/"/, '') # remove any quotes
280
+ names.gsub!(/\b\w\b/, '') # remove single letter words
281
+
282
+ name = names
283
+ names = name.split(' ')
284
+
285
+ article = 'a '
286
+ if name =~ /ves$/ or name =~ /s$/
287
+ article = 'some '
288
+ elsif name[0,1] == 'a'
289
+ article = 'an '
290
+ end
291
+
292
+
293
+ # If name begins with uppercase, assume it is an NPC
294
+ type = 'a thing'
295
+
296
+ if name =~ IS_ANIMAL
297
+ type = 'an animal'
298
+ elsif name =~ IS_PERSON or name =~ /[A-Z]/
299
+ if name !~ /'/ # possesive, like Michael's wallet
300
+ # if too many words, probably a book's title
301
+ if names.size <= 3
302
+ article = ''
303
+ if name =~ /^(?:Miss|Mrs)/
304
+ type = 'a woman'
305
+ else
306
+ type = 'a person'
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+
313
+ props = ''
314
+
315
+ if tag != name
316
+ props << "\n The printed name of #{tag} is \"#{article}#{name}\"."
317
+ end
318
+
319
+ if name =~ IS_CONTAINER
320
+ props << "\n It is a container."
321
+ end
322
+
323
+ if name =~ IS_SUPPORTER
324
+ props << "\n It is a supporter."
325
+ end
326
+
327
+ if name =~ IS_WEARABLE
328
+ props << "\n It is wearable."
329
+ end
330
+
331
+ if name =~ IS_EDIBLE
332
+ props << "\n It is edible."
333
+ end
334
+
335
+ @f.print <<"EOF"
336
+
337
+ In #{room}, there is #{type} called #{tag}. #{props}
338
+ The description of #{tag} is \"UNDER CONSTRUCTION.\"
339
+ EOF
340
+ if names.size > 1
341
+ names.each { |n|
342
+ next if n =~ IGNORED_ARTICLES
343
+ @f.puts " Understand \"#{n}\" as #{tag}."
344
+ }
345
+ end
346
+ }
347
+
348
+
349
+ end
350
+
351
+ def door(e)
352
+ name = get_door_name(e)
353
+ tag = get_tag(e, name)
354
+ roomA = get_tag(e.roomA)
355
+ roomB = get_tag(e.roomB)
356
+ dirA = e.roomA.exits.index(e)
357
+ dirB = e.roomB.exits.rindex(e)
358
+ dirA = DIRECTIONS[dirA].downcase
359
+ dirB = DIRECTIONS[dirB].downcase
360
+
361
+ props = ''
362
+ if e.type == Connection::LOCKED_DOOR
363
+ props = "It is locked."
364
+ elsif e.type == Connection::CLOSED_DOOR
365
+ props = "It is closed."
366
+ end
367
+
368
+ found_in = 'It is '
369
+ if e.dir == Connection::BOTH
370
+ found_in << "#{dirA} of #{roomA} and #{dirB} of #{roomB}"
371
+ elsif e.dir == Connection::AtoB
372
+ found_in << "#{dirA} of #{roomA}. Through it is #{roomB}"
373
+ elsif e.dir == Connection::BtoA
374
+ found_in << "#{dirB} of #{roomB}. Through it is #{roomA}"
375
+ end
376
+
377
+ @f.puts <<"EOF"
378
+
379
+ The #{tag} is a door. #{props}
380
+ The printed name of #{tag} is "a #{name}".
381
+ Understand "door" as #{tag}.
382
+ #{found_in}.
383
+ EOF
384
+
385
+ end
386
+
387
+
388
+ def room(r)
389
+ tag = get_tag(r)
390
+ name = r.name
391
+
392
+ prop = ''
393
+ if r.darkness
394
+ prop << 'dark '
395
+ end
396
+
397
+ @f.print "\n#{tag} is a #{prop}room."
398
+
399
+ if name != tag
400
+ name = inform_quote(name)
401
+ @f.print " The printed name of #{tag} is \"#{name}\"."
402
+ end
403
+
404
+ @f.puts
405
+ @f.puts " \"#{wrap_text(r.desc)}\""
406
+
407
+ # Now, handle exits...
408
+ r.exits.each_with_index { |e, dir|
409
+ next if (not e) or e.stub? or e.type == Connection::SPECIAL
410
+ if e.roomB == r
411
+ next if e.dir == Connection::AtoB
412
+ text = e.exitBtext
413
+ b = e.roomA
414
+ else
415
+ next if e.dir == Connection::BtoA
416
+ text = e.exitAtext
417
+ b = e.roomB
418
+ end
419
+ @f.print ' '
420
+ if text == 0
421
+ @f.print "#{DIRECTIONS[dir]} is "
422
+ else
423
+ @f.print "#{OTHERDIRS[text]} is "
424
+ end
425
+ if e.type == Connection::CLOSED_DOOR or
426
+ e.type == Connection::LOCKED_DOOR
427
+ @f.print get_door_tag(e)
428
+ else
429
+ @f.print get_tag(b)
430
+ end
431
+ @f.puts '.'
432
+ }
433
+ objects(r)
434
+ end
435
+
436
+ def section(sect, idx)
437
+ name = sect.name
438
+ name = 'Unnamed' if name.to_s == ''
439
+
440
+ @f.puts
441
+ @f.puts "Section #{idx+1} - #{name}"
442
+ @f.puts
443
+
444
+ @f.puts
445
+ @f.puts "Part 1 - Room Descriptions"
446
+ @f.puts
447
+ sect.rooms.each { |r| room(r) }
448
+ @f.puts
449
+ @f.puts
450
+
451
+ @f.puts
452
+ @f.puts "Part 2 - Doors"
453
+ @f.puts
454
+ sect.connections.each { |e|
455
+ next if (e.type != Connection::LOCKED_DOOR and
456
+ e.type != Connection::CLOSED_DOOR)
457
+ door(e)
458
+ }
459
+ end
460
+
461
+ def start
462
+ @f = File.open("#@root.inform", "w")
463
+ story = @map.name
464
+ story = 'Untitled' if story == ''
465
+ today = Date.today
466
+ serial = today.strftime("%y%m%d")
467
+
468
+ @f.puts
469
+ @f.puts "\"#{story}\" by \"#{@map.creator}\""
470
+ @f.puts
471
+ @f.puts <<"EOF"
472
+ The story genre is "Unknown". The release number is 1.
473
+ The story headline is "An Interactive Fiction".
474
+ The story description is "".
475
+ The story creation year is #{today.year}.
476
+
477
+
478
+ Use full-length room descriptions.
479
+
480
+ EOF
481
+
482
+ @map.sections.each_with_index { |sect, idx|
483
+ section(sect, idx)
484
+ }
485
+ @f.close
486
+ end
487
+
488
+ def initialize(map, fileroot)
489
+ @tags = {}
490
+ @root = fileroot
491
+ @base = File.basename(@root)
492
+ @map = map
493
+
494
+ start
495
+ end
496
+ end