-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.c
More file actions
652 lines (570 loc) · 25.6 KB
/
Copy pathmain.c
File metadata and controls
652 lines (570 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include "beat.h"
#include "game.h"
// ESC 키 코드. 직접 27 쓰면 헷갈려서 매크로로 뺌
#define ESC 27
// 화면 틀(박스) 위치. 맵은 박스 안쪽 6행 5열부터 그린다
#define FRAME_RIGHT 52 // 오른쪽 테두리 열
#define MAP_OFFSET_Y 6
#define MAP_OFFSET_X 5
// 게임 끝났을 때 결과 화면에서 쓸 플레이어 상태
static Player result_player;
// 화면 클리어 + 커서 좌상단. 배경을 어두운 던전 색으로 채운다
static void clear_screen(void) {
printf("\033[48;5;233m\033[2J\033[H");
}
// 가로 테두리 한 줄 (왼쪽모서리, 오른쪽모서리 문자를 받아 가운데를 ═ 로 채움)
// 끝에 \033[K 로 테두리 바깥(박스 밖)까지 지워 잔여물을 청소한다
static void draw_hbar(int row, const char *left, const char *right) {
printf("\033[%d;1H\033[38;5;94m%s", row, left);
for (int c = 2; c < FRAME_RIGHT; c++) printf("═");
printf("%s\033[0m\033[K", right);
}
// 세로 테두리만 (좌변 1열, 우변 FRAME_RIGHT열). 우변 너머는 \033[K 로 청소
static void draw_sides(int row) {
printf("\033[%d;1H\033[38;5;94m║\033[0m", row);
printf("\033[%d;%dH\033[38;5;94m║\033[0m\033[K", row, FRAME_RIGHT);
}
// 게임 화면 전체 틀을 그린다 (제목·HUD·맵·박자·도움말 패널 구분)
static void draw_frame(void) {
draw_hbar(1, "╔", "╗");
draw_sides(2);
printf("\033[2;14H\033[1;33m♪ B E A T C R A W L E R ♪\033[0m");
draw_hbar(3, "╠", "╣"); // 제목 / HUD 구분
draw_sides(4); // HUD 줄
draw_hbar(5, "╠", "╣"); // HUD / 맵 구분
for (int r = 6; r < 16; r++) draw_sides(r); // 맵 양옆
draw_hbar(16, "╠", "╣"); // 맵 / 박자 구분
draw_sides(17); // 박자 바 줄
draw_hbar(18, "╠", "╣"); // 박자 / 도움말 구분
draw_sides(19); // 도움말
draw_sides(20); // 메시지
draw_hbar(21, "╚", "╝");
}
// 게임 중에는 커서 안 보이게
static void hide_cursor(void) {
printf("\033[?25l");
}
// 배경음악 재생/정지 (macOS afplay 명령을 백그라운드로 실행)
static void start_bgm(void) {
system("afplay bgm.wav &");
}
static void stop_bgm(void) {
system("pkill afplay");
}
// 종료할 때 다시 보이게 (안 그러면 셸에서 커서 사라진 채로 남음)
static void show_cursor(void) {
printf("\033[?25h");
}
// Ctrl+C 같은 강제 종료 때도 음악을 끄고 터미널을 되돌린 뒤 끝낸다
// (안 그러면 게임을 나가도 afplay 가 살아남아 소리가 계속 난다)
static void on_quit_signal(int sig) {
(void)sig;
stop_bgm();
show_cursor();
disable_raw_mode();
exit(0);
}
// 메뉴 그리기
// 각 줄은 \033[행;열H 로 위치만 잡고 \n 안 붙임 (붙이면 커서가 밀려 정렬 깨짐)
static void draw_menu(int selected) {
clear_screen();
// 아스키 아트 로고 (청록 굵게). 세 줄 모두 같은 열에서 시작
printf("\033[1;36m");
printf("\033[3;30H█▀▄ █▀▀ █▀█ ▀█▀");
printf("\033[4;30H█▀▄ █▀▀ █▀█ █ ");
printf("\033[5;30H▀▀ ▀▀▀ ▀ ▀ ▀ ");
printf("\033[0m");
printf("\033[6;32H\033[90mC R A W L E R\033[0m");
// 메뉴 박스
printf("\033[9;31H╔══════════╗");
if (selected == 0) {
printf("\033[10;31H║ \033[1;33m> START\033[0m ║");
printf("\033[11;31H║ QUIT ║");
} else {
printf("\033[10;31H║ START ║");
printf("\033[11;31H║ \033[1;33m> QUIT\033[0m ║");
}
printf("\033[12;31H╚══════════╝");
printf("\033[15;30H\033[90m[W/S] 선택 [ENTER] 확정\033[0m");
fflush(stdout);
}
// 메뉴 처리. ENTER 누를 때까지 루프
static GameState run_menu(void) {
int selected = 0;
draw_menu(selected);
while (1) {
int key = read_key();
if (key == 'w' || key == 'W') {
selected = 0;
draw_menu(selected);
} else if (key == 's' || key == 'S') {
selected = 1;
draw_menu(selected);
} else if (key == '\n' || key == '\r') {
// ENTER 처리. \n 과 \r 둘 다 받음 (환경마다 다름)
if (selected == 0) return STATE_INTRO;
else return STATE_QUIT;
} else if (key == 'q' || key == 'Q') {
return STATE_QUIT;
}
// 너무 빨리 돌면 CPU 100% 잡아먹음. 20ms씩 쉼
usleep(20000);
}
}
// 인트로 화면 그리기
// 메뉴와 같은 이유로 \n 안 붙이고 위치만 절대지정
static void draw_intro(void) {
clear_screen();
printf("\033[5;20H\033[90m♪ ─────────────────────── ♪\033[0m");
printf("\033[7;22H박자가 사라진 던전.");
printf("\033[9;22H당신은 음악을 되찾기 위해");
printf("\033[10;22H이곳에 들어왔다.");
printf("\033[12;22H\033[1;36m박자에 맞춰 움직여라.\033[0m");
printf("\033[14;20H\033[90m♪ ─────────────────────── ♪\033[0m");
printf("\033[17;22H\033[90m[ENTER] 시작 [ESC] 메뉴\033[0m");
fflush(stdout);
}
// 조작법 + 적 설명 화면
static void draw_guide(void) {
clear_screen();
printf("\033[3;28H\033[1;36m─ 던전 안내 ─\033[0m");
printf("\033[5;14H\033[1;37m[조작]\033[0m");
printf("\033[6;16HWASD 박자에 맞춰 이동");
printf("\033[7;16HSPACE 바라보는 방향 공격");
printf("\033[8;16HESC 메뉴 Q 종료");
printf("\033[10;14H\033[1;37m[적]\033[0m");
printf("\033[11;16H\033[35mZ\033[0m 좀비 다가와서 때린다");
printf("\033[12;16H\033[36mS\033[0m 사수 같은 줄에서 화살을 쏜다");
printf("\033[13;16H\033[31mC\033[0m 포수 더 빠르게 쏜다");
printf("\033[15;14H\033[1;37m[규칙]\033[0m");
printf("\033[16;16H박자를 맞춰야 이동·공격이 된다");
printf("\033[17;16H날아오는 \033[1;33m*\033[0m 는 피하고, 적을 다 잡아야 출구가 열린다");
printf("\033[20;22H\033[90m[ENTER] 시작 [ESC] 메뉴\033[0m");
fflush(stdout);
}
// 인트로 처리. 스토리 → 안내 두 화면을 거쳐 게임 시작
static GameState run_intro(void) {
draw_intro();
int page = 0; // 0 스토리, 1 안내
while (1) {
int key = read_key();
if (key == '\n' || key == '\r') {
if (page == 0) {
page = 1;
draw_guide();
} else {
return STATE_PLAYING;
}
}
if (key == ESC) return STATE_MENU;
if (key == 'q' || key == 'Q') return STATE_QUIT;
usleep(20000);
}
}
// 박자 바에 채워야 할 칸 수 계산 (0~10)
// 박자 직후엔 0, 다음 박자 직전엔 10
static int beat_bar_fill(BeatState *bs) {
long off = beat_offset_ms(bs);
long beat_ms = 60000 / bs->bpm;
// offset이 음수면 다음 박자가 가까운 상태, 양수면 직전 박자가 가까운 상태
long into_beat;
if (off < 0) {
into_beat = beat_ms + off;
} else {
into_beat = off;
}
// 비율로 변환
int fill = (int)((into_beat * 10) / beat_ms);
if (fill > 10) fill = 10;
if (fill < 0) fill = 0;
return fill;
}
// HP를 하트로 그린다. 현재 HP만큼 채운 하트, 나머진 빈 하트
static void draw_hearts(int hp, int hp_max) {
printf("\033[1;31m");
for (int i = 0; i < hp; i++) printf("♥");
printf("\033[38;5;238m");
for (int i = hp; i < hp_max; i++) printf("♡");
printf("\033[0m");
}
// HUD: 층 + BPM + HP 하트 + 골드 + 콤보 + 등급. 박스 안 4행에 그린다
static void draw_hud(BeatState *bs, Player *p) {
// 보스층(4)은 층 번호 대신 'BOSS' 를 빨갛게 표시
if (p->floor >= 4)
printf("\033[4;3H\033[1;91mBOSS\033[0m \033[38;5;245mBPM\033[0m %d ", bs->bpm);
else
printf("\033[4;3H\033[1;36m%dF\033[0m \033[38;5;245mBPM\033[0m %d ", p->floor, bs->bpm);
draw_hearts(p->hp, p->hp_max);
printf(" \033[1;33m◆\033[0m %d \033[35mCOMBO\033[0m x%d \033[38;5;208m[%s]\033[0m ",
p->gold, p->combo, combo_grade(p->combo));
printf("\033[4;%dH\033[38;5;94m║\033[0m", FRAME_RIGHT); // 우변 테두리 복구
}
// 그 칸에 살아있는 적의 인덱스 (없으면 -1)
static int monster_index_at(Monster mons[], int count, int x, int y) {
for (int i = 0; i < count; i++) {
if (mons[i].alive && mons[i].x == x && mons[i].y == y) return i;
}
return -1;
}
// 그 칸에 날아다니는 투사체가 있나
static int shot_at(Projectile shots[], int x, int y) {
for (int i = 0; i < MAX_SHOTS; i++) {
if (shots[i].alive && shots[i].x == x && shots[i].y == y) return 1;
}
return 0;
}
// 그 칸에 방금 맞은 적(hit)이 있나. 처치된 적도 한 박자는 반짝이게 alive는 안 봄
static int hit_at(Monster mons[], int count, int hit, int x, int y) {
if (hit < 0 || hit >= count) return 0; // 맞은 적 없거나 범위 밖
return mons[hit].x == x && mons[hit].y == y;
}
// 적 종류별 글자
static char monster_glyph(MonsterType type) {
if (type == ARCHER) return 'S';
if (type == CANNON) return 'C';
if (type == BOSS) return 'B';
return 'Z';
}
// 적 종류별 색 (ANSI 코드 숫자)
static int monster_color(MonsterType type) {
if (type == ARCHER) return 36; // 청록
if (type == CANNON) return 31; // 빨강
if (type == BOSS) return 91; // 보스: 밝은 빨강
return 35; // 좀비: 보라
}
// 한 칸은 화면에서 2칸 폭으로 그린다 (글자 하나 + 공백). 그래야 맵이 정사각형에 가깝다.
// 바닥은 어두운 배경(48;5;235), 벽은 회색 블록으로 채워 던전 느낌을 낸다.
static void draw_floor_cell(const char *glyph_with_color) {
printf("\033[48;5;233m%s \033[0m", glyph_with_color);
}
// 맵 + 적 + 투사체 + 플레이어. hit은 방금 맞아 반짝일 적 인덱스(-1이면 없음)
static void draw_map(Map *m, Player *p, Monster mons[], int count, int hit, Projectile shots[]) {
for (int y = 0; y < MAP_H; y++) {
printf("\033[%d;%dH", y + MAP_OFFSET_Y, MAP_OFFSET_X);
for (int x = 0; x < MAP_W; x++) {
int mi = monster_index_at(mons, count, x, y);
if (m->tile[y][x] == '#') {
printf("\033[48;5;60m \033[0m"); // 벽: 청회색 돌벽 (바닥과 대비)
} else if (x == p->x && y == p->y) {
if (p->combo >= 32) draw_floor_cell("\033[1;31m@"); // FEVER면 빨강
else draw_floor_cell("\033[1;36m@"); // 플레이어: 청록 @
} else if (hit_at(mons, count, hit, x, y)) {
// 맞은 적: 흰색으로 반짝
printf("\033[48;5;233m\033[1;37m%c \033[0m", monster_glyph(mons[hit].type));
} else if (mi >= 0) {
// 적: 종류색 글자 (Z/S/C/B)
printf("\033[48;5;233m\033[%dm%c \033[0m", monster_color(mons[mi].type), monster_glyph(mons[mi].type));
} else if (shot_at(shots, x, y)) {
draw_floor_cell("\033[1;33m*"); // 투사체: 노랑 *
} else if (m->tile[y][x] == '$') {
draw_floor_cell("\033[1;33m$"); // 골드: 노랑 $
} else if (m->tile[y][x] == '>') {
draw_floor_cell("\033[1;32m>"); // 출구: 초록 >
} else {
draw_floor_cell(" "); // 바닥
}
}
// 줄 끝에서 박스 오른쪽 테두리를 다시 찍어 어떤 잔여물도 덮는다
printf("\033[%d;%dH\033[38;5;94m║\033[0m", y + MAP_OFFSET_Y, FRAME_RIGHT);
}
}
// 박자 바 그리기. 박스 안 17행에. 칸을 배경색 공백으로 채워 폭을 일정하게
static void draw_beat_bar(BeatState *bs) {
int fill = beat_bar_fill(bs);
printf("\033[17;3H\033[1;36m♪ BEAT \033[38;5;245m[");
for (int i = 0; i < 10; i++) {
if (i < fill) printf("\033[48;5;44m "); // 채운 칸: 청록 배경
else printf("\033[48;5;238m "); // 빈 칸: 어두운 배경
}
printf("\033[0m\033[38;5;245m]\033[0m");
}
// 판정 텍스트. 박자 바 오른쪽(17행)에 띄운다
static void draw_judgment(int judge) {
// 이전 텍스트가 더 길었을 수 있어서 공백으로 먼저 덮고 다시 씀
printf("\033[17;32H ");
printf("\033[17;32H");
// 판정마다 색 다르게. 끝에 \033[0m 으로 색 원래대로
if (judge == JUDGE_PERFECT) printf("\033[1;36mPERFECT!\033[0m");
else if (judge == JUDGE_GOOD) printf("\033[1;33mGOOD\033[0m");
else if (judge == JUDGE_MISS) printf("\033[1;31mMISS\033[0m");
else if (judge == JUDGE_HIT) printf("\033[1;33mHIT!\033[0m");
// 박자 바 줄의 오른쪽 테두리 복구 (] 잔여물 덮기)
printf("\033[17;%dH\033[38;5;94m║\033[0m", FRAME_RIGHT);
}
// 하단 도움말. 박스 안 19행
static void draw_help(void) {
printf("\033[19;3H\033[38;5;245m[WASD]\033[0m 이동 \033[38;5;245m[SPACE]\033[0m 공격 \033[38;5;245m[ESC]\033[0m 메뉴 \033[38;5;245m[Q]\033[0m 종료");
}
// 출구 상태 안내. 박스 안 20행 (적 남았으면 잠김, 다 잡으면 열림)
static void draw_gate_info(int gate_open) {
printf("\033[20;3H");
if (gate_open) printf("\033[1;32m> 출구가 열렸다!\033[0m ");
else printf("\033[38;5;208m적을 모두 처치하면 출구가 열린다\033[0m ");
printf("\033[20;%dH\033[38;5;94m║\033[0m", FRAME_RIGHT); // 우변 테두리 복구
}
// 게임 오버 화면. 결과 보여주고 ENTER 누르면 메뉴로
static GameState run_game_over(void) {
clear_screen();
printf("\033[8;28H\033[31mGAME OVER\033[0m\n");
printf("\033[10;25H%dF 에서 쓰러졌다\n", result_player.floor);
printf("\033[11;25H모은 골드: %d\n", result_player.gold);
printf("\033[14;25H[ENTER] 메뉴\n");
fflush(stdout);
while (1) {
int key = read_key();
if (key == '\n' || key == '\r') return STATE_MENU;
if (key == 'q' || key == 'Q') return STATE_QUIT;
usleep(20000);
}
}
// 승리 화면. 결과 보여주고 ENTER 누르면 메뉴로
static GameState run_victory(void) {
clear_screen();
printf("\033[3;22H\033[90m♪ ─────────────────────── ♪\033[0m");
printf("\033[5;24H\033[1;33m비트로스가 쓰러졌다.\033[0m");
printf("\033[7;24H멈춰 있던 심장이 다시 뛰기 시작하고,");
printf("\033[8;24H던전 깊은 곳에서 잊혔던 박자가 울린다.");
printf("\033[10;24H당신은 빛이 새어드는 통로를 따라");
printf("\033[11;24H천천히 밖으로 걸어 나온다.");
printf("\033[13;24H\033[1;36m세상에, 다시 음악이 돌아왔다.\033[0m");
printf("\033[15;22H\033[90m♪ ─────────────────────── ♪\033[0m");
printf("\033[17;24H\033[1;33m◆\033[0m 모은 골드: %d", result_player.gold);
printf("\033[19;24H\033[90m[ENTER] 메뉴로\033[0m");
fflush(stdout);
while (1) {
int key = read_key();
if (key == '\n' || key == '\r') return STATE_MENU;
if (key == 'q' || key == 'Q') return STATE_QUIT;
usleep(20000);
}
}
// 상점 화면 그리기. 골드로 살 수 있는 항목과 현재 선택 표시 (화면은 안 지움)
static void draw_shop(Player *p, int sel) {
printf("\033[3;26H\033[1;33m═══ 상 점 ═══\033[0m");
printf("\033[5;22H\033[1;33m◆\033[0m 보유 골드: %d", p->gold);
printf("\033[6;22H\033[1;31m♥\033[0m HP: %d/%d", p->hp, p->hp_max);
// 항목 0: HP 회복, 1: 최대 HP 증가, 2: 나가기
printf("\033[9;22H%s HP 회복 (+2) \033[38;5;245m20골드\033[0m",
sel == 0 ? "\033[1;33m▶\033[0m" : " ");
printf("\033[10;22H%s 최대 HP 증가 (+1) \033[38;5;245m40골드\033[0m",
sel == 1 ? "\033[1;33m▶\033[0m" : " ");
printf("\033[11;22H%s 다음 층으로 (나가기)",
sel == 2 ? "\033[1;33m▶\033[0m" : " ");
printf("\033[14;22H\033[38;5;245m[W/S] 선택 [ENTER] 구매/진행\033[0m");
fflush(stdout);
}
// 층 사이 상점. 플레이어 HP·골드를 직접 고친다. 나가기 고르면 끝
static void run_shop(Player *p) {
int sel = 0;
clear_screen(); // 진입 시 한 번만 지운다
draw_shop(p, sel);
while (1) {
int key = read_key();
if (key == 'w' || key =='W') {
if (sel > 0) sel--;
draw_shop(p, sel);
} else if (key == 's' || key == 'S') {
if (sel < 2) sel++;
draw_shop(p, sel);
} else if (key == '\n' || key == '\r') {
if (sel == 0 && p->gold >= 20 && p->hp < p->hp_max) {
p->gold -= 20;
p->hp += 2;
if (p->hp > p->hp_max) p->hp = p->hp_max;
draw_shop(p, sel);
} else if (sel == 1 && p->gold >= 40 && p->hp_max < 9) {
p->gold -= 40;
p->hp_max++;
p->hp++;
draw_shop(p, sel);
} else if (sel == 2) {
return; // 다음 층으로
}
}
usleep(20000);
}
}
// 게임 메인 루프
static GameState run_play(void) {
BeatState bs;
Player player;
Map map;
Monster monsters[MAX_MONSTERS];
int monster_count;
Projectile shots[MAX_SHOTS];
init_player(&player);
init_beat(&bs, bpm_for_floor(player.floor)); // 1층 BPM 으로 시작
init_map(&map, player.floor);
init_monsters(monsters, &monster_count, player.floor);
init_shots(shots);
clear_screen();
draw_frame(); // 화면 틀(박스) 한 번 그려둔다
// 첫 박자 오기 전에 맵·HUD를 한 번 그려둔다 (안 그러면 들어갈 때 맵이 비어 보임)
draw_hud(&bs, &player);
draw_map(&map, &player, monsters, monster_count, -1, shots);
draw_help();
draw_gate_info(0);
int last_judge = JUDGE_NONE;
long judge_show_until = 0; // 이 시각까지만 판정 텍스트 보임
int hit_index = -1; // 방금 맞은 적 (1박자 동안 반짝)
int show_hit = 0; // 판정 자리에 HIT! 표시할지
int gate_open = 0; // 이 층 적을 다 잡아 출구가 열렸는지
while (1) {
long now = get_time_ms();
int beat = current_beat(&bs);
int key = read_key();
if (key == ESC) return STATE_MENU;
if (key == 'q' || key == 'Q') return STATE_QUIT;
// 이동 키 (WASD)
if (key == 'w' || key == 'W' || key == 'a' || key == 'A' ||
key == 's' || key == 'S' || key == 'd' || key == 'D') {
int j = judge_input(&bs);
last_judge = j;
show_hit = 0;
judge_show_until = now + 400; // 0.4초 동안 판정 텍스트 보임
if (j == JUDGE_MISS) {
// 박자 빗나갔으니 이동 안 함 + 콤보 리셋
player.combo = 0;
} else {
// 박자 맞춤 → 방향 계산해서 이동
int dx = 0, dy = 0;
if (key == 'w' || key == 'W') { dy = -1; player.dir = 0; }
else if (key == 's' || key == 'S') { dy = 1; player.dir = 1; }
else if (key == 'a' || key == 'A') { dx = -1; player.dir = 2; }
else if (key == 'd' || key == 'D') { dx = 1; player.dir = 3; }
// 적이 있는 칸으론 안 들어감. 방향만 바꿔 두면 바로 SPACE로 칠 수 있다
if (monster_index_at(monsters, monster_count, player.x + dx, player.y + dy) < 0) {
move_player(&player, &map, dx, dy);
}
player.combo++;
// 투사체가 떠 있는 칸으로 걸어 들어갔으면 거기서 맞는다
int walk_hits = shot_hits_player(shots, &player);
if (walk_hits > 0) {
player.hp -= walk_hits;
player.combo = 0;
if (player.hp <= 0) {
result_player = player;
return STATE_GAME_OVER;
}
}
// 골드 밟으면 줍고 바닥으로
if (map.tile[player.y][player.x] == '$') {
player.gold += 10;
map.tile[player.y][player.x] = '.';
}
// 출구는 적을 다 잡아야 열린다
if (map.tile[player.y][player.x] == '>' && gate_open) {
if (player.floor >= 4) {
// 보스까지 깸 → 게임 클리어
result_player = player;
return STATE_VICTORY;
}
run_shop(&player); // 다음 층 가기 전 상점에서 골드 쓰기
next_floor(&player, &map, &bs);
init_monsters(monsters, &monster_count, player.floor);
init_shots(shots); // 이전 층 투사체는 지운다
gate_open = 0;
clear_screen();
draw_frame(); // 새 층이니 틀도 다시 그린다
// 새 층 맵을 바로 그려둔다 (첫 박자까지 비어 보이지 않게)
draw_hud(&bs, &player);
draw_map(&map, &player, monsters, monster_count, -1, shots);
draw_help();
draw_gate_info(0);
bs.last_beat = -1; // 새 층이니 맵/HUD 다시 그리게
}
}
}
// 공격 키 (SPACE) - 바라보는 방향 적을 친다
if (key == ' ') {
int j = judge_input(&bs);
last_judge = j;
judge_show_until = now + 400;
if (j == JUDGE_MISS) {
player.combo = 0;
show_hit = 0;
} else {
// 박자 맞춤 → 콤보 오르고 그 방향 적 타격
player.combo++;
hit_index = attack(&player, monsters, monster_count);
show_hit = (hit_index >= 0); // 맞췄을 때만 HIT! 표시
// 마지막 적까지 잡았으면 출구 열림
if (all_dead(monsters, monster_count)) gate_open = 1;
}
}
// 판정 텍스트 표시 시간 지나면 지움 (HIT! 도 같이)
if (now > judge_show_until) {
last_judge = JUDGE_NONE;
show_hit = 0;
}
// 맵이나 HUD는 박자 바뀔 때만 그려도 충분
if (beat != bs.last_beat) {
// 박자 바뀌는 순간 '삑' 소리 (\a = 터미널 벨)
printf("\a");
// 적이 박자마다 플레이어 쪽으로 다가온다 (좀비)
move_monsters(monsters, monster_count, &player, &map);
// 기존 투사체가 한 칸 날아간 뒤 명중 검사 (맞은 만큼 HP 깎임)
move_shots(shots, &map);
int hits = shot_hits_player(shots, &player);
// 사수/포수가 새 투사체를 쏜다 (다음 박자부터 날아감)
enemy_fire(monsters, monster_count, &player, shots, &map);
// 코앞에서 쏜 투사체는 생성 칸이 플레이어 자리일 수 있어 한 번 더 검사
hits += shot_hits_player(shots, &player);
if (hits > 0) {
player.hp -= hits;
player.combo = 0;
}
// 옆에 붙은 적이 플레이어를 때린다
melee_damage(monsters, monster_count, &player);
if (player.hp <= 0) {
result_player = player;
return STATE_GAME_OVER;
}
draw_hud(&bs, &player);
draw_map(&map, &player, monsters, monster_count, hit_index, shots);
draw_help();
draw_gate_info(gate_open);
bs.last_beat = beat;
hit_index = -1; // 반짝임은 한 박자만
}
// 박자 바와 판정 텍스트는 매번 갱신해야 부드럽게 차오름
draw_beat_bar(&bs);
if (show_hit) draw_judgment(JUDGE_HIT);
else draw_judgment(last_judge);
fflush(stdout);
// CPU 안 잡아먹게 짧게 쉼
usleep(10000);
}
}
int main(void) {
// 더블 버퍼링: 출력을 화면 버퍼에 모았다가 fflush 때 한 번에 내보낸다.
// 그리는 중간이 화면에 안 보여서 깜빡임이 사라진다.
static char screen_buf[1 << 16];
setvbuf(stdout, screen_buf, _IOFBF, sizeof(screen_buf));
// Ctrl+C(SIGINT)나 강제 종료(SIGTERM)로 나가도 정리되게 등록
signal(SIGINT, on_quit_signal);
signal(SIGTERM, on_quit_signal);
enable_raw_mode();
hide_cursor();
start_bgm(); // 배경음악 시작
// 게임 상태에 따라 다른 함수 호출
GameState state = STATE_MENU;
while (state != STATE_QUIT) {
if (state == STATE_MENU)state = run_menu();
else if (state == STATE_INTRO) state = run_intro();
else if (state == STATE_PLAYING) state = run_play();
else if (state == STATE_GAME_OVER) state = run_game_over();
else if (state == STATE_VICTORY) state = run_victory();
}
// 종료 처리 (raw mode 안 풀면 터미널 망가짐)
stop_bgm(); // 배경음악 정지
show_cursor();
disable_raw_mode();
clear_screen();
printf("게임 종료\n");
return 0;
}