Skip to content

Commit 11cd8f6

Browse files
authored
Restore Hangul Hero game (#231)
1 parent 1d8c845 commit 11cd8f6

16 files changed

Lines changed: 2988 additions & 2 deletions

File tree

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
"@astrojs/sitemap": "^3.6.0",
3636
"@iconify-json/pixel": "^1.2.1",
3737
"@sentry/astro": "^10.30.0",
38+
"@types/p5": "^1.7.6",
3839
"@webtui/css": "^0.1.5",
3940
"astro": "^5.16.3",
4041
"beautiful-mermaid": "^0.1.3",
4142
"marked": "^17.0.1",
4243
"mdast-util-mdx-jsx": "^3.2.0",
44+
"p5": "^1.11.3",
4345
"remark-mdc": "^3.9.0",
4446
"sharp": "^0.34.3",
4547
"unist-util-visit": "^5.0.0",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Basic Hangul characters (consonants and vowels)
2+
export const HANGUL_CHARS = [
3+
"ㄱ",
4+
"ㄴ",
5+
"ㄷ",
6+
"ㄹ",
7+
"ㅁ",
8+
"ㅂ",
9+
"ㅅ",
10+
"ㅇ",
11+
"ㅈ",
12+
"ㅊ",
13+
"ㅋ",
14+
"ㅌ",
15+
"ㅍ",
16+
"ㅎ",
17+
"ㅏ",
18+
"ㅑ",
19+
"ㅓ",
20+
"ㅕ",
21+
"ㅗ",
22+
"ㅛ",
23+
"ㅜ",
24+
"ㅠ",
25+
"ㅡ",
26+
"ㅣ",
27+
];
28+
29+
// Helper function to check if a character is Hangul
30+
export function isHangul(char: string): boolean {
31+
return /[\u3131-\u314E\u314F-\u3163\uAC00-\uD7A3]/.test(char);
32+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type p5 from "p5";
2+
3+
export class FallingChar {
4+
x: number;
5+
y: number;
6+
char: string;
7+
speed: number;
8+
isBackground: boolean;
9+
color: string;
10+
size: number;
11+
opacity: number;
12+
toDelete: boolean;
13+
koreanPixelFont: p5.Font;
14+
isGrowing: boolean;
15+
growthStartTime: number;
16+
growthDuration: number;
17+
wasSuccessfullyHit: boolean;
18+
private p: p5;
19+
20+
constructor(
21+
x: number,
22+
char: string,
23+
speed: number,
24+
koreanPixelFont: p5.Font,
25+
p: p5,
26+
isBackground = false,
27+
) {
28+
this.x = x;
29+
this.y = -30;
30+
this.char = char;
31+
this.speed = speed;
32+
this.isBackground = isBackground;
33+
this.color = isBackground ? this.getRandomColor() : "#FFFFFF";
34+
this.size = isBackground ? 24 : 32;
35+
this.opacity = isBackground ? 0.3 : 1;
36+
this.toDelete = false;
37+
this.koreanPixelFont = koreanPixelFont;
38+
this.isGrowing = false;
39+
this.growthStartTime = 0;
40+
this.growthDuration = 200; // Duration of growth animation in ms
41+
this.wasSuccessfullyHit = false;
42+
this.p = p;
43+
}
44+
45+
getRandomColor(): string {
46+
const colors = [
47+
"#00ff00", // Neon green
48+
"#ff0066", // Hot pink
49+
"#00ffff", // Cyan
50+
"#ffff00", // Yellow
51+
"#ff3399", // Pink
52+
"#33ccff", // Light blue
53+
];
54+
return colors[Math.floor(Math.random() * colors.length)];
55+
}
56+
57+
update(): void {
58+
this.y += this.speed;
59+
60+
if (this.isGrowing) {
61+
const currentTime = performance.now();
62+
const elapsed = currentTime - this.growthStartTime;
63+
const progress = Math.min(elapsed / this.growthDuration, 1);
64+
65+
// Ease out cubic function for smooth growth
66+
const easedProgress = 1 - Math.pow(1 - progress, 3);
67+
this.size = 32 + easedProgress * 48; // Grow from 32 to 80
68+
69+
// Interpolate color to green
70+
const startColor = this.p.color(this.color);
71+
const endColor = this.p.color("#00ff00");
72+
this.color = this.p.lerpColor(startColor, endColor, easedProgress).toString();
73+
74+
if (progress >= 1) {
75+
this.toDelete = true;
76+
}
77+
}
78+
}
79+
80+
startGrowth(): void {
81+
this.isGrowing = true;
82+
this.growthStartTime = performance.now();
83+
}
84+
85+
draw(p: p5): void {
86+
p.push();
87+
p.textSize(this.size);
88+
p.textAlign(p.CENTER, p.CENTER);
89+
p.textStyle(p.BOLD);
90+
p.textFont(this.koreanPixelFont);
91+
92+
// Draw outer glow
93+
if (!this.isBackground) {
94+
const glowColor = p.color(this.color);
95+
glowColor.setAlpha(this.opacity * 80);
96+
p.fill(glowColor);
97+
p.textSize(this.size + 4);
98+
p.text(this.char, this.x, this.y);
99+
100+
// Draw middle glow
101+
glowColor.setAlpha(this.opacity * 120);
102+
p.fill(glowColor);
103+
p.textSize(this.size + 2);
104+
p.text(this.char, this.x, this.y);
105+
}
106+
107+
// Draw main character
108+
const c = p.color(this.color);
109+
c.setAlpha(this.opacity * 255);
110+
p.fill(c);
111+
p.textSize(this.size);
112+
p.noStroke();
113+
p.text(this.char, this.x, this.y);
114+
115+
// Draw inner highlight
116+
if (!this.isBackground) {
117+
const highlightColor = p.color("#ffffff");
118+
highlightColor.setAlpha(this.opacity * 180);
119+
p.fill(highlightColor);
120+
p.textSize(this.size * 0.9);
121+
p.text(this.char, this.x, this.y);
122+
}
123+
p.pop();
124+
}
125+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { FallingChar } from "./falling-char";
2+
export { ScoreAnimation } from "./score-animation";
3+
export { Particle } from "./particle";
4+
export { LifeCharacter } from "./life-character";
5+
export { WarningMessage } from "./warning-message";
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type p5 from "p5";
2+
3+
export class LifeCharacter {
4+
x: number;
5+
y: number;
6+
baseY: number;
7+
char: string;
8+
color: string;
9+
isLost: boolean;
10+
bobOffset: number;
11+
private font: p5.Font;
12+
13+
// Predefined distinct colors for life indicators
14+
static readonly LIFE_COLORS = [
15+
"#FF6B6B", // Pink/Red
16+
"#4ECDC4", // Teal
17+
"#45B7D1", // Light Blue
18+
"#96CEB4", // Sage
19+
"#FFEEAD", // Light Yellow
20+
"#D4A5A5", // Light Pink
21+
"#ff0066", // Hot pink
22+
"#00ff00", // Neon green
23+
"#00ffff", // Cyan
24+
"#ffff00", // Yellow
25+
"#ff3399", // Pink
26+
"#33ccff", // Light blue
27+
];
28+
29+
constructor(x: number, y: number, font: p5.Font, availableColors: string[]) {
30+
this.x = x;
31+
this.y = y;
32+
this.baseY = y;
33+
this.char = "\u25CF";
34+
this.isLost = false;
35+
this.bobOffset = Math.random() * Math.PI * 2; // Random starting phase
36+
// Randomly select a color from the available colors
37+
const randomIndex = Math.floor(Math.random() * availableColors.length);
38+
this.color = availableColors[randomIndex];
39+
this.font = font;
40+
}
41+
42+
update(p: p5, currentTime: number): void {
43+
if (!this.isLost) {
44+
// Bob up and down
45+
this.y = this.baseY + Math.sin(currentTime / 400 + this.bobOffset) * 3;
46+
}
47+
}
48+
49+
draw(p: p5): void {
50+
p.push();
51+
p.textAlign(p.CENTER, p.CENTER);
52+
p.textFont(this.font);
53+
p.textSize(32);
54+
55+
const displayColor = this.isLost ? "#FF0000" : this.color;
56+
57+
if (this.isLost) {
58+
// Draw the lost character (Korean character that hit bottom)
59+
// Simple shadow for depth without blur
60+
p.fill(0, 0, 0, 100);
61+
p.text(this.char, this.x + 2, this.y + 2);
62+
63+
// Main character with solid red color
64+
p.fill(displayColor);
65+
p.text(this.char, this.x, this.y);
66+
} else {
67+
// Draw animated dot
68+
// Simple shadow for depth
69+
p.fill(0, 0, 0, 100);
70+
p.text("\u25CF", this.x + 2, this.y + 2);
71+
72+
// Main dot with fixed color
73+
p.fill(displayColor);
74+
p.text("\u25CF", this.x, this.y);
75+
}
76+
77+
p.pop();
78+
}
79+
80+
setLostChar(char: string): void {
81+
this.char = char;
82+
this.isLost = true;
83+
}
84+
}

0 commit comments

Comments
 (0)