nitro-v36-with-renderer-v36
/
src /
components /
wired /
views /
extras /
WiredExtraProjectileView.tsx
WiredExtraProjectileView.tsx 26.4 Ko
Télécharger le fichier
import { Collapsible } from '@base-ui/react/collapsible';
import { FC, PropsWithChildren, useEffect, useMemo, useState } from 'react';
import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
import {
clampProjectileParam,
decodeProjectileTokens,
defaultProjectileParams,
isProjectileVariableEnabled,
LocalizeText,
localizeWithFallback,
normalizeProjectileParams,
PROJECTILE_DISTANCE_FIXED,
PROJECTILE_DISTANCE_NORMAL,
PROJECTILE_DISTANCE_OVERSHOOT,
PROJECTILE_INTERNAL_VARIABLES,
PROJECTILE_PARAM_BUNNY_HOP,
PROJECTILE_PARAM_CHANGE_SHOOTER_DIRECTION,
PROJECTILE_PARAM_CURVE_STRENGTH,
PROJECTILE_PARAM_DIRECTIONAL_SYSTEM,
PROJECTILE_PARAM_DISTANCE_BY_HEIGHT,
PROJECTILE_PARAM_DISTANCE_BY_X,
PROJECTILE_PARAM_DISTANCE_BY_Y,
PROJECTILE_PARAM_DISTANCE_FURNI_SOURCE,
PROJECTILE_PARAM_DISTANCE_IS_VARIABLE,
PROJECTILE_PARAM_DISTANCE_MODE,
PROJECTILE_PARAM_DISTANCE_TARGET,
PROJECTILE_PARAM_DISTANCE_TILES,
PROJECTILE_PARAM_DISTANCE_USER_SOURCE,
PROJECTILE_PARAM_INTERNAL_VARIABLES,
PROJECTILE_PARAM_ROTATE,
PROJECTILE_PARAM_ROTATION_OFFSET,
PROJECTILE_PARAM_SCALE_TIME_WITH_DISTANCE,
PROJECTILE_PARAM_SHOOTER_SOURCE,
PROJECTILE_PARAM_SPEED_INCREASE,
PROJECTILE_PARAM_TIME_FURNI_SOURCE,
PROJECTILE_PARAM_TIME_IS_VARIABLE,
PROJECTILE_PARAM_TIME_PER_TILE,
PROJECTILE_PARAM_TIME_TARGET,
PROJECTILE_PARAM_TIME_USER_SOURCE,
toggleProjectileVariable,
WiredFurniType
} from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired, useWiredTools } from '../../../../hooks';
import { WiredFurniSelectorView } from '../WiredFurniSelectorView';
import { sortWiredSourceOptions, USER_SOURCES, useAvailableUserSources, WiredSourceOption } from '../WiredSourcesSelector';
import { WiredVariablePicker } from '../WiredVariablePicker';
import {
buildWiredVariablePickerEntries,
createFallbackVariableEntry,
flattenWiredVariablePickerEntries,
normalizeVariableTokenFromWire,
WiredVariablePickerTarget
} from '../WiredVariablePickerData';
import { readWiredVariableToken, toWiredVariableId, wiredVariableIdList } from '../WiredVariableIds';
import { WiredExtraBaseView } from './WiredExtraBaseView';
import { WiredProjectileDirectionGrid } from './WiredProjectileDirectionGrid';
import { Checkbox as BaseCheckbox } from '../../../../common/Checkbox';
import { Radio } from '../../../../common/Radio';
import { Select } from '../../../../common/Select';
import { NumberInput } from '../../../../common/NumberInput';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
const DIRECTIONAL_SYSTEMS: Array<{ fallback: string; value: number }> = [
{ fallback: '8 directions, straight', value: 0 },
{ fallback: '8 directions, diffuse', value: 1 },
{ fallback: '4 directions, prefer vertical', value: 2 },
{ fallback: '4 directions, prefer horizontal', value: 3 }
];
const DISTANCE_MODES: Array<{ fallback: string; key: string; value: number }> = [
{ fallback: 'Fly to the target', key: 'normal', value: PROJECTILE_DISTANCE_NORMAL },
{ fallback: 'Fly past the target', key: 'overshoot', value: PROJECTILE_DISTANCE_OVERSHOOT },
{ fallback: 'Always fly the same distance', key: 'fixed', value: PROJECTILE_DISTANCE_FIXED }
];
const DISTANCE_AXES: Array<{ fallback: string; key: string; param: number }> = [
{ fallback: 'X', key: 'x', param: PROJECTILE_PARAM_DISTANCE_BY_X },
{ fallback: 'Y', key: 'y', param: PROJECTILE_PARAM_DISTANCE_BY_Y },
{ fallback: 'Height', key: 'height', param: PROJECTILE_PARAM_DISTANCE_BY_HEIGHT }
];
/** Variable targets as the server numbers them. */
const VARIABLE_TARGETS: Array<{ key: WiredVariablePickerTarget; value: number; fallback: string }> = [
{ key: 'furni', value: 1, fallback: 'Furni' },
{ key: 'user', value: 0, fallback: 'User' },
{ key: 'global', value: 3, fallback: 'Global' },
{ key: 'context', value: 2, fallback: 'Context' }
];
const SOURCE_TRIGGER = 0;
const SOURCE_PROJECTILES = 101;
const FURNI_SOURCES: WiredSourceOption[] = sortWiredSourceOptions(
[
{ value: SOURCE_TRIGGER, label: 'wiredfurni.params.sources.furni.0' },
{ value: SOURCE_PROJECTILES, label: 'wiredfurni.params.sources.furni.101' },
{ value: 200, label: 'wiredfurni.params.sources.furni.200' },
{ value: 201, label: 'wiredfurni.params.sources.furni.201' }
],
'furni'
);
type SectionKey = 'info' | 'direction' | 'trajectory' | 'time' | 'rotation' | 'variables';
const INITIALLY_OPEN: Record<SectionKey, boolean> = {
info: true,
direction: true,
trajectory: false,
time: false,
rotation: true,
variables: false
};
const text = (key: string, fallback: string, parameters: string[] = null, replacements: string[] = null) =>
localizeWithFallback(`wiredfurni.params.projectile.${key}`, fallback, parameters, replacements);
const targetKeyOf = (value: number): WiredVariablePickerTarget => VARIABLE_TARGETS.find((target) => target.value === value)?.key ?? 'user';
const ProjectileSection: FC<PropsWithChildren<{ title: string; open: boolean; first?: boolean; onToggle: () => void }>> = (props) => {
const { title, open, first = false, onToggle, children } = props;
return (
<NativeTextTree>
<>
{!first && <div className="octane-wired__divider" />}
<Collapsible.Root className="octane-wired__projectile-section" open={open} onOpenChange={() => onToggle()}>
<Collapsible.Trigger className="octane-wired__projectile-section-header">
<Text bold>{title}</Text>
{open ? <FaChevronUp className="octane-wired__projectile-chevron" /> : <FaChevronDown className="octane-wired__projectile-chevron" />}
</Collapsible.Trigger>
<Collapsible.Panel className="octane-wired__projectile-section-body">{children}</Collapsible.Panel>
</Collapsible.Root>
</>
</NativeTextTree>
);
};
const Checkbox: FC<{ checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }> = (props) => {
const { checked, disabled = false, label, onChange } = props;
return (
<NativeTextTree>
<label className={`flex items-center gap-1 ${disabled ? 'opacity-50' : 'cursor-pointer'}`}>
<BaseCheckbox checked={checked} disabled={disabled} onCheckedChange={onChange} />
<Text small>{label}</Text>
</label>
</NativeTextTree>
);
};
const NumberField: FC<{ disabled?: boolean; label: string; value: number; onChange: (value: number) => void }> = ({ disabled = false, label, value, onChange }) => (
<NativeTextTree>
<label className={`flex items-center justify-between gap-2 ${disabled ? 'opacity-50' : ''}`}>
<Text small>{label}</Text>
<NumberInput className="form-control form-control-sm octane-wired__projectile-number" disabled={disabled} value={value} onChange={(event) => onChange(parseInt(event.target.value, 10))} />
</label>
</NativeTextTree>
);
const SourceSelect: FC<{ label: string; options: WiredSourceOption[]; value: number; onChange: (value: number) => void }> = ({ label, options, value, onChange }) => (
<NativeTextTree>
<label className="flex items-center justify-between gap-2">
<Text small>{label}</Text>
<Select aria-label={label} className="form-select form-select-sm" value={value} onChange={(event) => onChange(parseInt(event.target.value, 10))}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{LocalizeText(option.label)}
</option>
))}
</Select>
</label>
</NativeTextTree>
);
interface ValueOrVariableProps {
name: string;
label: string;
value: number;
fromVariable: boolean;
target: number;
token: string;
userSource: number;
furniSource: number;
userSources: WiredSourceOption[];
onValue: (value: number) => void;
onFromVariable: (fromVariable: boolean) => void;
onTarget: (target: number) => void;
onToken: (token: string) => void;
onUserSource: (source: number) => void;
onFurniSource: (source: number) => void;
}
/** Habbo's "set value / from variable" pair, as the time per tile and the distance use it. */
const ValueOrVariable: FC<ValueOrVariableProps> = (props) => {
const { name, label, value, fromVariable, target, token, userSource, furniSource, userSources } = props;
const { userVariableDefinitions = [], furniVariableDefinitions = [], roomVariableDefinitions = [], contextVariableDefinitions = [] } = useWiredTools();
const targetKey = targetKeyOf(target);
const entries = useMemo(() => {
const definitions =
targetKey === 'furni'
? furniVariableDefinitions
: targetKey === 'global'
? roomVariableDefinitions
: targetKey === 'context'
? contextVariableDefinitions
: userVariableDefinitions;
const built = buildWiredVariablePickerEntries(targetKey, 'change-reference', definitions);
if (!token || flattenWiredVariablePickerEntries(built).some((entry) => entry.token === token)) return built;
const fallback = createFallbackVariableEntry(targetKey, token);
return fallback ? [fallback, ...built] : built;
}, [contextVariableDefinitions, furniVariableDefinitions, roomVariableDefinitions, targetKey, token, userVariableDefinitions]);
return (
<NativeTextTree>
<div className="flex flex-col gap-1">
<label className="flex items-center gap-1 cursor-pointer">
<Radio checked={!fromVariable} name={name} onCheckedChange={() => props.onFromVariable(false)} />
<Text small>{label}</Text>
<NumberInput aria-label={label} className="form-control form-control-sm octane-wired__projectile-number ms-auto" disabled={fromVariable} value={value} onChange={(event) => props.onValue(parseInt(event.target.value, 10))} />
</label>
<label className="flex items-center gap-1 cursor-pointer">
<Radio checked={fromVariable} name={name} onCheckedChange={() => props.onFromVariable(true)} />
<Text small>{LocalizeText('wiredfurni.params.variables.reference_value.from_variable')}</Text>
</label>
{fromVariable && (
<div className="flex flex-col gap-1 octane-wired__projectile-indent">
<Select
aria-label={`${label} variable type`}
className="form-select form-select-sm"
value={target}
onChange={(event) => {
props.onTarget(parseInt(event.target.value, 10));
props.onToken('');
}}
>
{VARIABLE_TARGETS.map((option) => (
<option key={option.key} value={option.value}>
{text(`variable_target.${option.key}`, option.fallback)}
</option>
))}
</Select>
<WiredVariablePicker entries={entries} recentScope="projectile-reference" selectedToken={token} onSelect={(entry) => props.onToken(entry.token)} />
{targetKey === 'user' && (
<SourceSelect label={text('variable_source', 'Read it from:')} options={userSources} value={userSource} onChange={props.onUserSource} />
)}
{targetKey === 'furni' && (
<SourceSelect label={text('variable_source', 'Read it from:')} options={FURNI_SOURCES} value={furniSource} onChange={props.onFurniSource} />
)}
</div>
)}
</div>
</NativeTextTree>
);
};
export const WiredExtraProjectileView: FC<{}> = () => {
const { trigger = null, setIntParams = null, setStringParam = null, setVariableIds = null } = useWired();
const [params, setParams] = useState<number[]>(defaultProjectileParams);
const [timeToken, setTimeToken] = useState('');
const [distanceToken, setDistanceToken] = useState('');
const [openSections, setOpenSections] = useState<Record<SectionKey, boolean>>(INITIALLY_OPEN);
const userSources = sortWiredSourceOptions(useAvailableUserSources(trigger, USER_SOURCES), 'users');
useEffect(() => {
if (!trigger) return;
const [time, distance] = decodeProjectileTokens(trigger.stringData);
setParams(normalizeProjectileParams(trigger.intData));
// AIR: variable ids [time variable, distance variable]; an older server wrote both tokens as the string.
setTimeToken(readWiredVariableToken(trigger.variableIds, 0, normalizeVariableTokenFromWire(time)));
setDistanceToken(readWiredVariableToken(trigger.variableIds, 1, normalizeVariableTokenFromWire(distance)));
}, [trigger]);
const setParam = (index: number, value: number) =>
setParams((current) => current.map((entry, position) => (position === index ? clampProjectileParam(index, value) : entry)));
const setFlag = (index: number, checked: boolean) => setParam(index, checked ? 1 : 0);
const toggleSection = (key: SectionKey) => setOpenSections((current) => ({ ...current, [key]: !current[key] }));
const rotates = params[PROJECTILE_PARAM_ROTATE] === 1;
const changesShooter = params[PROJECTILE_PARAM_CHANGE_SHOOTER_DIRECTION] === 1;
const scalesTime = params[PROJECTILE_PARAM_SCALE_TIME_WITH_DISTANCE] === 1;
const distanceMode = params[PROJECTILE_PARAM_DISTANCE_MODE];
const rotationOffset = params[PROJECTILE_PARAM_ROTATION_OFFSET];
const variablesMask = params[PROJECTILE_PARAM_INTERNAL_VARIABLES];
const timeFromVariable = params[PROJECTILE_PARAM_TIME_IS_VARIABLE] === 1;
const distanceFromVariable = params[PROJECTILE_PARAM_DISTANCE_IS_VARIABLE] === 1;
const save = () => {
setIntParams(normalizeProjectileParams(params));
setStringParam('');
setVariableIds(
wiredVariableIdList(
timeFromVariable ? toWiredVariableId(targetKeyOf(params[PROJECTILE_PARAM_TIME_TARGET]), timeToken) : '',
distanceFromVariable ? toWiredVariableId(targetKeyOf(params[PROJECTILE_PARAM_DISTANCE_TARGET]), distanceToken) : ''
)
);
};
return (
<NativeTextTree>
<WiredExtraBaseView
footer={<WiredFurniSelectorView />}
hasSpecialInput={true}
requiresFurni={WiredFurniType.STUFF_SELECTION_OPTION_BY_ID}
save={save}
showSelection={false}
validate={() => !(scalesTime && timeFromVariable && !timeToken) && !(distanceMode !== PROJECTILE_DISTANCE_NORMAL && distanceFromVariable && !distanceToken)}
>
<div className="octane-wired__projectile">
<ProjectileSection first open={openSections.info} title={text('info', 'Usage info:')} onToggle={() => toggleSection('info')}>
<Text small>
{text(
'info.desc',
'This box is meant for projectile furni. Use it to add extra features to a projectile that is already being moved by Wired.'
)}
</Text>
</ProjectileSection>
<ProjectileSection open={openSections.direction} title={text('direction', 'Projectile direction:')} onToggle={() => toggleSection('direction')}>
<Checkbox
checked={rotates}
label={text('rotate', 'Rotate the projectile in the moving direction')}
onChange={(checked) => setFlag(PROJECTILE_PARAM_ROTATE, checked)}
/>
<div className="flex flex-col gap-1">
<Text small>{text('system', 'System:')}</Text>
<Select
aria-label={text('system', 'System:')}
className="form-select form-select-sm"
disabled={!rotates && !changesShooter}
value={params[PROJECTILE_PARAM_DIRECTIONAL_SYSTEM]}
onChange={(event) => setParam(PROJECTILE_PARAM_DIRECTIONAL_SYSTEM, parseInt(event.target.value, 10))}
>
{DIRECTIONAL_SYSTEMS.map((system) => (
<option key={system.value} value={system.value}>
{text(`system.${system.value}`, system.fallback)}
</option>
))}
</Select>
</div>
<WiredProjectileDirectionGrid disabled={!rotates && !changesShooter} system={params[PROJECTILE_PARAM_DIRECTIONAL_SYSTEM]} />
<Checkbox
checked={changesShooter}
label={text('shooter', "Visually change the shooter's direction")}
onChange={(checked) => setFlag(PROJECTILE_PARAM_CHANGE_SHOOTER_DIRECTION, checked)}
/>
<div className="flex flex-col gap-1 octane-wired__projectile-indent">
<Checkbox
checked={params[PROJECTILE_PARAM_BUNNY_HOP] === 1}
disabled={!changesShooter}
label={text('bunny_hop', 'Do a little hop when shooting sideways')}
onChange={(checked) => setFlag(PROJECTILE_PARAM_BUNNY_HOP, checked)}
/>
{changesShooter && (
<SourceSelect
label={text('shooter.source', 'Shooter:')}
options={userSources}
value={params[PROJECTILE_PARAM_SHOOTER_SOURCE]}
onChange={(value) => setParam(PROJECTILE_PARAM_SHOOTER_SOURCE, value)}
/>
)}
</div>
</ProjectileSection>
<ProjectileSection open={openSections.trajectory} title={text('trajectory', 'Animation trajectory:')} onToggle={() => toggleSection('trajectory')}>
<div className="flex flex-col gap-1">
{DISTANCE_MODES.map((mode) => (
<label key={mode.value} className="flex items-center gap-1 cursor-pointer">
<Radio checked={distanceMode === mode.value} name="projectileDistanceMode" onCheckedChange={() => setParam(PROJECTILE_PARAM_DISTANCE_MODE, mode.value)} />
<Text small>{text(`distance.${mode.key}`, mode.fallback)}</Text>
</label>
))}
</div>
{distanceMode !== PROJECTILE_DISTANCE_NORMAL && (
<div className="octane-wired__projectile-indent">
<ValueOrVariable
fromVariable={distanceFromVariable}
furniSource={params[PROJECTILE_PARAM_DISTANCE_FURNI_SOURCE]}
label={text('distance.tiles', 'Tiles:')}
name="projectileDistanceValue"
target={params[PROJECTILE_PARAM_DISTANCE_TARGET]}
token={distanceToken}
userSource={params[PROJECTILE_PARAM_DISTANCE_USER_SOURCE]}
userSources={userSources}
value={params[PROJECTILE_PARAM_DISTANCE_TILES]}
onFromVariable={(fromVariable) => setFlag(PROJECTILE_PARAM_DISTANCE_IS_VARIABLE, fromVariable)}
onFurniSource={(source) => setParam(PROJECTILE_PARAM_DISTANCE_FURNI_SOURCE, source)}
onTarget={(target) => setParam(PROJECTILE_PARAM_DISTANCE_TARGET, target)}
onToken={setDistanceToken}
onUserSource={(source) => setParam(PROJECTILE_PARAM_DISTANCE_USER_SOURCE, source)}
onValue={(value) => setParam(PROJECTILE_PARAM_DISTANCE_TILES, value)}
/>
</div>
)}
<div className="flex flex-col gap-1">
<Text small>
{text('curve', 'Curve strength: %value%', ['value'], [String(params[PROJECTILE_PARAM_CURVE_STRENGTH])]).replace(
'%value%',
String(params[PROJECTILE_PARAM_CURVE_STRENGTH])
)}
</Text>
<Slider
max={1000}
min={-1000}
step={10}
value={params[PROJECTILE_PARAM_CURVE_STRENGTH]}
onChange={(value) => setParam(PROJECTILE_PARAM_CURVE_STRENGTH, value as number)}
/>
</div>
</ProjectileSection>
<ProjectileSection open={openSections.time} title={text('time', 'Animation time:')} onToggle={() => toggleSection('time')}>
<Checkbox
checked={scalesTime}
label={text('time.scale', 'Scale the animation time with the distance')}
onChange={(checked) => setFlag(PROJECTILE_PARAM_SCALE_TIME_WITH_DISTANCE, checked)}
/>
{scalesTime && (
<>
<ValueOrVariable
fromVariable={timeFromVariable}
furniSource={params[PROJECTILE_PARAM_TIME_FURNI_SOURCE]}
label={text('time.per_tile', 'Time per tile (ms):')}
name="projectileTimeValue"
target={params[PROJECTILE_PARAM_TIME_TARGET]}
token={timeToken}
userSource={params[PROJECTILE_PARAM_TIME_USER_SOURCE]}
userSources={userSources}
value={params[PROJECTILE_PARAM_TIME_PER_TILE]}
onFromVariable={(fromVariable) => setFlag(PROJECTILE_PARAM_TIME_IS_VARIABLE, fromVariable)}
onFurniSource={(source) => setParam(PROJECTILE_PARAM_TIME_FURNI_SOURCE, source)}
onTarget={(target) => setParam(PROJECTILE_PARAM_TIME_TARGET, target)}
onToken={setTimeToken}
onUserSource={(source) => setParam(PROJECTILE_PARAM_TIME_USER_SOURCE, source)}
onValue={(value) => setParam(PROJECTILE_PARAM_TIME_PER_TILE, value)}
/>
<Text small>{text('time.distance_by', 'Measure the distance by:')}</Text>
<div className="flex items-center gap-3 octane-wired__projectile-indent">
{DISTANCE_AXES.map((axis) => (
<Checkbox
key={axis.key}
checked={params[axis.param] === 1}
label={text(`time.distance_by.${axis.key}`, axis.fallback)}
onChange={(checked) => setFlag(axis.param, checked)}
/>
))}
</div>
<NumberField
label={text('time.speed_increase', 'Speed increase (ms):')}
value={params[PROJECTILE_PARAM_SPEED_INCREASE]}
onChange={(value) => setParam(PROJECTILE_PARAM_SPEED_INCREASE, value)}
/>
</>
)}
</ProjectileSection>
<ProjectileSection
open={openSections.rotation}
title={text('rotation_offset', 'Shift the rotation by %value%', ['value'], [String(rotationOffset)]).replace('%value%', String(rotationOffset))}
onToggle={() => toggleSection('rotation')}
>
<Slider max={7} min={0} step={1} value={rotationOffset} onChange={(value) => setParam(PROJECTILE_PARAM_ROTATION_OFFSET, value as number)} />
</ProjectileSection>
<ProjectileSection open={openSections.variables} title={text('variables', 'Projectile internal variables:')} onToggle={() => toggleSection('variables')}>
{PROJECTILE_INTERNAL_VARIABLES.map((variable, bit) => (
<Checkbox
key={variable}
checked={isProjectileVariableEnabled(variablesMask, bit)}
label={variable}
onChange={(checked) => setParam(PROJECTILE_PARAM_INTERNAL_VARIABLES, toggleProjectileVariable(variablesMask, bit, checked))}
/>
))}
</ProjectileSection>
</div>
</WiredExtraBaseView>
</NativeTextTree>
);
};
