Explorar o código

Merge remote-tracking branch 'Steuchs_bug_hunt/QSRC2TW'

Kevin_Smarts hai 3 meses
pai
achega
467c538cbe

+ 1 - 1
locations/stat_sklattrib.qsrc

@@ -1,5 +1,5 @@
 # stat_sklattrib
-
+!! SKIP_QSRC2TW
 !!********************  Warning!  ********************
 !!The code in this location is both complex and very fundamental to most systems!
 !!Use EXTREME caution if modifying!

+ 1 - 1
locations/stat_sklattrib_lvlset.qsrc

@@ -1,5 +1,5 @@
 # stat_sklattrib_lvlset
-
+!! QSRC2TW_module stat_sklattrib_lvlset
 !!--------------- Attribute set section ------------------------
 !!This is where an attribute pcs_"name" is set by "name"_lvl and any adjustments are added
 !!Even though most of these could be done without doing a _lvl to pcs_ conversion, doing so is future proofing

+ 79 - 26
qsrc2tw/tools/QSRC2TW/index.js

@@ -1,4 +1,6 @@
 import qsrc2tw from "./src/qsrc2tw.js";
+import npcInit from "./src/npcInit.js";
+import skillDefinitions from "./src/skillDefinitions.js";
 
 import {glob} from 'glob';
 import fs from 'node:fs';
@@ -104,44 +106,74 @@ async function convertFile(filePath){
                 return;
             }
             const startTime = (new Date()).getTime();
-            const codeHash = md5(data);
+
             const baseFileNameStr = baseFileName(filePath);
             const outFilePath = path.join(outPath,generatedFilesPrefix,baseFileNameStr.split('.')[0]+'.tw');
