Skip to content
This repository was archived by the owner on May 17, 2023. It is now read-only.

Commit 4449cec

Browse files
authored
Added a quests GUI. (#188)
1 parent 0918a61 commit 4449cec

File tree

7 files changed

+220
-37
lines changed

7 files changed

+220
-37
lines changed

assets/game-spec.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@
241241
{
242242
"name": "exit-button-selected",
243243
"path": "assets/gui/exit-button-selected.png"
244+
},
245+
{
246+
"name": "quests-button",
247+
"path": "assets/gui/quests-button.png"
244248
}
245249
],
246250
"buttons": [
@@ -250,17 +254,27 @@
250254
"name": "resume-button",
251255
"center": [
252256
400,
253-
290
257+
370
254258
],
255259
"action": "engine.builtin.resume_game"
256260
},
261+
{
262+
"selected_image_asset": "quests-button",
263+
"unselected_image_asset": "quests-button",
264+
"name": "quests-button",
265+
"center": [
266+
400,
267+
260
268+
],
269+
"action": "game.gui.quests.show_quests_gui"
270+
},
257271
{
258272
"selected_image_asset": "exit-button-selected",
259273
"unselected_image_asset": "exit-button",
260274
"name": "exit-button",
261275
"center": [
262276
400,
263-
180
277+
150
264278
],
265279
"action": "engine.builtin.exit_game"
266280
}

assets/gui/quests-button.png

13.5 KB
Loading

engine/builtin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ def resume_game(api: scripts.GameAPI) -> None:
3535
api.start_game()
3636

3737

38-
def exit_game() -> None:
38+
def exit_game(api: scripts.GameAPI) -> None:
3939
"""Exits the game."""
40+
# pylint: disable=unused-argument
4041
sys.exit(0)
4142

4243

engine/gui/base.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import (
2+
Optional,
3+
)
4+
5+
from arcade import gui
6+
7+
from engine import scripts
8+
9+
10+
class GUI:
11+
"""A base GUI that implements the GUI protocol."""
12+
13+
api: Optional[scripts.GameAPI]
14+
manager: Optional[gui.UIManager]
15+
16+
def __init__(self):
17+
self.api = None
18+
self.manager = None
19+
20+
def set_api(self, api: scripts.GameAPI) -> None:
21+
"""Sets the game API for this GUI."""
22+
self.api = api
23+
24+
def set_manager(self, manager: gui.UIManager) -> None:
25+
"""Sets the UI manager for this GUI."""
26+
self.manager = manager
27+
28+
self.manager.clear()
29+
self._reset_widgets()
30+
31+
def draw(self) -> None:
32+
"""Renders the GUI."""
33+
34+
def _reset_widgets(self) -> None:
35+
"""Override to handle widget creation."""

engine/gui/conversation.py

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import functools
23
from typing import (
34
List,
45
Optional,
@@ -8,6 +9,7 @@
89
from arcade import gui
910

1011
from engine import scripts
12+
from engine.gui import base
1113

1214
CONVERSATION_PADDING = 10
1315
CONVERSATION_Y = 200
@@ -63,48 +65,21 @@ def title(self) -> Optional[str]:
6365
"""Gets the title to display at the top of the conversation GUI."""
6466

6567

66-
class _ChoiceButton(gui.UIFlatButton):
67-
"""Class to allow us to attach data to a UI Button."""
68-
69-
index: int
70-
71-
def __init__(self, index, *args, **kwargs):
72-
super().__init__(*args, **kwargs)
73-
self.index = index
74-
75-
76-
class GUI:
68+
class GUI(base.GUI):
7769
"""A GUI that navigates a conversation in the game."""
7870

7971
root: Conversation
8072
current: Conversation
8173

82-
api: Optional[scripts.GameAPI]
83-
manager: Optional[gui.UIManager]
84-
8574
def __init__(self, root_conversation: Conversation):
75+
super().__init__()
8676
self.root = root_conversation
8777
self.current = self.root
8878
self.api = None
8979
self.manager = None
9080

91-
def set_api(self, api: scripts.GameAPI) -> None:
92-
"""Sets the game API for this GUI."""
93-
self.api = api
94-
95-
def set_manager(self, manager: gui.UIManager) -> None:
96-
"""Sets the UI manager for this GUI."""
97-
self.manager = manager
98-
99-
self.manager.clear()
100-
self._reset_widgets()
101-
102-
def draw(self) -> None:
103-
"""Renders the conversation."""
104-
105-
def _choice_picked(self, event: gui.UIOnClickEvent):
106-
index = event.source.index
107-
choice = self.current.choices[index]
81+
def _choice_picked(self, choice: Choice, _event: gui.UIOnClickEvent):
82+
assert self.api is not None
10883

10984
if choice.link is not None:
11085
self.current = choice.link
@@ -137,8 +112,7 @@ def _reset_widgets(self) -> None:
137112
)
138113

