game.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. ########################################################################
  2. # Hello Worlds - Libre 3D RPG game.
  3. # Copyright (C) 2020 CYBERDEViL
  4. #
  5. # This file is part of Hello Worlds.
  6. #
  7. # Hello Worlds is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Hello Worlds is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from LUIFrame import LUIFrame
  22. from LUILabel import LUILabel
  23. from LUIProgressbar import LUIProgressbar
  24. from LUILayouts import LUIHorizontalLayout
  25. from LUIVerticalLayout import LUIVerticalLayout
  26. from LUISprite import LUISprite
  27. from LUIObject import LUIObject
  28. from LUIInitialState import LUIInitialState
  29. from core.models import SpellItemModel, SpellStatus
  30. class UI_ActionMessages(LUIObject):
  31. def __init__(self, **kwargs):
  32. LUIObject.__init__(self, center_horizontal=True)
  33. self._layout = LUIVerticalLayout(parent=self)
  34. self._previousMessage = ""
  35. self._count = 0
  36. LUIInitialState.init(self, kwargs)
  37. def print(self, message, lifeTime=2):
  38. if message == self._previousMessage: return
  39. self._previousMessage = message
  40. self._count += 1
  41. label = LUILabel(message, parent=self._layout.cell(), margin=0, font_size=20, center_horizontal=True)
  42. taskMgr.doMethodLater(
  43. lifeTime,
  44. self.remove,
  45. 'removeText',
  46. extraArgs=[label],
  47. )
  48. def remove(self, label):
  49. label.parent = None
  50. self._count -= 1
  51. if not self._count: self._previousMessage = ""
  52. def close(self): self.parent = None
  53. class UI_ItemInfo(LUIFrame):
  54. def __init__(self, **kwargs):
  55. LUIFrame.__init__(self, style=LUIFrame.FS_raised, **kwargs)
  56. self.margin = 0
  57. self._model = None
  58. verticalLayout = LUIVerticalLayout(parent=self)
  59. horizontalLayout = LUIHorizontalLayout(parent=verticalLayout.cell())
  60. ## Labels
  61. leftLayout = LUIVerticalLayout(parent=horizontalLayout.cell())
  62. LUILabel("Name: ", parent=leftLayout.cell(), margin=0)
  63. LUILabel("Points: ", parent=leftLayout.cell(), margin=0)
  64. LUILabel("Energy: ", parent=leftLayout.cell(), margin=0)
  65. LUILabel("Casting time: ", parent=leftLayout.cell(), margin=0)
  66. LUILabel("Cooling time: ", parent=leftLayout.cell(), margin=0)
  67. LUILabel("Range: ", parent=leftLayout.cell(), margin=0)
  68. ## Values
  69. rightLayout = LUIVerticalLayout(parent=horizontalLayout.cell())
  70. self._nameValue = LUILabel("", parent=rightLayout.cell(), margin=0, color=(0.2, 0.8, 0.2))
  71. self._pointsValue = LUILabel("", parent=rightLayout.cell(), margin=0, color=(0.8, 0.8, 0.0))
  72. self._energyValue = LUILabel("", parent=rightLayout.cell(), margin=0, color=(0.8, 0.8, 0.0))
  73. self._castTimeValue = LUILabel("", parent=rightLayout.cell(), margin=0)
  74. self._coolTimeValue = LUILabel("", parent=rightLayout.cell(), margin=0)
  75. self._rangeValue = LUILabel("", parent=rightLayout.cell(), margin=0)
  76. self._descValue = LUILabel("", parent=verticalLayout.cell(), margin=0)
  77. self.hide()
  78. def setModel(self, model):
  79. """
  80. @param model:
  81. @type model: core.model.SpellItemModel
  82. """
  83. self._model = model
  84. if model:
  85. self._nameValue.set_text(model.name)
  86. self._pointsValue.set_text("{0}".format(model.data.impactPoints))
  87. self._energyValue.set_text("{0}".format(model.data.energyCost))
  88. self._castTimeValue.set_text("{0}s".format(model.data.castTime))
  89. self._coolTimeValue.set_text("{0}s".format(model.data.coolDown))
  90. self._rangeValue.set_text("{0}min {1}max".format(model.data.rangeStart, model.data.rangeEnd))
  91. if model.description:
  92. self._descValue.set_text(str(model.description))
  93. self._descValue.show()
  94. else:
  95. self._descValue.hide()
  96. else: self._modelRemoved()
  97. def close(self): self.parent = None
  98. def _modelRemoved(self):
  99. self._descLabel.set_text(self._descLabelFmt)
  100. class UI_DragItem(LUIObject):
  101. icons = {
  102. 'unknown' : "assets/icons/unknown.png",
  103. 'empty' : "assets/icons/overlay_0.png"
  104. }
  105. def __init__(self, *args, model=None, **kwargs):
  106. LUIObject.__init__(self, *args)
  107. self.width = 48
  108. self.height = 48
  109. self._icon = LUISprite(self, self.icons['empty'], width=self.width, height=self.height)
  110. self.set_pos(100,100)
  111. self.setModel(model)
  112. def setModel(self, model=None):
  113. if model:
  114. self._icon.set_texture(model.iconPath, resize=False)
  115. class UI_Item(LUIObject):
  116. ## TODO use assets path
  117. icons = {
  118. 'unknown' : "assets/icons/unknown.png",
  119. 'empty' : "assets/icons/overlay_0.png"
  120. }
  121. ## TODO use assets path
  122. overlays = {
  123. 'highlite' : "assets/icons/overlay_1.png",
  124. 'disabled' : "assets/icons/overlay_2_disabled.png"
  125. }
  126. def __init__(self, *args, **kwargs):
  127. # Without solid=True it doesn't receive mouse-events
  128. LUIObject.__init__(self, *args, solid=True)
  129. self.width = 48
  130. self.height = 48
  131. self._model = None
  132. self._isDragging = False
  133. self._dragStartPos = None
  134. self._backupParent = self.parent
  135. self._dragItem = None
  136. self.__enabled = False
  137. self._backupModel = None
  138. self._allowedDropTypes = [SpellItemModel]
  139. self._icon = LUISprite(self, self.icons['empty'], width=self.width, height=self.height)
  140. self._label = LUILabel("", parent=self, margin=0, left=0, bottom=0)
  141. self._overlay = None
  142. LUIInitialState.init(self, kwargs)
  143. @property
  144. def dropTypes(self): return self._allowedDropTypes
  145. def setEnabled(self, state=True):
  146. if state and not self._model: return False
  147. self.__enabled = state
  148. return True
  149. @property
  150. def enabled(self): return self.__enabled
  151. def mousePos(self):
  152. if base.mouseWatcherNode.hasMouse():
  153. mpos = base.mouseWatcherNode.getMouse()
  154. mousePos = (mpos.getX() * base.getAspectRatio(), mpos.getY())
  155. screenWidth = base.win.getProperties().getXSize()
  156. screenHeight = base.win.getProperties().getYSize()
  157. ratio = base.getAspectRatio()
  158. # https://stackoverflow.com/questions/929103/convert-a-number-range-to-another-range-maintaining-ratio
  159. left = (((mousePos[0] - -ratio) * (screenWidth - 0)) / (ratio - -ratio)) + 0
  160. top = (((mousePos[1] - 1) * (screenHeight - 0)) / (-1 - 1)) + 0
  161. return (left, top)
  162. return(0,0)
  163. def dragDistance(self):
  164. if self._dragStartPos:
  165. mouseX, mouseY = self.mousePos()
  166. dragX, dragY = self._dragStartPos
  167. return abs((dragX + dragY) - (mouseX + mouseY))
  168. return 0
  169. def _dragWatcher(self, event):
  170. if not self._dragItem and self.dragDistance() > 6:
  171. self._removeOverlay()
  172. self._dragItem = UI_DragItem(base.luiRegion.root, model=self.model)
  173. base.uiManager.dragModel = self.model
  174. self.unbind('mousemove')
  175. taskMgr.add(self._dragTask, 'dragWatcher')
  176. def _dragTask(self, task):
  177. self._dragItem.set_pos(self.mousePos())
  178. return task.cont
  179. def _stopDrag(self):
  180. if self._dragItem:
  181. taskMgr.remove('dragWatcher')
  182. self._dragItem.parent = None
  183. self._dragItem = None
  184. base.uiManager.dragModel = None
  185. def on_mouseover(self, event):
  186. ## Replace model with the model behind the drag.
  187. if base.uiManager.dragModel:
  188. if type(base.uiManager.dragModel) in self.dropTypes:
  189. self._backupModel = self._model
  190. self.setModel(base.uiManager.dragModel)
  191. return
  192. ## Show information box
  193. if self._model:
  194. pos = self.get_abs_pos()
  195. pos -= (-60,80)
  196. base.uiManager.spellInfoFrame.setModel(self._model)
  197. base.uiManager.spellInfoFrame.set_pos(pos)
  198. base.uiManager.spellInfoFrame.show()
  199. def on_mouseout(self, event):
  200. if self._model:
  201. base.uiManager.spellInfoFrame.hide()
  202. if base.uiManager.dragModel:
  203. if type(base.uiManager.dragModel) in self.dropTypes:
  204. self.setModel(self._backupModel)
  205. self._backupModel = None
  206. def on_mousedown(self, event=None):
  207. if self.enabled:
  208. if not base.uiManager.dragModel:
  209. self._dragStartPos = self.mousePos()
  210. self.bind('mousemove', self._dragWatcher) # uncomment to enable drag 'n drop (that isn't complete yet)
  211. self._addOverlay()
  212. def on_mouseup(self, event=None):
  213. self._stopDrag()
  214. self._removeOverlay()
  215. self._dragStartPos = None
  216. def keyDown(self):
  217. if self.enabled:
  218. self._addOverlay()
  219. self.trigger_event("click")
  220. def keyUp(self):
  221. if self.enabled: self._removeOverlay()
  222. def setOverlay(self, overlay='highlite'):
  223. self._removeOverlay()
  224. self._addOverlay(overlay=overlay)
  225. def _addOverlay(self, overlay='highlite'):
  226. if not self._overlay:
  227. overlayPath = self.overlays[overlay]
  228. self._overlay = LUISprite(self, overlayPath, width=self.width, height=self.height)
  229. def _removeOverlay(self):
  230. if self._overlay:
  231. self._overlay.parent = None
  232. self._overlay = None
  233. @property
  234. def model(self): return self._model
  235. def setModel(self, model):
  236. self._model = model
  237. if model:
  238. self._icon.set_texture(model.iconPath, resize=False)
  239. self._label.set_text(model.name)
  240. self.setEnabled(True)
  241. else:
  242. self._modelRemoved()
  243. self.setEnabled(False)
  244. def _modelRemoved(self):
  245. self._icon.set_texture(self.icons['empty'], resize=False)
  246. self._label.set_text("")
  247. class UI_SpellItem(UI_Item):
  248. def __init__(self, *args, **kwargs):
  249. UI_Item.__init__(self, *args, **kwargs)
  250. self.castBar = None
  251. def _spellStatusChanged(self, status):
  252. """
  253. @param status: SpellStatus.*
  254. @type status: int
  255. """
  256. if status == SpellStatus.none or status == SpellStatus.done:
  257. self._removeOverlay()
  258. self.setEnabled(True)
  259. elif status == SpellStatus.casting:
  260. self.setEnabled(False)
  261. self.setOverlay('disabled')
  262. self.castBar = UI_CastingBar(self.model.data.castTime, parent=base.luiRegion.root)
  263. self.castBar.start()
  264. elif status == SpellStatus.cooling:
  265. self.setEnabled(False)
  266. self.setOverlay('disabled')
  267. elif status == SpellStatus.cancelled:
  268. if self.castBar: self.castBar.cancel()
  269. def setModel(self, model):
  270. """
  271. @param model:
  272. @type model: core.models.SpellItemModel
  273. """
  274. if self._model:
  275. self._model.statusChanged.disconnect(self._spellStatusChanged)
  276. UI_Item.setModel(self, model)
  277. if model:
  278. self._spellStatusChanged(model.status)
  279. model.statusChanged.connect(self._spellStatusChanged)
  280. class UI_ProgressBar(LUIObject):
  281. def __init__(self, **kwargs):
  282. LUIObject.__init__(self)
  283. self.width = 200
  284. self.height = 10
  285. self._layout = LUIVerticalLayout(parent=self)
  286. self._bar = LUIProgressbar(parent=self._layout.cell(), width=200, label_fmt="casting")
  287. LUIInitialState.init(self, kwargs)
  288. def __bool__(self):
  289. return self.is_visible()
  290. def close(self, event=None): self.parent = None
  291. class UI_CastingBar(UI_ProgressBar):
  292. def __init__(self, castingTime, **kwargs):
  293. UI_ProgressBar.__init__(self, **kwargs)
  294. self.center_horizontal = True
  295. self.bottom = 125
  296. self._cancelled = False
  297. self.castingTime = castingTime
  298. self._bar.set_max(castingTime)
  299. def start(self):
  300. self._bar.set_value(0)
  301. # TODO investigate where this weird 0.25 margin comes from.
  302. # it's only visual
  303. self._bar.set_max(self.castingTime - 0.25)
  304. ## Start a task that updates the progress-bar over time.
  305. taskMgr.doMethodLater(
  306. 0.1,
  307. self.updateProgress,
  308. 'player_castBar'
  309. )
  310. def cancel(self):
  311. self._cancelled = True
  312. def updateProgress(self, task):
  313. """ Task method that updates the progress-bar over time.
  314. """
  315. if not self._cancelled and self._bar.get_value() < self._bar.get_max():
  316. self._bar.set_value(self._bar.get_value() + 0.1)
  317. return task.again
  318. self.close()
  319. return task.done
  320. class UI_SpellBar(LUIFrame):
  321. def __init__(self, **kwargs):
  322. LUIFrame.__init__(self, style=LUIFrame.FS_sunken, **kwargs)
  323. self.height = 68
  324. self.margin = 0
  325. screenHeight = base.win.getProperties().getYSize()
  326. self.top = screenHeight - 68 - 3
  327. self.center_horizontal = True
  328. self._layout = LUIHorizontalLayout(parent=self)
  329. self._layout.spacing = 3
  330. self.maxItems = 12
  331. self._items = []
  332. ## List with spellId's; should be same ordered as self._items!
  333. for i in range(0, self.maxItems):
  334. item = UI_SpellItem(self._layout.cell())
  335. self._items.append(item)
  336. item.bind('click', self._spellClicked)
  337. # Assing shortcut keys 0 - 9
  338. keyMap = [
  339. ["1", 0],
  340. ["2", 1],
  341. ["3", 2],
  342. ["4", 3],
  343. ["5", 4],
  344. ["6", 5],
  345. ["7", 6],
  346. ["8", 7],
  347. ["9", 8],
  348. ["0", 9],
  349. ]
  350. for key, index in keyMap:
  351. base.accept(key, self._items[index].keyDown)
  352. base.accept('{0}-up'.format(key), self._items[index].keyUp)
  353. base.accept('WINDOW_RESIZED', self.reposition)
  354. def clear(self):
  355. for item in self._items: item.setModel(None)
  356. def characterChanged(self):
  357. self.clear()
  358. # TODO
  359. if base.world.playerController.character:
  360. count = 0
  361. for spell in base.world.playerController.character.characterData.spells:
  362. model = SpellItemModel(spell)
  363. self._items[count].setModel(model)
  364. if count == self.maxItems: break
  365. count += 1
  366. def _spellClicked(self, event):
  367. index = self._items.index(event.sender)
  368. if self._items[index].model:
  369. selectedSpawnId = base.world.npcsManager.selectedNpcModel.selectedId
  370. base.world.playerController.character.startCast(
  371. self._items[index].model.data.id,
  372. selectedSpawnId
  373. )
  374. def reposition(self):
  375. self.top = base.win.getProperties().getYSize() - self.height - 10
  376. def close(self): self.parent = None
  377. class UI_PlayerInfoBox(LUIFrame):
  378. def __init__(self, parent=None, top=10, left=10):
  379. if not parent: parent = base.luiRegion.root
  380. LUIFrame.__init__(self, parent=parent, style=LUIFrame.FS_sunken, width=175, height=80, margin=0, top=top, left=left)
  381. self._layout = LUIVerticalLayout(self)
  382. self.nameLabel = LUILabel(parent=self._layout.cell(), text="Name: ")
  383. self.healthBar = LUIProgressbar(parent=self._layout.cell(), width=145, label_fmt="Health: {value}/{max}")
  384. self.energyBar = LUIProgressbar(parent=self._layout.cell(), width=145, label_fmt="Energy: {value}/{max}")
  385. self._currentCharacterData = None
  386. def characterChanged(self):
  387. if self._currentCharacterData:
  388. self._currentCharacterData.stats.health.valueChanged.disconnect(self._healthChanged)
  389. self._currentCharacterData.stats.energy.valueChanged.disconnect(self._energyChanged)
  390. self._currentCharacterData = None
  391. if base.world.playerController.character:
  392. self._currentCharacterData = base.world.playerController.character.characterData
  393. self.nameLabel.set_text(self._currentCharacterData.name)
  394. self.healthBar.set_max(self._currentCharacterData.stats.health.max)
  395. self.healthBar.set_value(self._currentCharacterData.stats.health.value)
  396. self.energyBar.set_max(self._currentCharacterData.stats.energy.max)
  397. self.energyBar.set_value(self._currentCharacterData.stats.energy.value)
  398. self._currentCharacterData.stats.health.valueChanged.connect(self._healthChanged)
  399. self._currentCharacterData.stats.energy.valueChanged.connect(self._energyChanged)
  400. def _healthChanged(self, value): self.healthBar.set_value(value)
  401. def _energyChanged(self, value): self.energyBar.set_value(value)
  402. def close(self): self.parent = None
  403. class UI_CreatureInfoBox:
  404. def __init__(self, parent=None, top=100, left=10):
  405. if not parent: parent = base.luiRegion.root
  406. self._currentSpawnId = -1
  407. self._container = LUIFrame(parent=parent, width=175, height=55, style=LUIFrame.FS_sunken, margin=0, top=top, left=left)
  408. self.nameLabel = LUILabel(parent=self._container, text="Name: ", top=0)
  409. self.healthBar = LUIProgressbar(parent=self._container, width=145, top=20, label_fmt="Health: {value}/{max}")
  410. base.world.npcsManager.selectedNpcModel.changed.connect(self._selectionChanged)
  411. self.hide()
  412. def hasMouse(self): return self._container.hasMouse()
  413. def _selectionChanged(self, spawnId):
  414. # Disconnect previously set connections
  415. if self._currentSpawnId != -1 and self._currentSpawnId != spawnId:
  416. oldNpc = base.world.npcsManager.getSpawn(self._currentSpawnId) # creaturesManager.Creature
  417. if oldNpc:
  418. oldNpc.characterData.stats.health.valueChanged.disconnect(self._healthChanged)
  419. oldNpc.removeSelectPlane()
  420. # Hide if selected id is 0 or smaller
  421. if spawnId < 1:
  422. self.hide()
  423. self._currentSpawnId = -1
  424. return
  425. if self._currentSpawnId != spawnId:
  426. self.show()
  427. npc = base.world.npcsManager.getSpawn(spawnId) # creaturesManager.Creature
  428. npc.addSelectPlane()
  429. # Name
  430. self.nameLabel.text = "{0} (id: {1} lvl: {2})".format(npc.characterData.name, npc.characterData.spawnData.id, npc.characterData.stats.level.value)
  431. # Health
  432. self.healthBar.set_max(npc.characterData.stats.health.max)
  433. self.healthBar.set_value(npc.characterData.stats.health.value)
  434. self._currentSpawnId = spawnId
  435. npc.characterData.stats.health.valueChanged.connect(self._healthChanged)
  436. def _healthChanged(self, value):
  437. npc = base.world.npcsManager.getSpawn(self._currentSpawnId)
  438. if npc: self.healthBar.set_value(npc.characterData.stats.health.value)
  439. def close(self):
  440. self._container.parent = None
  441. self._container = None
  442. def hide(self):
  443. self._container.hide()
  444. def show(self):
  445. self._container.show()
  446. def isVisible(self): return self._container.visible