Add wheel item type and random step/wall movement audio

This commit is contained in:
Jage9
2026-02-21 00:55:19 -05:00
parent 9b518d79dc
commit 9ee915e42c
23 changed files with 118 additions and 39 deletions

View File

@@ -226,15 +226,6 @@ export class AudioEngine {
}
}
sfxMove(player: { x: number; y: number }): void {
void player;
this.playSound({ freq: 165, duration: 0.05, type: 'triangle', gain: 0.13 });
}
sfxPeerMove(peer: { x: number; y: number }): void {
this.playSound({ freq: 330, duration: 0.05, type: 'triangle', gain: 0.12, sourcePosition: peer });
}
sfxLocate(peer: { x: number; y: number }): void {
this.playSound({ freq: 880, duration: 0.2, type: 'sine', gain: 0.5, sourcePosition: peer });
}

View File

@@ -83,10 +83,11 @@ const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim();
dom.appVersion.textContent = APP_VERSION
? `Another AI experiment with Jage. Version ${APP_VERSION}`
: 'Another AI experiment with Jage. Version unknown';
const ITEM_TYPE_SEQUENCE: ItemType[] = ['radio_station', 'dice'];
const ITEM_TYPE_SEQUENCE: ItemType[] = ['radio_station', 'dice', 'wheel'];
const ITEM_TYPE_GLOBAL_PROPERTIES: Record<ItemType, Record<string, string | number | boolean>> = {
radio_station: { useCooldownMs: 1000 },
dice: { useCooldownMs: 1000 },
wheel: { useCooldownMs: 4000 },
};
const EDITABLE_ITEM_PROPERTY_KEYS = new Set([
'title',
@@ -95,6 +96,7 @@ const EDITABLE_ITEM_PROPERTY_KEYS = new Set([
'volume',
'effect',
'effectValue',
'spaces',
'sides',
'number',
]);
@@ -111,6 +113,8 @@ const SYSTEM_SOUND_URLS = {
logout: withBase('sounds/logout.ogg'),
notify: withBase('sounds/notify.ogg'),
} as const;
const FOOTSTEP_SOUND_URLS = Array.from({ length: 11 }, (_, index) => withBase(`sounds/step-${index + 1}.ogg`));
const WALL_SOUND_URL = withBase('sounds/wall.ogg');
const state = createInitialState();
const renderer = new CanvasRenderer(dom.canvas);
@@ -312,7 +316,8 @@ function getPeerNamesAtPosition(x: number, y: number): string[] {
}
function itemTypeLabel(type: ItemType): string {
return type === 'radio_station' ? 'radio' : type;
if (type === 'radio_station') return 'radio';
return type;
}
function itemLabel(item: WorldItem): string {
@@ -348,6 +353,8 @@ function getEditableItemPropertyKeys(item: WorldItem): string[] {
keys.push('streamUrl', 'enabled', 'volume', 'effect', 'effectValue');
} else if (item.type === 'dice') {
keys.push('sides', 'number');
} else if (item.type === 'wheel') {
keys.push('spaces');
}
return keys;
}
@@ -758,6 +765,10 @@ function persistPlayerPosition(): void {
}
}
function randomFootstepUrl(): string {
return FOOTSTEP_SOUND_URLS[Math.floor(Math.random() * FOOTSTEP_SOUND_URLS.length)];
}
function gameLoop(): void {
if (!state.running) return;
handleMovement();
@@ -784,13 +795,17 @@ function handleMovement(): void {
const nextX = state.player.x + dx;
const nextY = state.player.y + dy;
if (nextX < 0 || nextY < 0 || nextX >= GRID_SIZE || nextY >= GRID_SIZE) return;
if (nextX < 0 || nextY < 0 || nextX >= GRID_SIZE || nextY >= GRID_SIZE) {
state.player.lastMoveTime = now;
void audio.playSample(WALL_SOUND_URL, 1);
return;
}
state.player.x = nextX;
state.player.y = nextY;
persistPlayerPosition();
state.player.lastMoveTime = now;
audio.sfxMove(state.player);
void audio.playSample(randomFootstepUrl(), 1);
signaling.send({ type: 'update_position', x: nextX, y: nextY });
const namesOnTile = getPeerNamesAtPosition(nextX, nextY);
@@ -1034,7 +1049,7 @@ async function onMessage(message: IncomingMessage): Promise<void> {
}
peerManager.setPeerPosition(message.id, message.x, message.y);
if (peer) {
audio.sfxPeerMove({ x: peer.x - state.player.x, y: peer.y - state.player.y });
void audio.playSpatialSample(randomFootstepUrl(), { x: peer.x - state.player.x, y: peer.y - state.player.y }, 1);
}
break;
}
@@ -1773,6 +1788,27 @@ function handleItemPropertyEditModeInput(code: string, key: string): void {
return;
}
signaling.send({ type: 'item_update', itemId, params: { effectValue: clampEffectLevel(parsed) } });
} else if (propertyKey === 'spaces') {
const spaces = value
.split(',')
.map((token) => token.trim())
.filter((token) => token.length > 0);
if (spaces.length === 0) {
updateStatus('spaces must include at least one comma-delimited value.');
audio.sfxUiCancel();
return;
}
if (spaces.length > 100) {
updateStatus('spaces supports up to 100 values.');
audio.sfxUiCancel();
return;
}
if (spaces.some((token) => token.length > 80)) {
updateStatus('each space must be 80 chars or less.');
audio.sfxUiCancel();
return;
}
signaling.send({ type: 'item_update', itemId, params: { spaces: spaces.join(', ') } });
} else if (propertyKey === 'sides' || propertyKey === 'number') {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {

View File

@@ -2,7 +2,7 @@ import { z } from 'zod';
export const itemSchema = z.object({
id: z.string(),
type: z.enum(['radio_station', 'dice']),
type: z.enum(['radio_station', 'dice', 'wheel']),
title: z.string(),
x: z.number().int(),
y: z.number().int(),
@@ -129,7 +129,7 @@ export type OutgoingMessage =
| { type: 'update_nickname'; nickname: string }
| { type: 'chat_message'; message: string }
| { type: 'ping'; clientSentAt: number }
| { type: 'item_add'; itemType: 'radio_station' | 'dice' }
| { type: 'item_add'; itemType: 'radio_station' | 'dice' | 'wheel' }
| { type: 'item_pickup'; itemId: string }
| { type: 'item_drop'; itemId: string; x: number; y: number }
| { type: 'item_delete'; itemId: string }

View File

@@ -76,11 +76,11 @@ export class CanvasRenderer {
private drawItem(item: WorldItem): void {
const drawX = item.x * this.squarePixelSize;
const drawY = this.canvas.height - (item.y * this.squarePixelSize) - this.squarePixelSize;
this.ctx.fillStyle = item.type === 'radio_station' ? '#fbbf24' : '#60a5fa';
this.ctx.fillStyle = item.type === 'radio_station' ? '#fbbf24' : item.type === 'wheel' ? '#f97316' : '#60a5fa';
this.ctx.fillRect(drawX, drawY, this.squarePixelSize, this.squarePixelSize);
this.ctx.fillStyle = '#111827';
this.ctx.font = 'bold 12px Courier New';
this.ctx.textAlign = 'center';
this.ctx.fillText(item.type === 'radio_station' ? 'R' : 'D', drawX + this.squarePixelSize / 2, drawY + 13);
this.ctx.fillText(item.type === 'radio_station' ? 'R' : item.type === 'wheel' ? 'W' : 'D', drawX + this.squarePixelSize / 2, drawY + 13);
}
}

View File

@@ -1,8 +1,8 @@
export const GRID_SIZE = 40;
export const HEARING_RADIUS = 15;
export const MOVE_COOLDOWN_MS = 100;
export const MOVE_COOLDOWN_MS = 200;
export type ItemType = 'radio_station' | 'dice';
export type ItemType = 'radio_station' | 'dice' | 'wheel';
export type WorldItem = {
id: string;