File size: 15,158 Bytes
a3e5f70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
from settings import * 
from sprites import MonsterSprite, MonsterNameSprite, MonsterLevelSprite, MonsterStatsSprite, MonsterOutlineSprite, AttackSprite, TimedSprite
from groups import BattleSprites
from game_data import ATTACK_DATA
from support import draw_bar
from timer import Timer
from random import choice

class Battle:
	# main
	def __init__(self, player_monsters, opponent_monsters, monster_frames, bg_surf, fonts, end_battle, character, sounds):
		# general
		self.display_surface = pygame.display.get_surface()
		self.bg_surf = bg_surf
		self.monster_frames = monster_frames
		self.fonts = fonts
		self.monster_data = {'player': player_monsters, 'opponent': opponent_monsters}
		self.battle_over = False
		self.end_battle = end_battle
		self.character = character
		self.sounds = sounds

		# timers 
		self.timers = {
			'opponent delay': Timer(600, func = self.opponent_attack)
		}

		# groups
		self.battle_sprites   = BattleSprites()
		self.player_sprites   = pygame.sprite.Group()
		self.opponent_sprites = pygame.sprite.Group()

		# control
		self.current_monster = None
		self.selection_mode  = None
		self.selected_attack = None
		self.selection_side  = 'player'
		self.indexes = {
			'general': 0,
			'monster': 0,
			'attacks': 0,
			'switch' : 0,
			'target' : 0,
		}

		self.setup()

	def setup(self):
		for entity, monster in self.monster_data.items():
			for index, monster in {k:v for k,v in monster.items() if k <= 2}.items():
				self.create_monster(monster, index, index, entity)

			# remove opponent monster data 
			for i in range(len(self.opponent_sprites)):
				del self.monster_data['opponent'][i]

	def create_monster(self, monster, index, pos_index, entity):
		monster.paused = False
		frames = self.monster_frames['monsters'][monster.name]
		outline_frames = self.monster_frames['outlines'][monster.name]
		if entity == 'player':
			pos = list(BATTLE_POSITIONS['left'].values())[pos_index]
			groups = (self.battle_sprites, self.player_sprites)
			frames = {state: [pygame.transform.flip(frame, True, False) for frame in frames] for state, frames in frames.items()}
			outline_frames = {state: [pygame.transform.flip(frame, True, False) for frame in frames] for state, frames in outline_frames.items()}
		else:
			pos = list(BATTLE_POSITIONS['right'].values())[pos_index]
			groups = (self.battle_sprites, self.opponent_sprites)

		monster_sprite = MonsterSprite(pos, frames, groups, monster, index, pos_index, entity, self.apply_attack, self.create_monster)
		MonsterOutlineSprite(monster_sprite, self.battle_sprites, outline_frames)

		# ui
		name_pos = monster_sprite.rect.midleft + vector(16,-70) if entity == 'player' else monster_sprite.rect.midright + vector(-40,-70)
		name_sprite = MonsterNameSprite(name_pos, monster_sprite, self.battle_sprites, self.fonts['regular'])
		level_pos = name_sprite.rect.bottomleft if entity == 'player' else name_sprite.rect.bottomright 
		MonsterLevelSprite(entity, level_pos, monster_sprite, self.battle_sprites, self.fonts['small'])
		MonsterStatsSprite(monster_sprite.rect.midbottom + vector(0,20), monster_sprite, (150,48), self.battle_sprites, self.fonts['small'])

	def input(self):
		if self.selection_mode and self.current_monster:
			keys = pygame.key.get_just_pressed()

			match self.selection_mode:
				case 'general': limiter = len(BATTLE_CHOICES['full'])
				case 'attacks': limiter = len(self.current_monster.monster.get_abilities(all = False))
				case 'switch': limiter = len(self.available_monsters)
				case 'target': limiter = len(self.opponent_sprites) if self.selection_side == 'opponent' else len(self.player_sprites)

			if keys[pygame.K_DOWN]:
				self.indexes[self.selection_mode] = (self.indexes[self.selection_mode] + 1) % limiter
			if keys[pygame.K_UP]:
				self.indexes[self.selection_mode] = (self.indexes[self.selection_mode] - 1) % limiter
			if keys[pygame.K_SPACE]:
				
				if self.selection_mode == 'switch':
					index, new_monster = list(self.available_monsters.items())[self.indexes['switch']]
					self.current_monster.kill()
					self.create_monster(new_monster, index, self.current_monster.pos_index, 'player')
					self.selection_mode = None
					self.update_all_monsters('resume')

				if self.selection_mode == 'target':
					sprite_group = self.opponent_sprites if self.selection_side == 'opponent' else self.player_sprites
					sprites = {sprite.pos_index: sprite for sprite in sprite_group}
					monster_sprite = sprites[list(sprites.keys())[self.indexes['target']]]

					if self.selected_attack:
						self.current_monster.activate_attack(monster_sprite, self.selected_attack)
						self.selected_attack, self.current_monster, self.selection_mode = None, None, None
					else:
						if monster_sprite.monster.health < monster_sprite.monster.get_stat('max_health') * 0.9:
							self.monster_data['player'][len(self.monster_data['player'])] = monster_sprite.monster
							monster_sprite.delayed_kill(None)
							self.update_all_monsters('resume')
						else:
							TimedSprite(monster_sprite.rect.center, self.monster_frames['ui']['cross'], self.battle_sprites, 1000)

				if self.selection_mode == 'attacks':
					self.selection_mode = 'target'
					self.selected_attack = self.current_monster.monster.get_abilities(all = False)[self.indexes['attacks']]
					self.selection_side = ATTACK_DATA[self.selected_attack]['target']

				if self.selection_mode == 'general':
					if self.indexes['general'] == 0:
						self.selection_mode = 'attacks'
					
					if self.indexes['general'] == 1:
						self.current_monster.monster.defending = True
						self.update_all_monsters('resume')
						self.current_monster, self.selection_mode = None, None
						self.indexes['general'] = 0
					
					if self.indexes['general'] == 2:
						self.selection_mode = 'switch'

					if self.indexes['general'] == 3:
						self.selection_mode = 'target'
						self.selection_side = 'opponent'
				self.indexes = {k: 0 for k in self.indexes}

			if keys[pygame.K_ESCAPE]:
				if self.selection_mode in ('attacks', 'switch', 'target'):
					self.selection_mode = 'general'

	def update_timers(self):
		for timer in self.timers.values():
			timer.update()


	# battle system
	def check_active(self):
		for monster_sprite in self.player_sprites.sprites() + self.opponent_sprites.sprites():
			if monster_sprite.monster.initiative >= 100:
				monster_sprite.monster.defending = False
				self.update_all_monsters('pause')
				monster_sprite.monster.initiative = 0
				monster_sprite.set_highlight(True)
				self.current_monster = monster_sprite
				if self.player_sprites in monster_sprite.groups():
					self.selection_mode = 'general'
				else:
					self.timers['opponent delay'].activate()

	def update_all_monsters(self, option):
		for monster_sprite in self.player_sprites.sprites() + self.opponent_sprites.sprites():
			monster_sprite.monster.paused = True if option == 'pause' else False

	def apply_attack(self, target_sprite, attack, amount):
		AttackSprite(target_sprite.rect.center, self.monster_frames['attacks'][ATTACK_DATA[attack]['animation']], self.battle_sprites)
		self.sounds[ATTACK_DATA[attack]['animation']].play()

		# get correct attack damage amount (defense, element)
		attack_element = ATTACK_DATA[attack]['element']
		target_element = target_sprite.monster.element

		# double attack
		if attack_element == 'fire'  and target_element == 'plant' or \
		   attack_element == 'water' and target_element == 'fire'  or \
		   attack_element == 'plant' and target_element == 'water':
			amount *= 2

		# halve attack
		if attack_element == 'fire'  and target_element == 'water' or \
		   attack_element == 'water' and target_element == 'plant' or \
		   attack_element == 'plant' and target_element == 'fire':
			amount *= 0.5

		target_defense = 1 - target_sprite.monster.get_stat('defense') / 2000
		if target_sprite.monster.defending:
			target_defense -= 0.2
		target_defense = max(0, min(1, target_defense))

		# update the monster health 
		target_sprite.monster.health -= amount * target_defense
		self.check_death()

		# resume 
		self.update_all_monsters('resume')

	def check_death(self):
		for monster_sprite in self.opponent_sprites.sprites() + self.player_sprites.sprites():
			if monster_sprite.monster.health <= 0:
				if self.player_sprites in monster_sprite.groups(): # player
					active_monsters = [(monster_sprite.index, monster_sprite.monster) for monster_sprite in self.player_sprites.sprites()]
					available_monsters = [(index, monster) for index, monster in self.monster_data['player'].items() if monster.health > 0 and (index, monster) not in active_monsters]
					if available_monsters:
						new_monster_data = [(monster, index, monster_sprite.pos_index, 'player') for index, monster in available_monsters][0]
					else:
						new_monster_data = None
				else:
					new_monster_data = (list(self.monster_data['opponent'].values())[0], monster_sprite.index, monster_sprite.pos_index, 'opponent') if self.monster_data['opponent'] else None
					if self.monster_data['opponent']:
						del self.monster_data['opponent'][min(self.monster_data['opponent'])]
					# xp
					xp_amount = monster_sprite.monster.level * 100 / len(self.player_sprites)
					for player_sprite in self.player_sprites:
						player_sprite.monster.update_xp(xp_amount)

				monster_sprite.delayed_kill(new_monster_data)

	def opponent_attack(self):
		ability = choice(self.current_monster.monster.get_abilities())
		random_target = choice(self.opponent_sprites.sprites()) if ATTACK_DATA[ability]['target'] == 'player' else choice(self.player_sprites.sprites())
		self.current_monster.activate_attack(random_target, ability)

	def check_end_battle(self):
		# opponents have been defeated 
		if len(self.opponent_sprites) == 0 and not self.battle_over:
			self.battle_over = True
			self.end_battle(self.character)
			for monster in self. monster_data['player'].values():
				monster.initiative = 0

		# player has been defeated 
		if len(self.player_sprites) == 0:
			pygame.quit()
			exit()


	# ui 
	def draw_ui(self):
		if self.current_monster:
			if self.selection_mode == 'general':
				self.draw_general()
			if self.selection_mode == 'attacks':
				self.draw_attacks()
			if self.selection_mode == 'switch':
				self.draw_switch()

	def draw_general(self):
		for index, (option, data_dict) in enumerate(BATTLE_CHOICES['full'].items()):
			if index == self.indexes['general']:
				surf = self.monster_frames['ui'][f"{data_dict['icon']}_highlight"]
			else:
				surf = pygame.transform.grayscale(self.monster_frames['ui'][data_dict['icon']])
			rect = surf.get_frect(center = self.current_monster.rect.midright + data_dict['pos'])
			self.display_surface.blit(surf, rect)

	def draw_attacks(self):
		# data
		abilities = self.current_monster.monster.get_abilities(all = False)
		width, height = 150, 200
		visible_attacks = 4
		item_height = height / visible_attacks
		v_offset = 0 if self.indexes['attacks'] < visible_attacks else -(self.indexes['attacks'] - visible_attacks + 1) * item_height

		# bg
		bg_rect = pygame.FRect((0,0), (width,height)).move_to(midleft = self.current_monster.rect.midright + vector(20,0))
		pygame.draw.rect(self.display_surface, COLORS['white'], bg_rect, 0, 5)

		for index, ability in enumerate(abilities):
			selected = index == self.indexes['attacks']

			# text 
			if selected:
				element = ATTACK_DATA[ability]['element']
				text_color = COLORS[element] if element!= 'normal' else COLORS['black']
			else:
				text_color = COLORS['light']
			text_surf  = self.fonts['regular'].render(ability, False, text_color)

			# rect 
			text_rect = text_surf.get_frect(center = bg_rect.midtop + vector(0, item_height / 2 + index * item_height + v_offset))
			text_bg_rect = pygame.FRect((0,0), (width, item_height)).move_to(center = text_rect.center)

			# draw
			if bg_rect.collidepoint(text_rect.center):
				if selected:
					if text_bg_rect.collidepoint(bg_rect.topleft):
						pygame.draw.rect(self.display_surface, COLORS['dark white'], text_bg_rect,0,0,5,5)
					elif text_bg_rect.collidepoint(bg_rect.midbottom + vector(0,-1)):
						pygame.draw.rect(self.display_surface, COLORS['dark white'], text_bg_rect,0,0,0,0,5,5)
					else:
						pygame.draw.rect(self.display_surface, COLORS['dark white'], text_bg_rect)

				self.display_surface.blit(text_surf, text_rect)

	def draw_switch(self):
		# data 
		width, height = 300, 320
		visible_monsters = 4
		item_height = height / visible_monsters
		v_offset = 0 if self.indexes['switch'] < visible_monsters else -(self.indexes['switch'] - visible_monsters + 1) * item_height
		bg_rect = pygame.FRect((0,0), (width, height)).move_to(midleft = self.current_monster.rect.midright + vector(20,0))
		pygame.draw.rect(self.display_surface, COLORS['white'], bg_rect, 0, 5)

		# monsters 
		active_monsters = [(monster_sprite.index, monster_sprite.monster) for monster_sprite in self.player_sprites]
		self.available_monsters = {index: monster for index, monster in self.monster_data['player'].items() if (index, monster) not in active_monsters and monster.health > 0}

		for index, monster in enumerate(self.available_monsters.values()):
			selected = index == self.indexes['switch']
			item_bg_rect = pygame.FRect((0,0), (width, item_height)).move_to(midleft = (bg_rect.left, bg_rect.top + item_height / 2 + index * item_height + v_offset))

			icon_surf = self.monster_frames['icons'][monster.name]
			icon_rect = icon_surf.get_frect(midleft = bg_rect.topleft + vector(10,item_height / 2 + index * item_height + v_offset))
			text_surf = self.fonts['regular'].render(f'{monster.name} ({monster.level})', False, COLORS['red'] if selected else COLORS['black'])
			text_rect = text_surf.get_frect(topleft = (bg_rect.left + 90, icon_rect.top))

			# selection bg
			if selected:
				if item_bg_rect.collidepoint(bg_rect.topleft):
					pygame.draw.rect(self.display_surface, COLORS['dark white'], item_bg_rect, 0, 0, 5, 5)
				elif item_bg_rect.collidepoint(bg_rect.midbottom + vector(0,-1)):
					pygame.draw.rect(self.display_surface, COLORS['dark white'], item_bg_rect, 0, 0, 0, 0, 5, 5)
				else:
					pygame.draw.rect(self.display_surface, COLORS['dark white'], item_bg_rect)

			if bg_rect.collidepoint(item_bg_rect.center):
				for surf, rect in ((icon_surf, icon_rect), (text_surf, text_rect)):
					self.display_surface.blit(surf, rect)
				health_rect = pygame.FRect((text_rect.bottomleft + vector(0,4)), (100,4))
				energy_rect = pygame.FRect((health_rect.bottomleft + vector(0,2)), (80,4))
				draw_bar(self.display_surface, health_rect, monster.health, monster.get_stat('max_health'), COLORS['red'], COLORS['black'])
				draw_bar(self.display_surface, energy_rect, monster.energy, monster.get_stat('max_energy'), COLORS['blue'], COLORS['black'])

	def update(self, dt):
		self.check_end_battle()
		
		# updates
		self.input()
		self.update_timers()
		self.battle_sprites.update(dt)
		self.check_active()

		# drawing
		self.display_surface.blit(self.bg_surf, (0,0))
		self.battle_sprites.draw(self.current_monster, self.selection_side, self.selection_mode, self.indexes['target'], self.player_sprites, self.opponent_sprites)
		self.draw_ui()