Add shared core attribute allocation
This commit is contained in:
@@ -9,7 +9,7 @@ export const load: PageServerLoad = async () => {
|
||||
weapons, weaponRanges, weaponTalentOptions, weaponBonuses,
|
||||
talents, talentTiers, talentEffects, talentTemplates,
|
||||
chipSets, chipTwoPiece, chipPrimary, chipSecondary, chipTalents, chipTalentEffects,
|
||||
protocols, protocolPrimary, protocolProfiles, protocolSecondary, protocolTalents, protocolTalentEffects,
|
||||
protocols, protocolPrimary, protocolProfiles, protocolSecondary, protocolTalents, protocolTalentEffects, coreRanges,
|
||||
icons
|
||||
] = await Promise.all([
|
||||
(await database.prepare('SELECT * FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
@@ -40,6 +40,7 @@ export const load: PageServerLoad = async () => {
|
||||
(await database.prepare('SELECT * FROM os_protocol_secondary_effects ORDER BY effect_role')).all(),
|
||||
(await database.prepare('SELECT * FROM os_protocol_talents')).all(),
|
||||
(await database.prepare('SELECT * FROM os_protocol_talent_effects ORDER BY sort_order')).all(),
|
||||
(await database.prepare('SELECT * FROM core_attribute_tier_ranges ORDER BY tier')).all(),
|
||||
(await database.prepare("SELECT * FROM game_data_icons")).all()
|
||||
]);
|
||||
const iconUrl = (type: 'talent' | 'brand_set' | 'os_protocol' | 'skill_chip', id: unknown) => {
|
||||
@@ -57,6 +58,7 @@ export const load: PageServerLoad = async () => {
|
||||
}));
|
||||
return {
|
||||
attributes,
|
||||
coreRanges,
|
||||
gear: gear.map((item) => ({
|
||||
...item,
|
||||
brandIds: gearBrands.filter((link) => link.gear_id === item.id).map((link) => String(link.brand_set_id)),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
let saved = $state(false);
|
||||
let message = $state('');
|
||||
const weaponRangeKeys = ['damage','firepower','toughness','engineering'] as const;
|
||||
const coreKeys = ['firepower','toughness','engineering'] as const;
|
||||
const gearAttributes = $derived(data.attributes.filter((item) => item.gear_min !== null).map((item) => ({ ...item, category: `${item.type} · ${item.unit || 'FLAT'}` })));
|
||||
const weaponAttributes = $derived(data.attributes.filter((item) => item.weapon_min !== null).map((item) => ({ ...item, category: `${item.type} · ${item.unit || 'FLAT'}` })));
|
||||
const chipAttributes = $derived(data.attributes.filter((item) => item.skill_chip_min !== null).map((item) => ({ ...item, category: `${item.type} · ${item.unit || 'FLAT'}` })));
|
||||
@@ -38,7 +39,14 @@
|
||||
const gearBaseRange = (item: any, key: 'health' | 'armor', level: number) =>
|
||||
scaledRange(item?.[`base_${key}_min`], item?.[`base_${key}_max`], level, item?.range_reference_level, 'gear');
|
||||
const weaponStatRange = (range: any, key: typeof weaponRangeKeys[number], level: number) =>
|
||||
scaledRange(range?.[`${key}_min`], range?.[`${key}_max`], level, range?.reference_level, 'weapon');
|
||||
scaledRange(range?.[`${key}_min`], range?.[`${key}_max`], level, range?.reference_level ?? 50, 'weapon');
|
||||
const coreAllocationRange = (tier: number, level: number, source: 'gear' | 'weapon') => {
|
||||
const profile = data.coreRanges.find((item) => Number(item.tier) === Number(tier));
|
||||
const nominal = scaledRange(profile?.nominal_min, profile?.nominal_max, level, profile?.reference_level ?? 50, source);
|
||||
return { ...nominal, poolMin: nominal.min * 3, poolMax: nominal.max * 3 };
|
||||
};
|
||||
const coreTotal = (selection: { firepower: number; toughness: number; engineering: number }) =>
|
||||
coreKeys.reduce((total, key) => total + Number(selection[key] ?? 0), 0);
|
||||
const attributeRange = (stat: any, source: 'gear' | 'weapon', level: number) =>
|
||||
scaledRange(stat?.[`${source}_min`], stat?.[`${source}_max`], level, stat?.[`${source}_reference_level`], source);
|
||||
const setRollStat = (roll: Roll, id: string, source: 'gear' | 'weapon' | 'chip', level = 50) => {
|
||||
@@ -75,12 +83,14 @@
|
||||
selection.talentId = item?.talentIds[0] ?? '';
|
||||
selection.health = Number(item?.base_health_min ?? 0);
|
||||
selection.armor = Number(item?.base_armor_min ?? 0);
|
||||
const core = coreAllocationRange(Number(item?.tier ?? 0), selection.level, 'gear');
|
||||
for (const key of coreKeys) selection[key] = core.min;
|
||||
selection.rolls = [blankRoll(), blankRoll()];
|
||||
selection.augmentSlots = [];
|
||||
};
|
||||
const changeGearLevel = (slot: string, rawLevel: unknown) => {
|
||||
const selection = draft.gear[slot], item = gearItem(slot);
|
||||
const nextLevel = Math.round(clamp(rawLevel, 1, 50));
|
||||
const nextLevel = Math.round(clamp(rawLevel, Number(item?.tier) === 2 ? 40 : 1, 50));
|
||||
if (!item || nextLevel === selection.level) {
|
||||
selection.level = nextLevel;
|
||||
return;
|
||||
@@ -95,6 +105,9 @@
|
||||
gearBaseRange(item, 'armor', selection.level),
|
||||
gearBaseRange(item, 'armor', nextLevel)
|
||||
);
|
||||
const previousCore = coreAllocationRange(Number(item.tier), selection.level, 'gear');
|
||||
const nextCore = coreAllocationRange(Number(item.tier), nextLevel, 'gear');
|
||||
for (const key of coreKeys) selection[key] = remapRangeValue(selection[key], { ...previousCore, min: 0, max: previousCore.poolMax }, { ...nextCore, min: 0, max: nextCore.poolMax });
|
||||
for (const roll of selection.rolls) {
|
||||
const stat = attribute(roll.statId);
|
||||
if (!stat) continue;
|
||||
@@ -112,7 +125,9 @@
|
||||
selection.level = 50;
|
||||
selection.tier = Number(item?.native_tier ?? 0);
|
||||
const range = item?.ranges.find((entry: any) => Number(entry.tier) === selection.tier);
|
||||
for (const key of weaponRangeKeys) selection[key] = weaponStatRange(range, key, selection.level).min;
|
||||
selection.damage = weaponStatRange(range, 'damage', selection.level).min;
|
||||
const core = coreAllocationRange(selection.tier, selection.level, 'weapon');
|
||||
for (const key of coreKeys) selection[key] = core.min;
|
||||
selection.talent1Id = item?.talent1Ids[0] ?? '';
|
||||
selection.talent2Id = item?.talent2Ids[0] ?? '';
|
||||
selection.rolls = [blankRoll(), blankRoll()];
|
||||
@@ -131,22 +146,24 @@
|
||||
const changeWeaponTier = (index: number, tier: number) => {
|
||||
const selection = draft.weapons[index], item = weaponItem(index);
|
||||
selection.tier = tier;
|
||||
if (tier === 2 && selection.level < 40) selection.level = 40;
|
||||
const range = item?.ranges.find((entry: any) => Number(entry.tier) === tier);
|
||||
for (const key of weaponRangeKeys) selection[key] = weaponStatRange(range, key, selection.level).min;
|
||||
selection.damage = weaponStatRange(range, 'damage', selection.level).min;
|
||||
const core = coreAllocationRange(tier, selection.level, 'weapon');
|
||||
for (const key of coreKeys) selection[key] = core.min;
|
||||
};
|
||||
const changeWeaponLevel = (index: number, rawLevel: unknown) => {
|
||||
const selection = draft.weapons[index], item = weaponItem(index);
|
||||
const nextLevel = Math.round(clamp(rawLevel, 1, 50));
|
||||
const nextLevel = Math.round(clamp(rawLevel, selection.tier === 2 ? 40 : 1, 50));
|
||||
const range = item?.ranges.find((entry: any) => Number(entry.tier) === selection.tier);
|
||||
if (!range || nextLevel === selection.level) {
|
||||
selection.level = nextLevel;
|
||||
return;
|
||||
}
|
||||
for (const key of weaponRangeKeys) selection[key] = remapRangeValue(
|
||||
selection[key],
|
||||
weaponStatRange(range, key, selection.level),
|
||||
weaponStatRange(range, key, nextLevel)
|
||||
);
|
||||
selection.damage = remapRangeValue(selection.damage, weaponStatRange(range, 'damage', selection.level), weaponStatRange(range, 'damage', nextLevel));
|
||||
const previousCore = coreAllocationRange(selection.tier, selection.level, 'weapon');
|
||||
const nextCore = coreAllocationRange(selection.tier, nextLevel, 'weapon');
|
||||
for (const key of coreKeys) selection[key] = remapRangeValue(selection[key], { ...previousCore, min: 0, max: previousCore.poolMax }, { ...nextCore, min: 0, max: nextCore.poolMax });
|
||||
for (const roll of selection.rolls) {
|
||||
const stat = attribute(roll.statId);
|
||||
if (!stat) continue;
|
||||
@@ -188,6 +205,7 @@
|
||||
add(byName('Health'), selection.health * (1 + basePercentage / 100), `${slot} base`);
|
||||
if (item.base_armor_min !== null) add(byName('Armor'), selection.armor * (1 + basePercentage / 100), `${slot} base`);
|
||||
for (const roll of selection.rolls) add(roll.statId, roll.value, `${slot} roll`);
|
||||
for (const key of coreKeys) add(byName(key), selection[key], `${slot} core`);
|
||||
if (selection.brandId) brandCounts.set(selection.brandId, (brandCounts.get(selection.brandId) ?? 0) + 1);
|
||||
for (const augment of enabledAugments.filter((entry) => entry.effect_type === 'Custom')) {
|
||||
add(augment.stat_attribute_id, augment.value, `${slot} augment ${augment.augment_slot}`);
|
||||
@@ -273,6 +291,8 @@
|
||||
draft = parsed;
|
||||
for (const slot of gearSlots) {
|
||||
if (!Number.isInteger(draft.gear[slot].level)) draft.gear[slot].level = 50;
|
||||
const item = gearItem(slot), core = coreAllocationRange(Number(item?.tier ?? 0), draft.gear[slot].level, 'gear');
|
||||
for (const key of coreKeys) if (!Number.isFinite(draft.gear[slot][key])) draft.gear[slot][key] = core.min;
|
||||
draft.gear[slot].rolls = [...(draft.gear[slot].rolls ?? []).slice(0, 2), blankRoll(), blankRoll()].slice(0, 2) as [Roll, Roll];
|
||||
}
|
||||
for (const weapon of draft.weapons) {
|
||||
@@ -313,12 +333,15 @@
|
||||
{@const healthRange = gearBaseRange(item, 'health', selection.level)}
|
||||
{@const armorRange = item.base_armor_min !== null ? gearBaseRange(item, 'armor', selection.level) : null}
|
||||
<div class="builder-equipment-controls">
|
||||
<RangeValueControl label="GEAR LEVEL" min={1} max={50} value={selection.level} onchange={(value) => changeGearLevel(slot,value)}/>
|
||||
<BuilderRadioGrid label="BRAND SET" name={`gear-${slot}-brand`} items={item.brandIds.map((id: string) => brand(id)).filter(Boolean)} bind:value={selection.brandId} columns={Math.min(item.brandIds.length,4)} required/>
|
||||
<RangeValueControl label="GEAR LEVEL" min={Number(item.tier) === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeGearLevel(slot,value)}/>
|
||||
<BuilderRadioGrid label="BRAND SET" name={`gear-${slot}-brand`} items={item.brandIds.map((id: string) => brand(id)).filter(Boolean)} bind:value={selection.brandId} columns={Math.min(item.brandIds.length,4)} iconOnly required/>
|
||||
{#if item.talentIds.length}<BuilderRadioGrid label="EQUIPPED TALENT" name={`gear-${slot}-talent`} items={item.talentIds.map((id: string) => talent(id)).filter(Boolean)} bind:value={selection.talentId} columns={Math.min(item.talentIds.length,4)} searchable={item.talentIds.length > 6} required/>{/if}
|
||||
</div>
|
||||
<p class:provisional={healthRange.provisional} class="builder-scaling-note">{healthRange.provisional ? 'Estimated range: levels 1–9 remain provisional while additional game samples are collected.' : `Ranges scaled from the stored level-${item.range_reference_level ?? 50} decimal endpoints. Display values are truncated to match the game.`}</p>
|
||||
<div class="builder-range-grid"><RangeValueControl label="BASE HEALTH" min={healthRange.min} max={healthRange.max} provisional={healthRange.provisional} bind:value={selection.health}/>{#if armorRange}<RangeValueControl label="BASE ARMOR" min={armorRange.min} max={armorRange.max} provisional={armorRange.provisional} bind:value={selection.armor}/>{/if}</div>
|
||||
{@const gearCore = coreAllocationRange(Number(item.tier), selection.level, 'gear')}
|
||||
<div class="builder-section-title"><div><span>CORE ATTRIBUTE ALLOCATION</span><small>SHARED T{item.tier} POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES</small></div><b class:restricted={coreTotal(selection) < gearCore.poolMin || coreTotal(selection) > gearCore.poolMax}>{coreTotal(selection)} / {gearCore.poolMin}–{gearCore.poolMax}</b></div>
|
||||
<div class="builder-core-grid">{#each coreKeys as key}<RangeValueControl label={key.toUpperCase()} min={0} max={gearCore.poolMax} bind:value={selection[key]}/>{/each}</div>
|
||||
<div class="builder-section-title"><div><span>ROLLED ATTRIBUTES</span><small>TWO REQUIRED SLOTS · VALUES ARE LIMITED TO GEAR RANGES</small></div></div>
|
||||
<div class="builder-rolls required">{#each selection.rolls as roll, index}{@const rollRange = attributeRange(attribute(roll.statId), 'gear', selection.level)}<div><BuilderPicker items={gearAttributes} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'gear',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== index).map((entry) => entry.statId).filter(Boolean)} label={`ATTRIBUTE ${index + 1}`}/><RangeValueControl label="VALUE" min={rollRange.min} max={rollRange.max} provisional={rollRange.provisional} bind:value={roll.value}/></div>{/each}</div>
|
||||
{@const augments = data.gearAugments.filter((entry) => entry.gear_slot === slot)}
|
||||
@@ -331,12 +354,19 @@
|
||||
{#if item}
|
||||
{#if Number(item.is_exotic) && other && Number(other.is_exotic)}<p class="error">Only one exotic weapon may be equipped. Choose a standard weapon in either slot.</p>{/if}
|
||||
<div class="builder-weapon-controls">
|
||||
<RangeValueControl label="WEAPON LEVEL" min={1} max={50} value={selection.level} onchange={(value) => changeWeaponLevel(index,value)}/>
|
||||
<RangeValueControl label="WEAPON LEVEL" min={selection.tier === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeWeaponLevel(index,value)}/>
|
||||
<SegmentedChoice legend="TIER" name={`weapon-${index}-tier`} options={item.ranges.map((range: any) => ({ value: String(range.tier), label: `T${range.tier}` }))} value={String(selection.tier)} onchange={(values) => changeWeaponTier(index,Number(values[0]))} showIndicator={false} required/>
|
||||
<div class="builder-fixed"><span>{item.rpm} RPM</span><span>{item.magazine} SHOTS</span><span>{item.reload_speed}s RELOAD</span></div>
|
||||
</div>
|
||||
{@const range = item.ranges.find((entry: any) => Number(entry.tier) === selection.tier)}
|
||||
{#if range}<p class:provisional={selection.level < 40} class="builder-scaling-note">{selection.level < 40 ? 'Estimated range: the weapon curve below level 40 is provisional and may change as observations are added.' : `Ranges scaled from the stored level-${range.reference_level ?? 50} decimal endpoints. Display values are truncated to match the game.`}</p><div class="builder-range-grid weapon">{#each weaponRangeKeys as key}{@const statRange = weaponStatRange(range,key,selection.level)}<RangeValueControl label={key.toUpperCase()} min={statRange.min} max={statRange.max} provisional={statRange.provisional} bind:value={selection[key]}/>{/each}</div>{/if}
|
||||
{#if range}
|
||||
<p class:provisional={selection.level < 40} class="builder-scaling-note">{selection.level < 40 ? 'Estimated range: the weapon curve below level 40 is provisional and may change as observations are added.' : `Damage scaled from the stored level-${range.reference_level ?? 50} decimal endpoints. Core attributes use the shared tier allocation pool.`}</p>
|
||||
{@const damageRange = weaponStatRange(range,'damage',selection.level)}
|
||||
<div class="builder-range-grid weapon-damage"><RangeValueControl label="DAMAGE" min={damageRange.min} max={damageRange.max} provisional={damageRange.provisional} bind:value={selection.damage}/></div>
|
||||
{@const weaponCore = coreAllocationRange(selection.tier, selection.level, 'weapon')}
|
||||
<div class="builder-section-title"><div><span>CORE ATTRIBUTE ALLOCATION</span><small>SHARED T{selection.tier} POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES</small></div><b class:restricted={coreTotal(selection) < weaponCore.poolMin || coreTotal(selection) > weaponCore.poolMax}>{coreTotal(selection)} / {weaponCore.poolMin}–{weaponCore.poolMax}</b></div>
|
||||
<div class="builder-core-grid">{#each coreKeys as key}<RangeValueControl label={key.toUpperCase()} min={0} max={weaponCore.poolMax} bind:value={selection[key]}/>{/each}</div>
|
||||
{/if}
|
||||
<div class="builder-section-title"><div><span>ROLLED ATTRIBUTES</span><small>TWO REQUIRED SLOTS · VALUES ARE LIMITED TO WEAPON RANGES</small></div></div>
|
||||
<div class="builder-rolls required">{#each selection.rolls as roll, rollIndex}{@const rollRange = attributeRange(attribute(roll.statId), 'weapon', selection.level)}<div><BuilderPicker items={weaponAttributes} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'weapon',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== rollIndex).map((entry) => entry.statId).filter(Boolean)} label={`ATTRIBUTE ${rollIndex + 1}`}/><RangeValueControl label="VALUE" min={rollRange.min} max={rollRange.max} provisional={rollRange.provisional} bind:value={roll.value}/></div>{/each}</div>
|
||||
<div class="builder-talent-grids">
|
||||
|
||||
Reference in New Issue
Block a user