Add wheel item type and random step/wall movement audio
This commit is contained in:
BIN
client/public/sounds/spin.ogg
Normal file
BIN
client/public/sounds/spin.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-1.ogg
Normal file
BIN
client/public/sounds/step-1.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-10.ogg
Normal file
BIN
client/public/sounds/step-10.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-11.ogg
Normal file
BIN
client/public/sounds/step-11.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-2.ogg
Normal file
BIN
client/public/sounds/step-2.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-3.ogg
Normal file
BIN
client/public/sounds/step-3.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-4.ogg
Normal file
BIN
client/public/sounds/step-4.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-5.ogg
Normal file
BIN
client/public/sounds/step-5.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-6.ogg
Normal file
BIN
client/public/sounds/step-6.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-7.ogg
Normal file
BIN
client/public/sounds/step-7.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-8.ogg
Normal file
BIN
client/public/sounds/step-8.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/step-9.ogg
Normal file
BIN
client/public/sounds/step-9.ogg
Normal file
Binary file not shown.
BIN
client/public/sounds/wall.ogg
Normal file
BIN
client/public/sounds/wall.ogg
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
// Maintainer-controlled web client version.
|
||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||
window.CHGRID_WEB_VERSION = "2026.02.20 R69";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.21 R70";
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user