source: trunk/game/scripts/hud.py @ 287

Revision 287, 21.0 KB checked in by orlandov, 10 years ago (diff)

Ticket #74: Patch by or1andov, Integrate dialogue GUI and engine into game.

It should now be possible to engage in a conversation with NPC characters by
selecting talk from their context menu. fixes[s:trac, t:74]

Known Issues:


  • the GUI is terrible. instead of a dialog window, we could use a better looking background image and fixed position widgets
  • requires PyYAML to be installed sudo apt-get install python-yaml easy_install PyYAML
  • we have to create a simple state model for the PC can remember tasks
  • need to make NPC's be able to remember things - need a model in NPCs for this
  • no way currently to give or take items from npc's; again, more model work. this could be done by adding another callback similar to start_quest. may be necessary for npc's to have an inventory for this
  • The dialogue engine is not hooked up to specific NPC's... it currently always loads dialogue/sample.yaml - need to have the dialogue looked up from some XML attribute
  • dialogue is not broken off if the PC moves away from the NPC
  • npc image is hardcoded, it should use the image specified in the yaml file
  • Property svn:eol-style set to native
Line 
1#!/usr/bin/python
2
3#   This file is part of PARPG.
4
5#   PARPG is free software: you can redistribute it and/or modify
6#   it under the terms of the GNU General Public License as published by
7#   the Free Software Foundation, either version 3 of the License, or
8#   (at your option) any later version.
9
10#   PARPG is distributed in the hope that it will be useful,
11#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#   GNU General Public License for more details.
14
15#   You should have received a copy of the GNU General Public License
16#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
17
18import shutil, fife, pychan
19import pychan.widgets as widgets
20from pychan.tools import callbackWithArguments as cbwa
21from scripts.parpgfilebrowser import PARPGFileBrowser
22from scripts.context_menu import ContextMenu
23from scripts import inventory
24from scripts.popups import ExaminePopup, ContainerGUI
25from scripts.dialoguegui import DialogueGUI
26
27class Hud(object):
28    """Main Hud class"""
29    def __init__(self, engine, settings, inv_model, callbacks):
30        """Initialise the instance.
31           @type engine: fife.Engine
32           @param engine: An instance of the fife engine
33           @type settings: settings.Setting
34           @param settings: The settings data
35           @type inv_model: dict
36           @type callbacks: dict
37           @param callbacks: a dict of callbacks
38               saveGame: called when the user clicks on Save
39               loadGame: called when the user clicks on Load
40               quitGame: called when the user clicks on Quit
41           @return: None"""
42        pychan.init(engine, debug = True)
43
44        # TODO: perhaps this should not be hard-coded here
45        self.hud = pychan.loadXML("gui/hud.xml")
46        self.engine = engine
47        self.settings = settings
48
49        inv_callbacks = {
50            'refreshReadyImages': self.refreshReadyImages,
51            'toggleInventoryButton': self.toggleInventoryButton,
52        }
53
54        self.inventory = inventory.Inventory(self.engine, inv_model, inv_callbacks)
55        self.refreshReadyImages()
56
57        self.saveGameCallback = callbacks['saveGame']
58        self.loadGameCallback = callbacks['loadGame']
59        self.quitCallback     = callbacks['quitGame']
60
61        self.box_container = None
62        self.examine_box = None
63
64        self.actionsBox = self.hud.findChild(name="actionsBox")
65        self.actionsText = []
66        self.menu_displayed = False
67        self.initializeHud()
68        self.initializeMainMenu()
69        self.initializeContextMenu()
70        self.initializeOptionsMenu()
71        self.initializeHelpMenu()
72        self.initializeEvents()
73
74    def initializeHud(self):
75        """Initialize and show the main HUD
76           @return: None"""
77        self.events_to_map = {"menuButton":self.displayMenu,}
78        self.hud.mapEvents(self.events_to_map) 
79        # set HUD size accoriding to screen size
80        screen_width = int(self.settings.readSetting('ScreenWidth'))
81        self.hud.findChild(name="mainHudWindow").size = (screen_width, 65)
82        self.hud.findChild(name="inventoryButton").position = (screen_width-59, 7)
83        # add ready slots
84        ready1 = self.hud.findChild(name='hudReady1')
85        ready2 = self.hud.findChild(name='hudReady2')
86        ready3 = self.hud.findChild(name='hudReady3')
87        ready4 = self.hud.findChild(name='hudReady4')
88        actions_scroll_area = self.hud.findChild(name='actionsScrollArea')
89        if (screen_width <=800) :
90            gap = 0
91        else :
92            gap = 40
93        # annoying code that is both essential and boring to enter
94        ready1.position = (160+gap, 7)
95        ready2.position = (220+gap, 7)
96        ready3.position = (screen_width-180-gap, 7)
97        ready4.position = (screen_width-120-gap, 7)
98        actions_scroll_area.position = (280+gap, 5)
99        actions_width = screen_width - 470 - 2*gap
100
101        # and finally add an actions box
102        self.hud.findChild(name="actionsBox").min_size = (actions_width, 0)
103        actions_scroll_area.min_size = (actions_width, 55)
104        actions_scroll_area.max_size = (actions_width, 55)
105        # now it should be OK to display it all
106        self.hud.show()
107
108    def refreshActionsBox(self):
109        """Refresh the actions box so that it displays the contents of
110           self.actionsText
111           @return: None"""
112        self.actionsBox.items = self.actionsText
113
114    def addAction(self, action):
115        """Add an action to the actions box.
116           @type action: string
117           @param action: The text that you want to display in the actions box
118           @return: None"""
119        self.actionsText.insert(0, action)
120        self.refreshActionsBox()
121
122    def showHUD(self):
123        """Show the HUD.
124           @return: None"""
125        self.hud.show()
126
127    def hideHUD(self):
128        """Hide the HUD.
129           @return: None"""
130        self.hud.hide()
131
132    def initializeContextMenu(self):
133        """Initialize the Context Menu
134           @return: None"""
135        self.context_menu = ContextMenu (self.engine, [], (0,0))
136        self.context_menu.hide()
137
138    def showContextMenu(self, data, pos):
139        """Display the Context Menu with data at pos
140           @type data: list
141           @param data: data to pass to context menu
142           @type pos: tuple
143           @param pos: tuple of x and y coordinates
144           @return: None"""
145        self.context_menu = ContextMenu(self.engine, data, pos)
146
147    def hideContextMenu(self):
148        """Hides the context menu
149           @return: None"""
150        self.context_menu.hide()
151
152    def initializeMainMenu(self):
153        """Initalize the main menu.
154           @return: None"""
155        self.main_menu = pychan.loadXML("gui/hud_main_menu.xml")
156        self.menu_events = {"resumeButton":self.hideMenu, 
157                            "optionsButton":self.displayOptions,
158                            "helpButton":self.displayHelp}
159        self.main_menu.mapEvents(self.menu_events)
160
161    def displayMenu(self):
162        """Displays the main in-game menu.
163           @return: None"""
164        if (self.menu_displayed == False):
165            self.main_menu.show()
166            self.menu_displayed = True
167        elif (self.menu_displayed == True):
168            self.hideMenu()
169
170    def hideMenu(self):
171        """Hides the main in-game menu.
172           @return: None"""
173        self.main_menu.hide()
174        self.menu_displayed = False
175
176
177    def initializeHelpMenu(self):
178        """Initialize the help menu
179           @return: None"""
180        self.help_dialog = pychan.loadXML("gui/help.xml")
181        help_events = {"closeButton":self.help_dialog.hide}
182        self.help_dialog.mapEvents(help_events)
183        main_help_text = u"Welcome to Post-Apocalyptic RPG or PARPG![br][br]"\
184        "This game is still in development, so please expect for there to be bugs"\
185        " and[br]feel free to tell us about them at http://www.forums.parpg.net.[br]"\
186        "This game uses a \"Point 'N' Click\" interface, which means that to move around,[br]"\
187        "just click where you would like to go and your character will move there.[br]"\
188        "PARPG also utilizes a context menu. To access this, just right click "\
189        "anywhere[br]on the screen and a menu will come up. This menu will change"\
190        " depending on[br]what you have clicked on, hence it's name \"context menu\".[br][br]"
191       
192        k_text = u" Keybindings" 
193        k_text+="[br] A : Add a test action to the actions display"
194        k_text+="[br] I : Toggle the inventory screen"
195        k_text+="[br] F5 : Take a screenshot"
196        k_text+="[br]      (saves to <parpg>/screenshots/)"
197        k_text+="[br] Q : Quit the game"
198        self.help_dialog.distributeInitialData({
199                "MainHelpText":main_help_text,
200                "KeybindText":k_text
201                })
202
203    def displayHelp(self):
204        """Display the help screen.
205           @return: None"""
206        self.help_dialog.show()
207
208    def initializeOptionsMenu(self):
209        """Initalize the options menu.
210           @return: None"""
211        self.options_menu = pychan.loadXML("gui/hud_options.xml")
212        self.options_events = {"applyButton":self.applyOptions,
213                               "closeButton":self.options_menu.hide,
214                               "defaultsButton":self.setToDefaults,
215                               "InitialVolumeSlider":self.updateVolumeText}
216       
217        settings = self.engine.getSettings()
218        current_fullscreen = settings.isFullScreen()
219        settings.setFullScreen(True)
220        availableResolutions = settings.getPossibleResolutions()
221        self.Resolutions = [str(x[0])+'x'+str(x[1]) for x in availableResolutions];
222        settings.setFullScreen(current_fullscreen)
223        self.RenderBackends = ['OpenGL', 'SDL']
224        self.renderNumber = 0
225        if (str(self.settings.readSetting('RenderBackend')) == "SDL"):
226            self.renderNumber = 1
227        initialVolume = float(self.settings.readSetting('InitialVolume'))
228        initialVolumeText = str('Initial Volume: %.0f%s' %
229                                (int(initialVolume*10), "%"))
230        self.options_menu.distributeInitialData({
231                'ResolutionBox': self.Resolutions,
232                'RenderBox': self.RenderBackends,
233                'InitialVolumeLabel' : initialVolumeText
234                })
235
236        sFullscreen = self.settings.readSetting(name="FullScreen")
237        sSounds = self.settings.readSetting(name="PlaySounds")
238        sRender = self.renderNumber
239        sVolume = initialVolume
240
241        screen_width = self.settings.readSetting(name="ScreenWidth")
242        screen_height = self.settings.readSetting(name="ScreenHeight")
243        indexRes = str(screen_width + 'x' + screen_height)
244        try:
245            sResolution = self.Resolutions.index(indexRes)
246            resolutionInList = True
247        except:
248            resolutionInList = False
249
250        dataToDistribute = {
251                'FullscreenBox':int(sFullscreen), 
252                'SoundsBox':int(sSounds),
253                'RenderBox': sRender,
254                'InitialVolumeSlider':sVolume
255                }
256
257        if (resolutionInList == True):
258            dataToDistribute['ResolutionBox'] = sResolution
259
260        self.options_menu.distributeData(dataToDistribute)
261
262        self.options_menu.mapEvents(self.options_events)
263
264    def saveGame(self):
265        """ Called when the user wants to save the game.
266            @return: None"""
267        save_browser = PARPGFileBrowser(self.engine,
268                                   self.saveGameCallback,
269                                   savefile=True,
270                                   guixmlpath="gui/savebrowser.xml",
271                                   extensions = ('.dat'))
272        save_browser.showBrowser()
273           
274    def newGame(self):
275        """Called when user request to start a new game.
276           @return: None"""
277        print 'new game'
278
279    def loadGame(self):
280        """ Called when the user wants to load a game.
281            @return: None"""
282        load_browser = PARPGFileBrowser(self.engine,
283                                   self.loadGameCallback,
284                                   savefile=False,
285                                   guixmlpath='gui/loadbrowser.xml',
286                                   extensions=('.dat'))
287        load_browser.showBrowser()
288
289    def quitGame(self):
290        """Called when user requests to quit game.
291           @return: None"""
292
293        window = pychan.widgets.Window(title=unicode("Quit?"))
294
295        hbox = pychan.widgets.HBox()
296        are_you_sure = "Are you sure you want to quit?"
297        label = pychan.widgets.Label(text=unicode(are_you_sure))
298        yes_button = pychan.widgets.Button(name="yes_button", 
299                                           text=unicode("Yes"))
300        no_button = pychan.widgets.Button(name="no_button",
301                                          text=unicode("No"))
302
303        window.addChild(label)
304        hbox.addChild(yes_button)
305        hbox.addChild(no_button)
306        window.addChild(hbox)
307
308        events_to_map = { "yes_button": self.quitCallback,
309                          "no_button":  window.hide }
310       
311        window.mapEvents(events_to_map)
312        window.show()
313
314    def toggleInventoryButton(self):
315        """Manually toggles the inventory button.
316           @return: None"""
317        button = self.hud.findChild(name="inventoryButton")
318        if button.toggled == 0:
319            button.toggled = 1
320        else:
321            button.toggled = 0
322
323    def toggleInventory(self, toggleImage=True):
324        """Display's the inventory screen
325           @return: None"""
326
327        self.inventory.toggleInventory(toggleImage)
328
329    def refreshReadyImages(self):
330        """Make the Ready slot images on the HUD be the same as those
331           on the inventory
332           @return: None"""
333        self.setImages(self.hud.findChild(name="hudReady1"),
334                       self.inventory.getImage("Ready1").up_image)
335
336        self.setImages(self.hud.findChild(name="hudReady2"),
337                       self.inventory.getImage("Ready2").up_image)
338
339        self.setImages(self.hud.findChild(name="hudReady3"),
340                       self.inventory.getImage("Ready3").up_image)
341
342        self.setImages(self.hud.findChild(name="hudReady4"),
343                       self.inventory.getImage("Ready4").up_image)
344
345    def setImages(self, widget, image):
346        """Set the up, down, and hover images of an Imagebutton.
347           @type widget: pychan.widget
348           @param widget: widget to set
349           @type image: string
350           @param image: image to use
351           @return: None"""
352        widget.up_image = image
353        widget.down_image = image
354        widget.hover_image = image
355
356    def initializeEvents(self):
357        """Intialize Hud events
358           @return: None"""
359        events_to_map = {}
360
361        # when we click the toggle button don't change the image
362        events_to_map["inventoryButton"] = cbwa(self.toggleInventory, False)
363        events_to_map["saveButton"] = self.saveGame
364        events_to_map["loadButton"] = self.loadGame
365
366        hud_ready_buttons = ["hudReady1", "hudReady2", "hudReady3", "hudReady4"]
367
368        for item in hud_ready_buttons:
369            events_to_map[item] = cbwa(self.readyAction, item)
370
371        self.hud.mapEvents(events_to_map)
372
373        menu_events = {}
374        menu_events["newButton"] = self.newGame
375        menu_events["quitButton"] = self.quitGame
376        menu_events["saveButton"] = self.saveGame
377        menu_events["loadButton"] = self.loadGame
378        self.main_menu.mapEvents(menu_events)
379
380    def updateVolumeText(self):
381        """
382        Update the initial volume label to reflect the value of the slider
383        """
384        volume = float(self.options_menu.collectData("InitialVolumeSlider"))
385        volume_label = self.options_menu.findChild(name="InitialVolumeLabel")
386        volume_label.text = unicode("Initial Volume: %.0f%s" %
387                                    (int(volume*10), "%"))
388
389    def requireRestartDialog(self):
390        """Show a dialog asking the user to restart PARPG in order for their
391           changes to take effect.
392           @return: None"""
393        require_restart_dialog = pychan.loadXML('gui/hud_require_restart.xml')
394        require_restart_dialog.mapEvents({'okButton':require_restart_dialog.hide})
395        require_restart_dialog.show()
396
397    def applyOptions(self):
398        """Apply the current options.
399           @return: None"""
400        # At first no restart is required
401        self.requireRestart = False
402
403        # get the current values of each setting from the options menu
404        enable_fullscreen = self.options_menu.collectData('FullscreenBox')
405        enable_sound = self.options_menu.collectData('SoundsBox')
406        screen_resolution = self.options_menu.collectData('ResolutionBox')
407        partition = self.Resolutions[screen_resolution].partition('x')
408        screen_width = partition[0]
409        screen_height = partition[2]
410        render_backend = self.options_menu.collectData('RenderBox')
411        initial_volume = self.options_menu.collectData('InitialVolumeSlider')
412        initial_volume = "%.1f" % initial_volume
413
414        # get the options that are being used right now from settings.xml
415        sFullscreen = self.settings.readSetting('FullScreen')
416        sSound = self.settings.readSetting('PlaySounds')
417        sRender = self.settings.readSetting('RenderBackend')
418        sVolume = self.settings.readSetting('InitialVolume')
419
420        sScreenHeight = self.settings.readSetting('ScreenHeight')
421        sScreenWidth = self.settings.readSetting('ScreenWidth')
422        sResolution = sScreenWidth + 'x' + sScreenHeight
423
424        # On each:
425        # - Check to see whether the option within the xml matches the
426        #   option within the options menu
427        # - If they do not match, set the option within the xml to
428        #   to be what is within the options menu
429        # - Require a restart
430
431        if (int(enable_fullscreen) != int(sFullscreen)):
432            self.setOption('FullScreen', int(enable_fullscreen))
433            self.requireRestart = True
434           
435        if (int(enable_sound) != int(sSound)):
436            self.setOption('PlaySounds', int(enable_sound))
437            self.requireRestart = True
438
439        if (screen_resolution != sResolution):
440            self.setOption('ScreenWidth', int(screen_width))
441            self.setOption('ScreenHeight', int(screen_height))
442            self.requireRestart = True
443
444        # Convert the number from the list of render backends to
445        # the string that FIFE wants for its settings.xml
446        if (render_backend == 0):
447            render_backend = 'OpenGL'
448        else:
449            render_backend = 'SDL'
450
451        if (render_backend != str(sRender)):
452            self.setOption('RenderBackend', render_backend)
453            self.requireRestart = True
454
455        if (initial_volume != float(sVolume)):
456            self.setOption('InitialVolume', initial_volume)
457            self.requireRestart = True
458       
459        # Write all the settings to settings.xml
460        self.settings.tree.write('settings.xml')
461       
462        # If the changes require a restart, popup the dialog telling
463        # the user to do so
464        if (self.requireRestart):
465            self.requireRestartDialog()
466        # Once we are done, we close the options menu
467        self.options_menu.hide()
468
469    def setOption(self, name, value):
470        """Set an option within the xml.
471           @type name: string
472           @param name: The name of the option within the xml
473           @type value: any
474           @param value: The value that the option 'name' should be set to
475           @return: None"""
476        element = self.settings.root_element.find(name)
477        if(element != None):
478            if(value != element.text):
479                element.text = str(value)
480        else:
481            print 'Setting,', name, 'does not exist!'
482
483    def setToDefaults(self):
484        """Reset all the options to the options in settings-dist.xml.
485           @return: None"""
486        shutil.copyfile('settings-dist.xml', 'settings.xml')
487        self.requireRestartDialog()
488        self.options_menu.hide()
489
490    def displayOptions(self):
491        """Display the options menu.
492           @return: None"""
493        self.options_menu.show()
494   
495    def readyAction(self, ready_button):
496        """ Called when the user selects a ready button from the HUD """
497        text = "Used the item from %s" % ready_button
498        self.addAction(text)
499       
500    def createBoxGUI(self, title):
501        """Creates a window to display the contents of a box
502           @type title: string
503           @param title: The title for the window
504           @return: None"""
505        if self.box_container:
506            # if it has already been created, just show it
507            self.box_container.showContainer()
508        else:
509            # otherwise create it then show it
510            data = ["dagger01", "empty", "empty", "empty", "empty",
511                    "empty", "empty", "empty", "empty"]
512            self.box_container = ContainerGUI(self.engine, unicode(title), data)
513            def close_and_delete():
514                self.hideContainer()
515            events = {'takeAllButton':close_and_delete,
516                      'closeButton':close_and_delete}
517            self.box_container.container_gui.mapEvents(events)
518            self.box_container.showContainer()
519
520    def hideContainer(self):
521        """Hide the container box
522           @return: None"""
523        if self.box_container:
524            self.box_container.hideContainer()
525
526    def createExamineBox(self, title, desc):
527        """Create an examine box. It displays some textual description of an
528           object
529           @type title: string
530           @param title: The title of the examine box
531           @type desc: string
532           @param desc: The main body of the examine box
533           @return: None"""
534
535        if self.examine_box:
536            self.examine_box.closePopUp()
537        self.examine_box = ExaminePopup(self.engine, title, desc)
538        self.examine_box.showPopUp()
539
540    def showDialogue(self, npc):
541        """Show the NPC dialogue window
542           @type npc: ???
543           @param npc: the npc that we are having a dialogue with"""
544        dialogue = DialogueGUI(npc)
545        dialogue.initiateDialogue()
Note: See TracBrowser for help on using the repository browser.