· 7 years ago · Jan 18, 2019, 10:32 PM
1#!/usr/bin/python3
2###########################################
3#
4# Binding of Isaac: Rebirth Stage Editor
5# by Colin Noga
6# Chronometrics / Tempus
7#
8 #
9 #
10 # UI Elements
11 # Main Scene: Click to select, right click to paint. Auto resizes to match window zoom. Renders background.
12 # Entity: A QGraphicsItem to be added to the scene for drawing.
13 # Room List: Shows a list of rooms with mini-renders as icons. Needs add and remove buttons. Should drag and drop re-sort.
14 # Entity Palette: A palette from which to choose entities to draw.
15 # Properties: Possibly a contextual menu thing?
16 #
17 #
18#
19# Afterbirth Todo:
20# Fix up Rebirth/Afterbirth detection
21#
22# Low priority
23# Clear Corner Rooms Grid
24# Fix icon for win_setup.py
25# Bosscolours for the alternate boss entities
26# Fix mod auto-detection
27# Multi-entity stack improvments so it doesn't crash all the time
28#
29
30
31from PyQt5.QtCore import *
32from PyQt5.QtGui import *
33from PyQt5.QtWidgets import *
34from collections import OrderedDict
35from copy import deepcopy
36
37import struct, os, subprocess, platform, webbrowser, re
38import xml.etree.ElementTree as ET
39import psutil
40
41
42########################
43# XML Data #
44########################
45
46def getEntityXML():
47 tree = ET.parse('resources/EntitiesAfterbirthPlus.xml')
48 root = tree.getroot()
49
50 return root
51
52def findInstallPath():
53 installPath = ''
54
55 if QFile.exists(settings.value('InstallFolder')):
56 installPath = settings.value('InstallFolder')
57
58 else:
59 cantFindPath = False
60 # Windows path things
61 if "Windows" in platform.system():
62 basePath = QSettings('HKEY_CURRENT_USER\\Software\\Valve\\Steam', QSettings.NativeFormat).value('SteamPath')
63 if not basePath:
64 cantFindPath = True
65
66 installPath = os.path.join(basePath, "steamapps/common", "The Binding of Isaac Rebirth")
67 if not QFile.exists(installPath):
68 cantFindPath = True
69
70 libconfig = os.path.join(basePath, "steamapps/libraryfolders.vdf")
71 if os.path.isfile(libconfig):
72 libLines = list(open(libconfig, 'r'))
73 matcher = re.compile(r'"\d"')
74 installDirs = map(lambda parts: os.path.normpath(parts[1].strip('"')),
75 filter(lambda parts: matcher.match(parts[0]),
76 map(lambda line: line.split(), libLines)))
77 for root in installDirs:
78 installPath = os.path.join(root, 'steamapps/common', 'The Binding of Isaac Rebirth')
79 if QFile.exists(installPath):
80 cantFindPath = False
81 break
82
83 # Mac Path things
84 elif "Darwin" in platform.system():
85 installPath = os.path.expanduser("~/Library/Application Support/Steam/steamapps/common/The Binding of Isaac Rebirth/The Binding of Isaac Rebirth.app/Contents/Resources")
86 if not QFile.exists(installPath):
87 cantFindPath = True
88
89 # Linux and others
90 elif "Linux" in platform.system():
91 installPath = os.path.expanduser("~/.local/share/Steam/steamapps/common/The Binding of Isaac Rebirth")
92 if not QFile.exists(installPath):
93 cantFindPath = True
94 else:
95 cantFindPath = True
96
97 # Looks like nothing was selected
98 if len(installPath) == 0:
99 print("Could not find The Binding of Isaac: Afterbirth+ install folder (%s)" % installPath)
100 return ''
101
102 settings.setValue('InstallFolder', installPath)
103
104 return installPath
105
106def findModsPath(installPath=None):
107 installPath = installPath or findInstallPath()
108 modsPath = ''
109
110 if QFile.exists(settings.value('ModsFolder')):
111 modsPath = settings.value('ModsFolder')
112
113 elif len(installPath) > 0:
114 modd = os.path.join(installPath, "savedatapath.txt")
115 if os.path.isfile(modd):
116 lines = list(open(modd, 'r'))
117 modDirs = list(filter(lambda parts: parts[0] == 'Modding Data Path',
118 map(lambda line: line.split(': '), lines)))
119 if len(modDirs) > 0:
120 modsPath = os.path.normpath(modDirs[0][1].strip())
121 if not QFile.exists(modsPath):
122 cantFindPath = True
123 else:
124 cantFindPath = False
125 # Windows path things
126 if "Windows" in platform.system():
127 modsPath = os.path.join(os.path.expanduser("~"), "Documents", "My Games", "Binding of Isaac Afterbirth+ Mods")
128 if not QFile.exists(modsPath):
129 cantFindPath = True
130
131 # Mac Path things
132 elif "Darwin" in platform.system():
133 modsPath = os.path.expanduser("~/Library/Application Support/Binding of Isaac Afterbirth+ Mods")
134 if not QFile.exists(modsPath):
135 cantFindPath = True
136
137 # Linux and others
138 else:
139 modsPath = os.path.expanduser("~/.local/share/binding of isaac afterbirth+ mods/")
140 if not QFile.exists(modsPath):
141 cantFindPath = True
142
143 # Fallback Resource Folder Locating
144 if cantFindPath == True:
145 modsPathOut = QFileDialog.getExistingDirectory(None, 'Please Locate The Binding of Isaac: Afterbirth+ Mods Folder')
146 if not modsPathOut:
147 QMessageBox.warning(None, "Error", "Couldn't locate Mods folder and no folder was selected.")
148 return
149 else:
150 modsPath = modsPathOut
151 if modsPath == "":
152 QMessageBox.warning(None, "Error", "Couldn't locate Mods folder and no folder was selected.")
153 return
154 if not QDir(modsPath).exists:
155 QMessageBox.warning(None, "Error", "Selected folder does not exist or is not a folder.")
156 return
157
158 # Looks like nothing was selected
159 if len(modsPath) == 0:
160 QMessageBox.warning(None, "Error", "Could not find The Binding of Isaac: Afterbirth+ Mods folder (%s)" % modsPath)
161 return ''
162
163 settings.setValue('ModsFolder', modsPath)
164
165 return modsPath
166
167def linuxPathSensitivityTraining(path):
168
169 path = path.replace("\\", "/")
170
171 directory, file = os.path.split(os.path.normpath(path))
172
173 if not os.path.isdir(directory):
174 return None
175
176 contents = os.listdir(directory)
177
178 for item in contents:
179 if item.lower() == file.lower():
180 return os.path.normpath(os.path.join(directory, item))
181
182 return os.path.normpath(path)
183
184def loadFromModXML(modPath, name, entRoot, resourcePath):
185
186 anm2root = entRoot.get("anm2root")
187
188 # Iterate through all the entities
189 enList = entRoot.findall("entity")
190
191 # Skip if the mod is empty
192 if len(enList) == 0:
193 return
194
195 print('-----------------------\nLoading entities from "%s"' % name)
196
197 def mapEn(en):
198 # Fix some shit
199 i = int(en.get("id"))
200 s = en.get("subtype") or '0'
201 v = en.get("variant") or '0'
202
203 if i >= 1000 or i in (0, 1, 7, 8, 9):
204 print('Skipping: Invalid entity type %d: %s' % (i, en.get("name")))
205 return None
206
207 # Grab the anm location
208 anmPath = linuxPathSensitivityTraining(os.path.join(modPath, "resources", anm2root, en.get("anm2path"))) or ''
209 print('LOADING: %s' % anmPath)
210 if not os.path.isfile(anmPath):
211 print('Skipping: Invalid anm2!')
212 return None
213
214 anm2Dir, anm2File = os.path.split(anmPath)
215
216 # Grab the first frame of the anm
217 anmTree = ET.parse(anmPath)
218 spritesheets = anmTree.findall(".Content/Spritesheets/Spritesheet")
219 layers = anmTree.findall(".Content/Layers/Layer")
220 default = anmTree.find("Animations").get("DefaultAnimation")
221
222 anim = anmTree.find("./Animations/Animation[@Name='{0}']".format(default))
223 layer = anim.find(".//LayerAnimation[Frame]")
224
225 imgPath = None
226 x, y, h, w = 0, 0, 0, 0
227 if layer:
228 frame = layer.find('Frame')
229
230 # Here's the anm specs
231 x = int(frame.get("XCrop"))
232 y = int(frame.get("YCrop"))
233 h = int(frame.get("Height"))
234 w = int(frame.get("Width"))
235
236 sheetPath = spritesheets[int(layers[int(layer.get("LayerId"))].get("SpritesheetId"))].get("Path")
237 image = os.path.join(anm2Dir, sheetPath)
238 imgPath = linuxPathSensitivityTraining(image)
239 if not imgPath:
240 image = re.sub(r'.*resources', resourcePath, image)
241 imgPath = linuxPathSensitivityTraining(image)
242
243 filename = "resources/Entities/questionmark.png"
244 if imgPath:
245 # Get the Pixmap
246 pixmap = QPixmap()
247
248 # Load the Image
249 sourceImage = QImage(imgPath)
250
251 # Create the destination
252 pixmapImg = QImage(w, h, sourceImage.format())
253 pixmapImg.fill(0)
254
255 # Transfer the crop area to the pixmap
256 cropRect = QRect(x, y, w, h)
257
258 RenderPainter = QPainter()
259 RenderPainter.begin(pixmapImg)
260 RenderPainter.drawImage(QPoint(0,0), sourceImage, cropRect)
261 RenderPainter.end()
262
263 # Save it to a Temp file - better than keeping it in memory for user retrieval purposes?
264 filename = "resources/Entities/ModTemp/{0}.{1}.{2} - {3}.png".format(en.get("id"), v, s, en.get("name"))
265 pixmapImg.save(filename, "PNG")
266
267 # Write the modded entity to the entityXML temporarily for runtime
268 etmp = ET.Element("entity")
269 etmp.set("Name", en.get("name"))
270 etmp.set("ID", str(i))
271 etmp.set("Subtype", s)
272 etmp.set("Variant", v)
273 etmp.set("Image", filename)
274
275 i = int(i)
276 etmp.set("Group", "(Mod) %s" % name)
277 etmp.set("Kind", "Mods")
278 if i == 3:
279 etmp.set("Kind", "Collect")
280 etmp.set("Group", "(Mod) %s (Familiar)" % name)
281 elif i == 5: # pickups
282 if v == 100: # collectible
283 return None
284 etmp.set("Kind", "Pickup")
285 elif i in (2, 4, 6): # tears, live bombs, machines
286 etmp.set("Kind", "Stage")
287 elif en.get("boss") == '1':
288 etmp.set("Kind", "Bosses")
289 else:
290 etmp.set("Kind", "Enemies")
291
292 return etmp
293
294 return filter(lambda x: x != None, map(mapEn, enList))
295
296def loadFromMod(modPath, name, entRoot):
297
298 brPath = os.path.join(modPath, 'basementrenovator')
299 if not os.path.isdir(brPath):
300 return
301
302 entFile = os.path.join(brPath, 'EntitiesMod.xml')
303 if not os.path.isfile(entFile):
304 return
305
306 tree = ET.parse(entFile)
307 root = tree.getroot()
308
309 enList = root.findall('entity')
310 if len(enList) == 0:
311 return
312
313 print('-----------------------\nLoading entities from "%s"' % name)
314
315 def mapEn(en):
316 imgPath = linuxPathSensitivityTraining(os.path.join(brPath, en.get('Image')))
317
318 i = en.get('ID')
319 s = en.get('Subtype') or '0'
320 v = en.get('Variant') or '0'
321
322 entXML = None
323
324 if en.get('Metadata') != '1':
325 entXML = entRoot.find("entity[@id='%s'][@variant='%s'][@subtype='%s']" % (i, v, s))
326 if not entXML:
327 if s == '0':
328 entXML = entRoot.find("entity[@id='%s'][@variant='%s']" % (i, v))
329 if not entXML and v == '0':
330 entXML = entRoot.find("entity[@id='%s']" % i)
331 elif v == '0':
332 entXML = entRoot.find("entity[@id='%s'][@subtype='%s']" % (i, s))
333 if not entXML:
334 print('Loading invalid entity: ' + str(en.attrib))
335
336 # Write the modded entity to the entityXML temporarily for runtime
337 if not en.get('Group'):
338 en.set('Group', '(Mod) %s' % name)
339 en.set("Image", imgPath)
340
341 en.set("Subtype", s)
342 en.set("Variant", v)
343
344 en.set('BaseHP', entXML and entXML.get('baseHP') or en.get('BaseHP') or '0')
345
346 return en
347
348 return list(map(mapEn, enList))
349
350def loadMods(autogenerate, installPath, resourcePath):
351 global entityXML
352
353 # Each mod in the mod folder is a Group
354 modsPath = findModsPath(installPath)
355
356 modsInstalled = os.listdir(modsPath)
357
358 print('LOADING MOD CONTENT')
359 for mod in modsInstalled:
360 modPath = os.path.join(modsPath, mod)
361
362 # Make sure we're a mod
363 if not os.path.isdir(modPath) or os.path.isfile(os.path.join(modPath, 'disable.it')):
364 continue
365
366 # Get the mod name
367 name = mod
368 try:
369 tree = ET.parse(os.path.join(modPath, 'metadata.xml'))
370 root = tree.getroot()
371 name = root.find("name").text
372 except ET.ParseError:
373 print('Failed to parse mod metadata "%s", falling back on default name' % name)
374
375 # add dedicated entities
376 entPath = os.path.join(modPath, 'content/entities2.xml')
377 if os.path.exists(entPath):
378 # Grab their Entities2.xml
379 entRoot = None
380 try:
381 entRoot = ET.parse(entPath).getroot()
382 except ET.ParseError as e:
383 print('ERROR parsing entities2 xml for mod "{0}": {1}'.format(name, e))
384 continue
385
386 ents = None
387 if autogenerate:
388 ents = loadFromModXML(modPath, name, entRoot, resourcePath)
389 else:
390 ents = loadFromMod(modPath, name, entRoot)
391
392 if ents:
393 for ent in ents:
394 name, i, v, s = ent.get('Name'), int(ent.get('ID')), int(ent.get('Variant')), int(ent.get('Subtype'))
395
396 if i >= 1000:
397 print('Entity "%s" has a type outside the 0 - 999 range! (%d) It will not load properly from rooms!' % (name, i))
398 if v >= 4096:
399 print('Entity "%s" has a variant outside the 0 - 4095 range! (%d)' % (name, v))
400 if s >= 256:
401 print('Entity "%s" has a subtype outside the 0 - 255 range! (%d)' % (name, s))
402
403 entityXML.extend(ents)
404
405########################
406# Scene/View #
407########################
408
409class RoomScene(QGraphicsScene):
410
411 def __init__(self):
412 QGraphicsScene.__init__(self, 0, 0, 0, 0)
413 self.newRoomSize(13, 7, 1)
414
415 self.BG = [
416 "01_basement", "02_cellar", "03_caves", "04_catacombs",
417 "05_depths", "06_necropolis", "07_the womb", "08_utero",
418 "09_sheol", "10_cathedral", "11_chest", "12_darkroom",
419 "13_burningbasement", "14_floodedcaves", "15_dankdepths", "16_scarredwomb",
420 "18_bluewomb",
421 "0a_library", "0b_shop", "0c_isaacsroom", "0d_barrenroom",
422 "0e_arcade", "0e_diceroom", "0f_secretroom"
423 ]
424 self.grid = True
425
426 # Make the bitfont
427 q = QImage()
428 q.load('resources/UI/Bitfont.png')
429
430 self.bitfont = [ QPixmap.fromImage(q.copy(i * 12, j * 12, 12, 12)) for j in range(int(q.height() / 12)) for i in range(int(q.width() / 12)) ]
431 self.bitText = True
432
433 self.tile = None
434
435 def newRoomSize(self, w, h, s):
436 # Fuck their room size code is inelegant and annoying - these are kept for error checking purposes, of which I didn't do a great job
437 self.initialRoomWidth = w
438 self.initialRoomHeight = h
439
440 # Drawing variables
441 self.roomWidth = w
442 self.roomHeight = h
443 self.roomShape = s
444
445 self.setSceneRect(-52, -52, self.roomWidth * 26 + 52 * 2, self.roomHeight * 26 + 52 * 2)
446
447 def clearDoors(self):
448 for item in self.items():
449 if isinstance(item, Door):
450 item.remove()
451
452 def drawForeground(self, painter, rect):
453
454 # Bitfont drawing: moved to the RoomEditorWidget.drawForeground for easier anti-aliasing
455
456 # Grey out the screen to show it's inactive if there are no rooms selected
457 if mainWindow.roomList.selectedRoom() == None:
458 b = QBrush(QColor(255, 255, 255, 100))
459 painter.setPen(Qt.white)
460 painter.setBrush(b)
461
462 painter.fillRect(rect, b)
463 return
464
465 # Draw me a foreground grid
466 if not self.grid: return
467
468 painter.setRenderHint(QPainter.Antialiasing, True)
469 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
470
471 rect = QRectF(0, 0, self.roomWidth * 26, self.roomHeight * 26)
472
473 # Modify Rect for slim rooms
474 if self.roomShape in [2, 7]:
475 rect = QRectF(0, 52, self.roomWidth * 26, self.roomHeight * 26)
476
477 if self.roomShape in [3, 5]:
478 rect = QRectF(104, 0, self.roomWidth * 26, self.roomHeight * 26)
479
480 # Actual drawing code
481 ts = 26
482
483 startx = rect.x()
484 endx = startx + rect.width()
485
486 starty = rect.y()
487 endy = starty + rect.height()
488
489 painter.setPen(QPen(QColor.fromRgb(255, 255, 255, 100), 1, Qt.DashLine))
490
491 x = startx
492 y1 = rect.top()
493 y2 = rect.bottom()
494 while x <= endx:
495 painter.drawLine(x, starty, x, endy)
496 x += ts
497
498 y = starty
499 x1 = rect.left()
500 x2 = rect.right()
501 while y <= endy:
502 painter.drawLine(startx, y, endx, y)
503 y += ts
504
505 def loadBackground(self):
506
507 roomBG = 1
508
509 if mainWindow.roomList.selectedRoom():
510 roomBG = mainWindow.roomList.selectedRoom().roomBG
511
512 self.tile = QImage()
513 self.tile.load('resources/Backgrounds/{0}.png'.format(self.BG[roomBG-1]))
514
515 self.corner = self.tile.copy(QRect(0, 0, 26 * 7, 26 * 4))
516 self.vert = self.tile.copy(QRect(26 * 7, 0, 26 * 2, 26 * 6))
517 self.horiz = self.tile.copy(QRect(0, 26 * 4, 26 * 9, 26 * 2))
518
519 # I'll need to do something here for the other corner
520 self.innerCorner = QImage()
521 self.innerCorner.load('resources/Backgrounds/{0}Inner.png'.format(self.BG[roomBG - 1]))
522
523 def drawBackground(self, painter, rect):
524
525 self.loadBackground()
526
527 ########## SHAPE DEFINITIONS
528 # w x h
529 # 1 = 1x1, 2 = 1x0.5, 3 = 0.5x1, 4 = 2x1, 5 = 2x0.5, 6 = 1x2, 7 = 0.5x2, 8 = 2x2
530 # 9 = DR corner, 10 = DL corner, 11 = UR corner, 12 = UL corner
531
532 # Regular Rooms
533 if self.roomShape in [1, 4, 6, 8]:
534 self.drawBGRegularRooms(painter, rect)
535
536 # Slim Rooms
537 elif self.roomShape in [2, 3, 5, 7]:
538 self.drawBGSlimRooms(painter, rect)
539
540 # L Rooms
541 elif self.roomShape in [9, 10, 11, 12]:
542 self.drawBGCornerRooms(painter, rect)
543
544 # Uh oh
545 else:
546 print ("This room is not a known shape. {0} - {1} x {2}".format(self.roomShape, self.roomWidth, self.roomHeight))
547 self.drawBGRegularRooms(painter, rect)
548
549 def drawBGRegularRooms(self, painter, rect):
550 t = -52
551 xm = 26 * (self.roomWidth + 2) - 26 * 7
552 ym = 26 * (self.roomHeight + 2) - 26 * 4
553
554 # Corner Painting
555 painter.drawPixmap(t, t, QPixmap().fromImage(self.corner.mirrored(False, False)))
556 painter.drawPixmap(xm, t, QPixmap().fromImage(self.corner.mirrored(True, False)))
557 painter.drawPixmap(t, ym, QPixmap().fromImage(self.corner.mirrored(False, True)))
558 painter.drawPixmap(xm, ym, QPixmap().fromImage(self.corner.mirrored(True, True)))
559
560 # Mirrored Textures
561 uRect = QImage(26 * 4, 26 * 6, QImage.Format_RGB32)
562 lRect = QImage(26 * 9, 26 * 4, QImage.Format_RGB32)
563
564 uRect.fill(1)
565 lRect.fill(1)
566
567 vp = QPainter()
568 vp.begin(uRect)
569 vp.drawPixmap(0, 0, QPixmap().fromImage(self.vert))
570 vp.drawPixmap(52, 0, QPixmap().fromImage(self.vert.mirrored(True, False)))
571 vp.end()
572
573 vh = QPainter()
574 vh.begin(lRect)
575 vh.drawPixmap(0, 0, QPixmap().fromImage(self.horiz))
576 vh.drawPixmap(0, 52, QPixmap().fromImage(self.horiz.mirrored(False, True)))
577 vh.end()
578
579 painter.drawTiledPixmap(
580 26 * 7 - 52 - 13,
581 -52,
582 26 * (self.roomWidth - 10) + 26,
583 26 * 6,
584 QPixmap().fromImage(uRect)
585 )
586 painter.drawTiledPixmap(
587 -52,
588 26 * 4 - 52 - 13,
589 26 * 9,
590 26 * (self.roomHeight - 4) + 26,
591 QPixmap().fromImage(lRect)
592 )
593 painter.drawTiledPixmap(
594 26 * 7 - 52 - 13,
595 self.roomHeight * 26 - 26 * 4,
596 26 * (self.roomWidth - 10) + 26,
597 26 * 6,
598 QPixmap().fromImage(uRect.mirrored(False, True))
599 )
600 painter.drawTiledPixmap(
601 self.roomWidth * 26 - 26 * 7,
602 26 * 4 - 52 - 13,
603 26 * 9,
604 26 * (self.roomHeight - 4) + 26,
605 QPixmap().fromImage(lRect.mirrored(True, False))
606 )
607
608 if self.roomHeight == 14 and self.roomWidth == 26:
609
610 self.center = self.tile.copy(QRect(26 * 3, 26 * 3, 26 * 6, 26 * 3))
611
612 painter.drawPixmap (26 * 7, 26 * 4, QPixmap().fromImage(self.center.mirrored(False, False)))
613 painter.drawPixmap (26 * 13, 26 * 4, QPixmap().fromImage(self.center.mirrored(True, False)))
614 painter.drawPixmap (26 * 7, 26 * 7, QPixmap().fromImage(self.center.mirrored(False, True)))
615 painter.drawPixmap (26 * 13, 26 * 7, QPixmap().fromImage(self.center.mirrored(True, True)))
616
617 def drawBGSlimRooms(self, painter, rect):
618
619 t = -52
620 yo = 0
621 xo = 0
622
623 # Thin in Height
624 if self.roomShape in [2, 7]:
625 self.roomHeight = 3
626 yo = (2 * 26)
627
628 # Thin in Width
629 if self.roomShape in [3, 5]:
630 self.roomWidth = 5
631 xo = (4 * 26)
632
633 xm = 26 * (self.roomWidth+2) - 26 * 7
634 ym = 26 * (self.roomHeight+2) - 26 * 4
635
636 # Corner Painting
637 painter.drawPixmap(t + xo, t + yo, QPixmap().fromImage(self.corner.mirrored(False, False)))
638 painter.drawPixmap(xm + xo, t + yo, QPixmap().fromImage(self.corner.mirrored(True, False)))
639 painter.drawPixmap(t + xo, ym + yo, QPixmap().fromImage(self.corner.mirrored(False, True)))
640 painter.drawPixmap(xm + xo, ym + yo, QPixmap().fromImage(self.corner.mirrored(True, True)))
641
642 # Mirrored Textures
643 uRect = QImage(26 * 4, 26 * 4, QImage.Format_RGB32)
644 lRect = QImage(26 * 7, 26 * 4, QImage.Format_RGB32)
645
646 uRect.fill(1)
647 lRect.fill(1)
648
649 vp = QPainter()
650 vp.begin(uRect)
651 vp.drawPixmap(0, 0, QPixmap().fromImage(self.vert))
652 vp.drawPixmap(52, 0, QPixmap().fromImage(self.vert.mirrored(True, False)))
653 vp.end()
654
655 vh = QPainter()
656 vh.begin(lRect)
657 vh.drawPixmap(0, 0, QPixmap().fromImage(self.horiz))
658 vh.drawPixmap(0, 52, QPixmap().fromImage(self.horiz.mirrored(False, True)))
659 vh.end()
660
661 painter.drawTiledPixmap(
662 xo + 26 * 7 - 52 - 13,
663 yo + -52,
664 26 * (self.roomWidth - 10) + 26,
665 26 * 4,
666 QPixmap().fromImage(uRect)
667 )
668 painter.drawTiledPixmap(
669 xo + -52,
670 yo + 26 * 4 - 52 -13,
671 26 * 7,
672 26 * (self.roomHeight - 4) + 26,
673 QPixmap().fromImage(lRect)
674 )
675 painter.drawTiledPixmap(
676 xo + 26 * 7 - 52 - 13,
677 yo + self.roomHeight * 26 - 26 * 2,
678 26 * (self.roomWidth - 10) + 26,
679 26 * 4,
680 QPixmap().fromImage(uRect.mirrored(False, True))
681 )
682 painter.drawTiledPixmap(
683 xo + self.roomWidth * 26 - 26 * 5,
684 yo + 26 * 4 - 52 - 13,
685 26 * 7,
686 26 * (self.roomHeight - 4) + 26,
687 QPixmap().fromImage(lRect.mirrored(True, False))
688 )
689
690 if self.roomHeight == 14 and self.roomWidth == 26:
691
692 self.center = self.tile.copy(QRect(26 * 3, 26 * 3, 26 * 6, 26 * 3))
693
694 painter.drawPixmap(xo + 26 * 7, yo + 26 * 4, QPixmap().fromImage(self.center.mirrored(False, False)))
695 painter.drawPixmap(xo + 26 * 13, yo + 26 * 4, QPixmap().fromImage(self.center.mirrored(True, False)))
696 painter.drawPixmap(xo + 26 * 7 , yo + 26 * 7, QPixmap().fromImage(self.center.mirrored(False, True)))
697 painter.drawPixmap(xo + 26 * 13, yo + 26 * 7, QPixmap().fromImage(self.center.mirrored(True, True)))
698
699 def drawBGCornerRooms(self, painter, rect):
700 t = -52
701 xm = 26 * (self.roomWidth + 2) - 26 * 7
702 ym = 26 * (self.roomHeight + 2) - 26 * 4
703
704 # Mirrored Textures
705 uRect = QImage(26 * 4, 26 * 6, QImage.Format_RGB32)
706 lRect = QImage(26 * 9, 26 * 4, QImage.Format_RGB32)
707
708 uRect.fill(1)
709 lRect.fill(1)
710
711 vp = QPainter()
712 vp.begin(uRect)
713 vp.drawPixmap(0, 0, QPixmap().fromImage(self.vert))
714 vp.drawPixmap(52, 0, QPixmap().fromImage(self.vert.mirrored(True, False)))
715 vp.end()
716
717 vh = QPainter()
718 vh.begin(lRect)
719 vh.drawPixmap(0, 0, QPixmap().fromImage(self.horiz))
720 vh.drawPixmap(0, 52, QPixmap().fromImage(self.horiz.mirrored(False, True)))
721 vh.end()
722
723 # Exterior Corner Painting
724 painter.drawPixmap(t, t, QPixmap().fromImage(self.corner.mirrored(False, False)))
725 painter.drawPixmap(xm, t, QPixmap().fromImage(self.corner.mirrored(True, False)))
726 painter.drawPixmap(t, ym, QPixmap().fromImage(self.corner.mirrored(False, True)))
727 painter.drawPixmap(xm, ym, QPixmap().fromImage(self.corner.mirrored(True, True)))
728
729 # Exterior Wall Painting
730 painter.drawTiledPixmap(26 * 7 - 52 - 13, -52, 26 * (self.roomWidth - 10) + 26, 26 * 6, QPixmap().fromImage(uRect))
731 painter.drawTiledPixmap(-52, 26 * 4 - 52 - 13, 26 * 9, 26 * (self.roomHeight - 4) + 26, QPixmap().fromImage(lRect))
732 painter.drawTiledPixmap(26 * 7 - 52 - 13, self.roomHeight * 26 - 26 * 4, 26 * (self.roomWidth - 10) + 26, 26 * 6, QPixmap().fromImage(uRect.mirrored(False, True)))
733 painter.drawTiledPixmap(self.roomWidth * 26 - 26 * 7, 26 * 4 - 52 - 13, 26 * 9, 26 * (self.roomHeight - 4) + 26, QPixmap().fromImage(lRect.mirrored(True, False)))
734
735 # Center Floor Painting
736 self.center = self.tile.copy(QRect(26 * 3, 26 * 3, 26 * 6, 26 * 3))
737
738 painter.drawPixmap(26 * 7, 26 * 4, QPixmap().fromImage(self.center.mirrored(False, False)))
739 painter.drawPixmap(26 * 13, 26 * 4, QPixmap().fromImage(self.center.mirrored(True, False)))
740 painter.drawPixmap(26 * 7, 26 * 7, QPixmap().fromImage(self.center.mirrored(False, True)))
741 painter.drawPixmap(26 * 13, 26 * 7, QPixmap().fromImage(self.center.mirrored(True, True)))
742
743 # Interior Corner Painting (This is the annoying bit)
744 # New midpoints
745 xm = 26 * (self.roomWidth / 2)
746 ym = 26 * (self.roomHeight / 2)
747
748 # New half-lengths/heights
749 xl = 26 * (self.roomWidth + 4) / 2
750 yl = 26 * (self.roomHeight + 4) / 2
751
752
753 if self.roomShape == 9:
754 # Clear the dead area
755 painter.fillRect(t, t, xl, yl, QColor(0, 0, 0, 255))
756
757 # Draw the horizontal wall
758 painter.drawTiledPixmap(xm - 26 * 8, ym + t, 26 * 6, 26 * 6, QPixmap().fromImage(uRect))
759
760 # Draw the vertical wall
761 painter.drawTiledPixmap(xm + t, ym - 26 * 5, 26 * 9, 26 * 3, QPixmap().fromImage(lRect))
762
763 # Draw the three remaining corners
764 painter.drawPixmap(t, ym + t, QPixmap().fromImage(self.corner.mirrored(False, False)))
765 painter.drawPixmap(xm + t, t, QPixmap().fromImage(self.corner.mirrored(False, False)))
766 painter.drawPixmap(xm + t, ym + t, QPixmap().fromImage(self.innerCorner.mirrored(False, False)))
767
768 elif self.roomShape == 10:
769 # Clear the dead area
770 painter.fillRect(xm, t , xl, yl, QColor(0, 0, 0, 255))
771
772 # Draw the horizontal wall
773 painter.drawTiledPixmap(xm - t, ym + t, 26 * 6, 26 * 6, QPixmap().fromImage(uRect))
774
775 # Draw the vertical wall
776 painter.drawTiledPixmap(xm - 26 * 7, ym - 26 * 5, 26 * 9, 26 * 3, QPixmap().fromImage(lRect.mirrored(True, False)))
777
778 # Draw the three remaining corners
779 painter.drawPixmap(26 * (self.roomWidth + 2) - 26 * 7, ym + t, QPixmap().fromImage(self.corner.mirrored(True, False)))
780 painter.drawPixmap(xm - 26 * 5, t, QPixmap().fromImage(self.corner.mirrored(True, False)))
781 painter.drawPixmap(xm, ym + t, QPixmap().fromImage(self.innerCorner.mirrored(True, False)))
782
783 elif self.roomShape == 11:
784 # Clear the dead area
785 painter.fillRect(t, ym, xl, yl, QColor(0, 0, 0, 255))
786
787 # Draw the horizontal wall
788 painter.drawTiledPixmap(xm - 26 * 8, ym + t * 2, 26 * 6, 26 * 6, QPixmap().fromImage(uRect.mirrored(False, True)))
789
790 # Draw the vertical wall
791 painter.drawTiledPixmap(xm + t, ym - t, 26 * 9, 26 * 3, QPixmap().fromImage(lRect))
792
793 # Draw the three remaining corners
794 painter.drawPixmap(t, ym + t, QPixmap().fromImage(self.corner.mirrored(False, True)))
795 painter.drawPixmap(xm + t, ym * 2 + t, QPixmap().fromImage(self.corner.mirrored(False, True)))
796 painter.drawPixmap(xm + t, ym, QPixmap().fromImage(self.innerCorner.mirrored(False, True)))
797
798 elif self.roomShape == 12:
799 # Clear the dead area
800 painter.fillRect(xm, ym, xl, yl, QColor(0, 0, 0, 255))
801
802 # Draw the horizontal wall
803 painter.drawTiledPixmap(xm + 26 * 2, ym + t * 2, 26 * 6, 26 * 6, QPixmap().fromImage(uRect.mirrored(False, True)))
804
805 # Draw the vertical wall
806 painter.drawTiledPixmap(xm - 26 * 7, ym - t, 26 * 9, 26 * 3, QPixmap().fromImage(lRect.mirrored(True, False)))
807
808 # Draw the three remaining corners
809 painter.drawPixmap(xm + 26 * 8, ym + t, QPixmap().fromImage(self.corner.mirrored(True, True)))
810 painter.drawPixmap(xm - 26 * 5, ym + 26 * 5, QPixmap().fromImage(self.corner.mirrored(True, True)))
811 painter.drawPixmap(xm, ym, QPixmap().fromImage(self.innerCorner.mirrored(True, True)))
812
813class RoomEditorWidget(QGraphicsView):
814
815 def __init__(self, scene, parent=None):
816 QGraphicsView.__init__(self, scene, parent)
817
818 self.setViewportUpdateMode(self.FullViewportUpdate)
819 self.setDragMode(self.RubberBandDrag)
820 self.setTransformationAnchor(QGraphicsView.AnchorViewCenter)
821 self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
822 self.newScale = 1.0
823
824 self.assignNewScene(scene)
825
826 self.statusBar = True
827 self.canDelete = True
828
829 def assignNewScene(self, scene):
830 self.setScene(scene)
831 self.centerOn(0, 0)
832
833 self.objectToPaint = None
834 self.lastTile = None
835
836 def tryToPaint(self, event):
837 '''Called when a paint attempt is initiated'''
838
839 paint = self.objectToPaint
840 if paint == None: return
841
842 clicked = self.mapToScene(event.x(), event.y())
843 x, y = clicked.x(), clicked.y()
844
845 x = int(x / 26)
846 y = int(y / 26)
847
848 x = min(max(x, 0), self.scene().roomWidth - 1)
849 y = min(max(y, 0), self.scene().roomHeight - 1)
850
851 # Don't stack multiple grid entities
852 for i in self.scene().items():
853 if isinstance(i, Entity):
854 if i.entity['X'] == x and i.entity['Y'] == y:
855 if i.stackDepth == EntityStack.MAX_STACK_DEPTH:
856 return
857
858 i.hideWeightPopup()
859
860 if int(i.entity['Type']) > 999 and int(self.objectToPaint.ID) > 999:
861 return
862
863 # Make sure we're not spawning oodles
864 if (x, y) in self.lastTile: return
865 self.lastTile.add((x, y))
866
867 en = Entity(x, y, int(paint.ID), int(paint.variant), int(paint.subtype), 1.0)
868
869 self.scene().addItem(en)
870 mainWindow.dirt()
871
872 def mousePressEvent(self, event):
873 if event.button() == Qt.RightButton:
874 if mainWindow.roomList.selectedRoom() is not None:
875 self.lastTile = set()
876 self.tryToPaint(event)
877 event.accept()
878 else:
879 self.lastTile = None
880 # not calling this for right click + adding items to the scene causes crashes
881 QGraphicsView.mousePressEvent(self, event)
882
883 def mouseMoveEvent(self, event):
884 if self.lastTile:
885 if mainWindow.roomList.selectedRoom() is not None:
886 self.tryToPaint(event)
887 event.accept()
888 QGraphicsView.mouseMoveEvent(self, event)
889
890 def mouseReleaseEvent(self, event):
891 self.lastTile = None
892 QGraphicsView.mouseReleaseEvent(self, event)
893
894 def keyPressEvent(self, event):
895 if (event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace) and self.canDelete:
896 scene = self.scene()
897
898 selection = scene.selectedItems()
899 if len(selection) > 0:
900 for obj in selection:
901 obj.setSelected(False)
902 obj.remove()
903 scene.update()
904 self.update()
905 mainWindow.dirt()
906 return
907 else:
908 QGraphicsView.keyPressEvent(self, event)
909
910 def drawBackground(self, painter, rect):
911 painter.fillRect(rect, QColor(0, 0, 0))
912
913 QGraphicsView.drawBackground(self, painter, rect)
914
915 def resizeEvent(self, event):
916 QGraphicsView.resizeEvent(self, event)
917
918 w = self.scene().roomWidth
919 h = self.scene().roomHeight
920
921 xScale = event.size().width() / (w * 26 + 52 * 2)
922 yScale = event.size().height() / (h * 26 + 52 * 2)
923 newScale = min([xScale, yScale])
924
925 tr = QTransform()
926 tr.scale(newScale, newScale)
927 self.newScale = newScale
928
929 self.setTransform(tr)
930
931 if newScale == yScale:
932 self.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
933 else:
934 self.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
935
936 def paintEvent(self, event):
937 # Purely handles the status overlay text
938 QGraphicsView.paintEvent(self, event)
939
940 if not self.statusBar: return
941
942 # Display the room status in a text overlay
943 painter = QPainter()
944 painter.begin(self.viewport())
945
946 painter.setRenderHint(QPainter.Antialiasing, True)
947 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
948 painter.setPen(QPen(Qt.white, 1, Qt.SolidLine))
949
950 room = mainWindow.roomList.selectedRoom()
951 if room:
952 # Room Type Icon
953 q = QPixmap()
954 q.load('resources/UI/RoomIcons.png')
955
956 painter.drawPixmap(2, 3, q.copy(room.roomType * 16, 0, 16, 16))
957
958 # Top Text
959 font = painter.font()
960 font.setPixelSize(13)
961 painter.setFont(font)
962 painter.drawText(20, 16, "{0} - {1}".format(room.roomVariant, room.data(0x100)) )
963
964 # Bottom Text
965 font = painter.font()
966 font.setPixelSize(10)
967 painter.setFont(font)
968 painter.drawText(8, 30, "Difficulty: {1}, Weight: {2}, Subvariant: {0}".format(room.roomSubvariant, room.roomDifficulty, room.roomWeight))
969
970 # Display the currently selected entity in a text overlay
971 selectedEntities = self.scene().selectedItems()
972
973 if len(selectedEntities) == 1:
974 e = selectedEntities[0]
975 r = event.rect()
976
977 # Entity Icon
978 i = QIcon()
979 painter.drawPixmap(QRect(r.right() - 32, 2, 32, 32), e.entity["pixmap"])
980
981 # Top Text
982 font = painter.font()
983 font.setPixelSize(13)
984 painter.setFont(font)
985 painter.drawText(r.right() - 34 - 200, 2, 200, 16, Qt.AlignRight | Qt.AlignBottom, "{1}.{2}.{3} - {0}".format(e.entity["name"], e.entity["Type"], e.entity["Variant"], e.entity["Subtype"]) )
986
987 # Bottom Text
988 font = painter.font()
989 font.setPixelSize(10)
990 painter.setFont(font)
991 painter.drawText(r.right() - 34 - 200, 20, 200, 12, Qt.AlignRight | Qt.AlignBottom, "Base HP : {0}".format(e.entity["baseHP"]) )
992
993 elif len(selectedEntities) > 1:
994 e = selectedEntities[0]
995 r = event.rect()
996
997 # Case Two: more than one type of entity
998 # Entity Icon
999 i = QIcon()
1000 painter.drawPixmap(QRect(r.right() - 32, 2, 32, 32), e.entity["pixmap"])
1001
1002 # Top Text
1003 font = painter.font()
1004 font.setPixelSize(13)
1005 painter.setFont(font)
1006 painter.drawText(r.right() - 34 - 200, 2, 200, 16, Qt.AlignRight | Qt.AlignBottom, "{0} Entities Selected".format(len(selectedEntities)) )
1007
1008 # Bottom Text
1009 font = painter.font()
1010 font.setPixelSize(10)
1011 painter.setFont(font)
1012 painter.drawText(r.right() - 34 - 200, 20, 200, 12, Qt.AlignRight | Qt.AlignBottom, ", ".join(set([x.entity['name'] for x in selectedEntities])) )
1013
1014 pass
1015
1016 painter.end()
1017
1018 def drawForeground(self, painter, rect):
1019 QGraphicsView.drawForeground(self, painter, rect)
1020
1021 painter.setRenderHint(QPainter.Antialiasing, True)
1022 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
1023
1024 # Display the number of entities on a given tile, in bitFont or regular font
1025 tiles = [[0 for y in range(26)] for x in range(14)]
1026 for e in self.scene().items():
1027 if isinstance(e, Entity):
1028 tiles[e.entity['Y']][e.entity['X']] += 1
1029
1030 if not self.scene().bitText:
1031 painter.setPen(Qt.white)
1032 painter.font().setPixelSize(5)
1033
1034 for y, row in enumerate(tiles):
1035 yc = (y + 1) * 26 - 12
1036
1037 for x, count in enumerate(row):
1038 if count <= 1: continue
1039
1040 if self.scene().bitText:
1041 xc = (x + 1) * 26 - 12
1042
1043 digits = [ int(i) for i in str(count) ]
1044
1045 fontrow = count == EntityStack.MAX_STACK_DEPTH and 1 or 0
1046
1047 numDigits = len(digits) - 1
1048 for i, digit in enumerate(digits):
1049 painter.drawPixmap( xc - 12 * (numDigits - i), yc, self.scene().bitfont[digit + fontrow * 10] )
1050 else:
1051 if count == EntityStack.MAX_STACK_DEPTH: painter.setPen(Qt.red)
1052
1053 painter.drawText( x * 26, y * 26, 26, 26, Qt.AlignBottom | Qt.AlignRight, str(count) )
1054
1055 if count == EntityStack.MAX_STACK_DEPTH: painter.setPen(Qt.white)
1056
1057class Entity(QGraphicsItem):
1058 SNAP_TO = 26
1059
1060 def __init__(self, x, y, mytype, variant, subtype, weight):
1061 QGraphicsItem.__init__(self)
1062 self.setFlags(
1063 self.ItemSendsGeometryChanges |
1064 self.ItemIsSelectable |
1065 self.ItemIsMovable
1066 )
1067
1068 self.stackDepth = 1
1069 self.popup = None
1070 mainWindow.scene.selectionChanged.connect(self.hideWeightPopup)
1071
1072 # Supplied entity info
1073 self.entity = {}
1074 self.entity['X'] = x
1075 self.entity['Y'] = y
1076 self.entity['Type'] = mytype
1077 self.entity['Variant'] = variant
1078 self.entity['Subtype'] = subtype
1079 self.entity['Weight'] = weight
1080
1081 # Derived Entity Info
1082 self.entity['name'] = None
1083 self.isGridEnt = False
1084 self.entity['baseHP'] = None
1085 self.entity['boss'] = None
1086 self.entity['champion'] = None
1087 self.entity['pixmap'] = None
1088 self.entity['known'] = False
1089 self.entity['PlaceVisual'] = None
1090
1091 self.getEntityInfo(mytype, subtype, variant)
1092
1093 self.updatePosition()
1094 if self.entity['Type'] < 999:
1095 self.setZValue(1)
1096 else:
1097 self.setZValue(0)
1098
1099 if not hasattr(Entity, 'SELECTION_PEN'):
1100 Entity.SELECTION_PEN = QPen(Qt.green, 1, Qt.DashLine)
1101 Entity.OFFSET_SELECTION_PEN = QPen(Qt.red, 1, Qt.DashLine)
1102 Entity.OUT_OF_RANGE_WARNING_IMG = QPixmap('resources/UI/warning.png')
1103
1104 self.setAcceptHoverEvents(True)
1105
1106 def getEntityInfo(self, t, subtype, variant):
1107
1108 # Try catch so I can not crash BR so often
1109 try:
1110 global entityXML
1111 en = entityXML.find("entity[@ID='{0}'][@Subtype='{1}'][@Variant='{2}']".format(t, subtype, variant))
1112
1113 self.entity['name'] = en.get('Name')
1114 self.isGridEnt = en.get('Kind') == 'Stage' and \
1115 en.get('Group') in [ 'Grid', 'Poop', 'Fireplaces', 'Other', 'Props', 'Special Exits', 'Broken' ]
1116
1117 self.entity['baseHP'] = en.get('BaseHP')
1118 self.entity['boss'] = en.get('Boss')
1119 self.entity['champion'] = en.get('Champion')
1120 self.entity['PlaceVisual'] = en.get('PlaceVisual')
1121
1122 if t == 5 and variant == 100:
1123 i = QImage()
1124 i.load('resources/Entities/5.100.0 - Collectible.png')
1125 i = i.convertToFormat(QImage.Format_ARGB32)
1126
1127 d = QImage()
1128 d.load(en.get('Image'))
1129
1130 p = QPainter(i)
1131 p.drawImage(0, 0, d)
1132 p.end()
1133
1134 self.entity['pixmap'] = QPixmap.fromImage(i)
1135
1136 else:
1137 self.entity['pixmap'] = QPixmap()
1138 self.entity['pixmap'].load(en.get('Image'))
1139
1140 def checkNum(s):
1141 try:
1142 float(s)
1143 return True
1144 except ValueError:
1145 return False
1146
1147 if self.entity['PlaceVisual']:
1148 parts = list(map(lambda x: x.strip(), self.entity['PlaceVisual'].split(',')))
1149 if len(parts) == 2 and checkNum(parts[0]) and checkNum(parts[1]):
1150 self.entity['PlaceVisual'] = (float(parts[0]), float(parts[1]))
1151 else:
1152 self.entity['PlaceVisual'] = parts[0]
1153
1154 self.entity['known'] = True
1155
1156 except:
1157 print ("Entity {0}, Subtype {1}, Variant {2} expected, but was not found".format(t, subtype, variant))
1158 self.entity['pixmap'] = QPixmap()
1159 self.entity['pixmap'].load("resources/Entities/questionmark.png")
1160
1161 self.updateTooltip()
1162
1163 def updateTooltip(self):
1164 tooltipStr = "{name} @ {X} x {Y} - {Type}.{Variant}.{Subtype}; HP: {baseHP}".format(**self.entity)
1165
1166 if self.entity['Type'] >= 1000 and not self.isGridEnt:
1167 tooltipStr += '\nType is outside the valid range of 0 - 999! This will not load properly in-game!'
1168 if self.entity['Variant'] >= 4096:
1169 tooltipStr += '\nVariant is outside the valid range of 0 - 4095!'
1170 if self.entity['Subtype'] >= 255:
1171 tooltipStr += '\nSubtype is outside the valid range of 0 - 255!'
1172
1173 self.setToolTip(tooltipStr)
1174
1175 def itemChange(self, change, value):
1176
1177 if change == self.ItemPositionChange:
1178
1179 currentX, currentY = self.x(), self.y()
1180
1181 x, y = value.x(), value.y()
1182
1183 # Debug code
1184 # if 'eep' in self.entity['name']:
1185 # print (self.entity['X'], self.entity['Y'], x, y)
1186
1187 try:
1188 w = self.scene().initialRoomWidth
1189 h = self.scene().initialRoomHeight
1190 except:
1191 w = 26
1192 h = 14
1193
1194 x = int((x + (self.SNAP_TO / 2)) / self.SNAP_TO) * self.SNAP_TO
1195 y = int((y + (self.SNAP_TO / 2)) / self.SNAP_TO) * self.SNAP_TO
1196
1197 if x < 0: x = 0
1198 if x >= (self.SNAP_TO * (w - 1)): x = (self.SNAP_TO * (w - 1))
1199 if y < 0: y = 0
1200 if y >= (self.SNAP_TO * (h - 1)): y = (self.SNAP_TO * (h - 1))
1201
1202 if x != currentX or y != currentY:
1203 self.entity['X'] = int(x / self.SNAP_TO)
1204 self.entity['Y'] = int(y / self.SNAP_TO)
1205
1206 self.updateTooltip()
1207 if self.isSelected():
1208 mainWindow.dirt()
1209
1210 value.setX(x)
1211 value.setY(y)
1212
1213 self.getStack()
1214 if self.popup: self.popup.update(self.stack)
1215
1216 # Debug code
1217 # if 'eep' in self.entity['name']:
1218 # print (self.entity['X'], self.entity['Y'], x, y)
1219
1220 return value
1221
1222 return QGraphicsItem.itemChange(self, change, value)
1223
1224 def boundingRect(self):
1225
1226 #if self.entity['pixmap']:
1227 # return QRectF(self.entity['pixmap'].rect())
1228 #else:
1229 return QRectF(0.0, 0.0, 26.0, 26.0)
1230
1231 def updatePosition(self):
1232 self.setPos(self.entity['X'] * 26, self.entity['Y'] * 26)
1233
1234 def paint(self, painter, option, widget):
1235
1236 painter.setRenderHint(QPainter.Antialiasing, True)
1237 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
1238
1239 painter.setBrush(Qt.Dense5Pattern)
1240 painter.setPen(QPen(Qt.white))
1241
1242 if self.entity['pixmap']:
1243 w, h = self.entity['pixmap'].width(), self.entity['pixmap'].height()
1244 xc, yc = 0, 0
1245
1246 typ, var, sub = self.entity['Type'], self.entity['Variant'], self.entity['Subtype']
1247
1248 def WallSnap():
1249 rw = self.scene().roomWidth
1250 rh = self.scene().roomHeight
1251 ex = self.entity['X']
1252 ey = self.entity['Y']
1253
1254 shape = self.scene().roomShape
1255 if shape == 9:
1256 if ex < 13:
1257 ey -= 7
1258 rh = 7
1259 elif ey < 7:
1260 ex -= 13
1261 rw = 13
1262 elif shape == 10:
1263 if ex >= 13:
1264 ey -= 7
1265 rh = 7
1266 elif ey < 7:
1267 rw = 13
1268 elif shape == 11:
1269 if ex < 13:
1270 rh = 7
1271 elif ey >= 7:
1272 ex -= 13
1273 rw = 13
1274 elif shape == 12:
1275 if ex > 13:
1276 rh = 7
1277 elif ey >= 7:
1278 rw = 13
1279
1280 distances = [rw - ex - 1, ex, ey, rh - ey - 1]
1281 closest = min(distances)
1282 direction = distances.index(closest)
1283
1284 wx, wy = 0, 0
1285 if direction == 0: # Right
1286 wx, wy = 2 * closest + 1, 0
1287
1288 elif direction == 1: # Left
1289 wx, wy = -2 * closest - 1, 0
1290
1291 elif direction == 2: # Top
1292 wx, wy = 0, -closest - 1
1293
1294 elif direction == 3: # Bottom
1295 wx, wy = 0, closest
1296
1297 return wx, wy
1298
1299 customPlaceVisuals = {
1300 'WallSnap': WallSnap
1301 }
1302
1303 recenter = self.entity.get('PlaceVisual', None)
1304 if recenter:
1305 if isinstance(recenter, str):
1306 recenter = customPlaceVisuals.get(recenter, None)
1307 if recenter:
1308 xc, yc = recenter()
1309 else:
1310 xc, yc = recenter
1311
1312 xc += 1
1313 yc += 1
1314 x = (xc * 26 - w) / 2
1315 y = (yc * 26 - h)
1316
1317 def drawGridBorders():
1318 painter.drawLine(0, 0, 0, 4)
1319 painter.drawLine(0, 0, 4, 0)
1320
1321 painter.drawLine(26, 0, 26, 4)
1322 painter.drawLine(26, 0, 22, 0)
1323
1324 painter.drawLine(0, 26, 4, 26)
1325 painter.drawLine(0, 26, 0, 22)
1326
1327 painter.drawLine(26, 26, 22, 26)
1328 painter.drawLine(26, 26, 26, 22)
1329
1330 # Curse room special case
1331 if typ == 5 and var == 50 and mainWindow.roomList.selectedRoom().roomType == 10:
1332 self.entity['pixmap'] = QPixmap('resources/Entities/5.360.0 - Red Chest.png')
1333
1334 painter.drawPixmap(x, y, self.entity['pixmap'])
1335
1336 # if the offset is high enough, draw an indicator of the actual position
1337 if abs(1 - yc) > 0.5 or abs(1 - xc) > 0.5:
1338 painter.setPen(self.OFFSET_SELECTION_PEN)
1339 painter.setBrush(Qt.NoBrush)
1340 painter.drawLine(13, 13, x + w / 2, y + h - 13)
1341 drawGridBorders()
1342 painter.fillRect(x + w / 2 - 3, y + h - 13 - 3, 6, 6, Qt.red)
1343
1344 if self.isSelected():
1345 painter.setPen(self.SELECTION_PEN)
1346 painter.setBrush(Qt.NoBrush)
1347 painter.drawRect(x, y, w, h)
1348
1349 # Grid space boundary
1350 painter.setPen(Qt.green)
1351 drawGridBorders()
1352
1353 if not self.entity['known']:
1354 painter.setFont(QFont("Arial", 6))
1355
1356 painter.drawText(2, 26, "%d.%d.%d" % (typ, var, sub))
1357
1358 # entities have 12 bits for type and variant, 8 for subtype
1359 # common mod error is to make them outside that range
1360 if var >= 4096 or sub >= 256 or (typ >= 1000 and not self.isGridEnt):
1361 painter.drawPixmap(18, -8, Entity.OUT_OF_RANGE_WARNING_IMG)
1362
1363 def remove(self):
1364 if self.popup:
1365 self.popup.remove()
1366 self.scene().views()[0].canDelete = True
1367 self.scene().removeItem(self)
1368
1369 def mouseReleaseEvent(self, event):
1370 self.hideWeightPopup()
1371 QGraphicsItem.mouseReleaseEvent(self, event)
1372
1373 def hoverEnterEvent(self, event):
1374 self.createWeightPopup()
1375
1376 def hoverLeaveEvent(self, event):
1377 self.hideWeightPopup()
1378
1379 def getStack(self):
1380 # Get the stack
1381 stack = self.collidingItems(Qt.IntersectsItemBoundingRect)
1382 stack.append(self)
1383
1384 # Make sure there are no doors or popups involved
1385 self.stack = [x for x in stack if isinstance(x,Entity)]
1386
1387 # 1 is not a stack.
1388 self.stackDepth = len(self.stack)
1389
1390 def createWeightPopup(self):
1391 self.getStack()
1392 if self.stackDepth <= 1 or any(x.popup and x != self and x.popup.isVisible() for x in self.stack):
1393 self.hideWeightPopup()
1394 return
1395
1396 # If there's no popup, make a popup
1397 if self.popup:
1398 if self.popup.activeSpinners != self.stackDepth:
1399 self.popup.update(self.stack)
1400 self.popup.setVisible(True)
1401 return
1402
1403 self.scene().views()[0].canDelete = False
1404 self.popup = EntityStack(self.stack)
1405 self.scene().addItem(self.popup)
1406
1407 def hideWeightPopup(self):
1408 if self.popup and self not in mainWindow.scene.selectedItems():
1409 self.popup.setVisible(False)
1410 if self.scene(): self.scene().views()[0].canDelete = True
1411
1412class EntityStack(QGraphicsItem):
1413 MAX_STACK_DEPTH = 25
1414
1415 class WeightSpinner(QDoubleSpinBox):
1416 def __init__(self):
1417 QDoubleSpinBox.__init__(self)
1418
1419 self.setRange(0.0, 100.0)
1420 self.setDecimals(2)
1421 self.setSingleStep(0.1)
1422 self.setFrame(False)
1423 self.setAlignment(Qt.AlignHCenter)
1424
1425 self.setFont(QFont("Arial", 10))
1426
1427 palette = self.palette()
1428 palette.setColor(QPalette.Base, Qt.transparent)
1429 palette.setColor(QPalette.Text, Qt.white)
1430 palette.setColor(QPalette.Window, Qt.transparent)
1431
1432 self.setPalette(palette)
1433 self.setButtonSymbols(QAbstractSpinBox.NoButtons)
1434
1435 class Proxy(QGraphicsProxyWidget):
1436 def __init__(self, button, parent):
1437 QGraphicsProxyWidget.__init__(self, parent)
1438
1439 self.setWidget(button)
1440
1441 def __init__(self, items):
1442 QGraphicsItem.__init__(self)
1443 self.setZValue(1000)
1444
1445 self.spinners = []
1446 self.activeSpinners = 0
1447 self.update(items)
1448
1449 def update(self, items):
1450 activeSpinners = len(items)
1451
1452 for i in range(activeSpinners - len(self.spinners)):
1453 weight = self.WeightSpinner()
1454 weight.valueChanged.connect(lambda: self.weightChanged(i))
1455 self.spinners.append(self.Proxy(weight, self))
1456
1457 for i in range(activeSpinners, len(self.spinners)):
1458 self.spinners[i].setVisible(False)
1459
1460 if activeSpinners > 1:
1461 for i, item in enumerate(items):
1462 spinner = self.spinners[i]
1463 spinner.widget().setValue(item.entity["Weight"])
1464 spinner.setVisible(True)
1465 else:
1466 self.setVisible(False)
1467
1468 # it's very important that this happens AFTER setting up the spinners
1469 # it greatly increases the odds of races with weightChanged if items are updated first
1470 self.items = items
1471 self.activeSpinners = activeSpinners
1472
1473 def weightChanged(self, idx):
1474 if idx < self.activeSpinners:
1475 self.items[idx].entity['Weight'] = self.spinners[idx].widget().value()
1476
1477 def paint(self, painter, option, widget):
1478 painter.setRenderHint(QPainter.Antialiasing, True)
1479 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
1480
1481 brush = QBrush(QColor(0,0,0,80))
1482 painter.setPen(QPen(Qt.transparent))
1483 painter.setBrush(brush)
1484
1485 r = self.boundingRect().adjusted(0,0,0,-16)
1486
1487 path = QPainterPath()
1488 path.addRoundedRect(r, 4, 4)
1489 path.moveTo(r.center().x()-6, r.bottom())
1490 path.lineTo(r.center().x()+6, r.bottom())
1491 path.lineTo(r.center().x(), r.bottom()+12)
1492 painter.drawPath(path)
1493
1494 painter.setPen(QPen(Qt.white))
1495 painter.setFont(QFont("Arial", 8))
1496
1497 w = 0
1498 for i, item in enumerate(self.items):
1499 pix = item.entity['pixmap']
1500 self.spinners[i].setPos(w-8, r.bottom()-26)
1501 w += 4
1502 painter.drawPixmap(w, r.bottom()-20-pix.height(), pix)
1503
1504 # painter.drawText(w, r.bottom()-16, pix.width(), 8, Qt.AlignCenter, "{:.1f}".format(item.entity['Weight']))
1505 w += pix.width()
1506
1507 def boundingRect(self):
1508 width = 0
1509 height = 0
1510
1511 # Calculate the combined size
1512 for item in self.items:
1513 dx, dy = 26, 26
1514 pix = item.entity['pixmap']
1515 if pix:
1516 dx, dy = pix.rect().width(), pix.rect().height()
1517 width = width + dx
1518 height = max(height, dy)
1519
1520 # Add in buffers
1521 height = height + 8 + 8 + 8 + 16 # Top, bottom, weight text, and arrow
1522 width = width + 4 + len(self.items)*4 # Left and right and the middle bits
1523
1524 self.setX(self.items[-1].x() - width/2 + 13)
1525 self.setY(self.items[-1].y() - height)
1526
1527 return QRectF(0.0, 0.0, width, height)
1528
1529 def remove(self):
1530 # Fix for the nullptr left by the scene parent of the widget, avoids a segfault from the dangling pointer
1531 for spin in self.spinners:
1532 self.scene().removeItem(spin)
1533 # spin.widget().setParent(None)
1534 spin.setWidget(None) # Turns out this function calls the above commented out function
1535 del self.spinners
1536
1537 self.scene().removeItem(self)
1538
1539class Door(QGraphicsItem):
1540
1541 def __init__(self, doorItem):
1542 QGraphicsItem.__init__(self)
1543
1544 # Supplied entity info
1545 self.doorItem = doorItem
1546 self.exists = doorItem[2]
1547
1548 self.setPos(self.doorItem[0] * 26 - 13, self.doorItem[1] * 26 - 13)
1549
1550 tr = QTransform()
1551 if doorItem[0] in [-1, 12]:
1552 tr.rotate(270)
1553 self.moveBy(-13, 0)
1554 elif doorItem[0] in [13, 26]:
1555 tr.rotate(90)
1556 self.moveBy(13, 0)
1557 elif doorItem[1] in [7, 14]:
1558 tr.rotate(180)
1559 self.moveBy(0, 13)
1560 else:
1561 self.moveBy(0, -13)
1562
1563 self.image = QImage('resources/Backgrounds/Door.png').transformed(tr)
1564 self.disabledImage = QImage('resources/Backgrounds/DisabledDoor.png').transformed(tr)
1565
1566 def paint(self, painter, option, widget):
1567
1568 painter.setRenderHint(QPainter.Antialiasing, True)
1569 painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
1570
1571 if self.exists:
1572 painter.drawImage(0, 0, self.image)
1573 else:
1574 painter.drawImage(0, 0, self.disabledImage)
1575
1576 def boundingRect(self):
1577 return QRectF(0.0, 0.0, 64.0, 52.0)
1578
1579 def mouseDoubleClickEvent(self, event):
1580
1581 if self.exists:
1582 self.exists = False
1583 else:
1584 self.exists = True
1585
1586 self.doorItem[2] = self.exists
1587
1588 event.accept()
1589 self.update()
1590 mainWindow.dirt()
1591
1592 def remove(self):
1593 self.scene().removeItem(self)
1594
1595
1596
1597########################
1598# Dock Widgets #
1599########################
1600
1601# Room Selector
1602########################
1603
1604class Room(QListWidgetItem):
1605
1606 def __init__(self, name="New Room", doors=[], spawns=[], mytype=1, variant=0, subvariant=0, difficulty=1, weight=1.0, width=13, height=7, shape=1):
1607 """Initializes the room item."""
1608
1609 QListWidgetItem.__init__(self)
1610
1611 self.setData(0x100, name)
1612 self.setText("{0} - {1}".format(variant, self.data(0x100)))
1613
1614 self.roomSpawns = spawns
1615 self.roomDoors = doors
1616 self.roomType = mytype
1617 self.roomVariant = variant
1618 self.roomSubvariant = subvariant # 0-64, usually 0 except for special rooms
1619 self.setDifficulty(difficulty)
1620 self.roomWeight = weight
1621 self.roomWidth = width
1622 self.roomHeight = height
1623 self.roomShape = shape # w x h -> 1 = 1x1, 2 = 1x0.5, 3 = 0.5x1, 4 = 2x1, 5 = 2x0.5, 6 = 1x2, 7 = 0.5x2, 8 = 2x2, 9 = corner?, 10 = corner?, 11 = corner?, 12 = corner?
1624
1625 self.roomBG = 1
1626 self.setRoomBG()
1627
1628 self.setFlags(self.flags() | Qt.ItemIsEditable)
1629 self.setToolTip()
1630
1631 if doors == []: self.makeNewDoors()
1632 self.renderDisplayIcon()
1633
1634 def setDifficulty(self, d):
1635 self.roomDifficulty = d
1636 self.setForeground(QColor.fromHsvF(1, 1, min(max(d / 15, 0), 1), 1))
1637
1638 def makeNewDoors(self):
1639 self.roomDoors = []
1640
1641 ########## SHAPE DEFINITIONS
1642 # w x h
1643 # 1 = 1x1, 2 = 1x0.5, 3 = 0.5x1, 4 = 2x1, 5 = 2x0.5, 6 = 1x2, 7 = 0.5x2, 8 = 2x2
1644 # 9 = DR corner, 10 = DL corner, 11 = UR corner, 12 = UL corner
1645
1646 if self.roomShape == 1:
1647 self.roomDoors = [[6, -1, True], [-1, 3, True], [13, 3, True], [6, 7, True]]
1648
1649 elif self.roomShape == 2:
1650 self.roomDoors = [[-1, 3, True], [13, 3, True]]
1651
1652 elif self.roomShape == 3:
1653 self.roomDoors = [[6, -1, True], [6, 7, True]]
1654
1655 elif self.roomShape == 4:
1656 self.roomDoors = [[6, -1, True], [13, 3, True], [-1, 3, True], [13, 10, True], [-1, 10, True], [6, 14, True]]
1657
1658 elif self.roomShape == 5:
1659 self.roomDoors = [[6, -1, True], [6, 14, True]]
1660
1661 elif self.roomShape == 6:
1662 self.roomDoors = [[6, -1, True], [-1, 3, True], [6, 7, True], [19, 7, True], [26, 3, True], [19, -1, True]]
1663
1664 elif self.roomShape == 7:
1665 self.roomDoors = [[-1, 3, True], [26, 3, True]]
1666
1667 elif self.roomShape == 8:
1668 self.roomDoors = [[6, -1, True], [-1, 3, True], [-1, 10, True], [19, -1, True], [6, 14, True], [19, 14, True], [26, 3, True], [26, 10, True]]
1669
1670 elif self.roomShape == 9:
1671 self.roomDoors = [[19, -1, True], [26, 3, True], [6, 14, True], [19, 14, True], [12, 3, True], [-1, 10, True], [26, 10, True], [6, 6, True]]
1672
1673 elif self.roomShape == 10:
1674 self.roomDoors = [[-1, 3, True], [13, 3, True], [6, -1, True], [19, 6, True], [6, 14, True], [19, 14, True], [-1, 10, True], [26, 10, True]]
1675
1676 elif self.roomShape == 11:
1677 self.roomDoors = [[-1, 3, True], [6, 7, True], [6, -1, True], [12, 10, True], [19, -1, True], [26, 3, True], [19, 14, True], [26, 10, True]]
1678
1679 elif self.roomShape == 12:
1680 self.roomDoors = [[-1, 3, True], [6, -1, True], [19, -1, True], [13, 10, True], [26, 3, True], [6, 14, True], [-1, 10, True], [19, 7, True]]
1681
1682 def clearDoors(self):
1683 mainWindow.scene.clearDoors()
1684 for door in self.roomDoors:
1685 d = Door(door)
1686 mainWindow.scene.addItem(d)
1687
1688 def getSpawnCount(self):
1689 ret = 0
1690
1691 for x in self.roomSpawns:
1692 for y in x:
1693 if len(y) is not 0:
1694 ret += 1
1695
1696 return ret
1697
1698 def setToolTip(self):
1699 tip = "{4}x{5} - Type: {0}, Variant: {1}, Difficulty: {2}, Weight: {3}, Shape: {6}".format(self.roomType, self.roomVariant, self.roomDifficulty, self.roomWeight, self.roomWidth, self.roomHeight, self.roomShape)
1700 QListWidgetItem.setToolTip(self, tip)
1701
1702 def renderDisplayIcon(self):
1703 """Renders the mini-icon for display."""
1704
1705 q = QImage()
1706 q.load('resources/UI/RoomIcons.png')
1707
1708 i = QIcon(QPixmap.fromImage(q.copy(self.roomType * 16, 0, 16, 16)))
1709
1710 self.setIcon(i)
1711
1712 def setRoomBG(self):
1713 roomType = ['basement', 'cellar',
1714 'caves','catacombs',
1715 'depths', 'necropolis',
1716 'womb', 'utero',
1717 'sheol', 'cathedral',
1718 'chest', 'dark room',
1719 'burning basement', 'flooded caves',
1720 'dank depths', 'scarred womb',
1721 'blue womb']
1722
1723 self.roomBG = 1
1724
1725 for i in range(len(roomType)):
1726 if roomType[i] in mainWindow.path:
1727 self.roomBG = i + 1
1728
1729 c = self.roomType
1730
1731 if c == 12:
1732 self.roomBG = 18
1733 elif c == 2:
1734 self.roomBG = 19
1735 elif c == 18:
1736 self.roomBG = 20
1737 elif c == 19:
1738 self.roomBG = 21
1739 elif c == 9:
1740 self.roomBG = 22
1741 elif c == 21:
1742 self.roomBG = 23
1743 elif c == 7:
1744 self.roomBG = 24
1745
1746 elif c in [10, 11, 13, 14, 17, 22]:
1747 self.roomBG = 9
1748 elif c in [15]:
1749 self.roomBG = 10
1750 elif c in [20]:
1751 self.roomBG = 11
1752 elif c in [3, 16]:
1753 self.roomBG = 12
1754
1755 elif c in [8]:
1756 if self.roomVariant in [0, 11, 15]:
1757 self.roomBG = 7
1758 elif self.roomVariant in [1, 12, 16]:
1759 self.roomBG = 10
1760 elif self.roomVariant in [2, 13, 17]:
1761 self.roomBG = 9
1762 elif self.roomVariant in [3]:
1763 self.roomBG = 4
1764 elif self.roomVariant in [4]:
1765 self.roomBG = 2
1766 elif self.roomVariant in [5, 19]:
1767 self.roomBG = 1
1768 elif self.roomVariant in [6]:
1769 self.roomBG = 18
1770 elif self.roomVariant in [7]:
1771 self.roomBG = 12
1772 elif self.roomVariant in [8]:
1773 self.roomBG = 13
1774 elif self.roomVariant in [9]:
1775 self.roomBG = 14
1776 elif self.roomVariant in [14, 18]:
1777 self.roomBG = 19
1778 else:
1779 self.roomBG = 12
1780 # grave rooms
1781 elif c == 1 and self.roomVariant > 2 and 'special rooms' in mainWindow.path:
1782 self.roomBG = 12
1783
1784 def mirrorX(self):
1785 # Flip Spawns
1786 for column in self.roomSpawns:
1787 column[:self.roomWidth] = column[:self.roomWidth][::-1]
1788
1789 # Flip Directional Entities
1790 for row in column:
1791 for spawn in row:
1792 # 40 - Guts (1,3)
1793 if spawn[0] == 40:
1794 if spawn[2] == 1:
1795 spawn[2] = 3
1796 elif spawn[2] == 3:
1797 spawn[2] = 1
1798
1799 # 202 - Stone Shooter (0,2)
1800 elif spawn[0] == 202:
1801 if spawn[2] == 0:
1802 spawn[2] = 2
1803 elif spawn[2] == 2:
1804 spawn[2] = 0
1805
1806 # 203 - Brim Head (0,2)
1807 elif spawn[0] == 203:
1808 if spawn[2] == 0:
1809 spawn[2] = 2
1810 elif spawn[2] == 2:
1811 spawn[2] = 0
1812
1813 # 218 - Wall Hugger (1,3)
1814 elif spawn[0] == 218:
1815 if spawn[2] == 1:
1816 spawn[2] = 3
1817 elif spawn[2] == 3:
1818 spawn[2] = 1
1819
1820 # To flip, just reverse the signs then offset by room width (-1 for the indexing)
1821 # Flip Doors
1822 for door in self.roomDoors:
1823 door[0] = -door[0] + (self.roomWidth-1)
1824
1825 # Flip Shape
1826 if self.roomShape is 9:
1827 self.roomShape = 10
1828 elif self.roomShape is 10:
1829 self.roomShape = 9
1830 elif self.roomShape is 11:
1831 self.roomShape = 12
1832 elif self.roomShape is 12:
1833 self.roomShape = 11
1834
1835 def mirrorY(self):
1836 # To flip, just reverse the signs then offset by room width (-1 for the indexing)
1837
1838 # Flip Spawns
1839 self.roomSpawns[:self.roomHeight] = self.roomSpawns[:self.roomHeight][::-1]
1840
1841 # Flip Directional Entities
1842 for column in self.roomSpawns:
1843 for row in column:
1844 for spawn in row:
1845 # 40 - Guts (0,2)
1846 if spawn[0] == 40:
1847 if spawn[2] == 0:
1848 spawn[2] = 2
1849 elif spawn[2] == 2:
1850 spawn[2] = 0
1851
1852 # 202 - Stone Shooter (1,3)
1853 elif spawn[0] == 202:
1854 if spawn[2] == 1:
1855 spawn[2] = 3
1856 elif spawn[2] == 3:
1857 spawn[2] = 1
1858
1859 # 203 - Brim Head (1,3)
1860 elif spawn[0] == 203:
1861 if spawn[2] == 1:
1862 spawn[2] = 3
1863 elif spawn[2] == 3:
1864 spawn[2] = 1
1865
1866 # 218 - Wall Hugger (2,4)
1867 elif spawn[0] == 218:
1868 if spawn[2] == 2:
1869 spawn[2] = 4
1870 elif spawn[2] == 4:
1871 spawn[2] = 2
1872
1873 # Flip Doors
1874 for door in self.roomDoors:
1875 door[1] = -door[1] + (self.roomHeight-1)
1876
1877 # Flip Shape
1878 if self.roomShape is 9:
1879 self.roomShape = 11
1880 elif self.roomShape is 11:
1881 self.roomShape = 9
1882 elif self.roomShape is 10:
1883 self.roomShape = 12
1884 elif self.roomShape is 12:
1885 self.roomShape = 10
1886
1887class RoomDelegate(QStyledItemDelegate):
1888
1889 def __init__(self):
1890
1891 self.pixmap = QPixmap('resources/UI/CurrentRoom.png')
1892 QStyledItemDelegate.__init__(self)
1893
1894 def paint(self, painter, option, index):
1895
1896 painter.fillRect(option.rect.right() - 19, option.rect.top(), 17, 16, QBrush(Qt.white))
1897
1898 QStyledItemDelegate.paint(self, painter, option, index)
1899
1900 item = mainWindow.roomList.list.item(index.row())
1901 if item:
1902 if item.data(100):
1903 painter.drawPixmap(option.rect.right() - 19, option.rect.top(), self.pixmap)
1904
1905class FilterMenu(QMenu):
1906
1907 def __init__(self):
1908
1909 QMenu.__init__(self)
1910
1911 def paintEvent(self, event):
1912
1913 QMenu.paintEvent(self, event)
1914
1915 painter = QPainter(self)
1916
1917 for act in self.actions():
1918 rect = self.actionGeometry(act)
1919 painter.fillRect(rect.right() / 2 - 12, rect.top() - 2, 24, 24, QBrush(Qt.transparent))
1920 painter.drawPixmap(rect.right() / 2 - 12, rect.top() - 2, act.icon().pixmap(24, 24))
1921
1922class RoomSelector(QWidget):
1923
1924 def __init__(self):
1925 """Initialises the widget."""
1926
1927 QWidget.__init__(self)
1928
1929 self.layout = QVBoxLayout()
1930 self.layout.setSpacing(0)
1931
1932 self.filterEntity = None
1933
1934 self.setupFilters()
1935 self.setupList()
1936 self.setupToolbar()
1937
1938 self.layout.addLayout(self.filter)
1939 self.layout.addWidget(self.list)
1940 self.layout.addWidget(self.toolbar)
1941
1942 self.setLayout(self.layout)
1943 self.setButtonStates()
1944
1945 def setupFilters(self):
1946 self.filter = QGridLayout()
1947 self.filter.setSpacing(4)
1948
1949 fq = QImage()
1950 fq.load('resources/UI/FilterIcons.png')
1951
1952 # Set the custom data
1953 self.filter.typeData = -1
1954 self.filter.weightData = -1
1955 self.filter.sizeData = -1
1956
1957 # ID Filter
1958 self.IDFilter = QLineEdit()
1959 self.IDFilter.setPlaceholderText("ID / Name")
1960 self.IDFilter.textChanged.connect(self.changeFilter)
1961
1962 # Entity Toggle Button
1963 self.entityToggle = QToolButton()
1964 self.entityToggle.setCheckable(True)
1965 self.entityToggle.checked = False
1966 self.entityToggle.setIconSize(QSize(24, 24))
1967 self.entityToggle.toggled.connect(self.setEntityToggle)
1968 self.entityToggle.toggled.connect(self.changeFilter)
1969 self.entityToggle.setIcon(QIcon(QPixmap.fromImage(fq.copy(0, 0, 24, 24))))
1970
1971 # Type Toggle Button
1972 self.typeToggle = QToolButton()
1973 self.typeToggle.setIconSize(QSize(24, 24))
1974 self.typeToggle.setPopupMode(QToolButton.InstantPopup)
1975
1976 typeMenu = QMenu()
1977
1978 q = QImage()
1979 q.load('resources/UI/RoomIcons.png')
1980
1981 self.typeToggle.setIcon(QIcon(QPixmap.fromImage(fq.copy(1 * 24 + 4, 4, 16, 16))))
1982 act = typeMenu.addAction(QIcon(QPixmap.fromImage(fq.copy(1 * 24 + 4, 4, 16, 16))), '')
1983 act.setData(-1)
1984 self.typeToggle.setDefaultAction(act)
1985
1986 for i in range(24):
1987 act = typeMenu.addAction(QIcon(QPixmap.fromImage(q.copy(i * 16, 0, 16, 16))), '')
1988 act.setData(i)
1989
1990 self.typeToggle.triggered.connect(self.setTypeFilter)
1991 self.typeToggle.setMenu(typeMenu)
1992
1993 # Weight Toggle Button
1994 self.weightToggle = QToolButton()
1995 self.weightToggle.setIconSize(QSize(24, 24))
1996 self.weightToggle.setPopupMode(QToolButton.InstantPopup)
1997
1998 weightMenu = FilterMenu()
1999
2000 q = QImage()
2001 q.load('resources/UI/WeightIcons.png')
2002
2003 self.weightToggle.setIcon(QIcon(QPixmap.fromImage(fq.copy(2 * 24, 0, 24, 24))))
2004 act = weightMenu.addAction(QIcon(QPixmap.fromImage(fq.copy(2 * 24, 0, 24, 24))), '')
2005 act.setData(-1)
2006 act.setIconVisibleInMenu(False)
2007 self.weightToggle.setDefaultAction(act)
2008
2009 w = [0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 5.0, 1000.0]
2010 for i in range(9):
2011 act = weightMenu.addAction(QIcon(QPixmap.fromImage(q.copy(i * 24, 0, 24, 24))), '')
2012 act.setData(w[i])
2013 act.setIconVisibleInMenu(False)
2014
2015 self.weightToggle.triggered.connect(self.setWeightFilter)
2016 self.weightToggle.setMenu(weightMenu)
2017
2018 # Size Toggle Button
2019 self.sizeToggle = QToolButton()
2020 self.sizeToggle.setIconSize(QSize(24, 24))
2021 self.sizeToggle.setPopupMode(QToolButton.InstantPopup)
2022
2023 sizeMenu = FilterMenu()
2024
2025 q = QImage()
2026 q.load('resources/UI/ShapeIcons.png')
2027
2028 self.sizeToggle.setIcon(QIcon(QPixmap.fromImage(fq.copy(3 * 24, 0, 24, 24))))
2029 act = sizeMenu.addAction(QIcon(QPixmap.fromImage(fq.copy(3 * 24, 0, 24, 24))), '')
2030 act.setData(-1)
2031 act.setIconVisibleInMenu(False)
2032 self.sizeToggle.setDefaultAction(act)
2033
2034 for i in range(12):
2035 act = sizeMenu.addAction(QIcon(QPixmap.fromImage(q.copy(i * 16, 0, 16, 16))), '')
2036 act.setData(i + 1)
2037 act.setIconVisibleInMenu(False)
2038
2039 self.sizeToggle.triggered.connect(self.setSizeFilter)
2040 self.sizeToggle.setMenu(sizeMenu)
2041
2042 # Add to Layout
2043 self.filter.addWidget(QLabel("Filter by:"), 0, 0)
2044 self.filter.addWidget(self.IDFilter, 0, 1)
2045 self.filter.addWidget(self.entityToggle, 0, 2)
2046 self.filter.addWidget(self.typeToggle, 0, 3)
2047 self.filter.addWidget(self.weightToggle, 0, 4)
2048 self.filter.addWidget(self.sizeToggle, 0, 5)
2049 self.filter.setContentsMargins(4, 0, 0, 4)
2050
2051 # Filter active notification and clear buttons
2052
2053 # Palette
2054 self.clearAll = QToolButton()
2055 self.clearAll.setIconSize(QSize(24, 0))
2056 self.clearAll.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
2057 self.clearAll.clicked.connect(self.clearAllFilter)
2058
2059 self.clearName = QToolButton()
2060 self.clearName.setIconSize(QSize(24, 0))
2061 self.clearName.setSizePolicy(self.IDFilter.sizePolicy())
2062 self.clearName.clicked.connect(self.clearNameFilter)
2063
2064 self.clearEntity = QToolButton()
2065 self.clearEntity.setIconSize(QSize(24, 0))
2066 self.clearEntity.clicked.connect(self.clearEntityFilter)
2067
2068 self.clearType = QToolButton()
2069 self.clearType.setIconSize(QSize(24, 0))
2070 self.clearType.clicked.connect(self.clearTypeFilter)
2071
2072 self.clearWeight = QToolButton()
2073 self.clearWeight.setIconSize(QSize(24, 0))
2074 self.clearWeight.clicked.connect(self.clearWeightFilter)
2075
2076 self.clearSize = QToolButton()
2077 self.clearSize.setIconSize(QSize(24, 0))
2078 self.clearSize.clicked.connect(self.clearSizeFilter)
2079
2080 self.filter.addWidget(self.clearAll, 1, 0)
2081 self.filter.addWidget(self.clearName, 1, 1)
2082 self.filter.addWidget(self.clearEntity, 1, 2)
2083 self.filter.addWidget(self.clearType, 1, 3)
2084 self.filter.addWidget(self.clearWeight, 1, 4)
2085 self.filter.addWidget(self.clearSize, 1, 5)
2086
2087 def setupList(self):
2088 self.list = QListWidget()
2089 self.list.setViewMode(self.list.ListMode)
2090 self.list.setSelectionMode(self.list.ExtendedSelection)
2091 self.list.setResizeMode(self.list.Adjust)
2092 self.list.setContextMenuPolicy(Qt.CustomContextMenu)
2093
2094 self.list.setAutoScroll(True)
2095 self.list.setDragEnabled(True)
2096 self.list.setDragDropMode(4)
2097
2098 self.list.setVerticalScrollBarPolicy(0)
2099 self.list.setHorizontalScrollBarPolicy(1)
2100
2101 self.list.setIconSize(QSize(52, 52))
2102 d = RoomDelegate()
2103 self.list.setItemDelegate(d)
2104
2105 self.list.itemSelectionChanged.connect(self.setButtonStates)
2106 self.list.doubleClicked.connect(self.activateEdit)
2107 self.list.customContextMenuRequested.connect(self.customContextMenu)
2108
2109 self.list.itemDelegate().closeEditor.connect(self.editComplete)
2110
2111 def setupToolbar(self):
2112 self.toolbar = QToolBar()
2113
2114 self.addRoomButton = self.toolbar.addAction(QIcon(), 'Add', self.addRoom)
2115 self.removeRoomButton = self.toolbar.addAction(QIcon(), 'Delete', self.removeRoom)
2116 self.duplicateRoomButton = self.toolbar.addAction(QIcon(), 'Duplicate', self.duplicateRoom)
2117 self.exportRoomButton = self.toolbar.addAction(QIcon(), 'Export...', self.exportRoom)
2118
2119 self.mirror = False
2120 self.mirrorY = False
2121 # self.IDButton = self.toolbar.addAction(QIcon(), 'ID', self.turnIDsOn)
2122 # self.IDButton.setCheckable(True)
2123 # self.IDButton.setChecked(True)
2124
2125 def activateEdit(self):
2126 room = self.selectedRoom()
2127 room.setText(room.data(0x100))
2128 self.list.editItem(self.selectedRoom())
2129
2130 def editComplete(self, lineEdit):
2131 room = self.selectedRoom()
2132 room.setData(0x100, lineEdit.text())
2133 room.setText("{0} - {1}".format(room.roomVariant, room.data(0x100)))
2134
2135 #@pyqtSlot(bool)
2136 def turnIDsOn(self):
2137 return
2138
2139 #@pyqtSlot(QPoint)
2140 def customContextMenu(self, pos):
2141 if not self.selectedRoom(): return
2142
2143 menu = QMenu(self.list)
2144
2145 # Size Changing Menu
2146 size = menu.addMenu('Size')
2147
2148 q = QImage()
2149 q.load('resources/UI/ShapeIcons.png')
2150
2151 for sizeName in range(1, 13):
2152 i = QIcon(QPixmap.fromImage(q.copy((sizeName - 1) * 16, 0, 16, 16)))
2153
2154 s = size.addAction(i, str(sizeName))
2155 if self.selectedRoom().roomShape == sizeName:
2156 s.setCheckable(True)
2157 s.setChecked(True)
2158
2159 size.triggered.connect(self.changeSize)
2160
2161 menu.addSeparator()
2162
2163 # Type
2164 Type = QWidgetAction(menu)
2165 c = QComboBox()
2166
2167 types= [
2168 "Null Room", "Normal Room", "Shop", "Error Room", "Treasure Room", "Boss Room",
2169 "Mini-Boss Room", "Secret Room", "Super Secret Room", "Arcade", "Curse Room", "Challenge Room",
2170 "Library", "Sacrifice Room", "Devil Room", "Angel Room", "Item Dungeon", "Boss Rush Room",
2171 "Isaac's Room", "Barren Room", "Chest Room", "Dice Room", "Black Market", "Greed Mode Descent"
2172 ]
2173
2174 #if "00." not in mainWindow.path:
2175 # types=["Null Room", "Normal Room"]
2176
2177 q = QImage()
2178 q.load('resources/UI/RoomIcons.png')
2179
2180 for i, t in enumerate(types):
2181 c.addItem(QIcon(QPixmap.fromImage(q.copy(i * 16, 0, 16, 16))), t)
2182 c.setCurrentIndex(self.selectedRoom().roomType)
2183 c.currentIndexChanged.connect(self.changeType)
2184 Type.setDefaultWidget(c)
2185 menu.addAction(Type)
2186
2187 # Difficulty
2188 diff = menu.addMenu('Difficulty')
2189
2190 for d in [0, 1, 2, 5, 10]:
2191 m = diff.addAction('{0}'.format(d))
2192
2193 if self.selectedRoom().roomDifficulty == d:
2194 m.setCheckable(True)
2195 m.setChecked(True)
2196
2197 diff.triggered.connect(self.changeDifficulty)
2198
2199 # Weight (old)
2200 '''
2201 weight = menu.addMenu('Weight')
2202 for w in [0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 5.0, 1000.0]:
2203 m = weight.addAction('{0}'.format(w))
2204
2205 if self.selectedRoom().roomWeight == w:
2206 m.setCheckable(True)
2207 m.setChecked(True)
2208
2209 weight.triggered.connect(self.changeWeight)
2210 '''
2211
2212 menu.addSeparator()
2213
2214 # Weight (new)
2215 weight = QWidgetAction(menu)
2216 s = QDoubleSpinBox()
2217 s.setPrefix("Weight - ")
2218
2219 s.setValue(self.selectedRoom().roomWeight)
2220
2221 weight.setDefaultWidget(s)
2222 s.valueChanged.connect(self.changeWeight)
2223 menu.addAction(weight)
2224
2225 # Variant
2226 Variant = QWidgetAction(menu)
2227 s = QSpinBox()
2228 s.setRange(0, 65534)
2229 s.setPrefix("ID - ")
2230
2231 s.setValue(self.selectedRoom().roomVariant)
2232
2233 Variant.setDefaultWidget(s)
2234 s.valueChanged.connect(self.changeVariant)
2235 menu.addAction(Variant)
2236
2237 # SubVariant
2238 Subvariant = QWidgetAction(menu)
2239 sv = QSpinBox()
2240 sv.setRange(0, 256)
2241 sv.setPrefix("Sub - ")
2242
2243 sv.setValue(self.selectedRoom().roomSubvariant)
2244
2245 Subvariant.setDefaultWidget(sv)
2246 sv.valueChanged.connect(self.changeSubvariant)
2247 menu.addAction(Subvariant)
2248
2249 # End it
2250 menu.exec_(self.list.mapToGlobal(pos))
2251
2252 #@pyqtSlot(bool)
2253 def clearAllFilter(self):
2254 self.IDFilter.clear()
2255 self.entityToggle.setChecked(False)
2256 self.filter.typeData = -1
2257 self.typeToggle.setIcon(self.typeToggle.defaultAction().icon())
2258 self.filter.weightData = -1
2259 self.weightToggle.setIcon(self.weightToggle.defaultAction().icon())
2260 self.filter.sizeData = -1
2261 self.sizeToggle.setIcon(self.sizeToggle.defaultAction().icon())
2262 self.changeFilter()
2263
2264 def clearNameFilter(self):
2265 self.IDFilter.clear()
2266 self.changeFilter()
2267
2268 def clearEntityFilter(self):
2269 self.entityToggle.setChecked(False)
2270 self.changeFilter()
2271
2272 def clearTypeFilter(self):
2273 self.filter.typeData = -1
2274 self.typeToggle.setIcon(self.typeToggle.defaultAction().icon())
2275 self.changeFilter()
2276
2277 def clearWeightFilter(self):
2278 self.filter.weightData = -1
2279 self.weightToggle.setIcon(self.weightToggle.defaultAction().icon())
2280 self.changeFilter()
2281
2282 def clearSizeFilter(self):
2283 self.filter.sizeData = -1
2284 self.sizeToggle.setIcon(self.sizeToggle.defaultAction().icon())
2285 self.changeFilter()
2286
2287 #@pyqtSlot(bool)
2288 def setEntityToggle(self, checked):
2289 self.entityToggle.checked = checked
2290
2291 #@pyqtSlot(QAction)
2292 def setTypeFilter(self, action):
2293 self.filter.typeData = action.data()
2294 self.typeToggle.setIcon(action.icon())
2295 self.changeFilter()
2296
2297 #@pyqtSlot(QAction)
2298 def setWeightFilter(self, action):
2299 self.filter.weightData = action.data()
2300 self.weightToggle.setIcon(action.icon())
2301 self.changeFilter()
2302
2303 #@pyqtSlot(QAction)
2304 def setSizeFilter(self, action):
2305 self.filter.sizeData = action.data()
2306 self.sizeToggle.setIcon(action.icon())
2307 self.changeFilter()
2308
2309 def colourizeClearFilterButtons(self):
2310 colour = "background-color: #F00;"
2311
2312 all = False
2313
2314 # Name Button
2315 if len(self.IDFilter.text()) > 0:
2316 self.clearName.setStyleSheet(colour)
2317 all = True
2318 else:
2319 self.clearName.setStyleSheet("")
2320
2321 # Entity Button
2322 if self.entityToggle.checked:
2323 self.clearEntity.setStyleSheet(colour)
2324 all = True
2325 else:
2326 self.clearEntity.setStyleSheet("")
2327
2328 # Type Button
2329 if self.filter.typeData is not -1:
2330 self.clearType.setStyleSheet(colour)
2331 all = True
2332 else:
2333 self.clearType.setStyleSheet("")
2334
2335 # Weight Button
2336 if self.filter.weightData is not -1:
2337 self.clearWeight.setStyleSheet(colour)
2338 all = True
2339 else:
2340 self.clearWeight.setStyleSheet("")
2341
2342 # Size Button
2343 if self.filter.sizeData is not -1:
2344 self.clearSize.setStyleSheet(colour)
2345 all = True
2346 else:
2347 self.clearSize.setStyleSheet("")
2348
2349 # All Button
2350 if all:
2351 self.clearAll.setStyleSheet(colour)
2352 else:
2353 self.clearAll.setStyleSheet("")
2354
2355 #@pyqtSlot()
2356 def changeFilter(self):
2357 self.colourizeClearFilterButtons()
2358
2359 # Here we go
2360 for room in self.getRooms():
2361 IDCond = entityCond = typeCond = weightCond = sizeCond = True
2362
2363 if self.IDFilter.text().lower() not in room.text().lower():
2364 IDCond = False
2365
2366 # Check if the right entity is in the room
2367 if self.entityToggle.checked and self.filterEntity:
2368 entityCond = False
2369
2370 for x in room.roomSpawns:
2371 for y in x:
2372 for e in y:
2373 if int(self.filterEntity.ID) == e[0] and int(self.filterEntity.subtype) == e[2] and int(self.filterEntity.variant) == e[1]:
2374 entityCond = True
2375
2376 # Check if the room is the right type
2377 if self.filter.typeData is not -1:
2378 if self.filter.typeData == 0: # This is a null room, but we'll look for empty rooms too
2379 typeCond = (self.filter.typeData == room.roomType) or (len(room.roomSpawns) == 0)
2380
2381 uselessEntities = [0, 1, 2, 1940]
2382 hasUsefulEntities = False
2383 for y in enumerate(room.roomSpawns):
2384 for x in enumerate(y[1]):
2385 for entity in x[1]:
2386 if entity[0] not in uselessEntities:
2387 hasUsefulEntities = True
2388
2389 if typeCond == False and hasUsefulEntities == False:
2390 typeCond = True
2391
2392 else: # All the normal rooms
2393 typeCond = self.filter.typeData == room.roomType
2394
2395 # Check if the room is the right weight
2396 if self.filter.weightData is not -1:
2397 weightCond = self.filter.weightData == room.roomWeight
2398
2399 # Check if the room is the right size
2400 if self.filter.sizeData is not -1:
2401 sizeCond = False
2402
2403 shape = self.filter.sizeData
2404
2405 if room.roomShape == shape:
2406 sizeCond = True
2407
2408 # Filter em' out
2409 if IDCond and entityCond and typeCond and weightCond and sizeCond:
2410 room.setHidden(False)
2411 else:
2412 room.setHidden(True)
2413
2414 def setEntityFilter(self, entity):
2415 self.filterEntity = entity
2416 self.entityToggle.setIcon(entity.icon)
2417 self.changeFilter()
2418
2419 #@pyqtSlot(QAction)
2420 def changeSize(self, action):
2421
2422 # Set the Size - gotta lotta shit to do here
2423 s = int(action.text())
2424
2425 w = 26
2426 if s in [1, 2, 3, 4, 5]:
2427 w = 13
2428
2429 h = 14
2430 if s in [1, 2, 3, 6, 7]:
2431 h = 7
2432
2433 # No sense in doing work we don't have to!
2434 if self.selectedRoom().roomWidth == w and self.selectedRoom().roomHeight == h and self.selectedRoom().roomShape == s:
2435 return
2436
2437 # Check to see if resizing will destroy any entities
2438 warn = False
2439 mainWindow.storeEntityList()
2440
2441 for y in enumerate(self.selectedRoom().roomSpawns):
2442 for x in enumerate(y[1]):
2443 for entity in x[1]:
2444
2445 if x[0] >= w or y[0] >= h:
2446 warn = True
2447
2448 if warn:
2449 msgBox = QMessageBox(
2450 QMessageBox.Warning,
2451 "Resize Room?", "Resizing this room will delete entities placed outside the new size. Are you sure you want to resize this room?",
2452 QMessageBox.NoButton,
2453 self
2454 )
2455 msgBox.addButton("Resize", QMessageBox.AcceptRole)
2456 msgBox.addButton("Cancel", QMessageBox.RejectRole)
2457 if msgBox.exec_() == QMessageBox.RejectRole:
2458 # It's time for us to go now.
2459 return
2460
2461 # Clear the room and reset the size
2462 mainWindow.scene.clear()
2463 self.selectedRoom().roomWidth = w
2464 self.selectedRoom().roomHeight = h
2465 self.selectedRoom().roomShape = s
2466
2467 self.selectedRoom().makeNewDoors()
2468 self.selectedRoom().clearDoors()
2469 mainWindow.scene.newRoomSize(w, h, s)
2470
2471 mainWindow.editor.resizeEvent(QResizeEvent(mainWindow.editor.size(), mainWindow.editor.size()))
2472
2473 # Spawn those entities
2474 for y in enumerate(self.selectedRoom().roomSpawns):
2475 for x in enumerate(y[1]):
2476 for entity in x[1]:
2477 if x[0] >= w or y[0] >= h: continue
2478
2479 e = Entity(x[0], y[0], entity[0], entity[1], entity[2], entity[3])
2480 mainWindow.scene.addItem(e)
2481
2482 self.selectedRoom().setToolTip()
2483 mainWindow.dirt()
2484
2485 #@pyqtSlot(int)
2486 def changeType(self, rtype):
2487 for r in self.selectedRooms():
2488 r.roomType = rtype
2489 r.renderDisplayIcon()
2490 r.setRoomBG()
2491
2492 r.setToolTip()
2493
2494 mainWindow.scene.update()
2495 mainWindow.dirt()
2496
2497 #@pyqtSlot(int)
2498 def changeVariant(self, var):
2499 for r in self.selectedRooms():
2500 r.roomVariant = var
2501 r.setToolTip()
2502 r.setText("{0} - {1}".format(r.roomVariant, r.data(0x100)))
2503 mainWindow.dirt()
2504 mainWindow.scene.update()
2505
2506 #@pyqtSlot(int)
2507 def changeSubvariant(self, var):
2508 for r in self.selectedRooms():
2509 r.roomSubvariant = var
2510 r.setToolTip()
2511 mainWindow.dirt()
2512 mainWindow.scene.update()
2513
2514 #@pyqtSlot(QAction)
2515 def changeDifficulty(self, action):
2516 for r in self.selectedRooms():
2517 #self.selectedRoom().roomDifficulty = int(action.text())
2518 r.setDifficulty(int(action.text()))
2519 r.setToolTip()
2520 mainWindow.dirt()
2521 mainWindow.scene.update()
2522
2523 #@pyqtSlot(QAction)
2524 def changeWeight(self, action):
2525 for r in self.selectedRooms():
2526 #r.roomWeight = float(action.text())
2527 r.roomWeight = action
2528 r.setToolTip()
2529 mainWindow.dirt()
2530 mainWindow.scene.update()
2531
2532 def keyPressEvent(self, event):
2533 self.list.keyPressEvent(event)
2534
2535 if event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace:
2536 self.removeRoom()
2537
2538 def addRoom(self):
2539 """Creates a new room."""
2540
2541 r = Room()
2542 self.list.insertItem(self.list.currentRow()+1, r)
2543 self.list.setCurrentItem(r, QItemSelectionModel.ClearAndSelect)
2544 mainWindow.dirt()
2545
2546 def removeRoom(self):
2547 """Removes selected room (no takebacks)"""
2548
2549 rooms = self.selectedRooms()
2550 if rooms == None or len(rooms) == 0:
2551 return
2552
2553 if len(rooms) == 1:
2554 s = "this room"
2555 else:
2556 s = "these rooms"
2557
2558 msgBox = QMessageBox(QMessageBox.Warning,
2559 "Delete Room?", "Are you sure you want to delete {0}? This action cannot be undone.".format(s),
2560 QMessageBox.NoButton, self)
2561 msgBox.addButton("Delete", QMessageBox.AcceptRole)
2562 msgBox.addButton("Cancel", QMessageBox.RejectRole)
2563 if msgBox.exec_() == QMessageBox.AcceptRole:
2564
2565 self.list.clearSelection()
2566 for item in rooms:
2567 self.list.takeItem(self.list.row(item))
2568
2569 self.list.scrollToItem(self.list.currentItem())
2570 self.list.setCurrentItem(self.list.currentItem(), QItemSelectionModel.Select)
2571 mainWindow.dirt()
2572
2573 def duplicateRoom(self):
2574 """Duplicates the selected room"""
2575
2576 rooms = self.selectedRooms()
2577 if rooms == None or len(rooms) == 0:
2578 return
2579
2580 mainWindow.storeEntityList()
2581
2582 initialPlace = self.list.currentRow()
2583 self.selectedRoom().setData(100, False)
2584 self.list.setCurrentItem(None, QItemSelectionModel.ClearAndSelect)
2585
2586 for room in rooms:
2587 if self.mirrorY:
2588 v = 20000
2589 elif self.mirror:
2590 v = 10000
2591 else:
2592 v = 1
2593
2594 r = Room(
2595 deepcopy(room.data(0x100) + ' (copy)'),
2596 deepcopy([list(door) for door in room.roomDoors]),
2597 deepcopy(room.roomSpawns),
2598 deepcopy(room.roomType),
2599 deepcopy(room.roomVariant+v),
2600 deepcopy(room.roomSubvariant),
2601 deepcopy(room.roomDifficulty),
2602 deepcopy(room.roomWeight),
2603 deepcopy(room.roomWidth),
2604 deepcopy(room.roomHeight),
2605 deepcopy(room.roomShape)
2606 )
2607
2608 if self.mirror:
2609 # Change the name to mirrored.
2610 flipName = ' (flipped X)'
2611 if self.mirrorY:
2612 flipName = ' (flipped Y)'
2613 r.setData(0x100, room.data(0x100) + flipName)
2614 r.setText("{0} - {1}".format(r.roomVariant, room.data(0x100) + flipName))
2615
2616 # Mirror the room
2617 if self.mirrorY:
2618 r.mirrorY()
2619 else:
2620 r.mirrorX()
2621
2622 self.list.insertItem(initialPlace + v, r)
2623 self.list.setCurrentItem(r, QItemSelectionModel.Select)
2624
2625 mainWindow.dirt()
2626
2627 def mirrorButtonOn(self):
2628 self.mirror = True
2629 self.duplicateRoomButton.setText("Mirror X")
2630
2631 def mirrorButtonOff(self):
2632 self.mirror = False
2633 self.mirrorY = False
2634 self.duplicateRoomButton.setText("Duplicate")
2635
2636 def mirrorYButtonOn(self):
2637 if self.mirror:
2638 self.mirrorY = True
2639 self.duplicateRoomButton.setText("Mirror Y")
2640
2641 def mirrorYButtonOff(self):
2642 if self.mirror:
2643 self.mirrorY = False
2644 self.duplicateRoomButton.setText("Mirror X")
2645
2646 def exportRoom(self):
2647
2648 dialogDir = findModsPath()
2649
2650 recent = settings.value("RecentFiles", [])
2651 if len(recent) > 1:
2652 dialogDir = recent[1]
2653
2654 target, match = QFileDialog.getSaveFileName(self, 'Select a new name or an existing STB', dialogDir, 'Stage Bundle (*.stb)', '', QFileDialog.DontConfirmOverwrite)
2655 mainWindow.restoreEditMenu()
2656
2657 if len(target) == 0:
2658 return
2659
2660 path = target
2661
2662 # Append these rooms onto the new STB
2663 if os.path.exists(path):
2664 rooms = self.selectedRooms()
2665 oldRooms = mainWindow.open(path)
2666
2667 oldRooms.extend(rooms)
2668
2669 mainWindow.save(oldRooms, path)
2670
2671 # Make a new STB with the selected rooms
2672 else:
2673 mainWindow.save(self.selectedRooms(), path)
2674
2675 def setButtonStates(self):
2676 rooms = len(self.selectedRooms()) > 0
2677
2678 self.removeRoomButton.setEnabled(rooms)
2679 self.duplicateRoomButton.setEnabled(rooms)
2680 self.exportRoomButton.setEnabled(rooms)
2681
2682 def selectedRoom(self):
2683 return self.list.currentItem()
2684
2685 def selectedRooms(self):
2686 return self.list.selectedItems()
2687
2688 def getRooms(self):
2689 ret = []
2690 for i in range(self.list.count()):
2691 ret.append(self.list.item(i))
2692
2693 return ret
2694
2695# Entity Palette
2696########################
2697
2698class EntityGroupItem(object):
2699 """Group Item to contain Entities for sorting"""
2700
2701 def __init__(self, name):
2702
2703 self.objects = []
2704 self.startIndex = 0
2705 self.endIndex = 0
2706
2707 self.name = name
2708 self.alignment = Qt.AlignCenter
2709
2710 def getItem(self, index):
2711 ''' Retrieves an item of a specific index. The index is already checked for validity '''
2712
2713 if index == self.startIndex:
2714 return self
2715
2716 if (index <= self.startIndex + len(self.objects)):
2717 return self.objects[index - self.startIndex - 1]
2718
2719 def calculateIndices(self, index):
2720 self.startIndex = index
2721 self.endIndex = len(self.objects) + index
2722
2723class EntityItem(QStandardItem):
2724 """A single entity, pretty much just an icon and a few params."""
2725
2726 def __init__(self, name, ID, subtype, variant, iconPath):
2727 QStandardItem.__init__(self)
2728
2729 self.name = name
2730 self.ID = ID
2731 self.subtype = subtype
2732 self.variant = variant
2733 self.icon = QIcon(iconPath)
2734
2735 self.setToolTip(name)
2736
2737class EntityGroupModel(QAbstractListModel):
2738 """Model containing all the grouped objects in a tileset"""
2739
2740 def __init__(self, kind):
2741 self.groups = {}
2742 self.kind = kind
2743 self.view = None
2744
2745 self.filter = ""
2746
2747 QAbstractListModel.__init__(self)
2748
2749 global entityXML
2750 enList = entityXML.findall("entity")
2751
2752 for en in enList:
2753 g = en.get('Group')
2754 k = en.get('Kind')
2755
2756 if self.kind == k or self.kind == None:
2757 if g and g not in self.groups:
2758 self.groups[g] = EntityGroupItem(g)
2759
2760 e = EntityItem(en.get('Name'), en.get('ID'), en.get('Subtype'), en.get('Variant'), en.get('Image'))
2761
2762 if g != None:
2763 self.groups[g].objects.append(e)
2764
2765 i = 0
2766 for key, group in sorted(self.groups.items()):
2767 group.calculateIndices(i)
2768 i = group.endIndex + 1
2769
2770 def rowCount(self, parent=None):
2771 c = 0
2772
2773 for group in self.groups.values():
2774 c += len(group.objects) + 1
2775
2776 return c
2777
2778 def flags(self, index):
2779 item = self.getItem(index.row())
2780
2781 if isinstance(item, EntityGroupItem):
2782 return Qt.NoItemFlags
2783 else:
2784 return Qt.ItemIsEnabled | Qt.ItemIsSelectable
2785
2786 def getItem(self, index):
2787 for group in self.groups.values():
2788 if (group.startIndex <= index) and (index <= group.endIndex):
2789 return group.getItem(index)
2790
2791 def data(self, index, role=Qt.DisplayRole):
2792 # Should return the contents of a row when asked for the index
2793 #
2794 # Can be optimized by only dealing with the roles we need prior
2795 # to lookup: Role order is 13, 6, 7, 9, 10, 1, 0, 8
2796
2797 if ((role > 1) and (role < 6)):
2798 return None
2799
2800 elif role == Qt.ForegroundRole:
2801 return QBrush(Qt.black)
2802
2803 elif role == Qt.TextAlignmentRole:
2804 return Qt.AlignCenter
2805
2806
2807 if not index.isValid(): return None
2808 n = index.row()
2809
2810 if n < 0: return None
2811 if n >= self.rowCount(): return None
2812
2813 item = self.getItem(n)
2814
2815 if role == Qt.DecorationRole:
2816 if isinstance(item, EntityItem):
2817 return item.icon
2818
2819 if role == Qt.ToolTipRole or role == Qt.StatusTipRole or role == Qt.WhatsThisRole:
2820 if isinstance(item, EntityItem):
2821 return "{0}".format(item.name)
2822
2823 elif role == Qt.DisplayRole:
2824 if isinstance(item, EntityGroupItem):
2825 return item.name
2826
2827 elif (role == Qt.SizeHintRole):
2828 if isinstance(item, EntityGroupItem):
2829 return QSize(self.view.viewport().width(), 24)
2830
2831 elif role == Qt.BackgroundRole:
2832 if isinstance(item, EntityGroupItem):
2833
2834 colour = 165
2835
2836 if colour > 255:
2837 colour = 255
2838
2839 brush = QBrush(QColor(colour, colour, colour), Qt.Dense4Pattern)
2840
2841 return brush
2842
2843 elif (role == Qt.FontRole):
2844 font = QFont()
2845 font.setPixelSize(16)
2846 font.setBold(True)
2847
2848 return font
2849
2850 return None
2851
2852class EntityPalette(QWidget):
2853
2854 def __init__(self):
2855 """Initialises the widget. Remember to call setTileset() on it
2856 whenever the layer changes."""
2857
2858 QWidget.__init__(self)
2859
2860 # Make the layout
2861 self.layout = QVBoxLayout()
2862 self.layout.setSpacing(0)
2863
2864 # Create the tabs for the default and mod entities
2865 self.tabs = QTabWidget()
2866 self.populateTabs()
2867 self.layout.addWidget(self.tabs)
2868
2869 # Create the hidden search results tab
2870 self.searchTab = QTabWidget()
2871
2872 # Funky model setup
2873 listView = EntityList()
2874 listView.setModel(EntityGroupModel(None))
2875 listView.model().view = listView
2876 listView.clicked.connect(self.objSelected)
2877
2878 # Hide the search results
2879 self.searchTab.addTab(listView, "Search")
2880 self.searchTab.hide()
2881
2882 self.layout.addWidget(self.searchTab)
2883
2884 # Add the Search bar
2885 self.searchBar = QLineEdit()
2886 self.searchBar.setPlaceholderText("Search")
2887 self.searchBar.textEdited.connect(self.updateSearch)
2888 self.layout.addWidget(self.searchBar)
2889
2890 # And Done
2891 self.setLayout(self.layout)
2892
2893 def populateTabs(self):
2894
2895 groups = ["Pickups", "Enemies", "Bosses", "Stage", "Collect" ]
2896 if settings.value('ModAutogen') == '1':
2897 groups.append("Mods")
2898
2899 for group in groups:
2900
2901 listView = EntityList()
2902
2903 listView.setModel(EntityGroupModel(group))
2904 listView.model().view = listView
2905
2906 listView.clicked.connect(self.objSelected)
2907
2908 if group == "Bosses":
2909 listView.setIconSize(QSize(52, 52))
2910
2911 if group == "Collect":
2912 listView.setIconSize(QSize(32, 64))
2913
2914 self.tabs.addTab(listView, group)
2915
2916 return
2917
2918 def currentSelectedObject(self):
2919 """Returns the currently selected object reference, for painting purposes."""
2920
2921 if len(self.searchBar.text()) > 0:
2922 index = self.searchTab.currentWidget().currentIndex().row()
2923 obj = self.searchTab.currentWidget().model().getItem(index)
2924 else:
2925 index = self.tabs.currentWidget().currentIndex().row()
2926 obj = self.tabs.currentWidget().model().getItem(index)
2927
2928 return obj
2929
2930 #@pyqtSlot()
2931 def objSelected(self):
2932 """Throws a signal emitting the current object when changed"""
2933 if (self.currentSelectedObject()):
2934 self.objChanged.emit(self.currentSelectedObject())
2935
2936 # Throws a signal when the selected object is used as a replacement
2937 if QApplication.keyboardModifiers() == Qt.AltModifier:
2938 self.objReplaced.emit(self.currentSelectedObject())
2939
2940 #@pyqtSlot()
2941 def updateSearch(self, text):
2942 if len(self.searchBar.text()) > 0:
2943 self.tabs.hide()
2944 self.searchTab.widget(0).filter = text
2945 self.searchTab.widget(0).filterList()
2946 self.searchTab.show()
2947 else:
2948 self.tabs.show()
2949 self.searchTab.hide()
2950
2951 objChanged = pyqtSignal(EntityItem)
2952 objReplaced = pyqtSignal(EntityItem)
2953
2954class EntityList(QListView):
2955
2956 def __init__(self):
2957 QListView.__init__(self)
2958
2959 self.setFlow(QListView.LeftToRight)
2960 self.setLayoutMode(QListView.SinglePass)
2961 self.setMovement(QListView.Static)
2962 self.setResizeMode(QListView.Adjust)
2963 self.setWrapping(True)
2964 self.setIconSize(QSize(26, 26))
2965
2966 self.setMouseTracking(True)
2967
2968 self.filter = ""
2969
2970 def mouseMoveEvent(self, event):
2971
2972 index = self.indexAt(event.pos()).row()
2973
2974 if index is not -1:
2975 item = self.model().getItem(index)
2976
2977 if isinstance(item, EntityItem):
2978 QToolTip.showText(event.globalPos(), item.name)
2979
2980 def filterList(self):
2981 m = self.model()
2982 rows = m.rowCount()
2983
2984 # First loop for entity items
2985 for row in range(rows):
2986 item = m.getItem(row)
2987
2988 if isinstance(item, EntityItem):
2989 if self.filter.lower() in item.name.lower():
2990 self.setRowHidden(row, False)
2991 else:
2992 self.setRowHidden(row, True)
2993
2994 # Second loop for Group titles, check to see if all contents are hidden or not
2995 for row in range(rows):
2996 item = m.getItem(row)
2997
2998 if isinstance(item, EntityGroupItem):
2999 self.setRowHidden(row, True)
3000
3001 for i in range(item.startIndex, item.endIndex):
3002 if not self.isRowHidden(i):
3003 self.setRowHidden(row, False)
3004
3005
3006########################
3007# Main Window #
3008########################
3009
3010class MainWindow(QMainWindow):
3011
3012 def keyPressEvent(self, event):
3013 QMainWindow.keyPressEvent(self, event)
3014 if event.key() == Qt.Key_Alt:
3015 self.roomList.mirrorButtonOn()
3016 if event.key() == Qt.Key_Shift:
3017 self.roomList.mirrorYButtonOn()
3018
3019 def keyReleaseEvent(self, event):
3020 QMainWindow.keyReleaseEvent(self, event)
3021 if event.key() == Qt.Key_Alt:
3022 self.roomList.mirrorButtonOff()
3023 if event.key() == Qt.Key_Shift:
3024 self.roomList.mirrorYButtonOff()
3025
3026 defaultMapsDict = {
3027 "Special Rooms": "00.special rooms.stb",
3028 "Basement": "01.basement.stb",
3029 "Cellar": "02.cellar.stb",
3030 "Burning Basement": "03.burning basement.stb",
3031 "Caves": "04.caves.stb",
3032 "Catacombs": "05.catacombs.stb",
3033 "Flooded Caves": "06.flooded caves.stb",
3034 "Depths": "07.depths.stb",
3035 "Necropolis": "08.necropolis.stb",
3036 "Dank Depths": "09.dank depths.stb",
3037 "Womb": "10.womb.stb",
3038 "Utero": "11.utero.stb",
3039 "Scarred Womb": "12.scarred womb.stb",
3040 "Blue Womb": "13.blue womb.stb",
3041 "Sheol": "14.sheol.stb",
3042 "Cathedral": "15.cathedral.stb",
3043 "Dark Room": "16.dark room.stb",
3044 "Chest": "17.chest.stb",
3045 "Special Rooms [Greed]": "18.greed special.stb",
3046 "Basement [Greed]": "19.greed basement.stb",
3047 "Caves [Greed]": "20.greed caves.stb",
3048 "Depths [Greed]": "21.greed depths.stb",
3049 "Womb [Greed]": "22.greed womb.stb",
3050 "Sheol [Greed]": "23.greed sheol.stb",
3051 "The Shop [Greed]": "24.greed the shop.stb",
3052 "Ultra Greed [Greed]": "25.ultra greed.stb" }
3053
3054 defaultMapsOrdered = OrderedDict(sorted(defaultMapsDict.items(), key=lambda t: t[0]))
3055
3056 def __init__(self):
3057 super(QMainWindow, self).__init__()
3058
3059 self.setWindowTitle('Basement Renovator')
3060 self.setIconSize(QSize(16, 16))
3061
3062 self.dirty = False
3063
3064 self.scene = RoomScene()
3065 self.clipboard = None
3066
3067 self.editor = RoomEditorWidget(self.scene)
3068 self.setCentralWidget(self.editor)
3069
3070 self.setupDocks()
3071 self.setupMenuBar()
3072
3073 self.setGeometry(100, 500, 1280, 600)
3074
3075 # Restore Settings
3076 if not settings.value('GridEnabled', True) or settings.value('GridEnabled', True) == 'false': self.switchGrid()
3077 if not settings.value('StatusEnabled', True) or settings.value('StatusEnabled', True) == 'false': self.switchInfo()
3078 if not settings.value('BitfontEnabled', True) or settings.value('BitfontEnabled', True) == 'false': self.switchBitFont()
3079
3080 self.restoreState(settings.value('MainWindowState', self.saveState()), 0)
3081 self.restoreGeometry(settings.value('MainWindowGeometry', self.saveGeometry()))
3082
3083 self.resetWindow = {"state" : self.saveState(), "geometry" : self.saveGeometry()}
3084
3085 # Setup a new map
3086 self.newMap()
3087 self.clean()
3088
3089 def setupFileMenuBar(self):
3090 f = self.fileMenu
3091
3092 f.clear()
3093 self.fa = f.addAction('New', self.newMap, QKeySequence("Ctrl+N"))
3094 self.fc = f.addAction('Open', self.openMap, QKeySequence("Ctrl+O"))
3095 self.fb = f.addAction('Open by Stage', self.openMapDefault, QKeySequence("Ctrl+Shift+O"))
3096 f.addSeparator()
3097 self.fd = f.addAction('Save', self.saveMap, QKeySequence("Ctrl+S"))
3098 self.fe = f.addAction('Save As...', self.saveMapAs, QKeySequence("Ctrl+Shift+S"))
3099 f.addSeparator()
3100 self.fg = f.addAction('Take Screenshot...', self.screenshot, QKeySequence("Ctrl+Alt+S"))
3101 f.addSeparator()
3102 self.fh = f.addAction('Set Resources Path', self.setDefaultResourcesPath, QKeySequence("Ctrl+Shift+P"))
3103 self.fi = f.addAction('Reset Resources Path', self.resetResourcesPath, QKeySequence("Ctrl+Shift+R"))
3104 f.addSeparator()
3105 self.fl = f.addAction('Autogenerate mod content (discouraged)', self.toggleModAutogen)
3106 self.fl.setCheckable(True)
3107 self.fl.setChecked(settings.value('ModAutogen') == '1')
3108 f.addSeparator()
3109
3110 recent = settings.value("RecentFiles", [])
3111 for r in recent:
3112 f.addAction(os.path.normpath(r), self.openRecent).setData(r)
3113
3114 f.addSeparator()
3115
3116 self.fj = f.addAction('Exit', self.close, QKeySequence.Quit)
3117
3118 def setupMenuBar(self):
3119 mb = self.menuBar()
3120
3121 self.fileMenu = mb.addMenu('&File')
3122 self.setupFileMenuBar()
3123
3124 self.e = mb.addMenu('Edit')
3125 self.ea = self.e.addAction('Copy', self.copy, QKeySequence.Copy)
3126 self.eb = self.e.addAction('Cut', self.cut, QKeySequence.Cut)
3127 self.ec = self.e.addAction('Paste', self.paste, QKeySequence.Paste)
3128 self.ed = self.e.addAction('Select All', self.selectAll, QKeySequence.SelectAll)
3129 self.ee = self.e.addAction('Deselect', self.deSelect, QKeySequence("Ctrl+D"))
3130 self.e.addSeparator()
3131 self.ef = self.e.addAction('Clear Filters', self.roomList.clearAllFilter, QKeySequence("Ctrl+K"))
3132
3133 v = mb.addMenu('View')
3134 self.wa = v.addAction('Hide Grid', self.switchGrid, QKeySequence("Ctrl+G"))
3135 self.we = v.addAction('Hide Info', self.switchInfo, QKeySequence("Ctrl+I"))
3136 self.wd = v.addAction('Use Aliased Counter', self.switchBitFont, QKeySequence("Ctrl+Alt+A"))
3137 v.addSeparator()
3138 self.wb = v.addAction('Hide Entity Painter', self.showPainter, QKeySequence("Ctrl+Alt+P"))
3139 self.wc = v.addAction('Hide Room List', self.showRoomList, QKeySequence("Ctrl+Alt+R"))
3140 self.wf = v.addAction('Reset Window Defaults', self.resetWindowDefaults)
3141 v.addSeparator()
3142
3143 r = mb.addMenu('Test')
3144 self.ra = r.addAction('Test Current Room - InstaPreview', self.testMapInstapreview, QKeySequence("Ctrl+P"))
3145 self.ra = r.addAction('Test Current Room - Basement', self.testMap, QKeySequence("Ctrl+T"))
3146 self.ra = r.addAction('Test Current Room - Start', self.testStartMap, QKeySequence("Ctrl+Shift+T"))
3147
3148 h = mb.addMenu('Help')
3149 self.ha = h.addAction('About Basement Renovator', self.aboutDialog)
3150 self.hb = h.addAction('Basement Renovator Documentation', self.goToHelp)
3151 # self.hc = h.addAction('Keyboard Shortcuts')
3152
3153 def setupDocks(self):
3154 self.roomList = RoomSelector()
3155 self.roomListDock = QDockWidget('Rooms')
3156 self.roomListDock.setWidget(self.roomList)
3157 self.roomListDock.visibilityChanged.connect(self.updateDockVisibility)
3158 self.roomListDock.setObjectName("RoomListDock")
3159
3160 self.roomList.list.currentItemChanged.connect(self.handleSelectedRoomChanged)
3161
3162 self.addDockWidget(Qt.RightDockWidgetArea, self.roomListDock)
3163
3164 self.EntityPalette = EntityPalette()
3165 self.EntityPaletteDock = QDockWidget('Entity Palette')
3166 self.EntityPaletteDock.setWidget(self.EntityPalette)
3167 self.EntityPaletteDock.visibilityChanged.connect(self.updateDockVisibility)
3168 self.EntityPaletteDock.setObjectName("EntityPaletteDock")
3169
3170 self.EntityPalette.objChanged.connect(self.handleObjectChanged)
3171 self.EntityPalette.objReplaced.connect(self.handleObjectReplaced)
3172
3173 self.addDockWidget(Qt.LeftDockWidgetArea, self.EntityPaletteDock)
3174
3175 def restoreEditMenu(self):
3176 a = self.e.actions()
3177 self.e.insertAction(a[1], self.ea)
3178 self.e.insertAction(a[2], self.eb)
3179 self.e.insertAction(a[3], self.ec)
3180 self.e.insertAction(a[4], self.ed)
3181 self.e.insertAction(a[5], self.ee)
3182
3183 def updateTitlebar(self):
3184 if self.path == '':
3185 effectiveName = 'Untitled Map'
3186 else:
3187 if "Windows" in platform.system():
3188 effectiveName = os.path.normpath(self.path)
3189 else:
3190 effectiveName = os.path.basename(self.path)
3191
3192 self.setWindowTitle('%s - Basement Renovator' % effectiveName)
3193
3194 def checkDirty(self):
3195 if self.dirty == False:
3196 return False
3197
3198 msgBox = QMessageBox(QMessageBox.Warning,
3199 "File is not saved", "Completing this operation without saving could cause loss of data.",
3200 QMessageBox.NoButton, self)
3201 msgBox.addButton("Continue", QMessageBox.AcceptRole)
3202 msgBox.addButton("Cancel", QMessageBox.RejectRole)
3203 if msgBox.exec_() == QMessageBox.AcceptRole:
3204 self.clean()
3205 return False
3206
3207 return True
3208
3209 def dirt(self):
3210 self.setWindowIcon(QIcon('resources/UI/BasementRenovator-SmallDirty.png'))
3211 self.dirty = True
3212
3213 def clean(self):
3214 self.setWindowIcon(QIcon('resources/UI/BasementRenovator-Small.png'))
3215 self.dirty = False
3216
3217 def storeEntityList(self, room=None):
3218 if not room:
3219 room = self.roomList.selectedRoom()
3220
3221 eList = self.scene.items()
3222
3223 spawns = [[[] for y in range(26)] for x in range(14)]
3224 doors = []
3225
3226 for e in eList:
3227 if isinstance(e, Door):
3228 doors.append(e.doorItem)
3229
3230 elif isinstance(e, Entity):
3231 spawns[e.entity['Y']][e.entity['X']].append([e.entity['Type'], e.entity['Variant'], e.entity['Subtype'], e.entity['Weight']])
3232
3233 room.roomSpawns = spawns
3234 room.roomDoors = doors
3235
3236 def closeEvent(self, event):
3237 """Handler for the main window close event"""
3238
3239 if self.checkDirty():
3240 event.ignore()
3241 else:
3242 settings = QSettings('settings.ini', QSettings.IniFormat)
3243
3244 # Save our state
3245 settings.setValue('MainWindowGeometry', self.saveGeometry())
3246 settings.setValue('MainWindowState', self.saveState(0))
3247
3248 event.accept()
3249
3250 app.quit()
3251
3252
3253#####################
3254# Slots for Widgets #
3255#####################
3256
3257 #@pyqtSlot(Room, Room)
3258 def handleSelectedRoomChanged(self, current, prev):
3259
3260 if current:
3261
3262 # Encode the current room, just in case there are changes
3263 if prev:
3264 self.storeEntityList(prev)
3265
3266 # Clear the current room mark
3267 prev.setData(100, False)
3268
3269 # Clear the room and reset the size
3270 self.scene.clear()
3271 self.scene.newRoomSize(current.roomWidth, current.roomHeight, current.roomShape)
3272
3273 self.editor.resizeEvent(QResizeEvent(self.editor.size(), self.editor.size()))
3274
3275 # Make some doors
3276 current.clearDoors()
3277
3278 # Spawn those entities
3279 for y in enumerate(current.roomSpawns):
3280 for x in enumerate(y[1]):
3281 for entity in x[1]:
3282 e = Entity(x[0], y[0], entity[0], entity[1], entity[2], entity[3])
3283 self.scene.addItem(e)
3284
3285 # Make the current Room mark for clearer multi-selection
3286 current.setData(100, True)
3287
3288 #@pyqtSlot(EntityItem)
3289 def handleObjectChanged(self, entity):
3290 self.editor.objectToPaint = entity
3291 self.roomList.setEntityFilter(entity)
3292
3293 #@pyqtSlot(EntityItem)
3294 def handleObjectReplaced(self, entity):
3295 for item in self.scene.selectedItems():
3296 item.entity['Type'] = int(entity.ID)
3297 item.entity['Variant'] = int(entity.variant)
3298 item.entity['Subtype'] = int(entity.subtype)
3299
3300 item.getEntityInfo(int(entity.ID), int(entity.subtype), int(entity.variant))
3301 item.update()
3302
3303 self.dirt()
3304
3305
3306########################
3307# Slots for Menu Items #
3308########################
3309
3310# File
3311########################
3312
3313 def newMap(self):
3314 if self.checkDirty(): return
3315 self.roomList.list.clear()
3316 self.scene.clear()
3317 self.path = ''
3318
3319 self.updateTitlebar()
3320 self.dirt()
3321 self.roomList.changeFilter()
3322
3323 def setDefaultResourcesPath(self):
3324 settings = QSettings('settings.ini', QSettings.IniFormat)
3325 if not settings.contains("ResourceFolder"):
3326 settings.setValue("ResourceFolder", self.findResourcePath())
3327 resPath = settings.value("ResourceFolder")
3328 resPathDialog = QFileDialog()
3329 resPathDialog.setFilter(QDir.Hidden)
3330 newResPath = QFileDialog.getExistingDirectory(self, "Select directory", resPath)
3331
3332 if newResPath != "":
3333 settings.setValue("ResourceFolder", newResPath)
3334
3335 def resetResourcesPath(self):
3336 settings = QSettings('settings.ini', QSettings.IniFormat)
3337 settings.remove("ResourceFolder")
3338 settings.setValue("ResourceFolder", self.findResourcePath())
3339
3340 def toggleModAutogen(self):
3341 settings = QSettings('settings.ini', QSettings.IniFormat)
3342 settings.setValue('ModAutogen', settings.value('ModAutogen') == '1' and '0' or '1')
3343
3344 def openMapDefault(self):
3345 settings = QSettings('settings.ini', QSettings.IniFormat)
3346 if self.checkDirty(): return
3347
3348 selectedMap, selectedMapOk = QInputDialog.getItem(self, "Map selection", "Select floor", self.defaultMapsOrdered, 0, False)
3349 self.restoreEditMenu()
3350
3351 mapFileName = ""
3352 if selectedMapOk:
3353 mapFileName = self.defaultMapsDict[selectedMap]
3354 else:
3355 return
3356
3357 roomPath = os.path.join(os.path.expanduser(self.findResourcePath()), "rooms", mapFileName)
3358
3359 if not QFile.exists(roomPath):
3360 self.setDefaultResourcesPath()
3361 roomPath = os.path.join(os.path.expanduser(self.findResourcePath()), "rooms", mapFileName)
3362 if not QFile.exists(roomPath):
3363 QMessageBox.warning(self, "Error", "Failed opening stage. Make sure that the resources path is set correctly (see Edit menu) and that the proper STB file is present in the rooms directory.")
3364 return
3365
3366 self.openWrapper(roomPath)
3367
3368 def openMap(self):
3369 if self.checkDirty(): return
3370
3371 startPath = ""
3372
3373 # Get the mods folder if you can, no sense looking in rooms for explicit open
3374 settings = QSettings('settings.ini', QSettings.IniFormat)
3375 stagePath = findModsPath()
3376 if os.path.isdir(stagePath):
3377 startPath = stagePath
3378
3379 # Get the folder containing the last open file if you can
3380 recent = settings.value("RecentFiles", [])
3381 if len(recent):
3382 lastPath, file = os.path.split(recent[0])
3383 startPath = lastPath
3384
3385 target = QFileDialog.getOpenFileName(
3386 self, 'Open Map', os.path.expanduser(startPath), 'Stage Bundle (*.stb)')
3387 self.restoreEditMenu()
3388
3389 # Looks like nothing was selected
3390 if len(target[0]) == 0:
3391 return
3392
3393 self.openWrapper(target[0])
3394
3395 def openRecent(self):
3396 if self.checkDirty(): return
3397
3398 path = self.sender().data()
3399 self.restoreEditMenu()
3400
3401 self.openWrapper(path)
3402
3403 def openWrapper(self, path=None):
3404 print (path)
3405 self.path = path
3406
3407 rooms = self.open()
3408 if not rooms:
3409 QMessageBox.warning(self, "Error", "This is not a valid Afterbirth+ STB file. It may be a Rebirth STB, or it may be one of the prototype STB files accidentally included in the AB+ release.")
3410 return
3411
3412 self.roomList.list.clear()
3413 self.scene.clear()
3414 self.updateTitlebar()
3415
3416 for room in rooms:
3417 self.roomList.list.addItem(room)
3418
3419 self.clean()
3420 self.roomList.changeFilter()
3421
3422 def open(self, path=None, addToRecent=True):
3423
3424 if path==None:
3425 path = self.path
3426
3427 # Let's read the file and parse it into our list items
3428 stb = open(path, 'rb').read()
3429
3430 # Header
3431 try:
3432 header = struct.unpack_from('<4s', stb, 0)[0].decode()
3433 if header != "STB1":
3434 return
3435 except:
3436 return
3437
3438 off = 4
3439
3440 # Room count
3441 rooms = struct.unpack_from('<I', stb, off)[0]
3442 off += 4
3443 ret = []
3444
3445 for room in range(rooms):
3446
3447 # Room Type, Room Variant, Subvariant, Difficulty, Length of Room Name String
3448 roomData = struct.unpack_from('<IIIBH', stb, off)
3449 off += 0xF
3450 # print ("Room Data: {0}".format(roomData))
3451
3452 # Room Name
3453 roomName = struct.unpack_from('<{0}s'.format(roomData[4]), stb, off)[0].decode()
3454 off += roomData[4]
3455 #print ("Room Name: {0}".format(roomName))
3456
3457 # Weight, width, height, shape, number of doors, number of entities
3458 entityTable = struct.unpack_from('<fBBBBH', stb, off)
3459 off += 0xA
3460 #print ("Entity Table: {0}".format(entityTable))
3461
3462 doors = []
3463 for door in range(entityTable[-2]):
3464 # X, Y, exists
3465 d = struct.unpack_from('<hh?', stb, off)
3466 doors.append([d[0], d[1], d[2]])
3467 off += 5
3468
3469 spawns = [[[] for y in range(26)] for x in range(14)]
3470 for entity in range(entityTable[-1]):
3471 # x, y, number of entities at this position
3472 spawnLoc = struct.unpack_from('<hhB', stb, off)
3473 off += 5
3474
3475 if spawnLoc[0] < 0 or spawnLoc[1] < 0:
3476 print (spawnLoc[1], spawnLoc[0])
3477
3478 for spawn in range(spawnLoc[2]):
3479 # type, variant, subtype, weight
3480 t = struct.unpack_from('<HHHf', stb, off)
3481 spawns[spawnLoc[1]][spawnLoc[0]].append([t[0], t[1], t[2], t[3]])
3482 off += 0xA
3483
3484 r = Room(roomName, doors, spawns, roomData[0], roomData[1], roomData[2], roomData[3], entityTable[0], entityTable[1], entityTable[2], entityTable[3])
3485 ret.append(r)
3486
3487 # Update recent files
3488 if addToRecent:
3489 recent = settings.value("RecentFiles", [])
3490 while recent.count(path) > 0:
3491 recent.remove(path)
3492
3493 recent.insert(0, path)
3494 while len(recent) > 10:
3495 recent.pop()
3496
3497 settings.setValue("RecentFiles", recent)
3498 self.setupFileMenuBar()
3499
3500 return ret
3501
3502 def saveMap(self, forceNewName=False):
3503 target = self.path
3504
3505 if target == '' or forceNewName:
3506 dialogDir = target == '' and findModsPath() or os.path.dirname(target)
3507 target = QFileDialog.getSaveFileName(self, 'Save Map', dialogDir, 'Stage Bundle (*.stb)')
3508 self.restoreEditMenu()
3509
3510 if len(target) == 0:
3511 return
3512
3513 self.path = target[0]
3514 self.updateTitlebar()
3515
3516 try:
3517 self.save(self.roomList.getRooms())
3518 except:
3519 QMessageBox.warning(self, "Error", "Saving failed. Try saving to a new file instead.")
3520
3521 self.clean()
3522 self.roomList.changeFilter()
3523
3524 def saveMapAs(self):
3525 self.saveMap(True)
3526
3527 def save(self, rooms, path=None):
3528 if not path:
3529 path = self.path
3530
3531 self.storeEntityList()
3532
3533 stb = open(path, 'wb')
3534
3535 out = struct.pack('<4s', "STB1".encode())
3536 out += struct.pack('<I', len(rooms))
3537
3538 for room in rooms:
3539
3540 out += struct.pack('<IIIBH{0}sfBBB'.format(len(room.data(0x100))),
3541 room.roomType, room.roomVariant, room.roomSubvariant, room.roomDifficulty, len(room.data(0x100)),
3542 room.data(0x100).encode(), room.roomWeight, room.roomWidth, room.roomHeight, room.roomShape)
3543
3544 # Doors and Entities
3545 out += struct.pack('<BH', len(room.roomDoors), room.getSpawnCount())
3546
3547 for door in room.roomDoors:
3548 out += struct.pack('<hh?', door[0], door[1], door[2])
3549
3550 for y in enumerate(room.roomSpawns):
3551 for x in enumerate(y[1]):
3552 if len(x[1]) == 0: continue
3553
3554 out += struct.pack('<hhB', x[0], y[0], len(x[1]))
3555
3556 for entity in x[1]:
3557 out += struct.pack('<HHHf', entity[0], entity[1], entity[2], entity[3])
3558
3559 stb.write(out)
3560
3561 #@pyqtSlot()
3562 def screenshot(self):
3563 fn = QFileDialog.getSaveFileName(self, 'Choose a new filename', 'untitled.png', 'Portable Network Graphics (*.png)')[0]
3564 if fn == '': return
3565
3566 g = self.scene.grid
3567 self.scene.grid = False
3568
3569 ScreenshotImage = QImage(self.scene.sceneRect().width(), self.scene.sceneRect().height(), QImage.Format_ARGB32)
3570 ScreenshotImage.fill(Qt.transparent)
3571
3572 RenderPainter = QPainter(ScreenshotImage)
3573 self.scene.render(RenderPainter, QRectF(ScreenshotImage.rect()), self.scene.sceneRect())
3574 RenderPainter.end()
3575
3576 ScreenshotImage.save(fn, 'PNG', 50)
3577
3578 self.scene.grid = g
3579
3580 def makeTestMod(self):
3581 modFolder = findModsPath()
3582
3583 name = 'basement-renovator-test-room-loader'
3584 folder = os.path.join(modFolder, name)
3585 roomPath = os.path.join(folder, 'resources', 'rooms')
3586
3587 # delete the old files
3588 if os.path.isdir(folder):
3589 dis = os.path.join(folder, 'disable.it')
3590 if os.path.isfile(dis): os.unlink(dis)
3591
3592 for f in os.listdir(roomPath):
3593 f = os.path.join(roomPath, f)
3594 try:
3595 if os.path.isfile(f): os.unlink(f)
3596 except:
3597 pass
3598 # otherwise, make it fresh
3599 else:
3600 os.makedirs(roomPath)
3601
3602 metadata = open(os.path.join(folder, 'metadata.xml'), 'w')
3603 metadata.write("""<?xml version="1.0" encoding="UTF-8"?>
3604 <metadata>
3605 <name>%s</name>
3606 <directory>%s</directory>
3607 <description>Used by Basement Renovator to load test rooms in the starting room and basement</description>
3608 </metadata>
3609 """ % ('!!!!!!' + name, name)) # resources are loaded in reverse ascii order, so prepend with !s
3610 metadata.close()
3611
3612 return roomPath
3613
3614 #@pyqtSlot()
3615 def testMap(self):
3616 if self.roomList.selectedRoom() == None:
3617 QMessageBox.warning(self, "Error", "No room was selected to test.")
3618 return
3619
3620 # Auto-tests by adding the room to basement.
3621 modPath = findModsPath()
3622 if modPath == "": return
3623
3624 modPath = self.makeTestMod()
3625
3626 # Set the selected room to max weight
3627 self.storeEntityList(self.roomList.selectedRoom())
3628 r = self.roomList.selectedRoom()
3629 testRoom = Room(r.data(0x100), r.roomDoors, r.roomSpawns, 1, r.roomVariant, r.roomSubvariant, 1, 1000.0, r.roomWidth, r.roomHeight, r.roomShape)
3630
3631 # Make a new STB with a blank room
3632 padMe = True
3633 if testRoom.roomShape not in [2, 3, 5, 7]: # Always pad these rooms
3634 padMe = False
3635 for door in testRoom.roomDoors:
3636 if door[2] == False:
3637 padMe = True
3638
3639 # Needs a padded room
3640 newRooms = [testRoom]
3641 if padMe:
3642 newRooms.append(Room(difficulty=10, weight=0.1))
3643
3644 # Prevent accidental data loss from overwriting the file
3645 self.dirt()
3646
3647 # Check for existing files, and backup if necessary
3648 basements = [
3649 ("01.basement.stb", False),
3650 ("02.cellar.stb", False),
3651 ("03.burning basement.stb", False)
3652 ]
3653
3654 # Sanity check for saving
3655 #if not QFile.exists(os.path.join(resourcesPath, "rooms")):
3656 # os.mkdir(os.path.join(resourcesPath, "rooms"))
3657
3658 for b in basements:
3659 #path = os.path.join(resourcesPath, "rooms", b[0])
3660 path = os.path.join(modPath, b[0])
3661 #if QFile.exists():
3662 #os.replace(os.path.join(path, os.path.join(resourcesPath, "rooms", "(backup) %s" % b[0]))
3663 #b[1] = True
3664 self.save(newRooms, path)
3665
3666 # Launch Isaac
3667 installPath = findInstallPath()
3668 if installPath == '':
3669 webbrowser.open('steam://rungameid/250900')
3670 else:
3671 exePath = self.findExecutablePath()
3672 subprocess.Popen([exePath], cwd = installPath)
3673
3674 # Prompt to restore backup
3675 message = ""
3676 if padMe:
3677 message += "As you have a non-standard doors or shape, it's suggested to use the seed 'LABY RNTH' in order to spawn the room semi-regularly. You may have to reset a few times for your room to appear.\n\n"
3678 #message += 'Press "OK" when done testing to restore your original "01.basement.stb", "02.cellar.stb", and "03.burning basement.stb".'
3679 message += 'Press "OK when done testing to disable the BR mod replacing the rooms; this will not work if you have other mods that add rooms to the basement.'
3680 result = QMessageBox.information(self,#"Restore Backup",
3681 "Disable BR", message)
3682
3683 if result == QMessageBox.Ok:
3684 dis = open(os.path.normpath(modPath + '../../../disable.it'), 'w')
3685 dis.close()
3686 #for b in basements:
3687 #path = os.path.join(resourcesPath, "rooms", b[0])
3688 #if QFile.exists(path):
3689 #os.remove(path)
3690 #if b[1]:
3691 #backup = os.path.join(resourcesPath, "rooms", "(backup) %s" % b[0])
3692 #if QFile.exists(backup):
3693 #os.replace(backup), path))
3694
3695 # Extra warnings
3696 #if self.path == os.path.join(resourcesPath, "rooms", "01.basement.stb") or
3697 # self.path == os.path.join(resourcesPath, "rooms", "02.cellar.stb") or
3698 # self.path == os.path.join(resourcesPath, "rooms", "03.burning basement.stb"):
3699 # result = QMessageBox.information(self, "Warning", "When testing the basement.stb, cellar.stb, or burning basement.stb from the resources folder, it's recommended you save before quitting or risk losing the currently open STB file completely.")
3700
3701 # Why not, try catches are good practice, right? rmdir won't kill non-empty directories, so this will kill rooms dir if it's empty.
3702 #try:
3703 # if QFile.exists(os.path.join(resourcesPath, "rooms")):
3704 # os.rmdir(os.path.join(resourcesPath, "rooms"))
3705 #except:
3706 # pass
3707
3708 self.killIsaac()
3709
3710 #@pyqtSlot()
3711 # Auto-tests by replacing the starting room
3712 def testStartMap(self):
3713 if not self.roomList.selectedRoom():
3714 QMessageBox.warning(self, "Error", "No room was selected to test.")
3715 return
3716
3717 # Sanity check for 1x1 room
3718 self.storeEntityList(self.roomList.selectedRoom())
3719 testRoom = self.roomList.selectedRoom()
3720
3721 if testRoom.roomShape in [2, 7, 9] :
3722 QMessageBox.warning(self, "Error", "Room shapes 2 and 7 (Long and narrow) and 9 (L shaped with upper right corner missing) can't be tested as the Start Room.")
3723 return
3724
3725 modPath = findModsPath()
3726 if modPath == "": return
3727
3728 resourcePath = self.findResourcePath()
3729 if resourcePath == "": return
3730
3731 roomPath = os.path.join(resourcePath, "rooms", "00.special rooms.stb")
3732
3733 # Parse the special rooms, replace the spawns
3734 if not QFile.exists(roomPath):
3735 QMessageBox.warning(self, "Error", "You seem to be missing 00.special rooms.stb from resources. Please unpack your resource files.")
3736
3737 foundYou = False
3738 rooms = self.open(roomPath, False)
3739 for room in rooms:
3740 if "Start Room" in room.data(0x100):
3741 room.roomHeight = testRoom.roomHeight
3742 room.roomWidth = testRoom.roomWidth
3743 room.roomShape = testRoom.roomShape
3744 room.roomSpawns = testRoom.roomSpawns
3745 foundYou = True
3746
3747 if not foundYou:
3748 QMessageBox.warning(self, "Error", "00.special rooms.stb has been tampered with, and is no longer a valid STB file.")
3749 return
3750
3751 modPath = self.makeTestMod()
3752
3753 # Dirtify to prevent overwriting and then quitting without saving.
3754 self.dirt()
3755
3756 #path = roomPath
3757 path = os.path.join(modPath, "00.special rooms.stb")
3758
3759 # Sanity check for saving
3760 #if not QFile.exists(os.path.join(resourcesPath, "rooms")):
3761 # os.mkdir(os.path.join(resourcesPath, "rooms"))
3762
3763 # Backup, parse, find the start room, replace it, resave, restore backup
3764 #backupFlag = False
3765 #if QFile.exists(path):
3766 # os.replace(path, os.path.join(resourcesPath, "rooms", "(backup) 00.special rooms.stb"))
3767 # backupFlag = True
3768
3769 # Resave the file
3770 #self.save(rooms, os.path.join(resourcesPath, "rooms", "00.special rooms.stb"))
3771 self.save(rooms, os.path.join(modPath, "00.special rooms.stb"))
3772
3773 # Launch Isaac
3774 installPath = findInstallPath()
3775 if installPath == '':
3776 webbrowser.open('steam://rungameid/250900')
3777 else:
3778 exePath = self.findExecutablePath()
3779 subprocess.Popen([exePath], cwd = installPath)
3780
3781 # Prompt to restore backup
3782 result = QMessageBox.information(self,
3783 #"Restore Backup", "Press 'OK' when done testing to restore your original 00.special rooms.stb."
3784 'Disable BR', 'Press "OK" when done testing to disable the BR mod replacing the starting room')
3785 if result == QMessageBox.Ok:
3786 dis = open(os.path.normpath(modPath + '../../../disable.it'), 'w')
3787 dis.close()
3788 #if QFile.exists(path):
3789 # os.remove(path)
3790 #if backupFlag:
3791 # if QFile.exists(os.path.join(resourcesPath, "rooms", "(backup) 00.special rooms.stb")):
3792 # os.replace(os.path.join(resourcesPath, "rooms", "(backup) 00.special rooms.stb"), path)
3793
3794 # Extra warnings
3795 #if self.path == path:
3796 # result = QMessageBox.information(self, "Warning", "When testing the special rooms.stb from the resources folder, it's recommended you save before quitting or risk losing the currently open stb completely.")
3797
3798 # Why not, try catches are good practice, right? rmdir won't kill empty directories, so this will kill rooms dir if it's empty.
3799 #try:
3800 # if QFile.exists(os.path.join(resourcesPath, "rooms")):
3801 # os.rmdir(os.path.join(resourcesPath, "rooms"))
3802 #except:
3803 # pass
3804
3805 self.killIsaac()
3806
3807 def findExecutablePath(self):
3808 installPath = findInstallPath()
3809 if len(installPath) > 0:
3810 exeName = "isaac-ng.exe"
3811 if QFile.exists(os.path.join(installPath, "isaac-ng-rebirth.exe")):
3812 exeName = "isaac-ng-rebirth.exe"
3813 return os.path.join(installPath, exeName)
3814
3815 def findResourcePath(self):
3816
3817 resourcesPath = ''
3818
3819 if QFile.exists(settings.value('ResourceFolder')):
3820 resourcesPath = settings.value('ResourceFolder')
3821
3822 else:
3823 installPath = findInstallPath()
3824
3825 if len(installPath) != 0:
3826 resourcesPath = os.path.join(installPath, 'resources')
3827 # Fallback Resource Folder Locating
3828 else:
3829 resourcesPathOut = QFileDialog.getExistingDirectory(self, 'Please Locate The Binding of Isaac: Afterbirth+ Resources Folder')
3830 if not resourcesPathOut:
3831 QMessageBox.warning(self, "Error", "Couldn't locate resources folder and no folder was selected.")
3832 return
3833 else:
3834 resourcesPath = resourcesPathOut
3835 if resourcesPath == "":
3836 QMessageBox.warning(self, "Error", "Couldn't locate resources folder and no folder was selected.")
3837 return
3838 if not QDir(resourcesPath).exists:
3839 QMessageBox.warning(self, "Error", "Selected folder does not exist or is not a folder.")
3840 return
3841 if not QDir(os.path.join(resourcesPath, "rooms")).exists:
3842 QMessageBox.warning(self, "Error", "Could not find rooms folder in selected directory.")
3843 return
3844
3845 # Looks like nothing was selected
3846 if len(resourcesPath) == 0:
3847 QMessageBox.warning(self, "Error", "Could not find The Binding of Isaac: Afterbirth+ Resources folder (%s)" % resourcesPath)
3848 return ''
3849
3850 settings.setValue('ResourceFolder', resourcesPath)
3851
3852 # Make sure 'rooms' exists
3853 roomsdir = os.path.join(resourcesPath, "rooms")
3854 if not QDir(roomsdir).exists:
3855 os.mkdir(roomsdir)
3856 return resourcesPath
3857
3858 def killIsaac(self):
3859 for p in psutil.process_iter():
3860 try:
3861 if 'isaac' in p.name().lower():
3862 p.terminate()
3863 except:
3864 # This is totally kosher, I'm just avoiding zombies.
3865 pass
3866
3867 #@pyqtSlot()
3868 def testMapInstapreview(self):
3869 room = self.roomList.selectedRoom()
3870 if not room: return
3871
3872 installPath = findInstallPath()
3873 if len(installPath) == 0: return
3874
3875 testfile = "br_roomtest.xml"
3876 path = os.path.join(installPath, testfile)
3877 self.storeEntityList(room)
3878
3879 out = open(path, 'w')
3880
3881 # Floor type
3882 roomType = [
3883 # name, stage, stage type
3884 ('basement', 1, 0),
3885 ('cellar', 1, 1),
3886 ('burning basement', 1, 2),
3887 ('caves', 3, 0),
3888 ('catacombs', 3, 1),
3889 ('flooded caves', 3, 2),
3890 ('depths', 5, 0),
3891 ('necropolis', 5, 1),
3892 ('dank depths', 5, 2),
3893 ('womb', 7, 0),
3894 ('utero', 7, 1),
3895 ('scarred womb', 7, 2),
3896 ('sheol', 9, 0),
3897 ('cathedral', 9, 1),
3898 ('dark room', 11, 0),
3899 ('chest', 11, 1),
3900 # TODO update when repentance comes out
3901 ('downpour', 1, 2),
3902 ('mines', 3, 2),
3903 ('mausoleum', 5, 2),
3904 ('corpse', 7, 2),
3905 ('forest', 11, 2)
3906 ]
3907
3908 floorInfo = roomType[0]
3909 for t in roomType:
3910 if t[0] in mainWindow.path:
3911 floorInfo = t
3912
3913 # Room header
3914 out.write('<room type="%d" variant="%d" difficulty="%d" name="%s" weight="%g" width="%d" height="%d">\n' % (
3915 room.roomType, room.roomVariant, room.roomDifficulty, room.text(),
3916 room.roomWeight, room.roomWidth, room.roomHeight
3917 ))
3918
3919 # Doors
3920 for x, y, exists in room.roomDoors:
3921 out.write('\t<door x="%d" y="%d" exists="%s" />\n' % (x, y, "true" if exists else "false"))
3922
3923 # Spawns
3924 for y in enumerate(room.roomSpawns):
3925 for x in enumerate(y[1]):
3926 if len(x[1]) == 0: continue
3927
3928 out.write('\t<spawn x="%d" y="%d">\n' % (x[0], y[0]))
3929 for entity in x[1]:
3930 out.write('\t\t<entity type="%d" variant="%d" subtype="%d" weight="%g" />\n' % (
3931 entity[0], entity[1], entity[2], 2.0
3932 ))
3933 out.write('\t</spawn>\n')
3934
3935 out.write('</room>\n')
3936
3937 exePath = self.findExecutablePath()
3938
3939 # how to do it in rebirth/anti
3940 #subprocess.Popen([exePath, "-console",
3941 # "-room", testfile,
3942 # "-floorType", str(floorInfo[1]),
3943 # "-floorAlt", str(floorInfo[2])],
3944 # cwd = exePath
3945 #)
3946
3947 subprocess.Popen([exePath,
3948 "--load-room=%s" % testfile,
3949 "--set-stage=%d" % floorInfo[1],
3950 "--set-stage-type=%d" % floorInfo[2]],
3951 cwd = installPath
3952 )
3953
3954 # --set-stage-type=0 --set-stage=1 --load-room=superinstapreview.xml
3955
3956# Edit
3957########################
3958
3959 #@pyqtSlot()
3960 def selectAll(self):
3961
3962 path = QPainterPath()
3963 path.addRect(self.scene.sceneRect())
3964 self.scene.setSelectionArea(path)
3965
3966 #@pyqtSlot()
3967 def deSelect(self):
3968 self.scene.clearSelection()
3969
3970 #@pyqtSlot()
3971 def copy(self):
3972 self.clipboard = []
3973 for item in self.scene.selectedItems():
3974 self.clipboard.append([item.entity['X'], item.entity['Y'], item.entity['Type'], item.entity['Variant'], item.entity['Subtype'], item.entity['Weight']])
3975
3976 #@pyqtSlot()
3977 def cut(self):
3978 self.clipboard = []
3979 for item in self.scene.selectedItems():
3980 self.clipboard.append([item.entity['X'], item.entity['Y'], item.entity['Type'], item.entity['Variant'], item.entity['Subtype'], item.entity['Weight']])
3981 item.remove()
3982
3983 #@pyqtSlot()
3984 def paste(self):
3985 if not self.clipboard: return
3986
3987 self.scene.clearSelection()
3988 for item in self.clipboard:
3989 i = Entity(*item)
3990 self.scene.addItem(i)
3991
3992 self.dirt()
3993
3994# Miscellaneous
3995########################
3996
3997 #@pyqtSlot()
3998 def switchGrid(self):
3999 """Handle toggling of the grid being showed"""
4000
4001 self.scene.grid = not self.scene.grid
4002 settings.setValue('GridEnabled', self.scene.grid)
4003
4004 if self.scene.grid:
4005 self.wa.setText("Hide Grid")
4006 else:
4007 self.wa.setText("Show Grid")
4008
4009 self.scene.update()
4010
4011 #@pyqtSlot()
4012 def switchInfo(self):
4013 """Handle toggling of the grid being showed"""
4014
4015 self.editor.statusBar = not self.editor.statusBar
4016 settings.setValue('StatusEnabled', self.editor.statusBar)
4017
4018 if self.editor.statusBar:
4019 self.we.setText("Hide Info Bar")
4020 else:
4021 self.we.setText("Show Info Bar")
4022
4023 self.scene.update()
4024
4025 #@pyqtSlot()
4026 def switchBitFont(self):
4027 """Handle toggling of the bitfont for entity counting"""
4028
4029 self.scene.bitText = not self.scene.bitText
4030 settings.setValue('BitfontEnabled', self.scene.bitText)
4031
4032 if self.scene.bitText:
4033 self.wd.setText("Use Aliased Counter")
4034 else:
4035 self.wd.setText("Use Bitfont Counter")
4036
4037 self.scene.update()
4038
4039 #@pyqtSlot()
4040 def showPainter(self):
4041 if self.EntityPaletteDock.isVisible():
4042 self.EntityPaletteDock.hide()
4043 else:
4044 self.EntityPaletteDock.show()
4045
4046 self.updateDockVisibility()
4047
4048 #@pyqtSlot()
4049 def showRoomList(self):
4050 if self.roomListDock.isVisible():
4051 self.roomListDock.hide()
4052 else:
4053 self.roomListDock.show()
4054
4055 self.updateDockVisibility()
4056
4057 #@pyqtSlot()
4058 def updateDockVisibility(self):
4059
4060 if self.EntityPaletteDock.isVisible():
4061 self.wb.setText('Hide Entity Painter')
4062 else:
4063 self.wb.setText('Show Entity Painter')
4064
4065 if self.roomListDock.isVisible():
4066 self.wc.setText('Hide Room List')
4067 else:
4068 self.wc.setText('Show Room List')
4069
4070 #@pyqtSlot()
4071 def resetWindowDefaults(self):
4072 self.restoreState(self.resetWindow["state"], 0)
4073 self.restoreGeometry(self.resetWindow["geometry"])
4074
4075# Help
4076########################
4077
4078 #@pyqtSlot(bool)
4079 def aboutDialog(self):
4080 caption = "About the Basement Renovator"
4081
4082 text = "<big><b>Basement Renovator</b></big><br><br> The Basement Renovator Editor is an editor for custom rooms, for use with the Binding of Isaac Afterbirth. In order to use it, you must have unpacked the .stb files from Binding of Isaac Afterbirth.<br><br> The Basement Renovator was programmed by Tempus (u/Chronometrics).<br><br> Find the source on <a href='https://github.com/Tempus/Basement-Renovator'>github</a>."
4083
4084 msg = QMessageBox.about(mainWindow, caption, text)
4085
4086 #@pyqtSlot(bool)
4087 def goToHelp(self):
4088 QDesktopServices().openUrl(QUrl('http://www.reddit.com/r/themoddingofisaac'))
4089
4090
4091if __name__ == '__main__':
4092
4093 import sys
4094
4095 # Application
4096 app = QApplication(sys.argv)
4097 app.setWindowIcon(QIcon('resources/UI/BasementRenovator.png'))
4098
4099 cmdParser = QCommandLineParser()
4100 cmdParser.setApplicationDescription('Basement Renovator is a room editor for The Binding of Isaac: Afterbirth[+]')
4101 cmdParser.addHelpOption()
4102
4103 cmdParser.process(app)
4104
4105 settings = QSettings('settings.ini', QSettings.IniFormat)
4106
4107 # XML Globals
4108 entityXML = getEntityXML()
4109 if settings.value('DisableMods') != '1':
4110 loadMods(settings.value('ModAutogen') == '1', findInstallPath(), settings.value('ResourceFolder') or '')
4111
4112 mainWindow = MainWindow()
4113 recent = settings.value("RecentFiles", [])
4114 if len(recent) > 0 and os.path.exists(recent[0]):
4115 mainWindow.openWrapper(recent[0])
4116
4117 mainWindow.show()
4118
4119 sys.exit(app.exec())