1
0
Kaynağa Gözat

Merge remote-tracking branch 'Steuchs_bug_hunt/QSRC2TW'

Kevin_Smarts 3 ay önce
ebeveyn
işleme
467c538cbe

+ 1 - 1
locations/stat_sklattrib.qsrc

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

+ 1 - 1
locations/stat_sklattrib_lvlset.qsrc

@@ -1,5 +1,5 @@
 # stat_sklattrib_lvlset
 # stat_sklattrib_lvlset
-
+!! QSRC2TW_module stat_sklattrib_lvlset
 !!--------------- Attribute set section ------------------------
 !!--------------- Attribute set section ------------------------
 !!This is where an attribute pcs_"name" is set by "name"_lvl and any adjustments are added
 !!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
 !!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 qsrc2tw from "./src/qsrc2tw.js";
+import npcInit from "./src/npcInit.js";
+import skillDefinitions from "./src/skillDefinitions.js";
 
 
 import {glob} from 'glob';
 import {glob} from 'glob';
 import fs from 'node:fs';
 import fs from 'node:fs';
@@ -104,44 +106,74 @@ async function convertFile(filePath){
                 return;
                 return;
             }
             }
             const startTime = (new Date()).getTime();
             const startTime = (new Date()).getTime();
-            const codeHash = md5(data);
+
             const baseFileNameStr = baseFileName(filePath);
             const baseFileNameStr = baseFileName(filePath);
             const outFilePath = path.join(outPath,generatedFilesPrefix,baseFileNameStr.split('.')[0]+'.tw');
             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");
                 resolve("SKIP");
                 return;
                 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{
             try{
                 let twineCode = "";
                 let twineCode = "";
                 consoleActive = options.verboseErrors;
                 consoleActive = options.verboseErrors;
+                let twineCodeRaw = undefined;
+                let tsCodeRaw = undefined;
                 try{
                 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){
                 catch(e){
                     throw e;
                     throw e;
@@ -149,13 +181,34 @@ async function convertFile(filePath){
                 finally{
                 finally{
                     consoleActive = true;
                     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;
                 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 () {
 		clone = function () {
-			return (new setup.Example())._init(this);
+			return (new setup.Example())._init(this.clonableFields);
 		};
 		};
 
 
 		toJSON = function () {
 		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
 	//#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 {
     export interface SugarCubeSetupObject {
         Example: { new(): Example };
         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
          * Provides functions to overwrite behavior of functions and variables
@@ -39,6 +50,23 @@ declare module "twine-sugarcube" {
             )=>void
             )=>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 {};
 export {};