139114
for i, choice in enumerate(self.current.choices):
140-
button = _ChoiceButton(
141-
index=i,
115+
button = gui.UIFlatButton(
142116
x=CHOICES_OFFSET,
143117
y=(
144118
(len(self.current.choices) - i) * CHOICE_HEIGHT
@@ -147,7 +121,10 @@ def _reset_widgets(self) -> None:
147121
height=CHOICE_HEIGHT,
148122
text=choice.text,
149123
)
150-
button.on_click = self._choice_picked # type: ignore
124+
button.set_handler(
125+
"on_click",
126+
functools.partial(self._choice_picked, choice),
127+
)
151128
self.manager.add(button, index=0)
152129

153130
exit_button = gui.UIFlatButton(

game/gui/quests.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import functools
2+
from typing import (
3+
List,
4+
Optional,
5+
Tuple,
6+
)
7+
8+
from arcade import (
9+
csscolor,
10+
gui,
11+
)
12+
13+
from engine import (
14+
core,
15+
scripts,
16+
)
17+
from engine.gui import base
18+
from game.quests import (
19+
base as quests,
20+
db as quest_db,
21+
)
22+
23+
GUI_PADDING = 5
24+
25+
26+
class GUI(base.GUI):
27+
"""A GUI that shows all the quests for the player."""
28+
29+
_quest_title: Optional[gui.UILabel]
30+
_quest_description: Optional[gui.UITextArea]
31+
32+
def __init__(self):
33+
super().__init__()
34+
self._quest_title = None
35+
self._quest_description = None
36+
37+
def _select_quest(
38+
self,
39+
quest: quests.Quest,
40+
quest_state: quests.QuestState,
41+
_event: gui.UIEvent,
42+
) -> None:
43+
assert self._quest_title is not None
44+
assert self._quest_description is not None
45+
46+
self._quest_title.text = quest.title
47+
self._quest_title.fit_content()
48+
self._quest_description.text = quest.steps[quest_state.current_step].description
49+
50+
def _reset_widgets(self) -> None:
51+
assert self.api is not None
52+
assert self.manager is not None
53+
54+
self.manager.clear()
55+
# Dirty hack to get the UI manager to reset correctly.
56+
self.manager.children[0] = []
57+
58+
quest_states: List[Tuple[quests.Quest, quests.QuestState]] = [
59+
(quest_db.get(quest_id), quest_state)
60+
for quest_id, quest_state in self.api.player_data["quests"].items()
61+
]
62+
63+
self._quest_title = gui.UILabel(size_hint=(1.0, 0.1), text="")
64+
self._quest_description = gui.UITextArea(size_hint=(1.0, 0.9), text="")
65+
66+
quest_details_pane = gui.UIBoxLayout(
67+
vertical=True,
68+
size_hint=(0.5, 1.0),
69+
children=(self._quest_title, self._quest_description),
70+
space_between=GUI_PADDING,
71+
)
72+
resume_button = gui.UIFlatButton(text="Resume")
73+
74+
@resume_button.event("on_click")
75+
def _on_resume_button(_event: gui.UIEvent) -> None:
76+
assert self.api is not None
77+
self.api.start_game()
78+
79+
quest_buttons: List[gui.UIWidget] = []
80+
81+
for quest, quest_state in quest_states:
82+
button = gui.UIFlatButton(
83+
size_hint=(1.0, None),
84+
height=30,
85+
text=quest.title,
86+
)
87+
button.set_handler(
88+
"on_click",
89+
functools.partial(
90+
self._select_quest,
91+
quest=quest,
92+
quest_state=quest_state,
93+
),
94+
)
95+
quest_buttons.append(button)
96+
97+
if not quest_buttons:
98+
quest_buttons.append(gui.UILabel(italic=True, text="No quests yet"))
99+
100+
main_layout = gui.UIBoxLayout(
101+
vertical=True,
102+
space_between=GUI_PADDING,
103+
children=(
104+
gui.UIBoxLayout(
105+
vertical=False,
106+
size_hint=(1.0, 0.8),
107+
space_between=GUI_PADDING,
108+
children=(
109+
gui.UIBoxLayout(
110+
vertical=True,
111+
children=[
112+
gui.UILabel(text="Quests"),
113+
]
114+
+ quest_buttons,
115+
space_between=GUI_PADDING,
116+
size_hint=(0.5, 1.0),
117+
)
118+
.with_space_around(
119+
GUI_PADDING, GUI_PADDING, GUI_PADDING, GUI_PADDING
120+
)
121+
.with_border(color=csscolor.WHITE),
122+
quest_details_pane.with_space_around(
123+
GUI_PADDING, GUI_PADDING, GUI_PADDING, GUI_PADDING
124+
).with_border(color=csscolor.WHITE),
125+
),
126+
),
127+
gui.UIBoxLayout(size_hint=(1.0, 0.2), children=(resume_button,)),
128+
),
129+
)
130+
131+
anchor = gui.UIAnchorWidget(
132+
width=core.SCREEN_WIDTH,
133+
height=core.SCREEN_HEIGHT,
134+
child=gui.UIWrapper(child=main_layout, size_hint=(1.0, 1.0)),
135+
)
136+
137+
self.manager.add(anchor, index=0)
138+
139+
140+
def show_quests_gui(api: scripts.GameAPI) -> None:
141+
"""Shows the quests GUI."""
142+
api.show_gui(GUI())

game/quests/db.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""This module tracks a set of all the quests in the game."""
2+
3+
from typing import (
4+
Dict,
5+
)
6+
7+
from game.quests import base
8+
9+
_quests: Dict[str, base.Quest] = {}
10+
11+
12+
def get(name: str) -> base.Quest:
13+
"""Gets a quest by name."""
14+
return _quests[name]

0 commit comments

Comments
 (0)