-
-            if((data.split('\n')?.[1]).startsWith("!! SKIP_QSRC2TW")){
+            const outFilePathTS = path.join(outPath,generatedFilesPrefix,baseFileNameStr.split('.')[0]+'.ts');
+
+            const qsp2twOptions = data.split('\n')?.[1];
+            if(qsp2twOptions.startsWith("!! SKIP_QSRC2TW")){
+                if(fs.existsSync(outFilePath))
+                    fs.unlink(outFilePath,(err) => {if (err) throw err;});
+                if(fs.existsSync(outFilePathTS))
+                    fs.unlink(outFilePathTS,(err) => {if (err) throw err;});
                 resolve("SKIP");
                 return;
             }
 
-            if (fs.existsSync(outFilePath)) {
 
-                try{
-                    const secondLineData = fs.readFileSync(outFilePath, "utf-8").split('\n')[1];
-                    const qsrc2twResultMatch = secondLineData.match(/<!--\s*qsrc2twResult=({.*})\s*-->/);
-                    if(qsrc2twResultMatch){
-                        const qsrc2twResult = JSON.parse(qsrc2twResultMatch[1]);
-
-                        if((qsrc2twResult.code && codeHash == qsrc2twResult.code) &&
-                            (qsrc2twResult.version && VERSION == qsrc2twResult.version)){
-                                resolve("EXISTS");
-                                return;
+
+            //#region Skip File Output if outfile exists, is based on the same codebase (determined by hash) and has used the same compier-version
+                const codeHash = md5(data);
+                if (fs.existsSync(outFilePath)) {
+
+                    try{
+                        const secondLineData = fs.readFileSync(outFilePath, "utf-8").split('\n')[1];
+                        const qsrc2twResultMatch = secondLineData.match(/<!--\s*qsrc2twResult=({.*})\s*-->/);
+                        if(qsrc2twResultMatch){
+                            const qsrc2twResult = JSON.parse(qsrc2twResultMatch[1]);
+
+                            if((qsrc2twResult.code && codeHash == qsrc2twResult.code) &&
+                                (qsrc2twResult.version && VERSION == qsrc2twResult.version)){
+                                    resolve("EXISTS");
+                                    return;
+                            }
                         }
                     }
+                    catch(e){
+                        //Something unexpected happens. No need to handle this, because we'll just run the default file-processing.
+                    }
                 }
-                catch(e){
+            //#endregion
 
-                }
+            var convertMode = "default";
+
+            if(qsp2twOptions.startsWith("!! QSRC2TW_module")){
+                convertMode = qsp2twOptions.trim().split(" ").toReversed()[0];
+            }
+
+
+            /**
+             * Return value is Array [TwineCode, TSCode]. TwineCode must not be null.
+             */
+            var convertFunction = (code)=>[null,null];
+            switch (convertMode) {
+                case "default": convertFunction = (code) => [qsrc2tw(code, true),null]; break;
+                case "npcInit": convertFunction = (code) => npcInit(code); break;
+                case "stat_sklattrib_lvlset": convertFunction = (code) => skillDefinitions(code); break;
+                default:
+                    console.warn("Unreckognized Convert Mode");
+                    break;
             }
 
 
             try{
                 let twineCode = "";
                 consoleActive = options.verboseErrors;
+                let twineCodeRaw = undefined;
+                let tsCodeRaw = undefined;
                 try{
-                    twineCode = qsrc2tw(data, true)
-                                        .split('\n')
-                                        .toSpliced(1,0,`<!--qsrc2twResult={"version":${VERSION},"code":"${codeHash}","time":"${(new Date().toISOString())}"}-->`)
-                                        .join('\n');
+                    [twineCodeRaw,tsCodeRaw] = convertFunction(data);
                 }
                 catch(e){
                     throw e;
@@ -149,13 +181,34 @@ async function convertFile(filePath){
                 finally{
                     consoleActive = true;
                 }
+                if(!twineCodeRaw){
+                    console.error("Twine Code must be generated by every converMode");
+                    reject("Invalid convertFunction");
+                    return;
+                }else{
+                    twineCode = twineCodeRaw.split('\n')
+                                    .toSpliced(1,0,`<!--qsrc2twResult={"version":${VERSION},"code":"${codeHash}","time":"${(new Date().toISOString())}"}-->`)
+                                    .join('\n');
+                    fs.writeFile(outFilePath, twineCode, err => {
+                        if (err) {
+                            console.error(err);
+                        } else {
+                        }
+                    });
+                }
 
-                fs.writeFile(outFilePath, twineCode, err => {
-                    if (err) {
-                        console.error(err);
-                    } else {
-                    }
-                });
+
+                if(!tsCodeRaw){
+                    if(fs.existsSync(outFilePathTS))
+                        fs.unlink(outFilePathTS,(err) => {if (err) throw err;});
+                }else{
+                    fs.writeFile(outFilePathTS, tsCodeRaw, err => {
+                        if (err) {
+                            console.error(err);
+                        } else {
+                        }
+                    });
+                }
 
                 const executionTime =  (new Date()).getTime() - startTime;
 

+ 78 - 0
qsrc2tw/tools/QSRC2TW/src/npcInit.js

@@ -0,0 +1,78 @@
+import qsrc2tw from "./qsrc2tw.js";
+
+export default function npcInit(code){
+    let firstLine = code.split("\n")[0];
+    let twCode = firstLine.replace("#","::")+`\n<<run console.warn("Use of npcInit-passages is deprecated (${firstLine})")>>`;
+
+    let tsCode = "setup.npcStatic ??= {};\n";
+
+    let npcIDs = [];
+    const npcStartRegex = /npctemp\s*=\s*(\d+)/gd;
+    let result;
+    let previousEndIndex = 0;
+    let previousNPCId = 0;
+
+    let remainingQsrc = "";
+    while(result = npcStartRegex.exec(code)) {
+        const npcId = result[1];
+        npcIDs.push("A"+npcId);
+        let thisEndIndex = result.indices[0][1];
+        let thisStartIndex = result.indices[0][0];
+        if(previousEndIndex){
+            const [npcObj, remainingCode] = parseNpcInitCommand(code,previousEndIndex,thisStartIndex);
+            tsCode += `setup.npcStatic['A${previousNPCId}'] = ${npcObj}\n\n`;
+            remainingQsrc += remainingCode+"\n";
+        }
+
+        previousNPCId = npcId;
+        previousEndIndex = thisEndIndex;
+
+    }
+
+    let thisStartIndex = code.length;
+    const [npcObj, remainingCode] = parseNpcInitCommand(code,previousEndIndex,thisStartIndex);
+    tsCode += `setup.npcStatic['A${previousNPCId}'] = ${npcObj}\n\n`;
+    remainingQsrc += remainingCode;
+
+    //twCode += qsrc2tw(remainingQsrc,false);
+
+    return [twCode, tsCode];
+}
+
+function parseNpcInitCommand(code, startIndex, endIndex){
+    const npcCode = code.substring(startIndex,endIndex);
+    let npc = {_defaults:[]};
+
+    let codeThatNeedInitialization = [];
+
+    const fieldRegex = /\$?npc_(\w*)\['A<<npctemp>>'\]\s*=\s*(.*)/g;
+    let result;
+    while(result = fieldRegex.exec(npcCode)){
+        let field = result[1];
+        let val = result[2];
+        const valAsNumber = Number(val);
+        if(Number.isInteger(valAsNumber))
+            val = valAsNumber;
+        else if(val.length > 1 && val.startsWith("'") && val.endsWith("'")){
+            val = val.substring(1,val.length-1).replaceAll("''","'");
+            if(val.indexOf("<<") != -1){
+                codeThatNeedInitialization.push(result[0]);
+                continue;
+            }
+        }
+        else{ // Most likely a variable name or a function call.
+            //field = "$"+field;
+            codeThatNeedInitialization.push(result[0]);
+            continue;
+        }
+
+        npc[field] = val;
+
+    }
+
+    const defaultsRegex = /gs\s+'npcstaticdefaults'\s*,\s*'(.+)'/g;
+    while(result = defaultsRegex.exec(npcCode))
+        npc._defaults.push(result[1].replaceAll("'","").replaceAll(" ","").replaceAll(",","_"));
+
+    return [JSON.stringify(npc),codeThatNeedInitialization.join('\n')];
+}

+ 55 - 0
qsrc2tw/tools/QSRC2TW/src/skillDefinitions.js

@@ -0,0 +1,55 @@
+export default function skillDefinitions(code){
+    let firstLine = code.split("\n")[0];
+    let twCode = firstLine.replace("#","::")+`\n<<run console.warn("Use of skillDefinitions-passages is deprecated (${firstLine})")>>`;
+
+    let tsCode = "setup.skills ??= {};\n";
+
+    const attributeRegex = /\$att_name\[(\d+)\] = '(\w+)'/gd;
+
+    const attributes = [];
+
+    let result;
+    while(result = attributeRegex.exec(code)) {
+        attributes[Number(result[1])] = result[2];
+    }
+
+
+    const skilToAttributeEffectRegex = /(\w+)\[1\]\s*=\s*(\d)(?:\s*&\s*\1\[\d+\]\s*=\s*(\d+))*/gd;
+    while(result = skilToAttributeEffectRegex.exec(code)) {
+        const skillId = result[1];
+        const arrayIndizes = result[0].split("&").map((singleCommand)=>attributes[Number(singleCommand.split("=")[1].trim())]);
+        tsCode += `setup.skills['${skillId}'] = {attributes:${JSON.stringify(arrayIndizes)}}\n`;
+    }
+
+    tsCode += "setup.proficiencies ??= {};\n"
+    //const proficiencyRegex = /pcs_(\w+)\s*=\s*([^&]*)/gd;
+    const proficiencyRegex = /^\s*pcs_(\w+)\s*=\s*([^&^\n]*)([^\n]*)\s*$/gm;
+    const proficiencyInnerRegex = /(?:pcs_(\w+))|(?:(\w+)_lvl)|([a-z]\w*)/gd;
+
+    while(result = proficiencyRegex.exec(code)) {
+        const proficiencyId = result[1];
+
+        if(attributes.indexOf(proficiencyId) != -1)
+            continue;
+
+        const proficiencyCodeRaw = result[2];
+        let proficiencyCode = proficiencyCodeRaw;
+
+        let innerResult;
+        while(innerResult = proficiencyInnerRegex.exec(proficiencyCodeRaw)) {
+            const attributeOrSkillId = innerResult[1] ?? innerResult[2];
+            if(attributes.indexOf(attributeOrSkillId) != -1)
+                proficiencyCode = proficiencyCode.replace(innerResult[0],`attributes['${attributeOrSkillId}'].level`);
+            else if(innerResult[3])
+                proficiencyCode = proficiencyCode.replace(innerResult[3],"0");
+            else
+                proficiencyCode = proficiencyCode.replace(innerResult[0],`skills['${attributeOrSkillId}'].level`);
+
+        }
+
+        tsCode += `setup.proficiencies['${proficiencyId}'] = {calc:(attributes,skills)=>${proficiencyCode}}\n`;
+    }
+
+
+    return [twCode, tsCode];
+}

+ 98 - 0
qsrc2tw/twine-code/core/PlayerCharacter.ts

@@ -0,0 +1,98 @@
+/// <reference path="common/GameObject.ts" />
+
+
+class PlayerCharacter extends GameObject{
+
+	//#region Attributes, Proficiencies and Skills
+		//#region Attributes
+			private _attributes = new setup.Skills();
+			public get attributes(){
+				return this.attributesProxy;
+			}
+
+			private $attributesProxy:undefined|{[skillId:string]:Skill} = undefined;
+			private get attributesProxy(){
+				this.$attributesProxy ??= setup.Skills.createProxy(this._attributes);
+				return this.$attributesProxy;
+			}
+		//#endregion
+
+		//#region Proficiencies
+
+			public get proficiencies(){
+				return this.proficienciesProxy;
+			}
+			private $proficienciesProxy:undefined|{[skillId:string]:Skill} = undefined;
+			private get proficienciesProxy(){
+				const pc = this;
+				this.$proficienciesProxy ??= new Proxy({}, {
+					get(playerCharacter, proficiencyId){
+						if(typeof proficiencyId != "string"){
+							if(proficiencyId == Symbol.toStringTag)
+								return "{}";
+							throw new Error(`Unexpected symbol in Proficiencies.Proxy.get(): '${proficiencyId.toString()}'`);
+						}
+
+						try{
+							if(setup.proficiencies[proficiencyId])
+								return {level: setup.proficiencies[proficiencyId].calc(pc.attributes,pc.skills)};
+						}
+						catch(e){
+							console.error(e);
+						}
+						return {level: 0};
+
+					},
+					ownKeys(playerCharacter) {
+						return Object.keys(setup.proficiencies);
+					},
+				}) as unknown as {[proficiencyId:string]:Skill};
+				return this.$proficienciesProxy;
+			}
+
+		//#endregion
+
+		//#region Skills
+			private _skills = new setup.Skills();
+			public get skills(){
+				return this.skillsProxy;
+			}
+
+			public skillEperienceGainCallback(skillId:string, gain:number){
+				const attributeGainFactor = 0.2;
+				const skillDefinition = setup.skills[skillId] ?? {attributes:[]};
+				for(const attributeId of skillDefinition.attributes)
+					this.attributes[attributeId].experience += gain * attributeGainFactor;
+			}
+
+			private $skillsProxy:undefined|{[skillId:string]:Skill} = undefined;
+			private get skillsProxy(){
+				this.$skillsProxy ??= setup.Skills.createProxy(this._skills);
+				return this.$skillsProxy;
+			}
+		//#endregion
+	//#endregion
+
+
+    //#region SYSTEM
+		constructor(){
+			super();
+			this._postInit();
+		}
+
+		_postInit(){
+			this._skills.skillExperienceGainCallback = (skillId:string, gain:number) => this.skillEperienceGainCallback(skillId, gain);
+		}
+
+		clone = function () {
+			return (new setup.PlayerCharacter())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.PlayerCharacter())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+
+}
+
+setup.PlayerCharacter = PlayerCharacter;

+ 72 - 0
qsrc2tw/twine-code/core/Skills/Skill.ts

@@ -0,0 +1,72 @@
+/// <reference path="../common/GameObject.ts" />
+const maxLevel = 1000;
+
+class Skill extends GameObject{
+	$skillExperienceGainCallback: (gain:number) => any = (gain:number) => undefined;
+
+	muta = 0;
+
+	private get difficultyAdjustment(){return 60;}
+
+	private _experience = 0;
+	public get experience(){
+		return this._experience;
+	}
+	public set experience(v:number){
+		if(typeof v != "number")
+			throw new Error(`Unexpected value for Skill.experience.set(): '${v}'`);
+		const newExperience = Math.max(0,v);
+		const gain = newExperience - this._experience;
+		this.$skillExperienceGainCallback(gain);
+		this._experience = newExperience;
+	}
+
+
+	public get level(){
+		return this.experienceToLevel(this._experience);
+	}
+    public set level(v:number){
+        this.experience = this.levelToExperience(v);
+    }
+
+	levelInitialize(level: number, overwrite = false){
+		if(this._experience == 0 || overwrite)
+			this._experience = this.levelToExperience(level);
+	}
+
+	private experienceRequiredForLevelN(n:number){
+		if(!n)
+			return 0;
+		return Math.floor(73 / 2730 * this.difficultyAdjustment * n**2 + 1);
+
+	}
+
+
+	private experienceToLevel(exp:number){
+		for(let i = 1; i<= maxLevel; i++){
+			if(exp < this.experienceRequiredForLevelN(i))
+				return i - 1;
+		}
+	}
+
+	private levelToExperience(lvl:number){
+		return this.experienceRequiredForLevelN(lvl);
+	}
+
+    //#region SYSTEM
+		constructor(
+		){
+			super();
+		}
+
+		clone = function () {
+			return (new setup.Skill())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.Skill())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+}
+
+setup.Skill = Skill;

+ 53 - 0
qsrc2tw/twine-code/core/Skills/Skills.ts

@@ -0,0 +1,53 @@
+/// <reference path="../common/GameObject.ts" />
+
+class Skills extends GameObject{
+
+
+    static createProxy(skills:Skills):{[skillId:string]:Skill}{
+        return new Proxy(skills, {
+            get(skillsObject, skillId){
+                if(typeof skillId != "string"){
+					if(skillId == Symbol.toStringTag)
+						return JSON.stringify(skillsObject);
+                    throw new Error(`Unexpected symbol in Skills.Proxy.get(): '${skillId.toString()}'`);
+                }
+
+				if(!skillsObject._skills[skillId]){
+					skillsObject._skills[skillId] = new Skill();
+					skillsObject._skills[skillId].$skillExperienceGainCallback = (gain:number) => {skillsObject.$skillExperienceGainCallback(skillId,gain)};
+				}
+                return skillsObject._skills[skillId];
+            },
+			ownKeys(skillsObject) {
+				return Object.keys(skillsObject._skills);
+			},
+        }) as unknown as {[skillId:string]:Skill};
+    }
+
+    _skills: {[skillID:string]: Skill} = {};
+
+	set skillExperienceGainCallback(v: (skillId:string, gain:number) => any){
+		this.$skillExperienceGainCallback = v;
+		for(const [skillId, skill] of Object.entries(this._skills)){
+			skill.$skillExperienceGainCallback = (gain:number) => {this.$skillExperienceGainCallback(skillId,gain)};
+		}
+	}
+	$skillExperienceGainCallback: (skillId:string, gain:number) => any = (skillId:string, gain:number) => undefined;
+
+    //#region SYSTEM
+		constructor(
+		){
+			super();
+		}
+
+		clone = function () {
+			return (new setup.Skills())._init(this.clonableFields);
+		};
+
+		toJSON = function () {
+			return JSON.reviveWrapper('(new setup.Skills())._init($ReviveData$)', this.ownData);
+		};
+	//#endregion
+}
+
+setup.Skills = Skills;

+ 102 - 0
qsrc2tw/twine-code/core/Skills/definitions.ts

@@ -0,0 +1,102 @@
+setup.attributes = [
+	'stren',
+	'agil',
+	'vital',
+	'intel',
+	'react',
+	'sprt',
+	'chrsm',
+	'prcptn',
+	'magik',
+	'stren_plus',
+	'butt_tr',
+]
+setup.proficiencies ??= {};
+setup.skills ??= {};
+setup.skills = Object.assign({
+	jab:			{attributes:[]},
+	punch:			{attributes:[]},
+	kick:			{attributes:[]},
+	def:			{attributes:[]},
+	shoot:			{attributes:[]},
+	vokal:			{attributes:[]},
+	sewng:			{attributes:[]},
+	instrmusic:		{attributes:[]},
+	photoskl:		{attributes:[]},
+	artskls:		{attributes:[]},
+	danc:			{attributes:[]},
+	dancero:		{attributes:[]},
+	dancpol:		{attributes:[]},
+	chess:			{attributes:[]},
+	gaming:			{attributes:[]},
+	humint:			{attributes:[]},
+	persuas:		{attributes:[]},
+	run:			{attributes:[]},
+	vball:			{attributes:[]},
+	icesktng:		{attributes:[]},
+	wrstlng:		{attributes:[]},
+	ftbll:			{attributes:[]},
+	splcstng:		{attributes:[]},
+	observ:			{attributes:[]},
+	makupskl:		{attributes:[]},
+	compskl:		{attributes:[]},
+	comphckng:		{attributes:[]},
+	hndiwrk:		{attributes:[]},
+	servng:			{attributes:[]},
+	mdlng:			{attributes:[]},
+	medcn:			{attributes:[]},
+	heels:			{attributes:[]},
+	pool:			{attributes:[]},
+	inhib:			{attributes:[]},
+	perform:		{attributes:[]},
+	bushcraft:		{attributes:[]},
+	cleaning:		{attributes:[]},
+	bkbll:			{attributes:[]},
+	cheer:			{attributes:[]},
+	musicprod:		{attributes:[]},
+	songwrit:		{attributes:[]},
+},setup.skills);
+
+for(const skillName of Object.keys(setup.skills)){
+
+	setup.Overwrite.varRegister(
+		skillName+"_lvl",
+		(index)=>State.variables.PC.skills[skillName].level,
+		(index,val:number)=>State.variables.PC.skills[skillName].level=val
+	);
+
+	setup.Overwrite.varRegister(
+		skillName+"_exp",
+		(index)=>State.variables.PC.skills[skillName].experience,
+		(index,val:number)=>State.variables.PC.skills[skillName].experience=val
+	);
+
+	setup.Overwrite.varRegister(
+		"pcs_"+skillName,
+		(index)=>window.QSP['attsklupdate'][0] == 1 ? State.variables.PC.proficiencies[skillName].level : (State.variables["QSPVAR_n_pcs_"+skillName]?.[index ?? 0] ?? 0),
+		(index,val:number)=>State.variables["QSPVAR_n_pcs_"+skillName] = [val],
+	);
+
+}
+
+for(const attributeId of setup.attributes){
+
+	setup.Overwrite.varRegister(
+		attributeId+"_lvl",
+		(index)=>State.variables.PC.attributes[attributeId].level,
+		(index,val:number)=>State.variables.PC.attributes[attributeId].level=val
+	);
+
+	setup.Overwrite.varRegister(
+		attributeId+"_exp",
+		(index)=>State.variables.PC.attributes[attributeId].experience,
+		(index,val:number)=>State.variables.PC.attributes[attributeId].experience=val
+	);
+
+	setup.Overwrite.varRegister(
+		"pcs_"+attributeId,
+		(index)=>window.QSP['attsklupdate'][0] == 1 ? State.variables.PC.attributes[attributeId].level : (State.variables["QSPVAR_n_pcs_"+attributeId]?.[index ?? 0] ?? 0),
+		(index,val:number)=>State.variables["QSPVAR_n_pcs_"+attributeId] = [val],
+	);
+
+}

+ 28 - 0
qsrc2tw/twine-code/core/common/GameObject.ts

@@ -0,0 +1,28 @@
+abstract class GameObject{
+    get clonableFields(){
+        return Object.fromEntries(Object.entries(this).filter(([key,v])=>!key.startsWith("$")))
+    }
+
+    get ownData(){
+        var ownData = {};
+        Object.keys(this).forEach(function (pn) {
+            if(typeof this[pn] !== "function" && !pn.startsWith("$")){
+                ownData[pn] = clone(this[pn]);
+            }
+        }, this);
+        return ownData;
+    }
+
+
+    _init(data: { [x: string]: any; }){
+        Object.keys(data).forEach(function (pn) {
+            this[pn] = clone(data[pn]);
+        }, this);
+
+        this._postInit();
+
+        return this;
+    }
+
+    _postInit(){}
+}

+ 10 - 16
qsrc2tw/twine-code/examples/serializableObject.ts

@@ -1,26 +1,20 @@
-class Example{
-    //#region SYSTEM
-		constructor(){}
+/// <reference path="../core/common/GameObject.ts" />
+
 
-		_init(playerCharacter: { [x: string]: any; }){
-			Object.keys(playerCharacter).forEach(function (pn) {
-				this[pn] = clone(playerCharacter[pn]);
-			}, this);
 
-			return this;
-		}
+/**
+ * You might need to update the path (first line of this file) and replace every mention of `Example` with your class name
+ */
+class Example extends GameObject{
+    //#region SYSTEM
+		constructor(){super()}
 
 		clone = function () {
-			return (new setup.Example())._init(this);
+			return (new setup.Example())._init(this.clonableFields);
 		};
 
 		toJSON = function () {
-			var ownData = {};
-			Object.keys(this).forEach(function (pn) {
-				if(typeof this[pn] !== "function")
-					ownData[pn] = clone(this[pn]);
-			}, this);
-			return JSON.reviveWrapper('(new setup.Example())._init($ReviveData$)', ownData);
+			return JSON.reviveWrapper('(new setup.Example())._init($ReviveData$)', this.ownData);
 		};
 	//#endregion
 }

+ 2 - 0
qsrc2tw/twine-code/start/StoryInit.tw

@@ -0,0 +1,2 @@
+:: StoryInit
+<<set $PC = new setup.PlayerCharacter()>>

+ 11 - 0
qsrc2tw/twine-code/stat_sklattrib.tw

@@ -0,0 +1,11 @@
+:: stat_sklattrib
+<<if QSP['attsklupdate'][0] != 1>>
+	<<for _skillId, _skillDefinition range setup.skills>>
+		<<run $PC.skills[_skillId].levelInitialize(QSP["pcs_"+_skillId][0])>>
+	<</for>>
+
+	<<for _attributeId range setup.attributes>>
+		<<run $PC.attributes[_attributeId].levelInitialize(QSP["pcs_"+_attributeId][0])>>
+	<</for>>
+	<<set QSP['attsklupdate'][0] = 1>>
+<</if>>

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

@@ -2,6 +2,17 @@ declare module "twine-sugarcube" {
     export interface SugarCubeSetupObject {
         Example: { new(): Example };
 
+        //#region Player Character
+            attributes: string[];
+            proficiencies: {[proficiencyId:string]: ProficiencyDefinition};
+            skills: {[skillId:string]: SkillDefinition};
+            PlayerCharacter: { new(): PlayerCharacter };
+            Skill: { new(): Skill };
+            Skills: {
+                new(): Skills;
+                createProxy: ((skills:Skills)=>{[skillId:string]:Skill});
+            };
+        //#endregion
 
         /**
          * Provides functions to overwrite behavior of functions and variables
@@ -39,6 +50,23 @@ declare module "twine-sugarcube" {
             )=>void
         };
     }
+
+    export interface SugarCubeStoryVariables{
+        PC: PlayerCharacter;
+
+    }
+}
+
+declare global {
+    interface Window { QSP: {[key:(string|number)]: any}; }
+}
+
+export interface ProficiencyDefinition{
+    calc: (attributs:{[skillId:string]:Skill}, skills: {[skillId:string]:Skill}) => number;
+}
+
+export interface SkillDefinition{
+    attributes: string[];
 }
 
 export {};