Browse Source

[SugarCube] Add wardrobe-class and type definitions

Stephan Fuchs 2 months ago
parent
commit
b02e6df435

+ 57 - 0
qsrc2tw/twine-code/core/wardrobe/OutfitItem.ts

@@ -0,0 +1,57 @@
+class OutfitItem extends GameObject{
+	//#region Static Data
+		private _id: string = undefined;
+		public get id(){
+			return this._id;
+		}
+		public set id(v){
+			if(this._id)
+				throw new Error("The ID of an initialized OutfitItem must not be changed.");
+			if(!setup.wardrobeStatic[v])
+				throw new Error(`The chosen OutfitItem-ID does not exist: '${v}'`);
+			this._id = v;
+		}
+
+		get staticData(){
+			return setup.wardrobeStatic[this.id];
+		}
+	//#endregion
+
+	get image():string{
+		const imageFromStaticData = this.staticData.image as string;
+		if(imageFromStaticData)
+			return imageFromStaticData;
+		const shopData = setup.outfitShop[this.staticData.shop] ?? {};
+		if(shopData.outfitItems?.image)
+			return shopData.outfitItems.image(this.id);
+		return undefined;
+	}
+
+	get slots():string[]{
+		return ['clothes'];
+	}
+
+	get type(){
+		return this.staticData['type'];
+	}
+
+	//#region SYSTEM
+		constructor(id:string=undefined){
+			super();
+			if(id)
+				this.id = id;
+		}
+
+		clone = function () {
+			return (new setup.OutfitItem())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.OutfitItem())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+
+
+}
+
+setup.OutfitItem = OutfitItem;

+ 19 - 0
qsrc2tw/twine-code/core/wardrobe/OwnedOutfitItem.ts

@@ -0,0 +1,19 @@
+class OwnedOutfitItem extends OutfitItem{
+
+
+    //#region SYSTEM
+        constructor(id:string=undefined){
+			super(id);
+		}
+
+		clone = function () {
+			return (new setup.OwnedOutfitItem())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.OwnedOutfitItem())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+}
+
+setup.OwnedOutfitItem = OwnedOutfitItem;

+ 287 - 0
qsrc2tw/twine-code/core/wardrobe/Wardrobe.ts

