mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
1183 lines
47 KiB
JavaScript
1183 lines
47 KiB
JavaScript
//@ts-check
|
|
"use strict";
|
|
|
|
/** @type {Asset[]} */
|
|
var Asset = [];
|
|
/** @type {AssetGroup[]} */
|
|
var AssetGroup = [];
|
|
/** @type {Map<`${AssetGroupName}/${string}`, Asset>} */
|
|
var AssetMap = new Map();
|
|
/** @type {Map<AssetGroupName, AssetGroup>} */
|
|
var AssetGroupMap = new Map();
|
|
/** @type {Pose[]} */
|
|
var Pose = [];
|
|
/** A record mapping pose names to their respective {@link Pose}. */
|
|
const PoseRecord = /** @type {Record<AssetPoseName, Pose>} */({});
|
|
/**
|
|
* A record mapping pose categories to sorting priorities.
|
|
*
|
|
* Used for prioritising certain poses over others in {@link CommonDrawResolveAssetPose},
|
|
* a process required due to BC's lack of pose combinatorics support.
|
|
* @satisfies {Record<AssetPoseCategory, number>}
|
|
*/
|
|
const PoseCategoryPriority = {
|
|
// FIXME: Hack to ensure that `Suspension` poses take priority over `BodyUpper`
|
|
BodyAddon: 3,
|
|
// FIXME: Hack to ensure that `BodyLower` poses take priority over `BodyUpper` if both are specified.
|
|
// BC simply doesn't support the required combinatorics, so just stick to the (completely aribtrary) historical
|
|
// precedent of prioritizing the lower body (a precedent used most notably by the wedding dress and teachers outfit)
|
|
BodyLower: 2,
|
|
BodyFull: 1,
|
|
BodyHands: 1,
|
|
BodyUpper: 1,
|
|
};
|
|
|
|
/** @type {Map<AssetGroupName, AssetGroup[]>} */
|
|
var AssetActivityMirrorGroups = new Map();
|
|
|
|
/**
|
|
* A record mapping all {@link Asset.IsLock} asset names to their respective assets.
|
|
* @type {Record<AssetLockType, Asset>}
|
|
*/
|
|
const AssetLocks = /** @type {Record<AssetLockType, Asset>} */({});
|
|
|
|
/** Special values for {@link AssetDefinition.PoseMapping} for hiding or using pose-agnostic assets. */
|
|
const PoseType = /** @type {const} */({
|
|
/**
|
|
* Ensures that the asset is hidden for a specific pose.
|
|
* Supercedes the old `HideForPose` property.
|
|
*/
|
|
HIDE: "Hide",
|
|
/**
|
|
* Ensures that the default (pose-agnostic) asset used for a particular pose.
|
|
* Supercedes the old `AllowPose` property.
|
|
*/
|
|
DEFAULT: "",
|
|
});
|
|
|
|
/**
|
|
* Adds a new asset group to the main list
|
|
* @param {IAssetFamily} Family
|
|
* @param {AssetGroupDefinition} GroupDef
|
|
* @returns {AssetGroup}
|
|
*/
|
|
function AssetGroupAdd(Family, GroupDef) {
|
|
const AllowNone = typeof GroupDef.AllowNone === "boolean" ? GroupDef.AllowNone : true;
|
|
const ColorSchema = (GroupDef.Color == null) ? ["Default"] : GroupDef.Color;
|
|
|
|
/** @type {AssetGroup} */
|
|
var A = {
|
|
Family: Family,
|
|
Name: GroupDef.Group,
|
|
Description: GroupDef.Group,
|
|
Asset: [],
|
|
ParentGroup: AssetParseParentGroup(GroupDef.ParentGroup, {}),
|
|
Category: (GroupDef.Category == null) ? "Appearance" : GroupDef.Category,
|
|
IsDefault: (GroupDef.Default == null) ? true : GroupDef.Default,
|
|
IsRestraint: (GroupDef.IsRestraint == null) ? false : GroupDef.IsRestraint,
|
|
AllowNone,
|
|
AllowColorize: (GroupDef.AllowColorize == null) ? true : GroupDef.AllowColorize,
|
|
AllowCustomize: (GroupDef.AllowCustomize == null) ? true : GroupDef.AllowCustomize,
|
|
Random: (GroupDef.Random == null) ? true : GroupDef.Random,
|
|
ColorSchema,
|
|
DefaultColor: ColorSchema[0],
|
|
ParentSize: (GroupDef.ParentSize == null) ? "" : GroupDef.ParentSize,
|
|
ParentColor: (GroupDef.ParentColor == null) ? "" : GroupDef.ParentColor,
|
|
Clothing: (GroupDef.Clothing == null) ? false : GroupDef.Clothing,
|
|
Underwear: (GroupDef.Underwear == null) ? false : GroupDef.Underwear,
|
|
BodyCosplay: (GroupDef.BodyCosplay == null) ? false : GroupDef.BodyCosplay,
|
|
EditOpacity: (GroupDef.EditOpacity == null) ? false : GroupDef.EditOpacity,
|
|
MinOpacity: (GroupDef.MinOpacity == null) ? 1 : GroupDef.MinOpacity,
|
|
MaxOpacity: (GroupDef.MaxOpacity == null) ? 1 : GroupDef.MaxOpacity,
|
|
Hide: GroupDef.Hide,
|
|
Block: GroupDef.Block,
|
|
Zone: GroupDef.Zone,
|
|
ArousalZoneID: GroupDef.ArousalZoneID,
|
|
SetPose: GroupDef.SetPose,
|
|
PoseMapping: GroupDef.PoseMapping || {},
|
|
AllowExpression: GroupDef.AllowExpression,
|
|
Effect: Array.isArray(GroupDef.Effect) ? GroupDef.Effect : [],
|
|
MirrorGroup: (GroupDef.MirrorGroup == null) ? "" : GroupDef.MirrorGroup,
|
|
RemoveItemOnRemove: (GroupDef.RemoveItemOnRemove == null) ? [] : GroupDef.RemoveItemOnRemove,
|
|
DrawingPriority: (GroupDef.Priority == null) ? AssetGroup.length : GroupDef.Priority,
|
|
DrawingLeft: AssetParseTopLeft(GroupDef.Left, 0),
|
|
DrawingTop: AssetParseTopLeft(GroupDef.Top, 0),
|
|
DrawingBlink: (GroupDef.Blink == null) ? false : GroupDef.Blink,
|
|
InheritColor: (typeof GroupDef.InheritColor === "string" ? GroupDef.InheritColor : null),
|
|
PreviewZone: GroupDef.PreviewZone,
|
|
DynamicGroupName: GroupDef.DynamicGroupName || GroupDef.Group,
|
|
MirrorActivitiesFrom: GroupDef.MirrorActivitiesFrom || undefined,
|
|
ArousalZone: (typeof GroupDef.ArousalZone === "string" ? GroupDef.ArousalZone : undefined),
|
|
ColorSuffix: GroupDef.ColorSuffix || {},
|
|
ExpressionPrerequisite: GroupDef.ExpressionPrerequisite || [],
|
|
HasPreviewImages: typeof GroupDef.HasPreviewImages === "boolean" ? GroupDef.HasPreviewImages : AllowNone,
|
|
/** @type {() => this is AssetAppearanceGroup} */
|
|
IsAppearance() { return this.Category === "Appearance"; },
|
|
/** @type {() => this is AssetItemGroup} */
|
|
IsItem() { return this.Category === "Item"; },
|
|
/** @type {() => this is AssetScriptGroup} */
|
|
IsScript() { return this.Category === "Script"; },
|
|
};
|
|
AssetGroupMap.set(A.Name, A);
|
|
AssetActivityMirrorGroupSet(A);
|
|
AssetGroup.push(A);
|
|
return A;
|
|
}
|
|
|
|
/**
|
|
* Parse the passed {@link AssetDefinition.Top} and Left values
|
|
* @param {undefined | TopLeft.Definition} value
|
|
* @param {number | TopLeft.Data} fallback
|
|
* @returns {TopLeft.Data}
|
|
*/
|
|
function AssetParseTopLeft(value, fallback) {
|
|
fallback = typeof fallback === "number" ? { [PoseType.DEFAULT]: fallback } : { ...fallback };
|
|
if (value == null) {
|
|
return fallback;
|
|
} else if (typeof value === "number") {
|
|
return { [PoseType.DEFAULT]: value };
|
|
} else {
|
|
let def = value[PoseType.DEFAULT];
|
|
if (def == null) {
|
|
def = fallback[PoseType.DEFAULT];
|
|
}
|
|
return { ...value, [PoseType.DEFAULT]: def };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects the group equivalence classes defined by the MirrorActivitiesFrom property into a map for easy access to
|
|
* mirror group sets (i.e. all groups that are mirror activities from, or are mirrored by, each other).
|
|
* @param {AssetGroup} group - The group to register
|
|
*/
|
|
function AssetActivityMirrorGroupSet(group) {
|
|
if (group.MirrorActivitiesFrom) {
|
|
const mirrorGroups = AssetActivityMirrorGroups.get(group.MirrorActivitiesFrom);
|
|
if (mirrorGroups) {
|
|
mirrorGroups.push(group);
|
|
AssetActivityMirrorGroups.set(group.Name, mirrorGroups);
|
|
return;
|
|
}
|
|
}
|
|
AssetActivityMirrorGroups.set(group.Name, [group]);
|
|
}
|
|
|
|
/**
|
|
* Adds a new asset to the main list
|
|
* @param {AssetGroup} Group
|
|
* @param {AssetDefinition} AssetDef
|
|
* @param {ExtendedItemMainConfig} ExtendedConfig
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function AssetAdd(Group, AssetDef, ExtendedConfig) {
|
|
const allowLock = typeof AssetDef.AllowLock === "boolean" ? AssetDef.AllowLock : false;
|
|
const difficulty = AssetDef.Difficulty ?? 0;
|
|
|
|
const editOpacity = (typeof AssetDef.EditOpacity === "boolean") ? AssetDef.EditOpacity : (typeof Group.EditOpacity === "boolean") ? Group.EditOpacity : false;
|
|
let minOpacity = editOpacity ? AssetParseOpacity(typeof AssetDef.MinOpacity === "number" ? AssetDef.MinOpacity : Group.MinOpacity) : 1;
|
|
let maxOpacity = editOpacity ? AssetParseOpacity(typeof AssetDef.MaxOpacity === "number" ? AssetDef.MaxOpacity : Group.MaxOpacity) : 1;
|
|
const opacity = editOpacity ? AssetParseOpacity(AssetDef.Opacity, minOpacity, maxOpacity) : 1;
|
|
|
|
/** @type {Mutable<Asset>} */
|
|
var A = {
|
|
Name: AssetDef.Name,
|
|
Description: AssetDef.Name,
|
|
Group: Group,
|
|
ParentItem: AssetDef.ParentItem,
|
|
ParentGroup: AssetParseParentGroup(AssetDef.ParentGroup, Group.ParentGroup),
|
|
Enable: (AssetDef.Enable == null) ? true : AssetDef.Enable,
|
|
Visible: (AssetDef.Visible == null) ? true : AssetDef.Visible,
|
|
NotVisibleOnScreen: Array.isArray(AssetDef.NotVisibleOnScreen) ? AssetDef.NotVisibleOnScreen : [],
|
|
Wear: (AssetDef.Wear == null) ? true : AssetDef.Wear,
|
|
Activity: (typeof AssetDef.Activity === "string" ? AssetDef.Activity : null),
|
|
ActivityAudio: Array.isArray(AssetDef.ActivityAudio) ? AssetDef.ActivityAudio : [],
|
|
AllowActivity: Array.isArray(AssetDef.AllowActivity) ? AssetDef.AllowActivity : [],
|
|
AllowActivityOn: Array.isArray(AssetDef.AllowActivityOn) ? AssetDef.AllowActivityOn : [],
|
|
ActivityExpression: Array.isArray(AssetDef.ActivityExpression) ? AssetDef.ActivityExpression : {},
|
|
BuyGroup: AssetDef.BuyGroup,
|
|
InventoryID: AssetDef.InventoryID,
|
|
Effect: (AssetDef.Effect == null) ? Group.Effect : AssetDef.Effect,
|
|
Bonus: AssetDef.Bonus,
|
|
Block: (AssetDef.Block == null) ? Group.Block : AssetDef.Block,
|
|
Expose: (AssetDef.Expose == null) ? [] : AssetDef.Expose,
|
|
Hide: (AssetDef.Hide == null) ? Group.Hide : AssetDef.Hide,
|
|
HideItem: AssetDef.HideItem,
|
|
HideItemExclude: AssetDef.HideItemExclude || [],
|
|
HideItemAttribute: AssetDef.HideItemAttribute || [],
|
|
Require: (!Array.isArray(AssetDef.Require) ? [] : AssetDef.Require),
|
|
SetPose: (AssetDef.SetPose == null) ? Group.SetPose : AssetDef.SetPose,
|
|
AllowActivePose: AssetDef.AllowActivePose,
|
|
Value: (AssetDef.Value == null) ? 0 : AssetDef.Value,
|
|
NeverSell: AssetDef.NeverSell ?? false,
|
|
Difficulty: difficulty,
|
|
SelfBondage: (AssetDef.SelfBondage == null) ? 0 : AssetDef.SelfBondage,
|
|
SelfUnlock: (AssetDef.SelfUnlock == null) ? true : AssetDef.SelfUnlock,
|
|
ExclusiveUnlock: (AssetDef.ExclusiveUnlock == null) ? false : AssetDef.ExclusiveUnlock,
|
|
Random: (AssetDef.Random == null) ? true : AssetDef.Random,
|
|
RemoveAtLogin: (AssetDef.RemoveAtLogin == null) ? false : AssetDef.RemoveAtLogin,
|
|
WearTime: (AssetDef.Time == null) ? 0 : AssetDef.Time,
|
|
RemoveTime: (AssetDef.RemoveTime == null) ? ((AssetDef.Time == null) ? 0 : AssetDef.Time) : AssetDef.RemoveTime,
|
|
RemoveTimer: (AssetDef.RemoveTimer == null) ? 0 : AssetDef.RemoveTimer,
|
|
MaxTimer: (AssetDef.MaxTimer == null) ? 0 : AssetDef.MaxTimer,
|
|
DrawingPriority: AssetDef.Priority,
|
|
DrawingLeft: AssetParseTopLeft(AssetDef.Left, Group.DrawingLeft),
|
|
DrawingTop: AssetParseTopLeft(AssetDef.Top, Group.DrawingTop),
|
|
HeightModifier: (AssetDef.Height == null) ? 0 : AssetDef.Height,
|
|
ZoomModifier: (AssetDef.Zoom == null) ? 1 : AssetDef.Zoom,
|
|
Alpha: AssetParseAlpha(AssetDef.Alpha, "Asset.Alpha"),
|
|
FullAlpha: AssetDef.FullAlpha == null ? true : AssetDef.FullAlpha,
|
|
Prerequisite: (typeof AssetDef.Prerequisite === "string" ? [AssetDef.Prerequisite] : Array.isArray(AssetDef.Prerequisite) ? AssetDef.Prerequisite : []),
|
|
Extended: (AssetDef.Extended == null) ? false : AssetDef.Extended,
|
|
AlwaysExtend: (AssetDef.AlwaysExtend == null) ? false : AssetDef.AlwaysExtend,
|
|
AlwaysInteract: (AssetDef.AlwaysInteract == null) ? false : AssetDef.AlwaysInteract,
|
|
AllowLock: allowLock,
|
|
LayerVisibility: (AssetDef.LayerVisibility == null) ? false : AssetDef.LayerVisibility,
|
|
IsLock: (AssetDef.IsLock == null) ? false : AssetDef.IsLock,
|
|
PickDifficulty: (AssetDef.PickDifficulty == null) ? 0 : AssetDef.PickDifficulty,
|
|
OwnerOnly: (AssetDef.OwnerOnly == null) ? false : AssetDef.OwnerOnly,
|
|
LoverOnly: (AssetDef.LoverOnly == null) ? false : AssetDef.LoverOnly,
|
|
FamilyOnly: (AssetDef.FamilyOnly == null) ? false : AssetDef.FamilyOnly,
|
|
ExpressionTrigger: AssetDef.ExpressionTrigger,
|
|
RemoveItemOnRemove: (AssetDef.RemoveItemOnRemove == null) ? Group.RemoveItemOnRemove : Group.RemoveItemOnRemove.concat(AssetDef.RemoveItemOnRemove),
|
|
AllowEffect: AssetDef.AllowEffect,
|
|
AllowBlock: AssetDef.AllowBlock,
|
|
AllowTighten: AssetDef.AllowTighten ?? (AssetDef.IsRestraint || difficulty > 0),
|
|
AllowHide: AssetDef.AllowHide,
|
|
AllowHideItem: AssetDef.AllowHideItem,
|
|
DefaultColor: [],
|
|
EditOpacity: editOpacity,
|
|
Opacity: opacity,
|
|
MinOpacity: minOpacity,
|
|
MaxOpacity: maxOpacity,
|
|
Audio: AssetDef.Audio,
|
|
Category: AssetDef.Category,
|
|
Fetish: AssetDef.Fetish,
|
|
ArousalZone: typeof AssetDef.ArousalZone === "string" ? AssetDef.ArousalZone : (Group.ArousalZone ? Group.ArousalZone : /** @type {AssetGroupItemName} */ (Group.Name)),
|
|
IsRestraint: (AssetDef.IsRestraint == null) ? ((Group.IsRestraint == null) ? false : Group.IsRestraint) : AssetDef.IsRestraint,
|
|
BodyCosplay: (AssetDef.BodyCosplay == null) ? Group.BodyCosplay : AssetDef.BodyCosplay,
|
|
OverrideBlinking: (AssetDef.OverrideBlinking == null) ? false : AssetDef.OverrideBlinking,
|
|
DialogSortOverride: AssetDef.DialogSortOverride,
|
|
// @ts-ignore: this has no type, because we are in JS file
|
|
DynamicDescription: (typeof AssetDef.DynamicDescription === 'function') ? AssetDef.DynamicDescription : function () { return this.Description; },
|
|
DynamicPreviewImage: (typeof AssetDef.DynamicPreviewImage === 'function') ? AssetDef.DynamicPreviewImage : function () { return ""; },
|
|
DynamicAllowInventoryAdd: (typeof AssetDef.DynamicAllowInventoryAdd === 'function') ? AssetDef.DynamicAllowInventoryAdd : function () { return true; },
|
|
// @ts-ignore: this has no type, because we are in JS file
|
|
DynamicName: (typeof AssetDef.DynamicName === 'function') ? AssetDef.DynamicName : function () { return this.Name; },
|
|
DynamicGroupName: (AssetDef.DynamicGroupName || Group.DynamicGroupName),
|
|
DynamicActivity: (typeof AssetDef.DynamicActivity === 'function') ? AssetDef.DynamicActivity : function () { return AssetDef.Activity; },
|
|
DynamicAudio: (typeof AssetDef.DynamicAudio === 'function') ? AssetDef.DynamicAudio : null,
|
|
AllowRemoveExclusive: typeof AssetDef.AllowRemoveExclusive === 'boolean' ? AssetDef.AllowRemoveExclusive : false,
|
|
InheritColor: AssetDef.InheritColor || Group.InheritColor,
|
|
DynamicBeforeDraw: (typeof AssetDef.DynamicBeforeDraw === 'boolean') ? AssetDef.DynamicBeforeDraw : false,
|
|
DynamicAfterDraw: (typeof AssetDef.DynamicAfterDraw === 'boolean') ? AssetDef.DynamicAfterDraw : false,
|
|
DynamicScriptDraw: (typeof AssetDef.DynamicScriptDraw === 'boolean') ? AssetDef.DynamicScriptDraw : false,
|
|
CreateLayerTypes: Array.isArray(AssetDef.CreateLayerTypes) ? AssetDef.CreateLayerTypes : [],
|
|
AllowLockType: null,
|
|
AllowColorizeAll: typeof AssetDef.AllowColorizeAll === "boolean" ? AssetDef.AllowColorizeAll : true,
|
|
AvailableLocations: AssetDef.AvailableLocations || [],
|
|
OverrideHeight: AssetDef.OverrideHeight,
|
|
DrawLocks: allowLock && (typeof AssetDef.DrawLocks === "boolean" ? AssetDef.DrawLocks : true),
|
|
AllowExpression: AssetDef.AllowExpression,
|
|
MirrorExpression: AssetDef.MirrorExpression,
|
|
FixedPosition: typeof AssetDef.FixedPosition === "boolean" ? AssetDef.FixedPosition : false,
|
|
Layer: [],
|
|
ColorableLayerCount: 0,
|
|
CustomBlindBackground: typeof AssetDef.CustomBlindBackground === 'string' ? AssetDef.CustomBlindBackground : undefined,
|
|
Attribute: AssetDef.Attribute || [],
|
|
PreviewIcons: AssetDef.PreviewIcons || [],
|
|
PoseMapping: AssetParsePoseMapping(AssetDef.PoseMapping, Group.PoseMapping, AssetDef.InheritPoseMappingFields),
|
|
Tint: Array.isArray(AssetDef.Tint) ? AssetDef.Tint : [],
|
|
AllowTint: Array.isArray(AssetDef.Tint) && AssetDef.Tint.length > 0,
|
|
DefaultTint: typeof AssetDef.DefaultTint === "string" ? AssetDef.DefaultTint : undefined,
|
|
Gender: AssetDef.Gender,
|
|
CraftGroup: typeof AssetDef.CraftGroup === "string" ? AssetDef.CraftGroup : AssetDef.Name,
|
|
ColorSuffix: AssetDef.ColorSuffix || Group.ColorSuffix,
|
|
ExpressionPrerequisite: Array.isArray(AssetDef.ExpressionPrerequisite) ? AssetDef.ExpressionPrerequisite : Group.ExpressionPrerequisite,
|
|
AllowColorize: typeof AssetDef.AllowColorize === "boolean" ? AssetDef.AllowColorize : Group.AllowColorize,
|
|
};
|
|
|
|
if (A.SetPose) {
|
|
Object.assign(A, AssetParsePosePrerequisite(A));
|
|
}
|
|
|
|
const layers = AssetDef.Layer ? [...AssetDef.Layer] : [{}];
|
|
if (A.DrawLocks) {
|
|
layers.push({ Name: "Lock", LockLayer: true, AllowColorize: false, ParentGroup: {} });
|
|
}
|
|
A.Layer = layers.map((Layer, I) => AssetMapLayer(Layer, A, I));
|
|
A.Layer.forEach((layer, i) => {
|
|
// Now that most layer pose mappings are initialized, handle all `Layer.CopyLayerPoseMapping` cases
|
|
if (layer.PoseMapping) {
|
|
return;
|
|
}
|
|
|
|
const layerDef = layers[i];
|
|
const copiedLayer = A.Layer.find(l => l.Name === layerDef.CopyLayerPoseMapping);
|
|
if (copiedLayer) {
|
|
if (!copiedLayer.PoseMapping) {
|
|
throw new Error(`${A.Group.Name}:${A.Name}:${layer.Name}: Trying to copy a null-valued pose mapping from layer "${copiedLayer.Name}"`);
|
|
}
|
|
/** @type {Mutable<AssetLayer>} */(layer).PoseMapping = AssetParsePoseMapping(
|
|
layerDef.PoseMapping, copiedLayer.PoseMapping, layerDef.InheritPoseMappingFields,
|
|
);
|
|
}
|
|
});
|
|
|
|
AssetAssignColorIndices(A);
|
|
A.DefaultColor = AssetParseDefaultColor(A.ColorableLayerCount, Group.DefaultColor, AssetDef.DefaultColor);
|
|
|
|
// Unwearable assets are not visible but can be overwritten
|
|
if (!A.Wear && AssetDef.Visible != true) A.Visible = false;
|
|
/** @type {Asset[]} */(Group.Asset).push(A);
|
|
AssetMap.set(`${Group.Name}/${A.Name}`, A);
|
|
Asset.push(A);
|
|
if (A.IsLock) {
|
|
AssetLocks[/** @type {AssetLockType} */(A.Name)] = A;
|
|
}
|
|
|
|
// Initialize the extended item data of archetypical items
|
|
if (ExtendedConfig) {
|
|
const assetBaseConfig = AssetFindExtendedConfig(ExtendedConfig, A.Group.Name, A.Name);
|
|
if (assetBaseConfig != null) {
|
|
AssetBuildExtended(A, assetBaseConfig, ExtendedConfig);
|
|
}
|
|
}
|
|
|
|
// Ensure that restraints do not have clothing-exclusive properties and vice versa
|
|
switch (Group.Category) {
|
|
case "Item":
|
|
A.BodyCosplay = false;
|
|
break;
|
|
case "Appearance":
|
|
A.AllowLock = false;
|
|
A.AllowLockType = null;
|
|
A.AllowRemoveExclusive = false;
|
|
A.AllowTighten = false;
|
|
A.AlwaysExtend = false;
|
|
A.AlwaysInteract = false;
|
|
A.CustomBlindBackground = undefined;
|
|
A.Difficulty = 0;
|
|
A.DrawLocks = false;
|
|
A.ExclusiveUnlock = false;
|
|
A.FamilyOnly = false;
|
|
A.IsLock = false;
|
|
A.IsRestraint = false;
|
|
A.LoverOnly = false;
|
|
A.MaxTimer = 0;
|
|
A.OwnerOnly = false;
|
|
A.PickDifficulty = 0;
|
|
A.RemoveTime = 0;
|
|
A.RemoveTimer = 0;
|
|
A.SelfBondage = 0;
|
|
A.SelfUnlock = true;
|
|
A.WearTime = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Automatically generated pose-related asset prerequisites
|
|
* @param {Partial<Pick<Asset, "AllowActivePose" | "SetPose" | "Prerequisite" | "Effect">>} asset The asset or any other object with the expected asset interface subset
|
|
* @returns {{ Prerequisite?: AssetPrerequisite[], AllowActivePose?: AssetPoseName[] }} The newly generated prerequisites
|
|
*/
|
|
function AssetParsePosePrerequisite({ SetPose, AllowActivePose, Effect, Prerequisite }) {
|
|
if (SetPose == null) {
|
|
return {};
|
|
}
|
|
|
|
const allowActivePose = CommonArrayConcatDedupe([...SetPose], AllowActivePose || []);
|
|
const poseMapping = PoseToMapping.Array(allowActivePose);
|
|
|
|
// Automatically add support for `BodyFull` poses, which are a bit weird in the sense that
|
|
// they're sort of a combination of (hypothetical) `BodyUpper` and `BodyLower` poses
|
|
if (
|
|
(poseMapping.BodyUpper || poseMapping.BodyLower)
|
|
&& (!poseMapping.BodyUpper || poseMapping.BodyUpper.includes("BackElbowTouch"))
|
|
&& (!poseMapping.BodyLower || poseMapping.BodyLower.includes("Kneel"))
|
|
) {
|
|
// Add explicit Hogtied support if `Kneel` and/or `BackElbowTouch` are set
|
|
CommonArrayConcatDedupe(allowActivePose, ["Hogtied"]);
|
|
}
|
|
if (
|
|
!poseMapping.BodyUpper
|
|
&& poseMapping.BodyLower
|
|
&& poseMapping.BodyLower.includes("Kneel")
|
|
) {
|
|
// Add explicit AllFours support if `Kneel` is set and no `BodyUpper` poses are set
|
|
CommonArrayConcatDedupe(allowActivePose, ["AllFours"]);
|
|
}
|
|
|
|
/** @type {AssetPrerequisite[]} */
|
|
const posePrerequisite = SetPose.map(i => /** @type {const} */(`Can${i}`));
|
|
if (PoseAllStanding.some(p => SetPose.includes(p)) && !allowActivePose.includes("Kneel")) {
|
|
posePrerequisite.push("NotKneeling");
|
|
}
|
|
|
|
if (Effect && Effect.includes("Suspended")) {
|
|
posePrerequisite.push("NotChained");
|
|
}
|
|
|
|
return {
|
|
Prerequisite: CommonArrayConcatDedupe([...(Prerequisite || [])], posePrerequisite),
|
|
AllowActivePose: allowActivePose,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @param config The config
|
|
* @param superConfig The super config
|
|
* @param key A human-readable identifier for `config`
|
|
* @param superKey A human-readable identifier for `superConfig`
|
|
* @returns `true` if the validation failed and `false` otherwise
|
|
* @typedef {(config: T, superConfig: T, key: string, superKey: string) => boolean} AssetCopyConfigValidator
|
|
*/
|
|
|
|
/**
|
|
* Namespace for resolving `CopyConfig` fields in (extended) asset configs.
|
|
* @namespace
|
|
*/
|
|
var AssetResolveCopyConfig = {
|
|
/**
|
|
* Take an (ordered) list of `CopyConfig`-referenced configs and group them all together in a `BuyGroup`.
|
|
* The buygroup's name will either be extracted from the configs if present, or alternatively use the `Name` of the top-most config.
|
|
* @param {readonly { BuyGroup?: string, Value?: number, Name?: string }[]} configList
|
|
*/
|
|
_AssignBuyGroup: function _AssignBuyGroup(configList) {
|
|
const topIndex = configList.length - 1;
|
|
const topConfig = configList[topIndex];
|
|
|
|
// Check if a buygroup is already present in one of the super configs
|
|
/** @type {number | undefined} */
|
|
let stop = undefined;
|
|
const buyGroup = configList.find((cfg, i) => {
|
|
if (cfg.BuyGroup) {
|
|
stop = Math.max(1, i);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
})?.BuyGroup ?? topConfig.Name;
|
|
if (buyGroup == null) {
|
|
// Only relevant if an extended config (instead of a normal one) somehow gets passed
|
|
return;
|
|
}
|
|
|
|
configList.slice(0, stop).forEach((cfg, i) => {
|
|
cfg.BuyGroup = buyGroup;
|
|
if (i !== topIndex) {
|
|
cfg.Value = -1;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Merge the passed config with all it's to-be copied super configs (per its `CopyConfig` settings)
|
|
* @template {{ CopyConfig?: { GroupName?: AssetGroupName, AssetName: string }, BuyGroup?: string, Value?: number, Name?: string }} T
|
|
* @param {T} config - The (extended) asset config
|
|
* @param {string} assetName - The name of the corresponding asset
|
|
* @param {AssetGroupName} groupName - The name of the corresponding asset group
|
|
* @param {Partial<Record<AssetGroupName, Record<string, T>>>} configRecord - A (nested) record containing the configs of all assets
|
|
* @param {string} configType - The name of the config type. Used for error reporting
|
|
* @param {null | AssetCopyConfigValidator<T>} configValidator - An optional validator for comparing the config with its to-be copied counterpart(s)
|
|
* @param {boolean} setBuyGroup - Whether to automatically assign a buygroup to the config and, if required, all `CopyConfig`-referenced super configs
|
|
* @returns {null | T} - The original config merged with its to-be copied super configs. Returns `null` if an error is encountered.
|
|
*/
|
|
_Resolve: function _Resolve(config, assetName, groupName, configRecord, configType, configValidator=null, setBuyGroup=false) {
|
|
const key = `${groupName}:${assetName}`;
|
|
const visitedKeys = new Set([key]);
|
|
const visitedConfigs = [config];
|
|
while (config.CopyConfig) {
|
|
const { GroupName, AssetName } = config.CopyConfig;
|
|
const superKey = `${GroupName ?? groupName}:${AssetName}`;
|
|
|
|
if (visitedKeys.has(superKey)) {
|
|
console.error(`Found cyclic ${configType} CopyConfig reference ${superKey} in ${key}:`, visitedKeys);
|
|
return null;
|
|
}
|
|
|
|
const superConfig = configRecord[GroupName ?? groupName]?.[AssetName];
|
|
if (!superConfig) {
|
|
console.error(`${superKey} ${configType} CopyConfig not found for ${key}`);
|
|
return null;
|
|
} else if (configValidator?.(config, superConfig, key, superKey)) {
|
|
return null;
|
|
} else {
|
|
visitedKeys.add(superKey);
|
|
visitedConfigs.push(superConfig);
|
|
}
|
|
|
|
config = {
|
|
...superConfig,
|
|
...CommonOmit(config, ["CopyConfig"]),
|
|
};
|
|
}
|
|
|
|
if (setBuyGroup) {
|
|
visitedConfigs[0] = config;
|
|
AssetResolveCopyConfig._AssignBuyGroup(visitedConfigs);
|
|
}
|
|
|
|
return config;
|
|
},
|
|
|
|
/**
|
|
* Validator for extended item configs
|
|
* @type {AssetCopyConfigValidator<AssetArchetypeConfig>}
|
|
*/
|
|
_ExtendedValidator: function _ExtendedValidator(config, superConfig, key, superKey) {
|
|
if (config.Archetype !== superConfig.Archetype) {
|
|
console.error(`Archetype for ${superKey} (${superConfig.Archetype}) doesn't match archetype for ${key} (${config.Archetype})`);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Construct the items asset config, merging via {@link AssetDefinition.CopyConfig} if required.
|
|
* @param {AssetDefinition} assetDef - The asset definition
|
|
* @param {AssetGroupName} groupName - The name of the asset group
|
|
* @param {Partial<Record<AssetGroupName, Record<string, AssetDefinition>>>} assetRecord - A record containg all asset definitions
|
|
* @returns {null | AssetDefinition} - The oiginally passed base item configuration.
|
|
* Returns `null` insstead if an error was encountered.
|
|
*/
|
|
AssetDefinition: function AssetDefinition(assetDef, groupName, assetRecord) {
|
|
return AssetResolveCopyConfig._Resolve(
|
|
assetDef, assetDef.Name, groupName, assetRecord,
|
|
"AssetDefinition", undefined, assetDef.CopyConfig?.BuyGroup,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Construct the items extended item config, merging via {@link AssetArchetypeConfig.CopyConfig} if required.
|
|
* @param {Asset} asset - The asset to configure
|
|
* @param {AssetArchetypeConfig} config - The extended item configuration of the base item
|
|
* @param {ExtendedItemMainConfig} extendedConfig - The extended item configuration object for the asset's family
|
|
* @returns {null | AssetArchetypeConfig} - The oiginally passed base item configuration.
|
|
* Returns `null` instead if an error was encountered.
|
|
*/
|
|
ExtendedItemConfig: function ExtendedItemConfig(asset, config, extendedConfig) {
|
|
return AssetResolveCopyConfig._Resolve(
|
|
config, asset.Name, asset.Group.Name, extendedConfig, "ExtendedItemConfig",
|
|
AssetResolveCopyConfig._ExtendedValidator,
|
|
);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Constructs extended item functions for an asset, if extended item configuration exists for the asset.
|
|
* Updates the passed config inplace if {@link ExtendedItem.CopyConfig} is present.
|
|
* @param {Asset} A - The asset to configure
|
|
* @param {AssetArchetypeConfig} baseConfig - The extended item configuration of the base item
|
|
* @param {ExtendedItemMainConfig} extendedConfig - The extended item configuration object for the asset's family
|
|
* @param {null | ExtendedItemOption} parentOption
|
|
* @param {boolean} createCallbacks
|
|
* @returns {null | AssetArchetypeData} - The extended itemdata or `null` if an error was encoutered
|
|
*/
|
|
function AssetBuildExtended(A, baseConfig, extendedConfig, parentOption=null, createCallbacks=true) {
|
|
const config = AssetResolveCopyConfig.ExtendedItemConfig(A, baseConfig, extendedConfig);
|
|
if (config === null) {
|
|
return null;
|
|
}
|
|
|
|
/** @type {null | AssetArchetypeData} */
|
|
let data = null;
|
|
switch (config.Archetype) {
|
|
case ExtendedArchetype.MODULAR:
|
|
data = ModularItemRegister(A, config);
|
|
break;
|
|
case ExtendedArchetype.TYPED:
|
|
data = TypedItemRegister(A, config);
|
|
break;
|
|
case ExtendedArchetype.VIBRATING:
|
|
data = VibratorModeRegister(A, config, parentOption);
|
|
break;
|
|
case ExtendedArchetype.VARIABLEHEIGHT:
|
|
data = VariableHeightRegister(A, config, parentOption);
|
|
break;
|
|
case ExtendedArchetype.TEXT:
|
|
data = TextItemRegister(A, config, parentOption, createCallbacks);
|
|
break;
|
|
case ExtendedArchetype.NOARCH:
|
|
data = NoArchItemRegister(A, config, parentOption);
|
|
break;
|
|
}
|
|
|
|
if (!A.Archetype && parentOption == null) {
|
|
/** @type {Mutable<Asset>} */(A).Archetype = config.Archetype;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Finds the extended item configuration for the provided group and asset name, if any exists
|
|
* @param {ExtendedItemMainConfig} ExtendedConfig - The full extended item configuration object
|
|
* @param {AssetGroupName} GroupName - The name of the asset group to find extended configuration for
|
|
* @param {string} AssetName - The name of the asset to find extended configuration fo
|
|
* @returns {AssetArchetypeConfig | undefined} - The extended asset configuration object for the specified asset, if
|
|
* any exists, or undefined otherwise
|
|
*/
|
|
function AssetFindExtendedConfig(ExtendedConfig, GroupName, AssetName) {
|
|
const GroupConfig = ExtendedConfig[GroupName] || {};
|
|
return GroupConfig[AssetName];
|
|
}
|
|
|
|
/**
|
|
* Maps a layer definition to a drawable layer object
|
|
* @param {AssetLayerDefinition} Layer - The raw layer definition
|
|
* @param {Asset} A - The built asset
|
|
* @param {number} I - The index of the layer within the asset
|
|
* @return {AssetLayer} - A Layer object representing the drawable properties of the given layer
|
|
*/
|
|
function AssetMapLayer(Layer, A, I) {
|
|
const editOpacity = A.EditOpacity;
|
|
const minOpacity = editOpacity ? AssetParseOpacity(Layer.MinOpacity ?? A.MinOpacity, 0, 1) : 1;
|
|
const maxOpacity = editOpacity ? AssetParseOpacity(Layer.MaxOpacity ?? A.MaxOpacity, 0, 1) : 1;
|
|
const opacity = AssetParseOpacity(Layer.Opacity ?? A.Opacity, minOpacity, maxOpacity);
|
|
|
|
/** @type {AssetLayer} */
|
|
const L = {
|
|
Name: Layer.Name || null,
|
|
AllowColorize: !Layer.TextureMask && (typeof Layer.AllowColorize === "boolean" ? Layer.AllowColorize : A.AllowColorize),
|
|
CopyLayerColor: typeof Layer.CopyLayerColor === "string" ? Layer.CopyLayerColor : null,
|
|
ColorGroup: typeof Layer.ColorGroup === "string" ? Layer.ColorGroup : null,
|
|
HideColoring: typeof Layer.HideColoring === "boolean" ? Layer.HideColoring : false,
|
|
AllowTypes: Layer.AllowTypes != null ? AssetParseAllowTypes(Layer.AllowTypes) : null,
|
|
CreateLayerTypes: Array.isArray(Layer.CreateLayerTypes) ? Layer.CreateLayerTypes : A.CreateLayerTypes,
|
|
Visibility: typeof Layer.Visibility === "string" ? Layer.Visibility : null,
|
|
ParentGroup: AssetParseParentGroup(Layer.ParentGroup, A.ParentGroup),
|
|
Priority: Layer.Priority || A.DrawingPriority || A.Group.DrawingPriority,
|
|
InheritColor: typeof Layer.InheritColor === "string" ? Layer.InheritColor : A.InheritColor,
|
|
Alpha: AssetParseLayerAlpha(Layer, A, I),
|
|
Asset: A,
|
|
DrawingLeft: AssetParseTopLeft(Layer.Left, A.DrawingLeft),
|
|
DrawingTop: AssetParseTopLeft(Layer.Top, A.DrawingTop),
|
|
HideAs: Layer.HideAs,
|
|
FixedPosition: typeof Layer.FixedPosition === "boolean" ? Layer.FixedPosition : false,
|
|
HasImage: Layer.HasImage ?? A.Visible,
|
|
Opacity: opacity,
|
|
MinOpacity: minOpacity,
|
|
MaxOpacity: maxOpacity,
|
|
BlendingMode: Layer.BlendingMode || "source-over",
|
|
TextureMask: Layer.TextureMask,
|
|
LockLayer: typeof Layer.LockLayer === "boolean" ? Layer.LockLayer : false,
|
|
MirrorExpression: Layer.MirrorExpression,
|
|
ColorIndex: 0,
|
|
// If `null`, initialized in the second asset layer loop in `AssetAdd()`
|
|
PoseMapping: Layer.CopyLayerPoseMapping != null ? /** @type {never} */(null) : AssetParsePoseMapping(Layer.PoseMapping, A.PoseMapping, Layer.InheritPoseMappingFields),
|
|
HideForAttribute: Array.isArray(Layer.HideForAttribute) ? Layer.HideForAttribute : null,
|
|
ShowForAttribute: Array.isArray(Layer.ShowForAttribute) ? Layer.ShowForAttribute : null,
|
|
ColorSuffix: Layer.ColorSuffix || A.ColorSuffix,
|
|
};
|
|
return L;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {null | undefined | AssetPoseMapping} poseMapping
|
|
* @param {AssetPoseMapping} superPoseMapping
|
|
* @param {boolean} inheritFields
|
|
* @returns {AssetPoseMapping}
|
|
*/
|
|
function AssetParsePoseMapping(poseMapping, superPoseMapping, inheritFields=false) {
|
|
if (inheritFields) {
|
|
return { ...superPoseMapping, ...(poseMapping ?? {}) };
|
|
} else {
|
|
return poseMapping ?? { ...superPoseMapping };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {AllowTypes.Definition} allowTypes
|
|
* @returns {AllowTypes.Data}
|
|
*/
|
|
function AssetParseAllowTypes(allowTypes) {
|
|
/** @type {{ [k in keyof AllowTypes.Data]: Mutable<AllowTypes.Data[k]> }} */
|
|
const ret = {
|
|
TypeToID: {},
|
|
IDToTypeKey: {},
|
|
AllTypes: {},
|
|
};
|
|
if (!CommonIsArray(allowTypes)) {
|
|
allowTypes = [allowTypes];
|
|
}
|
|
|
|
for (const [id, typeRecord] of CommonEnumerate(allowTypes)) {
|
|
for (let [name, indices] of Object.entries(typeRecord)) {
|
|
if (!CommonIsArray(indices)) {
|
|
indices = [indices];
|
|
}
|
|
|
|
for (const i of indices) {
|
|
/** @type {PartialType} */
|
|
const key = `${name}${i}`;
|
|
|
|
const typeToIDSet = ret.TypeToID[key];
|
|
if (typeToIDSet) {
|
|
typeToIDSet.add(id);
|
|
} else {
|
|
ret.TypeToID[key] = new Set([id]);
|
|
}
|
|
|
|
const allTypesSet = ret.AllTypes[name];
|
|
if (allTypesSet) {
|
|
allTypesSet.add(i);
|
|
} else {
|
|
ret.AllTypes[name] = new Set([i]);
|
|
}
|
|
}
|
|
|
|
const idToTypeList = ret.IDToTypeKey[id];
|
|
if (idToTypeList) {
|
|
/** @type {Mutable<idToTypeList>} */(idToTypeList).push(name);
|
|
} else {
|
|
ret.IDToTypeKey[id] = [name];
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Parse the passed {@link AssetDefinition.ParentGroup}.
|
|
* @param {null | undefined | ParentGroup.Definition} parentGroup - The to-be parsed parent group value
|
|
* @param {ParentGroup.Data} superParentGroup - The parsed parent group value of the layer's/asset's super-asset/group
|
|
* @returns {ParentGroup.Data} - The parsed parent group value
|
|
*/
|
|
function AssetParseParentGroup(parentGroup, superParentGroup) {
|
|
if (parentGroup == null) {
|
|
return superParentGroup;
|
|
} else if (typeof parentGroup === "string") {
|
|
return { [PoseType.DEFAULT]: parentGroup };
|
|
} else {
|
|
return parentGroup;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses and validates asset's opacity
|
|
* @param {number|undefined} opacity
|
|
* @param {number} min - The minimum opacity
|
|
* @param {number} max - The maximum opacity
|
|
* @returns {number}
|
|
*/
|
|
function AssetParseOpacity(opacity, min=0, max=1) {
|
|
if (CommonIsNumeric(opacity)) {
|
|
return CommonClamp(opacity, min, max);
|
|
}
|
|
return max;
|
|
}
|
|
|
|
/**
|
|
* Parse the passed alpha mask definitions.
|
|
* @param {undefined | Alpha.Definition[]} alphaDef The unparsed alpha mask definition
|
|
* @param {null | string} warningPrefix - A prefix to-be prepended to any warning messages
|
|
* @returns {null | Alpha.Data[]} The parsed alpha mask data
|
|
*/
|
|
function AssetParseAlpha(alphaDef, warningPrefix=null) {
|
|
return (alphaDef == null) ? null : alphaDef.map(a => {
|
|
/** @type {Mutable<Alpha.Data>} */
|
|
const data = {
|
|
...a,
|
|
Pose: a.Pose ? PoseToMapping.Array(a.Pose, warningPrefix) : null,
|
|
AllowTypes: a.AllowTypes ? AssetParseAllowTypes(a.AllowTypes) : null,
|
|
};
|
|
return data;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse the passed layers alpha mask definitions.
|
|
* @param {AssetLayerDefinition} Layer - The raw layer definition
|
|
* @param {Asset} NewAsset - The raw asset definition
|
|
* @param {number} I - The index of the layer within its asset
|
|
* @return {Alpha.Data[]} - a list of alpha mask data for the layer
|
|
*/
|
|
function AssetParseLayerAlpha(Layer, NewAsset, I) {
|
|
const Alpha = AssetParseAlpha(Layer.Alpha, "Layer.Alpha") || [];
|
|
// If the layer is the first layer for an asset, add the asset's alpha masks
|
|
if (I === 0 && NewAsset.Alpha) {
|
|
Alpha.push(...NewAsset.Alpha);
|
|
}
|
|
return Alpha;
|
|
}
|
|
|
|
/**
|
|
* Assigns color indices to the layers of an asset. These determine which colors get applied to the layer. Also adds
|
|
* a count of colorable layers to the asset definition.
|
|
* @param {Mutable<Asset>} A - The built asset
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function AssetAssignColorIndices(A) {
|
|
var colorIndex = 0;
|
|
/** @type {Record<string, number>} */
|
|
var colorMap = {};
|
|
A.Layer.forEach(Layer => {
|
|
// If the layer can't be colored, we don't need to set a color index
|
|
if (!Layer.AllowColorize) return;
|
|
|
|
var LayerKey = Layer.CopyLayerColor || Layer.Name;
|
|
if (LayerKey === undefined)
|
|
LayerKey = "undefined";
|
|
if (LayerKey === null)
|
|
LayerKey = "null";
|
|
|
|
if (typeof colorMap[LayerKey] !== "number") {
|
|
colorMap[LayerKey] = colorIndex;
|
|
colorIndex++;
|
|
}
|
|
/** @type {Mutable<AssetLayer>} */(Layer).ColorIndex = colorMap[LayerKey];
|
|
});
|
|
A.ColorableLayerCount = colorIndex;
|
|
}
|
|
|
|
/**
|
|
* Builds the asset description from the CSV file
|
|
* @param {IAssetFamily} Family
|
|
* @param {string[][]} CSV
|
|
*/
|
|
function AssetBuildDescription(Family, CSV) {
|
|
|
|
/** @type {Map<string, string>} */
|
|
const map = new Map();
|
|
|
|
for (const line of CSV) {
|
|
if (Array.isArray(line) && line.length === 3) {
|
|
if (map.has(`${line[0]}:${line[1]}`)) {
|
|
console.warn("Duplicate Asset Description: ", line);
|
|
}
|
|
map.set(`${line[0]}:${line[1]}`, line[2].trim());
|
|
} else {
|
|
console.warn("Bad Asset Description line: ", line);
|
|
}
|
|
}
|
|
|
|
// For each asset group in family
|
|
for (const G of AssetGroup) {
|
|
if (G.Family !== Family)
|
|
continue;
|
|
|
|
const res = map.get(`${G.Name}:`);
|
|
/** @type {Mutable<AssetGroup>} */(G).Description = (res !== undefined) ? res : `MISSING ASSETGROUP DESCRIPTION: ${G.Name}`;
|
|
}
|
|
|
|
// For each asset in the family
|
|
for (const A of Asset) {
|
|
if (A.Group.Family !== Family)
|
|
continue;
|
|
|
|
const res = map.get(`${A.DynamicGroupName}:${A.Name}`);
|
|
/** @type {Mutable<Asset>} */(A).Description = (res !== undefined) ? res : `MISSING ASSET DESCRIPTION: ${A.DynamicGroupName}:${A.Name}`;
|
|
}
|
|
|
|
// Translates the descriptions to a foreign language
|
|
TranslationAsset(Family);
|
|
|
|
}
|
|
|
|
/**
|
|
* Loads the description of the assets in a specific language
|
|
* @param {IAssetFamily} Family The asset family to load the description for
|
|
*/
|
|
function AssetLoadDescription(Family) {
|
|
|
|
// Finds the full path of the CSV file to use cache
|
|
var FullPath = "Assets/" + Family + "/" + Family + ".csv";
|
|
if (CommonCSVCache[FullPath]) {
|
|
AssetBuildDescription(Family, CommonCSVCache[FullPath]);
|
|
return;
|
|
}
|
|
|
|
// Opens the file, parse it and returns the result it to build the dialog
|
|
CommonGet(FullPath, function () {
|
|
if (this.status == 200) {
|
|
CommonCSVCache[FullPath] = CommonParseCSV(this.responseText);
|
|
AssetBuildDescription(Family, CommonCSVCache[FullPath]);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* Loads a specific asset file
|
|
* @param {readonly AssetGroupDefinition[]} Groups
|
|
* @param {IAssetFamily} Family
|
|
* @param {ExtendedItemMainConfig} ExtendedConfig
|
|
*/
|
|
function AssetLoad(Groups, Family, ExtendedConfig) {
|
|
/**
|
|
* Pass one: Resolve all string-based asset definitions and convert the entire thing into a
|
|
* nested record mapping group names, to asset names, to asset definitions.
|
|
* @type {Record<AssetGroupName, Record<string, AssetDefinition>>}
|
|
*/
|
|
const assetDefsParsed = CommonFromEntries(Groups.map(g => {
|
|
return [
|
|
g.Group,
|
|
CommonFromEntries(g.Asset.map(a => {
|
|
/** @type {[name: string, config: AssetDefinition]} */
|
|
const ret = typeof a === "string" ? [a, { Name: a }] : [a.Name, a];
|
|
return ret;
|
|
})),
|
|
];
|
|
}));
|
|
|
|
// Pass two: handle all copy configs
|
|
for (const [groupName, assetDefs] of CommonEntries(assetDefsParsed)) {
|
|
for (const [assetName, assetDef] of Object.entries(assetDefs)) {
|
|
const assetDefNew = AssetResolveCopyConfig.AssetDefinition(assetDef, groupName, assetDefsParsed);
|
|
if (assetDefNew == null) {
|
|
delete assetDefs[assetName];
|
|
} else {
|
|
assetDefs[assetName] = assetDefNew;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass three: finally parse the asset/group defs
|
|
for (const groupDef of Groups) {
|
|
const G = AssetGroupAdd(Family, groupDef);
|
|
for (const assetDef of Object.values(assetDefsParsed[G.Name])) {
|
|
AssetAdd(G, assetDef, ExtendedConfig);
|
|
}
|
|
}
|
|
|
|
AssetLoadDescription(Family);
|
|
}
|
|
|
|
// Reset and load all the assets
|
|
function AssetLoadAll() {
|
|
Asset = [];
|
|
AssetGroup = [];
|
|
Pose = PoseFemale3DCG;
|
|
Pose.forEach(p => PoseRecord[p.Name] = p);
|
|
AssetLoad(AssetFemale3DCG, "Female3DCG", AssetFemale3DCGExtended);
|
|
CraftingAssets = CraftingAssetsPopulate();
|
|
ExtendedItemManualRegister();
|
|
PropertyAutoPunishHandled = new Set(AssetGroup.map((a) => a.Name));
|
|
Shop2._PopulateBuyGroups();
|
|
Shop2._PopulateKeysAndRemotes();
|
|
}
|
|
|
|
/**
|
|
* Gets a specific asset by family/group/name
|
|
* @param {IAssetFamily} Family - The family to search in (Ignored until other family is added)
|
|
* @param {AssetGroupName} Group - Name of the group of the searched asset
|
|
* @param {string} Name - Name of the searched asset
|
|
* @returns {Asset | null}
|
|
*/
|
|
function AssetGet(Family, Group, Name) {
|
|
return AssetMap.get(`${Group}/${Name}`) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Gets all activities on a family and name
|
|
* @param {IAssetFamily} family - The family to search in
|
|
* @returns {Activity[]}
|
|
*/
|
|
function AssetAllActivities(family) {
|
|
if (family == "Female3DCG")
|
|
return ActivityFemale3DCG;
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Gets an activity asset by family and name
|
|
* @param {IAssetFamily} family - The family to search in
|
|
* @param {string} name - Name of activity to search for
|
|
* @returns {Activity|null}
|
|
*/
|
|
function AssetGetActivity(family, name) {
|
|
return AssetAllActivities(family).find(a => (a.Name === name)) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get the list of all activities on a group for a given family.
|
|
*
|
|
* @description Note that this just returns activities as defined, no checks are
|
|
* actually done on whether the activity makes sense.
|
|
*
|
|
* @param {IAssetFamily} family
|
|
* @param {AssetGroupName} groupname
|
|
* @param {"self" | "other" | "any"} onSelf
|
|
* @returns {Activity[]}
|
|
*/
|
|
function AssetActivitiesForGroup(family, groupname, onSelf = "other") {
|
|
const activities = AssetAllActivities(family);
|
|
/** @type {Activity[]} */
|
|
const defined = [];
|
|
activities.forEach(a => {
|
|
/** @type {string[] | undefined} */
|
|
let targets;
|
|
// Get the correct target list
|
|
if (onSelf === "self") {
|
|
targets = (typeof a.TargetSelf === "boolean" ? a.Target : a.TargetSelf);
|
|
} else if (onSelf === "any") {
|
|
targets = a.Target;
|
|
if (Array.isArray(a.TargetSelf))
|
|
targets = targets.concat(a.TargetSelf);
|
|
} else {
|
|
targets = a.Target;
|
|
}
|
|
if (targets && targets.includes(groupname))
|
|
defined.push(a);
|
|
});
|
|
return defined;
|
|
}
|
|
|
|
/**
|
|
* Gets an asset group by the asset family name and group name
|
|
* @template {AssetGroupName} T
|
|
* @param {IAssetFamily} Family - The asset family that the group belongs to (Ignored until other family is added)
|
|
* @param {T} Group - The name of the asset group to find
|
|
* @returns {AssetGroupMapping[T] | null} - The asset group matching the provided family and group name
|
|
*/
|
|
function AssetGroupGet(Family, Group) {
|
|
return /** @type {AssetGroupMapping[T]} */(AssetGroupMap.get(Group)) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Utility function for retrieving the preview image directory path for an asset
|
|
* @param {Asset} A - The asset whose preview path to retrieve
|
|
* @returns {string} - The path to the asset's preview image directory
|
|
*/
|
|
function AssetGetPreviewPath(A) {
|
|
return `Assets/${A.Group.Family}/${A.DynamicGroupName}/Preview`;
|
|
}
|
|
|
|
/**
|
|
* Utility function for retrieving the base path of an asset's inventory directory, where extended item scripts are
|
|
* held
|
|
* @param {Asset} A - The asset whose inventory path to retrieve
|
|
* @returns {string} - The path to the asset's inventory directory
|
|
*/
|
|
function AssetGetInventoryPath(A) {
|
|
return `Screens/Inventory/${A.DynamicGroupName}/${A.Name}`;
|
|
}
|
|
|
|
/**
|
|
* Sort a list of asset layers for the {@link Character.AppearanceLayers } property.
|
|
* Performs an inplace update of the passed array and then returns it.
|
|
* @param {AssetLayer[]} layers - The to-be sorted asset layers
|
|
* @returns {AssetLayer[]} - The newly sorted asset layers
|
|
*/
|
|
function AssetLayerSort(layers) {
|
|
return layers.sort((l1, l2) => {
|
|
// If priorities are different, sort by priority
|
|
if (l1.Priority !== l2.Priority) return l1.Priority - l2.Priority;
|
|
// If the priorities are identical and the layers belong to the same Asset, ensure layer order is preserved
|
|
if (l1.Asset === l2.Asset) return l1.Asset.Layer.indexOf(l1) - l1.Asset.Layer.indexOf(l2);
|
|
// If priorities are identical, first try to sort by group name
|
|
if (l1.Asset.Group !== l2.Asset.Group) return l1.Asset.Group.Name.localeCompare(l2.Asset.Group.Name);
|
|
// If the groups are identical, then sort by asset name - this shouldn't actually be possible unless you've
|
|
// somehow equipped two different assets from the same group, but use it as an if-the-unexpected-happens
|
|
// fallback.
|
|
return l1.Asset.Name.localeCompare(l2.Asset.Name);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert {@link AssetDefinition} default color into a {@link Asset} default color list
|
|
* @param {number} colorableLayerCount The number of colorable layers
|
|
* @param {string} fillValue The default color. Usually `"Default"` though skin colors can also be supplied on occasion.
|
|
* @param {string | readonly string[]} [color] See {@link AssetDefinition.DefaultColor}
|
|
* @returns {string[]} See {@link Asset.DefaultColor}
|
|
*/
|
|
function AssetParseDefaultColor(colorableLayerCount, fillValue, color) {
|
|
/** @type {string[]} */
|
|
const defaultColor = Array(colorableLayerCount).fill(fillValue);
|
|
if (typeof color === "string") {
|
|
defaultColor.fill(color);
|
|
} else if (CommonIsArray(color)) {
|
|
color.slice(0, colorableLayerCount).forEach((c, i) => defaultColor[i] = c);
|
|
}
|
|
return defaultColor;
|
|
}
|
|
|
|
const AssetStringsPath = "Assets/Female3DCG/AssetStrings.csv";
|
|
|
|
/**
|
|
* Get the translated string for an asset-specific message
|
|
* @param {string} msg
|
|
* @returns {string}
|
|
*/
|
|
function AssetTextGet(msg) {
|
|
const repo = TextAllScreenCache.get(AssetStringsPath);
|
|
if (!repo) return "";
|
|
return repo.get(msg);
|
|
}
|
|
|
|
/**
|
|
* Validates that the InventoryID is setup properly in the Female3DCG assets
|
|
* Launched each time the game is started for assets maker to apply corrections
|
|
* Outputs all possible errors in the console log, it runs aynscronious
|
|
*/
|
|
async function AssetInventoryIDValidate() {
|
|
|
|
// Skips these validations in production environments
|
|
let URL = window.location.href.toLowerCase();
|
|
if (URL.includes("bondageprojects.elementfx.com")) return;
|
|
if (URL.includes("bondageprojects.com")) return;
|
|
if (URL.includes("bondage-europe.com")) return;
|
|
|
|
const allAssets = AssetFemale3DCG.reduce((assets, group) => {
|
|
const objAssets = /** @type {AssetDefinition[]} */ (group.Asset.filter(a => CommonIsObject(a)));
|
|
return assets.concat(objAssets);
|
|
}, /** @type {(AssetDefinition)[]} */ ([]));
|
|
|
|
const realLastID = Math.max(...allAssets.map(a => a.InventoryID ?? 0 ));
|
|
let lastId = realLastID;
|
|
|
|
/** @type {Map<string, AssetDefinition[]>} */
|
|
const buyGroups = new Map();
|
|
/** @type {AssetDefinition[][]} */
|
|
// A bit cursed; this is a sparse array because we want to track the holes
|
|
const inventoryIDs = [];
|
|
for (const A of allAssets) {
|
|
const isFreeOrUnbuyableAsset = (A.Value == null || A.Value === 0 || A.Enable === false);
|
|
|
|
if (A.InventoryID && isFreeOrUnbuyableAsset) {
|
|
console.warn(`Unnecessary InventoryID on asset "${A.Name}", remove it`);
|
|
} else if (!A.InventoryID && !isFreeOrUnbuyableAsset && !A.BuyGroup && A.Value !== -1 && !A.RemoveAtLogin) {
|
|
// Additional checks are for scenario-only items
|
|
lastId++;
|
|
console.warn(`Missing InventoryID on asset "${A.Name}", suggesting ${lastId}`);
|
|
}
|
|
|
|
// Warn for all Inventory IDs that are duplicated out of a buy group
|
|
if (A.BuyGroup && !isFreeOrUnbuyableAsset) {
|
|
const buyAssets = buyGroups.get(A.BuyGroup) ?? [];
|
|
let match;
|
|
if ((match = buyAssets.find(asset => asset.InventoryID !== A.InventoryID))) {
|
|
lastId++;
|
|
console.warn(`InventoryID should be the same within BuyGroup ${A.BuyGroup}, ${A.Name} (${A.InventoryID}) is different: ${match.Name} (${match.InventoryID}), suggesting ${lastId}`);
|
|
} else if ((match = allAssets.find(asset => asset.InventoryID === A.InventoryID && asset.BuyGroup !== A.BuyGroup))) {
|
|
console.warn(`InventoryID matching without being in the same BuyGroup ${A.BuyGroup}, ${A.Name} (${A.InventoryID}) is different: ${match.Name} (${match.InventoryID})`);
|
|
}
|
|
buyAssets.push(A);
|
|
buyGroups.set(A.BuyGroup, buyAssets);
|
|
}
|
|
|
|
// Warn for all Inventory ID conflicts between buy groups
|
|
if (A.InventoryID && !isFreeOrUnbuyableAsset) {
|
|
const inventoryAssets = inventoryIDs[A.InventoryID] ?? [];
|
|
let match;
|
|
// Find matches with different names that either have no buygroup or aren't part of the same buygroup
|
|
if ((match = inventoryAssets.find(asset => (!asset.BuyGroup || !A.BuyGroup || asset.BuyGroup !== A.BuyGroup) && asset.Name !== A.Name))) {
|
|
lastId++;
|
|
console.warn(`InventoryID should be different between different BuyGroups: asset "${A.Name}", buyGroup: ${A.BuyGroup}, ID ${A.InventoryID}, doesn't match: "${match.Name}", buyGroup: ${match.BuyGroup}, ID: ${match.InventoryID}, suggesting ${lastId}`);
|
|
}
|
|
inventoryAssets.push(A);
|
|
inventoryIDs[A.InventoryID] = inventoryAssets;
|
|
}
|
|
}
|
|
const debugHoles = false;
|
|
if (debugHoles) {
|
|
/** @type {Set<number>} */
|
|
const holes = new Set();
|
|
for (let index = 100; index < inventoryIDs.length; index++) {
|
|
if (!inventoryIDs[index]) holes.add(index);
|
|
}
|
|
console.warn(`Known holes in InventoryIDs: ${[...holes.values()]}`);
|
|
}
|
|
}
|