import tkinter as tk
from tkinter import messagebox, font
import random
import time
import math
# ==================== 常量 ====================
WORLD_W = 20000
WORLD_H = 20000
VIEW_W = 1024
VIEW_H = 668
FOV_ANGLE = math.pi / 2
FOV_RADIUS = 450
PLAYER_R = 16
PLAYER_SPEED = 10
WALL_T = 20
DOOR_W = 100
INTERACT_RANGE = 90
RESPAWN_TIME = 600 # 10分钟
DOOR_HEALTH = 100 # 门的生命值,可被枪打烂
MEDKIT_HEAL = 50 # 医疗包回血量
WEAPON_DATA = {
"pistol": {"name": "手枪", "damage": 34, "range": 250, "auto": False, "w": 24, "h": 8, "color": "#ffd700", "cooldown": 300},
"smg": {"name": "冲锋枪", "damage": 40, "range": 380, "auto": True, "w": 38, "h": 9, "color": "#ffd700", "cooldown": 100},
"rifle": {"name": "突击步枪", "damage": 50, "range": 550, "auto": True, "w": 50, "h": 11, "color": "#ffd700", "cooldown": 120},
}
MONSTER_DATA = {
"watcher": {"name": "窥视者", "health": 50000, "speed": 8, "damage": 50, "radius": 30, "color": "#4a0080", "attack_range": 50, "attack_cd": 800},
"rusher": {"name": "冲锋者", "health": 100000, "speed": 3, "damage": 50, "radius": 25, "color": "#8b0000", "attack_range": 45, "attack_cd": 600},
"able": {"name": "Able", "health": 100000, "speed": 3, "damage": 50, "radius": 25, "color": "#0066cc", "attack_range": 60, "attack_cd": 600},
"zombie": {"name": "丧尸", "health": 100, "speed": 2.5, "damage": 10, "radius": 12, "color": "#2d5016", "attack_range": 30, "attack_cd": 500},
}
class Game:
def __init__(self, root):
self.root = root
self.root.title("SCP: Breakout 突围")
self.root.geometry("1024x768")
self.root.resizable(False, False)
self.font_family = "SimHei"
self.game_active = False
self.story_mode = False
self.cam_x = 0
self.cam_y = 0
self.key_pressed = {"w": False, "a": False, "s": False, "d": False}
self.mouse_x = VIEW_W // 2
self.mouse_y = VIEW_H // 2
self.mouse_down = False
self.shoot_cooldown = 0
# 实体
self.rooms = []
self.walls = []
self.doors = []
self.items = []
self.monsters = []
self.generators = []
self.comms = None
self.bullets = []
self.player = None
# 背包
self.inventory = {"weapons": [], "documents": 0, "wrench": False, "current": None}
# 任务
self.tasks = {"generators_done": 0, "comms_done": False, "extraction_ready": False}
# 提示
self.log_lines = []
self.game_over = False
self.start_time = 0
self.create_start_screen()
# ==================== 开始界面 ====================
def create_start_screen(self):
for w in self.root.winfo_children():
w.destroy()
f = tk.Frame(self.root, bg="black")
f.pack(fill=tk.BOTH, expand=True)
tf = font.Font(family=self.font_family, size=36, weight="bold")
tk.Label(f, text="SCP: Breakout", font=tf, fg="white", bg="black").pack(pady=40)
tk.Label(f, text="突 围", font=(self.font_family, 22), fg="#888", bg="black").pack(pady=5)
tk.Label(f, text="BC2O270371", font=(self.font_family, 12), fg="#555", bg="black").pack(pady=10)
tk.Label(f, text="WASD 移动 | 鼠标瞄准 | 左键 交互/射击 | 1/2/3 切换武器",
font=(self.font_family, 13), fg="#aaa", bg="black").pack(pady=15)
tk.Label(f, text="目标:收集20份机密文件 → 修复4台发电机 → 修复通讯 → 返回停车场撤离",
font=(self.font_family, 12), fg="#e94560", bg="black").pack(pady=5)
bf = tk.Frame(f, bg="black")
bf.pack(pady=60)
tk.Button(bf, text="开始", command=self.start_game, font=(self.font_family, 16),
bg="#8B0000", fg="white", width=14, height=2, cursor="hand2").pack()
def start_game(self):
self.create_game_screen()
self.generate_map()
self.spawn_player()
self.spawn_items()
self.spawn_monsters()
self.spawn_objectives()
self.game_active = False
self.game_over = False
self.start_time = time.time()
self.setup_controls()
self.add_log("通讯中断。你被困在设施内。")
self.add_log("目标:收集文件、修复电力与通讯,然后撤离。")
# 先显示背景故事,按Shift跳过
self.show_story()
# ==================== 背景故事 ====================
def show_story(self):
self.story_mode = True
self.render()
# 半透明黑色遮罩
self.canvas.create_rectangle(0, 0, VIEW_W, VIEW_H, fill="black", stipple="gray75")
story_lines = [
"受混沌分裂者武装入侵影响,本设施全域发生大规模收容失效,",
"各项收容协议与安保系统相继瘫痪。",
"",
"我是一名持有四级权限的科学家,在确认地下避难所彻底沦陷、",
"无安全滞留条件后,即刻撤离至地面停车场",
"计划驾驶车辆紧急撤离设施区域。",
"",
"正当撤离准备工作就绪、即将驶离之际,",
"设施全自动应急封锁程序全面启动",
"停车场出入口大门强制闭锁,彻底切断外部撤离通道",
"个人紧急撤离方案终止。",
"",
"迫于现场安保封锁状态,本人只能滞留于停车场区域",
"静待外部救援梯队抵达。",
"",
"在原地待命期间,基于四级权限对应的保密职责与应急处置使命,",
"我即刻判定现场存在大量未及时销毁的核心机密文件,",
"此类涉密资料一旦落入敌对势力手中,",
"将对设施安全与全域收容体系造成致命威胁。",
"",
"因此,我计划全面搜集散落的机密文件,",
"并依规完成销毁处置,杜绝涉密信息泄露风险。",
"",
"",
"—— 按 Shift 键开始任务 ——",
]
y = 60
for line in story_lines:
if line == "":
y += 14
continue
color = "#ffd700" if "Shift" in line else "#cccccc"
self.canvas.create_text(VIEW_W // 2, y, text=line, fill=color, font=(self.font_family, 14))
y += 26
# 绑定Shift跳过
self.root.bind("<Shift_L>", self.skip_story)
self.root.bind("<Shift_R>", self.skip_story)
def skip_story(self, event=None):
if not self.story_mode:
return
self.story_mode = False
self.root.unbind("<Shift_L>")
self.root.unbind("<Shift_R>")
self.game_active = True
self.game_loop()
def create_game_screen(self):
for w in self.root.winfo_children():
w.destroy()
self.game_frame = tk.Frame(self.root, bg="#0a0a12")
self.game_frame.pack(fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(self.game_frame, bg="black", width=VIEW_W, height=VIEW_H, highlightthickness=0)
self.canvas.pack()
self.hud_frame = tk.Frame(self.game_frame, bg="#0a0a12", height=100)
self.hud_frame.pack(fill=tk.X)
self.hud_left = tk.Label(self.hud_frame, text="", font=(self.font_family, 11), fg="#ccc",
bg="#0a0a12", anchor="w", justify="left")
self.hud_left.pack(side=tk.LEFT, padx=15, pady=5)
self.hud_right = tk.Label(self.hud_frame, text="", font=(self.font_family, 11), fg="#ccc",
bg="#0a0a12", anchor="e", justify="right")
self.hud_right.pack(side=tk.RIGHT, padx=15, pady=5)
self.hud_center = tk.Label(self.hud_frame, text="", font=(self.font_family, 12), fg="#ffd700", bg="#0a0a12")
self.hud_center.pack(side=tk.TOP, pady=3)
# ==================== 地图生成 ====================
def generate_map(self):
self.rooms = []
self.doors = []
R = [
# 修改1:停车场新增底部门,连通收容区走廊
{"x1": 4800, "y1": 1400, "x2": 8400, "y2": 4600, "name": "停车场", "color": "#1e1e2a",
"doors": [{"side": "left", "pos": 3000}, {"side": "bottom", "pos": 6500}]},
{"x1": 2800, "y1": 2000, "x2": 4800, "y2": 3000, "name": "食堂", "color": "#2a2520",
"doors": [{"side": "right", "pos": 2500}, {"side": "bottom", "pos": 3800}]},
{"x1": 2800, "y1": 1000, "x2": 4800, "y2": 2000, "name": "维修办公室", "color": "#202828",
"doors": [{"side": "right", "pos": 1500}, {"side": "bottom", "pos": 3800}]},
{"x1": 1400, "y1": 1400, "x2": 2800, "y2": 3600, "name": "员工宿舍", "color": "#202520",
"doors": [{"side": "right", "pos": 2500}]},
# 修改2:研究办公室新增右侧门,连通停车场
{"x1": 2800, "y1": 3600, "x2": 5400, "y2": 4600, "name": "研究办公室", "color": "#1e1e30",
"doors": [{"side": "top", "pos": 3800}, {"side": "bottom", "pos": 4000}, {"side": "left", "pos": 4100}, {"side": "right", "pos": 4100}]},
{"x1": 1400, "y1": 3600, "x2": 2800, "y2": 5000, "name": "行政办公室", "color": "#282020",
"doors": [{"side": "right", "pos": 4100}, {"side": "bottom", "pos": 2100}]},
{"x1": 2800, "y1": 5000, "x2": 4400, "y2": 6200, "name": "收容室B", "color": "#301818",
"doors": [{"side": "top", "pos": 3600}, {"side": "left", "pos": 5600}, {"side": "right", "pos": 5600}]},
{"x1": 1400, "y1": 5000, "x2": 2800, "y2": 6200, "name": "废物管理", "color": "#222222",
"doors": [{"side": "right", "pos": 5600}, {"side": "top", "pos": 2100}]},
# 修改3:收容室A新增右侧门,连通收容区走廊
{"x1": 4400, "y1": 5000, "x2": 6400, "y2": 6800, "name": "收容室A", "color": "#301818",
"doors": [{"side": "top", "pos": 5400}, {"side": "left", "pos": 5600}, {"side": "bottom", "pos": 5400}, {"side": "right", "pos": 5900}]},
{"x1": 2800, "y1": 6800, "x2": 4800, "y2": 8000, "name": "初步测试室", "color": "#182828",
"doors": [{"side": "top", "pos": 3600}, {"side": "left", "pos": 7400}, {"side": "right", "pos": 7400}]},
{"x1": 1400, "y1": 6200, "x2": 2800, "y2": 8000, "name": "D级宿舍", "color": "#252520",
"doors": [{"side": "right", "pos": 7400}, {"side": "top", "pos": 2100}]},
# ----- 新增:东侧走廊 -----
{"x1": 4800, "y1": 7300, "x2": 9800, "y2": 7500, "name": "东侧走廊-横段", "color": "#1a1a24",
"doors": [{"side": "left", "pos": 7400}, {"side": "right", "pos": 7400}]},
{"x1": 9800, "y1": 7300, "x2": 10000, "y2": 9500, "name": "东侧走廊-竖段", "color": "#1a1a24",
"doors": [{"side": "left", "pos": 7400}, {"side": "right", "pos": 9500}]},
# --------------------------
# 修改4:新增收容区走廊,连接收容室A与停车场
{"x1": 6400, "y1": 4600, "x2": 6600, "y2": 6000, "name": "收容区走廊", "color": "#1a1a24",
"doors": [{"side": "left", "pos": 5900}, {"side": "top", "pos": 6500}]},
{"x1": 10000, "y1": 9400, "x2": 11400, "y2": 10400, "name": "军械库", "color": "#302818",
"doors": [{"side": "bottom", "pos": 10700}, {"side": "left", "pos": 9500}]},
# 修改5:发电机房新增右侧门,连通D级收容区
{"x1": 11400, "y1": 9400, "x2": 12800, "y2": 10400, "name": "发电机房", "color": "#282818",
"doors": [{"side": "bottom", "pos": 12100}, {"side": "left", "pos": 9900}, {"side": "right", "pos": 10150}]},
# 修改6:D级收容区左侧门调整位置,与发电机房对齐
{"x1": 12800, "y1": 9900, "x2": 14600, "y2": 11400, "name": "D级收容区", "color": "#301818",
"doors": [{"side": "top", "pos": 13700}, {"side": "left", "pos": 10150}]},
{"x1": 14600, "y1": 9900, "x2": 16000, "y2": 11400, "name": "生物危害区", "color": "#183018",
"doors": [{"side": "top", "pos": 15300}, {"side": "left", "pos": 10650}]},
{"x1": 10000, "y1": 10900, "x2": 12000, "y2": 12400, "name": "异常部门", "color": "#281830",
"doors": [{"side": "top", "pos": 11000}, {"side": "right", "pos": 11650}]},
{"x1": 12000, "y1": 11400, "x2": 13600, "y2": 12900, "name": "简报室", "color": "#182030",
"doors": [{"side": "top", "pos": 12800}, {"side": "left", "pos": 11650}, {"side": "right", "pos": 12150}]},
{"x1": 10000, "y1": 12900, "x2": 12000, "y2": 14400, "name": "避难所", "color": "#182820",
"doors": [{"side": "top", "pos": 1100}]},
{"x1": 13600, "y1": 11400, "x2": 15200, "y2": 12900, "name": "异常收容区", "color": "#301828",
"doors": [{"side": "top", "pos": 14400}, {"side": "left", "pos": 12150}]},
{"x1": 15200, "y1": 10400, "x2": 16800, "y2": 11900, "name": "医疗室", "color": "#182828",
"doors": [{"side": "bottom", "pos": 16000}, {"side": "left", "pos": 11150}]},
{"x1": 15200, "y1": 11900, "x2": 16800, "y2": 13400, "name": "通讯室", "color": "#282818",
"doors": [{"side": "top", "pos": 16000}, {"side": "left", "pos": 12650}]},
{"x1": 16800, "y1": 9400, "x2": 18800, "y2": 11400, "name": "X收容区", "color": "#381818",
"doors": [{"side": "bottom", "pos": 17800}, {"side": "left", "pos": 10400}]},
]
self.rooms = R
for room in self.rooms:
for d in room.get("doors", []):
self._add_door(room, d["side"], d["pos"])
self.generate_walls()
def _add_door(self, room, side, pos):
# 计算门的实际坐标
if side == "top":
x, y, ori = pos, room["y1"], "h"
elif side == "bottom":
x, y, ori = pos, room["y2"], "h"
elif side == "left":
x, y, ori = room["x1"], pos, "v"
elif side == "right":
x, y, ori = room["x2"], pos, "v"
else:
return
# 去重:相邻房间共享墙上的配对门只保留一扇(先定义的优先)
for d in self.doors:
if d["ori"] == ori and abs(d["x"] - x) < 5 and abs(d["y"] - y) < 5:
return
self.doors.append({"x": x, "y": y, "w": DOOR_W, "ori": ori, "open": False, "broken": False,
"health": DOOR_HEALTH, "room": room["name"]})
def generate_walls(self):
self.walls = []
t = WALL_T
for room in self.rooms:
x1, y1, x2, y2 = room["x1"], room["y1"], room["x2"], room["y2"]
dh1 = [d for d in self.doors if d["ori"] == "h" and abs(d["y"] - y1) < 5]
dh2 = [d for d in self.doors if d["ori"] == "h" and abs(d["y"] - y2) < 5]
dv1 = [d for d in self.doors if d["ori"] == "v" and abs(d["x"] - x1) < 5]
dv2 = [d for d in self.doors if d["ori"] == "v" and abs(d["x"] - x2) < 5]
self._wall_h(x1, x2, y1 - t // 2, y1 + t // 2, dh1)
self._wall_h(x1, x2, y2 - t // 2, y2 + t // 2, dh2)
self._wall_v(y1, y2, x1 - t // 2, x1 + t // 2, dv1)
self._wall_v(y1, y2, x2 - t // 2, x2 + t // 2, dv2)
self.walls.append((500, 500, 19500, 520))
self.walls.append((500, 19480, 19500, 19500))
self.walls.append((500, 500, 520, 19500))
self.walls.append((19480, 500, 19500, 19500))
def _wall_h(self, x1, x2, y1, y2, doors):
ds = sorted(doors, key=lambda d: d["x"])
cur = x1
for d in ds:
dl, dr = d["x"] - d["w"] // 2, d["x"] + d["w"] // 2
if dl > cur + 2:
self.walls.append((cur, y1, dl, y2))
cur = dr
if cur < x2 - 2:
self.walls.append((cur, y1, x2, y2))
def _wall_v(self, y1, y2, x1, x2, doors):
ds = sorted(doors, key=lambda d: d["y"])
cur = y1
for d in ds:
dt, db = d["y"] - d["w"] // 2, d["y"] + d["w"] // 2
if dt > cur + 2:
self.walls.append((x1, cur, x2, dt))
cur = db
if cur < y2 - 2:
self.walls.append((x1, cur, x2, y2))
def get_closed_doors_rects(self):
rects = []
for d in self.doors:
if d["open"] or d["broken"]:
continue
if d["ori"] == "h":
rects.append((d["x"] - d["w"] // 2, d["y"] - WALL_T // 2, d["x"] + d["w"] // 2, d["y"] + WALL_T // 2))
else:
rects.append((d["x"] - WALL_T // 2, d["y"] - d["w"] // 2, d["x"] + WALL_T // 2, d["y"] + d["w"] // 2))
return rects
# ==================== 玩家 & 道具生成 ====================
def spawn_player(self):
self.player = {"x": 6600, "y": 3000, "health": 100, "max_health": 100,
"direction": 0, "is_player": True, "role": "survivor", "hurt_cd": 0}
def spawn_items(self):
self.items = []
fixed = [
("pistol", 3800, 1500),
("smg", 4000, 4100),
("rifle", 10700, 9900),
("wrench", 3500, 1500),
]
for typ, x, y in fixed:
self.items.append({"type": typ, "x": x, "y": y, "taken": False})
doc_rooms = [r for r in self.rooms if r["name"] != "停车场"]
for i in range(20):
room = random.choice(doc_rooms)
for _ in range(50):
x = random.randint(room["x1"] + 60, room["x2"] - 60)
y = random.randint(room["y1"] + 60, room["y2"] - 60)
if not self.check_collision(x, y, 12):
self.items.append({"type": "document", "x": x, "y": y, "taken": False})
break
# 医疗包:只在远离出生点(停车场)的南翼区域随机生成8个
far_rooms = [r for r in self.rooms if r["name"] not in
["停车场", "食堂", "维修办公室", "员工宿舍", "研究办公室", "行政办公室",
"收容室B", "废物管理", "收容室A", "初步测试室", "D级宿舍"]]
for i in range(8):
room = random.choice(far_rooms)
for _ in range(50):
x = random.randint(room["x1"] + 60, room["x2"] - 60)
y = random.randint(room["y1"] + 60, room["y2"] - 60)
if not self.check_collision(x, y, 12):
self.items.append({"type": "medkit", "x": x, "y": y, "taken": False})
break
def spawn_objectives(self):
self.generators = [
{"x": 12100, "y": 9900, "repaired": False, "name": "发电机#1"},
{"x": 3800, "y": 7400, "repaired": False, "name": "发电机#2"},
{"x": 5400, "y": 5900, "repaired": False, "name": "发电机#3"},
{"x": 11000, "y": 11650, "repaired": False, "name": "发电机#4"},
]
self.comms = {"x": 16000, "y": 12650, "repaired": False, "name": "通讯终端"}
def spawn_monsters(self):
self.monsters = []
# 窥视者:X收容区
self._add_monster("watcher", 17800, 10400, 17800, 10400, 900)
# 冲锋者:收容室A
self._add_monster("rusher", 5400, 5900, 5400, 5900, 700)
# Able:异常收容区(原冲锋者替换)
self._add_monster("able", 14400, 12150, 14400, 12150, 700)
# 丧尸:分散在各危险区域
zombie_spawns = [
(3600, 2500, 400), (3600, 4100, 400), (2100, 4300, 350),
(3600, 5600, 400), (2100, 5600, 350), (5400, 5900, 500),
(3800, 7400, 400), (2100, 7100, 350), (10700, 9900, 300),
(12100, 9900, 350), (13700, 10650, 400), (15300, 10650, 400),
(11000, 11650, 400), (12800, 12150, 400), (11000, 13650, 400),
(14400, 12150, 400), (16000, 11150, 350), (16000, 12650, 350),
(17800, 10400, 500), (3800, 1500, 300), (2100, 2500, 350),
]
for i, (x, y, tr) in enumerate(zombie_spawns):
self._add_monster("zombie", x, y, x, y, tr)
# 补足到50只,随机撒在非停车场区域
safe_rooms = ["停车场"]
danger_rooms = [r for r in self.rooms if r["name"] not in safe_rooms]
while len([m for m in self.monsters if m["type"] == "zombie"]) < 50:
room = random.choice(danger_rooms)
for _ in range(30):
x = random.randint(room["x1"] + 80, room["x2"] - 80)
y = random.randint(room["y1"] + 80, room["y2"] - 80)
if not self.check_collision(x, y, 12):
self._add_monster("zombie", x, y, x, y, random.randint(300, 500))
break
def _add_monster(self, mtype, x, y, tx, ty, tr):
d = MONSTER_DATA[mtype]
self.monsters.append({
"type": mtype, "x": x, "y": y,
"health": d["health"], "max_health": d["health"],
"speed": d["speed"], "damage": d["damage"], "radius": d["radius"],
"color": d["color"], "attack_range": d["attack_range"],
"attack_cd_max": d["attack_cd"], "attack_cd": 0,
"territory_x": tx, "territory_y": ty, "territory_r": tr,
"alive": True, "death_time": 0, "active": False,
"attack_anim": 0, # 攻击动画计时器
})
# ==================== 碰撞检测 ====================
def check_collision(self, x, y, r):
if x - r < 500 or x + r > 19500 or y - r < 500 or y + r > 19500:
return True
all_rects = self.walls + self.get_closed_doors_rects()
for w in all_rects:
cx = max(w[0], min(x, w[2]))
cy = max(w[1], min(y, w[3]))
if (x - cx) ** 2 + (y - cy) ** 2 < r * r:
return True
return False
# ==================== 射线检测(射击) ====================
def raycast_wall_dist(self, x0, y0, angle, max_dist):
dx, dy = math.cos(angle), math.sin(angle)
nearest = max_dist
for rect in self.walls + self.get_closed_doors_rects():
d = self._ray_rect(x0, y0, dx, dy, rect)
if d is not None and d < nearest:
nearest = d
return nearest
def _ray_rect(self, ox, oy, dx, dy, rect):
x1, y1, x2, y2 = rect
tmin = 0.0
tmax = float('inf')
if abs(dx) < 1e-9:
if ox < x1 or ox > x2:
return None
else:
t1 = (x1 - ox) / dx
t2 = (x2 - ox) / dx
if t1 > t2:
t1, t2 = t2, t1
tmin = max(tmin, t1)
tmax = min(tmax, t2)
if tmin > tmax:
return None
if abs(dy) < 1e-9:
if oy < y1 or oy > y2:
return None
else:
t1 = (y1 - oy) / dy
t2 = (y2 - oy) / dy
if t1 > t2:
t1, t2 = t2, t1
tmin = max(tmin, t1)
tmax = min(tmax, t2)
if tmin > tmax:
return None
if tmin > 0:
return tmin
return None
def raycast_door(self, x0, y0, angle, max_dist):
"""射线检测最近的未损坏关闭门,返回(门对象, 距离)"""
dx, dy = math.cos(angle), math.sin(angle)
nearest = None
nd = max_dist
for d in self.doors:
if d["open"] or d["broken"]:
continue
if d["ori"] == "h":
rect = (d["x"] - d["w"] // 2, d["y"] - WALL_T // 2, d["x"] + d["w"] // 2, d["y"] + WALL_T // 2)
else:
rect = (d["x"] - WALL_T // 2, d["y"] - d["w"] // 2, d["x"] + WALL_T // 2, d["y"] + d["w"] // 2)
dist = self._ray_rect(x0, y0, dx, dy, rect)
if dist is not None and dist < nd:
nd = dist
nearest = d
return nearest, nd
def raycast_monster(self, x0, y0, angle, max_dist):
dx, dy = math.cos(angle), math.sin(angle)
wall_dist = self.raycast_wall_dist(x0, y0, angle, max_dist)
nearest = None
nd = wall_dist
for m in self.monsters:
if not m["alive"]:
continue
ex, ey = m["x"] - x0, m["y"] - y0
proj = ex * dx + ey * dy
if proj < 0 or proj > nd:
continue
perp = abs(ex * (-dy) + ey * dx)
if perp <= m["radius"]:
nd = proj
nearest = m
return nearest, nd
# ==================== 玩家控制 ====================
def setup_controls(self):
self.root.bind("<KeyPress>", self.on_key_press)
self.root.bind("<KeyRelease>", self.on_key_release)
self.root.bind("<Motion>", self.on_mouse_move)
self.root.bind("<ButtonPress-1>", self.on_mouse_down)
self.root.bind("<ButtonRelease-1>", self.on_mouse_up)
self.root.bind("1", lambda e: self.switch_weapon(0))
self.root.bind("2", lambda e: self.switch_weapon(1))
self.root.bind("3", lambda e: self.switch_weapon(2))
def on_key_press(self, e):
k = e.keysym.lower()
if k in self.key_pressed:
self.key_pressed[k] = True
def on_key_release(self, e):
k = e.keysym.lower()
if k in self.key_pressed:
self.key_pressed[k] = False
def on_mouse_move(self, e):
self.mouse_x = e.x
self.mouse_y = e.y
if self.player:
wx = e.x + self.cam_x
wy = e.y + self.cam_y
self.player["direction"] = math.atan2(wy - self.player["y"], wx - self.player["x"])
def on_mouse_down(self, e):
if self.story_mode:
return
self.mouse_down = True
if not self.player or self.game_over:
return
wx = e.x + self.cam_x
wy = e.y + self.cam_y
target = self.find_interactable(wx, wy)
if target:
self.interact(target)
else:
self.try_shoot()
def on_mouse_up(self, e):
self.mouse_down = False
def switch_weapon(self, idx):
if idx < len(self.inventory["weapons"]):
self.inventory["current"] = self.inventory["weapons"][idx]
self.add_log(f"切换至 {WEAPON_DATA[self.inventory['current']]['name']}")
def try_shoot(self):
if self.shoot_cooldown > 0:
return
if self.inventory["current"] is None:
self.add_log("没有武器!")
return
wd = WEAPON_DATA[self.inventory["current"]]
self.shoot_cooldown = wd["cooldown"]
angle = self.player["direction"]
# 先检测是否命中门(门可以被打烂)
door, door_dist = self.raycast_door(self.player["x"], self.player["y"], angle, wd["range"])
wall_dist = self.raycast_wall_dist(self.player["x"], self.player["y"], angle, wd["range"])
if door and door_dist <= wall_dist + 1:
# 子弹打在门上
end_x = self.player["x"] + math.cos(angle) * door_dist
end_y = self.player["y"] + math.sin(angle) * door_dist
self.bullets.append({"x1": self.player["x"], "y1": self.player["y"], "x2": end_x, "y2": end_y, "time": 80})
door["health"] -= wd["damage"]
if door["health"] <= 0:
door["broken"] = True
door["open"] = True
self.add_log(f"{door['room']} 门已被摧毁!")
else:
self.add_log(f"射击 {door['room']} 门(剩余耐久 {door['health']})")
return
# 正常射击怪物
target, dist = self.raycast_monster(self.player["x"], self.player["y"], angle, wd["range"])
end_x = self.player["x"] + math.cos(angle) * dist
end_y = self.player["y"] + math.sin(angle) * dist
self.bullets.append({"x1": self.player["x"], "y1": self.player["y"], "x2": end_x, "y2": end_y, "time": 80})
if target:
target["health"] -= wd["damage"]
if target["health"] <= 0:
self.kill_monster(target)
def kill_monster(self, m):
m["alive"] = False
m["death_time"] = time.time()
d = MONSTER_DATA[m["type"]]
self.add_log(f"击杀 {d['name']}!(10分钟后在其领地重生)")
def update_player(self):
if not self.player or self.game_over:
return
dx = dy = 0
if self.key_pressed["w"]:
dy -= PLAYER_SPEED
if self.key_pressed["s"]:
dy += PLAYER_SPEED
if self.key_pressed["a"]:
dx -= PLAYER_SPEED
if self.key_pressed["d"]:
dx += PLAYER_SPEED
if dx and dy:
n = 1 / math.sqrt(2)
dx *= n
dy *= n
if not self.check_collision(self.player["x"] + dx, self.player["y"], PLAYER_R):
self.player["x"] += dx
if not self.check_collision(self.player["x"], self.player["y"] + dy, PLAYER_R):
self.player["y"] += dy
if self.player["hurt_cd"] > 0:
self.player["hurt_cd"] -= 16
def update_camera(self):
if not self.player:
return
tx = self.player["x"] - VIEW_W / 2
ty = self.player["y"] - VIEW_H / 2
self.cam_x = max(0, min(tx, WORLD_W - VIEW_W))
self.cam_y = max(0, min(ty, WORLD_H - VIEW_H))
# ==================== 怪物AI ====================
def update_monsters(self):
if self.game_over:
return
now = time.time()
for m in self.monsters:
if not m["alive"]:
# respawn检查
if now - m["death_time"] >= RESPAWN_TIME:
m["alive"] = True
m["health"] = m["max_health"]
m["x"] = m["territory_x"]
m["y"] = m["territory_y"]
m["active"] = False
m["attack_anim"] = 0
self.add_log(f"{MONSTER_DATA[m['type']]['name']} 已在领地重生")
continue
if m["attack_cd"] > 0:
m["attack_cd"] -= 16
# 攻击动画递减
if m["attack_anim"] > 0:
m["attack_anim"] -= 1
px, py = self.player["x"], self.player["y"]
dist_to_player = math.sqrt((m["x"] - px) ** 2 + (m["y"] - py) ** 2)
dist_to_territory = math.sqrt((m["x"] - m["territory_x"]) ** 2 + (m["y"] - m["territory_y"]) ** 2)
player_in_territory = math.sqrt((px - m["territory_x"]) ** 2 + (py - m["territory_y"]) ** 2) < m["territory_r"]
# 激活/脱离逻辑
if player_in_territory:
m["active"] = True
elif not player_in_territory and dist_to_player > m["territory_r"] * 1.5:
m["active"] = False
# 行为
if m["active"]:
if m["type"] == "watcher":
# 窥视者:玩家看着它就不动,不看就高速突袭
being_watched = self.is_player_looking_at(m)
if not being_watched:
self.move_monster_toward(m, px, py, m["speed"])
elif m["type"] in ["rusher", "able", "zombie"]:
self.move_monster_toward(m, px, py, m["speed"])
# 攻击玩家
if dist_to_player < m["attack_range"] + PLAYER_R and m["attack_cd"] <= 0:
self.player["health"] -= m["damage"]
m["attack_cd"] = m["attack_cd_max"]
# Able触发横扫动画
if m["type"] == "able":
m["attack_anim"] = 10
self.add_log(f"受到 {MONSTER_DATA[m['type']]['name']} 攻击!-{m['damage']}HP")
if self.player["health"] <= 0:
self.player_dies()
else:
# 返回领地中心
if dist_to_territory > 30:
self.move_monster_toward(m, m["territory_x"], m["territory_y"], m["speed"] * 0.5)
def is_player_looking_at(self, m):
"""判断玩家是否正看着怪物(在FOV内且无墙遮挡且距离够近)"""
dx = m["x"] - self.player["x"]
dy = m["y"] - self.player["y"]
dist = math.sqrt(dx*dx + dy*dy)
if dist > FOV_RADIUS:
return False
angle_to = math.atan2(dy, dx)
diff = abs(angle_to - self.player["direction"]) % (2 * math.pi)
if diff > math.pi:
diff = 2 * math.pi - diff
if diff > FOV_ANGLE / 2:
return False
# 无墙遮挡
wall_d = self.raycast_wall_dist(self.player["x"], self.player["y"], angle_to, dist)
return wall_d >= dist - 5
def move_monster_toward(self, m, tx, ty, speed):
dx = tx - m["x"]
dy = ty - m["y"]
dist = math.sqrt(dx*dx + dy*dy)
if dist < 2:
return
mx = (dx / dist) * speed
my = (dy / dist) * speed
if not self.check_collision(m["x"] + mx, m["y"], m["radius"]):
m["x"] += mx
elif not self.check_collision(m["x"] + mx * 0.5, m["y"], m["radius"]):
m["x"] += mx * 0.5
if not self.check_collision(m["x"], m["y"] + my, m["radius"]):
m["y"] += my
elif not self.check_collision(m["x"], m["y"] + my * 0.5, m["radius"]):
m["y"] += my * 0.5
def player_dies(self):
self.game_over = True
self.game_active = False
self.add_log("你已死亡...")
messagebox.showinfo("游戏结束", "你在设施中阵亡了。\n设施将永远保守它的秘密。")
self.root.after(500, self.create_start_screen)
# ==================== 交互系统 ====================
def find_interactable(self, wx, wy):
best = None
best_d = INTERACT_RANGE
px, py = self.player["x"], self.player["y"]
# 道具
for item in self.items:
if item["taken"]:
continue
d = math.sqrt((item["x"] - wx) ** 2 + (item["y"] - wy) ** 2)
if d < 50:
pd = math.sqrt((item["x"] - px) ** 2 + (item["y"] - py) ** 2)
if pd < INTERACT_RANGE and d < best_d:
best_d = d
best = {"kind": "item", "obj": item}
# 门(已损坏的门不可交互)
for door in self.doors:
if door["broken"]:
continue
d = math.sqrt((door["x"] - wx) ** 2 + (door["y"] - wy) ** 2)
if d < 60:
pd = math.sqrt((door["x"] - px) ** 2 + (door["y"] - py) ** 2)
if pd < INTERACT_RANGE + 30 and d < best_d:
best_d = d
best = {"kind": "door", "obj": door}
# 发电机
for g in self.generators:
if g["repaired"]:
continue
d = math.sqrt((g["x"] - wx) ** 2 + (g["y"] - wy) ** 2)
if d < 50:
pd = math.sqrt((g["x"] - px) ** 2 + (g["y"] - py) ** 2)
if pd < INTERACT_RANGE and d < best_d:
best_d = d
best = {"kind": "generator", "obj": g}
# 通讯终端
if self.comms and not self.comms["repaired"]:
c = self.comms
d = math.sqrt((c["x"] - wx) ** 2 + (c["y"] - wy) ** 2)
if d < 50:
pd = math.sqrt((c["x"] - px) ** 2 + (c["y"] - py) ** 2)
if pd < INTERACT_RANGE and d < best_d:
best_d = d
best = {"kind": "comms", "obj": c}
# 撤离点(停车场出生点,所有任务完成后可交互)
if self.tasks["extraction_ready"]:
ex, ey = 6600, 3000
d = math.sqrt((ex - wx) ** 2 + (ey - wy) ** 2)
if d < 80:
pd = math.sqrt((ex - px) ** 2 + (ey - py) ** 2)
if pd < INTERACT_RANGE + 20 and d < best_d:
best = {"kind": "extract", "obj": None}
return best
def interact(self, target):
kind = target["kind"]
if kind == "item":
self.pickup_item(target["obj"])
elif kind == "door":
self.toggle_door(target["obj"])
elif kind == "generator":
self.repair_generator(target["obj"])
elif kind == "comms":
self.repair_comms(target["obj"])
elif kind == "extract":
self.do_extraction()
def pickup_item(self, item):
item["taken"] = True
t = item["type"]
if t == "document":
self.inventory["documents"] += 1
self.add_log(f"获得机密文件 ({self.inventory['documents']}/20)")
self.check_tasks()
elif t == "wrench":
self.inventory["wrench"] = True
self.add_log("获得扳手,可修复电力与通讯系统")
elif t == "medkit":
heal = min(MEDKIT_HEAL, self.player["max_health"] - self.player["health"])
if heal > 0:
self.player["health"] += heal
self.add_log(f"使用医疗包,恢复 {heal} 点生命值")
else:
self.add_log("生命值已满,医疗包未消耗")
item["taken"] = False # 满血不消耗
elif t in WEAPON_DATA:
if t not in self.inventory["weapons"]:
self.inventory["weapons"].append(t)
if self.inventory["current"] is None:
self.inventory["current"] = t
self.add_log(f"获得 {WEAPON_DATA[t]['name']}(无限弹药)")
else:
self.add_log(f"已拥有 {WEAPON_DATA[t]['name']}")
def toggle_door(self, door):
if door["broken"]:
self.add_log("这扇门已被摧毁,无法操作")
return
door["open"] = not door["open"]
self.add_log(f"{door['room']} 门已{'开启' if door['open'] else '关闭'}")
def repair_generator(self, g):
if not self.inventory["wrench"]:
self.add_log("需要扳手才能修复发电机!")
return
g["repaired"] = True
self.tasks["generators_done"] += 1
self.add_log(f"{g['name']} 修复完成!({self.tasks['generators_done']}/4)")
self.check_tasks()
# 通讯终端修复增加前置条件
def repair_comms(self, c):
if not self.inventory["wrench"]:
self.add_log("需要扳手才能修复通讯终端!")
return
# 前置条件校验
if self.inventory["documents"] < 20:
self.add_log("需先收集全部20份机密文件,才能修复通讯终端!")
return
if self.tasks["generators_done"] < 4:
self.add_log("需先修复全部4台发电机,才能修复通讯终端!")
return
c["repaired"] = True
self.tasks["comms_done"] = True
self.add_log("通讯系统已修复!可以请求撤离了。")
self.check_tasks()
def check_tasks(self):
docs = self.inventory["documents"] >= 20
gens = self.tasks["generators_done"] >= 4
comms = self.tasks["comms_done"]
if docs and gens and comms and not self.tasks["extraction_ready"]:
self.tasks["extraction_ready"] = True
self.add_log("所有任务完成!返回停车场(出生点)请求撤离!")
def do_extraction(self):
self.game_over = True
self.game_active = False
elapsed = int(time.time() - self.start_time)
mins, secs = elapsed // 60, elapsed % 60
messagebox.showinfo("撤离成功",
f"你成功逃离了设施!\n\n"
f"用时:{mins}分{secs}秒\n"
f"机密文件:{self.inventory['documents']}/20\n"
f"发电机:{self.tasks['generators_done']}/4\n"
f"通讯:已修复\n\n"
f"设施的秘密被你带出了黑暗。")
self.root.after(500, self.create_start_screen)
# ==================== 日志 ====================
def add_log(self, text):
self.log_lines.append(text)
if len(self.log_lines) > 5:
self.log_lines.pop(0)
# ==================== 渲染 ====================
def render(self):
self.canvas.delete("all")
self.canvas.create_rectangle(0, 0, VIEW_W, VIEW_H, fill="black")
# 房间地板
for r in self.rooms:
x1 = r["x1"] - self.cam_x
y1 = r["y1"] - self.cam_y
x2 = r["x2"] - self.cam_x
y2 = r["y2"] - self.cam_y
if x2 < 0 or x1 > VIEW_W or y2 < 0 or y1 > VIEW_H:
continue
self.canvas.create_rectangle(x1, y1, x2, y2, fill=r["color"], outline="")
self.canvas.create_text((x1 + x2) // 2, (y1 + y2) // 2, text=r["name"],
fill="#3a3a4a", font=(self.font_family, 14))
# 墙壁
for w in self.walls:
x1, y1, x2, y2 = w[0] - self.cam_x, w[1] - self.cam_y, w[2] - self.cam_x, w[3] - self.cam_y
if x2 < 0 or x1 > VIEW_W or y2 < 0 or y1 > VIEW_H:
continue
self.canvas.create_rectangle(x1, y1, x2, y2, fill="#3a3a4a", outline="#555", width=1)
# 门
for d in self.doors:
if d["ori"] == "h":
x1, y1 = d["x"] - d["w"] // 2 - self.cam_x, d["y"] - WALL_T // 2 - self.cam_y
x2, y2 = d["x"] + d["w"] // 2 - self.cam_x, d["y"] + WALL_T // 2 - self.cam_y
else:
x1, y1 = d["x"] - WALL_T // 2 - self.cam_x, d["y"] - d["w"] // 2 - self.cam_y
x2, y2 = d["x"] + WALL_T // 2 - self.cam_x, d["y"] + d["w"] // 2 - self.cam_y
if x2 < -20 or x1 > VIEW_W + 20 or y2 < -20 or y1 > VIEW_H + 20:
continue
if d["broken"]:
# 被摧毁的门:画残骸
self.canvas.create_rectangle(x1, y1, x2, y2, fill="#2a1a0a", outline="#553311", width=1)
self.canvas.create_line(x1, y1, x2, y2, fill="#664422", width=1)
self.canvas.create_line(x1, y2, x2, y1, fill="#664422", width=1)
elif d["open"]:
self.canvas.create_rectangle(x1, y1, x2, y2, fill="", outline="#555", width=1, dash=(4, 4))
else:
self.canvas.create_rectangle(x1, y1, x2, y2, fill="#8B6914", outline="#a0782a", width=1)
# 门的耐久条(受伤时显示)
if d["health"] < DOOR_HEALTH:
bw = d["w"] if d["ori"] == "h" else WALL_T
pct = d["health"] / DOOR_HEALTH
if d["ori"] == "h":
self.canvas.create_rectangle(x1, y1 - 6, x2, y1 - 2, fill="#333", outline="")
self.canvas.create_rectangle(x1, y1 - 6, x1 + bw * pct, y1 - 2, fill="#ff8800", outline="")
else:
self.canvas.create_rectangle(x1 - 6, y1, x1 - 2, y2, fill="#333", outline="")
self.canvas.create_rectangle(x1 - 6, y1, x1 - 2, y1 + (y2 - y1) * pct, fill="#ff8800", outline="")
# 发电机
for g in self.generators:
sx, sy = g["x"] - self.cam_x, g["y"] - self.cam_y
if sx < -40 or sx > VIEW_W + 40 or sy < -40 or sy > VIEW_H + 40:
continue
color = "#00ff88" if g["repaired"] else "#666"
self.canvas.create_rectangle(sx - 20, sy - 25, sx + 20, sy + 25, fill=color, outline="#aaa", width=2)
self.canvas.create_text(sx, sy, text="⚡", fill="white", font=(self.font_family, 16))
if not g["repaired"]:
self.canvas.create_text(sx, sy + 38, text=g["name"], fill="#888", font=(self.font_family, 9))
# 通讯终端
if self.comms:
c = self.comms
sx, sy = c["x"] - self.cam_x, c["y"] - self.cam_y
if -40 < sx < VIEW_W + 40 and -40 < sy < VIEW_H + 40:
color = "#00ff88" if c["repaired"] else "#666"
self.canvas.create_rectangle(sx - 22, sy - 18, sx + 22, sy + 18, fill=color, outline="#aaa", width=2)
self.canvas.create_text(sx, sy, text="📡", fill="white", font=(self.font_family, 14))
# 道具
for item in self.items:
if item["taken"]:
continue
sx, sy = item["x"] - self.cam_x, item["y"] - self.cam_y
if sx < -30 or sx > VIEW_W + 30 or sy < -30 or sy > VIEW_H + 30:
continue
t = item["type"]
if t == "document":
self.canvas.create_rectangle(sx - 8, sy - 8, sx + 8, sy + 8, fill="white", outline="#ccc")
elif t == "wrench":
self.canvas.create_rectangle(sx - 12, sy - 4, sx + 12, sy + 4, fill="#888", outline="#aaa")
elif t == "medkit":
# 蓝色医疗包,带白色十字
self.canvas.create_rectangle(sx - 9, sy - 9, sx + 9, sy + 9, fill="#0066cc", outline="#66aaff", width=1)
self.canvas.create_rectangle(sx - 2, sy - 6, sx + 2, sy + 6, fill="white", outline="")
self.canvas.create_rectangle(sx - 6, sy - 2, sx + 6, sy + 2, fill="white", outline="")
elif t in WEAPON_DATA:
wd = WEAPON_DATA[t]
self.canvas.create_rectangle(sx - wd["w"] // 2, sy - wd["h"] // 2,
sx + wd["w"] // 2, sy + wd["h"] // 2,
fill=wd["color"], outline="#fff")
# 怪物
for m in self.monsters:
if not m["alive"]:
continue
sx, sy = m["x"] - self.cam_x, m["y"] - self.cam_y
if sx < -60 or sx > VIEW_W + 60 or sy < -60 or sy > VIEW_H + 60:
continue
r = m["radius"]
self.canvas.create_oval(sx - r, sy - r, sx + r, sy + r, fill=m["color"], outline="#000", width=2)
# Able的黑色长剑 + 横扫动画
if m["type"] == "able" and self.player:
angle = math.atan2(self.player["y"] - m["y"], self.player["x"] - m["x"])
# 横扫动画:角度从-0.5rad扫到+0.5rad
if m["attack_anim"] > 0:
progress = 1 - (m["attack_anim"] / 10)
angle += -0.5 + progress * 1.0
sword_len = 45
ex = sx + math.cos(angle) * sword_len
ey = sy + math.sin(angle) * sword_len
self.canvas.create_line(sx, sy, ex, ey, fill="black", width=4)
# 剑柄
hilt_len = 10
hx = sx - math.cos(angle) * hilt_len
hy = sy - math.sin(angle) * hilt_len
self.canvas.create_line(sx, sy, hx, hy, fill="#333", width=3)
# 血条(只对高血量怪物显示)
if m["max_health"] > 200:
bw = r * 2
hp_pct = m["health"] / m["max_health"]
self.canvas.create_rectangle(sx - bw // 2, sy - r - 10, sx + bw // 2, sy - r - 5, fill="#333", outline="")
self.canvas.create_rectangle(sx - bw // 2, sy - r - 10, sx - bw // 2 + bw * hp_pct, sy - r - 5,
fill="#e94560", outline="")
# 子弹
for b in self.bullets:
x1, y1 = b["x1"] - self.cam_x, b["y1"] - self.cam_y
x2, y2 = b["x2"] - self.cam_x, b["y2"] - self.cam_y
self.canvas.create_line(x1, y1, x2, y2, fill="#ffff00", width=2)
# 玩家
if self.player:
px, py = self.player["x"] - self.cam_x, self.player["y"] - self.cam_y
# 受伤闪烁
if self.player["hurt_cd"] > 0 and (self.player["hurt_cd"] // 100) % 2 == 0:
self.canvas.create_oval(px - PLAYER_R - 4, py - PLAYER_R - 4,
px + PLAYER_R + 4, py + PLAYER_R + 4,
fill="", outline="#ff0000", width=3)
self.canvas.create_oval(px - PLAYER_R, py - PLAYER_R, px + PLAYER_R, py + PLAYER_R,
fill="#e94560", outline="white", width=2)
ang = self.player["direction"]
self.canvas.create_line(px, py, px + math.cos(ang) * 35, py + math.sin(ang) * 35,
fill="yellow", width=2)
# 玩家血条
hp_pct = self.player["health"] / self.player["max_health"]
self.canvas.create_rectangle(px - 20, py - PLAYER_R - 14, px + 20, py - PLAYER_R - 8,
fill="#333", outline="")
self.canvas.create_rectangle(px - 20, py - PLAYER_R - 14, px - 20 + 40 * hp_pct, py - PLAYER_R - 8,
fill="#00ff88", outline="")
# 撤离点标记
if self.tasks["extraction_ready"]:
ex, ey = 6600 - self.cam_x, 3000 - self.cam_y
if -60 < ex < VIEW_W + 60 and -60 < ey < VIEW_H + 60:
self.canvas.create_oval(ex - 40, ey - 40, ex + 40, ey + 40,
fill="", outline="#00ff88", width=3, dash=(8, 4))
self.canvas.create_text(ex, ey - 55, text="撤离点", fill="#00ff88",
font=(self.font_family, 12, "bold"))
# 交互提示
wx = self.mouse_x + self.cam_x
wy = self.mouse_y + self.cam_y
target = self.find_interactable(wx, wy)
if target:
txt = self.get_interact_hint(target)
self.canvas.create_text(self.mouse_x, self.mouse_y - 30, text=txt,
fill="#ffd700", font=(self.font_family, 12))
# 扇形视野遮罩
self.draw_fov()
self.update_hud()
def get_interact_hint(self, target):
kind = target["kind"]
if kind == "item":
t = target["obj"]["type"]
if t == "document":
return "[左键] 拾取机密文件"
if t == "wrench":
return "[左键] 拾取扳手"
if t == "medkit":
return "[左键] 使用医疗包"
return f"[左键] 拾取 {WEAPON_DATA[t]['name']}"
if kind == "door":
d = target["obj"]
if d["broken"]:
return "门已被摧毁"
return f"[左键] {'关闭' if d['open'] else '开启'} {d['room']}门"
if kind == "generator":
return "[左键] 修复发电机(需扳手)"
if kind == "comms":
return "[左键] 修复通讯终端(需扳手)"
if kind == "extract":
return "[左键] 请求撤离"
return ""
def draw_fov(self):
if not self.player:
return
px = self.player["x"] - self.cam_x
py = self.player["y"] - self.cam_y
ang = self.player["direction"]
half = FOV_ANGLE / 2
far = 5000
steps = 72
for i in range(steps):
a1 = 2 * math.pi * i / steps
a2 = 2 * math.pi * (i + 1) / steps
mid = (a1 + a2) / 2
diff = abs(mid - ang) % (2 * math.pi)
if diff > math.pi:
diff = 2 * math.pi - diff
if diff > half:
x1 = px + math.cos(a1) * far
y1 = py + math.sin(a1) * far
x2 = px + math.cos(a2) * far
y2 = py + math.sin(a2) * far
self.canvas.create_polygon(px, py, x1, y1, x2, y2, fill="black", outline="")
def update_hud(self):
lines = []
if self.inventory["current"]:
wd = WEAPON_DATA[self.inventory["current"]]
lines.append(f"武器: {wd['name']} (伤害{wd['damage']} 射程{wd['range']})")
else:
lines.append("武器: 无(寻找武器!)")
owned = " | ".join(WEAPON_DATA[w]["name"] for w in self.inventory["weapons"]) or "无"
lines.append(f"武器库: {owned}")
lines.append(f"机密文件: {self.inventory['documents']}/20 | 扳手: {'✓' if self.inventory['wrench'] else '✗'}")
lines.append(f"发电机: {self.tasks['generators_done']}/4 | 通讯: {'✓' if self.tasks['comms_done'] else '✗'}")
if self.tasks["extraction_ready"]:
lines.append("★ 所有任务完成!返回停车场撤离 ★")
self.hud_left.config(text="\n".join(lines))
if self.player:
room_name = "未知区域"
for r in self.rooms:
if r["x1"] <= self.player["x"] <= r["x2"] and r["y1"] <= self.player["y"] <= r["y2"]:
room_name = r["name"]
break
hp = max(0, self.player["health"])
self.hud_right.config(text=f"HP: {hp}/{self.player['max_health']}\n位置: {room_name}\n坐标: ({int(self.player['x'])}, {int(self.player['y'])})")
self.hud_center.config(text=" | ".join(self.log_lines[-2:]))
# ==================== 主循环 ====================
def game_loop(self):
if not self.game_active:
self.root.after(16, self.game_loop)
return
self.update_player()
self.update_camera()
self.update_monsters()
# 射击冷却 & 自动武器连发
if self.shoot_cooldown > 0:
self.shoot_cooldown -= 16
if self.mouse_down and self.inventory["current"] and WEAPON_DATA[self.inventory["current"]]["auto"]:
self.try_shoot()
# 子弹生命周期
new_bullets = []
for b in self.bullets:
b["time"] -= 16
if b["time"] > 0:
new_bullets.append(b)
self.bullets = new_bullets
self.render()
self.root.after(16, self.game_loop)
if __name__ == "__main__":
root = tk.Tk()
game = Game(root)
root.mainloop()