ifmapper 1.0.0 → 1.0.6

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