2026-02-22 23:42:17 -05:00
|
|
|
"""Piano item schema metadata and behavior."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Callable
|
|
|
|
|
|
|
|
|
|
from ..item_types import ItemUseResult
|
|
|
|
|
from ..models import WorldItem
|
|
|
|
|
|
|
|
|
|
LABEL = "piano"
|
|
|
|
|
TOOLTIP = "Playable keyboard instrument with multiple synth voices."
|
2026-02-23 00:05:01 -05:00
|
|
|
EDITABLE_PROPERTIES: tuple[str, ...] = ("title", "instrument", "attack", "decay", "release", "brightness", "emitRange")
|
2026-02-22 23:42:17 -05:00
|
|
|
CAPABILITIES: tuple[str, ...] = ("editable", "carryable", "deletable", "usable")
|
|
|
|
|
USE_SOUND: str | None = None
|
|
|
|
|
EMIT_SOUND: str | None = None
|
|
|
|
|
USE_COOLDOWN_MS = 1000
|
|
|
|
|
EMIT_RANGE = 15
|
|
|
|
|
DIRECTIONAL = False
|
|
|
|
|
DEFAULT_TITLE = "piano"
|
|
|
|
|
DEFAULT_PARAMS: dict = {
|
|
|
|
|
"instrument": "piano",
|
|
|
|
|
"attack": 15,
|
|
|
|
|
"decay": 45,
|
2026-02-23 00:05:01 -05:00
|
|
|
"release": 35,
|
|
|
|
|
"brightness": 55,
|
2026-02-22 23:42:17 -05:00
|
|
|
"emitRange": 15,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
INSTRUMENT_OPTIONS: tuple[str, ...] = (
|
|
|
|
|
"piano",
|
|
|
|
|
"electric_piano",
|
|
|
|
|
"guitar",
|
|
|
|
|
"organ",
|
|
|
|
|
"bass",
|
|
|
|
|
"violin",
|
|
|
|
|
"synth_lead",
|
2026-02-22 23:51:13 -05:00
|
|
|
"nintendo",
|
2026-02-22 23:42:17 -05:00
|
|
|
"drum_kit",
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-23 00:05:01 -05:00
|
|
|
DEFAULT_ENVELOPE_BY_INSTRUMENT: dict[str, tuple[int, int, int, int]] = {
|
|
|
|
|
"piano": (15, 45, 35, 55),
|
|
|
|
|
"electric_piano": (12, 40, 30, 62),
|
|
|
|
|
"guitar": (8, 35, 25, 50),
|
|
|
|
|
"organ": (25, 70, 45, 48),
|
|
|
|
|
"bass": (10, 35, 28, 38),
|
|
|
|
|
"violin": (22, 75, 55, 58),
|
|
|
|
|
"synth_lead": (6, 30, 22, 72),
|
|
|
|
|
"nintendo": (2, 28, 18, 85),
|
|
|
|
|
"drum_kit": (1, 22, 12, 68),
|
2026-02-22 23:51:13 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 23:42:17 -05:00
|
|
|
PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
|
|
|
|
"title": {"valueType": "text", "tooltip": "Display name spoken and shown for this item.", "maxLength": 80},
|
|
|
|
|
"instrument": {"valueType": "list", "tooltip": "Instrument voice used when playing this piano."},
|
|
|
|
|
"attack": {
|
|
|
|
|
"valueType": "number",
|
|
|
|
|
"tooltip": "How quickly notes ramp in. Lower is sharper; higher is softer.",
|
|
|
|
|
"range": {"min": 0, "max": 100, "step": 1},
|
|
|
|
|
},
|
|
|
|
|
"decay": {
|
|
|
|
|
"valueType": "number",
|
|
|
|
|
"tooltip": "How long notes ring out after the initial hit.",
|
|
|
|
|
"range": {"min": 0, "max": 100, "step": 1},
|
|
|
|
|
},
|
2026-02-23 00:05:01 -05:00
|
|
|
"release": {
|
|
|
|
|
"valueType": "number",
|
|
|
|
|
"tooltip": "How long notes continue after key release.",
|
|
|
|
|
"range": {"min": 0, "max": 100, "step": 1},
|
|
|
|
|
},
|
|
|
|
|
"brightness": {
|
|
|
|
|
"valueType": "number",
|
|
|
|
|
"tooltip": "Tone brightness; higher values sound brighter.",
|
|
|
|
|
"range": {"min": 0, "max": 100, "step": 1},
|
|
|
|
|
},
|
2026-02-22 23:42:17 -05:00
|
|
|
"emitRange": {
|
|
|
|
|
"valueType": "number",
|
|
|
|
|
"tooltip": "Maximum distance in squares where this piano can be heard.",
|
|
|
|
|
"range": {"min": 5, "max": 20, "step": 1},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
|
|
|
|
"""Validate and normalize piano params."""
|
|
|
|
|
|
|
|
|
|
instrument = str(next_params.get("instrument", "piano")).strip().lower()
|
|
|
|
|
if instrument not in INSTRUMENT_OPTIONS:
|
|
|
|
|
raise ValueError(f"instrument must be one of: {', '.join(INSTRUMENT_OPTIONS)}.")
|
2026-02-22 23:51:13 -05:00
|
|
|
previous_instrument = str(_item.params.get("instrument", "piano")).strip().lower()
|
2026-02-22 23:42:17 -05:00
|
|
|
next_params["instrument"] = instrument
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
attack = int(next_params.get("attack", 15))
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError("attack must be an integer between 0 and 100.") from exc
|
|
|
|
|
if not (0 <= attack <= 100):
|
|
|
|
|
raise ValueError("attack must be between 0 and 100.")
|
|
|
|
|
try:
|
|
|
|
|
decay = int(next_params.get("decay", 45))
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError("decay must be an integer between 0 and 100.") from exc
|
|
|
|
|
if not (0 <= decay <= 100):
|
|
|
|
|
raise ValueError("decay must be between 0 and 100.")
|
2026-02-22 23:51:13 -05:00
|
|
|
|
2026-02-23 00:05:01 -05:00
|
|
|
try:
|
|
|
|
|
release = int(next_params.get("release", 35))
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError("release must be an integer between 0 and 100.") from exc
|
|
|
|
|
if not (0 <= release <= 100):
|
|
|
|
|
raise ValueError("release must be between 0 and 100.")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
brightness = int(next_params.get("brightness", 55))
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError("brightness must be an integer between 0 and 100.") from exc
|
|
|
|
|
if not (0 <= brightness <= 100):
|
|
|
|
|
raise ValueError("brightness must be between 0 and 100.")
|
|
|
|
|
|
2026-02-22 23:51:13 -05:00
|
|
|
# When instrument changes, reset envelope to instrument-appropriate defaults.
|
|
|
|
|
if instrument != previous_instrument:
|
2026-02-23 00:05:01 -05:00
|
|
|
attack, decay, release, brightness = DEFAULT_ENVELOPE_BY_INSTRUMENT.get(instrument, (15, 45, 35, 55))
|
2026-02-22 23:51:13 -05:00
|
|
|
next_params["attack"] = attack
|
2026-02-22 23:42:17 -05:00
|
|
|
next_params["decay"] = decay
|
2026-02-23 00:05:01 -05:00
|
|
|
next_params["release"] = release
|
|
|
|
|
next_params["brightness"] = brightness
|
2026-02-22 23:42:17 -05:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
emit_range = int(next_params.get("emitRange", 15))
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
raise ValueError("emitRange must be an integer between 5 and 20.") from exc
|
|
|
|
|
if not (5 <= emit_range <= 20):
|
|
|
|
|
raise ValueError("emitRange must be between 5 and 20.")
|
|
|
|
|
next_params["emitRange"] = emit_range
|
|
|
|
|
|
|
|
|
|
return next_params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def use_item(item: WorldItem, nickname: str, _clock_formatter: Callable[[dict], str]) -> ItemUseResult:
|
|
|
|
|
"""Enter piano play mode for the user who used the item."""
|
|
|
|
|
|
|
|
|
|
return ItemUseResult(
|
|
|
|
|
self_message=f"You begin playing {item.title}.",
|
|
|
|
|
others_message=f"{nickname} begins playing {item.title}.",
|
|
|
|
|
)
|