diff --git "a/docs/src/\351\273\221\346\230\237\347\232\204Macro\345\220\210\351\233\206/\345\256\217\345\210\227\350\241\250 \347\254\254\344\270\200\345\274\271.md" "b/docs/src/\351\273\221\346\230\237\347\232\204Macro\345\220\210\351\233\206/\345\256\217\345\210\227\350\241\250 \347\254\254\344\270\200\345\274\271.md" index b37d457..3a5be40 100644 --- "a/docs/src/\351\273\221\346\230\237\347\232\204Macro\345\220\210\351\233\206/\345\256\217\345\210\227\350\241\250 \347\254\254\344\270\200\345\274\271.md" +++ "b/docs/src/\351\273\221\346\230\237\347\232\204Macro\345\220\210\351\233\206/\345\256\217\345\210\227\350\241\250 \347\254\254\344\270\200\345\274\271.md" @@ -67,19 +67,23 @@ setup.profiles = { last: ['Smith', 'Doe', 'Johnson', 'Williams'], // 姓氏列表 favorite: ['蛋糕', '牛排', '香蕉'], // 喜欢的食物列表 race: [ - { - value: "精灵", - prob: 0.5, // 概率50% - affects: {agility: 2}, // 增加2点敏捷 - limits: {agility: {min: 5, max: 10}, strength: {min: 0, max: 5}} // 敏捷上下限5/10,力量上下限0/5 - }, - { - value: "侏儒", - prob: 0.5, // 概率50% - affects: {strength: -2}, // 减少2点力量 - limits: {agility: {min: 0, max: 8}, strength: {min: -5, max: 5}} // 敏捷上下限0/8,力量上下限-5/5 - } - ], + { + value: '巨人', + prob: 0.02 // 巨人,用于测试expr + }, + { + value: "精灵", + prob: 0.49, // 概率49% + affects: {agility: 2}, // 增加2点敏捷 + limits: {agility: {min: 5, max: 10}, strength: {min: 0, max: 5}} // 敏捷上下限5/10,力量上下限0/5 + }, + { + value: "侏儒", + prob: 0.49, // 概率49% + affects: {strength: -2}, // 减少2点力量 + limits: {agility: {min: 0, max: 8}, strength: {min: -5, max: 5}} // 敏捷上下限0/8,力量上下限-5/5 + } + ], itinerary: [ 'list', // 未支持格式,当前情况做占位符。如果缺少占位符,无法随机object。 {AM0700: "食堂", AM0800: "商业街"}, @@ -88,19 +92,11 @@ setup.profiles = { }; setup.attrs = { + strength: {expr: "3d6*5"}, // 骰3个6面骰其结果成为5,无上下限限制 intelligence: { min: 1, max: 10 // 智力上下限1/10 }, - strength: { - min: 1, - max: 10, // 力量上下限1/10 - ranges: [ - {min: 1, max: 3, prob: 0.3}, // 力量范围 1-3,概率 30% - {min: 4, max: 8, prob: 0.6}, // 力量范围 4-8,概率 60% - {min: 9, max: 10, prob: 0.1} // 力量范围 9-10,概率 10% - ] - }, agility: { min: 1, max: 10, // 敏捷上下限1/10 diff --git a/src/js/utils/generate-npc.min.js b/src/js/utils/generate-npc.min.js index 763cd08..67925d3 100644 --- a/src/js/utils/generate-npc.min.js +++ b/src/js/utils/generate-npc.min.js @@ -1,3 +1,3 @@ -/* 生成NPC, By BlackStar, Ver.0.6.0 */ -(()=>{"use strict";const defaultConfig={maxtrait:2};const drawRandomInRange=ranges=>{const options=ranges.reduce(((opt,{min:min,max:max,prob:prob})=>{opt[`${min}-${max}`]=prob;return opt}),{});const range=drawOption(options);const[min,max]=range.split("-").map(Number);return randomInt(min,max)};const updateAttributes=(chosenProfile,npcAttrs,attrs)=>{if(chosenProfile){const{affects:affects={},limits:limits={}}=chosenProfile;Object.entries(affects).forEach((([attrKey,attrValue])=>{npcAttrs[attrKey]=(npcAttrs[attrKey]??0)+attrValue}));Object.entries(limits).forEach((([attrKey,limit])=>{if(limit.min)attrs[attrKey].min=limit.min;if(limit.max)attrs[attrKey].max=limit.max}))}};const generateNPC=(profiles,attrs,traits,config={},defaults={},pinned={})=>{config=Object.assign({},defaultConfig,config);const npcAttrs=Object.entries(attrs).reduce(((acc,[key,value])=>{const pinnedVal=pinned[key];acc[key]=pinnedVal&&Number.isFinite(pinnedVal)?pinnedVal:value.ranges?drawRandomInRange(value.ranges):randomInt(value.min,value.max);return acc}),{});const npcProfiles=Object.entries(profiles).reduce(((acc,[key,value])=>{const pinnedVal=pinned[key];const selectedValue=pinnedVal?pinnedVal:Array.isArray(value)&&typeof value[0]==="object"?drawOption(value.reduce(((opt,{value:value,prob:prob})=>({...opt,[value]:prob})),{})):value.random();acc[key]=selectedValue;if(Array.isArray(value)&&typeof value[0]==="object"){const chosenProfile=value.find((opt=>opt.value===selectedValue));updateAttributes(chosenProfile,npcAttrs,attrs)}return acc}),{});const applies=pinned.traits?[...pinned.traits]:[];const traitMap={};let availableTraits=[...traits].filter((trait=>!applies.includes(trait.name)));while(applies.length0){const trait=availableTraits.random();if(!traitMap[trait.name]){applies.push(trait.name);traitMap[trait.name]=trait;availableTraits=availableTraits.filter((t=>t!==trait))}availableTraits=availableTraits.filter((trait=>!applies.some((appliedTrait=>{const applied=traitMap[appliedTrait];return applied?.exclusive?.includes(trait.name)||trait.exclusive?.includes(appliedTrait)}))))}Object.values(traitMap).forEach((({affects:affects={}})=>{Object.entries(affects).forEach((([key,value])=>{if(npcAttrs[key]!==undefined){npcAttrs[key]+=value}}))}));Object.keys(npcAttrs).forEach((key=>{const{min:min,max:max}=attrs[key];npcAttrs[key]=applyBounds(npcAttrs[key],min,max)}));return{...npcProfiles,...defaults,...npcAttrs,traits:applies}};const isRange=condition=>condition&&typeof condition==="object"&&("min"in condition||"max"in condition);const filterNPC=(npcs,filters)=>npcs.filter((npc=>Object.entries(filters).every((([key,condition])=>{const val=npc[key];if(isRange(condition)){const{min:min,max:max}=condition;if(min!==undefined&&valmax)return false;return true}else if(Array.isArray(condition)){return condition.includesAll(val)}else if(typeof condition==="number"||typeof condition==="string"){return val===condition}return true}))));Macro.add("pushnpc",{tags:null,handler(){if(this.args.length<2){return this.error("Not enough arguments provided. At least two arguments are required.")}const cleanArg=arg=>arg.replace(/["']/g,"").trim();const parts=this.args.raw.trim().split(" ");const varName=cleanArg(parts[0]);if(!isValidVariable(varName)){return this.error("argument must be a story or temporary variable!")}let arr=State.getVar(varName);if(!Array.isArray(arr)){return this.error("Expected an array but received: "+typeof arr)}if(!setup.profiles||!setup.attrs||!setup.traits){return this.error("setup.profiles, setup.attrs, and setup.traits must be defined!")}const times=parseInt(parts[1]);const startIndex=arr.length;const defaults=this.args[2]&&typeof this.args[2]==="object"?{...setup.defaults,...this.args[2]}:setup.defaults;const pinned=this.args[3]&&typeof this.args[3]==="object"&&!Array.isArray(this.args[2])?this.args[3]:convertToObject(this.args.slice(3));repeat(times,(_=>{arr.push(generateNPC(setup.profiles,setup.attrs,setup.traits,setup.config||{},defaults,pinned))}));const endIndex=arr.length-1;WikiVars({start:startIndex,end:endIndex},this)}});Macro.add("filternpc",{tags:null,handler(){if(this.args.length<2){return this.error("Not enough arguments provided. At least two arguments are required.")}const npcs=this.args[0];const filters=this.args[1];if(!Array.isArray(npcs)){throw new Error("Invalid input: 'npcs' should be an array")}if(filters&&typeof filters!=="object"){throw new Error("Invalid input: 'filters' should be an object")}npcs.forEach(((npc,index)=>{if(typeof npc!=="object"||npc===null){throw new Error(`Invalid NPC at index ${index}: NPC should be an object`)}}));const result=filterNPC(npcs,filters);WikiVars({result:result},this)}});setup.filterNPC=filterNPC;window.filterNPC=window.filterNPC||filterNPC;setup.generateNPC=generateNPC;window.generateNPC=window.generateNPC||generateNPC})(); +/* 生成NPC, By BlackStar, Ver.0.7.0 */ +(()=>{"use strict";const defaultConfig={maxtrait:2};const drawRandomInRange=ranges=>{const options=ranges.reduce(((opt,{min:min,max:max,prob:prob})=>{opt[`${min}-${max}`]=prob;return opt}),{});const range=drawOption(options);const[min,max]=range.split("-").map(Number);return randomInt(min,max)};const parseExpr=expr=>{const match=expr.match(/(\d*)[dD](\d+)/);if(!match)return 0;const num=parseInt(match[1],10)||1;const dice=parseInt(match[2],10);return Array.from({length:num}).reduce((sum=>sum+randomInt(1,dice)),0)};const rollDice=expr=>{const replaced=expr.replace(/(\d*[dD]\d+)/g,(match=>parseExpr(match)));return eval(replaced)};const updateAttributes=(chosenProfile,npcAttrs,attrs)=>{if(chosenProfile){const{affects:affects={},limits:limits={}}=chosenProfile;Object.entries(affects).forEach((([attrKey,attrValue])=>{npcAttrs[attrKey]=(npcAttrs[attrKey]??0)+attrValue}));Object.entries(limits).forEach((([attrKey,limit])=>{if(limit.min)attrs[attrKey].min=limit.min;if(limit.max)attrs[attrKey].max=limit.max}))}};const generateNPC=(profiles,attrs,traits,config={},defaults={},pinned={})=>{config=Object.assign({},defaultConfig,config);const npcAttrs=Object.entries(attrs).reduce(((acc,[key,value])=>{const pinnedVal=pinned[key];if(pinnedVal&&Number.isFinite(pinnedVal)){acc[key]=pinnedVal}else if(value.expr&&typeof value.expr==="string"){acc[key]=rollDice(value.expr)}else{acc[key]=value.ranges?drawRandomInRange(value.ranges):randomInt(value.min,value.max)}return acc}),{});const npcProfiles=Object.entries(profiles).reduce(((acc,[key,value])=>{const pinnedVal=pinned[key];const selectedValue=pinnedVal?pinnedVal:Array.isArray(value)&&typeof value[0]==="object"?drawOption(value.reduce(((opt,{value:value,prob:prob})=>({...opt,[value]:prob})),{})):value.random();acc[key]=selectedValue;if(Array.isArray(value)&&typeof value[0]==="object"){const chosenProfile=value.find((opt=>opt.value===selectedValue));updateAttributes(chosenProfile,npcAttrs,attrs)}return acc}),{});const applies=pinned.traits?[...pinned.traits]:[];const traitMap={};let availableTraits=[...traits].filter((trait=>!applies.includes(trait.name)));while(applies.length0){const trait=availableTraits.random();if(!traitMap[trait.name]){applies.push(trait.name);traitMap[trait.name]=trait;availableTraits=availableTraits.filter((t=>t!==trait))}availableTraits=availableTraits.filter((trait=>!applies.some((appliedTrait=>{const applied=traitMap[appliedTrait];return applied?.exclusive?.includes(trait.name)||trait.exclusive?.includes(appliedTrait)}))))}Object.values(traitMap).forEach((({affects:affects={}})=>{Object.entries(affects).forEach((([key,value])=>{if(npcAttrs[key]!==undefined){npcAttrs[key]+=value}}))}));Object.keys(npcAttrs).forEach((key=>{const{min:min,max:max}=attrs[key];npcAttrs[key]=applyBounds(npcAttrs[key],min,max)}));return{...npcProfiles,...defaults,...npcAttrs,traits:applies}};const isRange=condition=>condition&&typeof condition==="object"&&("min"in condition||"max"in condition);const filterNPC=(npcs,filters)=>npcs.filter((npc=>Object.entries(filters).every((([key,condition])=>{const val=npc[key];if(isRange(condition)){const{min:min,max:max}=condition;if(min!==undefined&&valmax)return false;return true}else if(Array.isArray(condition)){return condition.includesAll(val)}else if(typeof condition==="number"||typeof condition==="string"){return val===condition}return true}))));Macro.add("pushnpc",{tags:null,handler(){if(this.args.length<2){return this.error("Not enough arguments provided. At least two arguments are required.")}const cleanArg=arg=>arg.replace(/["']/g,"").trim();const parts=this.args.raw.trim().split(" ");const varName=cleanArg(parts[0]);if(!isValidVariable(varName)){return this.error("argument must be a story or temporary variable!")}let arr=State.getVar(varName);if(!Array.isArray(arr)){return this.error("Expected an array but received: "+typeof arr)}if(!setup.profiles||!setup.attrs||!setup.traits){return this.error("setup.profiles, setup.attrs, and setup.traits must be defined!")}const times=parseInt(parts[1]);const startIndex=arr.length;const defaults=this.args[2]&&typeof this.args[2]==="object"?{...setup.defaults,...this.args[2]}:setup.defaults;const pinned=this.args[3]&&typeof this.args[3]==="object"&&!Array.isArray(this.args[2])?this.args[3]:convertToObject(this.args.slice(3));repeat(times,(_=>{arr.push(generateNPC(setup.profiles,setup.attrs,setup.traits,setup.config||{},defaults,pinned))}));const endIndex=arr.length-1;WikiVars({start:startIndex,end:endIndex},this)}});Macro.add("filternpc",{tags:null,handler(){if(this.args.length<2){return this.error("Not enough arguments provided. At least two arguments are required.")}const npcs=this.args[0];const filters=this.args[1];if(!Array.isArray(npcs)){throw new Error("Invalid input: 'npcs' should be an array")}if(filters&&typeof filters!=="object"){throw new Error("Invalid input: 'filters' should be an object")}npcs.forEach(((npc,index)=>{if(typeof npc!=="object"||npc===null){throw new Error(`Invalid NPC at index ${index}: NPC should be an object`)}}));const result=filterNPC(npcs,filters);WikiVars({result:result},this)}});setup.filterNPC=filterNPC;window.filterNPC=window.filterNPC||filterNPC;setup.generateNPC=generateNPC;window.generateNPC=window.generateNPC||generateNPC})(); /* End 生成NPC */ \ No newline at end of file diff --git a/src/js/utils/macro-utils.min.js b/src/js/utils/macro-utils.min.js index f02dc26..87e9bf3 100644 --- a/src/js/utils/macro-utils.min.js +++ b/src/js/utils/macro-utils.min.js @@ -1,3 +1,3 @@ -/* Macro Utils, By BlackStar, Partially adapted from ChapelR, Ver.0.3.0 */ -(()=>{const isValidVariable=varName=>varName&&typeof varName==="string"&&varName.length>=2&&(varName[0]==="$"||varName[0]==="_");const repeat=(times,action)=>{for(let i=0;i{const shadowStore={};for(const[varName,value]of Object.entries(pairs)){if(Object.hasOwn(State.temporary,varName)){shadowStore[`_${varName}`]=State.temporary[varName]}State.temporary[varName]=value;entity.addShadow(`_${varName}`);if(Object.hasOwn(State.variables,varName)){shadowStore[`$${varName}`]=State.variables[varName]}State.variables[varName]=State.temporary[varName];entity.addShadow(`$${varName}`)}try{new Wikifier(entity.output,entity?.payload?.[0]?.contents||"")}finally{for(const[varName]of Object.entries(pairs)){if(Object.hasOwn(shadowStore,`_${varName}`)){State.temporary[varName]=shadowStore[`_${varName}`]}else{delete State.temporary[varName]}if(Object.hasOwn(shadowStore,`$${varName}`)){State.variables[varName]=shadowStore[`$${varName}`]}else{delete State.variables[varName]}}}};const applyBounds=(value,min,max)=>Math.max(min,Math.min(max,value));const convertToArray=input=>{const array=[];for(const[key,value]of Object.entries(input)){array.push(key,value)}return array};const convertToObject=arr=>{if(!Array.isArray(arr)||arr.length===0){return{}}const flatten=array=>array.reduce(((flatArr,item)=>flatArr.concat(Array.isArray(item)?flatten(item):item)),[]);const flattened=flatten(arr);const obj={};for(let i=0;i{const isValidVariable=varName=>varName&&typeof varName==="string"&&varName.length>=2&&(varName[0]==="$"||varName[0]==="_");const repeat=(times,action)=>{for(let i=0;i{const shadowStore={};for(const[varName,value]of Object.entries(pairs)){if(Object.hasOwn(State.temporary,varName)){shadowStore[`_${varName}`]=State.temporary[varName]}State.temporary[varName]=value;entity.addShadow(`_${varName}`);if(Object.hasOwn(State.variables,varName)){shadowStore[`$${varName}`]=State.variables[varName]}State.variables[varName]=State.temporary[varName];entity.addShadow(`$${varName}`)}try{new Wikifier(entity.output,entity?.payload?.[0]?.contents||"")}finally{for(const[varName]of Object.entries(pairs)){if(Object.hasOwn(shadowStore,`_${varName}`)){State.temporary[varName]=shadowStore[`_${varName}`]}else{delete State.temporary[varName]}if(Object.hasOwn(shadowStore,`$${varName}`)){State.variables[varName]=shadowStore[`$${varName}`]}else{delete State.variables[varName]}}}};const applyBounds=(value,min,max)=>{if(min===undefined&&max===undefined){return value}if(max===undefined){return Math.max(min,value)}if(min===undefined){return Math.min(max,value)}return Math.max(min,Math.min(max,value))};const convertToArray=input=>{const array=[];for(const[key,value]of Object.entries(input)){array.push(key,value)}return array};const convertToObject=arr=>{if(!Array.isArray(arr)||arr.length===0){return{}}const flatten=array=>array.reduce(((flatArr,item)=>flatArr.concat(Array.isArray(item)?flatten(item):item)),[]);const flattened=flatten(arr);const obj={};for(let i=0;i