-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.gd
More file actions
327 lines (252 loc) · 9.64 KB
/
Copy pathmain.gd
File metadata and controls
327 lines (252 loc) · 9.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
extends Node
# Preload classes (like Java imports)
const WrongResultGenerator = preload("res://wrong_result_generator.gd")
const QuestionBankClass = preload("res://generators/question_bank.gd")
const QuestionGeneratorClass = preload("res://generators/question_generator.gd")
const WeightedQuestionGeneratorClass = preload("res://generators/weighted_question_generator.gd")
@export var mob_scene: PackedScene
@export var result_view_scene: PackedScene
@export var fire_effect_scene: PackedScene
@export var floating_text_scene: PackedScene
# Game state
var score: int
var health: int
var pets: Array[String] = [] # Collected pets act as extra shields
const MAX_HEALTH: int = 3
const CORRECT_ANSWER_POINTS: int = 10
const SPAWN_MOBS_ON_SCORE_THRESHOLD: int = 300
const MOBS_GROUP: String = "mobs"
# Pet types in order of earning (emoji representations)
const PET_TYPES: Array[String] = ["🐱", "🐶", "🐦", "🐟", "🐢", "🐰", "🦊", "🐻"]
# Generator for wrong answers
var wrong_generator = WrongResultGenerator.new()
# Question generator (adaptive learning - wrong answers appear more often)
var question_generator: QuestionGeneratorClass
var current_question_index: int = -1
# Array to hold all result views (1 correct + multiple wrong)
var wrong_results_views: Array[Node2D] = []
var correct_result_view: Node2D
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# Initialize question generator with adaptive learning
var bank = QuestionBankClass.multiplication_table_5()
question_generator = WeightedQuestionGeneratorClass.new(bank)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
pass
func game_over() -> void:
$MobTimer.stop()
$HUD.show_game_over()
$DeathSound.play()
func new_game():
score = 0
health = MAX_HEALTH
pets.clear()
$Player.start($StartPosition.position)
$StartTimer.start()
$LevelTimer.start()
$MobTimer.wait_time = 2
$HUD.update_score(score)
$HUD.update_health(health)
$HUD.update_pets(pets)
$HUD.show_message("Get Ready")
$Music.play()
show_result()
func _on_score_timer_timeout() -> void:
score += 1
$HUD.update_score(score)
func _on_start_timer_timeout() -> void:
pass
func show_result() -> void:
$MobTimer.stop()
_clear_result_views()
# Move player to center of screen for fair positioning
_move_player_to_center()
# Use the adaptive question generator
current_question_index = question_generator.select_question()
var question_text = question_generator.get_question_text(current_question_index)
var correct_answer = question_generator.get_correct_answer(current_question_index)
# Update the question label display
$Result/QuestionLabel.text = question_text
$Result/QuestionLabel.position = Vector2(200, 10)
var wrong_answers = wrong_generator.generate(correct_answer, 3)
var all_answers: Array[int] = [correct_answer]
all_answers.append_array(wrong_answers)
all_answers.shuffle()
# Generate random positions for answers
var positions = _generate_random_answer_positions(all_answers.size())
# Spawn a ResultView for each answer at random positions
for i in range(all_answers.size()):
var answer = all_answers[i]
var is_correct = (answer == correct_answer)
var result_view = _spawn_result_view(answer, positions[i])
if (is_correct):
correct_result_view = result_view
correct_result_view.hit.connect(proper_result_hit)
else:
wrong_results_views.append(result_view)
# Bind the result_view to the callback so we know which one was hit
result_view.hit.connect(incorrect_result_hit.bind(result_view))
# spawn mobs if appropriate level
if score >= SPAWN_MOBS_ON_SCORE_THRESHOLD :
$MobTimer.wait_time = 5
$MobTimer.start()
## Moves player to the center of the screen
func _move_player_to_center() -> void:
var screen_size = get_viewport().get_visible_rect().size
var center = screen_size / 2
$Player.position = center
## Generates random positions for answers, keeping them spread apart
## and away from the center (where player spawns)
func _generate_random_answer_positions(count: int) -> Array[Vector2]:
var positions: Array[Vector2] = []
var screen_size = get_viewport().get_visible_rect().size
var center = screen_size / 2
# Minimum distance from center (player spawn) and between answers
var min_distance_from_center = 150.0
var min_distance_between_answers = 100.0
# Margin from screen edges
var margin = 50.0
var attempts = 0
var max_attempts = 100
while positions.size() < count and attempts < max_attempts:
# Generate random position within screen bounds (with margin)
var random_pos = Vector2(
randf_range(margin, screen_size.x - margin),
randf_range(margin, screen_size.y - margin)
)
# Check if position is far enough from center
if random_pos.distance_to(center) < min_distance_from_center:
attempts += 1
continue
# Check if position is far enough from other answers
var valid = true
for existing_pos in positions:
if random_pos.distance_to(existing_pos) < min_distance_between_answers:
valid = false
break
if valid:
positions.append(random_pos)
attempts += 1
# Fallback: if we couldn't generate enough positions, use path-based positioning
if positions.size() < count:
print("Warning: Using fallback positioning")
var result_view_location = $ResultPath/PathFollow2D
for i in range(positions.size(), count):
result_view_location.progress_ratio = randf()
positions.append(result_view_location.position)
return positions
func incorrect_result_hit(_wrong_result_view: Node2D) -> void:
print("❌ WRONG answer hit!")
# Tell generator this question was answered wrong (will appear more often)
question_generator.on_wrong_answer(current_question_index)
$Player.flash_damage()
# Pets act as shields - lose pet first, then health
if pets.size() > 0:
var lost_pet = pets.pop_back()
_spawn_floating_text("-" + lost_pet, $Player.global_position, Color.ORANGE)
$HUD.update_pets(pets)
$HUD.flash_pets_down()
else:
health -= 1
$HUD.update_health(health)
$HUD.flash_health_down()
if health <= 0:
game_over()
## Spawns a fire effect at the given position
func _spawn_fire_effect(pos: Vector2) -> void:
if fire_effect_scene == null:
print("Warning: fire_effect_scene not assigned!")
return
var fire = fire_effect_scene.instantiate()
fire.position = pos
add_child(fire)
func proper_result_hit() -> void:
print("✅ CORRECT answer hit!")
# Tell generator this question was answered correctly (will appear less often)
question_generator.on_correct_answer(current_question_index)
# Clear all mobs with disappear animation
_clear_all_mobs()
# Spawn floating "+10" at correct answer position
var center_offset = Vector2(32, 36)
_spawn_floating_text("+" + str(CORRECT_ANSWER_POINTS),
correct_result_view.global_position + center_offset, Color.GOLD)
score += CORRECT_ANSWER_POINTS
$HUD.update_score(score)
var tween = $Player.flash_success()
# Every 50 points: earn health or pet
if score % 50 == 0:
if health < MAX_HEALTH:
# Health not full - earn health
health += 1
$HUD.update_health(health)
_spawn_floating_text("+1 ❤️", $Player.global_position, Color.RED)
var tween_health_up = $HUD.flash_health_up()
await tween_health_up.finished
else:
# Health full - earn a pet!
var pet_index = pets.size() % PET_TYPES.size()
var new_pet = PET_TYPES[pet_index]
pets.append(new_pet)
$HUD.update_pets(pets)
_spawn_floating_text("+" + new_pet, $Player.global_position, Color.CYAN)
var tween_pets = $HUD.flash_pets_up()
await tween_pets.finished
show_result()
else:
await tween.finished
show_result()
## Spawns floating text at the given position
func _spawn_floating_text(text: String, pos: Vector2, color: Color = Color.WHITE) -> void:
if floating_text_scene == null:
print("Warning: floating_text_scene not assigned!")
return
var floating = floating_text_scene.instantiate()
floating.position = pos
add_child(floating)
floating.set_text(text)
floating.set_color(color)
## Spawns a single ResultView at the given position
func _spawn_result_view(answer: int, pos: Vector2) -> Node2D:
var result_view = result_view_scene.instantiate()
result_view.position = pos
result_view.set_label(answer)
call_deferred("add_child", result_view)
return result_view
## Removes all current result views from the scene
func _clear_result_views() -> void:
# Note: queue_free() automatically cleans up signal connections
for view in wrong_results_views:
if is_instance_valid(view):
view.queue_free()
wrong_results_views.clear()
if is_instance_valid(correct_result_view):
correct_result_view.queue_free()
correct_result_view = null
func _on_mob_timer_timeout() -> void:
$MobTimer.wait_time = 2
var mob = mob_scene.instantiate()
# Choose a random location on Path2D.
var mob_spawn_location = $MobPath/MobSpawnLocation
mob_spawn_location.progress_ratio = randf()
# Set the mob's position to the random location.
mob.position = mob_spawn_location.position
# Set the mob's direction perpendicular to the path direction.
var direction = mob_spawn_location.rotation + PI / 2
# Add some randomness to the direction.
direction += randf_range(-PI / 4, PI / 4)
mob.rotation = direction
# Choose the velocity for the mob.
var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
mob.linear_velocity = velocity.rotated(direction)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
mob.add_to_group(MOBS_GROUP)
func _on_level_timer_timeout() -> void:
pass
## Clears all mobs from the scene with disappear animation
func _clear_all_mobs() -> void:
$MobTimer.stop()
get_tree().call_group(MOBS_GROUP, "disappear")
func hit_by_mob() -> void:
incorrect_result_hit(null)