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,15 +1,15 @@
1
-
2
- class FXDCPrint
3
- alias :old_font= :font=
4
- alias :old_font :font
5
-
6
- def font
7
- return @f
8
- end
9
-
10
- def font=(x)
11
- old_font=(x)
12
- @f = x
13
- end
14
-
15
- end
1
+
2
+ class FXDCPrint
3
+ alias :old_font= :font=
4
+ alias :old_font :font
5
+
6
+ def font
7
+ return @f
8
+ end
9
+
10
+ def font=(x)
11
+ old_font=(x)
12
+ @f = x
13
+ end
14
+
15
+ end
@@ -0,0 +1,108 @@
1
+
2
+ #
3
+ # Class that lists all rooms in a map and allows you to jump to them
4
+ #
5
+ class FXItemList < FXDialogBox
6
+
7
+ def pick(sender, sel, ptr)
8
+ it = @box.currentItem
9
+ name, section, room = @box.getItemData(it)
10
+
11
+ @map.section = section - 1
12
+ @map.clear_selection
13
+ room.selected = true
14
+ @map.center_view_on_room(room)
15
+ @map.draw
16
+ end
17
+
18
+ def copy_from(map)
19
+ @map = map
20
+ sort
21
+ end
22
+
23
+ def sort
24
+ it = @box.currentItem
25
+ room = nil
26
+ if it >= 0
27
+ item, section, room = @box.getItemData(it)
28
+ end
29
+
30
+ @box.clearItems
31
+
32
+ items = []
33
+ @map.sections.each_with_index { |s, idx|
34
+ s.rooms.each { |r|
35
+ r.objects.each_line { |i|
36
+ i.chomp!
37
+ next if i.empty?
38
+ items << [i, idx+1, r ]
39
+ }
40
+ }
41
+ }
42
+
43
+ dir = @box.header.getArrowDir(0)
44
+ if dir != MAYBE
45
+ items = items.sort_by { |r| r[0] }
46
+ else
47
+ dir = @box.header.getArrowDir(1)
48
+ if dir != MAYBE
49
+ items = items.sort_by { |r| r[1] }
50
+ else
51
+ dir = @box.header.getArrowDir(2)
52
+ if dir != MAYBE
53
+ items = items.sort_by { |r| r[2].name }
54
+ end
55
+ end
56
+ end
57
+
58
+ if dir == Fox::TRUE
59
+ items.reverse!
60
+ end
61
+
62
+ items.each { |r|
63
+ item = "#{r[0]}\t#{r[1]}\t#{r[2].name}"
64
+ @box.appendItem(item, nil, nil, r)
65
+ }
66
+
67
+ if room
68
+ items.each_with_index { |r, idx|
69
+ if r[2] == room
70
+ @box.currentItem = idx
71
+ break
72
+ end
73
+ }
74
+ end
75
+ end
76
+
77
+ def initialize(map)
78
+ super(map.window.parent, BOX_LOCATIONS, DECOR_ALL, 40, 40, 420, 400)
79
+
80
+ @box = FXIconList.new(self, nil, 0,
81
+ ICONLIST_BROWSESELECT|
82
+ LAYOUT_FILL_X|LAYOUT_FILL_Y)
83
+ @box.appendHeader(BOX_NAME, nil, 120)
84
+ @box.appendHeader(BOX_SECTION, nil, 60)
85
+ @box.appendHeader(BOX_LOCATION, nil, 200)
86
+ @box.header.connect(SEL_COMMAND) { |sender, sel, which|
87
+ if @box.header.arrowUp?(which)
88
+ dir = MAYBE
89
+ elsif @box.header.arrowDown?(which)
90
+ dir = TRUE
91
+ else
92
+ dir = FALSE
93
+ end
94
+ 0.upto(2) { |idx|
95
+ @box.header.setArrowDir(idx, MAYBE)
96
+ }
97
+ @box.header.setArrowDir(which, dir)
98
+
99
+ sort
100
+ }
101
+
102
+ @box.connect(SEL_COMMAND, method(:pick))
103
+
104
+ create
105
+ copy_from(map)
106
+ end
107
+
108
+ end
@@ -1,2116 +1,2147 @@
1
- #!/usr/bin/env ruby
2
-
3
-
4
- require 'IFMapper/Map'
5
- require 'IFMapper/FXSection'
6
- require 'IFMapper/FXMapDialogBox'
7
- require 'IFMapper/FXSectionDialogBox'
8
- require 'IFMapper/AStar'
9
- require 'thread'
10
-
11
-
12
- class FXMap < Map
13
- FILE_FORMAT_VERSION = 1 # Upgrade this if incompatible changes are made
14
- # in the class so that file loading of old files
15
- # can still be checked against.
16
-
17
- attr_reader :zoom # Current zooming factor
18
- attr_accessor :filename # Filename of current map (if any)
19
- attr_reader :modified # Was map modified since being loaded?
20
- attr_accessor :navigation # Map is navigation mode (no new nodes can be
21
- # created)
22
- attr_accessor :options # Map options
23
- attr_reader :window # Fox Window for this map
24
- attr :version # file format version
25
- attr_reader :mutex # Mutex to avoid racing conditions while
26
- # automapping
27
- attr_reader :automap # automapping transcript
28
-
29
- # pmap is a path map (a matrix or grid used for path finding).
30
- # Rooms and paths are recorded there. Path finding is needed
31
- # to draw complex connections (ie. those that are farther than one square)
32
- # We now also use this for selecting of stuff, particularly complex paths.
33
- attr :pmap
34
-
35
- @@win = nil # Map Info window
36
- @@roomlist = nil # Room List Window
37
-
38
- @@tooltip = nil
39
- @@cursor_arrow = nil
40
- @@cursor_cross = nil
41
- @@cursor_n = nil
42
- @@cursor_ne = nil
43
- @@cursor_e = nil
44
- @@cursor_se = nil
45
- @@cursor_s = nil
46
- @@cursor_sw = nil
47
- @@cursor_w = nil
48
- @@cursor_nw = nil
49
-
50
- #
51
- # Map has changed dramatically. Update pathmap, title and redraw
52
- #
53
- def _changed
54
- create_pathmap
55
- if @window and @window.shown?
56
- update_title
57
- draw
58
- end
59
- end
60
-
61
-
62
- #
63
- # Jump to a certain section #
64
- #
65
- def section=(x)
66
- clear_selection
67
- super
68
- @complexConnection = false
69
- _changed
70
- end
71
-
72
- #
73
- # Go to previous section (if any)
74
- #
75
- def previous_section
76
- self.section = @section - 1
77
- _changed
78
- end
79
-
80
- #
81
- # Go to next section (if any)
82
- #
83
- def next_section
84
- self.section = @section + 1
85
- _changed
86
- end
87
-
88
- #
89
- # Map has been modified. Update also pathmap.
90
- #
91
- def modified=(x)
92
- @modified = true
93
- _changed
94
- end
95
-
96
- #
97
- # Popup the section properties to allow renaming it
98
- #
99
- def rename_section
100
- @sections[@section].properties(self)
101
- modified = true
102
- end
103
-
104
- #
105
- # Delete current section from map
106
- #
107
- def delete_section
108
- return navigation_warning if @navigation
109
- w = FXWarningBox.new(@window, WARN_DELETE_SECTION)
110
- return if w.execute == 0
111
-
112
- delete_section_at(@section)
113
- modified = true
114
- end
115
-
116
- #
117
- # Add a new section to map and make it current
118
- #
119
- def new_section
120
- return navigation_warning if @navigation
121
- @sections.push( FXSection.new )
122
- @section = @sections.size - 1
123
- end
124
-
125
- #
126
- # A simple debugging function that will spit out the path map with a
127
- # very simple ascii representation.
128
- #
129
- def dump_pathmap
130
- s = ' ' + ' ' * @width + "\n"
131
- m = s * @height
132
- (0...@width).each { |x|
133
- (0...@height).each { |y|
134
- m[y * (@width+2)] = (y % 10).to_s
135
- loc = y * (@width+2) + x + 1
136
- if @pmap.at(x).at(y).kind_of?(Connection)
137
- m[loc] = '-'
138
- elsif @pmap.at(x).at(y).kind_of?(Room)
139
- m[loc] = 'R'
140
- end
141
- }
142
- }
143
- puts ' 0123456789' * (@width/10)
144
- puts m
145
- end
146
-
147
- #
148
- # Determine whether a pathmap area from x,y to x+w,y+h is free
149
- # of rooms
150
- #
151
- def _free_area?(x, y, w, h)
152
- x.upto(x+w) { |xx|
153
- y.upto(x+h) { |yy|
154
- return false if @pmap.at(xx).at(yy).kind_of?(Room)
155
- }
156
- }
157
- return true
158
- end
159
-
160
- #
161
- # Return true or false if map is free at location x, y.
162
- # If x and y coords are within pathmap, we use it.
163
- # If not, we will use the brute force search of Map.rb
164
- #
165
- def free?(x, y)
166
- if @pmap and x >= 0 and y >= 0 and @pmap.size > x and @pmap.at(x).size > y
167
- return false if @pmap.at(x).at(y)
168
- return true
169
- else
170
- super
171
- end
172
- end
173
-
174
- #
175
- # Given a list of rooms, find an area in the map where we could
176
- # place them. If found, return x/y offset so rooms can be moved
177
- # there. This is used for pasting rooms.
178
- #
179
- def find_empty_area(rooms)
180
- return nil if rooms.empty?
181
- minx = maxx = rooms[0].x
182
- miny = maxy = rooms[0].y
183
- rooms.each { |r|
184
- minx = r.x if r.x < minx
185
- maxx = r.x if r.x > maxx
186
- miny = r.y if r.y < miny
187
- maxy = r.y if r.y > maxy
188
- }
189
- w = maxx - minx
190
- h = maxy - miny
191
-
192
- # Find an area in pathmap that has w x h empty rooms
193
- 0.upto(@width-1-w) { |x|
194
- 0.upto(@height-1-h) { |y|
195
- if _free_area?(x, y, w, h)
196
- return [x - minx, y - miny]
197
- end
198
- }
199
- }
200
- return nil
201
- end
202
-
203
- #
204
- # Reinitialize the pathmap to an empty matrix
205
- #
206
- def empty_pathmap
207
- # First, create an empty grid of width x height
208
- @pmap = Array.new(@width)
209
- (0...@width).each { |x|
210
- @pmap[x] = Array.new(@height)
211
- }
212
- return pmap
213
- end
214
-
215
- #
216
- # Recreate the pathmap based on rooms and connections
217
- # This routine is used on loading a new map.
218
- #
219
- def create_pathmap
220
- # First, create an empty grid of width x height
221
- empty_pathmap
222
- # Then, fill it in with all rooms...
223
- @sections[@section].rooms.each { |r| @pmap[r.x][r.y] = r }
224
- # And following, add all paths
225
- @sections[@section].connections.each { |c| path_find(c) }
226
- end
227
-
228
- #
229
- # Given a connection, clean its path from path map.
230
- #
231
- def clean_path(c)
232
- c.gpts.each { |p| @pmap[p[0]][p[1]] = nil if @pmap[p[0]][p[1]] == c }
233
- end
234
-
235
- #
236
- # Remove a connection from map, since path creation failed.
237
- #
238
- def remove_connection(c)
239
- c.failed = true
240
- status "#{ERR_PATH_FOR_CONNECTION} #{c} #{ERR_IS_BLOCKED}."
241
- end
242
-
243
- # Given a connection, create the path for it, if not a simple
244
- # connection. Also, add the paths to pathmap.
245
- def path_find(c)
246
- unless c.complex?
247
- c.pts = c.gpts = []
248
- c.failed = false
249
- return true
250
- end
251
-
252
- # Complex path... Generate points.
253
- a, b = c.room
254
- dirA, dirB = c.dirs
255
- raise "A connection not found #{c} at #{a}" unless dirA
256
- raise "B connection not found #{c} at #{b}" unless dirB
257
-
258
- vA = FXRoom::DIR_TO_VECTOR[dirA]
259
- vB = FXRoom::DIR_TO_VECTOR[dirB]
260
-
261
- pA = [ a.x + vA[0], a.y + vA[1] ]
262
- pB = [ b.x + vB[0], b.y + vB[1] ]
263
-
264
- c.gpts = []
265
- c.pts = []
266
-
267
- # Check for the special case of looping path (path that begins and
268
- # returns to same exit)
269
- if a == b and dirA == dirB
270
- pt = a.corner(c, 1, dirA)
271
- n = 1.0 / Math.sqrt(vA[0] * vA[0] + vA[1] * vA[1])
272
- vA = [ vA[0] * n, vA[1] * n ]
273
- c.pts.push( [ pt[0], pt[1] ] )
274
- pA = [ pt[0] + vA[0] * 20, pt[1] + vA[1] * 20 ]
275
- c.pts.push( [pA[0], pA[1]] )
276
- pB = [ pA[0] + vA[1] * 20, pA[1] - vA[0] * 20 ]
277
- c.pts.push( [pB[0], pB[1]] )
278
- pC = [ pB[0] - vA[0] * 20, pB[1] - vA[1] * 20 ]
279
- c.pts.push( [pC[0], pC[1]] )
280
- c.dir = Connection::AtoB
281
- return true
282
- end
283
-
284
- # Now check if start or goal are fully blocked. If so,
285
- # fail quickly
286
- if pA[0] < 0 or pA[0] >= @width or
287
- pB[0] < 0 or pB[0] >= @width or
288
- pA[1] < 0 or pA[1] >= @height or
289
- pB[1] < 0 or pB[1] >= @height or
290
- @pmap.at(pA[0]).at(pA[1]).kind_of?(Room) or
291
- @pmap.at(pB[0]).at(pB[1]).kind_of?(Room)
292
- remove_connection(c)
293
- return false
294
- end
295
-
296
- if (pA[0] - pB[0]).abs > 30 or
297
- (pA[1] - pB[1]).abs > 30
298
- c.failed = true
299
- return
300
- end
301
-
302
- # No, okay, we need to do true A* path finding
303
- c.failed = false
304
- aStar = AStar.new
305
- MapNode::map(@pmap)
306
- start = MapNode.new( pA[0], pA[1] )
307
- goal = MapNode.new( pB[0], pB[1] )
308
- aStar.goals( start, goal )
309
- while aStar.search_step == AStar::SEARCH_STATE_SEARCHING
310
- end
311
-
312
- # Oops, AStar failed. Not a clean path
313
- if aStar.state == AStar::SEARCH_STATE_FAILED
314
- remove_connection(c)
315
- return false
316
- end
317
-
318
- # We succeeded. Get the path
319
- c.failed = false
320
- c.gpts = aStar.path
321
- # Put path in pathmap so subsequent paths will try to avoid crossing it.
322
- c.gpts.each { |p| @pmap[p[0]][p[1]] = c }
323
-
324
- # Okay, we have a valid path.
325
- # Create real path in display coordinates now...
326
- # Start with a's corner
327
- pt = a.corner(c, 1, dirA)
328
- c.pts.push( [ pt[0], pt[1] ] )
329
- # Then, add each grid point we calculated
330
- c.gpts.each { |pt|
331
- x = pt[0] * WW + WW / 2
332
- y = pt[1] * HH + HH / 2
333
- c.pts.push([x, y])
334
- }
335
- # And end with b's corner
336
- pt = b.corner(c, 1, dirB)
337
- return c.pts.push([pt[0], pt[1]])
338
- end
339
-
340
- #
341
- # Add a new path point to a connection
342
- #
343
- def add_path_pt( c, x, y )
344
- @pmap[x][y] = c
345
- c.gpts.push([x, y])
346
- end
347
-
348
- #
349
- # Used for loading class with Marshal
350
- #
351
- def marshal_load(variables)
352
- @zoom = variables.shift
353
- @navigation = variables.shift
354
- @options = variables.shift
355
- super
356
- @modified = false
357
- end
358
-
359
- #
360
- # Used for saving class with Marshal
361
- #
362
- def marshal_dump
363
- [ @zoom, @navigation, @options ] + super
364
- end
365
-
366
- #
367
- # Used to copy relevant data from one (temporary) map to another
368
- #
369
- def copy(b)
370
- super(b)
371
- if b.kind_of?(FXMap)
372
- @options = b.options if b.options
373
- @filename = b.filename
374
- self.zoom = b.zoom
375
- end
376
- end
377
-
378
- #
379
- # Create a new room for the current section
380
- #
381
- def new_room(x, y)
382
- @modified = true
383
- r = @sections[@section].new_room(x, y)
384
- return r
385
- end
386
-
387
- #
388
- # Function used to add a new room. x and y are absolute pixel positions
389
- # in canvas.
390
- #
391
- def new_xy_room(x, y)
392
- x = x / WW
393
- y = y / HH
394
- r = new_room(x, y)
395
- @pmap[x][y] = r
396
-
397
- r.selected = true
398
-
399
- if @options['Edit on Creation']
400
- if not r.modal_properties(self)
401
- @sections[@section].delete_room(r)
402
- @pmap[x][y] = nil
403
- return nil
404
- end
405
- end
406
- update_roomlist
407
- return r
408
- end
409
-
410
- #
411
- # Given a room, delete it
412
- #
413
- def delete_room_only(r)
414
- if @pmap and r.x < @pmap.size and r.y < @pmap.size and
415
- r.x >= 0 and r.y >= 0
416
- @pmap[r.x][r.y] = nil
417
- end
418
- super
419
- end
420
-
421
- #
422
- # Given a room, delete it
423
- #
424
- def delete_room(r)
425
- if @pmap and r.x < @pmap.size and r.y < @pmap.size and
426
- r.x >= 0 and r.y >= 0
427
- @pmap[r.x][r.y] = nil
428
- end
429
- super
430
- end
431
-
432
- # Given a canvas (mouse) x/y position, return:
433
- #
434
- # false - arrow click
435
- # true - room click
436
- #
437
- def click_type(x, y)
438
- x = (x % WW).to_i
439
- y = (y % HH).to_i
440
-
441
- if x >= WS_2 and y >= HS_2 and
442
- x <= (W + WS_2) and y <= (H + HS_2)
443
- return true
444
- else
445
- return false
446
- end
447
- end
448
-
449
- # Given an x and y canvas position, return room object,
450
- # complex connection or nil
451
- def to_room(x,y)
452
- xx = x / WW
453
- yy = y / HH
454
- return @pmap.at(xx).at(yy)
455
- end
456
-
457
- # Given a mouse click x/y position, return object(s) if any or nil
458
- def to_object(x, y)
459
-
460
- exitA = get_quadrant(x, y)
461
- unless exitA
462
- # Not in arrow section, return element based on pmap
463
- # can be a room or complex arrow connection.
464
- xx = x / WW
465
- yy = y / HH
466
- return nil if xx >= @width or yy >= @height
467
- return @pmap.at(xx).at(yy)
468
- else
469
- # Possible arrow
470
- @sections[@section].connections.each { |c|
471
- a = c.roomA
472
- b = c.roomB
473
- next if not b and @complexConnection
474
-
475
- if c.gpts.size > 0
476
- 2.times { |t|
477
- if t == 0
478
- r = a
479
- dir = r.exits.index(c)
480
- else
481
- r = b
482
- dir = r.exits.rindex(c)
483
- end
484
- next if not r
485
- x1, y1 = r.corner(c, 1, dir)
486
- v = FXRoom::DIR_TO_VECTOR[dir]
487
- x2 = x1 + v[0] * WS
488
- y2 = y1 + v[1] * HS
489
- if x1 == x2
490
- x1 -= W / 2
491
- x2 += W / 2
492
- end
493
- if y1 == y2
494
- y1 -= H / 2
495
- y2 += H / 2
496
- end
497
- x1, x2 = x2, x1 if x2 < x1
498
- y1, y2 = y2, y1 if y2 < y1
499
-
500
- if x >= x1 and x <= x2 and
501
- y >= y1 and y < y2
502
- return c
503
- end
504
- }
505
- else
506
- x1, y1 = a.corner(c, 1, a.exits.index(c))
507
- if b
508
- x2, y2 = b.corner(c, 1, b.exits.rindex(c))
509
- else
510
- dir = a.exits.index(c)
511
- v = FXRoom::DIR_TO_VECTOR[dir]
512
- x2 = x1 + v[0] * WS
513
- y2 = y1 + v[1] * HS
514
- end
515
-
516
- x1, x2 = x2, x1 if x2 < x1
517
- y1, y2 = y2, y1 if y2 < y1
518
- if x1 == x2
519
- x1 -= W / 2
520
- x2 += W / 2
521
- end
522
- if y1 == y2
523
- y1 -= H / 2
524
- y2 += H / 2
525
- end
526
- if x >= x1 and x <= x2 and
527
- y >= y1 and y < y2
528
- return c
529
- end
530
- end
531
- }
532
-
533
- return nil
534
- end
535
-
536
- return nil
537
- end
538
-
539
- #
540
- # Update the map window's title
541
- #
542
- def update_title
543
- title = @name.dup
544
- if @navigation
545
- title << ' #{TITLE_READ_ONLY}'
546
- end
547
- if @automap
548
- title << ' #{TITLE_AUTOMAP}'
549
- end
550
- title << " #{TITLE_ZOOM} %.3f" % @zoom
551
- title << " #{TITLE_SECTION} #{@section+1} #{TITLE_OF} #{@sections.size}"
552
- title << " #{@sections[@section].name}"
553
- @window.title = title
554
- end
555
-
556
- # Change zoom factor of map. Rebuild fonts and canvas sizes.
557
- def zoom=(value)
558
- @zoom = ("%.2f" % value).to_f
559
- # Create the font
560
- fontsize = (11 * @zoom).to_i
561
-
562
- if @window
563
- @font = FXFont.new(@window.getApp, @options['Font Text'], fontsize)
564
- @font.create
565
-
566
- @objfont = FXFont.new(@window.getApp, @options['Font Objects'],
567
- (fontsize * 0.75).to_i)
568
- @objfont.create
569
-
570
- width = (WW * @width * @zoom).to_i
571
- height = (HH * @height * @zoom).to_i
572
- @canvas.width = width
573
- @canvas.height = height
574
-
575
- # Then, create an off-screen image with that same size for double
576
- # buffering
577
- @image.release
578
- @image.destroy
579
- GC.start
580
-
581
- @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
582
- width, height)
583
- @image.create
584
- update_title
585
- end
586
-
587
- end
588
-
589
- # Given a mouse x/y position to WS/HS, return an index
590
- # indicating what quadrant it belongs to.
591
- def get_quadrant(ax, ay)
592
- # First get relative x/y position
593
- x = ax % WW
594
- y = ay % HH
595
-
596
- quadrant = nil
597
- if x < WS_2
598
- #left
599
- if y < HS_2
600
- # top
601
- quadrant = 7
602
- elsif y > H + HS_2
603
- # bottom
604
- quadrant = 5
605
- else
606
- # center
607
- quadrant = 6
608
- end
609
- elsif x > W + WS_2
610
- # right
611
- if y < HS_2
612
- # top
613
- quadrant = 1
614
- elsif y > H + HS_2
615
- # bottom
616
- quadrant = 3
617
- else
618
- # center
619
- quadrant = 2
620
- end
621
- else
622
- #center
623
- if y < HS_2
624
- # top
625
- quadrant = 0
626
- elsif y > H + HS_2
627
- # bottom
628
- quadrant = 4
629
- else
630
- # center
631
- quadrant = nil
632
- end
633
- end
634
-
635
-
636
- return quadrant
637
- end
638
-
639
- # Given an x,y absolute position corresponding to a connection,
640
- # return connected rooms (if any).
641
- def quadrant_to_rooms( q, x, y )
642
- maxX = @width * WW
643
- maxY = @height * HH
644
-
645
- # First check if user tried adding a connection
646
- # at the edges of the map. If so, return empty stuff.
647
- if x < WS_2 or y < HS_2 or
648
- x > maxX - WS_2 or y > maxY - HS_2
649
- return [nil, nil, nil, nil]
650
- end
651
-
652
- x1 = x2 = x
653
- y1 = y2 = y
654
-
655
- case q
656
- when 0, 4
657
- y1 -= HS
658
- y2 += HS
659
- when 1, 5
660
- x1 -= WS
661
- x2 += WS
662
- y1 += HS
663
- y2 -= HS
664
- when 2, 6
665
- x1 -= WS
666
- x2 += WS
667
- when 3, 7
668
- x1 -= WS
669
- y1 -= HS
670
- x2 += WS
671
- y2 += HS
672
- end
673
-
674
- case q
675
- when 0, 5, 6, 7
676
- x1, x2 = x2, x1
677
- y1, y2 = y2, y1
678
- end
679
-
680
- roomA = to_room(x1, y1)
681
- roomB = to_room(x2, y2)
682
- # Oops, user tried to create rooms where we already
683
- # have a complex connection. Don't create anything, then.
684
- if roomA.kind_of?(Connection) or
685
- (roomB.kind_of?(Connection) and not @complexConnection)
686
- return [roomA, roomB, nil, nil]
687
- end
688
-
689
- return [roomA, roomB, [x1, y1], [x2, y2]]
690
- end
691
-
692
- #
693
- # Add a new complex connection (or its first point)
694
- #
695
- def new_complex_connection( x, y )
696
- exitA = get_quadrant(x, y)
697
- unless exitA
698
- raise "not a connection"
699
- end
700
- roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
701
- unless a and roomA and roomA[exitA] == nil
702
- return
703
- end
704
- _new_complex_connection(roomA, exitA)
705
- end
706
-
707
- def _new_complex_connection(roomA, exitA)
708
- if @complexConnection.kind_of?(TrueClass)
709
- @complexConnection = [roomA, exitA]
710
- c = new_connection( roomA, exitA, nil )
711
- status MSG_COMPLEX_CONNECTION_OTHER_EXIT
712
- else
713
- @sections[@section].delete_connection_at(-1)
714
- c = new_connection( @complexConnection[0],
715
- @complexConnection[1], roomA, exitA )
716
- if path_find(c) # Do A* path finding to connect both exits
717
- @modified = true
718
- status MSG_COMPLEX_CONNECTION_DONE
719
- else
720
- @sections[@section].delete_connection_at(-1)
721
- end
722
- draw
723
- @complexConnection = nil
724
- end
725
- end
726
-
727
- #
728
- # Add a new room connection among contiguous rooms.
729
- #
730
- def new_xy_connection( x, y )
731
- exitA = get_quadrant(x, y)
732
- unless exitA
733
- raise "not a connection"
734
- end
735
-
736
- # Then, get rooms being connected
737
- roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
738
- return unless a # User click outside map
739
-
740
- if @options['Create on Connection']
741
- roomA = new_xy_room( a[0], a[1] ) unless roomA
742
- roomB = new_xy_room( b[0], b[1] ) unless roomB
743
- end
744
-
745
- return nil unless roomA and roomB
746
-
747
- if roomA == roomB
748
- raise "error: same room connection"
749
- end
750
-
751
- @modified = true
752
- if roomA and exitA
753
- # get old connection
754
- if roomA[exitA]
755
- c = roomA[exitA]
756
- delete_connection(c) if c.roomB == nil
757
- end
758
- exitB = (exitA + 4) % 8
759
- if roomB[exitB]
760
- c = roomB[exitB]
761
- delete_connection(c) if c.roomB == nil
762
- end
763
- end
764
- begin
765
- new_connection( roomA, exitA, roomB )
766
- rescue Section::ConnectionError
767
- end
768
- end
769
-
770
- #
771
- # Handle mouse button double clicks in canvas
772
- #
773
- def double_click_cb(selection, event)
774
- return unless selection
775
- if selection.kind_of?(FXRoom) or selection.kind_of?(FXConnection)
776
- selection.properties( self, event )
777
- end
778
- end
779
-
780
- # Self-explanatory.
781
- def zoom_out
782
- if @zoom > 0.1
783
- self.zoom -= 0.1
784
- end
785
- end
786
-
787
- # Self-explanatory.
788
- def zoom_in
789
- if @zoom < 1.25
790
- self.zoom += 0.1
791
- end
792
- end
793
-
794
- # Spit out a new message to the status line.
795
- def status(msg)
796
- mw = @window.parent.parent
797
- statusbar = mw.children.find() { |x| x.kind_of?(FXStatusBar) }
798
- s = statusbar.statusLine
799
- s.normalText = s.text = msg
800
- end
801
-
802
- #
803
- # Based on x,y coordinate, switch mouse icon shape
804
- #
805
- def cursor_switch(x, y)
806
- if not @options['Use Room Cursor']
807
- @canvas.defaultCursor = @@cursor_arrow
808
- return
809
- end
810
- q = get_quadrant(x, y)
811
- case q
812
- when 0
813
- @canvas.defaultCursor = @@cursor_n
814
- when 1
815
- @canvas.defaultCursor = @@cursor_ne
816
- when 2
817
- @canvas.defaultCursor = @@cursor_e
818
- when 3
819
- @canvas.defaultCursor = @@cursor_se
820
- when 4
821
- @canvas.defaultCursor = @@cursor_s
822
- when 5
823
- @canvas.defaultCursor = @@cursor_sw
824
- when 6
825
- @canvas.defaultCursor = @@cursor_w
826
- when 7
827
- @canvas.defaultCursor = @@cursor_nw
828
- else
829
- @canvas.defaultCursor = @@cursor_arrow
830
- end
831
- end
832
-
833
- #
834
- # Based on mouse position on canvas, create a tooltip
835
- #
836
- def tooltip_cb(sender, id, ptr)
837
- if @zoom < 0.6 and @tooltip_msg != ''
838
- sender.text = @tooltip_msg.to_s
839
- sender.show
840
- else
841
- sender.hide
842
- end
843
- end
844
-
845
-
846
- #
847
- # Show some help status in status line based on cursor position
848
- #
849
- def help_cb(sender, sel, event)
850
-
851
- x = (event.last_x / @zoom).to_i
852
- y = (event.last_y / @zoom).to_i
853
- if @complexConnection
854
- cursor_switch(x,y)
855
- return
856
- end
857
-
858
- @tooltip_msg = ''
859
- sel = to_object(x, y)
860
- if sel
861
- @canvas.defaultCursor = @@cursor_arrow
862
- if sel.kind_of?(Room)
863
- @tooltip_msg = sel.name
864
- status "\"#{sel.name}\": #{MSG_CLICK_TO_SELECT_AND_MOVE}"
865
- elsif sel.kind_of?(Connection)
866
- status MSG_CLICK_CHANGE_DIR
867
- end
868
- else
869
- if click_type(x, y)
870
- @canvas.defaultCursor = @@cursor_cross
871
- status MSG_CLICK_CREATE_ROOM
872
- else
873
- cursor_switch(x, y)
874
- status MSG_CLICK_CREATE_LINK
875
- end
876
- end
877
- end
878
-
879
- #
880
- # zoom in/out based on mousewheel, keeping position relative
881
- # to cursor position
882
- #
883
- def mousewheel_cb(sender, sel, event)
884
- case event.code
885
- when -120 # Hmm, there does not seem to be constants for these
886
- zoom_out
887
- when 120 # Hmm, there does not seem to be constants for these
888
- zoom_in
889
- end
890
- end
891
-
892
- #
893
- # Handle middle mouse button click in canvas
894
- #
895
- def mmb_click_cb(server, sel, event)
896
- @canvas.grab
897
- @dx = @dy = 0
898
- @mouseButton = MIDDLEBUTTON
899
- end
900
-
901
- #
902
- # Select all rooms and connections within (drag) rectangle
903
- #
904
- def select_rectangle(x1, y1, x2, y2)
905
- x1, x2 = x2, x1 if x2 < x1
906
- y1, y2 = y2, y1 if y2 < y1
907
-
908
- x = x1 * zoom
909
- y = y1 * zoom
910
- w = x2 - x1
911
- h = y2 - y1
912
-
913
- x1 = ((x1 - WS_2) / WW).floor
914
- y1 = ((y1 - HS_2) / HH).floor
915
- x2 = ((x2 - WS_2) / WW).ceil
916
- y2 = ((y2 - HS_2) / HH).ceil
917
-
918
- @sections[@section].rooms.each { |r|
919
- if r.x >= x1 and r.x <= x2 and
920
- r.y >= y1 and r.y <= y2
921
- r.selected = true
922
- r.update_properties(self)
923
- else
924
- r.selected = false
925
- end
926
- }
927
-
928
- @sections[@section].connections.each { |c|
929
- next if not c.roomB
930
- a = c.roomA
931
- b = c.roomB
932
-
933
- if (a.x >= x1 and a.x <= x2 and
934
- a.y >= y1 and a.y <= y2) or
935
- (b.x >= x1 and b.x <= x2 and
936
- b.y >= y1 and b.y <= y2)
937
- c.selected = true
938
- c.update_properties(self)
939
- else
940
- c.selected = false
941
- end
942
- }
943
-
944
- draw
945
- dc = FXDCWindow.new(@canvas)
946
- dc.function = BLT_NOT_SRC_XOR_DST
947
- dc.drawRectangle(x, y, w * @zoom, h * @zoom)
948
- dc.end
949
- end
950
-
951
- #
952
- # Handle mouse motion in canvas
953
- #
954
- def motion_cb(server, sel, event)
955
- if @mouseButton == MIDDLEBUTTON
956
- @canvas.dragCursor = @@cursor_move
957
- pos = @scrollwindow.position
958
- dx = event.last_x - event.win_x
959
- dy = event.last_y - event.win_y
960
- if dx != 0 or dy != 0 and not (dx == @dx and dy == @dy)
961
- pos[0] += dx
962
- pos[1] += dy
963
- @dx = dx
964
- @dy = dy
965
- @scrollwindow.setPosition(pos[0], pos[1])
966
- @canvas.repaint
967
- end
968
- elsif @mouseButton == LEFTBUTTON
969
- x = (event.last_x / @zoom).to_i
970
- y = (event.last_y / @zoom).to_i
971
- select_rectangle( @dx, @dy, x, y )
972
- else
973
- help_cb(server, sel, event)
974
- end
975
- end
976
-
977
- #
978
- # Handle release of middle mouse button
979
- #
980
- def mmb_release_cb(server, sel, event)
981
- if @mouseButton
982
- @canvas.ungrab
983
- @canvas.dragCursor = @@cursor_arrow
984
- if @mouseButton == LEFTBUTTON
985
- draw
986
- else
987
- @canvas.repaint
988
- end
989
- @mouseButton = nil
990
- end
991
- end
992
-
993
- #
994
- # Given a room, center scrollwindow so room is visible
995
- #
996
- def center_view_on_room(r)
997
- xx = (r.xx + W / 2) * @zoom
998
- yy = (r.yy + H / 2) * @zoom
999
- center_view_on_xy(xx, yy)
1000
- end
1001
-
1002
- #
1003
- # Given an x and y coordinate for the canvas, center on it
1004
- #
1005
- def center_view_on_xy(xx, yy)
1006
- cw = @scrollwindow.getContentWidth
1007
- ch = @scrollwindow.getContentHeight
1008
- w = @scrollwindow.getViewportWidth
1009
- h = @scrollwindow.getViewportHeight
1010
-
1011
- xx -= w / 2
1012
- yy -= h / 2
1013
- maxx = cw - w / 2
1014
- maxy = ch - h / 2
1015
- if xx > maxx
1016
- xx = maxx
1017
- elsif xx < 0
1018
- xx = 0
1019
- end
1020
- if yy > maxy
1021
- yy = maxy
1022
- elsif yy < 0
1023
- yy = 0
1024
- end
1025
-
1026
- @scrollwindow.setPosition( -xx, -yy )
1027
- end
1028
-
1029
- #
1030
- # Return current selection as an array of rooms and an array of
1031
- # connections
1032
- #
1033
- def get_selection
1034
- rooms = @sections[@section].rooms.find_all { |r| r.selected }
1035
- conns = @sections[@section].connections.find_all { |r| r.selected }
1036
- return rooms, conns
1037
- end
1038
-
1039
- #
1040
- # Clear rooms/connections selected
1041
- #
1042
- def clear_selection
1043
- @sections[@section].rooms.each { |r| r.selected = false }
1044
- @sections[@section].connections.each { |r| r.selected = false }
1045
- end
1046
-
1047
- #
1048
- # Add a proper link submenu option for an exit
1049
- #
1050
- def rmb_link_menu(submenu, c, room, idx, old_idx)
1051
- return if room[idx] == c
1052
- dir = Room::DIRECTIONS[idx]
1053
- dir = dir.upcase
1054
- if room[idx]
1055
- cmd = FXMenuCommand.new(submenu, "Switch with link in #{dir} Exit")
1056
- cmd.connect(SEL_COMMAND) {
1057
- c2 = room[idx]
1058
- room[old_idx] = c2
1059
- room[idx] = c
1060
- create_pathmap
1061
- draw
1062
- }
1063
- else
1064
- cmd = FXMenuCommand.new(submenu, "Move link to #{dir} Exit")
1065
- cmd.connect(SEL_COMMAND) {
1066
- room[old_idx] = nil
1067
- room[idx] = c
1068
- create_pathmap
1069
- draw
1070
- }
1071
- end
1072
- end
1073
-
1074
- #
1075
- # Handle right mouse button click
1076
- #
1077
- def rmb_click_cb(sender, sel, event)
1078
- rooms, links = get_selection
1079
-
1080
- menu = nil
1081
- if not links.empty? and links.size == 1
1082
- c = links[0]
1083
- a = c.roomA
1084
- b = c.roomB
1085
- menu = FXMenuPane.new(@window)
1086
- if c.dir == Connection::AtoB
1087
- cmd = FXMenuCommand.new(menu, "Flip Direction")
1088
- cmd.connect(SEL_COMMAND) { c.flip; draw }
1089
- FXMenuSeparator.new(menu)
1090
- end
1091
-
1092
- submenu = FXMenuPane.new(@window)
1093
- old_idx = a.exits.index(c)
1094
- 0.upto(7) { |idx|
1095
- rmb_link_menu( submenu, c, a, idx, old_idx )
1096
- }
1097
- FXMenuCascade.new(menu, a.name, nil, submenu)
1098
- if b
1099
- submenu = FXMenuPane.new(@window)
1100
- old_idx = b.exits.rindex(c)
1101
- 0.upto(7) { |idx|
1102
- rmb_link_menu( submenu, c, b, idx, old_idx )
1103
- }
1104
- FXMenuCascade.new(menu, b.name, nil, submenu)
1105
- end
1106
- end
1107
- if menu
1108
- menu.create
1109
- menu.popup(nil, event.root_x, event.root_y)
1110
- @window.getApp.runModalWhileShown(menu)
1111
- end
1112
- end
1113
-
1114
-
1115
- #
1116
- # Handle left mouse button click
1117
- #
1118
- def lmb_click_cb(sender, sel, event)
1119
- x = (event.last_x / @zoom).to_i
1120
- y = (event.last_y / @zoom).to_i
1121
-
1122
- if event.state & ALTMASK != 0
1123
- mmb_click_cb(sender, sel, event)
1124
- return
1125
- end
1126
-
1127
- selection = to_object(x, y)
1128
-
1129
- if event.state & SHIFTMASK != 0
1130
- @mouseButton = LEFTBUTTON
1131
- @canvas.grab
1132
- @dx, @dy = [ x, y ]
1133
- return if not selection
1134
- end
1135
-
1136
-
1137
- if event.click_count == 2
1138
- double_click_cb(selection, event)
1139
- return
1140
- end
1141
-
1142
- unless selection
1143
- clear_selection
1144
-
1145
- # If in navigation mode, we don't allow user to modify map.
1146
- return if @navigation
1147
-
1148
- # if we did not select anything, check to see if we
1149
- # clicked in a room area or connection area.
1150
- if click_type(x, y)
1151
- return if @complexConnection
1152
- # Add a new room
1153
- roomB = @sections[@section].rooms[-1]
1154
- roomA = new_xy_room( x, y )
1155
- if roomB and roomA and @options['Automatic Connection']
1156
- # check to see if rooms are next to each other
1157
- # if so, try to connect them (assuming there's no connection there
1158
- # already).
1159
- exitB = roomB.next_to?(roomA)
1160
- if exitB and roomB.exits[exitB] == nil
1161
- new_connection( roomB, exitB, roomA )
1162
- end
1163
- end
1164
- else
1165
- # Add a new connection
1166
- if @complexConnection
1167
- new_complex_connection(x, y)
1168
- else
1169
- # Add a new simple connection (plus rooms if needed)
1170
- if event.state & CONTROLMASK != 0
1171
- exitA = get_quadrant(x, y)
1172
- roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
1173
- if not roomA
1174
- new_xy_connection( x, y )
1175
- else
1176
- if a and roomA
1177
- @complexConnection = [roomA, exitA]
1178
- new_connection( roomA, exitA, nil )
1179
- _new_complex_connection( roomA, exitA )
1180
- end
1181
- end
1182
- else
1183
- new_xy_connection( x, y )
1184
- end
1185
- end
1186
- end
1187
- else
1188
- if selection.kind_of?(Connection) and selection.selected
1189
- # Toggle arrow direction
1190
- selection.toggle_direction
1191
- draw
1192
- return
1193
- else
1194
- if event.state & SHIFTMASK == 0 and
1195
- event.state & CONTROLMASK == 0
1196
- clear_selection
1197
- end
1198
- # Select the stuff
1199
- selection.update_properties(self)
1200
- if event.state & CONTROLMASK != 0
1201
- selection.selected ^= true # toggle selection
1202
- else
1203
- selection.selected = true
1204
- end
1205
- end
1206
- end
1207
- draw(sender, sel, event)
1208
- end
1209
-
1210
- #
1211
- # Close the map. Pop up a warning box to allow saving
1212
- # if map has been modified.
1213
- #
1214
- def close_cb()
1215
- if @modified
1216
- dlg = FXDialogBox.new( @window.parent, "Warning",
1217
- DECOR_ALL,
1218
- 0, 0, 400, 130)
1219
- # Frame
1220
- s = FXVerticalFrame.new(dlg,
1221
- LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
1222
-
1223
- f = FXHorizontalFrame.new(s, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
1224
-
1225
- font = FXFont.new(@window.getApp, "Helvetica", 30)
1226
- font.create
1227
- oops = FXLabel.new(f, "!", nil, 0, LAYOUT_SIDE_LEFT|LAYOUT_FILL_X|
1228
- LAYOUT_CENTER_Y)
1229
- oops.frameStyle = FRAME_RAISED|FRAME_THICK
1230
- oops.baseColor = 'dark grey'
1231
- oops.textColor = 'red'
1232
- oops.padLeft = oops.padRight = 15
1233
- oops.shadowColor = 'black'
1234
- oops.borderColor = 'white'
1235
- oops.font = font
1236
-
1237
- FXLabel.new(f, "\n#{@name} was modified.\n" +
1238
- "Should I save the changes before closing?",
1239
- nil, 0)
1240
-
1241
- # Separator
1242
- FXHorizontalSeparator.new(s,
1243
- LAYOUT_SIDE_TOP|LAYOUT_FILL_X|SEPARATOR_GROOVE)
1244
-
1245
- # Bottom buttons
1246
- buttons = FXHorizontalFrame.new(s,
1247
- LAYOUT_SIDE_BOTTOM|FRAME_NONE|
1248
- LAYOUT_FILL_X|PACK_UNIFORM_WIDTH)
1249
- # Accept
1250
- yes = FXButton.new(buttons, BUTTON_YES, nil, dlg, FXDialogBox::ID_ACCEPT,
1251
- FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1252
- LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1253
- yes.connect(SEL_COMMAND) {
1254
- dlg.close
1255
- if save
1256
- @window.close
1257
- return true
1258
- else
1259
- return false
1260
- end
1261
- }
1262
- FXButton.new(buttons, BUTTON_NO, nil, dlg, FXDialogBox::ID_ACCEPT,
1263
- FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1264
- LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1265
-
1266
- # Cancel
1267
- FXButton.new(buttons, BUTTON_CANCEL, nil, dlg, FXDialogBox::ID_CANCEL,
1268
- FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1269
- LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1270
- yes.setDefault
1271
- yes.setFocus
1272
-
1273
- return false if dlg.execute == 0
1274
- end
1275
- @automap.destroy if @automap
1276
- @automap = nil
1277
- @window.close
1278
- return true
1279
- end
1280
-
1281
- #
1282
- # Create cursors
1283
- #
1284
- def _make_cursors
1285
- pix = []
1286
- 32.times { 32.times { pix << 255 << 255 << 255 << 255 } }
1287
- pix = pix.pack('c*')
1288
-
1289
-
1290
- ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'].each { |d|
1291
- eval(<<-"EOF")
1292
- @@cursor_#{d} = FXGIFCursor.new(@window.getApp, pix)
1293
- FXFileStream.open('icons/room_#{d}.gif', FXStreamLoad) { |stream|
1294
- @@cursor_#{d}.loadPixels(stream)
1295
- }
1296
- @@cursor_#{d}.create
1297
- EOF
1298
- }
1299
-
1300
- @@cursor_move = FXCursor.new(@window.getApp, CURSOR_MOVE)
1301
- @@cursor_move.create
1302
-
1303
- @@cursor_arrow = FXCursor.new(@window.getApp, CURSOR_ARROW)
1304
- @@cursor_arrow.create
1305
-
1306
- @@cursor_cross = FXCursor.new(@window.getApp, CURSOR_CROSS)
1307
- @@cursor_cross.create
1308
- end
1309
-
1310
- #
1311
- # Create widgets for this map window
1312
- #
1313
- def _make_widgets
1314
- @scrollwindow = FXScrollWindow.new(@window,
1315
- SCROLLERS_NORMAL|SCROLLERS_TRACK)
1316
- width = WW * @width
1317
- height = HH * @height
1318
-
1319
- @canvasFrame = FXVerticalFrame.new(@scrollwindow,
1320
- FRAME_SUNKEN|LAYOUT_FILL_X|
1321
- LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
1322
-
1323
- # First, create an off-screen image for drawing and double buffering
1324
- @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
1325
- width, height)
1326
- @image.create
1327
-
1328
- # Then create canvas
1329
- @canvas = FXCanvas.new(@canvasFrame, nil, 0,
1330
- LAYOUT_FIX_WIDTH|LAYOUT_FIX_HEIGHT|
1331
- LAYOUT_TOP|LAYOUT_LEFT,
1332
- 0, 0, width, height)
1333
- @dirty = true
1334
- # @canvas.connect(SEL_UPDATE, method(:update_cb))
1335
-
1336
- @canvas.connect(SEL_PAINT, method(:draw))
1337
- @canvas.backColor = @options['BG Color']
1338
-
1339
- @canvas.connect(SEL_MOUSEWHEEL, method(:mousewheel_cb))
1340
- @canvas.connect(SEL_LEFTBUTTONPRESS, method(:lmb_click_cb))
1341
- @canvas.connect(SEL_LEFTBUTTONRELEASE, method(:mmb_release_cb))
1342
- @canvas.connect(SEL_MOTION, method(:motion_cb))
1343
- @canvas.connect(SEL_MIDDLEBUTTONPRESS, method(:mmb_click_cb))
1344
- @canvas.connect(SEL_MIDDLEBUTTONRELEASE, method(:mmb_release_cb))
1345
- @canvas.connect(SEL_RIGHTBUTTONPRESS, method(:rmb_click_cb))
1346
- @canvas.connect(SEL_KEYPRESS, method(:keypress_cb))
1347
-
1348
- if fxversion !~ /^1.2/
1349
- @@tooltip = FXToolTip.new(@canvas.app, FXToolTip::TOOLTIP_PERMANENT)
1350
- # Tooltip is too buggy and annoying. Turning it off for now.
1351
- # @canvas.connect(SEL_QUERY_TIP, method(:tooltip_cb))
1352
- end
1353
- end
1354
-
1355
- #
1356
- # Main constructor. Create the object, initialize the pathmap
1357
- # and initialize the widgets and cursors if a parent window is passed.
1358
- #
1359
- def initialize(name, parent = nil, default_options = nil,
1360
- icon = nil, menu = nil, mode = nil,
1361
- x = 0, y = 0, w = 0, h = 0)
1362
- super(name)
1363
- @mutex = Mutex.new
1364
- @automap = nil
1365
- @navigation = false
1366
- if parent
1367
- @window = FXMDIChild.new(parent, name, icon, menu, mode, x, y, w, h)
1368
- @options = default_options
1369
- _make_cursors if not @@cursor_arrow
1370
- _make_widgets
1371
- end
1372
- empty_pathmap
1373
- self.zoom = 0.8
1374
- end
1375
-
1376
- #
1377
- # Handle 'cutting' any selected rooms and/or connections
1378
- #
1379
- def _cut_selected
1380
- rooms = @sections[@section].rooms.find_all { |r| r.selected }
1381
- conns = @sections[@section].connections.find_all { |c| c.selected }
1382
- ############################
1383
- # First, handle rooms...
1384
- ############################
1385
- # Remove rooms from path map
1386
- rooms.each { |r| @pmap[r.x][r.y] = nil }
1387
- # Remove rooms from current section in map
1388
- @sections[@section].rooms -= rooms
1389
- # Add any connections pointing to removed rooms as connection to remove
1390
- rooms.each { |r|
1391
- conns += r.exits.find_all { |e| e != nil }
1392
- }
1393
-
1394
- #########################
1395
- # Now, handle connections
1396
- #########################
1397
- conns.uniq!
1398
-
1399
- # Remove connections from path map
1400
- conns.each { |c| clean_path(c) }
1401
- # Remove connections from current section in map
1402
- @sections[@section].connections -= conns
1403
-
1404
- return conns
1405
- end
1406
-
1407
- def cut_selected
1408
- _cut_selected
1409
- modified = true
1410
- draw
1411
- end
1412
-
1413
- #
1414
- # Handle deleting selected rooms and/or connections
1415
- #
1416
- def delete_selected
1417
- conns = _cut_selected
1418
-
1419
- # Remove room exits pointing to any removed connection
1420
- conns.each { |c|
1421
- a = c.roomA
1422
- b = c.roomB
1423
- a[a.exits.index(c)] = nil
1424
- if b
1425
- idx = b.exits.rindex(c)
1426
- b[idx] = nil if idx
1427
- end
1428
- }
1429
-
1430
- modified = true
1431
- create_pathmap
1432
- draw
1433
- end
1434
-
1435
- #
1436
- # Start a complex connection
1437
- #
1438
- def complex_connection
1439
- return if @complexConnection or @navigation
1440
- @complexConnection = true
1441
- status MSG_COMPLEX_CONNECTION
1442
- end
1443
-
1444
- #
1445
- # Given a selection of rooms, clear all of them from path map
1446
- #
1447
- def clean_room_selection(selection)
1448
- selection.each { |r|
1449
- @pmap[r.x][r.y] = nil
1450
- clean_exits(r)
1451
- }
1452
- end
1453
-
1454
- #
1455
- # Given a selection of rooms, clear all of them from path map
1456
- #
1457
- def store_room_selection(selection)
1458
- selection.each { |r|
1459
- @pmap[r.x][r.y] = r
1460
- }
1461
- update_exits(selection)
1462
- end
1463
-
1464
- #
1465
- # Clean all paths from path map for a room
1466
- #
1467
- def clean_exits(room)
1468
- room.exits.each { |c|
1469
- next if not c
1470
- clean_path(c)
1471
- }
1472
- end
1473
-
1474
- def show_roomlist
1475
- if @@roomlist
1476
- @@roomlist.copy_from(self)
1477
- else
1478
- require 'IFMapper/FXRoomList'
1479
- @@roomlist = FXRoomList.new(self)
1480
- end
1481
- @@roomlist.show
1482
- end
1483
-
1484
- def self.no_maps
1485
- @@roomlist.hide if @@roomlist
1486
- @@win.hide if @@win
1487
- FXRoom::no_maps
1488
- FXConnection::no_maps
1489
- end
1490
-
1491
- #
1492
- # If roomlist window is present, update it
1493
- #
1494
- def update_roomlist
1495
- @@roomlist.copy_from(self) if @@roomlist
1496
- @@win.copy_from(self) if @@win
1497
- end
1498
-
1499
- #
1500
- # Find and update all paths in path map for a room
1501
- #
1502
- def update_exits(selection)
1503
- create_pathmap
1504
- @modified = true
1505
- end
1506
-
1507
-
1508
- #
1509
- # Return the current active room
1510
- #
1511
- def current_room
1512
- rooms = @sections[@section].rooms.find_all { |r| r.selected }
1513
- return nil if rooms.empty? or rooms.size > 1
1514
- return rooms[0]
1515
- end
1516
-
1517
- def cannot_automap(why)
1518
- w = FXWarningBox.new(@window, "#{ERR_CANNOT_AUTOMAP}\n#{why}")
1519
- w.execute
1520
- end
1521
-
1522
-
1523
- #
1524
- # Give user some warning if he tries to modify a read-only mode.
1525
- #
1526
- def navigation_warning
1527
- w = FXWarningBox.new(@window, ERR_READ_ONLY_MAP)
1528
- w.execute
1529
- end
1530
-
1531
-
1532
- #
1533
- # Move selected rooms in one of the 8 cardinal directions.
1534
- #
1535
- def move_to(idx)
1536
- return navigation_warning if @navigation
1537
- selection = @sections[@section].rooms.find_all { |r| r.selected }
1538
- return if selection.empty?
1539
- clean_room_selection(selection)
1540
-
1541
-
1542
- dx, dy = Room::DIR_TO_VECTOR[idx]
1543
-
1544
- # Check that all nodes can be moved in the specified direction
1545
- selection.each { |r|
1546
- x = r.x + dx
1547
- y = r.y + dy
1548
- if x < 0 or y < 0 or x >= @width or y >= @height or
1549
- @pmap.at(x).at(y).kind_of?(Room)
1550
- store_room_selection(selection)
1551
- dir = Room::DIRECTIONS[idx]
1552
- status "#{ERR_CANNOT_MOVE_SELECTION} #{dir}."
1553
- return
1554
- end
1555
- }
1556
- selection.each { |r|
1557
- r.x += dx
1558
- r.y += dy
1559
- }
1560
- update_exits(selection)
1561
- draw
1562
- end
1563
-
1564
- #
1565
- # Move through an exit into another room. If exit is empty, create
1566
- # a new neighboring room.
1567
- #
1568
- def move_thru(idx)
1569
- room = current_room
1570
- return if not room
1571
- exit = room[idx]
1572
- if exit
1573
- room.selected = false
1574
- if room == exit.roomA
1575
- roomB = exit.roomB
1576
- else
1577
- roomB = exit.roomA
1578
- end
1579
- roomB.selected = true
1580
- draw
1581
- else
1582
- return navigation_warning if @navigation
1583
- x, y = room.x, room.y
1584
- dx, dy = Room::DIR_TO_VECTOR[idx]
1585
- x += dx
1586
- y += dy
1587
- x = 0 if x < 0
1588
- y = 0 if y < 0
1589
- x = @width-1 if x > @width-1
1590
- y = @height-1 if y > @height-1
1591
- if not @pmap.at(x).at(y).kind_of?(Room)
1592
- room.selected = false
1593
- roomB = new_xy_room(x * WW, y * HH)
1594
- exitB = roomB.next_to?(room)
1595
- if exitB and roomB.exits[exitB] == nil
1596
- new_connection( roomB, exitB, room )
1597
- end
1598
- draw
1599
- end
1600
- end
1601
- end
1602
-
1603
- #
1604
- # Handle a keypress
1605
- #
1606
- def keypress_cb( server, sel, event)
1607
- case event.code
1608
- when KEY_Escape
1609
- if @complexConnection
1610
- if @complexConnection.kind_of?(Array)
1611
- @sections[@section].delete_connection_at(-1)
1612
- status MSG_COMPLEX_CONNECTION_STOPPED
1613
- draw
1614
- end
1615
- @complexConnection = false
1616
- end
1617
- when KEY_BackSpace, KEY_Delete
1618
- return navigation_warning if @navigation
1619
- delete_selected
1620
- when KEY_c
1621
- if event.state & CONTROLMASK != 0
1622
- FXMapperWindow::copy_selected(self)
1623
- draw
1624
- end
1625
- when KEY_v
1626
- if event.state & CONTROLMASK != 0
1627
- FXMapperWindow::paste_selected(self)
1628
- @modified = true
1629
- draw
1630
- end
1631
- when KEY_x
1632
- return navigation_warning if @navigation
1633
- if event.state & CONTROLMASK != 0
1634
- FXMapperWindow::cut_selected(self)
1635
- @modified = true
1636
- draw
1637
- else
1638
- complex_connection
1639
- end
1640
- when KEY_KP_8
1641
- move_thru(0)
1642
- when KEY_KP_9
1643
- move_thru(1)
1644
- when KEY_KP_6
1645
- move_thru(2)
1646
- when KEY_KP_3
1647
- move_thru(3)
1648
- when KEY_KP_2
1649
- move_thru(4)
1650
- when KEY_KP_1
1651
- move_thru(5)
1652
- when KEY_KP_4
1653
- move_thru(6)
1654
- when KEY_KP_7
1655
- move_thru(7)
1656
- when KEY_Up
1657
- move_to(0)
1658
- when KEY_Down
1659
- move_to(4)
1660
- when KEY_Left
1661
- move_to(6)
1662
- when KEY_Right
1663
- move_to(2)
1664
- when KEY_End
1665
- move_to(5)
1666
- when KEY_Home
1667
- move_to(7)
1668
- when KEY_Page_Up
1669
- move_to(1)
1670
- when KEY_Page_Down
1671
- move_to(3)
1672
- end
1673
- end
1674
-
1675
- #
1676
- # Draw template of diagonal connections in grid background
1677
- #
1678
- def draw_diagonal_connections(dc, event)
1679
- ww = WW * @zoom
1680
- hh = HH * @zoom
1681
-
1682
- w = W * @zoom
1683
- h = H * @zoom
1684
-
1685
- ws = WS * @zoom
1686
- hs = HS * @zoom
1687
-
1688
- ws_2 = WS_2 * @zoom
1689
- hs_2 = HS_2 * @zoom
1690
-
1691
- maxy = @height - 1
1692
- maxx = @width - 1
1693
-
1694
- (0...@height).each { |yy|
1695
- (0...@width).each { |xx|
1696
- next if @pmap.at(xx).at(yy).kind_of?(Connection)
1697
- x = xx * ww
1698
- y = yy * hh
1699
-
1700
- if yy < maxy and xx < maxx
1701
- # First, draw \
1702
- x1 = x + w + ws_2
1703
- y1 = y + h + hs_2
1704
-
1705
- x2 = x1 + ws
1706
- y2 = y1 + hs
1707
- dc.drawLine( x1, y1, x2, y2 )
1708
-
1709
- end
1710
-
1711
- if yy < maxy and xx > 0 and xx <= maxx
1712
- # Then, draw /
1713
- x1 = x + ws_2
1714
- y1 = y + h + hs_2
1715
-
1716
- x2 = x1 - ws
1717
- y2 = y1 + hs
1718
- dc.drawLine( x2, y2, x1, y1 )
1719
- end
1720
- }
1721
- }
1722
- end
1723
-
1724
- #
1725
- # Draw template of straight connections in grid background
1726
- #
1727
- def draw_straight_connections(dc, event)
1728
- ww = WW * @zoom
1729
- hh = HH * @zoom
1730
-
1731
- w = W * @zoom
1732
- h = H * @zoom
1733
-
1734
- ws_2 = WS_2 * @zoom
1735
- hs_2 = HS_2 * @zoom
1736
-
1737
- #---- dummy check to catch an ugly bug that I cannot track...
1738
- create_pathmap if @pmap.size < @width or @pmap[0].size < @height
1739
-
1740
- # First, draw horizontal lines
1741
- (0...@height).each { |yy|
1742
- (0..@width-2).each { |xx|
1743
- next if @pmap.at(xx).at(yy).kind_of?(Connection) or
1744
- @pmap.at(xx+1).at(yy).kind_of?(Connection)
1745
- x1 = xx * ww + w + ws_2
1746
- x2 = (xx + 1) * ww + ws_2
1747
- y1 = yy * hh + h / 2 + hs_2
1748
-
1749
- dc.drawLine( x1, y1, x2, y1 )
1750
- }
1751
- }
1752
-
1753
- # Then, draw vertical lines
1754
- (0...@width).each { |xx|
1755
- (0..@height-2).each { |yy|
1756
- next if @pmap.at(xx).at(yy).kind_of?(Connection) or
1757
- @pmap.at(xx).at(yy+1).kind_of?(Connection)
1758
- x1 = xx * ww + w / 2 + ws_2
1759
- y1 = yy * hh + h + hs_2
1760
- y2 = (yy + 1) * hh + hs_2
1761
-
1762
- dc.drawLine( x1, y1, x1, y2 )
1763
- }
1764
- }
1765
- end
1766
-
1767
-
1768
-
1769
- #
1770
- # Draw template of room squares in background
1771
- #
1772
- def draw_grid(dc, event = nil)
1773
-
1774
- dc.foreground = "black"
1775
- dc.lineWidth = 0
1776
- dc.lineStyle = LINE_ONOFF_DASH
1777
-
1778
- ww = WW * @zoom
1779
- hh = HH * @zoom
1780
-
1781
- w = W * @zoom
1782
- h = H * @zoom
1783
-
1784
- ws_2 = WS_2 * @zoom
1785
- hs_2 = HS_2 * @zoom
1786
-
1787
- (0...@width).each { |xx|
1788
- (0...@height).each { |yy|
1789
- next if @pmap.at(xx).at(yy)
1790
- x = xx * ww + ws_2
1791
- y = yy * hh + hs_2
1792
- dc.drawRectangle( x, y, w, h )
1793
- }
1794
- }
1795
- end
1796
-
1797
- #
1798
- # Clean background to solid color
1799
- #
1800
- def draw_background(dc, event = nil)
1801
- dc.foreground = @options['BG Color']
1802
- dc.fillRectangle(0,0, @canvas.width, @canvas.height)
1803
- end
1804
-
1805
- #
1806
- # Draw connections among rooms
1807
- #
1808
- def draw_connections(dc)
1809
- dc.lineStyle = LINE_SOLID
1810
- dc.lineWidth = 3 * @zoom
1811
- dc.lineWidth = 3 if dc.lineWidth < 3
1812
- @sections[@section].connections.each { |c| c.draw(dc, @zoom, @options) }
1813
- end
1814
-
1815
- #
1816
- # Draw a single room (callback used when editing room dialog box)
1817
- #
1818
- def draw_room(room)
1819
- idx = @sections[@section].rooms.index(room)
1820
- return unless idx
1821
-
1822
- dc = FXDCWindow.new(@canvas)
1823
- dc.font = @font
1824
- data = { }
1825
- data['font'] = @font
1826
- data['objfont'] = @objfont
1827
- room.draw(dc, @zoom, idx, @options, data)
1828
- dc.end
1829
- end
1830
-
1831
- #
1832
- # Draw all rooms in current section
1833
- #
1834
- def draw_rooms(dc)
1835
- data = { }
1836
- data['font'] = @font
1837
- data['objfont'] = @objfont
1838
- @sections[@section].rooms.each_with_index { |room, idx|
1839
- room.draw(dc, @zoom, idx, @options, data)
1840
- }
1841
- end
1842
-
1843
- #
1844
- # Draw mapname
1845
- #
1846
- def draw_mapname(dc)
1847
- fontsize = (24 * @zoom).to_i
1848
- font = FXFont.new(@window.getApp, @options['Font Text'], fontsize)
1849
- font.create
1850
-
1851
- x = @width * WW / 2.0 - @name.size * 24
1852
- dc.drawText(x, 30, @name)
1853
- end
1854
-
1855
- #
1856
- # Print map
1857
- #
1858
- def print(printer)
1859
- # dc = FXDCPrint.new(@window.getApp)
1860
- require 'IFMapper/MapPrinting'
1861
- require 'IFMapper/FXDCPostscript'
1862
- oldzoom = @zoom
1863
- oldsection = @section
1864
- self.zoom = 1.0
1865
-
1866
- num = pack_sections( @width, @height )
1867
- begin
1868
- dc = FXDCPostscript.new(@window.getApp)
1869
- xmax = @width * WW
1870
- ymax = @height * HH
1871
- dc.setContentRange(0, 0, xmax, ymax)
1872
- dc.beginPrint(printer) {
1873
- page = -1
1874
- 0.upto(@sections.size-1) { |p|
1875
- self.section = p
1876
- clear_selection
1877
- if page != sect.page
1878
- dc.beginPage(sect.page)
1879
- draw_mapname( dc )
1880
- end
1881
-
1882
- dc.lineCap = CAP_ROUND
1883
- # draw_grid(dc)
1884
- draw_connections(dc)
1885
- draw_rooms(dc)
1886
-
1887
- if page != sect.page
1888
- page = sect.page
1889
- dc.endPage()
1890
- end
1891
- }
1892
- }
1893
- rescue => e
1894
- status "#{e}"
1895
- end
1896
- self.section = oldsection
1897
- self.zoom = oldzoom
1898
- draw
1899
- end
1900
-
1901
-
1902
- #
1903
- # Draw map
1904
- #
1905
- def draw(sender = nil, sel = nil, event = nil)
1906
- return if @mutex.locked? or not @canvas.created?
1907
-
1908
- if not @image.created?
1909
- # puts "Image was not created. Try again"
1910
- self.zoom = @zoom
1911
- end
1912
-
1913
- pos = @scrollwindow.position
1914
- w = @scrollwindow.getViewportWidth
1915
- h = @scrollwindow.getViewportHeight
1916
-
1917
- # The -5 seems to be a bug in fox. don't ask me.
1918
- cx = -pos[0]-5
1919
- cx = 0 if cx < 0
1920
- cy = -pos[1]-5
1921
- cy = 0 if cy < 0
1922
-
1923
- dc = FXDCWindow.new(@image)
1924
- dc.setClipRectangle( cx, cy, w, h)
1925
- dc.font = @font
1926
- #dc.lineCap = CAP_ROUND
1927
- draw_background(dc, event)
1928
- draw_grid(dc, event) if @options['Grid Boxes']
1929
- if @options['Grid Straight Connections']
1930
- draw_straight_connections(dc, event)
1931
- end
1932
- if @options['Grid Diagonal Connections']
1933
- draw_diagonal_connections(dc, event)
1934
- end
1935
- draw_connections(dc)
1936
- draw_rooms(dc)
1937
- dc.end
1938
-
1939
-
1940
- # Blit the off-screen image into canvas
1941
- dc = FXDCWindow.new(@canvas)
1942
- dc.setClipRectangle( cx, cy, w, h)
1943
- dc.drawImage(@image,0,0)
1944
- dc.end
1945
-
1946
- end
1947
-
1948
-
1949
- #
1950
- # Perform the actual saving of the map
1951
- #
1952
- def _save
1953
- if @complexConnection
1954
- # If we have an incomplete connection, remove it
1955
- @sections[@section].delete_connection_at(-1)
1956
- end
1957
-
1958
- if @filename !~ /\.map$/i
1959
- @filename << '.map'
1960
- end
1961
-
1962
- status "#{MSG_SAVING} '#{@filename}'..."
1963
-
1964
- # Make sure we save a valid map. This is mainly a fail-safe
1965
- # in case of an autosave due to a bug.
1966
- verify_integrity
1967
-
1968
- @version = FILE_FORMAT_VERSION
1969
- begin
1970
- f = File.open(@filename, "wb")
1971
- f.puts Marshal.dump(self)
1972
- f.close
1973
- rescue => e
1974
- status "#{ERR_COULD_NOT_SAVE} '#{@filename}': #{e}"
1975
- sleep 4
1976
- return false
1977
- end
1978
- @modified = false
1979
- status "#{MSG_SAVED} '#{@filename}'."
1980
- sleep 0.5
1981
- return true
1982
- end
1983
-
1984
- #
1985
- # Save the map. If the map's filename is not defined, call save_as
1986
- #
1987
- def save
1988
- unless @filename
1989
- save_as
1990
- else
1991
- _save
1992
- end
1993
- end
1994
-
1995
-
1996
- #
1997
- # Export map as an IFM map file
1998
- #
1999
- def export_ifm(file)
2000
- require 'IFMapper/IFMWriter'
2001
- file += '.ifm' if file !~ /\.ifm$/
2002
- IFMWriter.new(self, file)
2003
- end
2004
-
2005
-
2006
- #
2007
- # Export map as a set of TADS3 source code files
2008
- #
2009
- def export_tads(file)
2010
- require 'IFMapper/TADSWriter'
2011
- file.sub!(/(-\d+)?\.t/, '')
2012
- TADSWriter.new(self, file)
2013
- end
2014
-
2015
- #
2016
- # Export map as a set of Inform source code files
2017
- #
2018
- def export_inform7(file)
2019
- require 'IFMapper/Inform7Writer'
2020
- file.sub!(/.inform$/, '')
2021
- Inform7Writer.new(self, file)
2022
- end
2023
-
2024
- #
2025
- # Export map as a set of Inform source code files
2026
- #
2027
- def export_inform(file, version = 6)
2028
- if file =~ /\.inform$/ or version > 6
2029
- return export_inform7(file)
2030
- end
2031
-
2032
- require 'IFMapper/InformWriter'
2033
- file.sub!(/(-\d+)?\.inf/, '')
2034
- InformWriter.new(self, file)
2035
- end
2036
-
2037
- #
2038
- # Save the map under a new filename, bringing up a file requester
2039
- #
2040
- def save_as
2041
- require 'IFMapper/FXMapFileDialog'
2042
- file = FXMapFileDialog.new(@window, "#{MSG_SAVE_MAP} #{@name}",
2043
- FXMapFileDialog::KNOWN_SAVE_EXTENSIONS).filename
2044
- if file != ''
2045
- if File.exists?(file)
2046
- dlg = FXWarningBox.new(@window, "#{file}\n#{WARN_OVERWRITE_MAP}")
2047
- return if dlg.execute == 0
2048
- end
2049
-
2050
- case file
2051
- when /\.inform$/, /\.inf$/
2052
- export_inform(file)
2053
- when /\.ifm$/
2054
- export_ifm(file)
2055
- when /\.t$/
2056
- export_tads(file)
2057
- else
2058
- @filename = file
2059
- return _save
2060
- end
2061
- end
2062
- return false
2063
- end
2064
-
2065
- #
2066
- # Open the map's property window
2067
- #
2068
- def properties
2069
- if not @@win
2070
- @@win = FXMapDialogBox.new(@window)
2071
- end
2072
- @@win.copy_from(self)
2073
- @@win.show
2074
- end
2075
-
2076
- def stop_automap
2077
- return unless @automap
2078
- @automap.destroy
2079
- @automap = nil
2080
- GC.start
2081
- update_title
2082
- end
2083
-
2084
- def start_automap
2085
- if @automap
2086
- stop_automap
2087
- end
2088
- require 'IFMapper/FXMapFileDialog'
2089
- file = FXMapFileDialog.new(@window, MSG_LOAD_TRANSCRIPT,
2090
- [
2091
- EXT_TRANSCRIPT,
2092
- EXT_ALL_FILES
2093
- ]).filename
2094
- return if file == ''
2095
- require 'IFMapper/TranscriptReader'
2096
-
2097
- begin
2098
- @automap = TranscriptReader.new(self, file)
2099
- @automap.properties(true)
2100
- @automap.start
2101
- rescue Errno::EACCES, Errno::ENOENT => e
2102
- dlg = FXWarningBox.new(@window, "#{ERR_CANNOT_OPEN_TRANSCRIPT}\n#{e}")
2103
- dlg.execute
2104
- return
2105
- rescue => e
2106
- puts e.backtrace
2107
- dlg = FXWarningBox.new(@window, "#{ERR_PARSE_TRANSCRIPT}\n#{e}\n#{e.backtrace}")
2108
- dlg.execute
2109
- raise
2110
- end
2111
- create_pathmap
2112
- draw
2113
- update_title
2114
- end
2115
-
2116
- end
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'IFMapper/Map'
5
+ require 'IFMapper/FXSection'
6
+ require 'IFMapper/FXMapDialogBox'
7
+ require 'IFMapper/FXSectionDialogBox'
8
+ require 'IFMapper/AStar'
9
+ require 'IFMapper/FXWarningBox'
10
+ require 'thread'
11
+
12
+
13
+ class FXMap < Map
14
+ FILE_FORMAT_VERSION = 1 # Upgrade this if incompatible changes are made
15
+ # in the class so that file loading of old files
16
+ # can still be checked against.
17
+
18
+ attr_reader :zoom # Current zooming factor
19
+ attr_accessor :filename # Filename of current map (if any)
20
+ attr_reader :modified # Was map modified since being loaded?
21
+ attr_accessor :navigation # Map is navigation mode (no new nodes can be
22
+ # created)
23
+ attr_accessor :options # Map options
24
+ attr_reader :window # Fox Window for this map
25
+ attr :version # file format version
26
+ attr_reader :mutex # Mutex to avoid racing conditions while
27
+ # automapping
28
+ attr_reader :automap # automapping transcript
29
+
30
+ attr_reader :complexConnection
31
+ attr_reader :mouseButton
32
+
33
+ # pmap is a path map (a matrix or grid used for path finding).
34
+ # Rooms and paths are recorded there. Path finding is needed
35
+ # to draw complex connections (ie. those that are farther than one square)
36
+ # We now also use this for selecting of stuff, particularly complex paths.
37
+ attr :pmap
38
+
39
+ @@win = nil # Map Info window
40
+ @@roomlist = nil # Room List Window
41
+ @@itemlist = nil # Item List Window
42
+
43
+ @@tooltip = nil
44
+ @@cursor_arrow = nil
45
+ @@cursor_cross = nil
46
+ @@cursor_n = nil
47
+ @@cursor_ne = nil
48
+ @@cursor_e = nil
49
+ @@cursor_se = nil
50
+ @@cursor_s = nil
51
+ @@cursor_sw = nil
52
+ @@cursor_w = nil
53
+ @@cursor_nw = nil
54
+
55
+ #
56
+ # Map has changed dramatically. Update pathmap, title and redraw
57
+ #
58
+ def _changed
59
+ create_pathmap
60
+ if @window and @window.shown?
61
+ update_title
62
+ draw
63
+ end
64
+ end
65
+
66
+
67
+ #
68
+ # Jump to a certain section #
69
+ #
70
+ def section=(x)
71
+ clear_selection
72
+ super
73
+ @complexConnection = false
74
+ _changed
75
+ end
76
+
77
+ #
78
+ # Go to previous section (if any)
79
+ #
80
+ def previous_section
81
+ self.section = @section - 1
82
+ _changed
83
+ end
84
+
85
+ #
86
+ # Go to next section (if any)
87
+ #
88
+ def next_section
89
+ self.section = @section + 1
90
+ _changed
91
+ end
92
+
93
+ #
94
+ # Map has been modified. Update also pathmap.
95
+ #
96
+ def modified=(x)
97
+ @modified = x
98
+ _changed
99
+ end
100
+
101
+ #
102
+ # Popup the section properties to allow renaming it
103
+ #
104
+ def rename_section
105
+ @sections[@section].properties(self)
106
+ modified = true
107
+ end
108
+
109
+ #
110
+ # Delete current section from map
111
+ #
112
+ def delete_section
113
+ return navigation_warning if @navigation
114
+ w = FXWarningBox.new(@window, WARN_DELETE_SECTION)
115
+ return if w.execute == 0
116
+
117
+ delete_section_at(@section)
118
+ modified = true
119
+ end
120
+
121
+ #
122
+ # Add a new section to map and make it current
123
+ #
124
+ def new_section
125
+ return navigation_warning if @navigation
126
+ @sections.push( FXSection.new )
127
+ @section = @sections.size - 1
128
+ end
129
+
130
+ #
131
+ # A simple debugging function that will spit out the path map with a
132
+ # very simple ascii representation.
133
+ #
134
+ def dump_pathmap
135
+ s = ' ' + ' ' * @width + "\n"
136
+ m = s * @height
137
+ (0...@width).each { |x|
138
+ (0...@height).each { |y|
139
+ m[y * (@width+2)] = (y % 10).to_s
140
+ loc = y * (@width+2) + x + 1
141
+ if @pmap.at(x).at(y).kind_of?(Connection)
142
+ m[loc] = '-'
143
+ elsif @pmap.at(x).at(y).kind_of?(Room)
144
+ m[loc] = 'R'
145
+ end
146
+ }
147
+ }
148
+ puts ' 0123456789' * (@width/10)
149
+ puts m
150
+ end
151
+
152
+ #
153
+ # Determine whether a pathmap area from x,y to x+w,y+h is free
154
+ # of rooms
155
+ #
156
+ def _free_area?(x, y, w, h)
157
+ x.upto(x+w) { |xx|
158
+ y.upto(x+h) { |yy|
159
+ return false if @pmap.at(xx).at(yy).kind_of?(Room)
160
+ }
161
+ }
162
+ return true
163
+ end
164
+
165
+ #
166
+ # Return true or false if map is free at location x, y.
167
+ # If x and y coords are within pathmap, we use it.
168
+ # If not, we will use the brute force search of Map.rb
169
+ #
170
+ def free?(x, y)
171
+ if @pmap and x >= 0 and y >= 0 and @pmap.size > x and @pmap.at(x).size > y
172
+ return false if @pmap.at(x).at(y)
173
+ return true
174
+ else
175
+ super
176
+ end
177
+ end
178
+
179
+ #
180
+ # Given a list of rooms, find an area in the map where we could
181
+ # place them. If found, return x/y offset so rooms can be moved
182
+ # there. This is used for pasting rooms.
183
+ #
184
+ def find_empty_area(rooms)
185
+ return nil if rooms.empty?
186
+ minx = maxx = rooms[0].x
187
+ miny = maxy = rooms[0].y
188
+ rooms.each { |r|
189
+ minx = r.x if r.x < minx
190
+ maxx = r.x if r.x > maxx
191
+ miny = r.y if r.y < miny
192
+ maxy = r.y if r.y > maxy
193
+ }
194
+ w = maxx - minx
195
+ h = maxy - miny
196
+
197
+ # Find an area in pathmap that has w x h empty rooms
198
+ 0.upto(@width-1-w) { |x|
199
+ 0.upto(@height-1-h) { |y|
200
+ if _free_area?(x, y, w, h)
201
+ return [x - minx, y - miny]
202
+ end
203
+ }
204
+ }
205
+ return nil
206
+ end
207
+
208
+ #
209
+ # Reinitialize the pathmap to an empty matrix
210
+ #
211
+ def empty_pathmap
212
+ # First, create an empty grid of width x height
213
+ @pmap = Array.new(@width)
214
+ (0...@width).each { |x|
215
+ @pmap[x] = Array.new(@height)
216
+ }
217
+ return pmap
218
+ end
219
+
220
+ #
221
+ # Recreate the pathmap based on rooms and connections
222
+ # This routine is used on loading a new map.
223
+ #
224
+ def create_pathmap
225
+ # First, create an empty grid of width x height
226
+ empty_pathmap
227
+ # Then, fill it in with all rooms...
228
+ @sections[@section].rooms.each { |r| @pmap[r.x][r.y] = r }
229
+ # And following, add all paths
230
+ @sections[@section].connections.each { |c| path_find(c) }
231
+ end
232
+
233
+ #
234
+ # Given a connection, clean its path from path map.
235
+ #
236
+ def clean_path(c)
237
+ c.gpts.each { |p| @pmap[p[0]][p[1]] = nil if @pmap[p[0]][p[1]] == c }
238
+ end
239
+
240
+ #
241
+ # Remove a connection from map, since path creation failed.
242
+ #
243
+ def remove_connection(c)
244
+ c.failed = true
245
+ status "#{ERR_PATH_FOR_CONNECTION} #{c} #{ERR_IS_BLOCKED}."
246
+ end
247
+
248
+ # Given a connection, create the path for it, if not a simple
249
+ # connection. Also, add the paths to pathmap.
250
+ def path_find(c)
251
+ unless c.complex?
252
+ c.pts = c.gpts = []
253
+ c.failed = false
254
+ return true
255
+ end
256
+
257
+ # Complex path... Generate points.
258
+ a, b = c.room
259
+ dirA, dirB = c.dirs
260
+ raise "A connection not found #{c} at #{a}" unless dirA
261
+ raise "B connection not found #{c} at #{b}" unless dirB
262
+
263
+ vA = FXRoom::DIR_TO_VECTOR[dirA]
264
+ vB = FXRoom::DIR_TO_VECTOR[dirB]
265
+
266
+ pA = [ a.x + vA[0], a.y + vA[1] ]
267
+ pB = [ b.x + vB[0], b.y + vB[1] ]
268
+
269
+ c.gpts = []
270
+ c.pts = []
271
+
272
+ # Check for the special case of looping path (path that begins and
273
+ # returns to same exit)
274
+ if a == b and dirA == dirB
275
+ pt = a.corner(c, 1, dirA)
276
+ n = 1.0 / Math.sqrt(vA[0] * vA[0] + vA[1] * vA[1])
277
+ vA = [ vA[0] * n, vA[1] * n ]
278
+ c.pts.push( [ pt[0], pt[1] ] )
279
+ pA = [ pt[0] + vA[0] * 20, pt[1] + vA[1] * 20 ]
280
+ c.pts.push( [pA[0], pA[1]] )
281
+ pB = [ pA[0] + vA[1] * 20, pA[1] - vA[0] * 20 ]
282
+ c.pts.push( [pB[0], pB[1]] )
283
+ pC = [ pB[0] - vA[0] * 20, pB[1] - vA[1] * 20 ]
284
+ c.pts.push( [pC[0], pC[1]] )
285
+ c.dir = Connection::AtoB
286
+ return true
287
+ end
288
+
289
+ # Now check if start or goal are fully blocked. If so,
290
+ # fail quickly
291
+ if pA[0] < 0 or pA[0] >= @width or
292
+ pB[0] < 0 or pB[0] >= @width or
293
+ pA[1] < 0 or pA[1] >= @height or
294
+ pB[1] < 0 or pB[1] >= @height or
295
+ @pmap.at(pA[0]).at(pA[1]).kind_of?(Room) or
296
+ @pmap.at(pB[0]).at(pB[1]).kind_of?(Room)
297
+ remove_connection(c)
298
+ return false
299
+ end
300
+
301
+ if (pA[0] - pB[0]).abs > 30 or
302
+ (pA[1] - pB[1]).abs > 30
303
+ c.failed = true
304
+ return
305
+ end
306
+
307
+ # No, okay, we need to do true A* path finding
308
+ c.failed = false
309
+ aStar = AStar.new
310
+ MapNode::map(@pmap)
311
+ start = MapNode.new( pA[0], pA[1] )
312
+ goal = MapNode.new( pB[0], pB[1] )
313
+ aStar.goals( start, goal )
314
+ while aStar.search_step == AStar::SEARCH_STATE_SEARCHING
315
+ end
316
+
317
+ # Oops, AStar failed. Not a clean path
318
+ if aStar.state == AStar::SEARCH_STATE_FAILED
319
+ remove_connection(c)
320
+ return false
321
+ end
322
+
323
+ # We succeeded. Get the path
324
+ c.failed = false
325
+ c.gpts = aStar.path
326
+ # Put path in pathmap so subsequent paths will try to avoid crossing it.
327
+ c.gpts.each { |p| @pmap[p[0]][p[1]] = c }
328
+
329
+ # Okay, we have a valid path.
330
+ # Create real path in display coordinates now...
331
+ # Start with a's corner
332
+ pt = a.corner(c, 1, dirA)
333
+ c.pts.push( [ pt[0], pt[1] ] )
334
+ # Then, add each grid point we calculated
335
+ c.gpts.each { |pt|
336
+ x = pt[0] * WW + WW / 2
337
+ y = pt[1] * HH + HH / 2
338
+ c.pts.push([x, y])
339
+ }
340
+ # And end with b's corner
341
+ pt = b.corner(c, 1, dirB)
342
+ return c.pts.push([pt[0], pt[1]])
343
+ end
344
+
345
+ #
346
+ # Add a new path point to a connection
347
+ #
348
+ def add_path_pt( c, x, y )
349
+ @pmap[x][y] = c
350
+ c.gpts.push([x, y])
351
+ end
352
+
353
+ #
354
+ # Used for loading class with Marshal
355
+ #
356
+ def marshal_load(variables)
357
+ @zoom = variables.shift
358
+ @navigation = variables.shift
359
+ @options = variables.shift
360
+ super
361
+ @modified = false
362
+ @complexConnection = nil
363
+ end
364
+
365
+ #
366
+ # Used for saving class with Marshal
367
+ #
368
+ def marshal_dump
369
+ [ @zoom, @navigation, @options ] + super
370
+ end
371
+
372
+ #
373
+ # Used to copy relevant data from one (temporary) map to another
374
+ #
375
+ def copy(b)
376
+ super(b)
377
+ if b.kind_of?(FXMap)
378
+ @options = b.options if b.options
379
+ @filename = b.filename
380
+ self.zoom = b.zoom
381
+ @navigation = b.navigation
382
+ @modified = true
383
+ @complexConnection = nil
384
+ end
385
+ end
386
+
387
+ #
388
+ # Create a new room for the current section
389
+ #
390
+ def new_room(x, y)
391
+ @modified = true
392
+ r = @sections[@section].new_room(x, y)
393
+ return r
394
+ end
395
+
396
+ #
397
+ # Function used to add a new room. x and y are absolute pixel positions
398
+ # in canvas.
399
+ #
400
+ def new_xy_room(x, y)
401
+ x = x / WW
402
+ y = y / HH
403
+ r = new_room(x, y)
404
+ @pmap[x][y] = r
405
+
406
+ r.selected = true
407
+
408
+ if @options['Edit on Creation']
409
+ if not r.modal_properties(self)
410
+ @sections[@section].delete_room(r)
411
+ @pmap[x][y] = nil
412
+ return nil
413
+ end
414
+ end
415
+ update_roomlist
416
+ return r
417
+ end
418
+
419
+ #
420
+ # Given a room, delete it
421
+ #
422
+ def delete_room_only(r)
423
+ if @pmap and r.x < @pmap.size and r.y < @pmap.size and
424
+ r.x >= 0 and r.y >= 0
425
+ @pmap[r.x][r.y] = nil
426
+ end
427
+ super
428
+ end
429
+
430
+ #
431
+ # Given a room, delete it
432
+ #
433
+ def delete_room(r)
434
+ if @pmap and r.x < @pmap.size and r.y < @pmap.size and
435
+ r.x >= 0 and r.y >= 0
436
+ @pmap[r.x][r.y] = nil
437
+ end
438
+ super
439
+ end
440
+
441
+ # Given a canvas (mouse) x/y position, return:
442
+ #
443
+ # false - arrow click
444
+ # true - room click
445
+ #
446
+ def click_type(x, y)
447
+ x = (x % WW).to_i
448
+ y = (y % HH).to_i
449
+
450
+ if x >= WS_2 and y >= HS_2 and
451
+ x <= (W + WS_2) and y <= (H + HS_2)
452
+ return true
453
+ else
454
+ return false
455
+ end
456
+ end
457
+
458
+ # Given an x and y canvas position, return room object,
459
+ # complex connection or nil
460
+ def to_room(x,y)
461
+ xx = x / WW
462
+ yy = y / HH
463
+ return @pmap.at(xx).at(yy)
464
+ end
465
+
466
+ # Given a mouse click x/y position, return object(s) if any or nil
467
+ def to_object(x, y)
468
+
469
+ exitA = get_quadrant(x, y)
470
+ unless exitA
471
+ # Not in arrow section, return element based on pmap
472
+ # can be a room or complex arrow connection.
473
+ xx = x / WW
474
+ yy = y / HH
475
+ return nil if xx >= @width or yy >= @height
476
+ return @pmap.at(xx).at(yy)
477
+ else
478
+ # Possible arrow
479
+ @sections[@section].connections.each { |c|
480
+ a = c.roomA
481
+ b = c.roomB
482
+ next if not b and @complexConnection
483
+
484
+ if c.gpts.size > 0
485
+ 2.times { |t|
486
+ if t == 0
487
+ r = a
488
+ dir = r.exits.index(c)
489
+ else
490
+ r = b
491
+ dir = r.exits.rindex(c)
492
+ end
493
+ next if not r
494
+ x1, y1 = r.corner(c, 1, dir)
495
+ v = FXRoom::DIR_TO_VECTOR[dir]
496
+ x2 = x1 + v[0] * WS
497
+ y2 = y1 + v[1] * HS
498
+ if x1 == x2
499
+ x1 -= W / 2
500
+ x2 += W / 2
501
+ end
502
+ if y1 == y2
503
+ y1 -= H / 2
504
+ y2 += H / 2
505
+ end
506
+ x1, x2 = x2, x1 if x2 < x1
507
+ y1, y2 = y2, y1 if y2 < y1
508
+
509
+ if x >= x1 and x <= x2 and
510
+ y >= y1 and y < y2
511
+ return c
512
+ end
513
+ }
514
+ else
515
+ x1, y1 = a.corner(c, 1, a.exits.index(c))
516
+ if b
517
+ x2, y2 = b.corner(c, 1, b.exits.rindex(c))
518
+ else
519
+ dir = a.exits.index(c)
520
+ v = FXRoom::DIR_TO_VECTOR[dir]
521
+ x2 = x1 + v[0] * WS
522
+ y2 = y1 + v[1] * HS
523
+ end
524
+
525
+ x1, x2 = x2, x1 if x2 < x1
526
+ y1, y2 = y2, y1 if y2 < y1
527
+ if x1 == x2
528
+ x1 -= W / 2
529
+ x2 += W / 2
530
+ end
531
+ if y1 == y2
532
+ y1 -= H / 2
533
+ y2 += H / 2
534
+ end
535
+ if x >= x1 and x <= x2 and
536
+ y >= y1 and y < y2
537
+ return c
538
+ end
539
+ end
540
+ }
541
+
542
+ return nil
543
+ end
544
+
545
+ return nil
546
+ end
547
+
548
+ #
549
+ # Update the map window's title
550
+ #
551
+ def update_title
552
+ title = @name.dup
553
+ if @navigation
554
+ title << " #{TITLE_READ_ONLY}"
555
+ end
556
+ if @automap
557
+ title << " #{TITLE_AUTOMAP}"
558
+ end
559
+ title << " #{TITLE_ZOOM} %.3f" % @zoom
560
+ title << " #{TITLE_SECTION} #{@section+1} #{TITLE_OF} #{@sections.size}"
561
+ title << " #{@sections[@section].name}"
562
+ @window.title = title
563
+ end
564
+
565
+ # Change zoom factor of map. Rebuild fonts and canvas sizes.
566
+ def zoom=(value)
567
+ @zoom = ("%.2f" % value).to_f
568
+ # Create the font
569
+ fontsize = (11 * @zoom).to_i
570
+
571
+ if @window
572
+ @font = FXFont.new(@window.getApp, @options['Font Text'], fontsize)
573
+ @font.create
574
+
575
+ @objfont = FXFont.new(@window.getApp, @options['Font Objects'],
576
+ (fontsize * 0.75).to_i)
577
+ @objfont.create
578
+
579
+ width = (WW * @width * @zoom).to_i
580
+ height = (HH * @height * @zoom).to_i
581
+ @canvas.width = width
582
+ @canvas.height = height
583
+
584
+ # Then, create an off-screen image with that same size for double
585
+ # buffering
586
+ @image.release
587
+ @image.destroy
588
+ GC.start
589
+
590
+ @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
591
+ width, height)
592
+ @image.create
593
+ update_title
594
+ end
595
+
596
+ end
597
+
598
+ # Given a mouse x/y position to WS/HS, return an index
599
+ # indicating what quadrant it belongs to.
600
+ def get_quadrant(ax, ay)
601
+ # First get relative x/y position
602
+ x = ax % WW
603
+ y = ay % HH
604
+
605
+ quadrant = nil
606
+ if x < WS_2
607
+ #left
608
+ if y < HS_2
609
+ # top
610
+ quadrant = 7
611
+ elsif y > H + HS_2
612
+ # bottom
613
+ quadrant = 5
614
+ else
615
+ # center
616
+ quadrant = 6
617
+ end
618
+ elsif x > W + WS_2
619
+ # right
620
+ if y < HS_2
621
+ # top
622
+ quadrant = 1
623
+ elsif y > H + HS_2
624
+ # bottom
625
+ quadrant = 3
626
+ else
627
+ # center
628
+ quadrant = 2
629
+ end
630
+ else
631
+ #center
632
+ if y < HS_2
633
+ # top
634
+ quadrant = 0
635
+ elsif y > H + HS_2
636
+ # bottom
637
+ quadrant = 4
638
+ else
639
+ # center
640
+ quadrant = nil
641
+ end
642
+ end
643
+
644
+
645
+ return quadrant
646
+ end
647
+
648
+ # Given an x,y absolute position corresponding to a connection,
649
+ # return connected rooms (if any).
650
+ def quadrant_to_rooms( q, x, y )
651
+ maxX = @width * WW
652
+ maxY = @height * HH
653
+
654
+ # First check if user tried adding a connection
655
+ # at the edges of the map. If so, return empty stuff.
656
+ if x < WS_2 or y < HS_2 or
657
+ x > maxX - WS_2 or y > maxY - HS_2
658
+ return [nil, nil, nil, nil]
659
+ end
660
+
661
+ x1 = x2 = x
662
+ y1 = y2 = y
663
+
664
+ case q
665
+ when 0, 4
666
+ y1 -= HS
667
+ y2 += HS
668
+ when 1, 5
669
+ x1 -= WS
670
+ x2 += WS
671
+ y1 += HS
672
+ y2 -= HS
673
+ when 2, 6
674
+ x1 -= WS
675
+ x2 += WS
676
+ when 3, 7
677
+ x1 -= WS
678
+ y1 -= HS
679
+ x2 += WS
680
+ y2 += HS
681
+ end
682
+
683
+ case q
684
+ when 0, 5, 6, 7
685
+ x1, x2 = x2, x1
686
+ y1, y2 = y2, y1
687
+ end
688
+
689
+ roomA = to_room(x1, y1)
690
+ roomB = to_room(x2, y2)
691
+ # Oops, user tried to create rooms where we already
692
+ # have a complex connection. Don't create anything, then.
693
+ if roomA.kind_of?(Connection) or
694
+ (roomB.kind_of?(Connection) and not @complexConnection)
695
+ return [roomA, roomB, nil, nil]
696
+ end
697
+
698
+ return [roomA, roomB, [x1, y1], [x2, y2]]
699
+ end
700
+
701
+ #
702
+ # Add a new complex connection (or its first point)
703
+ #
704
+ def new_complex_connection( x, y )
705
+ exitA = get_quadrant(x, y)
706
+ unless exitA
707
+ raise "not a connection"
708
+ end
709
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
710
+ unless a and roomA and roomA[exitA] == nil
711
+ return
712
+ end
713
+ _new_complex_connection(roomA, exitA)
714
+ end
715
+
716
+ def _new_complex_connection(roomA, exitA)
717
+ if @complexConnection.kind_of?(TrueClass)
718
+ @complexConnection = [roomA, exitA]
719
+ c = new_connection( roomA, exitA, nil )
720
+ status MSG_COMPLEX_CONNECTION_OTHER_EXIT
721
+ else
722
+ @sections[@section].delete_connection_at(-1)
723
+ c = new_connection( @complexConnection[0],
724
+ @complexConnection[1], roomA, exitA )
725
+ if path_find(c) # Do A* path finding to connect both exits
726
+ @modified = true
727
+ status MSG_COMPLEX_CONNECTION_DONE
728
+ else
729
+ @sections[@section].delete_connection_at(-1)
730
+ end
731
+ draw
732
+ @complexConnection = nil
733
+ end
734
+ end
735
+
736
+ #
737
+ # Add a new room connection among contiguous rooms.
738
+ #
739
+ def new_xy_connection( x, y )
740
+ exitA = get_quadrant(x, y)
741
+ unless exitA
742
+ raise "not a connection"
743
+ end
744
+
745
+ # Then, get rooms being connected
746
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
747
+ return unless a # User click outside map
748
+
749
+ if @options['Create on Connection']
750
+ roomA = new_xy_room( a[0], a[1] ) unless roomA
751
+ roomB = new_xy_room( b[0], b[1] ) unless roomB
752
+ end
753
+
754
+ return nil unless roomA and roomB
755
+
756
+ if roomA == roomB
757
+ raise "error: same room connection"
758
+ end
759
+
760
+ @modified = true
761
+ if roomA and exitA
762
+ # get old connection
763
+ if roomA[exitA]
764
+ c = roomA[exitA]
765
+ delete_connection(c) if c.roomB == nil
766
+ end
767
+ exitB = (exitA + 4) % 8
768
+ if roomB[exitB]
769
+ c = roomB[exitB]
770
+ delete_connection(c) if c.roomB == nil
771
+ end
772
+ end
773
+ begin
774
+ new_connection( roomA, exitA, roomB )
775
+ rescue Section::ConnectionError
776
+ end
777
+ end
778
+
779
+ #
780
+ # Handle mouse button double clicks in canvas
781
+ #
782
+ def double_click_cb(selection, event)
783
+ return unless selection
784
+ if selection.kind_of?(FXRoom) or selection.kind_of?(FXConnection)
785
+ selection.properties( self, event )
786
+ end
787
+ end
788
+
789
+ # Self-explanatory.
790
+ def zoom_out
791
+ if @zoom > 0.1
792
+ self.zoom -= 0.1
793
+ end
794
+ end
795
+
796
+ # Self-explanatory.
797
+ def zoom_in
798
+ if @zoom < 1.25
799
+ self.zoom += 0.1
800
+ end
801
+ end
802
+
803
+ # Spit out a new message to the status line.
804
+ def status(msg)
805
+ mw = @window.parent.parent
806
+ statusbar = mw.children.find() { |x| x.kind_of?(FXStatusBar) }
807
+ s = statusbar.statusLine
808
+ s.normalText = s.text = msg
809
+ end
810
+
811
+ #
812
+ # Based on x,y coordinate, switch mouse icon shape
813
+ #
814
+ def cursor_switch(x, y)
815
+ if not @options['Use Room Cursor']
816
+ @canvas.defaultCursor = @@cursor_arrow
817
+ return
818
+ end
819
+ q = get_quadrant(x, y)
820
+ case q
821
+ when 0
822
+ @canvas.defaultCursor = @@cursor_n
823
+ when 1
824
+ @canvas.defaultCursor = @@cursor_ne
825
+ when 2
826
+ @canvas.defaultCursor = @@cursor_e
827
+ when 3
828
+ @canvas.defaultCursor = @@cursor_se
829
+ when 4
830
+ @canvas.defaultCursor = @@cursor_s
831
+ when 5
832
+ @canvas.defaultCursor = @@cursor_sw
833
+ when 6
834
+ @canvas.defaultCursor = @@cursor_w
835
+ when 7
836
+ @canvas.defaultCursor = @@cursor_nw
837
+ else
838
+ @canvas.defaultCursor = @@cursor_arrow
839
+ end
840
+ end
841
+
842
+ #
843
+ # Based on mouse position on canvas, create a tooltip
844
+ #
845
+ def tooltip_cb(sender, id, ptr)
846
+ if @zoom < 0.6 and @tooltip_msg != ''
847
+ sender.text = @tooltip_msg.to_s
848
+ sender.show
849
+ else
850
+ sender.hide
851
+ end
852
+ end
853
+
854
+
855
+ #
856
+ # Show some help status in status line based on cursor position
857
+ #
858
+ def help_cb(sender, sel, event)
859
+
860
+ x = (event.last_x / @zoom).to_i
861
+ y = (event.last_y / @zoom).to_i
862
+ if @complexConnection
863
+ cursor_switch(x,y)
864
+ return
865
+ end
866
+
867
+ @tooltip_msg = ''
868
+ sel = to_object(x, y)
869
+ if sel
870
+ @canvas.defaultCursor = @@cursor_arrow
871
+ if sel.kind_of?(Room)
872
+ @tooltip_msg = sel.name
873
+ status "\"#{sel.name}\": #{MSG_CLICK_TO_SELECT_AND_MOVE}"
874
+ elsif sel.kind_of?(Connection)
875
+ status MSG_CLICK_CHANGE_DIR
876
+ end
877
+ else
878
+ if click_type(x, y)
879
+ @canvas.defaultCursor = @@cursor_cross
880
+ status MSG_CLICK_CREATE_ROOM
881
+ else
882
+ cursor_switch(x, y)
883
+ status MSG_CLICK_CREATE_LINK
884
+ end
885
+ end
886
+ end
887
+
888
+ #
889
+ # zoom in/out based on mousewheel, keeping position relative
890
+ # to cursor position
891
+ #
892
+ def mousewheel_cb(sender, sel, event)
893
+ case event.code
894
+ when -120 # Hmm, there does not seem to be constants for these
895
+ zoom_out
896
+ when 120 # Hmm, there does not seem to be constants for these
897
+ zoom_in
898
+ end
899
+ end
900
+
901
+ #
902
+ # Handle middle mouse button click in canvas
903
+ #
904
+ def mmb_click_cb(server, sel, event)
905
+ @canvas.grab
906
+ @dx = @dy = 0
907
+ @mouseButton = MIDDLEBUTTON
908
+ end
909
+
910
+ #
911
+ # Select all rooms and connections within (drag) rectangle
912
+ #
913
+ def select_rectangle(x1, y1, x2, y2)
914
+ x1, x2 = x2, x1 if x2 < x1
915
+ y1, y2 = y2, y1 if y2 < y1
916
+
917
+ x = x1 * zoom
918
+ y = y1 * zoom
919
+ w = x2 - x1
920
+ h = y2 - y1
921
+
922
+ x1 = ((x1 - WS_2) / WW).floor
923
+ y1 = ((y1 - HS_2) / HH).floor
924
+ x2 = ((x2 - WS_2) / WW).ceil
925
+ y2 = ((y2 - HS_2) / HH).ceil
926
+
927
+ @sections[@section].rooms.each { |r|
928
+ if r.x >= x1 and r.x <= x2 and
929
+ r.y >= y1 and r.y <= y2
930
+ r.selected = true
931
+ r.update_properties(self)
932
+ else
933
+ r.selected = false
934
+ end
935
+ }
936
+
937
+ @sections[@section].connections.each { |c|
938
+ next if not c.roomB
939
+ a = c.roomA
940
+ b = c.roomB
941
+
942
+ if (a.x >= x1 and a.x <= x2 and
943
+ a.y >= y1 and a.y <= y2) or
944
+ (b.x >= x1 and b.x <= x2 and
945
+ b.y >= y1 and b.y <= y2)
946
+ c.selected = true
947
+ c.update_properties(self)
948
+ else
949
+ c.selected = false
950
+ end
951
+ }
952
+
953
+ draw
954
+ dc = FXDCWindow.new(@canvas)
955
+ dc.function = BLT_NOT_SRC_XOR_DST
956
+ dc.drawRectangle(x, y, w * @zoom, h * @zoom)
957
+ dc.end
958
+ end
959
+
960
+ #
961
+ # Handle mouse motion in canvas
962
+ #
963
+ def motion_cb(server, sel, event)
964
+ if @mouseButton == MIDDLEBUTTON
965
+ @canvas.dragCursor = @@cursor_move
966
+ pos = @scrollwindow.position
967
+ dx = event.last_x - event.win_x
968
+ dy = event.last_y - event.win_y
969
+ if dx != 0 or dy != 0 and not (dx == @dx and dy == @dy)
970
+ pos[0] += dx
971
+ pos[1] += dy
972
+ @dx = dx
973
+ @dy = dy
974
+ @scrollwindow.setPosition(pos[0], pos[1])
975
+ @canvas.repaint
976
+ end
977
+ elsif @mouseButton == LEFTBUTTON
978
+ x = (event.last_x / @zoom).to_i
979
+ y = (event.last_y / @zoom).to_i
980
+ select_rectangle( @dx, @dy, x, y )
981
+ else
982
+ help_cb(server, sel, event)
983
+ end
984
+ end
985
+
986
+ #
987
+ # Handle release of middle mouse button
988
+ #
989
+ def mmb_release_cb(server, sel, event)
990
+ if @mouseButton
991
+ @canvas.ungrab
992
+ @canvas.dragCursor = @@cursor_arrow
993
+ if @mouseButton == LEFTBUTTON
994
+ draw
995
+ else
996
+ @canvas.repaint
997
+ end
998
+ @mouseButton = nil
999
+ end
1000
+ end
1001
+
1002
+ #
1003
+ # Given a room, center scrollwindow so room is visible
1004
+ #
1005
+ def center_view_on_room(r)
1006
+ xx = (r.xx + W / 2) * @zoom
1007
+ yy = (r.yy + H / 2) * @zoom
1008
+ center_view_on_xy(xx, yy)
1009
+ end
1010
+
1011
+ #
1012
+ # Given an x and y coordinate for the canvas, center on it
1013
+ #
1014
+ def center_view_on_xy(xx, yy)
1015
+ cw = @scrollwindow.getContentWidth
1016
+ ch = @scrollwindow.getContentHeight
1017
+ w = @scrollwindow.getViewportWidth
1018
+ h = @scrollwindow.getViewportHeight
1019
+
1020
+ xx -= w / 2
1021
+ yy -= h / 2
1022
+ maxx = cw - w / 2
1023
+ maxy = ch - h / 2
1024
+ if xx > maxx
1025
+ xx = maxx
1026
+ elsif xx < 0
1027
+ xx = 0
1028
+ end
1029
+ if yy > maxy
1030
+ yy = maxy
1031
+ elsif yy < 0
1032
+ yy = 0
1033
+ end
1034
+
1035
+ @scrollwindow.setPosition( -xx, -yy )
1036
+ end
1037
+
1038
+ #
1039
+ # Return current selection as an array of rooms and an array of
1040
+ # connections
1041
+ #
1042
+ def get_selection
1043
+ rooms = @sections[@section].rooms.find_all { |r| r.selected }
1044
+ conns = @sections[@section].connections.find_all { |r| r.selected }
1045
+ return rooms, conns
1046
+ end
1047
+
1048
+ #
1049
+ # Clear rooms/connections selected
1050
+ #
1051
+ def clear_selection
1052
+ @sections[@section].rooms.each { |r| r.selected = false }
1053
+ @sections[@section].connections.each { |r| r.selected = false }
1054
+ end
1055
+
1056
+ #
1057
+ # Add a proper link submenu option for an exit
1058
+ #
1059
+ def rmb_link_menu(submenu, c, room, idx, old_idx)
1060
+ return if room[idx] == c
1061
+ dir = Room::DIRECTIONS[idx]
1062
+ dir = dir.upcase
1063
+ if room[idx]
1064
+ cmd = FXMenuCommand.new(submenu, "Switch with link in #{dir} Exit")
1065
+ cmd.connect(SEL_COMMAND) {
1066
+ c2 = room[idx]
1067
+ room[old_idx] = c2
1068
+ room[idx] = c
1069
+ create_pathmap
1070
+ draw
1071
+ }
1072
+ else
1073
+ cmd = FXMenuCommand.new(submenu, "Move link to #{dir} Exit")
1074
+ cmd.connect(SEL_COMMAND) {
1075
+ room[old_idx] = nil
1076
+ room[idx] = c
1077
+ create_pathmap
1078
+ draw
1079
+ }
1080
+ end
1081
+ end
1082
+
1083
+ #
1084
+ # Handle right mouse button click
1085
+ #
1086
+ def rmb_click_cb(sender, sel, event)
1087
+ rooms, links = get_selection
1088
+
1089
+ menu = nil
1090
+ if not links.empty? and links.size == 1
1091
+ c = links[0]
1092
+ a = c.roomA
1093
+ b = c.roomB
1094
+ menu = FXMenuPane.new(@window)
1095
+ if c.dir == Connection::AtoB
1096
+ cmd = FXMenuCommand.new(menu, "Flip Direction")
1097
+ cmd.connect(SEL_COMMAND) { c.flip; draw }
1098
+ FXMenuSeparator.new(menu)
1099
+ end
1100
+
1101
+ submenu = FXMenuPane.new(@window)
1102
+ old_idx = a.exits.index(c)
1103
+ 0.upto(7) { |idx|
1104
+ rmb_link_menu( submenu, c, a, idx, old_idx )
1105
+ }
1106
+ FXMenuCascade.new(menu, a.name, nil, submenu)
1107
+ if b
1108
+ submenu = FXMenuPane.new(@window)
1109
+ old_idx = b.exits.rindex(c)
1110
+ 0.upto(7) { |idx|
1111
+ rmb_link_menu( submenu, c, b, idx, old_idx )
1112
+ }
1113
+ FXMenuCascade.new(menu, b.name, nil, submenu)
1114
+ end
1115
+ end
1116
+ if menu
1117
+ menu.create
1118
+ menu.popup(nil, event.root_x, event.root_y)
1119
+ @window.getApp.runModalWhileShown(menu)
1120
+ end
1121
+ end
1122
+
1123
+
1124
+ #
1125
+ # Handle left mouse button click
1126
+ #
1127
+ def lmb_click_cb(sender, sel, event)
1128
+ x = (event.last_x / @zoom).to_i
1129
+ y = (event.last_y / @zoom).to_i
1130
+
1131
+ if event.state & ALTMASK != 0
1132
+ mmb_click_cb(sender, sel, event)
1133
+ return
1134
+ end
1135
+
1136
+ selection = to_object(x, y)
1137
+
1138
+ if event.state & SHIFTMASK != 0
1139
+ @mouseButton = LEFTBUTTON
1140
+ @canvas.grab
1141
+ @dx, @dy = [ x, y ]
1142
+ return if not selection
1143
+ end
1144
+
1145
+
1146
+ if event.click_count == 2
1147
+ double_click_cb(selection, event)
1148
+ return
1149
+ end
1150
+
1151
+ unless selection
1152
+ clear_selection
1153
+
1154
+ # If in navigation mode, we don't allow user to modify map.
1155
+ return if @navigation
1156
+
1157
+ # if we did not select anything, check to see if we
1158
+ # clicked in a room area or connection area.
1159
+ if click_type(x, y)
1160
+ return if @complexConnection
1161
+ # Add a new room
1162
+ roomB = @sections[@section].rooms[-1]
1163
+ roomA = new_xy_room( x, y )
1164
+ if roomB and roomA and @options['Automatic Connection']
1165
+ # check to see if rooms are next to each other
1166
+ # if so, try to connect them (assuming there's no connection there
1167
+ # already).
1168
+ exitB = roomB.next_to?(roomA)
1169
+ if exitB and roomB.exits[exitB] == nil
1170
+ new_connection( roomB, exitB, roomA )
1171
+ end
1172
+ end
1173
+ else
1174
+ # Add a new connection
1175
+ if @complexConnection
1176
+ new_complex_connection(x, y)
1177
+ else
1178
+ # Add a new simple connection (plus rooms if needed)
1179
+ if event.state & CONTROLMASK != 0
1180
+ exitA = get_quadrant(x, y)
1181
+ roomA, roomB, a, b = quadrant_to_rooms( exitA, x, y )
1182
+ if not roomA
1183
+ new_xy_connection( x, y )
1184
+ else
1185
+ if a and roomA
1186
+ @complexConnection = [roomA, exitA]
1187
+ new_connection( roomA, exitA, nil )
1188
+ _new_complex_connection( roomA, exitA )
1189
+ end
1190
+ end
1191
+ else
1192
+ new_xy_connection( x, y )
1193
+ end
1194
+ end
1195
+ end
1196
+ else
1197
+ if selection.kind_of?(Connection) and selection.selected
1198
+ # Toggle arrow direction
1199
+ selection.toggle_direction
1200
+ draw
1201
+ return
1202
+ else
1203
+ if event.state & SHIFTMASK == 0 and
1204
+ event.state & CONTROLMASK == 0
1205
+ clear_selection
1206
+ end
1207
+ # Select the stuff
1208
+ selection.update_properties(self)
1209
+ if event.state & CONTROLMASK != 0
1210
+ selection.selected ^= true # toggle selection
1211
+ else
1212
+ selection.selected = true
1213
+ end
1214
+ end
1215
+ end
1216
+ draw(sender, sel, event)
1217
+ end
1218
+
1219
+ #
1220
+ # Close the map. Pop up a warning box to allow saving
1221
+ # if map has been modified.
1222
+ #
1223
+ def close_cb()
1224
+ if @modified
1225
+ dlg = FXDialogBox.new( @window.parent, "Warning",
1226
+ DECOR_ALL,
1227
+ 0, 0, 400, 130)
1228
+ # Frame
1229
+ s = FXVerticalFrame.new(dlg,
1230
+ LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
1231
+
1232
+ f = FXHorizontalFrame.new(s, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
1233
+
1234
+ font = FXFont.new(@window.getApp, "Helvetica", 30)
1235
+ font.create
1236
+ oops = FXLabel.new(f, "!", nil, 0, LAYOUT_SIDE_LEFT|LAYOUT_FILL_X|
1237
+ LAYOUT_CENTER_Y)
1238
+ oops.frameStyle = FRAME_RAISED|FRAME_THICK
1239
+ oops.baseColor = 'dark grey'
1240
+ oops.textColor = 'red'
1241
+ oops.padLeft = oops.padRight = 15
1242
+ oops.shadowColor = 'black'
1243
+ oops.borderColor = 'white'
1244
+ oops.font = font
1245
+
1246
+ FXLabel.new(f,
1247
+ "\n#{@name} #{MSG_WAS_MODIFIED}#{MSG_SHOULD_I_SAVE_CHANGES}",
1248
+ nil, 0)
1249
+
1250
+ # Separator
1251
+ FXHorizontalSeparator.new(s,
1252
+ LAYOUT_SIDE_TOP|LAYOUT_FILL_X|SEPARATOR_GROOVE)
1253
+
1254
+ # Bottom buttons
1255
+ buttons = FXHorizontalFrame.new(s,
1256
+ LAYOUT_SIDE_BOTTOM|FRAME_NONE|
1257
+ LAYOUT_FILL_X|PACK_UNIFORM_WIDTH)
1258
+ # Accept
1259
+ yes = FXButton.new(buttons, BUTTON_YES, nil, dlg, FXDialogBox::ID_ACCEPT,
1260
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1261
+ LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1262
+ yes.connect(SEL_COMMAND) {
1263
+ dlg.close
1264
+ if save
1265
+ @window.close
1266
+ return true
1267
+ else
1268
+ return false
1269
+ end
1270
+ }
1271
+ FXButton.new(buttons, BUTTON_NO, nil, dlg, FXDialogBox::ID_ACCEPT,
1272
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1273
+ LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1274
+
1275
+ # Cancel
1276
+ FXButton.new(buttons, BUTTON_CANCEL, nil, dlg, FXDialogBox::ID_CANCEL,
1277
+ FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_X|
1278
+ LAYOUT_RIGHT|LAYOUT_CENTER_Y)
1279
+ yes.setDefault
1280
+ yes.setFocus
1281
+
1282
+ return false if dlg.execute == 0
1283
+ end
1284
+ @automap.destroy if @automap
1285
+ @automap = nil
1286
+ @window.close
1287
+ return true
1288
+ end
1289
+
1290
+ #
1291
+ # Create cursors
1292
+ #
1293
+ def _make_cursors
1294
+ pix = []
1295
+ 32.times { 32.times { pix << 255 << 255 << 255 << 255 } }
1296
+ pix = pix.pack('c*')
1297
+
1298
+
1299
+ ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'].each { |d|
1300
+ eval(<<-"EOF")
1301
+ @@cursor_#{d} = FXGIFCursor.new(@window.getApp, pix)
1302
+ FXFileStream.open('icons/room_#{d}.gif', FXStreamLoad) { |stream|
1303
+ @@cursor_#{d}.loadPixels(stream)
1304
+ }
1305
+ @@cursor_#{d}.create
1306
+ EOF
1307
+ }
1308
+
1309
+ @@cursor_move = FXCursor.new(@window.getApp, CURSOR_MOVE)
1310
+ @@cursor_move.create
1311
+
1312
+ @@cursor_arrow = FXCursor.new(@window.getApp, CURSOR_ARROW)
1313
+ @@cursor_arrow.create
1314
+
1315
+ @@cursor_cross = FXCursor.new(@window.getApp, CURSOR_CROSS)
1316
+ @@cursor_cross.create
1317
+ end
1318
+
1319
+ #
1320
+ # Create widgets for this map window
1321
+ #
1322
+ def _make_widgets
1323
+ @scrollwindow = FXScrollWindow.new(@window,
1324
+ SCROLLERS_NORMAL|SCROLLERS_TRACK)
1325
+ width = WW * @width
1326
+ height = HH * @height
1327
+
1328
+ @canvasFrame = FXVerticalFrame.new(@scrollwindow,
1329
+ FRAME_SUNKEN|LAYOUT_FILL_X|
1330
+ LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
1331
+
1332
+ # First, create an off-screen image for drawing and double buffering
1333
+ @image = FXBMPImage.new(@window.getApp, nil, IMAGE_SHMI|IMAGE_SHMP,
1334
+ width, height)
1335
+ @image.create
1336
+
1337
+ # Then create canvas
1338
+ @canvas = FXCanvas.new(@canvasFrame, nil, 0,
1339
+ LAYOUT_FIX_WIDTH|LAYOUT_FIX_HEIGHT|
1340
+ LAYOUT_TOP|LAYOUT_LEFT,
1341
+ 0, 0, width, height)
1342
+ @dirty = true
1343
+ # @canvas.connect(SEL_UPDATE, method(:update_cb))
1344
+
1345
+ @canvas.connect(SEL_PAINT, method(:draw))
1346
+ @canvas.backColor = @options['BG Color']
1347
+
1348
+ @canvas.connect(SEL_MOUSEWHEEL, method(:mousewheel_cb))
1349
+ @canvas.connect(SEL_LEFTBUTTONPRESS, method(:lmb_click_cb))
1350
+ @canvas.connect(SEL_LEFTBUTTONRELEASE, method(:mmb_release_cb))
1351
+ @canvas.connect(SEL_MOTION, method(:motion_cb))
1352
+ @canvas.connect(SEL_MIDDLEBUTTONPRESS, method(:mmb_click_cb))
1353
+ @canvas.connect(SEL_MIDDLEBUTTONRELEASE, method(:mmb_release_cb))
1354
+ @canvas.connect(SEL_RIGHTBUTTONPRESS, method(:rmb_click_cb))
1355
+ @canvas.connect(SEL_KEYPRESS, method(:keypress_cb))
1356
+
1357
+ if fxversion !~ /^1.2/
1358
+ @@tooltip = FXToolTip.new(@canvas.app, FXToolTip::TOOLTIP_PERMANENT)
1359
+ # Tooltip is too buggy and annoying. Turning it off for now.
1360
+ # @canvas.connect(SEL_QUERY_TIP, method(:tooltip_cb))
1361
+ end
1362
+ end
1363
+
1364
+ #
1365
+ # Main constructor. Create the object, initialize the pathmap
1366
+ # and initialize the widgets and cursors if a parent window is passed.
1367
+ #
1368
+ def initialize(name, parent = nil, default_options = nil,
1369
+ icon = nil, menu = nil, mode = nil,
1370
+ x = 0, y = 0, w = 0, h = 0)
1371
+ @mutex = Mutex.new
1372
+ @automap = nil
1373
+ @navigation = false
1374
+ @modified = false
1375
+ @mouseButton = false
1376
+ @filename = nil
1377
+ @complexConnection = false
1378
+ super(name)
1379
+ if parent
1380
+ @window = FXMDIChild.new(parent, name, icon, menu, mode, x, y, w, h)
1381
+ @options = default_options
1382
+ _make_cursors if not @@cursor_arrow
1383
+ _make_widgets
1384
+ end
1385
+ empty_pathmap
1386
+ self.zoom = 0.8
1387
+ end
1388
+
1389
+ #
1390
+ # Handle 'cutting' any selected rooms and/or connections
1391
+ #
1392
+ def _cut_selected
1393
+ rooms = @sections[@section].rooms.find_all { |r| r.selected }
1394
+ conns = @sections[@section].connections.find_all { |c| c.selected }
1395
+ ############################
1396
+ # First, handle rooms...
1397
+ ############################
1398
+ # Remove rooms from path map
1399
+ rooms.each { |r| @pmap[r.x][r.y] = nil }
1400
+ # Remove rooms from current section in map
1401
+ @sections[@section].rooms -= rooms
1402
+ # Add any connections pointing to removed rooms as connection to remove
1403
+ rooms.each { |r|
1404
+ conns += r.exits.find_all { |e| e != nil }
1405
+ }
1406
+
1407
+ #########################
1408
+ # Now, handle connections
1409
+ #########################
1410
+ conns.uniq!
1411
+
1412
+ # Remove connections from path map
1413
+ conns.each { |c| clean_path(c) }
1414
+ # Remove connections from current section in map
1415
+ @sections[@section].connections -= conns
1416
+
1417
+ return conns
1418
+ end
1419
+
1420
+ def cut_selected
1421
+ _cut_selected
1422
+ modified = true
1423
+ draw
1424
+ end
1425
+
1426
+ #
1427
+ # Handle deleting selected rooms and/or connections
1428
+ #
1429
+ def delete_selected
1430
+ conns = _cut_selected
1431
+
1432
+ # Remove room exits pointing to any removed connection
1433
+ conns.each { |c|
1434
+ a = c.roomA
1435
+ b = c.roomB
1436
+ a[a.exits.index(c)] = nil
1437
+ if b
1438
+ idx = b.exits.rindex(c)
1439
+ b[idx] = nil if idx
1440
+ end
1441
+ }
1442
+
1443
+ if @complexConnection
1444
+ @complexConnection = false
1445
+ status MSG_COMPLEX_CONNECTION_STOPPED
1446
+ end
1447
+
1448
+ modified = true
1449
+ create_pathmap
1450
+ draw
1451
+ end
1452
+
1453
+ #
1454
+ # Start a complex connection
1455
+ #
1456
+ def complex_connection
1457
+ return if @complexConnection or @navigation
1458
+ @complexConnection = true
1459
+ status MSG_COMPLEX_CONNECTION
1460
+ end
1461
+
1462
+ #
1463
+ # Given a selection of rooms, clear all of them from path map
1464
+ #
1465
+ def clean_room_selection(selection)
1466
+ selection.each { |r|
1467
+ @pmap[r.x][r.y] = nil
1468
+ clean_exits(r)
1469
+ }
1470
+ end
1471
+
1472
+ #
1473
+ # Given a selection of rooms, clear all of them from path map
1474
+ #
1475
+ def store_room_selection(selection)
1476
+ selection.each { |r|
1477
+ @pmap[r.x][r.y] = r
1478
+ }
1479
+ update_exits(selection)
1480
+ end
1481
+
1482
+ #
1483
+ # Clean all paths from path map for a room
1484
+ #
1485
+ def clean_exits(room)
1486
+ room.exits.each { |c|
1487
+ next if not c
1488
+ clean_path(c)
1489
+ }
1490
+ end
1491
+
1492
+ def show_roomlist
1493
+ if @@roomlist
1494
+ @@roomlist.copy_from(self)
1495
+ else
1496
+ require 'IFMapper/FXRoomList'
1497
+ @@roomlist = FXRoomList.new(self)
1498
+ end
1499
+ @@roomlist.show
1500
+ end
1501
+
1502
+ def show_itemlist
1503
+ if @@itemlist
1504
+ @@itemlist.copy_from(self)
1505
+ else
1506
+ require 'IFMapper/FXItemList'
1507
+ @@itemlist = FXItemList.new(self)
1508
+ end
1509
+ @@itemlist.show
1510
+ end
1511
+
1512
+ def self.no_maps
1513
+ @@roomlist.hide if @@roomlist
1514
+ @@win.hide if @@win
1515
+ FXRoom::no_maps
1516
+ FXConnection::no_maps
1517
+ end
1518
+
1519
+ #
1520
+ # If roomlist window is present, update it
1521
+ #
1522
+ def update_roomlist
1523
+ @@roomlist.copy_from(self) if @@roomlist
1524
+ @@win.copy_from(self) if @@win
1525
+ end
1526
+
1527
+ #
1528
+ # Find and update all paths in path map for a room
1529
+ #
1530
+ def update_exits(selection)
1531
+ create_pathmap
1532
+ @modified = true
1533
+ end
1534
+
1535
+
1536
+ #
1537
+ # Return the current active room
1538
+ #
1539
+ def current_room
1540
+ rooms = @sections[@section].rooms.find_all { |r| r.selected }
1541
+ return nil if rooms.empty? or rooms.size > 1
1542
+ return rooms[0]
1543
+ end
1544
+
1545
+ def cannot_automap(why)
1546
+ w = FXWarningBox.new(@window, "#{ERR_CANNOT_AUTOMAP}\n#{why}")
1547
+ w.execute
1548
+ end
1549
+
1550
+ #
1551
+ # Give user some warning if he tries to modify a read-only mode.
1552
+ #
1553
+ def navigation_warning
1554
+ w = FXWarningBox.new(@window, ERR_READ_ONLY_MAP)
1555
+ w.execute
1556
+ end
1557
+
1558
+
1559
+ #
1560
+ # Move selected rooms in one of the 8 cardinal directions.
1561
+ #
1562
+ def move_to(idx)
1563
+ return navigation_warning if @navigation
1564
+ selection = @sections[@section].rooms.find_all { |r| r.selected }
1565
+ return if selection.empty?
1566
+ clean_room_selection(selection)
1567
+
1568
+
1569
+ dx, dy = Room::DIR_TO_VECTOR[idx]
1570
+
1571
+ # Check that all nodes can be moved in the specified direction
1572
+ selection.each { |r|
1573
+ x = r.x + dx
1574
+ y = r.y + dy
1575
+ if x < 0 or y < 0 or x >= @width or y >= @height or
1576
+ @pmap.at(x).at(y).kind_of?(Room)
1577
+ store_room_selection(selection)
1578
+ dir = Room::DIRECTIONS[idx]
1579
+ status "#{ERR_CANNOT_MOVE_SELECTION} #{dir}."
1580
+ return
1581
+ end
1582
+ }
1583
+ selection.each { |r|
1584
+ r.x += dx
1585
+ r.y += dy
1586
+ }
1587
+ update_exits(selection)
1588
+ draw
1589
+ end
1590
+
1591
+ #
1592
+ # Move through an exit into another room. If exit is empty, create
1593
+ # a new neighboring room.
1594
+ #
1595
+ def move_thru(idx)
1596
+ room = current_room
1597
+ return if not room
1598
+ exit = room[idx]
1599
+ if exit
1600
+ room.selected = false
1601
+ if room == exit.roomA
1602
+ roomB = exit.roomB
1603
+ else
1604
+ roomB = exit.roomA
1605
+ end
1606
+ roomB.selected = true
1607
+ draw
1608
+ else
1609
+ return navigation_warning if @navigation
1610
+ x, y = room.x, room.y
1611
+ dx, dy = Room::DIR_TO_VECTOR[idx]
1612
+ x += dx
1613
+ y += dy
1614
+ x = 0 if x < 0
1615
+ y = 0 if y < 0
1616
+ x = @width-1 if x > @width-1
1617
+ y = @height-1 if y > @height-1
1618
+ if not @pmap.at(x).at(y).kind_of?(Room)
1619
+ room.selected = false
1620
+ roomB = new_xy_room(x * WW, y * HH)
1621
+ exitB = roomB.next_to?(room)
1622
+ if exitB and roomB.exits[exitB] == nil
1623
+ new_connection( roomB, exitB, room )
1624
+ end
1625
+ draw
1626
+ end
1627
+ end
1628
+ end
1629
+
1630
+ #
1631
+ # Handle a keypress
1632
+ #
1633
+ def keypress_cb( server, sel, event)
1634
+ case event.code
1635
+ when KEY_Escape
1636
+ if @complexConnection
1637
+ if @complexConnection.kind_of?(Array)
1638
+ @sections[@section].delete_connection_at(-1)
1639
+ status MSG_COMPLEX_CONNECTION_STOPPED
1640
+ draw
1641
+ end
1642
+ @complexConnection = false
1643
+ end
1644
+ when KEY_BackSpace, KEY_Delete
1645
+ return navigation_warning if @navigation
1646
+ delete_selected
1647
+ when KEY_c
1648
+ if event.state & CONTROLMASK != 0
1649
+ FXMapperWindow::copy_selected(self)
1650
+ draw
1651
+ end
1652
+ when KEY_v
1653
+ if event.state & CONTROLMASK != 0
1654
+ FXMapperWindow::paste_selected(self)
1655
+ @modified = true
1656
+ draw
1657
+ end
1658
+ when KEY_x
1659
+ return navigation_warning if @navigation
1660
+ if event.state & CONTROLMASK != 0
1661
+ FXMapperWindow::cut_selected(self)
1662
+ @modified = true
1663
+ draw
1664
+ else
1665
+ complex_connection
1666
+ end
1667
+ when KEY_plus
1668
+ zoom_in
1669
+ when KEY_minus
1670
+ zoom_out
1671
+ when KEY_KP_8
1672
+ move_thru(0)
1673
+ when KEY_KP_9
1674
+ move_thru(1)
1675
+ when KEY_KP_6
1676
+ move_thru(2)
1677
+ when KEY_KP_3
1678
+ move_thru(3)
1679
+ when KEY_KP_2
1680
+ move_thru(4)
1681
+ when KEY_KP_1
1682
+ move_thru(5)
1683
+ when KEY_KP_4
1684
+ move_thru(6)
1685
+ when KEY_KP_7
1686
+ move_thru(7)
1687
+ when KEY_Up
1688
+ move_to(0)
1689
+ when KEY_Down
1690
+ move_to(4)
1691
+ when KEY_Left
1692
+ move_to(6)
1693
+ when KEY_Right
1694
+ move_to(2)
1695
+ when KEY_End
1696
+ move_to(5)
1697
+ when KEY_Home
1698
+ move_to(7)
1699
+ when KEY_Page_Up
1700
+ move_to(1)
1701
+ when KEY_Page_Down
1702
+ move_to(3)
1703
+ end
1704
+ end
1705
+
1706
+ #
1707
+ # Draw template of diagonal connections in grid background
1708
+ #
1709
+ def draw_diagonal_connections(dc, event)
1710
+ ww = WW * @zoom
1711
+ hh = HH * @zoom
1712
+
1713
+ w = W * @zoom
1714
+ h = H * @zoom
1715
+
1716
+ ws = WS * @zoom
1717
+ hs = HS * @zoom
1718
+
1719
+ ws_2 = WS_2 * @zoom
1720
+ hs_2 = HS_2 * @zoom
1721
+
1722
+ maxy = @height - 1
1723
+ maxx = @width - 1
1724
+
1725
+ (0...@height).each { |yy|
1726
+ (0...@width).each { |xx|
1727
+ next if @pmap.at(xx).at(yy).kind_of?(Connection)
1728
+ x = xx * ww
1729
+ y = yy * hh
1730
+
1731
+ if yy < maxy and xx < maxx
1732
+ # First, draw \
1733
+ x1 = x + w + ws_2
1734
+ y1 = y + h + hs_2
1735
+
1736
+ x2 = x1 + ws
1737
+ y2 = y1 + hs
1738
+ dc.drawLine( x1, y1, x2, y2 )
1739
+
1740
+ end
1741
+
1742
+ if yy < maxy and xx > 0 and xx <= maxx
1743
+ # Then, draw /
1744
+ x1 = x + ws_2
1745
+ y1 = y + h + hs_2
1746
+
1747
+ x2 = x1 - ws
1748
+ y2 = y1 + hs
1749
+ dc.drawLine( x2, y2, x1, y1 )
1750
+ end
1751
+ }
1752
+ }
1753
+ end
1754
+
1755
+ #
1756
+ # Draw template of straight connections in grid background
1757
+ #
1758
+ def draw_straight_connections(dc, event)
1759
+ ww = WW * @zoom
1760
+ hh = HH * @zoom
1761
+
1762
+ w = W * @zoom
1763
+ h = H * @zoom
1764
+
1765
+ ws_2 = WS_2 * @zoom
1766
+ hs_2 = HS_2 * @zoom
1767
+
1768
+ #---- dummy check to catch an ugly bug that I cannot track...
1769
+ create_pathmap if @pmap.size < @width or @pmap[0].size < @height
1770
+
1771
+ # First, draw horizontal lines
1772
+ (0...@height).each { |yy|
1773
+ (0..@width-2).each { |xx|
1774
+ next if @pmap.at(xx).at(yy).kind_of?(Connection) or
1775
+ @pmap.at(xx+1).at(yy).kind_of?(Connection)
1776
+ x1 = xx * ww + w + ws_2
1777
+ x2 = (xx + 1) * ww + ws_2
1778
+ y1 = yy * hh + h / 2 + hs_2
1779
+
1780
+ dc.drawLine( x1, y1, x2, y1 )
1781
+ }
1782
+ }
1783
+
1784
+ # Then, draw vertical lines
1785
+ (0...@width).each { |xx|
1786
+ (0..@height-2).each { |yy|
1787
+ next if @pmap.at(xx).at(yy).kind_of?(Connection) or
1788
+ @pmap.at(xx).at(yy+1).kind_of?(Connection)
1789
+ x1 = xx * ww + w / 2 + ws_2
1790
+ y1 = yy * hh + h + hs_2
1791
+ y2 = (yy + 1) * hh + hs_2
1792
+
1793
+ dc.drawLine( x1, y1, x1, y2 )
1794
+ }
1795
+ }
1796
+ end
1797
+
1798
+
1799
+
1800
+ #
1801
+ # Draw template of room squares in background
1802
+ #
1803
+ def draw_grid(dc, event = nil)
1804
+
1805
+ dc.foreground = "black"
1806
+ dc.lineWidth = 0
1807
+ dc.lineStyle = LINE_ONOFF_DASH
1808
+
1809
+ ww = WW * @zoom
1810
+ hh = HH * @zoom
1811
+
1812
+ w = W * @zoom
1813
+ h = H * @zoom
1814
+
1815
+ ws_2 = WS_2 * @zoom
1816
+ hs_2 = HS_2 * @zoom
1817
+
1818
+ (0...@width).each { |xx|
1819
+ (0...@height).each { |yy|
1820
+ next if @pmap.at(xx).at(yy)
1821
+ x = xx * ww + ws_2
1822
+ y = yy * hh + hs_2
1823
+ dc.drawRectangle( x, y, w, h )
1824
+ }
1825
+ }
1826
+ end
1827
+
1828
+ #
1829
+ # Clean background to solid color
1830
+ #
1831
+ def draw_background(dc, event = nil)
1832
+ dc.foreground = @options['BG Color']
1833
+ dc.fillRectangle(0,0, @canvas.width, @canvas.height)
1834
+ end
1835
+
1836
+ #
1837
+ # Draw connections among rooms
1838
+ #
1839
+ def draw_connections(dc)
1840
+ dc.lineStyle = LINE_SOLID
1841
+ dc.lineWidth = 3 * @zoom
1842
+ dc.lineWidth = 3 if dc.lineWidth < 3
1843
+ @sections[@section].connections.each { |c| c.draw(dc, @zoom, @options) }
1844
+ end
1845
+
1846
+ #
1847
+ # Draw a single room (callback used when editing room dialog box)
1848
+ #
1849
+ def draw_room(room)
1850
+ idx = @sections[@section].rooms.index(room)
1851
+ return unless idx
1852
+
1853
+ dc = FXDCWindow.new(@canvas)
1854
+ dc.font = @font
1855
+ data = { }
1856
+ data['font'] = @font
1857
+ data['objfont'] = @objfont
1858
+ room.draw(dc, @zoom, idx, @options, data)
1859
+ dc.end
1860
+ end
1861
+
1862
+ #
1863
+ # Draw all rooms in current section
1864
+ #
1865
+ def draw_rooms(dc)
1866
+ data = { }
1867
+ data['font'] = @font
1868
+ data['objfont'] = @objfont
1869
+ @sections[@section].rooms.each_with_index { |room, idx|
1870
+ room.draw(dc, @zoom, idx, @options, data)
1871
+ }
1872
+ end
1873
+
1874
+ #
1875
+ # Draw mapname
1876
+ #
1877
+ def draw_mapname(dc)
1878
+ fontsize = (24 * @zoom).to_i
1879
+ font = FXFont.new(@window.getApp, @options['Font Text'], fontsize)
1880
+ font.create
1881
+
1882
+ x = @width * WW / 2.0 - @name.size * 24
1883
+ dc.drawText(x, 30, @name)
1884
+ end
1885
+
1886
+ #
1887
+ # Print map
1888
+ #
1889
+ def print(printer)
1890
+ # dc = FXDCPrint.new(@window.getApp)
1891
+ require 'IFMapper/MapPrinting'
1892
+ require 'IFMapper/FXDCPostscript'
1893
+ oldzoom = @zoom
1894
+ oldsection = @section
1895
+ self.zoom = 1.0
1896
+
1897
+ num = pack_sections( @width, @height )
1898
+ begin
1899
+ dc = FXDCPostscript.new(@window.getApp)
1900
+ xmax = @width * WW
1901
+ ymax = @height * HH
1902
+ dc.setContentRange(0, 0, xmax, ymax)
1903
+ dc.beginPrint(printer) {
1904
+ page = -1
1905
+ 0.upto(@sections.size-1) { |p|
1906
+ self.section = p
1907
+ clear_selection
1908
+ if page != sect.page
1909
+ dc.beginPage(sect.page)
1910
+ draw_mapname( dc )
1911
+ end
1912
+
1913
+ dc.lineCap = CAP_ROUND
1914
+ # draw_grid(dc)
1915
+ draw_connections(dc)
1916
+ draw_rooms(dc)
1917
+
1918
+ if page != sect.page
1919
+ page = sect.page
1920
+ dc.endPage()
1921
+ end
1922
+ }
1923
+ }
1924
+ rescue => e
1925
+ status "#{e}"
1926
+ end
1927
+ self.section = oldsection
1928
+ self.zoom = oldzoom
1929
+ draw
1930
+ end
1931
+
1932
+
1933
+ #
1934
+ # Draw map
1935
+ #
1936
+ def draw(sender = nil, sel = nil, event = nil)
1937
+ return if @mutex.locked? or not @canvas.created?
1938
+
1939
+ if not @image.created?
1940
+ # puts "Image was not created. Try again"
1941
+ self.zoom = @zoom
1942
+ end
1943
+
1944
+ pos = @scrollwindow.position
1945
+ w = @scrollwindow.getViewportWidth
1946
+ h = @scrollwindow.getViewportHeight
1947
+
1948
+ # The -5 seems to be a bug in fox. don't ask me.
1949
+ cx = -pos[0]-5
1950
+ cx = 0 if cx < 0
1951
+ cy = -pos[1]-5
1952
+ cy = 0 if cy < 0
1953
+
1954
+ dc = FXDCWindow.new(@image)
1955
+ dc.setClipRectangle( cx, cy, w, h)
1956
+ dc.font = @font
1957
+ #dc.lineCap = CAP_ROUND
1958
+ draw_background(dc, event)
1959
+ draw_grid(dc, event) if @options['Grid Boxes']
1960
+ if @options['Grid Straight Connections']
1961
+ draw_straight_connections(dc, event)
1962
+ end
1963
+ if @options['Grid Diagonal Connections']
1964
+ draw_diagonal_connections(dc, event)
1965
+ end
1966
+ draw_connections(dc)
1967
+ draw_rooms(dc)
1968
+ dc.end
1969
+
1970
+
1971
+ # Blit the off-screen image into canvas
1972
+ dc = FXDCWindow.new(@canvas)
1973
+ dc.setClipRectangle( cx, cy, w, h)
1974
+ dc.drawImage(@image,0,0)
1975
+ dc.end
1976
+
1977
+ end
1978
+
1979
+
1980
+ #
1981
+ # Perform the actual saving of the map
1982
+ #
1983
+ def _save
1984
+ if @complexConnection
1985
+ # If we have an incomplete connection, remove it
1986
+ @sections[@section].delete_connection_at(-1)
1987
+ end
1988
+
1989
+ if @filename !~ /\.map$/i
1990
+ @filename << '.map'
1991
+ end
1992
+
1993
+ status "#{MSG_SAVING} '#{@filename}'..."
1994
+
1995
+ # Make sure we save a valid map. This is mainly a fail-safe
1996
+ # in case of an autosave due to a bug.
1997
+ verify_integrity
1998
+
1999
+ @version = FILE_FORMAT_VERSION
2000
+ begin
2001
+ f = File.open(@filename, "wb")
2002
+ f.puts Marshal.dump(self)
2003
+ f.close
2004
+ rescue => e
2005
+ status "#{ERR_COULD_NOT_SAVE} '#{@filename}': #{e}"
2006
+ sleep 4
2007
+ return false
2008
+ end
2009
+ @modified = false
2010
+ status "#{MSG_SAVED} '#{@filename}'."
2011
+ sleep 0.5
2012
+ return true
2013
+ end
2014
+
2015
+ #
2016
+ # Save the map. If the map's filename is not defined, call save_as
2017
+ #
2018
+ def save
2019
+ unless @filename
2020
+ save_as
2021
+ else
2022
+ _save
2023
+ end
2024
+ end
2025
+
2026
+
2027
+ #
2028
+ # Export map as an IFM map file
2029
+ #
2030
+ def export_ifm(file)
2031
+ require 'IFMapper/IFMWriter'
2032
+ file += '.ifm' if file !~ /\.ifm$/
2033
+ IFMWriter.new(self, file)
2034
+ end
2035
+
2036
+
2037
+ #
2038
+ # Export map as a set of TADS3 source code files
2039
+ #
2040
+ def export_tads(file)
2041
+ require 'IFMapper/TADSWriter'
2042
+ file.sub!(/(-\d+)?\.t/, '')
2043
+ TADSWriter.new(self, file)
2044
+ end
2045
+
2046
+ #
2047
+ # Export map as a set of Inform source code files
2048
+ #
2049
+ def export_inform7(file)
2050
+ require 'IFMapper/Inform7Writer'
2051
+ file.sub!(/.inform$/, '')
2052
+ Inform7Writer.new(self, file)
2053
+ end
2054
+
2055
+ #
2056
+ # Export map as a set of Inform source code files
2057
+ #
2058
+ def export_inform(file, version = 6)
2059
+ if file =~ /\.inform$/ or version > 6
2060
+ return export_inform7(file)
2061
+ end
2062
+
2063
+ require 'IFMapper/InformWriter'
2064
+ file.sub!(/(-\d+)?\.inf/, '')
2065
+ InformWriter.new(self, file)
2066
+ end
2067
+
2068
+ #
2069
+ # Save the map under a new filename, bringing up a file requester
2070
+ #
2071
+ def save_as
2072
+ require 'IFMapper/FXMapFileDialog'
2073
+ file = FXMapFileDialog.new(@window, "#{MSG_SAVE_MAP} #{@name}",
2074
+ FXMapFileDialog::KNOWN_SAVE_EXTENSIONS).filename
2075
+ if file != ''
2076
+ if File.exists?(file)
2077
+ dlg = FXWarningBox.new(@window, "#{file}\n#{WARN_OVERWRITE_MAP}")
2078
+ return if dlg.execute == 0
2079
+ end
2080
+
2081
+ case file
2082
+ when /\.inform$/, /\.inf$/
2083
+ export_inform(file)
2084
+ when /\.ifm$/
2085
+ export_ifm(file)
2086
+ when /\.t$/
2087
+ export_tads(file)
2088
+ else
2089
+ @filename = file
2090
+ return _save
2091
+ end
2092
+ end
2093
+ return false
2094
+ end
2095
+
2096
+ #
2097
+ # Open the map's property window
2098
+ #
2099
+ def properties
2100
+ if not @@win
2101
+ @@win = FXMapDialogBox.new(@window)
2102
+ end
2103
+ @@win.copy_from(self)
2104
+ @@win.show
2105
+ end
2106
+
2107
+ def stop_automap
2108
+ return unless @automap
2109
+ @automap.destroy
2110
+ @automap = nil
2111
+ GC.start
2112
+ update_title
2113
+ end
2114
+
2115
+ def start_automap
2116
+ if @automap
2117
+ stop_automap
2118
+ end
2119
+ require 'IFMapper/FXMapFileDialog'
2120
+ file = FXMapFileDialog.new(@window, MSG_LOAD_TRANSCRIPT,
2121
+ [
2122
+ EXT_TRANSCRIPT,
2123
+ EXT_ALL_FILES
2124
+ ]).filename
2125
+ return if file == ''
2126
+ require 'IFMapper/TranscriptReader'
2127
+
2128
+ begin
2129
+ @automap = TranscriptReader.new(self, file)
2130
+ @automap.properties(true)
2131
+ @automap.start
2132
+ rescue Errno::EACCES, Errno::ENOENT => e
2133
+ dlg = FXWarningBox.new(@window, "#{ERR_CANNOT_OPEN_TRANSCRIPT}\n#{e}")
2134
+ dlg.execute
2135
+ return
2136
+ rescue => e
2137
+ puts e.backtrace
2138
+ dlg = FXWarningBox.new(@window, "#{ERR_PARSE_TRANSCRIPT}\n#{e}\n#{e.backtrace}")
2139
+ dlg.execute
2140
+ raise
2141
+ end
2142
+ create_pathmap
2143
+ draw
2144
+ update_title
2145
+ end
2146
+
2147
+ end