7
7
package com.almasb.fxgl.quest
8
8
9
9
import com.almasb.fxgl.core.collection.PropertyMap
10
+
import com.almasb.fxgl.logging.Logger
10
11
import javafx.beans.binding.Bindings
11
12
import javafx.beans.property.*
13
+
import javafx.collections.FXCollections
14
+
import javafx.collections.ObservableList
12
15
import javafx.util.Duration
13
16
import java.util.concurrent.Callable
14
17
15
18
/**
16
-
* TODO: allow adding / removing objectives after construction.
17
-
*
18
19
* A single quest.
19
-
* A quest can have multiple objectives but at least 1.
20
+
* A quest can have multiple objectives but to start, it needs at least 1.
20
21
* A quest is always in one of four states [QuestState].
22
+
* Valid transitions:
23
+
* NOT_STARTED -> ACTIVE
24
+
* ACTIVE -> COMPLETED
25
+
* ACTIVE -> FAILED
26
+
* FAILED -> ACTIVE
21
27
*
22
28
* @author Almas Baimagambetov (almaslvl@gmail.com)
23
29
*/
24
-
class Quest(val name: String,
25
-
val objectives: List<QuestObjective>) {
30
+
class Quest(val name: String) {
31
+
32
+
private val log = Logger.get(javaClass)
33
+
34
+
private val objectives = FXCollections.observableArrayList<QuestObjective>()
35
+
private val objectivesReadOnly = FXCollections.unmodifiableObservableList(objectives)
36
+
37
+
/**
38
+
* @return read-only copy of the quest's objectives
39
+
*/
40
+
fun objectivesProperty(): ObservableList<QuestObjective> = objectivesReadOnly
26
41
27
42
private val stateProp = ReadOnlyObjectWrapper(QuestState.NOT_STARTED)
28
43
@@ -31,12 +46,52 @@ class Quest(val name: String,
31
46
32
47
fun stateProperty(): ReadOnlyObjectProperty<QuestState> = stateProp.readOnlyProperty
33
48
34
-
// TODO: check transitions between states
49
+
/**
50
+
* @return true if any of the states apart from NOT_STARTED
51
+
*/
52
+
val hasStarted: Boolean
53
+
get() = state != QuestState.NOT_STARTED
54
+
55
+
@JvmOverloads fun addIntObjective(desc: String, varName: String, varValue: Int, duration: Duration = Duration.ZERO): QuestObjective {
56
+
return IntQuestObjective(desc, varName, varValue, duration).also { addObjective(it) }
57
+
}
58
+
59
+
@JvmOverloads fun addBooleanObjective(desc: String, varName: String, varValue: Boolean, duration: Duration = Duration.ZERO): QuestObjective {
60
+
return BooleanQuestObjective(desc, varName, varValue, duration).also { addObjective(it) }
61
+
}
62
+
63
+
private fun addObjective(objective: QuestObjective) {
64
+
objectives += objective
65
+
66
+
if (hasStarted)
67
+
rebindStateToObjectives()
68
+
}
69
+
70
+
fun removeObjective(objective: QuestObjective) {
71
+
objectives -= objective
72
+
73
+
if (hasStarted)
74
+
rebindStateToObjectives()
75
+
}
76
+
77
+
/**
78
+
* Can only be called from NOT_STARTED state.
79
+
*/
35
80
internal fun start() {
36
-
stateProp.value = QuestState.ACTIVE
81
+
if (objectives.isEmpty()) {
82
+
log.warning("Cannot start quest $name because it has no objectives")
83
+
return
84
+
}
85
+
86
+
if (hasStarted) {
87
+
log.warning("Cannot start quest $name because it has already been started")
88
+
return
89
+
}
37
90
38
-
require(objectives.isNotEmpty()) { "Quest must have at least 1 objective" }
91
+
rebindStateToObjectives()
92
+
}
39
93
94
+
private fun rebindStateToObjectives() {
40
95
val failedBinding = objectives.map { it.stateProperty() }
41
96
.foldRight(Bindings.createBooleanBinding(Callable { false })) { state, binding ->
42
97
state.isEqualTo(QuestState.FAILED).or(binding)
@@ -57,7 +112,10 @@ class Quest(val name: String,
57
112
/**
58
113
* A single quest objective.
59
114
*
60
-
* TODO: allow user to manually fail an objective.
115
+
* Valid transitions:
116
+
* ACTIVE -> COMPLETED
117
+
* ACTIVE -> FAILED
118
+
* FAILED -> ACTIVE
61
119
*/
62
120
sealed class QuestObjective
63
121
@JvmOverloads
@@ -68,13 +126,14 @@ constructor(
68
126
*/
69
127
val description: String,
70
128
129
+
// TODO:
71
130
/**
72
131
* How much time is given to complete this objective.
73
132
* Default: 0 - unlimited.
74
133
*/
75
134
val expireDuration: Duration = Duration.ZERO) {
76
135
77
-
private val stateProp = ReadOnlyObjectWrapper(QuestState.NOT_STARTED)
136
+
private val stateProp = ReadOnlyObjectWrapper(QuestState.ACTIVE)
78
137
79
138
val state: QuestState
80
139
get() = stateProp.get()
@@ -94,6 +153,39 @@ constructor(
94
153
successProp.addListener(successListener)
95
154
}
96
155
156
+
fun complete() {
157
+
if (state != QuestState.ACTIVE) {
158
+
return
159
+
}
160
+
161
+
unbind()
162
+
successProp.value = true
163
+
}
164
+
165
+
fun fail() {
166
+
if (state != QuestState.ACTIVE) {
167
+
return
168
+
}
169
+
170
+
unbind()
171
+
successProp.value = false
172
+
clean()
173
+
stateProp.value = QuestState.FAILED
174
+
}
175
+
176
+
/**
177
+
* Transition from FAILED -> ACTIVE.
178
+
*/
179
+
fun reactivate(vars: PropertyMap) {
180
+
if (state != QuestState.FAILED) {
181
+
return
182
+
}
183
+
184
+
stateProp.value = QuestState.ACTIVE
185
+
successProp.addListener(successListener)
186
+
bindTo(vars)
187
+
}
188
+
97
189
abstract fun bindTo(vars: PropertyMap)
98
190
99
191
internal fun unbind() {
@@ -102,27 +194,10 @@ constructor(
102
194
103
195
private fun clean() {
104
196
successProp.removeListener(successListener)
105
-
//failBinding?.expire()
106
197
}
107
-
108
-
// TODO:
109
-
// private fun setState(state: QuestState) {
110
-
// require(state != QuestState.ACTIVE) { "Quest objective cannot be reactivated!" }
111
-
//
112
-
// clean()
113
-
// this.state.set(state)
114
-
// }
115
-
//
116
-
// private var failBinding: com.almasb.fxgl.time.TimerAction? = null
117
-
//
118
-
// init {
119
-
// if (expireDuration !== Duration.ZERO) {
120
-
// //failBinding = FXGL.getMasterTimer().runOnceAfter({ setState(QuestState.FAILED) }, expireDuration)
121
-
// }
122
-
// }
123
198
}
124
199
125
-
class IntQuestObjective
200
+
private class IntQuestObjective
126
201
@JvmOverloads constructor(
127
202
/**
128
203
* Text that tells the player how to achieve this objective.
@@ -155,7 +230,7 @@ class IntQuestObjective
155
230
}
156
231
}
157
232
158
-
class BooleanQuestObjective
233
+
private class BooleanQuestObjective
159
234
@JvmOverloads constructor(
160
235
/**
161
236
* Text that tells the player how to achieve this objective.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4