From 62fdef71a988891a02e9443f841bb58ad6f2aab2 Mon Sep 17 00:00:00 2001 From: moranshanzha <1412472761@qq.com> Date: Sun, 2 Nov 2025 14:11:30 +0800 Subject: [PATCH 1/2] test --- bin/main.py | 75 +++++- score-system/pom.xml | 64 +++++ .../scoresystem/ScoreSystemApplication.java | 12 + .../controller/ScoreController.java | 111 +++++++++ .../com/example/scoresystem/entity/Score.java | 34 +++ .../repository/ScoreRepository.java | 24 ++ .../scoresystem/service/ScoreService.java | 69 ++++++ .../src/main/resources/application.properties | 22 ++ src/menu.py | 221 ++++++++++++++++++ src/score.py | 116 +++++++++ 10 files changed, 744 insertions(+), 4 deletions(-) create mode 100644 score-system/pom.xml create mode 100644 score-system/src/main/java/com/example/scoresystem/ScoreSystemApplication.java create mode 100644 score-system/src/main/java/com/example/scoresystem/controller/ScoreController.java create mode 100644 score-system/src/main/java/com/example/scoresystem/entity/Score.java create mode 100644 score-system/src/main/java/com/example/scoresystem/repository/ScoreRepository.java create mode 100644 score-system/src/main/java/com/example/scoresystem/service/ScoreService.java create mode 100644 score-system/src/main/resources/application.properties create mode 100644 src/menu.py create mode 100644 src/score.py diff --git a/bin/main.py b/bin/main.py index 6623a0b..8a0bab4 100644 --- a/bin/main.py +++ b/bin/main.py @@ -2,12 +2,21 @@ # -*- coding: utf-8 -*- import sys +import time +import os + from pygame.locals import * +# 添加项目根目录到sys.path +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, project_root) + from config.settings import * from src.plane import OurPlane # 导入我们的飞机 from src.enemy import SmallEnemy from src.bullet import Bullet +from src.score import ScoreSystem +from src.menu import StartMenu, ScoreHistoryScreen bg_size = 480, 852 # 初始化游戏背景大小(宽, 高) @@ -23,6 +32,9 @@ color_red = (255, 0, 0) color_white = (255, 255, 255) +# 初始化得分系统 +score_system = ScoreSystem() + # 获取我方飞机 our_plane = OurPlane(bg_size) @@ -41,12 +53,14 @@ def add_small_enemies(group1, group2, num): group2.add(small_enemy) -def main(): +def game_loop(): + """游戏主循环""" # 响应音乐 pygame.mixer.music.play(-1) # loops 接收该参数, -1 表示无限循环(默认循环播放一次) running = True switch_image = False # 切换飞机的标识位(使飞机具有喷气式效果) delay = 60 # 对一些效果进行延迟, 效果更好一些 + start_time = time.time() # 记录游戏开始时间 enemies = pygame.sprite.Group() # 生成敌方飞机组(一种精灵组用以存储所有敌机精灵) small_enemies = pygame.sprite.Group() # 敌方小型飞机组(不同型号敌机创建不同的精灵组来存储) @@ -57,12 +71,17 @@ def main(): bullet_index = 0 e1_destroy_index = 0 me_destroy_index = 0 + score_font = pygame.font.Font(None, 36) # 得分显示字体 + high_score_font = pygame.font.Font(None, 24) # 最高分显示字体 # 定义子弹实例化个数 bullet1 = [] bullet_num = 6 for i in range(bullet_num): bullet1.append(Bullet(our_plane.rect.midtop)) + + # 重置得分系统 + score_system.reset_score() while running: @@ -127,6 +146,7 @@ def main(): b.active = False # 子弹损毁 for e in enemies_hit: e.active = False # 小型敌机损毁 + score_system.add_score("small") # 添加得分 # 毁坏状态绘制爆炸的场面 else: @@ -135,7 +155,8 @@ def main(): me_destroy_index = (me_destroy_index + 1) % 4 if me_destroy_index == 0: me_down_sound.play() - our_plane.reset() + score_system.save_score() # 保存得分 + return # 游戏结束,返回主菜单 # 调用 pygame 实现的碰撞方法 spritecollide (我方飞机如果和敌机碰撞, 更改飞机的存活属性) enemies_down = pygame.sprite.spritecollide(our_plane, enemies, False, pygame.sprite.collide_mask) @@ -147,8 +168,7 @@ def main(): # 响应用户的操作 for event in pygame.event.get(): if event.type == 12: # 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出 - pygame.quit() - sys.exit() + return "quit" if delay == 0: delay = 60 @@ -165,8 +185,55 @@ def main(): if key_pressed[K_d] or key_pressed[K_RIGHT]: our_plane.move_right() + # 显示得分 + score_text = score_font.render(f"Score: {score_system.score}", True, color_white) + screen.blit(score_text, (10, 10)) + + # 显示最高分 + high_score_text = high_score_font.render(f"High Score: {score_system.high_score}", True, color_white) + screen.blit(high_score_text, (10, 50)) + # 绘制图像并输出到屏幕上面 pygame.display.flip() + return "quit" + +def main(): + """游戏主入口""" + running = True + current_screen = "start_menu" + + # 创建开始菜单和得分历史屏幕 + start_menu = StartMenu(screen, score_system) + score_history = ScoreHistoryScreen(screen, score_system) + + while running: + if current_screen == "start_menu": + result = start_menu.run() + if result == "start_game": + # 重置飞机状态 + our_plane.reset() + # 开始游戏循环 + game_result = game_loop() + if game_result == "quit": + running = False + else: + current_screen = "start_menu" + elif result == "show_history": + current_screen = "score_history" + elif result == "quit": + running = False + elif current_screen == "score_history": + result = score_history.run() + if result == "back": + current_screen = "start_menu" + elif result == "quit": + running = False + + # 清理资源 + score_system.save_score() + score_system.close() + pygame.quit() + sys.exit() diff --git a/score-system/pom.xml b/score-system/pom.xml new file mode 100644 index 0000000..368486c --- /dev/null +++ b/score-system/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.example + score-system + 1.0.0 + score-system + Score System for Game + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/score-system/src/main/java/com/example/scoresystem/ScoreSystemApplication.java b/score-system/src/main/java/com/example/scoresystem/ScoreSystemApplication.java new file mode 100644 index 0000000..56dabe1 --- /dev/null +++ b/score-system/src/main/java/com/example/scoresystem/ScoreSystemApplication.java @@ -0,0 +1,12 @@ +package com.example.scoresystem; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ScoreSystemApplication { + + public static void main(String[] args) { + SpringApplication.run(ScoreSystemApplication.class, args); + } +} \ No newline at end of file diff --git a/score-system/src/main/java/com/example/scoresystem/controller/ScoreController.java b/score-system/src/main/java/com/example/scoresystem/controller/ScoreController.java new file mode 100644 index 0000000..d3d9325 --- /dev/null +++ b/score-system/src/main/java/com/example/scoresystem/controller/ScoreController.java @@ -0,0 +1,111 @@ +package com.example.scoresystem.controller; + +import com.example.scoresystem.entity.Score; +import com.example.scoresystem.service.ScoreService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/scores") +public class ScoreController { + + @Autowired + private ScoreService scoreService; + + /** + * 创建得分记录 + * @param score 得分记录 + * @return 创建后的得分记录 + */ + @PostMapping + public ResponseEntity createScore(@Valid @RequestBody Score score) { + Score savedScore = scoreService.saveScore(score); + return new ResponseEntity<>(savedScore, HttpStatus.CREATED); + } + + /** + * 根据ID获取得分记录 + * @param id 得分记录ID + * @return 得分记录 + */ + @GetMapping("/{id}") + public ResponseEntity getScoreById(@PathVariable Long id) { + Optional score = scoreService.getScoreById(id); + return score.map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * 获取所有得分记录 + * @return 得分记录列表 + */ + @GetMapping + public ResponseEntity> getAllScores() { + List scores = scoreService.getAllScores(); + return ResponseEntity.ok(scores); + } + + /** + * 获取最高分 + * @return 最高分记录 + */ + @GetMapping("/high") + public ResponseEntity getHighScore() { + Optional highScore = scoreService.getHighScore(); + return highScore.map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * 获取前N名的得分记录 + * @param limit 限制数量 + * @return 前N名的得分记录列表 + */ + @GetMapping("/top") + public ResponseEntity> getTopScores(@RequestParam(defaultValue = "10") int limit) { + Iterable topScores = scoreService.getTopScores(limit); + return ResponseEntity.ok(topScores); + } + + /** + * 更新得分记录 + * @param id 得分记录ID + * @param score 得分记录 + * @return 更新后的得分记录 + */ + @PutMapping("/{id}") + public ResponseEntity updateScore(@PathVariable Long id, @Valid @RequestBody Score score) { + Optional existingScore = scoreService.getScoreById(id); + + if (existingScore.isPresent()) { + score.setId(id); + Score updatedScore = scoreService.saveScore(score); + return ResponseEntity.ok(updatedScore); + } else { + return ResponseEntity.notFound().build(); + } + } + + /** + * 删除得分记录 + * @param id 得分记录ID + * @return 响应状态 + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteScore(@PathVariable Long id) { + Optional score = scoreService.getScoreById(id); + + if (score.isPresent()) { + scoreService.deleteScore(id); + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } +} \ No newline at end of file diff --git a/score-system/src/main/java/com/example/scoresystem/entity/Score.java b/score-system/src/main/java/com/example/scoresystem/entity/Score.java new file mode 100644 index 0000000..848ccea --- /dev/null +++ b/score-system/src/main/java/com/example/scoresystem/entity/Score.java @@ -0,0 +1,34 @@ +package com.example.scoresystem.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "score_records") +public class Score { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "玩家名称不能为空") + private String playerName; + + @Positive(message = "得分必须为正数") + private int score; + + private LocalDateTime gameTime; + + @PrePersist + protected void onCreate() { + gameTime = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/score-system/src/main/java/com/example/scoresystem/repository/ScoreRepository.java b/score-system/src/main/java/com/example/scoresystem/repository/ScoreRepository.java new file mode 100644 index 0000000..9cf1baa --- /dev/null +++ b/score-system/src/main/java/com/example/scoresystem/repository/ScoreRepository.java @@ -0,0 +1,24 @@ +package com.example.scoresystem.repository; + +import com.example.scoresystem.entity.Score; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface ScoreRepository extends JpaRepository { + + /** + * 查询最高分 + * @return 最高分记录 + */ + Score findTopByOrderByScoreDesc(); + + /** + * 查询前N名的得分记录 + * @param limit 限制数量 + * @return 前N名的得分记录列表 + */ + @Query("SELECT s FROM Score s ORDER BY s.score DESC LIMIT ?1") + Iterable findTopScores(int limit); +} \ No newline at end of file diff --git a/score-system/src/main/java/com/example/scoresystem/service/ScoreService.java b/score-system/src/main/java/com/example/scoresystem/service/ScoreService.java new file mode 100644 index 0000000..cdbcd29 --- /dev/null +++ b/score-system/src/main/java/com/example/scoresystem/service/ScoreService.java @@ -0,0 +1,69 @@ +package com.example.scoresystem.service; + +import com.example.scoresystem.entity.Score; +import com.example.scoresystem.repository.ScoreRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class ScoreService { + + @Autowired + private ScoreRepository scoreRepository; + + /** + * 保存得分记录 + * @param score 得分记录 + * @return 保存后的得分记录 + */ + public Score saveScore(Score score) { + return scoreRepository.save(score); + } + + /** + * 根据ID获取得分记录 + * @param id 得分记录ID + * @return 得分记录 + */ + public Optional getScoreById(Long id) { + return scoreRepository.findById(id); + } + + /** + * 获取所有得分记录 + * @return 得分记录列表 + */ + public List getAllScores() { + return scoreRepository.findAll(); + } + + /** + * 获取最高分 + * @return 最高分记录 + */ + public Optional getHighScore() { + return Optional.ofNullable(scoreRepository.findTopByOrderByScoreDesc()); + } + + /** + * 获取前N名的得分记录 + * @param limit 限制数量 + * @return 前N名的得分记录列表 + */ + public Iterable getTopScores(int limit) { + return scoreRepository.findTopScores(limit); + } + + /** + * 删除得分记录 + * @param id 得分记录ID + */ + public void deleteScore(Long id) { + scoreRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/score-system/src/main/resources/application.properties b/score-system/src/main/resources/application.properties new file mode 100644 index 0000000..04323c8 --- /dev/null +++ b/score-system/src/main/resources/application.properties @@ -0,0 +1,22 @@ +# 应用配置 +spring.application.name=score-system + +# 服务器配置 +server.port=8080 + +# 数据库配置 +spring.datasource.url=jdbc:mysql://localhost:3306/game_score?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true +spring.datasource.username=root +spring.datasource.password=123456 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA配置 +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect + +# CORS配置 +spring.web.cors.allowed-origins=* +spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS +spring.web.cors.allowed-headers=* diff --git a/src/menu.py b/src/menu.py new file mode 100644 index 0000000..7642147 --- /dev/null +++ b/src/menu.py @@ -0,0 +1,221 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +游戏菜单模块 +负责处理游戏的开始界面和得分历史界面 +""" +import pygame +from config.settings import * +from src.score import ScoreSystem + + +class StartMenu: + """开始菜单类""" + + def __init__(self, screen, score_system): + self.screen = screen + self.score_system = score_system + self.width, self.height = screen.get_size() + + # 加载背景图片 + self.background = pygame.image.load(os.path.join(BASE_DIR, "material/image/background.png")) + + # 加载标题图片 + self.title_font = pygame.font.Font(None, 72) + self.title_text = self.title_font.render("飞机大战", True, (255, 0, 0)) + self.title_rect = self.title_text.get_rect(center=(self.width // 2, 150)) + + # 加载按钮图片 + self.start_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_resume_nor.png")) + self.start_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_resume_pressed.png")) + self.quit_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_nor.png")) + self.quit_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_pressed.png")) + self.score_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/btn_finish.png")) + self.score_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/btn_finish.png")) # 暂时用同一个图片 + + # 设置按钮位置 + self.start_button_rect = self.start_button_normal.get_rect(center=(self.width // 2, 350)) + self.quit_button_rect = self.quit_button_normal.get_rect(center=(self.width // 2, 450)) + self.score_button_rect = self.score_button_normal.get_rect(center=(self.width // 2, 550)) + + # 加载最高分 + self.high_score = self.score_system.high_score + self.high_score_font = pygame.font.Font(None, 36) + self.high_score_text = self.high_score_font.render(f"历史最高分: {self.high_score}", True, (255, 255, 255)) + self.high_score_rect = self.high_score_text.get_rect(center=(self.width // 2, 250)) + + # 当前选中的按钮 + self.selected_button = None + + def draw(self): + """绘制开始菜单""" + # 绘制背景 + self.screen.blit(self.background, (0, 0)) + + # 绘制标题 + self.screen.blit(self.title_text, self.title_rect) + + # 绘制最高分 + self.screen.blit(self.high_score_text, self.high_score_rect) + + # 绘制按钮 + if self.selected_button == "start": + self.screen.blit(self.start_button_pressed, self.start_button_rect) + else: + self.screen.blit(self.start_button_normal, self.start_button_rect) + + if self.selected_button == "quit": + self.screen.blit(self.quit_button_pressed, self.quit_button_rect) + else: + self.screen.blit(self.quit_button_normal, self.quit_button_rect) + + if self.selected_button == "score": + self.screen.blit(self.score_button_pressed, self.score_button_rect) + else: + self.screen.blit(self.score_button_normal, self.score_button_rect) + + def handle_event(self, event): + """处理菜单事件""" + if event.type == pygame.MOUSEMOTION: + # 鼠标移动事件 + if self.start_button_rect.collidepoint(event.pos): + self.selected_button = "start" + elif self.quit_button_rect.collidepoint(event.pos): + self.selected_button = "quit" + elif self.score_button_rect.collidepoint(event.pos): + self.selected_button = "score" + else: + self.selected_button = None + + elif event.type == pygame.MOUSEBUTTONDOWN: + # 鼠标点击事件 + if event.button == 1: # 左键点击 + if self.start_button_rect.collidepoint(event.pos): + button_down_sound.play() + return "start" + elif self.quit_button_rect.collidepoint(event.pos): + button_down_sound.play() + return "quit" + elif self.score_button_rect.collidepoint(event.pos): + button_down_sound.play() + return "score_history" + + elif event.type == pygame.KEYDOWN: + # 键盘事件 + if event.key == pygame.K_UP or event.key == pygame.K_w: + if self.selected_button == "start": + self.selected_button = "score" + elif self.selected_button == "quit": + self.selected_button = "start" + elif self.selected_button == "score": + self.selected_button = "quit" + elif event.key == pygame.K_DOWN or event.key == pygame.K_s: + if self.selected_button == "start": + self.selected_button = "quit" + elif self.selected_button == "quit": + self.selected_button = "score" + elif self.selected_button == "score": + self.selected_button = "start" + elif event.key == pygame.K_RETURN: + if self.selected_button == "start": + button_down_sound.play() + return "start" + elif self.selected_button == "quit": + button_down_sound.play() + return "quit" + elif self.selected_button == "score": + button_down_sound.play() + return "score_history" + + return None + + +class ScoreHistoryScreen: + """得分历史记录屏幕类""" + + def __init__(self, screen, score_system): + self.screen = screen + self.score_system = score_system + self.width, self.height = screen.get_size() + + # 加载背景图片 + self.background = pygame.image.load(os.path.join(BASE_DIR, "material/image/background.png")) + + # 标题 + self.title_font = pygame.font.Font(None, 60) + self.title_text = self.title_font.render("得分历史", True, (255, 0, 0)) + self.title_rect = self.title_text.get_rect(center=(self.width // 2, 100)) + + # 返回按钮 + self.back_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_nor.png")) + self.back_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_pressed.png")) + self.back_button_rect = self.back_button_normal.get_rect(topleft=(20, 20)) + + # 得分列表 + self.scores = self.score_system.get_top_scores(10) + self.score_font = pygame.font.Font(None, 36) + + # 当前选中的按钮 + self.selected_button = None + + def update_scores(self): + """更新得分列表""" + self.scores = self.score_system.get_top_scores(10) + + def draw(self): + """绘制得分历史记录屏幕""" + # 绘制背景 + self.screen.blit(self.background, (0, 0)) + + # 绘制标题 + self.screen.blit(self.title_text, self.title_rect) + + # 绘制返回按钮 + if self.selected_button == "back": + self.screen.blit(self.back_button_pressed, self.back_button_rect) + else: + self.screen.blit(self.back_button_normal, self.back_button_rect) + + # 绘制得分列表 + y_offset = 200 + for i, score in enumerate(self.scores): + if i >= 10: # 最多显示10条记录 + break + + score_text = self.score_font.render(f"{i+1}. {score['playerName']}: {score['score']}", True, (255, 255, 255)) + score_rect = score_text.get_rect(center=(self.width // 2, y_offset)) + self.screen.blit(score_text, score_rect) + y_offset += 50 + + # 如果没有得分记录 + if not self.scores: + no_scores_text = self.score_font.render("暂无得分记录", True, (255, 255, 255)) + no_scores_rect = no_scores_text.get_rect(center=(self.width // 2, 300)) + self.screen.blit(no_scores_text, no_scores_rect) + + def handle_event(self, event): + """处理得分历史屏幕事件""" + if event.type == pygame.MOUSEMOTION: + # 鼠标移动事件 + if self.back_button_rect.collidepoint(event.pos): + self.selected_button = "back" + else: + self.selected_button = None + + elif event.type == pygame.MOUSEBUTTONDOWN: + # 鼠标点击事件 + if event.button == 1: # 左键点击 + if self.back_button_rect.collidepoint(event.pos): + button_down_sound.play() + return "back" + + elif event.type == pygame.KEYDOWN: + # 键盘事件 + if event.key == pygame.K_ESCAPE or event.key == pygame.K_BACKSPACE: + button_down_sound.play() + return "back" + elif event.key == pygame.K_RETURN and self.selected_button == "back": + button_down_sound.play() + return "back" + + return None diff --git a/src/score.py b/src/score.py new file mode 100644 index 0000000..657b46b --- /dev/null +++ b/src/score.py @@ -0,0 +1,116 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +得分记录模块 +负责游戏得分的计算和记录 +""" +import json +import os +import requests +from config.settings import BASE_DIR + + +class ScoreSystem: + """得分系统类,负责得分计算和记录""" + + def __init__(self): + self.score = 0 + self.high_score = self.load_high_score() + self.api_url = "http://localhost:8080/api/scores" # SpringBoot后端API地址 + + def add_score(self, enemy_type): + """根据敌机类型添加得分""" + if enemy_type == "small": + self.score += 10 + elif enemy_type == "mid": + self.score += 50 + elif enemy_type == "big": + self.score += 100 + + # 更新最高分 + if self.score > self.high_score: + self.high_score = self.score + + def reset_score(self): + """重置得分""" + self.score = 0 + + def save_score(self, player_name="player"): + """保存得分到服务器""" + try: + score_data = { + "playerName": player_name, + "score": self.score, + "gameTime": "" + } + + response = requests.post(self.api_url, json=score_data) + if response.status_code == 201: + print("得分保存成功") + else: + print(f"得分保存失败: {response.status_code}") + # 如果服务器不可用,保存到本地 + self.save_local_score(player_name) + except requests.exceptions.ConnectionError: + print("无法连接到服务器,保存到本地") + self.save_local_score(player_name) + + def save_local_score(self, player_name="player"): + """保存得分到本地文件""" + score_file = os.path.join(BASE_DIR, "scores.json") + + try: + if os.path.exists(score_file): + with open(score_file, "r") as f: + scores = json.load(f) + else: + scores = [] + + # 添加新得分 + scores.append({ + "playerName": player_name, + "score": self.score, + "gameTime": "" + }) + + # 按得分降序排序 + scores.sort(key=lambda x: x["score"], reverse=True) + + with open(score_file, "w") as f: + json.dump(scores, f, indent=4) + + except Exception as e: + print(f"保存本地得分失败: {e}") + + def load_high_score(self): + """加载本地最高分""" + score_file = os.path.join(BASE_DIR, "scores.json") + + try: + if os.path.exists(score_file): + with open(score_file, "r") as f: + scores = json.load(f) + if scores: + return scores[0]["score"] + except Exception as e: + print(f"加载最高分失败: {e}") + + return 0 + + def get_top_scores(self, count=10): + """获取本地排行榜前N名""" + score_file = os.path.join(BASE_DIR, "scores.json") + + try: + if os.path.exists(score_file): + with open(score_file, "r") as f: + scores = json.load(f) + return scores[:count] + except Exception as e: + print(f"加载排行榜失败: {e}") + + return [] + + def close(self): + """关闭得分系统""" + pass From 1f16b2a43f44ed287ec56a789de12e55fa145bee Mon Sep 17 00:00:00 2001 From: moranshanzha <1412472761@qq.com> Date: Sun, 2 Nov 2025 15:05:25 +0800 Subject: [PATCH 2/2] test --- as | 0 bin/main.py | 120 +++++++- src/plane.py | 33 ++ src/score.py | 11 +- src/systems.py | 816 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 966 insertions(+), 14 deletions(-) create mode 100644 as create mode 100644 src/systems.py diff --git a/as b/as new file mode 100644 index 0000000..e69de29 diff --git a/bin/main.py b/bin/main.py index 8a0bab4..50e9290 100644 --- a/bin/main.py +++ b/bin/main.py @@ -17,6 +17,7 @@ from src.bullet import Bullet from src.score import ScoreSystem from src.menu import StartMenu, ScoreHistoryScreen +from src.systems import KillEffectSystem, SkinSystem, WeaponSystem, ItemSystem, EffectSystem bg_size = 480, 852 # 初始化游戏背景大小(宽, 高) @@ -38,6 +39,16 @@ # 获取我方飞机 our_plane = OurPlane(bg_size) +# 初始化新系统 +kill_effect_system = KillEffectSystem(screen) +skin_system = SkinSystem(screen, our_plane) +weapon_system = WeaponSystem(our_plane) +item_system = ItemSystem(screen, our_plane, weapon_system, score_system) +effect_system = EffectSystem(screen) + +# 加载保存的皮肤选择 +skin_system.load_skin_selection() + def add_small_enemies(group1, group2, num): """ @@ -82,6 +93,17 @@ def game_loop(): # 重置得分系统 score_system.reset_score() + score_system.score_multiplier = 1 + + # 重置飞机状态 + our_plane.reset() + + # 重置道具系统 + item_system.bomb_count = 3 + + # 重置武器系统 + weapon_system.current_weapon = 'normal' + weapon_system.weapon_effects = {} while running: @@ -91,6 +113,13 @@ def game_loop(): # 微信的飞机貌似是喷气式的, 那么这个就涉及到一个帧数的问题 clock = pygame.time.Clock() clock.tick(60) + + # 更新各种系统 + our_plane.update() + kill_effect_system.update() + weapon_system.update() + item_system.update(enemies) + effect_system.update() # 绘制我方飞机的两种不同的形式 if not delay % 3: @@ -125,17 +154,44 @@ def game_loop(): # 当我方飞机存活状态, 正常展示 if our_plane.active: - if switch_image: - screen.blit(our_plane.image_one, our_plane.rect) - else: - screen.blit(our_plane.image_two, our_plane.rect) + # 绘制飞机(考虑无敌状态的闪烁效果) + if not our_plane.invincible or delay % 5 < 3: + if switch_image: + screen.blit(our_plane.image_one, our_plane.rect) + else: + screen.blit(our_plane.image_two, our_plane.rect) # 飞机存活的状态下才可以发射子弹 - if not (delay % 10): # 每十帧发射一颗移动的子弹 + current_weapon = weapon_system.get_current_weapon() + if not (delay % current_weapon['fire_rate']): # 根据当前武器的射速发射子弹 bullet_sound.play() bullets = bullet1 - bullets[bullet_index].reset(our_plane.rect.midtop) - bullet_index = (bullet_index + 1) % bullet_num + + # 根据武器类型发射不同数量的子弹 + if current_weapon['name'] == '普通子弹': + bullets[bullet_index].reset(our_plane.rect.midtop) + bullet_index = (bullet_index + 1) % bullet_num + elif current_weapon['name'] == '双发子弹': + # 发射两颗子弹,左右分开 + bullets[bullet_index].reset((our_plane.rect.midtop[0] - 20, our_plane.rect.midtop[1])) + bullet_index = (bullet_index + 1) % bullet_num + bullets[bullet_index].reset((our_plane.rect.midtop[0] + 20, our_plane.rect.midtop[1])) + bullet_index = (bullet_index + 1) % bullet_num + elif current_weapon['name'] == '激光': + # 发射激光 + effect_system.add_laser_effect( + our_plane.rect.midtop, + (our_plane.rect.midtop[0], 0), + color=(0, 255, 255), + duration=5 + ) + + # 检测激光是否击中敌机 + for enemy in enemies: + if enemy.active and enemy.rect.collidepoint((our_plane.rect.midtop[0], enemy.rect.top)): + enemy.active = False + score_system.add_score("small") + kill_effect_system.add_kill_effect(enemy.rect.center, "small") for b in bullets: if b.active: # 只有激活的子弹才可能击中敌机 @@ -147,6 +203,7 @@ def game_loop(): for e in enemies_hit: e.active = False # 小型敌机损毁 score_system.add_score("small") # 添加得分 + kill_effect_system.add_kill_effect(e.rect.center, "small") # 毁坏状态绘制爆炸的场面 else: @@ -161,14 +218,30 @@ def game_loop(): # 调用 pygame 实现的碰撞方法 spritecollide (我方飞机如果和敌机碰撞, 更改飞机的存活属性) enemies_down = pygame.sprite.spritecollide(our_plane, enemies, False, pygame.sprite.collide_mask) if enemies_down: - our_plane.active = False - for row in enemies: - row.active = False + for enemy in enemies_down: + if enemy.active: + enemy.active = False + # 飞机受到伤害 + if not our_plane.take_damage(): + # 飞机被摧毁 + for row in enemies: + row.active = False # 响应用户的操作 for event in pygame.event.get(): if event.type == 12: # 如果用户按下屏幕上的关闭按钮,触发QUIT事件,程序退出 return "quit" + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_t: + # 切换皮肤菜单 + skin_system.toggle_skin_menu() + elif event.key == pygame.K_SPACE: + # 使用炸弹 + item_system.use_bomb(enemies) + + # 处理皮肤菜单事件 + if skin_system.skin_menu_active: + skin_system.handle_event(event) if delay == 0: delay = 60 @@ -192,7 +265,32 @@ def game_loop(): # 显示最高分 high_score_text = high_score_font.render(f"High Score: {score_system.high_score}", True, color_white) screen.blit(high_score_text, (10, 50)) - + + # 显示当前武器 + current_weapon = weapon_system.get_current_weapon() + weapon_text = high_score_font.render(f"Weapon: {current_weapon['name']}", True, color_white) + screen.blit(weapon_text, (10, 90)) + + # 显示生命值 + health_bar_width = 100 + health_bar_height = 10 + health_ratio = our_plane.health / our_plane.max_health + pygame.draw.rect(screen, color_black, (10, 130, health_bar_width, health_bar_height)) + pygame.draw.rect(screen, color_red if health_ratio < 0.3 else color_green, + (10, 130, health_bar_width * health_ratio, health_bar_height)) + health_text = high_score_font.render(f"Health: {our_plane.health}/{our_plane.max_health}", True, color_white) + screen.blit(health_text, (10, 150)) + + # 绘制各种效果 + kill_effect_system.draw() + effect_system.draw() + + # 绘制道具 + item_system.draw() + + # 绘制皮肤菜单 + skin_system.draw() + # 绘制图像并输出到屏幕上面 pygame.display.flip() return "quit" diff --git a/src/plane.py b/src/plane.py index 2670884..ba54316 100644 --- a/src/plane.py +++ b/src/plane.py @@ -44,6 +44,12 @@ def __init__(self, bg_size): pygame.image.load(os.path.join(BASE_DIR, "material/image/hero_blowup_n4.png")), ] ) + # 飞机生命值 + self.max_health = 3 + self.health = self.max_health + # 无敌状态 + self.invincible = False + self.invincible_time = 0 def move_up(self): """ @@ -86,6 +92,33 @@ def reset(self): self.rect.left, self.rect.top = (self.width - self.rect.width) // 2, (self.height - self.rect.height - 60) # 重置飞机的存活状态 self.active = True + # 重置生命值 + self.health = self.max_health + # 设置无敌状态 + self.invincible = True + self.invincible_time = 120 # 2秒无敌时间 + + def update(self): + """更新飞机状态""" + # 处理无敌状态 + if self.invincible: + self.invincible_time -= 1 + if self.invincible_time <= 0: + self.invincible = False + + def take_damage(self, damage=1): + """处理飞机受伤""" + if not self.invincible and self.active: + self.health -= damage + if self.health <= 0: + self.active = False + return False + else: + # 受伤后短暂无敌 + self.invincible = True + self.invincible_time = 60 # 1秒无敌时间 + return True + return False diff --git a/src/score.py b/src/score.py index 657b46b..2b44603 100644 --- a/src/score.py +++ b/src/score.py @@ -17,16 +17,21 @@ def __init__(self): self.score = 0 self.high_score = self.load_high_score() self.api_url = "http://localhost:8080/api/scores" # SpringBoot后端API地址 + self.score_multiplier = 1 def add_score(self, enemy_type): """根据敌机类型添加得分""" + base_score = 0 if enemy_type == "small": - self.score += 10 + base_score = 10 elif enemy_type == "mid": - self.score += 50 + base_score = 50 elif enemy_type == "big": - self.score += 100 + base_score = 100 + # 应用得分加倍效果 + self.score += base_score * self.score_multiplier + # 更新最高分 if self.score > self.high_score: self.high_score = self.score diff --git a/src/systems.py b/src/systems.py new file mode 100644 index 0000000..4bb70e7 --- /dev/null +++ b/src/systems.py @@ -0,0 +1,816 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +""" +游戏系统模块 +负责实现击杀画面系统、飞机皮肤调换界面和道具系统 +""" +import pygame +import random +import os +from config.settings import BASE_DIR, button_down_sound, get_bullet_sound, get_bomb_sound, level_up_sound, bomb_sound + + +class KillEffectSystem: + """击杀画面系统类""" + + def __init__(self, screen): + self.screen = screen + self.width, self.height = screen.get_size() + + # 加载击杀特效资源 + self.effects = { + 'small': [ + pygame.image.load("material/image/enemy1_down1.png"), + pygame.image.load("material/image/enemy1_down2.png"), + pygame.image.load("material/image/enemy1_down3.png"), + pygame.image.load("material/image/enemy1_down4.png") + ], + 'mid': [ + pygame.image.load("material/image/enemy2_down1.png"), + pygame.image.load("material/image/enemy2_down2.png"), + pygame.image.load("material/image/enemy2_down3.png"), + pygame.image.load("material/image/enemy2_down4.png") + ], + 'big': [ + pygame.image.load("material/image/enemy3_down1.png"), + pygame.image.load("material/image/enemy3_down2.png"), + pygame.image.load("material/image/enemy3_down3.png"), + pygame.image.load("material/image/enemy3_down4.png"), + pygame.image.load("material/image/enemy3_down5.png"), + pygame.image.load("material/image/enemy3_down6.png") + ] + } + + # 当前正在播放的特效 + self.active_effects = [] + + # 击杀信息面板 + self.font = pygame.font.Font(None, 36) + self.kill_messages = [] + + def add_kill_effect(self, position, enemy_type): + """添加击杀特效""" + if enemy_type not in self.effects: + enemy_type = 'small' + + effect = { + 'position': position, + 'type': enemy_type, + 'images': self.effects[enemy_type], + 'current_frame': 0, + 'frame_delay': 0, + 'active': True + } + + self.active_effects.append(effect) + + # 添加击杀信息 + message = { + 'text': self.get_kill_message(enemy_type), + 'position': (position[0], position[1] - 30), + 'alpha': 255, + 'y_speed': -2, + 'active': True + } + + self.kill_messages.append(message) + + def get_kill_message(self, enemy_type): + """获取随机击杀消息""" + messages = { + 'small': ['干得漂亮!', '完美击杀!', '太棒了!'], + 'mid': ['精彩绝伦!', '太厉害了!', '漂亮的一击!'], + 'big': ['史诗级击杀!', '太壮观了!', '难以置信!'] + } + + if enemy_type not in messages: + enemy_type = 'small' + + return random.choice(messages[enemy_type]) + + def update(self): + """更新击杀特效和消息""" + # 更新特效 + for effect in self.active_effects[:]: + if not effect['active']: + self.active_effects.remove(effect) + continue + + effect['frame_delay'] += 1 + if effect['frame_delay'] >= 3: + effect['current_frame'] += 1 + effect['frame_delay'] = 0 + + if effect['current_frame'] >= len(effect['images']): + effect['active'] = False + + # 更新消息 + for message in self.kill_messages[:]: + if not message['active']: + self.kill_messages.remove(message) + continue + + message['position'] = (message['position'][0], message['position'][1] + message['y_speed']) + message['alpha'] -= 5 + + if message['alpha'] <= 0: + message['active'] = False + + def draw(self): + """绘制击杀特效和消息""" + # 绘制特效 + for effect in self.active_effects: + if effect['active']: + image = effect['images'][effect['current_frame']] + rect = image.get_rect(center=effect['position']) + self.screen.blit(image, rect) + + # 绘制消息 + for message in self.kill_messages: + if message['active']: + text_surface = self.font.render(message['text'], True, (255, 255, 255)) + text_surface.set_alpha(message['alpha']) + rect = text_surface.get_rect(center=message['position']) + self.screen.blit(text_surface, rect) + + +class SkinSystem: + """飞机皮肤系统类""" + + def __init__(self, screen, our_plane): + self.screen = screen + self.our_plane = our_plane + self.width, self.height = screen.get_size() + + # 加载皮肤资源 + # 使用现有图片作为所有皮肤的默认图片 + hero1 = pygame.image.load(os.path.join(BASE_DIR, "material/image/hero1.png")) + hero2 = pygame.image.load(os.path.join(BASE_DIR, "material/image/hero2.png")) + destroy_images = [ + pygame.image.load(os.path.join(BASE_DIR, "material/image/hero_blowup_n1.png")), + pygame.image.load(os.path.join(BASE_DIR, "material/image/hero_blowup_n2.png")), + pygame.image.load(os.path.join(BASE_DIR, "material/image/hero_blowup_n3.png")), + pygame.image.load(os.path.join(BASE_DIR, "material/image/hero_blowup_n4.png")), + ] + + self.skins = [ + { + 'name': '默认皮肤', + 'image_one': hero1, + 'image_two': hero2, + 'destroy_images': destroy_images, + 'unlocked': True, + 'price': 0 + }, + { + 'name': '红色皮肤', + 'image_one': hero1, + 'image_two': hero2, + 'destroy_images': destroy_images, + 'unlocked': False, + 'price': 1000 + }, + { + 'name': '蓝色皮肤', + 'image_one': hero1, + 'image_two': hero2, + 'destroy_images': destroy_images, + 'unlocked': False, + 'price': 2000 + } + ] + + # 当前选中的皮肤索引 + self.current_skin_index = 0 + + # 皮肤选择界面状态 + self.skin_menu_active = False + + # 界面元素 + self.title_font = pygame.font.Font(None, 60) + self.text_font = pygame.font.Font(None, 36) + + # 按钮 + self.back_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_nor.png")) + self.back_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_pause_pressed.png")) + self.select_button_normal = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_resume_nor.png")) + self.select_button_pressed = pygame.image.load(os.path.join(BASE_DIR, "material/image/game_resume_pressed.png")) + + self.back_button_rect = self.back_button_normal.get_rect(topleft=(20, 20)) + self.select_button_rect = self.select_button_normal.get_rect(bottomright=(self.width - 20, self.height - 20)) + + # 皮肤预览区域 + self.preview_rect = pygame.Rect(self.width // 2 - 150, 150, 300, 300) + + # 皮肤列表区域 + self.list_rect = pygame.Rect(50, 150, 200, 500) + + # 当前选中的列表项 + self.selected_list_item = 0 + + def update_skin(self): + """更新飞机皮肤""" + skin = self.skins[self.current_skin_index] + self.our_plane.image_one = skin['image_one'] + self.our_plane.image_two = skin['image_two'] + self.our_plane.destroy_images = skin['destroy_images'] + self.our_plane.mask = pygame.mask.from_surface(self.our_plane.image_one) + + def toggle_skin_menu(self): + """切换皮肤菜单的显示状态""" + self.skin_menu_active = not self.skin_menu_active + if not self.skin_menu_active: + # 保存当前皮肤选择 + self.save_skin_selection() + + def save_skin_selection(self): + """保存皮肤选择到本地""" + import json + import os + + skin_file = os.path.join(BASE_DIR, "skin_selection.json") + + try: + with open(skin_file, "w") as f: + json.dump({'current_skin': self.current_skin_index}, f) + except Exception as e: + print(f"保存皮肤选择失败: {e}") + + def load_skin_selection(self): + """从本地加载皮肤选择""" + import json + import os + + skin_file = os.path.join(BASE_DIR, "skin_selection.json") + + try: + if os.path.exists(skin_file): + with open(skin_file, "r") as f: + data = json.load(f) + if 'current_skin' in data: + self.current_skin_index = data['current_skin'] + self.update_skin() + except Exception as e: + print(f"加载皮肤选择失败: {e}") + + def handle_event(self, event): + """处理皮肤菜单事件""" + if event.type == pygame.MOUSEMOTION: + # 鼠标移动事件 + if self.back_button_rect.collidepoint(event.pos): + self.selected_button = "back" + elif self.select_button_rect.collidepoint(event.pos): + self.selected_button = "select" + else: + # 检查皮肤列表 + list_y = self.list_rect.y + 50 + for i, skin in enumerate(self.skins): + item_rect = pygame.Rect(self.list_rect.x + 10, list_y + i * 60, self.list_rect.width - 20, 50) + if item_rect.collidepoint(event.pos): + self.selected_list_item = i + break + else: + self.selected_button = None + + elif event.type == pygame.MOUSEBUTTONDOWN: + # 鼠标点击事件 + if event.button == 1: + if self.back_button_rect.collidepoint(event.pos): + button_down_sound.play() + self.toggle_skin_menu() + elif self.select_button_rect.collidepoint(event.pos): + button_down_sound.play() + self.current_skin_index = self.selected_list_item + self.update_skin() + else: + # 检查皮肤列表 + list_y = self.list_rect.y + 50 + for i, skin in enumerate(self.skins): + item_rect = pygame.Rect(self.list_rect.x + 10, list_y + i * 60, self.list_rect.width - 20, 50) + if item_rect.collidepoint(event.pos): + self.selected_list_item = i + break + + elif event.type == pygame.KEYDOWN: + # 键盘事件 + if event.key == pygame.K_ESCAPE or event.key == pygame.K_b: + self.toggle_skin_menu() + elif event.key == pygame.K_UP or event.key == pygame.K_w: + self.selected_list_item = (self.selected_list_item - 1) % len(self.skins) + elif event.key == pygame.K_DOWN or event.key == pygame.K_s: + self.selected_list_item = (self.selected_list_item + 1) % len(self.skins) + elif event.key == pygame.K_RETURN: + self.current_skin_index = self.selected_list_item + self.update_skin() + + def draw(self): + """绘制皮肤选择界面""" + if not self.skin_menu_active: + return + + # 绘制半透明背景 + overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 180)) + self.screen.blit(overlay, (0, 0)) + + # 绘制标题 + title_text = self.title_font.render("飞机皮肤", True, (255, 0, 0)) + title_rect = title_text.get_rect(center=(self.width // 2, 80)) + self.screen.blit(title_text, title_rect) + + # 绘制皮肤预览区域 + pygame.draw.rect(self.screen, (255, 255, 255), self.preview_rect, 2) + skin = self.skins[self.selected_list_item] + preview_image = skin['image_one'] + preview_image = pygame.transform.scale(preview_image, (200, 200)) + preview_rect = preview_image.get_rect(center=self.preview_rect.center) + self.screen.blit(preview_image, preview_rect) + + # 绘制皮肤信息 + name_text = self.text_font.render(f"名称: {skin['name']}", True, (255, 255, 255)) + self.screen.blit(name_text, (self.preview_rect.x, self.preview_rect.y + self.preview_rect.height + 20)) + + if skin['unlocked']: + status_text = self.text_font.render("状态: 已解锁", True, (0, 255, 0)) + else: + status_text = self.text_font.render(f"状态: 未解锁 (价格: {skin['price']})", True, (255, 255, 0)) + self.screen.blit(status_text, (self.preview_rect.x, self.preview_rect.y + self.preview_rect.height + 60)) + + # 绘制皮肤列表 + pygame.draw.rect(self.screen, (255, 255, 255), self.list_rect, 2) + list_title = self.text_font.render("可用皮肤", True, (255, 255, 255)) + self.screen.blit(list_title, (self.list_rect.x + 10, self.list_rect.y + 10)) + + list_y = self.list_rect.y + 50 + for i, skin in enumerate(self.skins): + item_rect = pygame.Rect(self.list_rect.x + 10, list_y + i * 60, self.list_rect.width - 20, 50) + + if i == self.selected_list_item: + pygame.draw.rect(self.screen, (255, 0, 0), item_rect) + + if skin['unlocked']: + color = (255, 255, 255) + else: + color = (100, 100, 100) + + skin_name = self.text_font.render(skin['name'], True, color) + self.screen.blit(skin_name, (item_rect.x + 10, item_rect.y + 10)) + + # 绘制按钮 + if self.selected_button == "back": + self.screen.blit(self.back_button_pressed, self.back_button_rect) + else: + self.screen.blit(self.back_button_normal, self.back_button_rect) + + if self.selected_button == "select": + self.screen.blit(self.select_button_pressed, self.select_button_rect) + else: + self.screen.blit(self.select_button_normal, self.select_button_rect) + + # 绘制按钮文字 + back_text = self.text_font.render("返回", True, (255, 255, 255)) + back_text_rect = back_text.get_rect(center=self.back_button_rect.center) + self.screen.blit(back_text, back_text_rect) + + select_text = self.text_font.render("选择", True, (255, 255, 255)) + select_text_rect = select_text.get_rect(center=self.select_button_rect.center) + self.screen.blit(select_text, select_text_rect) + + +class WeaponSystem: + """武器系统类""" + + def __init__(self, our_plane): + self.our_plane = our_plane + + # 武器类型 + self.weapon_types = { + 'normal': { + 'name': '普通子弹', + 'damage': 1, + 'fire_rate': 10, + 'spread': 0, + 'image': pygame.image.load("material/image/bullet1.png") + }, + 'double': { + 'name': '双发子弹', + 'damage': 1, + 'fire_rate': 10, + 'spread': 20, + 'image': pygame.image.load("material/image/bullet2.png") + }, + 'laser': { + 'name': '激光', + 'damage': 3, + 'fire_rate': 5, + 'spread': 0, + 'image': pygame.image.load("material/image/bullet1.png") + } + } + + # 当前武器 + self.current_weapon = 'normal' + + # 武器切换冷却时间 + self.weapon_switch_cooldown = 0 + + # 武器效果持续时间 + self.weapon_effects = {} + + def update(self): + """更新武器系统""" + # 处理武器切换冷却 + if self.weapon_switch_cooldown > 0: + self.weapon_switch_cooldown -= 1 + + # 处理武器效果持续时间 + for weapon_type in list(self.weapon_effects.keys()): + self.weapon_effects[weapon_type] -= 1 + if self.weapon_effects[weapon_type] <= 0: + del self.weapon_effects[weapon_type] + + # 如果当前武器效果结束,切换回普通武器 + if self.current_weapon == weapon_type: + self.current_weapon = 'normal' + + def switch_weapon(self, weapon_type): + """切换武器""" + if weapon_type not in self.weapon_types: + return + + if self.weapon_switch_cooldown == 0: + self.current_weapon = weapon_type + self.weapon_switch_cooldown = 60 # 1秒冷却时间 + + def add_weapon_effect(self, weapon_type, duration): + """添加武器效果""" + if weapon_type not in self.weapon_types: + return + + if weapon_type in self.weapon_effects: + # 如果武器效果已存在,延长持续时间 + self.weapon_effects[weapon_type] += duration + else: + # 否则添加新的武器效果 + self.weapon_effects[weapon_type] = duration + self.current_weapon = weapon_type + + def get_current_weapon(self): + """获取当前武器""" + return self.weapon_types[self.current_weapon] + + def get_weapon_duration(self, weapon_type): + """获取武器效果剩余持续时间""" + return self.weapon_effects.get(weapon_type, 0) + + +class ItemSystem: + """道具系统类""" + + def __init__(self, screen, our_plane, weapon_system, score_system): + self.screen = screen + self.our_plane = our_plane + self.weapon_system = weapon_system + self.score_system = score_system + self.width, self.height = screen.get_size() + + # 道具类型 + self.item_types = { + 'double_bullet': { + 'name': '双发子弹', + 'image': pygame.image.load("material/image/powerup_double.png"), + 'effect': 'weapon', + 'effect_param': 'double', + 'duration': 600 # 10秒 + }, + 'laser': { + 'name': '激光', + 'image': pygame.image.load("material/image/powerup_laser.png"), + 'effect': 'weapon', + 'effect_param': 'laser', + 'duration': 300 # 5秒 + }, + 'bomb': { + 'name': '炸弹', + 'image': pygame.image.load("material/image/bomb.png"), + 'effect': 'bomb', + 'effect_param': 1, + 'duration': 0 # 立即生效 + }, + 'health': { + 'name': '生命', + 'image': pygame.image.load("material/image/powerup_health.png"), + 'effect': 'health', + 'effect_param': 1, + 'duration': 0 # 立即生效 + }, + 'score_multiplier': { + 'name': '得分加倍', + 'image': pygame.image.load("material/image/powerup_score.png"), + 'effect': 'score_multiplier', + 'effect_param': 2, + 'duration': 600 # 10秒 + } + } + + # 当前活动的道具 + self.active_items = [] + + # 道具生成定时器 + self.item_spawn_timer = 0 + self.item_spawn_interval = 300 # 5秒生成一个道具 + + # 道具效果 + self.active_effects = { + 'score_multiplier': 0, + 'health_boost': 0 + } + + # 炸弹数量 + self.bomb_count = 3 + + def update(self, enemies): + """更新道具系统""" + # 生成道具 + self.item_spawn_timer += 1 + if self.item_spawn_timer >= self.item_spawn_interval: + self.spawn_item() + self.item_spawn_timer = 0 + self.item_spawn_interval = random.randint(200, 400) # 随机生成间隔 + + # 更新道具位置 + for item in self.active_items[:]: + item['rect'].y += item['speed'] + + # 检查是否超出屏幕 + if item['rect'].y > self.height: + self.active_items.remove(item) + continue + + # 检查是否与玩家飞机碰撞 + if pygame.sprite.collide_mask(self.our_plane, item['sprite']): + self.collect_item(item) + self.active_items.remove(item) + continue + + # 更新道具效果 + for effect_type in list(self.active_effects.keys()): + if self.active_effects[effect_type] > 0: + self.active_effects[effect_type] -= 1 + else: + # 效果结束 + if effect_type == 'score_multiplier': + self.score_system.score_multiplier = 1 + + def draw(self): + """绘制道具""" + for item in self.active_items: + self.screen.blit(item['image'], item['rect']) + + # 绘制炸弹数量 + bomb_font = pygame.font.Font(None, 36) + bomb_text = bomb_font.render(f"炸弹: {self.bomb_count}", True, (255, 255, 255)) + self.screen.blit(bomb_text, (self.width - 120, 10)) + + # 绘制道具效果指示器 + self.draw_effect_indicators() + + def spawn_item(self): + """生成道具""" + # 随机选择道具类型 + item_type = random.choice(list(self.item_types.keys())) + item_data = self.item_types[item_type] + + # 创建道具精灵 + item_sprite = pygame.sprite.Sprite() + item_sprite.image = item_data['image'] + item_sprite.rect = item_data['image'].get_rect() + item_sprite.mask = pygame.mask.from_surface(item_data['image']) + + # 随机生成位置 + x = random.randint(0, self.width - item_sprite.rect.width) + y = random.randint(-200, -50) + + item_sprite.rect.x = x + item_sprite.rect.y = y + + # 添加到活动道具列表 + self.active_items.append({ + 'type': item_type, + 'data': item_data, + 'sprite': item_sprite, + 'image': item_data['image'], + 'rect': item_sprite.rect, + 'speed': random.randint(2, 5) + }) + + def collect_item(self, item): + """收集道具""" + item_type = item['type'] + item_data = item['data'] + + if item_data['effect'] == 'weapon': + self.weapon_system.add_weapon_effect(item_data['effect_param'], item_data['duration']) + get_bullet_sound.play() + elif item_data['effect'] == 'bomb': + self.bomb_count += item_data['effect_param'] + get_bomb_sound.play() + elif item_data['effect'] == 'health': + # 这里假设飞机有生命值属性 + if hasattr(self.our_plane, 'health'): + self.our_plane.health += item_data['effect_param'] + if self.our_plane.health > self.our_plane.max_health: + self.our_plane.health = self.our_plane.max_health + elif item_data['effect'] == 'score_multiplier': + self.active_effects['score_multiplier'] = item_data['duration'] + self.score_system.score_multiplier = item_data['effect_param'] + level_up_sound.play() + + def use_bomb(self, enemies): + """使用炸弹""" + if self.bomb_count > 0: + bomb_sound.play() + self.bomb_count -= 1 + + # 清除所有敌机 + for enemy in enemies: + enemy.active = False + + # 播放炸弹特效 + # 这里可以添加炸弹爆炸的特效动画 + + def draw_effect_indicators(self): + """绘制道具效果指示器""" + x = 10 + y = 100 + + # 绘制得分加倍效果 + if self.active_effects['score_multiplier'] > 0: + multiplier_text = self.text_font.render(f"得分加倍: {self.active_effects['score_multiplier'] // 60}s", True, (255, 255, 0)) + self.screen.blit(multiplier_text, (x, y)) + y += 40 + + # 绘制当前武器效果 + for weapon_type in self.weapon_system.weapon_effects: + duration = self.weapon_system.weapon_effects[weapon_type] + if duration > 0: + weapon_text = self.text_font.render(f"{self.weapon_system.weapon_types[weapon_type]['name']}: {duration // 60}s", True, (0, 255, 0)) + self.screen.blit(weapon_text, (x, y)) + y += 40 + + def get_score_multiplier(self): + """获取得分倍数""" + return self.active_effects.get('score_multiplier', 0) > 0 and self.score_system.score_multiplier or 1 + + +class EffectSystem: + """效果系统类,负责管理所有视觉效果""" + + def __init__(self, screen): + self.screen = screen + self.width, self.height = screen.get_size() + + # 爆炸效果 + self.explosion_effects = [] + + # 激光效果 + self.laser_effects = [] + + # 粒子效果 + self.particle_effects = [] + + def add_explosion_effect(self, position, size='small'): + """添加爆炸效果""" + explosion = { + 'position': position, + 'size': size, + 'frames': [], + 'current_frame': 0, + 'frame_delay': 0, + 'active': True + } + + # 根据爆炸大小加载不同的图像 + if size == 'small': + for i in range(1, 5): + image = pygame.image.load(f"material/image/explosion_small_{i}.png") + explosion['frames'].append(image) + elif size == 'medium': + for i in range(1, 7): + image = pygame.image.load(f"material/image/explosion_medium_{i}.png") + explosion['frames'].append(image) + elif size == 'large': + for i in range(1, 9): + image = pygame.image.load(f"material/image/explosion_large_{i}.png") + explosion['frames'].append(image) + + self.explosion_effects.append(explosion) + + def add_laser_effect(self, start_pos, end_pos, color=(255, 0, 0), duration=5): + """添加激光效果""" + laser = { + 'start_pos': start_pos, + 'end_pos': end_pos, + 'color': color, + 'width': 3, + 'duration': duration, + 'active': True + } + + self.laser_effects.append(laser) + + def add_particle_effect(self, position, count=10, color=(255, 255, 255), size=2, speed=5): + """添加粒子效果""" + particles = [] + + for _ in range(count): + angle = random.uniform(0, 360) + x_speed = speed * random.cos(angle) + y_speed = speed * random.sin(angle) + + particle = { + 'position': list(position), + 'velocity': (x_speed, y_speed), + 'color': color, + 'size': size, + 'life': random.randint(10, 30), + 'active': True + } + + particles.append(particle) + + self.particle_effects.append({ + 'particles': particles, + 'active': True + }) + + def update(self): + """更新所有效果""" + # 更新爆炸效果 + for explosion in self.explosion_effects[:]: + if not explosion['active']: + self.explosion_effects.remove(explosion) + continue + + explosion['frame_delay'] += 1 + if explosion['frame_delay'] >= 3: + explosion['current_frame'] += 1 + explosion['frame_delay'] = 0 + + if explosion['current_frame'] >= len(explosion['frames']): + explosion['active'] = False + + # 更新激光效果 + for laser in self.laser_effects[:]: + if not laser['active']: + self.laser_effects.remove(laser) + continue + + laser['duration'] -= 1 + if laser['duration'] <= 0: + laser['active'] = False + + # 更新粒子效果 + for effect in self.particle_effects[:]: + if not effect['active']: + self.particle_effects.remove(effect) + continue + + active_particles = [] + for particle in effect['particles']: + if not particle['active']: + continue + + particle['position'][0] += particle['velocity'][0] + particle['position'][1] += particle['velocity'][1] + particle['life'] -= 1 + + if particle['life'] > 0: + active_particles.append(particle) + + if not active_particles: + effect['active'] = False + else: + effect['particles'] = active_particles + + def draw(self): + """绘制所有效果""" + # 绘制爆炸效果 + for explosion in self.explosion_effects: + if explosion['active']: + image = explosion['frames'][explosion['current_frame']] + rect = image.get_rect(center=explosion['position']) + self.screen.blit(image, rect) + + # 绘制激光效果 + for laser in self.laser_effects: + if laser['active']: + pygame.draw.line(self.screen, laser['color'], laser['start_pos'], laser['end_pos'], laser['width']) + + # 绘制粒子效果 + for effect in self.particle_effects: + for particle in effect['particles']: + if particle['active']: + pygame.draw.circle(self.screen, particle['color'], + (int(particle['position'][0]), int(particle['position'][1])), + particle['size'])