Add Shift+Enter secondary item action with radio handler
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.25 R259";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R260";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type MainModeCommand =
|
|||||||
| 'openMicGainEdit'
|
| 'openMicGainEdit'
|
||||||
| 'calibrateMicrophone'
|
| 'calibrateMicrophone'
|
||||||
| 'useItem'
|
| 'useItem'
|
||||||
|
| 'secondaryUseItem'
|
||||||
| 'speakUsers'
|
| 'speakUsers'
|
||||||
| 'addItem'
|
| 'addItem'
|
||||||
| 'locateOrListItems'
|
| 'locateOrListItems'
|
||||||
@@ -51,7 +52,7 @@ export function resolveMainModeCommand(code: string, shiftKey: boolean): MainMod
|
|||||||
if (code === 'NumpadSubtract') return 'masterVolumeDown';
|
if (code === 'NumpadSubtract') return 'masterVolumeDown';
|
||||||
if (code === 'KeyC') return shiftKey ? null : 'speakCoordinates';
|
if (code === 'KeyC') return shiftKey ? null : 'speakCoordinates';
|
||||||
if (code === 'KeyV') return shiftKey ? 'calibrateMicrophone' : 'openMicGainEdit';
|
if (code === 'KeyV') return shiftKey ? 'calibrateMicrophone' : 'openMicGainEdit';
|
||||||
if (code === 'Enter') return 'useItem';
|
if (code === 'Enter') return shiftKey ? 'secondaryUseItem' : 'useItem';
|
||||||
if (code === 'KeyU') return shiftKey ? null : 'speakUsers';
|
if (code === 'KeyU') return shiftKey ? null : 'speakUsers';
|
||||||
if (code === 'KeyA') return shiftKey ? null : 'addItem';
|
if (code === 'KeyA') return shiftKey ? null : 'addItem';
|
||||||
if (code === 'KeyI') return 'locateOrListItems';
|
if (code === 'KeyI') return 'locateOrListItems';
|
||||||
|
|||||||
@@ -1021,6 +1021,11 @@ function useItem(item: WorldItem): void {
|
|||||||
signaling.send({ type: 'item_use', itemId: item.id });
|
signaling.send({ type: 'item_use', itemId: item.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sends an item secondary-use request for the selected item. */
|
||||||
|
function secondaryUseItem(item: WorldItem): void {
|
||||||
|
signaling.send({ type: 'item_secondary_use', itemId: item.id });
|
||||||
|
}
|
||||||
|
|
||||||
/** Opens option-list selection mode for list-based item properties. */
|
/** Opens option-list selection mode for list-based item properties. */
|
||||||
function openItemPropertyOptionSelect(item: WorldItem, key: string): void {
|
function openItemPropertyOptionSelect(item: WorldItem, key: string): void {
|
||||||
const options = getItemPropertyOptionValues(item.type, key);
|
const options = getItemPropertyOptionValues(item.type, key);
|
||||||
@@ -1825,6 +1830,26 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void {
|
|||||||
beginItemSelection('use', usable);
|
beginItemSelection('use', usable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'secondaryUseItem': {
|
||||||
|
const carried = getCarriedItem();
|
||||||
|
if (carried) {
|
||||||
|
secondaryUseItem(carried);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const squareItems = getItemsAtPosition(state.player.x, state.player.y);
|
||||||
|
const usable = squareItems.filter((item) => item.capabilities.includes('usable'));
|
||||||
|
if (usable.length === 0) {
|
||||||
|
updateStatus('No usable items here.');
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (usable.length === 1) {
|
||||||
|
secondaryUseItem(usable[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beginItemSelection('secondaryUse', usable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'speakUsers': {
|
case 'speakUsers': {
|
||||||
const allUsers = [state.player.nickname, ...Array.from(state.peers.values()).map((p) => p.nickname)];
|
const allUsers = [state.player.nickname, ...Array.from(state.peers.values()).map((p) => p.nickname)];
|
||||||
const label = allUsers.length === 1 ? 'user' : 'users';
|
const label = allUsers.length === 1 ? 'user' : 'users';
|
||||||
@@ -2383,6 +2408,10 @@ function handleSelectItemModeInput(code: string, key: string): void {
|
|||||||
useItem(selected);
|
useItem(selected);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (context === 'secondaryUse') {
|
||||||
|
secondaryUseItem(selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (context === 'inspect') {
|
if (context === 'inspect') {
|
||||||
beginItemProperties(selected, true);
|
beginItemProperties(selected, true);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -256,10 +256,10 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (message.ok) {
|
if (message.ok) {
|
||||||
if (message.action === 'use') {
|
if (message.action === 'use' || message.action === 'secondary_use') {
|
||||||
deps.pushChatMessage(message.message);
|
deps.pushChatMessage(message.message);
|
||||||
const item = message.itemId ? deps.getItemById(message.itemId) : null;
|
const item = message.itemId ? deps.getItemById(message.itemId) : null;
|
||||||
if (!item?.useSound && item && item.type !== 'piano') {
|
if (message.action === 'use' && !item?.useSound && item && item.type !== 'piano') {
|
||||||
deps.playLocateToneAt(item.x, item.y);
|
deps.playLocateToneAt(item.x, item.y);
|
||||||
}
|
}
|
||||||
} else if (message.action !== 'update') {
|
} else if (message.action !== 'update') {
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export const itemRemoveSchema = z.object({
|
|||||||
export const itemActionResultSchema = z.object({
|
export const itemActionResultSchema = z.object({
|
||||||
type: z.literal('item_action_result'),
|
type: z.literal('item_action_result'),
|
||||||
ok: z.boolean(),
|
ok: z.boolean(),
|
||||||
action: z.enum(['add', 'pickup', 'drop', 'delete', 'use', 'update']),
|
action: z.enum(['add', 'pickup', 'drop', 'delete', 'use', 'secondary_use', 'update']),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
itemId: z.string().optional(),
|
itemId: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -287,6 +287,7 @@ export type OutgoingMessage =
|
|||||||
| { type: 'item_drop'; itemId: string; x: number; y: number }
|
| { type: 'item_drop'; itemId: string; x: number; y: number }
|
||||||
| { type: 'item_delete'; itemId: string }
|
| { type: 'item_delete'; itemId: string }
|
||||||
| { type: 'item_use'; itemId: string }
|
| { type: 'item_use'; itemId: string }
|
||||||
|
| { type: 'item_secondary_use'; itemId: string }
|
||||||
| { type: 'item_piano_note'; itemId: string; keyId: string; midi: number; on: boolean }
|
| { type: 'item_piano_note'; itemId: string; keyId: string; midi: number; on: boolean }
|
||||||
| { type: 'item_piano_recording'; itemId: string; action: 'toggle_record' | 'playback' | 'stop_playback' | 'stop_record' }
|
| { type: 'item_piano_recording'; itemId: string; action: 'toggle_record' | 'playback' | 'stop_playback' | 'stop_record' }
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type WorldItem = {
|
|||||||
carrierId?: string | null;
|
carrierId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | 'inspect' | null;
|
export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | 'secondaryUse' | 'inspect' | null;
|
||||||
|
|
||||||
export type GameMode =
|
export type GameMode =
|
||||||
| 'normal'
|
| 'normal'
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ This document is the authoritative keymap for the client.
|
|||||||
- `D`: Pick up/drop item
|
- `D`: Pick up/drop item
|
||||||
- `Shift+D`: Delete item
|
- `Shift+D`: Delete item
|
||||||
- `Enter`: Use item
|
- `Enter`: Use item
|
||||||
|
- `Shift+Enter`: Secondary item action
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
- `P`: Ping server
|
- `P`: Ping server
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
|
|
||||||
### Use
|
### Use
|
||||||
- `use` toggles `enabled` on/off and broadcasts chat status.
|
- `use` toggles `enabled` on/off and broadcasts chat status.
|
||||||
|
- `secondary use` reports now-playing metadata (`Playing <song> from <station>`), or `<title> is off` when disabled.
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
- `mediaChannel`: `stereo | mono | left | right`
|
- `mediaChannel`: `stereo | mono | left | right`
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `chat_message`: player chat.
|
- `chat_message`: player chat.
|
||||||
- `ping`: latency measurement.
|
- `ping`: latency measurement.
|
||||||
- `item_add`, `item_pickup`, `item_drop`, `item_delete`, `item_use`, `item_update`: item actions.
|
- `item_add`, `item_pickup`, `item_drop`, `item_delete`, `item_use`, `item_update`: item actions.
|
||||||
|
- `item_secondary_use`: trigger type-specific secondary action when implemented.
|
||||||
- `item_piano_note`: realtime piano note on/off for active piano use mode.
|
- `item_piano_note`: realtime piano note on/off for active piano use mode.
|
||||||
- `item_piano_recording`: piano record/playback control (`toggle_record`, `playback`, `stop_playback`).
|
- `item_piano_recording`: piano record/playback control (`toggle_record`, `playback`, `stop_playback`).
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from .items.registry import ITEM_MODULES
|
|||||||
from .item_types import ItemTypeHandler
|
from .item_types import ItemTypeHandler
|
||||||
|
|
||||||
ITEM_TYPE_HANDLERS: dict[ItemType, ItemTypeHandler] = {
|
ITEM_TYPE_HANDLERS: dict[ItemType, ItemTypeHandler] = {
|
||||||
item_type: ItemTypeHandler(validate_update=module.validate_update, use=module.use_item)
|
item_type: ItemTypeHandler(
|
||||||
|
validate_update=module.validate_update,
|
||||||
|
use=module.use_item,
|
||||||
|
secondary_use=getattr(module, "secondary_use_item", None),
|
||||||
|
)
|
||||||
for item_type, module in ITEM_MODULES.items()
|
for item_type, module in ITEM_MODULES.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ class ItemTypeHandler:
|
|||||||
|
|
||||||
validate_update: Callable[[WorldItem, dict], dict]
|
validate_update: Callable[[WorldItem, dict], dict]
|
||||||
use: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult]
|
use: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult]
|
||||||
|
secondary_use: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult] | None = None
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class ItemModule(Protocol):
|
|||||||
PROPERTY_METADATA: dict[str, dict[str, object]]
|
PROPERTY_METADATA: dict[str, dict[str, object]]
|
||||||
validate_update: Callable[[WorldItem, dict], dict]
|
validate_update: Callable[[WorldItem, dict], dict]
|
||||||
use_item: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult]
|
use_item: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult]
|
||||||
|
secondary_use_item: Callable[[WorldItem, str, Callable[[dict], str]], ItemUseResult] | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from types import SimpleNamespace
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def build_item_module(definition: Any, *, validate_update: Any, use_item: Any) -> Any:
|
def build_item_module(definition: Any, *, validate_update: Any, use_item: Any, secondary_use_item: Any = None) -> Any:
|
||||||
"""Compose a plugin module-like object from split definition/validator/actions files."""
|
"""Compose a plugin module-like object from split definition/validator/actions files."""
|
||||||
|
|
||||||
exports: dict[str, Any] = {
|
exports: dict[str, Any] = {
|
||||||
@@ -16,4 +16,6 @@ def build_item_module(definition: Any, *, validate_update: Any, use_item: Any) -
|
|||||||
}
|
}
|
||||||
exports["validate_update"] = validate_update
|
exports["validate_update"] = validate_update
|
||||||
exports["use_item"] = use_item
|
exports["use_item"] = use_item
|
||||||
|
if secondary_use_item is not None:
|
||||||
|
exports["secondary_use_item"] = secondary_use_item
|
||||||
return SimpleNamespace(**exports)
|
return SimpleNamespace(**exports)
|
||||||
|
|||||||
@@ -19,3 +19,25 @@ def use_item(item: WorldItem, nickname: str, _clock_formatter: Callable[[dict],
|
|||||||
others_message=f"{nickname} turns {state_text} {item.title}.",
|
others_message=f"{nickname} turns {state_text} {item.title}.",
|
||||||
updated_params={**item.params, "enabled": next_enabled},
|
updated_params={**item.params, "enabled": next_enabled},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def secondary_use_item(item: WorldItem, _nickname: str, _clock_formatter: Callable[[dict], str]) -> ItemUseResult:
|
||||||
|
"""Speak now-playing metadata for this radio."""
|
||||||
|
|
||||||
|
if item.params.get("enabled") is False:
|
||||||
|
return ItemUseResult(
|
||||||
|
self_message=f"{item.title} is off.",
|
||||||
|
others_message=f"{item.title} is off.",
|
||||||
|
)
|
||||||
|
|
||||||
|
station_name = str(item.params.get("stationName", "")).strip()
|
||||||
|
now_playing = str(item.params.get("nowPlaying", "")).strip()
|
||||||
|
if now_playing and station_name:
|
||||||
|
message = f"Playing {now_playing} from {station_name}."
|
||||||
|
elif now_playing:
|
||||||
|
message = f"Playing {now_playing}."
|
||||||
|
elif station_name:
|
||||||
|
message = f"Playing from {station_name}."
|
||||||
|
else:
|
||||||
|
message = "No now playing data."
|
||||||
|
return ItemUseResult(self_message=message, others_message=message)
|
||||||
|
|||||||
@@ -8,5 +8,10 @@ from . import actions, definition, validator
|
|||||||
ITEM_TYPE_PLUGIN = {
|
ITEM_TYPE_PLUGIN = {
|
||||||
"type": "radio_station",
|
"type": "radio_station",
|
||||||
"order": 40,
|
"order": 40,
|
||||||
"module": build_item_module(definition, validate_update=validator.validate_update, use_item=actions.use_item),
|
"module": build_item_module(
|
||||||
|
definition,
|
||||||
|
validate_update=validator.validate_update,
|
||||||
|
use_item=actions.use_item,
|
||||||
|
secondary_use_item=actions.secondary_use_item,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ class ItemUsePacket(BasePacket):
|
|||||||
itemId: str
|
itemId: str
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSecondaryUsePacket(BasePacket):
|
||||||
|
type: Literal["item_secondary_use"]
|
||||||
|
itemId: str
|
||||||
|
|
||||||
|
|
||||||
class ItemPianoNotePacket(BasePacket):
|
class ItemPianoNotePacket(BasePacket):
|
||||||
type: Literal["item_piano_note"]
|
type: Literal["item_piano_note"]
|
||||||
itemId: str
|
itemId: str
|
||||||
@@ -132,6 +137,7 @@ ClientPacket = (
|
|||||||
| ItemDropPacket
|
| ItemDropPacket
|
||||||
| ItemDeletePacket
|
| ItemDeletePacket
|
||||||
| ItemUsePacket
|
| ItemUsePacket
|
||||||
|
| ItemSecondaryUsePacket
|
||||||
| ItemPianoNotePacket
|
| ItemPianoNotePacket
|
||||||
| ItemPianoRecordingPacket
|
| ItemPianoRecordingPacket
|
||||||
| ItemUpdatePacket
|
| ItemUpdatePacket
|
||||||
@@ -275,7 +281,7 @@ class ItemRemovePacket(BasePacket):
|
|||||||
class ItemActionResultPacket(BasePacket):
|
class ItemActionResultPacket(BasePacket):
|
||||||
type: Literal["item_action_result"]
|
type: Literal["item_action_result"]
|
||||||
ok: bool
|
ok: bool
|
||||||
action: Literal["add", "pickup", "drop", "delete", "use", "update"]
|
action: Literal["add", "pickup", "drop", "delete", "use", "secondary_use", "update"]
|
||||||
message: str
|
message: str
|
||||||
itemId: str | None = None
|
itemId: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ from .models import (
|
|||||||
ItemPianoStatusPacket,
|
ItemPianoStatusPacket,
|
||||||
ItemPickupPacket,
|
ItemPickupPacket,
|
||||||
ItemRemovePacket,
|
ItemRemovePacket,
|
||||||
|
ItemSecondaryUsePacket,
|
||||||
ItemUpdatePacket,
|
ItemUpdatePacket,
|
||||||
ItemUpsertPacket,
|
ItemUpsertPacket,
|
||||||
ItemUsePacket,
|
ItemUsePacket,
|
||||||
@@ -876,7 +877,7 @@ class SignalingServer:
|
|||||||
self,
|
self,
|
||||||
client: ClientConnection,
|
client: ClientConnection,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
action: Literal["add", "pickup", "drop", "delete", "use", "update"],
|
action: Literal["add", "pickup", "drop", "delete", "use", "secondary_use", "update"],
|
||||||
message: str,
|
message: str,
|
||||||
item_id: str | None = None,
|
item_id: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1671,6 +1672,49 @@ class SignalingServer:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if isinstance(packet, ItemSecondaryUsePacket):
|
||||||
|
item = self.items.get(packet.itemId)
|
||||||
|
if not item:
|
||||||
|
await self._send_item_result(client, False, "secondary_use", "Item not found.")
|
||||||
|
return
|
||||||
|
if item.carrierId not in (None, client.id):
|
||||||
|
await self._send_item_result(client, False, "secondary_use", "Item is not available.", item.id)
|
||||||
|
return
|
||||||
|
if item.carrierId is None and (item.x != client.x or item.y != client.y):
|
||||||
|
await self._send_item_result(client, False, "secondary_use", "Item is not on your square.", item.id)
|
||||||
|
return
|
||||||
|
handler = get_item_type_handler(item.type)
|
||||||
|
if handler.secondary_use is None:
|
||||||
|
await self._send_item_result(
|
||||||
|
client,
|
||||||
|
False,
|
||||||
|
"secondary_use",
|
||||||
|
f"No secondary action for {item.title}.",
|
||||||
|
item.id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
secondary_result = handler.secondary_use(item, client.nickname, self._format_clock_display_time)
|
||||||
|
except ValueError as exc:
|
||||||
|
await self._send_item_result(client, False, "secondary_use", str(exc), item.id)
|
||||||
|
return
|
||||||
|
if secondary_result.updated_params is not None:
|
||||||
|
try:
|
||||||
|
item.params = handler.validate_update(item, {**item.params, **secondary_result.updated_params})
|
||||||
|
except ValueError as exc:
|
||||||
|
await self._send_item_result(client, False, "secondary_use", str(exc), item.id)
|
||||||
|
return
|
||||||
|
item.updatedAt = self.item_service.now_ms()
|
||||||
|
item.version += 1
|
||||||
|
self._request_state_save()
|
||||||
|
await self._broadcast_item(item)
|
||||||
|
await self._broadcast(
|
||||||
|
BroadcastChatMessagePacket(type="chat_message", message=secondary_result.others_message, system=True),
|
||||||
|
exclude=client.websocket,
|
||||||
|
)
|
||||||
|
await self._send_item_result(client, True, "secondary_use", secondary_result.self_message, item.id)
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(packet, ItemPianoNotePacket):
|
if isinstance(packet, ItemPianoNotePacket):
|
||||||
item = self.items.get(packet.itemId)
|
item = self.items.get(packet.itemId)
|
||||||
if not item or item.type != "piano":
|
if not item or item.type != "piano":
|
||||||
|
|||||||
@@ -102,6 +102,69 @@ async def test_radio_metadata_refresh_skips_when_no_listener_in_range(monkeypatc
|
|||||||
assert called is False
|
assert called is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_secondary_use_radio_reports_now_playing(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
server = SignalingServer("127.0.0.1", 8765, None, None, grid_size=41)
|
||||||
|
ws = _fake_ws()
|
||||||
|
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=5)
|
||||||
|
server.clients[ws] = client
|
||||||
|
|
||||||
|
radio = server.item_service.default_item(client, "radio_station")
|
||||||
|
radio.x = 5
|
||||||
|
radio.y = 5
|
||||||
|
radio.params["enabled"] = True
|
||||||
|
radio.params["stationName"] = "Station X"
|
||||||
|
radio.params["nowPlaying"] = "Song Y"
|
||||||
|
server.item_service.add_item(radio)
|
||||||
|
|
||||||
|
send_payloads: list[object] = []
|
||||||
|
|
||||||
|
async def fake_send(websocket: ServerConnection, packet: object) -> None:
|
||||||
|
send_payloads.append(packet)
|
||||||
|
|
||||||
|
async def fake_broadcast(packet: object, exclude: ServerConnection | None = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_send", fake_send)
|
||||||
|
monkeypatch.setattr(server, "_broadcast", fake_broadcast)
|
||||||
|
|
||||||
|
await server._handle_message(client, json.dumps({"type": "item_secondary_use", "itemId": radio.id}))
|
||||||
|
|
||||||
|
results = [packet for packet in send_payloads if getattr(packet, "type", "") == "item_action_result"]
|
||||||
|
assert results
|
||||||
|
assert results[-1].ok is True
|
||||||
|
assert results[-1].action == "secondary_use"
|
||||||
|
assert "Playing Song Y from Station X." in results[-1].message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_item_secondary_use_missing_handler_returns_message(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
server = SignalingServer("127.0.0.1", 8765, None, None, grid_size=41)
|
||||||
|
ws = _fake_ws()
|
||||||
|
client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=5)
|
||||||
|
server.clients[ws] = client
|
||||||
|
|
||||||
|
dice = server.item_service.default_item(client, "dice")
|
||||||
|
dice.x = 5
|
||||||
|
dice.y = 5
|
||||||
|
server.item_service.add_item(dice)
|
||||||
|
|
||||||
|
send_payloads: list[object] = []
|
||||||
|
|
||||||
|
async def fake_send(websocket: ServerConnection, packet: object) -> None:
|
||||||
|
send_payloads.append(packet)
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_send", fake_send)
|
||||||
|
|
||||||
|
await server._handle_message(client, json.dumps({"type": "item_secondary_use", "itemId": dice.id}))
|
||||||
|
|
||||||
|
results = [packet for packet in send_payloads if getattr(packet, "type", "") == "item_action_result"]
|
||||||
|
assert results
|
||||||
|
assert results[-1].ok is False
|
||||||
|
assert results[-1].action == "secondary_use"
|
||||||
|
assert "No secondary action" in results[-1].message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_auth_login_uses_hash_offload(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_auth_login_uses_hash_offload(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
server = SignalingServer("127.0.0.1", 8765, None, None)
|
server = SignalingServer("127.0.0.1", 8765, None, None)
|
||||||
|
|||||||
Reference in New Issue
Block a user