ifmapper 0.9.8 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
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