5180 fichiers 38.5 Mo 0 téléchargements 378 vues mis à jour il y a 7 heures ui
nitro-v36-with-renderer-v36 / src / components / wired / views / conditions / WiredConditionHasAltitudeView.tsx
WiredConditionHasAltitudeView.tsx 10.2 Ko Télécharger le fichier
import { FC, useEffect, useState } from 'react';
import { LocalizeText, localizeWithFallback, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
import { WiredConditionQuantifierSection } from './WiredConditionQuantifierSection';
import { Radio } from '../../../../common/Radio';
import { BaseButton } from '../../../../common/BaseButton';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';


const MIN_ALTITUDE = 0;
const MAX_ALTITUDE = 80;
const ALTITUDE_STEP = 0.01;
const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/;

const clampAltitude = (value: number) => {
    if (isNaN(value)) return MIN_ALTITUDE;

    const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value));

    return parseFloat(clamped.toFixed(2));
};

const formatAltitude = (value: number) => {
    const normalized = clampAltitude(value);
    const text = normalized.toFixed(2);

    return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
};

const parseAltitude = (value: string) => {
    if (!value || !value.trim().length) return 0;

    const parsed = parseFloat(value);

    if (isNaN(parsed)) return 0;

    return clampAltitude(parsed);
};

/**
 * Seven conditions borrowed this dialog and inherited things that belong to the altitude check alone.
 * The worst was the counter-only furni gate: it refuses every furni that is not a game counter, which
 * makes "furni in range", "owns furni" and "same height" impossible to configure. They also inherited
 * the altitude label for what is really a radius, a furni source picker where a user source is meant,
 * and a comparison operator four of them never read.
 *
 * The four range boxes read it now. Their middle option is inclusive — "within the radius", the
 * behaviour they always had — so it gets its own label instead of the altitude box's "Equals".
 */
type AltitudeVariant = 'altitude' | 'userRange' | 'furniRange' | 'furniProperty';

const VARIANTS: Record<
    AltitudeVariant,
    { counterOnly: boolean; comparison: boolean; value: 'altitude' | 'radius' | null; users: boolean }
> = {
    // Any furni has an altitude, as in the official editor.
    altitude: { counterOnly: false, comparison: true, value: 'altitude', users: false },
    userRange: { counterOnly: false, comparison: true, value: 'radius', users: true },
    furniRange: { counterOnly: false, comparison: true, value: 'radius', users: false },
    furniProperty: { counterOnly: false, comparison: false, value: null, users: false }
};

const RANGE_COMPARISON_LABELS: Record<number, { key: string; fallback: string }> = {
    0: { key: 'wiredfurni.params.range.comparison.0', fallback: 'Closer than' },
    1: { key: 'wiredfurni.params.range.comparison.1', fallback: 'Within the radius' },
    2: { key: 'wiredfurni.params.range.comparison.2', fallback: 'Further than' }
};

const VALUE_LABELS = {
    altitude: { key: 'wiredfurni.params.setaltitude', fallback: 'Altitude' },
    radius: { key: 'wiredfurni.params.setradius', fallback: 'Radius in tiles' }
};

interface WiredConditionHasAltitudeViewProps {
    variant?: AltitudeVariant;
}

