feat: 56k modem SFX for code answers, fix crossed entrance voices
- Add sfxModem() — synthesized 56k handshake sound with carrier tones, data burst, and chirps. Plays instead of TTS for code_golf/hack_battle rounds and code-detected answers. - Fix entrance voice overlap: remove duplicate announceDeepIntro() from robe entrance, add cancelPrevious to entrance-specific voice calls (girlfriend, bouncer, shopping cart, spotlight, creator) so they cleanly replace the global intro instead of overlapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
53ae4b485d
commit
a98d94d24c
@@ -12,7 +12,7 @@ import {
|
|||||||
prefetchQuestion, prefetchAnswer, prefetchNarration,
|
prefetchQuestion, prefetchAnswer, prefetchNarration,
|
||||||
awaitQuestionReady, awaitAnswerReady,
|
awaitQuestionReady, awaitAnswerReady,
|
||||||
playQuestionNow, playAnswerNow, playNarrationNow,
|
playQuestionNow, playAnswerNow, playNarrationNow,
|
||||||
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
|
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage, sfxModem,
|
||||||
} from '../game/audio'
|
} from '../game/audio'
|
||||||
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
|
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
|
||||||
import { isPerfMode, setPerfMode } from '../game/fight/config'
|
import { isPerfMode, setPerfMode } from '../game/fight/config'
|
||||||
@@ -257,6 +257,16 @@ const challengeLabel = (type: string) => {
|
|||||||
return labels[type] || type.replace(/_/g, ' ').toUpperCase()
|
return labels[type] || type.replace(/_/g, ' ').toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CODE_CHALLENGE_TYPES = new Set(['code_golf', 'hack_battle'])
|
||||||
|
function isCodeAnswer(challengeType: string, answer: string): boolean {
|
||||||
|
if (CODE_CHALLENGE_TYPES.has(challengeType)) return true
|
||||||
|
// Heuristic: code-like tokens in the answer
|
||||||
|
const codeTokens = ['=>', '===', '!==', '&&', '||', '++', '--', '{}', '();', 'function ', 'return ', 'const ', 'let ', 'var ', 'def ', 'import ', 'class ']
|
||||||
|
let hits = 0
|
||||||
|
for (const t of codeTokens) { if (answer.includes(t)) hits++ }
|
||||||
|
return hits >= 2
|
||||||
|
}
|
||||||
|
|
||||||
const tierClass = (t: number) => `tier-${t}`
|
const tierClass = (t: number) => `tier-${t}`
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -431,8 +441,9 @@ async function _doReplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Bot A: ensure audio ready, then show log + bubble + mouth + voice all at once
|
// 2. Bot A: ensure audio ready, then show log + bubble + mouth + voice all at once
|
||||||
|
const isCodeRound = isCodeAnswer(round.challengeType, round.botAResponse || '')
|
||||||
if (round.botAResponse) {
|
if (round.botAResponse) {
|
||||||
if (doTTS) await awaitAnswerReady(props.fight.botA!.name, round.botAResponse)
|
if (doTTS && !isCodeRound) await awaitAnswerReady(props.fight.botA!.name, round.botAResponse)
|
||||||
scene?.stopShowboating('a')
|
scene?.stopShowboating('a')
|
||||||
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'}`, color: 'neon-cyan' })
|
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'}`, color: 'neon-cyan' })
|
||||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
|
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
|
||||||
@@ -443,7 +454,10 @@ async function _doReplay() {
|
|||||||
}
|
}
|
||||||
// Ensure bubble stays visible for at least 400ms even if TTS resolves instantly
|
// Ensure bubble stays visible for at least 400ms even if TTS resolves instantly
|
||||||
const minBubbleA = sleep(400).catch(() => {})
|
const minBubbleA = sleep(400).catch(() => {})
|
||||||
if (doTTS) await playAnswerNow(props.fight.botA!.name, round.botAResponse)
|
if (doTTS) {
|
||||||
|
if (isCodeRound) await sfxModem()
|
||||||
|
else await playAnswerNow(props.fight.botA!.name, round.botAResponse)
|
||||||
|
}
|
||||||
await minBubbleA
|
await minBubbleA
|
||||||
if (!doTTS && !insanityMode.value) await sleep(400)
|
if (!doTTS && !insanityMode.value) await sleep(400)
|
||||||
scene?.stopTalking('a')
|
scene?.stopTalking('a')
|
||||||
@@ -452,7 +466,7 @@ async function _doReplay() {
|
|||||||
|
|
||||||
// 3. Bot B: ensure audio ready, then show log + bubble + mouth + voice all at once
|
// 3. Bot B: ensure audio ready, then show log + bubble + mouth + voice all at once
|
||||||
if (round.botBResponse) {
|
if (round.botBResponse) {
|
||||||
if (doTTS) await awaitAnswerReady(props.fight.botB!.name, round.botBResponse)
|
if (doTTS && !isCodeRound) await awaitAnswerReady(props.fight.botB!.name, round.botBResponse)
|
||||||
scene?.stopShowboating('b')
|
scene?.stopShowboating('b')
|
||||||
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'}`, color: 'neon-pink' })
|
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'}`, color: 'neon-pink' })
|
||||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
|
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
|
||||||
@@ -464,7 +478,10 @@ async function _doReplay() {
|
|||||||
scene.startTalking('b')
|
scene.startTalking('b')
|
||||||
}
|
}
|
||||||
const minBubbleB = sleep(400).catch(() => {})
|
const minBubbleB = sleep(400).catch(() => {})
|
||||||
if (doTTS) await playAnswerNow(props.fight.botB!.name, round.botBResponse)
|
if (doTTS) {
|
||||||
|
if (isCodeRound) await sfxModem()
|
||||||
|
else await playAnswerNow(props.fight.botB!.name, round.botBResponse)
|
||||||
|
}
|
||||||
await minBubbleB
|
await minBubbleB
|
||||||
if (!doTTS && !insanityMode.value) await sleep(400)
|
if (!doTTS && !insanityMode.value) await sleep(400)
|
||||||
scene?.stopTalking('b')
|
scene?.stopTalking('b')
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export {
|
|||||||
sfxCrowdCheer,
|
sfxCrowdCheer,
|
||||||
sfxApplause,
|
sfxApplause,
|
||||||
sfxDrumRoll,
|
sfxDrumRoll,
|
||||||
|
sfxModem,
|
||||||
announceCrowdReaction,
|
announceCrowdReaction,
|
||||||
} from './sfx'
|
} from './sfx'
|
||||||
|
|
||||||
|
|||||||
@@ -885,3 +885,54 @@ export function announceCrowdReaction(type: 'cheer' | 'gasp' | 'ooh' | 'applause
|
|||||||
const fns = { cheer: sfxCrowdCheer, gasp: sfxCrowdGasp, ooh: sfxCrowdOoh, applause: sfxApplause }
|
const fns = { cheer: sfxCrowdCheer, gasp: sfxCrowdGasp, ooh: sfxCrowdOoh, applause: sfxApplause }
|
||||||
fns[type]()
|
fns[type]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 56k modem handshake — bleeps, carrier tones, and white noise bursts */
|
||||||
|
export function sfxModem(durationMs = 1800): Promise<void> {
|
||||||
|
const c = getCtx()
|
||||||
|
const d = getSfxDest()
|
||||||
|
const t = c.currentTime
|
||||||
|
const dur = durationMs / 1000
|
||||||
|
|
||||||
|
// Phase 1: Initial dial tone beeps (0-0.3s)
|
||||||
|
tone(2600, 'sine', 0.08, d, t)
|
||||||
|
tone(2400, 'sine', 0.08, d, t + 0.1)
|
||||||
|
tone(2600, 'sine', 0.06, d, t + 0.2)
|
||||||
|
|
||||||
|
// Phase 2: Carrier negotiation — rapid alternating tones (0.3-0.8s)
|
||||||
|
const freqs = [1200, 2400, 1800, 2100, 1650, 2250, 1950, 2400, 1200, 2100]
|
||||||
|
for (let i = 0; i < freqs.length; i++) {
|
||||||
|
tone(freqs[i], 'square', 0.04, d, t + 0.3 + i * 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Data burst — filtered white noise (0.8-1.4s)
|
||||||
|
const noiseLen = Math.max(1, Math.floor(c.sampleRate * 0.6))
|
||||||
|
const noiseBuf = c.createBuffer(1, noiseLen, c.sampleRate)
|
||||||
|
const noiseData = noiseBuf.getChannelData(0)
|
||||||
|
for (let i = 0; i < noiseLen; i++) {
|
||||||
|
// Modulated noise — gives it that distinctive warbling character
|
||||||
|
noiseData[i] = (Math.random() * 2 - 1) * Math.sin(i * 0.02) * Math.cos(i * 0.005)
|
||||||
|
}
|
||||||
|
const noiseSrc = c.createBufferSource()
|
||||||
|
noiseSrc.buffer = noiseBuf
|
||||||
|
const noiseGain = c.createGain()
|
||||||
|
noiseGain.gain.setValueAtTime(0.12, t + 0.8)
|
||||||
|
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + 1.4)
|
||||||
|
const bp = c.createBiquadFilter()
|
||||||
|
bp.type = 'bandpass'
|
||||||
|
bp.frequency.value = 1800
|
||||||
|
bp.Q.value = 2
|
||||||
|
noiseSrc.connect(bp)
|
||||||
|
bp.connect(noiseGain)
|
||||||
|
noiseGain.connect(d)
|
||||||
|
noiseSrc.start(t + 0.8)
|
||||||
|
noiseSrc.stop(t + 1.4)
|
||||||
|
|
||||||
|
// Phase 4: Final handshake chirps (1.4-dur)
|
||||||
|
const chirpCount = Math.floor((dur - 1.4) / 0.08)
|
||||||
|
for (let i = 0; i < chirpCount; i++) {
|
||||||
|
const f = 1200 + Math.sin(i * 1.7) * 800
|
||||||
|
tone(f, 'sine', 0.04, d, t + 1.4 + i * 0.08)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => setTimeout(resolve, durationMs))
|
||||||
|
}
|
||||||
|
|||||||
@@ -378,10 +378,10 @@ const DRAMATIC_VOICES = ['deep', 'boomer', 'movie', 'preacher', 'opera', 'demon_
|
|||||||
const HYPE_VOICES = ['hype', 'screamer', 'sportscaster', 'auctioneer', 'hyper', 'punk', 'drill', 'wrestler_v', 'karen', 'terrified', 'power_up', 'news', 'scottish']
|
const HYPE_VOICES = ['hype', 'screamer', 'sportscaster', 'auctioneer', 'hyper', 'punk', 'drill', 'wrestler_v', 'karen', 'terrified', 'power_up', 'news', 'scottish']
|
||||||
const SILLY_VOICES = ['chipmunk', 'baby', 'surfer', 'valley', 'pirate_v', 'alien_v', 'glitch', 'drunk', 'stoner', 'fairy', 'tutorial', 'npc', 'dial_up', 'glitchbot', 'grandma', 'conspiracy']
|
const SILLY_VOICES = ['chipmunk', 'baby', 'surfer', 'valley', 'pirate_v', 'alien_v', 'glitch', 'drunk', 'stoner', 'fairy', 'tutorial', 'npc', 'dial_up', 'glitchbot', 'grandma', 'conspiracy']
|
||||||
const COOL_VOICES = ['smooth', 'wizard_v', 'ninja_v', 'cowboy_v', 'angel', 'whisper', 'posh', 'aussie', 'french', 'sensei', 'ai_core', 'android_v', 'siri', 'professor', 'texan', 'boss_taunt', 'sleepy']
|
const COOL_VOICES = ['smooth', 'wizard_v', 'ninja_v', 'cowboy_v', 'angel', 'whisper', 'posh', 'aussie', 'french', 'sensei', 'ai_core', 'android_v', 'siri', 'professor', 'texan', 'boss_taunt', 'sleepy']
|
||||||
export function announceDramatic(text: string) { speak(text, DRAMATIC_VOICES[Math.floor(Math.random() * DRAMATIC_VOICES.length)]) }
|
export function announceDramatic(text: string, cancel = false) { speak(text, DRAMATIC_VOICES[Math.floor(Math.random() * DRAMATIC_VOICES.length)], cancel) }
|
||||||
export function announceHype(text: string) { speak(text, HYPE_VOICES[Math.floor(Math.random() * HYPE_VOICES.length)]) }
|
export function announceHype(text: string, cancel = false) { speak(text, HYPE_VOICES[Math.floor(Math.random() * HYPE_VOICES.length)], cancel) }
|
||||||
export function announceSilly(text: string) { speak(text, SILLY_VOICES[Math.floor(Math.random() * SILLY_VOICES.length)]) }
|
export function announceSilly(text: string, cancel = false) { speak(text, SILLY_VOICES[Math.floor(Math.random() * SILLY_VOICES.length)], cancel) }
|
||||||
export function announceCool(text: string) { speak(text, COOL_VOICES[Math.floor(Math.random() * COOL_VOICES.length)]) }
|
export function announceCool(text: string, cancel = false) { speak(text, COOL_VOICES[Math.floor(Math.random() * COOL_VOICES.length)], cancel) }
|
||||||
|
|
||||||
// === HYPE LINES ===
|
// === HYPE LINES ===
|
||||||
|
|
||||||
@@ -494,8 +494,8 @@ export function announceRoundHype() {
|
|||||||
|
|
||||||
const CREATOR_VOICES = ['ancient', 'hal', 'mainframe', 'echo_v', 'final_boss', 'wizard_v', 'deep', 'preacher']
|
const CREATOR_VOICES = ['ancient', 'hal', 'mainframe', 'echo_v', 'final_boss', 'wizard_v', 'deep', 'preacher']
|
||||||
|
|
||||||
function announceCreator(text: string) {
|
function announceCreator(text: string, cancel = false) {
|
||||||
speak(text, CREATOR_VOICES[Math.floor(Math.random() * CREATOR_VOICES.length)])
|
speak(text, CREATOR_VOICES[Math.floor(Math.random() * CREATOR_VOICES.length)], cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CREATOR_ENTRANCE_LINES = [
|
const CREATOR_ENTRANCE_LINES = [
|
||||||
@@ -640,9 +640,9 @@ const CREATOR_DEVASTATING_LINES = [
|
|||||||
|
|
||||||
const CREATOR_ANSWER_VOICES = ['hal', 'mainframe', 'echo_v', 'ancient', 'wizard_v']
|
const CREATOR_ANSWER_VOICES = ['hal', 'mainframe', 'echo_v', 'ancient', 'wizard_v']
|
||||||
|
|
||||||
export function announceCreatorEntrance() {
|
export function announceCreatorEntrance(cancel = false) {
|
||||||
const line = CREATOR_ENTRANCE_LINES[Math.floor(Math.random() * CREATOR_ENTRANCE_LINES.length)]
|
const line = CREATOR_ENTRANCE_LINES[Math.floor(Math.random() * CREATOR_ENTRANCE_LINES.length)]
|
||||||
announceCreator(line)
|
announceCreator(line, cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function announceCreatorRound() {
|
export function announceCreatorRound() {
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ async function playEntrance() {
|
|||||||
// Robe overlay
|
// Robe overlay
|
||||||
const robeColor = ['#8b0000', '#00008b', '#006400', '#4b0082', '#8b4513'][Math.floor(Math.random() * 5)]
|
const robeColor = ['#8b0000', '#00008b', '#006400', '#4b0082', '#8b4513'][Math.floor(Math.random() * 5)]
|
||||||
const robe = k.add([k.rect(50, 55), k.pos(startX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(robeColor)), k.opacity(0.85), k.z(11)])
|
const robe = k.add([k.rect(50, 55), k.pos(startX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(robeColor)), k.opacity(0.85), k.z(11)])
|
||||||
announceDeepIntro()
|
|
||||||
// Slow walk in
|
// Slow walk in
|
||||||
await k.tween(startX, homeX, 0.8, (v) => { fighter.pos.x = v; robe.pos.x = v }, k.easings.easeInOutQuad)
|
await k.tween(startX, homeX, 0.8, (v) => { fighter.pos.x = v; robe.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
await k.wait(0.3)
|
await k.wait(0.3)
|
||||||
@@ -109,7 +108,7 @@ async function playEntrance() {
|
|||||||
const gfX = startX + dir * 40
|
const gfX = startX + dir * 40
|
||||||
const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor('#ff69b4')), k.opacity(0.9), k.z(11)])
|
const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor('#ff69b4')), k.opacity(0.9), k.z(11)])
|
||||||
const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor('#ff0000')), k.z(12)])
|
const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor('#ff0000')), k.z(12)])
|
||||||
announceSilly('We need to talk! About your ELO!')
|
announceSilly('We need to talk! About your ELO!', true)
|
||||||
// Walk in arguing
|
// Walk in arguing
|
||||||
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
|
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
|
||||||
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
|
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
|
||||||
@@ -322,7 +321,7 @@ async function playEntrance() {
|
|||||||
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||||
// Bouncer arm
|
// Bouncer arm
|
||||||
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor('#444444')), k.z(12)])
|
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor('#444444')), k.z(12)])
|
||||||
announceSilly('And stay out! You\'re BANNED from the other fight!')
|
announceSilly('And stay out! You\'re BANNED from the other fight!', true)
|
||||||
await k.wait(0.3)
|
await k.wait(0.3)
|
||||||
// Throw
|
// Throw
|
||||||
sfxZoomWhoosh()
|
sfxZoomWhoosh()
|
||||||
@@ -381,7 +380,7 @@ async function playEntrance() {
|
|||||||
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor('#888888')), k.opacity(0.9), k.z(9)])
|
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor('#888888')), k.opacity(0.9), k.z(9)])
|
||||||
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor('#444')), k.z(9)])
|
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor('#444')), k.z(9)])
|
||||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
|
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
|
||||||
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!')
|
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!', true)
|
||||||
sfxZoomWhoosh()
|
sfxZoomWhoosh()
|
||||||
await k.tween(startX, homeX, 0.5, (v) => {
|
await k.tween(startX, homeX, 0.5, (v) => {
|
||||||
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
|
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
|
||||||
@@ -400,7 +399,7 @@ async function playEntrance() {
|
|||||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||||
// Spotlight cone
|
// Spotlight cone
|
||||||
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor('#ffe14d')), k.opacity(0.08), k.z(1)])
|
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor('#ffe14d')), k.opacity(0.08), k.z(1)])
|
||||||
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!')
|
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!', true)
|
||||||
await k.tween(startX, homeX, 1.0, (v) => {
|
await k.tween(startX, homeX, 1.0, (v) => {
|
||||||
fighter.pos.x = v; spot.pos.x = v - 30
|
fighter.pos.x = v; spot.pos.x = v - 30
|
||||||
}, k.easings.easeInOutQuad)
|
}, k.easings.easeInOutQuad)
|
||||||
@@ -494,7 +493,7 @@ async function playEntrance() {
|
|||||||
p.angle = Math.sin(k.time() * 3 + i) * 15
|
p.angle = Math.sin(k.time() * 3 + i) * 15
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
announceCreatorEntrance()
|
announceCreatorEntrance(true)
|
||||||
rainDrops.forEach(r => { if (r.exists()) r.destroy() })
|
rainDrops.forEach(r => { if (r.exists()) r.destroy() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user