Skip to content

Commit 725534d

Browse files
committed
feat: add safe zone and chest system
1 parent fa36d38 commit 725534d

24 files changed

Lines changed: 1907 additions & 160 deletions

src/config/GameConfig.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,83 @@ export const GameConfig = {
162162
CPU_TIME_LIMIT_SECONDS: 1,
163163
MEMORY_LIMIT_KB: 128 * 1024,
164164
WALL_TIME_LIMIT_SECONDS: 10,
165+
},
166+
167+
// Treasure & Key System Configuration
168+
TREASURE_SYSTEM: {
169+
// Feature toggle - set to false to disable entirely
170+
ENABLED: true,
171+
172+
// Spawn timing (ticks)
173+
FIRST_TREASURE_TICK: 120,
174+
SECOND_TREASURE_TICK: 220,
175+
176+
// Treasure scoring
177+
BASE_SCORE: 30,
178+
SCORE_MULTIPLIER: 0.6,
179+
MIN_SCORE: 30,
180+
MAX_SCORE: 75,
181+
182+
// Key mechanics
183+
KEY_HOLD_TIME_LIMIT: 40, // ticks before auto-drop
184+
MIN_TREASURE_DISTANCE: 12, // Manhattan distance between keys and treasure
185+
186+
// Spatial constraints
187+
MIN_DISTANCE_FROM_SNAKE_HEAD: 3, // Minimum distance when spawning
188+
189+
// Key quantity calculation: max(2, floor(alive_snakes / 2))
190+
MIN_KEYS_PER_TREASURE: 2,
191+
MAX_KEYS_PER_TREASURE: 4,
192+
KEYS_PER_SNAKE_DIVISOR: 2,
193+
},
194+
195+
// Safe Zone System Configuration
196+
SAFE_ZONE: {
197+
// Feature toggle - set to false to disable entirely
198+
ENABLED: true,
199+
200+
// Game phase definitions (in ticks)
201+
PHASES: {
202+
[GamePhase.EARLY]: {
203+
START_TICK: 1,
204+
END_TICK: 80,
205+
SAFE_ZONE_STATE: "STABLE" // No shrinking
206+
},
207+
[GamePhase.MID]: {
208+
START_TICK: 81,
209+
END_TICK: 200,
210+
SAFE_ZONE_STATE: "SHRINKING", // Two shrinking periods
211+
SHRINK_EVENTS: [
212+
{ START_TICK: 81, DURATION: 20, TARGET_SIZE: { WIDTH: 32, HEIGHT: 24 } },
213+
{ START_TICK: 120, DURATION: 20, TARGET_SIZE: { WIDTH: 24, HEIGHT: 18 } }
214+
]
215+
},
216+
[GamePhase.LATE]: {
217+
START_TICK: 201,
218+
END_TICK: 256,
219+
SAFE_ZONE_STATE: "SHRINKING", // Final shrinking
220+
SHRINK_EVENTS: [
221+
{ START_TICK: 221, DURATION: 20, TARGET_SIZE: { WIDTH: 20, HEIGHT: 16 } }
222+
]
223+
}
224+
},
225+
226+
// Initial safe zone (full map)
227+
INITIAL_BOUNDS: {
228+
X_MIN: 0,
229+
Y_MIN: 0,
230+
X_MAX: 39, // CANVAS.COLUMNS - 1
231+
Y_MAX: 29 // CANVAS.ROWS - 1
232+
},
233+
234+
// Visual settings
235+
DANGER_ZONE_COLOR: "#ff0000",
236+
DANGER_ZONE_ALPHA: 0.3,
237+
BORDER_COLOR: "#ff0000",
238+
BORDER_WIDTH: 2,
239+
240+
// Warning system
241+
WARNING_TICKS_BEFORE_SHRINK: 10 // Show warning 10 ticks before shrinking
165242
}
166243

167244
} as const;

src/core/CollisionDetector.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { VortexFieldManager } from "../managers/VortexFieldManager";
88
import { VortexZoneType } from "../types/VortexField";
99

1010
export interface CollisionResult {
11-
type: "wall" | "obstacle" | "food" | "snake";
11+
type: "wall" | "obstacle" | "food" | "snake" | "treasure_chest" | "key";
1212
snake: Snake; // The snake that collided
13-
collidedWith?: Food | Obstacle | Snake | Position; // What it collided with
13+
collidedWith?: Food | Obstacle | Snake | Position | any; // What it collided with (using any for treasure/key)
1414
position: Position; // Where the collision occurred (head position)
1515
}
1616

