Skip to content

Commit ab52acf

Browse files
committed
bugbubblemonster 상대공격 알림, 날아와서 터짐
bugbubblemonster 상대공격 알림, 날아와서 터지는 로직으로 변경
1 parent 5cfdf5c commit ab52acf

File tree

3 files changed

+124
-11
lines changed

3 files changed

+124
-11
lines changed

backend/core/socket_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ async def draw_submit(sid, data):
409409
print(f"⚠️ [ArchDraw] Skipping gameover emit — room already in new game phase: {room.get('phase')}")
410410

411411
# 스냅샷 점수 기준으로 전적 저장 (await 중 점수 변조 방지)
412+
print(f"📊 [ArchDraw] DB Save: P1(uid={p1.get('user_id')}, score={p1_snap['score']}) vs P2(uid={p2.get('user_id')}, score={p2_snap['score']})")
412413
if p1_snap['score'] > p2_snap['score']:
413414
await update_battle_record(p1.get('user_id'), 'win')
414415
await update_battle_record(p2.get('user_id'), 'lose')
@@ -712,6 +713,7 @@ async def bubble_game_over(sid, data):
712713
players = bubble_rooms[room_id]['players']
713714
for p in players:
714715
result = 'lose' if p['sid'] == sid else 'win'
716+
print(f"📊 [BugBubble] DB Save: uid={p.get('user_id')}, result={result}")
715717
await update_battle_record(p.get('user_id'), result)
716718

717719
# 공통 채팅 및 기타 이벤트 유지 (chat_message, update_role 등 기존 코드와 동일)