@@ -0,0 +1,287 @@
+
+type ItemReference = string | OutfitItem;
+
+function outfitItemId(item:ItemReference){
+	if(typeof item == "string")
+		return item;
+	if(typeof item == "object")
+		return item.id;
+	throw new Error("Invalid Argument");
+}
+
+type FilterData = {[fieldId:string]:number|string|boolean|{
+	max?: number;
+	min?: number;
+}}
+
+class Filter{
+	private _filterData = [];
+	public get filterData():FilterData{
+		return Object.fromEntries(this._filterData);
+	}
+	private set filterData(v:FilterData){
+		function easeOfChecking(val:any):number{
+			switch(typeof val){
+				case 'boolean': return 0;
+				case 'number': return 1;
+				case 'string': return 3;
+				case 'object': return 10;
+				default:
+					return 100;
+			}
+		}
+		this._filterData = Object.entries(v).toSorted((a,b)=>easeOfChecking(a) - easeOfChecking(b));
+	}
+	constructor(filterData: FilterData){
+		this.filterData = filterData;
+	}
+
+	public passes(val:any){
+		if(typeof val != "object")
+			return false;
+		for(const [fieldId, checkValue] of this._filterData){
+			if(val[fieldId] === undefined)
+				return false;
+			switch(typeof checkValue){
+				case "string":
+				case "number":
+				case "boolean":
+					if(val[fieldId] !== checkValue)
+						return false;
+					break;
+				case "object":
+					throw new Error("NOT IMPLEMENTED");
+			}
+		}
+		return true;
+	}
+}
+
+class Wardrobe extends GameObject{
+
+	/*private $itemProxy=undefined;
+	get item(){
+		this.$itemProxy ??= new Proxy(this, {
+			get(wardrobeObject, staticItemId){
+				if(typeof staticItemId == "symbol")
+					return undefined;
+				if(!setup.wardrobeStatic[staticItemId])
+					return undefined;
+
+				return new Proxy(new WardrobeItem(staticItemId),{
+					get(wardrobeItem, propertyKey){
+						if(typeof propertyKey == "symbol")
+							return undefined;
+						return wardrobeItem[propertyKey];
+					},
+					set(wardrobeItem, propertyKey, v){
+						return false;
+					}
+				});
+			},
+			ownKeys(wardrobeObject) {
+				return Object.keys(wardrobeObject.ownedItemData);
+			},
+		}) as unknown as any;
+		return this.$itemProxy;
+	}
+
+	private ownedItemData:{[key:string]: OwnedWardrobeItemData} = {};
+	private $ownedItemProxy=undefined;
+	get owned(){
+		//The outer proxy catches all requests in the form of $Wardrobe.owned["itemId"].
+		//A proxy is used here to intercept calls that don't refer to itemIds, such as $Wardrobe.owned["*Coat"].
+		this.$ownedItemProxy ??= new Proxy(this, {
+			get(wardrobeObject, itemId){
+				if(typeof itemId == "symbol")
+					return undefined;
+
+				let itemIsOwned = true;
+
+				if(!wardrobeObject.ownedItemData[itemId]){
+					if(itemId.startsWith('*')){
+						const slot = itemId.substring(1);
+						if(this.worn[slot])
+							itemId = wardrobeObject.worn[slot];
+						else
+							return undefined;
+					}else if(setup.wardrobeStatic[itemId]){
+						const itemsOwnedByItemId = Object.entries(wardrobeObject.ownedItemData).filter(([id,data])=>data.itemId == itemId).map(([id,data])=>id);
+						if(!itemsOwnedByItemId.length){
+							itemIsOwned = false;
+						}else if(itemsOwnedByItemId.length == 1){
+							itemId = itemsOwnedByItemId[0];
+						}else{
+							const wornItemIds = Object.values(wardrobeObject.worn);
+							const itemsWorn = itemsOwnedByItemId.filter(itemId=>wornItemIds.includes(itemId));
+							if(itemsWorn.length != 1)
+								throw new Error(`Ambiguous wardrobe item access for id '${itemId}'`);
+							itemId = itemsWorn[0];
+						}
+					}else{
+						return undefined;
+					}
+				}
+
+				if(!itemIsOwned)
+					return undefined; //Maybe handle this differently in the future.
+
+				return new Proxy(new OwnedWardrobeItem(itemId, wardrobeObject.ownedItemData[itemId]),{
+					get(ownedWardrobeItem, propertyKey){
+						if(typeof propertyKey == "symbol")
+							return undefined;
+						return ownedWardrobeItem.get(propertyKey);
+					},
+					set(ownedWardrobeItem, propertyKey, v){
+						if(typeof propertyKey == "symbol")
+							return undefined;
+						return ownedWardrobeItem.set(propertyKey,v);
+					}
+				});
+			},
+			ownKeys(wardrobeObject) {
+				return Object.keys(wardrobeObject.ownedItemData);
+			},
+		}) as unknown as any;
+		return this.$ownedItemProxy;
+	}
+
+	get ownedItems(){
+		return Object.fromEntries(Object.entries(this.ownedItemData).map(([inventoryId, ownedItemData])=>this.owned[inventoryId]));
+	}
+
+	public get Bra(){return this.owned["*Bra"]};
+	public get Clothes(){return this.owned["*Clothes"]};
+	public get Coat(){return this.owned["*Coat"]};
+	public get Panties(){return this.owned["*Panties"]};
+	public get Purse(){return this.owned["*Purse"]};
+	public get Shoes(){return this.owned["*Shoes"]};
+
+	private worn: {[slotId:string]: string} = {};
+
+	public unwear(itemId:string){
+		const itemData = this.item[itemId];
+		const slots = itemData.slots;
+		for(const slot of slots){
+			this.worn[slot] = undefined;
+		}
+	}
+
+	public unwearBySlot(slot:string){
+		if(this.worn[slot])
+			this.unwear(this.worn[slot]);
+	}
+
+	public wear(itemId){
+		const itemData = this.owned[itemId];
+		const slots = itemData.slots;
+		for(const slot of slots){
+			this.unwearBySlot(slot);
+		}
+	}*/
+
+	//#region Ownership
+		private _ownedItems:{[itemId:string]:OwnedOutfitItem} = {};
+		public get ownedItems(){
+			return this._ownedItems;
+		}
+
+		public ownedItemsFiltered(filter:FilterData){
+			const f = new Filter(filter);
+			return Object.fromEntries(
+				Object.entries(this._ownedItems).
+					filter(
+						([itemId,item])=>f.passes(item)
+					)
+			);
+		}
+
+		public add(item: ItemReference){
+			try{
+				const itemId = outfitItemId(item);
+				this._ownedItems[itemId] = new setup.OwnedOutfitItem(itemId);
+				console.log("Wardrobe Added",itemId);
+			}
+			catch(e){
+				console.error(e);
+
+			}
+		}
+
+		public delete(item: ItemReference){
+			const itemId = outfitItemId(item);
+			delete this._ownedItems[itemId];
+			console.log("Wardrobe Deleted",itemId);
+		}
+
+		public ownsItem(item: ItemReference){
+			const itemId = outfitItemId(item);
+			return Object.keys(this._ownedItems).includes(itemId);
+		}
+	//#endregion
+
+	//#region Worn
+		private _wornItemIds:{[slotId:string]:string} = {};
+		public get wornItems():{[slotId:string]:OutfitItem}{
+			return Object.fromEntries(
+				Object.keys(this._wornItemIds)
+					.map(
+						slotId=>[
+							slotId,
+							this.wornAtSlot(slotId)
+						]
+					)
+			);
+		}
+
+		public wornAtSlot(slotId:string):OutfitItem|null{
+			const itemId = this._wornItemIds[slotId];
+			if(!itemId)
+				return null;
+			return this.getItemOwnedOrStatic(itemId);
+		}
+
+		public unwear(item: ItemReference){
+			if(!item) return;
+			const itemId = outfitItemId(item);
+			const itemData = this.getItemOwnedOrStatic(itemId);
+			for(const slot of itemData.slots)
+				this._wornItemIds[slot] = null;
+		}
+
+		public wear(item: ItemReference){
+			if(!item) return;
+			const itemId = outfitItemId(item);
+			const itemData = this.getItemOwnedOrStatic(itemId);
+			for(const slot of itemData.slots){
+				const itemAtSlot = this.wornAtSlot(slot);
+				if(itemAtSlot)
+					this.unwear(itemAtSlot);
+			}
+		}
+	//#endregion
+
+	//#region Data
+
+		private getItemOwnedOrStatic(itemId:string):OutfitItem{
+			if(this._ownedItems[itemId])
+				return this._ownedItems[itemId];
+			return new setup.OutfitItem(itemId);
+		}
+
+	//#endregion
+
+
+
+	//#region SYSTEM
+		clone = function () {
+			return (new setup.Wardrobe())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.Wardrobe())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+}
+
+setup.Wardrobe = Wardrobe;

+ 20 - 0
qsrc2tw/twine-code/twine-code.d.ts

@@ -14,6 +14,24 @@ declare module "twine-sugarcube" {
             };
         //#endregion
 
+        //#region Static Wardrobe
+            OutfitItem: {new(itemId?:string): OutfitItem};
+            OwnedOutfitItem: {new(itemId?:string): OwnedOutfitItem};
+
+            outfitShop: {[shopId: string]: {
+                outfitItems?: {
+                    image?: (key:string)=>string;
+                }
+            }};
+
+            Wardrobe: {new(): Wardrobe};
+            wardrobeStatic: {[wardrobeItemId:string]: {[propertyKey:string]:(string|number)}};
+        //#endregion
+
+        //#region Wardrobe Overwrites
+            wardrobeItemVars: {[type:string]:string[]};
+        //#endregion
+
         /**
          * Provides functions to overwrite behavior of functions and variables
          */
@@ -86,6 +104,8 @@ declare module "twine-sugarcube" {
         npcDynamic: {[npcID:string]: {[field:string]:any}};
 
         npcs: NPCs;
+
+        wardrobe: Wardrobe;
     }
 }