@@ -46,7 +46,8 @@ export class CollisionDetector {
4646

4747
for (const snake of snakes) {
4848
// If this snake already had a fatal collision determined in this tick, skip further checks for it
49-
if (fatallyCollidedSnakes.has(snake) || !snake.isAlive()) {
49+
// Also skip snakes that are in death animation
50+
if (fatallyCollidedSnakes.has(snake) || !snake.isAlive() || snake.isDyingAnimation()) {
5051
continue;
5152
}
5253

@@ -126,6 +127,26 @@ export class CollisionDetector {
126127
// Continue checking other nearby items for this snake
127128
break;
128129

130+
case "key":
131+
// Key collision is not fatal, just report it for pickup
132+
collisionResults.push({
133+
type: "key",
134+
snake: snake,
135+
collidedWith: item.position,
136+
position: head,
137+
});
138+
break;
139+
140+
case "treasure_chest":
141+
// Treasure chest collision is not fatal, just report it for opening attempt
142+
collisionResults.push({
143+
type: "treasure_chest",
144+
snake: snake,
145+
collidedWith: item.position,
146+
position: head,
147+
});
148+
break;
149+
129150
case "snake":
130151
// Collision with another snake's segment
131152
// 检查是否与其他蛇发生碰撞(头部或身体)

src/core/Entity.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { EntityType } from "../types/EntityType";
44
import { Snake } from "../entities/Snake";
55
import { Food } from "../entities/Food";
66
import { Obstacle } from "../entities/Obstacle";
7+
import { TreasureChest } from "../entities/TreasureChest";
8+
import { Key } from "../entities/Key";
79
import { GameState } from "../types/GameState";
810

911
export abstract class Entity implements Updatable {
@@ -46,3 +48,11 @@ export function isFood(entity: Entity): entity is Food {
4648
export function isObstacle(entity: Entity): entity is Obstacle {
4749
return entity.getEntityType() === EntityType.OBSTACLE;
4850
}
51+
52+
export function isTreasureChest(entity: Entity): entity is TreasureChest {
53+
return entity.getEntityType() === EntityType.TREASURE_CHEST;
54+
}
55+
56+
export function isKey(entity: Entity): entity is Key {
57+
return entity.getEntityType() === EntityType.KEY;
58+
}

src/core/EventBus.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export enum GameEventType {
2929
VORTEX_FIELD_ACTIVATED = "vortex:field_activated",
3030
VORTEX_FIELD_DEACTIVATED = "vortex:field_deactivated",
3131
VORTEX_FIELD_COOLDOWN_ENDED = "vortex:field_cooldown_ended",
32+
// 宝箱钥匙系统事件
33+
TREASURE_SPAWNED = "treasure:spawned",
34+
TREASURE_OPENED = "treasure:opened",
35+
KEY_PICKED_UP = "treasure:key_picked_up",
36+
KEY_DROPPED = "treasure:key_dropped",
37+
KEY_REMOVED = "treasure:key_removed",
3238
}
3339

3440
// Callback function type

src/core/renderers/KeyRenderer.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Key } from "../../entities/Key";
2+
import { EntityRenderer } from "./EntityRenderer";
3+
4+
export class KeyRenderer implements EntityRenderer<Key> {
5+
private frameCount: number = 0;
6+
7+
render(ctx: CanvasRenderingContext2D, key: Key): void {
8+
const position = key.getPosition();
9+
const size = key.getSize();
10+
11+
// 帧计数器,用于动画效果
12+
this.frameCount = (this.frameCount + 1) % 60;
13+
14+
this.renderKey(ctx, position.x, position.y, size);
15+
}
16+
17+
private renderKey(
18+
ctx: CanvasRenderingContext2D,
19+
x: number,
20+
y: number,
21+
size: number
22+
): void {
23+
const centerX = x + size / 2;
24+
const centerY = y + size / 2;
25+
26+
// 旋转动画效果
27+
const rotation = Math.sin(this.frameCount * 0.05) * 0.2;
28+
29+
ctx.save();
30+
ctx.translate(centerX, centerY);
31+
ctx.rotate(rotation);
32+
33+
// 钥匙主体颜色 (金色)
34+
ctx.fillStyle = "#FFD700";
35+
36+
// 钥匙柄 (圆形部分)
37+
const handleRadius = size / 4;
38+
const handleCenterX = -size / 4;
39+
const handleCenterY = 0;
40+
41+
// 绘制圆形钥匙柄 (像素风格)
42+
this.drawPixelCircle(ctx, handleCenterX, handleCenterY, handleRadius);
43+
44+
// 钥匙柄中心的孔
45+
ctx.fillStyle = "#000000";
46+
this.drawPixelCircle(ctx, handleCenterX, handleCenterY, 2);
47+
48+
// 钥匙杆
49+
ctx.fillStyle = "#FFD700";
50+
const shaftWidth = 2;
51+
const shaftLength = size / 2;
52+
ctx.fillRect(-shaftWidth / 2, -shaftWidth / 2, shaftLength, shaftWidth);
53+
54+
// 钥匙齿 (像素风格)
55+
ctx.fillStyle = "#FFD700";
56+
const toothX = shaftLength / 2;
57+
58+
// 第一个齿
59+
ctx.fillRect(toothX - 2, shaftWidth / 2, 2, 3);
60+
// 第二个齿
61+
ctx.fillRect(toothX, shaftWidth / 2, 2, 2);
62+
63+
// 钥匙高光
64+
ctx.fillStyle = "#FFFF99";
65+
ctx.fillRect(handleCenterX - 1, handleCenterY - 2, 2, 1);
66+
ctx.fillRect(-1, -1, shaftLength / 2, 1);
67+
68+
// 钥匙阴影/边框
69+
ctx.strokeStyle = "#DAA520";
70+
ctx.lineWidth = 1;
71+
72+
// 描边钥匙柄
73+
this.strokePixelCircle(ctx, handleCenterX, handleCenterY, handleRadius);
74+
75+
// 描边钥匙杆
76+
ctx.strokeRect(-shaftWidth / 2, -shaftWidth / 2, shaftLength, shaftWidth);
77+
78+
ctx.restore();
79+
80+
// 发光效果
81+
const glowAlpha = 0.2 + Math.sin(this.frameCount * 0.1) * 0.1;
82+
ctx.fillStyle = `rgba(255, 215, 0, ${glowAlpha})`;
83+
84+
// 在钥匙周围绘制发光像素点
85+
const sparkleOffsets = [
86+
{ x: -3, y: -3 }, { x: 3, y: -3 },
87+
{ x: -3, y: 3 }, { x: 3, y: 3 },
88+
{ x: 0, y: -4 }, { x: 0, y: 4 },
89+
{ x: -4, y: 0 }, { x: 4, y: 0 }
90+
];
91+
92+
sparkleOffsets.forEach((offset, index) => {
93+
if (this.frameCount % 15 === index * 2) {
94+
ctx.fillRect(centerX + offset.x, centerY + offset.y, 1, 1);
95+
}
96+
});
97+
}
98+
99+
private drawPixelCircle(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, radius: number): void {
100+
// 绘制像素风格的圆形
101+
const pixelSize = 1;
102+
103+
for (let x = -radius; x <= radius; x += pixelSize) {
104+
for (let y = -radius; y <= radius; y += pixelSize) {
105+
const distance = Math.sqrt(x * x + y * y);
106+
if (distance <= radius && distance > radius - 2) {
107+
ctx.fillRect(centerX + x, centerY + y, pixelSize, pixelSize);
108+
}
109+
}
110+
}
111+
112+
// 填充内部
113+
for (let x = -radius + 2; x <= radius - 2; x += pixelSize) {
114+
for (let y = -radius + 2; y <= radius - 2; y += pixelSize) {
115+
const distance = Math.sqrt(x * x + y * y);
116+
if (distance <= radius - 2) {
117+
ctx.fillRect(centerX + x, centerY + y, pixelSize, pixelSize);
118+
}
119+
}
120+
}
121+
}
122+
123+
private strokePixelCircle(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, radius: number): void {
124+
// 绘制像素风格圆形的描边
125+
const pixelSize = 1;
126+
127+
for (let x = -radius; x <= radius; x += pixelSize) {
128+
for (let y = -radius; y <= radius; y += pixelSize) {
129+
const distance = Math.sqrt(x * x + y * y);
130+
if (distance <= radius && distance > radius - 1) {
131+
ctx.strokeRect(centerX + x, centerY + y, pixelSize, pixelSize);
132+
}
133+
}
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)