frontend/src/App.vue

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,13 @@ const isPracticePage = computed(() => {
200200
'BugHunt',
201201
'Wars',
202202
'ArchDrawQuiz',
203-
'LogicRun', // [수정일: 2026-02-24] SpeedArchBuilder → LogicRun으로 교체 (미추가 시 메인으로 이동되는 버그)
204-
'BugBubbleMonster', // [추가일: 2026-02-25] 신규 1:1 디펜스 게임
205-
'PressureInterviewRoom',
203+
'LogicRun',
204+
'BugBubbleMonster',
206205
'GrowthReport',
207-
'ProgressiveProblems',
208206
'Management',
209207
'MyRecords',
210208
'AICoach',
211209
'MockInterview',
212-
'WarLobby',
213210
'LogViewer'
214211
];
215212
return practiceRoutes.includes(route?.name);

frontend/src/features/wars/minigames/BugBubbleMonster.vue

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="bubble-game" :class="{ 'screen-shake': shaking }">
2+
<div class="bubble-game" :class="{ 'screen-shake': shaking, 'bubble-flash': bubbleFlash }">
33
<div class="crt-lines"></div>
44

55
<!-- ===== JOIN SCREEN ===== -->
@@ -163,7 +163,21 @@
163163
</div>
164164
</div>
165165

166+
<!-- 상대 공격 경고 알림 -->
167+
<transition name="alert-slide">
168+
<div v-if="incomingAlert" class="incoming-alert">
169+
<span class="alert-text">{{ incomingAlert }}</span>
170+
</div>
171+
</transition>
172+
166173
<div class="monster-overlay">
174+
<!-- 수신 버블: 날아와서 터짐 -->
175+
<div v-for="b in incomingBubbles" :key="'ib'+b.id" class="incoming-bubble"
176+
:class="{ 'ib-flying': b.phase === 'flying', 'ib-pop': b.phase === 'pop' }"
177+
:style="{ '--target-x': b.targetX + 'px', '--target-y': b.targetY + 'px', '--start-y': b.y + 'px' }">
178+
<span class="ib-shell">🫧</span>
179+
<span class="ib-inner">👾</span>
180+
</div>
167181
<div v-for="m in activeMonsters" :key="m.id" class="monster-bug"
168182
:style="{ left: m.x + 'px', top: m.y + 'px', fontSize: m.size + 'rem' }">👾</div>
169183
<transition-group name="bubble-fly" tag="div">
@@ -218,14 +232,17 @@ const termBody = ref(null)
218232
const activeMonsters = ref([])
219233
const flyingBubbles = ref([])
220234
const comboPops = ref([])
221-
const maxMonsters = 25
235+
const maxMonsters = 12
222236
const opponentMonsterCount = ref(0)
223237
const isWinner = ref(false)
224238
const combo = ref(0)
225239
const bestCombo = ref(0)
226240
const totalSolved = ref(0)
227241
const totalBubblesSent = ref(0)
228242
const shaking = ref(false)
243+
const incomingAlert = ref('') // 상대 공격 경고
244+
const bubbleFlash = ref(false)
245+
let alertTimer = null
229246
let flyBubbleId = 0
230247
let combPopId = 0
231248
let animFrameId = null
@@ -318,7 +335,7 @@ function getChoiceClass(idx) {
318335
}
319336
320337
function selectChoice(idx) {
321-
if (answerState.value !== 'idle') return
338+
if (answerState.value !== 'idle' || gamePhase.value !== 'playing') return
322339
selectedChoiceIdx.value = idx
323340
const choice = currentProblem.value?.choices?.[idx]
324341
if (!choice) return
@@ -355,6 +372,51 @@ function selectChoice(idx) {
355372
}, 1200)
356373
}
357374
375+
// 상대 버블 날아와서 터지면서 몬스터 스폰
376+
const incomingBubbles = ref([])
377+
let incomingBubbleId = 0
378+
379+
function spawnIncomingBubble(monsterCount) {
380+
const id = ++incomingBubbleId
381+
const targetX = 100 + Math.random() * (window.innerWidth * 0.5)
382+
const targetY = 150 + Math.random() * (window.innerHeight * 0.4)
383+
incomingBubbles.value.push({ id, x: window.innerWidth + 50, y: targetY, targetX, targetY, phase: 'flying' })
384+
385+
// 0.7초 후 도작 → 터짐
386+
setTimeout(() => {
387+
const b = incomingBubbles.value.find(b => b.id === id)
388+
if (b) b.phase = 'pop'
389+
// 화면 빨간 플래시
390+
bubbleFlash.value = true
391+
setTimeout(() => { bubbleFlash.value = false }, 400)
392+
// 터진 위치에서 몬스터 스폰 (버블 위치 기준)
393+
for (let i = 0; i < monsterCount; i++) {
394+
activeMonsters.value.push({
395+
id: Date.now() + Math.random(),
396+
x: targetX + (Math.random() - 0.5) * 80,
397+
y: targetY + (Math.random() - 0.5) * 80,
398+
dx: (Math.random() - 0.5) * 3, dy: (Math.random() - 0.5) * 3,
399+
size: 1.5 + Math.random() * 0.8
400+
})
401+
}
402+
// 게임오버 판정
403+
if (activeMonsters.value.length >= maxMonsters && gamePhase.value === 'playing') {
404+
gamePhase.value = 'gameover-pending'
405+
bs.emitGameOver(currentRoomId.value)
406+
}
407+
// 0.6초 후 버블 제거
408+
setTimeout(() => {
409+
incomingBubbles.value = incomingBubbles.value.filter(b => b.id !== id)
410+
}, 600)
411+
}, 700)
412+
}
413+
414+
function showAlert(msg) {
415+
incomingAlert.value = msg
416+
if (alertTimer) clearTimeout(alertTimer)
417+
alertTimer = setTimeout(() => { incomingAlert.value = '' }, 1500)
418+
}
419+
358420
function nextProblem() {
359421
if (allProblems.value.length === 0) return
360422
currentProblemIndex.value = (currentProblemIndex.value + 1) % allProblems.value.length
@@ -384,7 +446,10 @@ function spawnMonsters(count) {
384446
size: 1.5 + Math.random() * 0.8
385447
})
386448
}
387-
if (activeMonsters.value.length >= maxMonsters) bs.emitGameOver(currentRoomId.value)
449+
if (activeMonsters.value.length >= maxMonsters && gamePhase.value === 'playing') {
450+
gamePhase.value = 'gameover-pending' // 즉시 입력 차단
451+
bs.emitGameOver(currentRoomId.value)
452+
}
388453
}
389454
390455
function startGameLoop() {
@@ -439,8 +504,17 @@ onMounted(() => {
439504
}, 800)
440505
}
441506
442-
bs.onReceiveMonster.value = () => spawnMonsters(1)
443-
bs.onReceiveFever.value = (data) => spawnMonsters(data.count || 3)
507+
bs.onReceiveMonster.value = () => {
508+
showAlert('⚠️ 상대 공격!')
509+
spawnIncomingBubble(1)
510+
}
511+
bs.onReceiveFever.value = (data) => {
512+
const cnt = data.count || 3
513+
showAlert(`🔥 FEVER 공격! 몬스터 ${cnt}마리!`)
514+
shaking.value = true
515+
setTimeout(() => shaking.value = false, 600)
516+
spawnIncomingBubble(cnt)
517+
}
444518
445519
bs.onMonsterSync.value = (data) => {
446520
if (!data?.counts || !bs.socket.value) return
@@ -648,4 +722,44 @@ onUnmounted(() => {
648722
.btn-retry { flex:1; max-width:180px; padding:.6rem; font-family:'Orbitron',sans-serif; font-size:.75rem; font-weight:700; background:transparent; border:2px solid #00f0ff; color:#00f0ff; border-radius:.6rem; cursor:pointer; transition:all .2s }
649723
.btn-retry:hover { background:rgba(0,240,255,.1) }
650724
.btn-exit { flex:1; max-width:180px; padding:.6rem; font-family:'Orbitron',sans-serif; font-size:.75rem; font-weight:700; background:transparent; border:1px solid #334155; color:#64748b; border-radius:.6rem; cursor:pointer }
725+
726+
/* 수신 버블 애니메이션 */
727+
.incoming-bubble { position:absolute; z-index:35; font-size:5rem; pointer-events:none }
728+
.ib-shell { position:relative; z-index:1; filter:drop-shadow(0 0 20px rgba(0,240,255,.6)) }
729+
.ib-inner { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:2.2rem; z-index:2 }
730+
.ib-flying { animation: ib-fly .7s cubic-bezier(.2,.8,.3,1) forwards }
731+
.ib-pop .ib-shell { animation: ib-burst-shell .6s ease-out forwards }
732+
.ib-pop .ib-inner { animation: ib-burst-monster .6s ease-out forwards }
733+
.ib-pop::after { content:''; position:absolute; top:50%; left:50%; width:200px; height:200px; transform:translate(-50%,-50%); border-radius:50%; background:radial-gradient(circle, rgba(255,45,117,.5) 0%, rgba(255,45,117,0) 70%); animation: ib-shockwave .6s ease-out forwards; pointer-events:none }
734+
@keyframes ib-fly {
735+
0% { right:-80px; top:var(--start-y); opacity:0; transform:scale(.5) }
736+
20% { opacity:1; transform:scale(1.1) }
737+
100% { left:var(--target-x); top:var(--target-y); opacity:1; transform:scale(1) }
738+
}
739+
@keyframes ib-burst-shell {
740+
0% { transform:scale(1); opacity:1 }
741+
30% { transform:scale(2.2); opacity:1; filter:brightness(3) drop-shadow(0 0 40px rgba(255,45,117,1)) }
742+
100% { transform:scale(3); opacity:0 }
743+
}
744+
@keyframes ib-burst-monster {
745+
0% { transform:translate(-50%,-50%) scale(1); opacity:1 }
746+
30% { transform:translate(-50%,-50%) scale(1.8); opacity:1 }
747+
100% { transform:translate(-50%,-50%) scale(.5); opacity:0 }
748+
}
749+
@keyframes ib-shockwave {
750+
0% { width:0; height:0; opacity:1 }
751+
100% { width:300px; height:300px; opacity:0 }
752+
}
753+
/* 버블 터질 때 화면 빨간 플래시 */
754+
.bubble-flash::after { content:''; position:fixed; inset:0; background:rgba(255,45,117,.15); z-index:9000; pointer-events:none; animation:bflash .4s ease-out forwards }
755+
@keyframes bflash { 0%{opacity:1} 100%{opacity:0} }
756+
757+
/* 상대 공격 경고 알림 */
758+
.incoming-alert { position:fixed; top:80px; left:50%; transform:translateX(-50%); z-index:50; background:rgba(255,45,117,.15); border:2px solid #ff2d75; border-radius:12px; padding:.5rem 1.5rem; backdrop-filter:blur(8px); box-shadow:0 0 30px rgba(255,45,117,.3); animation:alert-pulse .4s ease infinite alternate }
759+
.alert-text { font-family:'Orbitron',sans-serif; font-size:.85rem; font-weight:700; color:#ff2d75; letter-spacing:1px; text-shadow:0 0 10px rgba(255,45,117,.6) }
760+
@keyframes alert-pulse { from{box-shadow:0 0 15px rgba(255,45,117,.2)} to{box-shadow:0 0 40px rgba(255,45,117,.5)} }
761+
.alert-slide-enter-active { transition:all .3s ease-out }
762+
.alert-slide-leave-active { transition:all .4s ease-in }
763+
.alert-slide-enter-from { opacity:0; transform:translateX(-50%) translateY(-20px) }
764+
.alert-slide-leave-to { opacity:0; transform:translateX(-50%) translateY(-10px) }
651765
</style>

0 commit comments

Comments
 (0)