feat: initial prototype — core game systems (#30)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint, Build & Test (push) Successful in 1m6s

## Summary

This PR represents the full v1 prototype, implementing the core game systems for Elysium.

- Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests
- Adventurer hiring with batch size selector and progressive tier cost scaling
- Prestige, transcendence, and apotheosis systems with auto-prestige support
- Character sheet, titles, leaderboards, companion system, and daily login bonuses
- Auto-quest and auto-boss toggles
- Discord webhook notifications on prestige/transcendence/apotheosis
- Discord role awarded on apotheosis
- Responsive design and overarching story/lore system
- In-game sound effects and browser notifications for key events
- Support link button in the resource bar
- Full test coverage (100% on `apps/api` and `packages/types`)
- CI pipeline: lint → build → test

## Closes

Closes #1
Closes #2
Closes #3
Closes #4
Closes #5
Closes #6
Closes #7
Closes #8
Closes #9
Closes #10
Closes #11
Closes #12
Closes #13
Closes #14
Closes #16
Closes #19
Closes #20
Closes #21
Closes #22
Closes #23
Closes #24
Closes #25
Closes #26
Closes #27
Closes #29

 This issue was created with help from Hikari~ 🌸

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #30
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
2026-03-08 15:53:39 -07:00
committed by Naomi Carrigan
parent c69e155de3
commit 29c817230d
172 changed files with 50706 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* @file Sound effect utilities using the Web Audio API.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
type SoundEvent =
| "achievement"
| "apotheosis"
| "bossVictory"
| "prestige"
| "questCompleted"
| "questFailed"
| "transcendence";
interface SoundPattern {
frequencies: Array<number>;
gain: number;
noteDuration: number;
type: OscillatorType;
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
const SOUND_PATTERNS: Record<SoundEvent, SoundPattern> = {
achievement: {
frequencies: [ 523, 659, 784, 1047 ],
gain: 0.3,
noteDuration: 0.12,
type: "triangle",
},
apotheosis: {
frequencies: [ 1047, 880, 784, 659, 523 ],
gain: 0.35,
noteDuration: 0.25,
type: "sine",
},
bossVictory: {
frequencies: [ 523, 784, 1047 ],
gain: 0.4,
noteDuration: 0.18,
type: "square",
},
prestige: {
frequencies: [ 392, 523, 659, 784 ],
gain: 0.35,
noteDuration: 0.15,
type: "sawtooth",
},
questCompleted: {
frequencies: [ 523, 659 ],
gain: 0.25,
noteDuration: 0.15,
type: "sine",
},
questFailed: {
frequencies: [ 392, 330, 261 ],
gain: 0.25,
noteDuration: 0.18,
type: "triangle",
},
transcendence: {
frequencies: [ 261, 329, 392, 523 ],
gain: 0.3,
noteDuration: 0.3,
type: "sine",
},
};
// eslint-disable-next-line @typescript-eslint/init-declarations -- lazily initialised on first use
let audioContext: AudioContext | undefined;
const getAudioContext = (): AudioContext => {
if (audioContext === undefined) {
audioContext = new AudioContext();
}
return audioContext;
};
/**
* Plays a sound effect for a given game event using the Web Audio API.
* @param event - The game event to play a sound for.
*/
const playSound = (event: SoundEvent): void => {
try {
const context = getAudioContext();
const pattern = SOUND_PATTERNS[event];
for (const [ index, frequency ] of pattern.frequencies.entries()) {
const oscillator = context.createOscillator();
const gainNode = context.createGain();
oscillator.connect(gainNode);
gainNode.connect(context.destination);
oscillator.type = pattern.type;
oscillator.frequency.value = frequency;
const noteOffset = index * pattern.noteDuration;
const startTime = context.currentTime + noteOffset;
const endTime = startTime + pattern.noteDuration;
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(pattern.gain, startTime + 0.01);
gainNode.gain.linearRampToValueAtTime(0, endTime);
oscillator.start(startTime);
oscillator.stop(endTime);
}
} catch {
// Silently ignore — audio may not be available in all environments
}
};
export type { SoundEvent };
export { playSound };