nitro-v36-with-renderer-v36
/
src /
components /
wired /
views /
extras /
WiredCustomContractOfficialView.tsx
WiredCustomContractOfficialView.tsx 15.9 Ko
Télécharger le fichier
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { LocalizeText, localizeWithFallback, WiredFurniType, WiredSelectionVisualizer } from '../../../../api';
import {
CUSTOM_CONTRACT_COINS,
CUSTOM_CONTRACT_CONTEXT_TARGET,
CUSTOM_CONTRACT_FURNI,
CUSTOM_CONTRACT_GLOBAL_TARGET,
CUSTOM_CONTRACT_MAX_AMOUNT,
CUSTOM_CONTRACT_MIN_AMOUNT,
CustomContractElement,
emptyCustomContractElement,
readCustomContractElement,
writeCustomContractElement
} from '../../../../api/wired/WiredCustomContract';
import { Text } from '../../../../common';
import { Checkbox } from '../../../../common/Checkbox';
import { Radio } from '../../../../common/Radio';
import { useWired } from '../../../../hooks';
import { WiredFurniSelectionSourceRow } from '../WiredFurniSelectionSourceRow';
import { FURNI_SOURCES, sortWiredSourceOptions, WiredSourceOption } from '../WiredSourcesSelector';
import { getCustomVariableItemId } from '../WiredVariablePickerData';
import { variableTokenFromId, WIRED_FIXED_HOLDER_SOURCE, WiredValueOrVariableField } from '../WiredValueOrVariableField';
import { toWiredVariableIdOnTarget, wiredVariableIdList } from '../WiredVariableIds';
import { WiredExtraBaseView } from './WiredExtraBaseView';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
const SOURCE_SELECTED = 100;
const SOURCE_SECONDARY_SELECTED = 101;
const ELEMENT_TYPES = [CUSTOM_CONTRACT_COINS, CUSTOM_CONTRACT_FURNI];
const ELEMENT_TYPE_LABELS = ['Coins', 'Furni'];
/** The reward furni take the second pick list (source 101), as AIR's dual furni picking does. */
const REWARD_SOURCES: WiredSourceOption[] = sortWiredSourceOptions(
[
{ value: 0, label: 'wiredfurni.params.sources.furni.0' },
{ value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' },
{ value: 200, label: 'wiredfurni.params.sources.furni.200' },
{ value: 201, label: 'wiredfurni.params.sources.furni.201' }
],
'furni'
);
type Half = 'payment' | 'reward';
interface ElementSectionProps {
half: Half;
element: CustomContractElement;
onChange: (element: CustomContractElement) => void;
onValidityChange: (valid: boolean) => void;
}
/** AIR: a collapsed section with the enable checkbox revealing the element type and the value-or-variable amount. */
const ElementSection: FC<ElementSectionProps> = ({ half, element, onChange, onValidityChange }) => {
const title = LocalizeText(`wiredfurni.params.custom_contract.${half}`);
const enableLabel = LocalizeText(`wiredfurni.params.custom_contract.enable_${half}`);
const amountLabel = localizeWithFallback('wiredfurni.params.custom_contract.amount_selection', 'Select amount:');
return (
<NativeTextTree>
<details>
<summary>
<Text bold>{title}</Text>
</summary>
<div className="flex flex-col gap-1 pt-1">
<label className="flex items-center gap-1">
<Checkbox
aria-label={enableLabel}
checked={element.enabled}
onCheckedChange={(nextChecked) => onChange({ ...element, enabled: nextChecked })}
/>
<Text>{enableLabel}</Text>
</label>
<fieldset className="flex flex-col gap-1 ps-3" disabled={!element.enabled}>
<Text bold>{localizeWithFallback('wiredfurni.params.custom_contract.element_type_selection', 'Element type:')}</Text>
<div className="flex items-center gap-3">
{ELEMENT_TYPES.map((type) => (
<label key={type} className="flex items-center gap-1">
<Radio
name={`customContract-${half}-type`}
checked={element.type === type}
onCheckedChange={() => onChange({ ...element, type })}
/>
<Text>{localizeWithFallback(`wiredfurni.params.custom_contract.element_type_selection.${type}`, ELEMENT_TYPE_LABELS[type])}</Text>
</label>
))}
</div>
<Text bold>{amountLabel}</Text>
<WiredValueOrVariableField
disabled={!element.enabled}
holderSource={WIRED_FIXED_HOLDER_SOURCE}
isVariable={element.fromVariable}
label={`${title}: ${amountLabel}`}
literal={element.literal}
max={CUSTOM_CONTRACT_MAX_AMOUNT}
min={CUSTOM_CONTRACT_MIN_AMOUNT}
target={element.target}
targets={[CUSTOM_CONTRACT_GLOBAL_TARGET, CUSTOM_CONTRACT_CONTEXT_TARGET]}
token={element.token}
onIsVariableChange={(fromVariable) => onChange({ ...element, fromVariable })}
onLiteralChange={(literal) => onChange({ ...element, literal })}
onTargetChange={(target) => onChange({ ...element, target })}
onTokenChange={(token) => onChange({ ...element, token })}
onValidityChange={onValidityChange}
/>
</fieldset>
</div>
</details>
</NativeTextTree>
);
};
/**
* The official Custom Contract (addon 20, AIR `chests/class_4234`): the usage warning, then a payment and a reward
* section, each switched on by its checkbox and holding the element type (coins or furni) and the amount (1..100000,
* a value or a room / context variable). Ints `[payment on, type, from a variable, amount, target, reward on, type,
* from a variable, amount, target]`, variable ids `[payment, reward]`; furni slot 0 the payment furni (first picks),
* slot 1 the reward furni (second picks), each usable only while its element is on and furni.
*/
export const WiredCustomContractOfficialView: FC<{}> = () => {
const { trigger = null, furniIds = [], setFurniIds, setFurniIds2, setIntParams, setStringParam, setVariableIds, setSourceTypes, setAllowsFurni } = useWired();
const [payment, setPayment] = useState<CustomContractElement>(emptyCustomContractElement);
const [reward, setReward] = useState<CustomContractElement>(emptyCustomContractElement);
const [paymentValid, setPaymentValid] = useState(true);
const [rewardValid, setRewardValid] = useState(true);
const [paymentSource, setPaymentSource] = useState(SOURCE_SELECTED);
const [rewardSource, setRewardSource] = useState(SOURCE_SECONDARY_SELECTED);
const [paymentIds, setPaymentIds] = useState<number[]>([]);
const [rewardIds, setRewardIds] = useState<number[]>([]);
const [selectionMode, setSelectionMode] = useState<Half>('payment');
const highlightedIds = useRef<number[]>([]);
const paymentPicksFurni = payment.enabled && payment.type === CUSTOM_CONTRACT_FURNI;
const rewardPicksFurni = reward.enabled && reward.type === CUSTOM_CONTRACT_FURNI;
const syncHighlights = useCallback((nextPaymentIds: number[], nextRewardIds: number[]) => {
if (highlightedIds.current.length) {
WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
}
const rewardSet = new Set(nextRewardIds);
const paymentOnlyIds = nextPaymentIds.filter((id) => !rewardSet.has(id));
if (paymentOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(paymentOnlyIds);
if (nextRewardIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextRewardIds);
highlightedIds.current = Array.from(new Set([...nextPaymentIds, ...nextRewardIds]));
}, []);
const switchSelection = useCallback(
(next: Half) => {
if (next === 'payment' && (paymentSource !== SOURCE_SELECTED || !paymentPicksFurni)) return;
if (next === 'reward' && (rewardSource !== SOURCE_SECONDARY_SELECTED || !rewardPicksFurni)) return;
const nextPaymentIds = selectionMode === 'payment' ? [...furniIds] : [...paymentIds];
const nextRewardIds = selectionMode === 'reward' ? [...furniIds] : [...rewardIds];
setPaymentIds(nextPaymentIds);
setRewardIds(nextRewardIds);
setSelectionMode(next);
setFurniIds([...(next === 'payment' ? nextPaymentIds : nextRewardIds)]);
},
[selectionMode, furniIds, paymentSource, rewardSource, paymentPicksFurni, rewardPicksFurni, paymentIds, rewardIds, setFurniIds]
);
useEffect(() => {
if (!trigger) return;
const data = trigger.intData ?? [];
const slots = trigger.furniSourceTypes ?? [];
const nextPaymentIds = [...(trigger.selectedItems ?? [])];
const nextRewardIds = [...(trigger.selectedItems2 ?? [])];
setPayment(readCustomContractElement(data, 0, variableTokenFromId(trigger.variableIds?.[0] ?? '')));
setReward(readCustomContractElement(data, 5, variableTokenFromId(trigger.variableIds?.[1] ?? '')));
setPaymentSource(slots.length >= 1 ? slots[0] : SOURCE_SELECTED);
setRewardSource(slots.length >= 2 ? (slots[1] === SOURCE_SELECTED ? SOURCE_SECONDARY_SELECTED : slots[1]) : SOURCE_SECONDARY_SELECTED);
setPaymentIds(nextPaymentIds);
setRewardIds(nextRewardIds);
setSelectionMode('payment');
setFurniIds([...nextPaymentIds]);
}, [trigger, setFurniIds]);
useEffect(() => {
if (selectionMode === 'payment') setPaymentIds(furniIds);
else setRewardIds(furniIds);
}, [furniIds, selectionMode]);
useEffect(() => {
syncHighlights(paymentIds, rewardIds);
}, [paymentIds, rewardIds, syncHighlights]);
useEffect(() => {
const canEditPayment = paymentSource === SOURCE_SELECTED && paymentPicksFurni;
const canEditReward = rewardSource === SOURCE_SECONDARY_SELECTED && rewardPicksFurni;
if (selectionMode === 'payment' && !canEditPayment && canEditReward) {
switchSelection('reward');
return;
}
if (selectionMode === 'reward' && !canEditReward && canEditPayment) {
switchSelection('payment');
return;
}
const canEditCurrent = selectionMode === 'payment' ? canEditPayment : canEditReward;
setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE);
}, [selectionMode, paymentSource, rewardSource, paymentPicksFurni, rewardPicksFurni, switchSelection, setAllowsFurni]);
useEffect(() => {
return () => {
if (!highlightedIds.current.length) return;
WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
highlightedIds.current = [];
};
}, []);
const save = () => {
const nextPaymentIds = selectionMode === 'payment' ? [...furniIds] : [...paymentIds];
const nextRewardIds = selectionMode === 'reward' ? [...furniIds] : [...rewardIds];
const paymentVariable = payment.enabled && payment.fromVariable ? getCustomVariableItemId(payment.token) : 0;
const rewardVariable = reward.enabled && reward.fromVariable ? getCustomVariableItemId(reward.token) : 0;
setPaymentIds(nextPaymentIds);
setRewardIds(nextRewardIds);
if (selectionMode === 'reward') {
setSelectionMode('payment');
setFurniIds([...nextPaymentIds]);
}
setIntParams([...writeCustomContractElement(payment, paymentVariable), ...writeCustomContractElement(reward, rewardVariable)]);
setStringParam('');
// The variable list's ids (room:12 / ctx:12), as AIR sends them.
setVariableIds(wiredVariableIdList(
paymentVariable > 0 ? toWiredVariableIdOnTarget(payment.target, payment.token) : '',
rewardVariable > 0 ? toWiredVariableIdOnTarget(reward.target, reward.token) : ''
));
setSourceTypes({ furni: [paymentSource, rewardSource], users: [] });
setFurniIds2(rewardSource === SOURCE_SECONDARY_SELECTED ? nextRewardIds.filter((id) => id > 0) : []);
};
const elementValid = (element: CustomContractElement, valid: boolean) =>
!element.enabled || (valid && (!element.fromVariable || getCustomVariableItemId(element.token) > 0));
const validate = () => elementValid(payment, paymentValid) && elementValid(reward, rewardValid);
const selectionLimit = trigger?.maximumItemSelectionCount ?? 0;
// AIR forceHidePickFurniInstructions, widthModifier 1.2: the two source rows are the whole selection.
return (
<NativeTextTree>
<WiredExtraBaseView
hasSpecialInput={true}
requiresFurni={WiredFurniType.STUFF_SELECTION_OPTION_BY_ID}
save={save}
validate={validate}
widthModifier={1.2}
allowScrolling
showSelection={false}
footerCollapsible={false}
footer={
<div className="flex flex-col gap-2">
<fieldset disabled={!paymentPicksFurni} className={paymentPicksFurni ? undefined : 'opacity-50'}>
<WiredFurniSelectionSourceRow
title="wiredfurni.params.sources.furni.title.payment"
options={FURNI_SOURCES}
value={paymentSource}
selectionKind="primary"
selectionActive={selectionMode === 'payment'}
selectionCount={paymentIds.length}
selectionLimit={selectionLimit}
selectionEnabledValues={[SOURCE_SELECTED]}
onChange={setPaymentSource}
onSelectionActivate={() => switchSelection('payment')}
/>
</fieldset>
<fieldset disabled={!rewardPicksFurni} className={rewardPicksFurni ? undefined : 'opacity-50'}>
<WiredFurniSelectionSourceRow
title="wiredfurni.params.sources.furni.title.reward"
options={REWARD_SOURCES}
value={rewardSource}
selectionKind="secondary"
selectionActive={selectionMode === 'reward'}
selectionCount={rewardIds.length}
selectionLimit={selectionLimit}
selectionEnabledValues={[SOURCE_SECONDARY_SELECTED]}
onChange={(value) => setRewardSource(value === SOURCE_SELECTED ? SOURCE_SECONDARY_SELECTED : value)}
onSelectionActivate={() => switchSelection('reward')}
/>
</fieldset>
</div>
}
>
<div className="flex flex-col gap-2">
<Text small className="whitespace-pre-line">{LocalizeText('wiredfurni.params.custom_contract.usage_warning').replace(/\\n/g, '\n')}</Text>
<ElementSection half="payment" element={payment} onChange={setPayment} onValidityChange={setPaymentValid} />
<ElementSection half="reward" element={reward} onChange={setReward} onValidityChange={setRewardValid} />
</div>
</WiredExtraBaseView>
</NativeTextTree>
);
};