export const WiredConditionHasAltitudeView: FC<WiredConditionHasAltitudeViewProps> = ({ variant = 'altitude' }) => {
    const spec = VARIANTS[variant];

    const { trigger = null, setIntParams = null, setStringParam = null, setSourceTypes = null } = useWired();
    // The official has-altitude box: [altitude in hundredths, comparison], the furni in the official source slot.
    // Our range / property variants keep their own layout. The quantifier goes in its slot for every variant.
    const official = variant === 'altitude';
    const [comparison, setComparison] = useState(1);
    const [furniSource, setFurniSource] = useState<number>(() => {
        if (trigger?.intData?.length > 1) return trigger.intData[1];
        return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
    });
    const [showAdvanced, setShowAdvanced] = useState(false);
    const [altitude, setAltitude] = useState(0);
    const [altitudeInput, setAltitudeInput] = useState('0');

    useEffect(() => {
        if (!trigger) return;

        const quantified = (trigger.quantifierCode ?? 0) !== 0;

        if (official) {
            const nextAltitude = clampAltitude((trigger.intData[0] ?? 0) / 100);
            const nextSource = trigger.furniSourceTypes?.[0] ?? ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0);

            setComparison(trigger.intData.length > 1 ? trigger.intData[1] : 1);
            setFurniSource(nextSource);
            setShowAdvanced(nextSource !== 0 || quantified);
            setAltitude(nextAltitude);
            setAltitudeInput(formatAltitude(nextAltitude));
            return;
        }

        const sourceSlot = spec.comparison ? 1 : 0;
        const fallbackSource = (trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0;

        setComparison(spec.comparison && trigger.intData.length > 0 ? trigger.intData[0] : 1);
        setFurniSource(trigger.intData.length > sourceSlot ? trigger.intData[sourceSlot] : fallbackSource);
        setShowAdvanced((trigger.intData.length > sourceSlot && trigger.intData[sourceSlot] !== 0) || quantified);

        const nextAltitude = parseAltitude(trigger.stringData);
        setAltitude(nextAltitude);
        setAltitudeInput(formatAltitude(nextAltitude));
    }, [official, spec.comparison, trigger]);

    const updateAltitude = (value: number) => {
        const nextValue = clampAltitude(value);

        setAltitude(nextValue);
        setAltitudeInput(formatAltitude(nextValue));
    };

    const updateAltitudeInput = (value: string) => {
        if (!ALTITUDE_PATTERN.test(value)) return;

        setAltitudeInput(value);

        if (!value.length) {
            setAltitude(0);
            return;
        }

        const parsedValue = parseFloat(value);

        if (isNaN(parsedValue)) return;

        if (parsedValue > MAX_ALTITUDE) {
            updateAltitude(MAX_ALTITUDE);
            return;
        }

        setAltitude(clampAltitude(parsedValue));
    };

    const save = () => {
        if (official) {
            setIntParams([Math.round(altitude * 100), comparison]);
            setStringParam('');
            setSourceTypes({ furni: [furniSource], users: [] });
            return;
        }

        // furniProperty reads [source]; the others keep the comparison in slot 0.
        setIntParams(spec.comparison ? [comparison, furniSource] : [furniSource]);
        setStringParam(spec.value ? formatAltitude(altitude) : '');
    };

    return (
        <NativeTextTree>
            <WiredConditionBaseView
                hasSpecialInput={true}
                requiresFurni={WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT}
                save={save}
                footerCollapsible={false}
                showQuantifier={false}
                footer={
                    <div className="flex flex-col gap-2">
                        <BaseButton className="octane-wired__advanced-toggle" type="button" onClick={() => setShowAdvanced((value) => !value)}>
                            {LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand')}
                        </BaseButton>
                        {showAdvanced && (
                            <>
                                <WiredConditionQuantifierSection />
                                {spec.users ? (
                                    <WiredSourcesSelector showUsers={true} userSource={furniSource} onChangeUsers={setFurniSource} />
                                ) : (
                                    <WiredSourcesSelector showFurni={true} furniSource={furniSource} onChangeFurni={setFurniSource} />
                                )}
                            </>
                        )}
                    </div>
                }
            >
                {spec.comparison && (
                    <div className="flex flex-col gap-1">
                        <Text bold>{LocalizeText('wiredfurni.params.comparison_selection')}</Text>
                        {[0, 1, 2].map((value) => {
                            return (
                                <div key={value} className="flex items-center gap-1">
                                    <Radio checked={comparison === value} id={`altitudeComparison${value}`} name="altitudeComparison" onCheckedChange={() => setComparison(value)} />
                                    <Text>
                                        {spec.value === 'radius'
                                            ? localizeWithFallback(RANGE_COMPARISON_LABELS[value].key, RANGE_COMPARISON_LABELS[value].fallback)
                                            : LocalizeText(`wiredfurni.params.comparison.${value}`)}
                                    </Text>
                                </div>
                            );
                        })}
                    </div>
                )}
                {spec.value && (
                    <>
                        <div className="flex flex-col gap-1">
                            <Text bold>{localizeWithFallback(VALUE_LABELS[spec.value].key, VALUE_LABELS[spec.value].fallback)}</Text>
                            <input
                                className="form-control form-control-sm"
                                inputMode="decimal"
                                type="text"
                                value={altitudeInput}
                                onBlur={() => setAltitudeInput(formatAltitude(altitude))}
                                onChange={(event) => updateAltitudeInput(event.target.value)}
                            />
                        </div>
                        <div className="flex flex-col gap-1">
                            <Slider max={MAX_ALTITUDE} min={MIN_ALTITUDE} step={ALTITUDE_STEP} value={altitude} onChange={(event) => updateAltitude(event as number)} />
                            <Text small>{formatAltitude(altitude)}</Text>
                        </div>
                    </>
                )}
            </WiredConditionBaseView>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
3En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser