Master judgment: every year of life

Posted by marcel on Sun, 20 Feb 2022 04:43:02 +0100

Life reopen simulator git
https://github.com/VickScarlet/lifeRestart

1. The process of painfully reading code

Under Windows 11 system, according to readme, start the game with a series of instructions beginning with npm

After entering the life simulation, use f10, and soon find it in life JS, he calls next() every year

next() {
        const {age, event, talent} = this.#property.ageNext();

        const talentContent = this.doTalent(talent);
        const eventContent = this.doEvent(this.random(event));

        const isEnd = this.#property.isEnd();

        const content = [talentContent, eventContent].flat();
        this.#achievement.achieve(this.AchievementOpportunity.TRAJECTORY);
        return { age, content, isEnd };
    }

This is an instance of this class. Looking up, this is a life class

class Life {
    constructor() {
        this.#property = new Property(this);
        this.#event = new Event(this);
        this.#talent = new Talent(this);
        this.#achievement = new Achievement(this);
        this.#character = new Character(this);
    }

    Module = {
        PROPERTY: 'PROPERTY',
        TALENT: 'TALENT',
        EVENT: 'EVENT',
        ACHIEVEMENT: 'ACHIEVEMENT',
        CHARACTER: 'CHARACTER',
    }

    Function = {
        CONDITION: 'CONDITION',
        UTIL: 'UTIL',
    }

    #property;
    #event;
    #talent;
    #achievement;
    #character;
    #triggerTalents;
    #defaultPropertyPoints;
    #talentSelectLimit;
    #propertyAllocateLimit;
    #defaultPropertys;
    #specialThanks;
    #initialData;

	//Later strategy

#Property is the property of this class, and ageNext() is in property js

ageNext() {
        this.change(this.TYPES.AGE, 1);
        const age = this.get(this.TYPES.AGE);
        const {event, talent} = this.getAgeData(age);
        return {age, event, talent};
    }

In this function, this is an instance of the Property class

class Property {
    constructor(system) {
        this.#system = system;
    }

    TYPES = {
        // This Council
        AGE: "AGE", // Age
        CHR: "CHR", // Beauty charm CHR
        INT: "INT", // intelligence INT
        STR: "STR", // Physical strength STR
        MNY: "MNY", // Family money MNY
        SPR: "SPR", // Happy spirit SPR
        LIF: "LIF", // life LIFE
        TLT: "TLT", // talent TLT
        EVT: "EVT", // event EVT
        TMS: "TMS", // times TMS

        // Auto calc
        LAGE: "LAGE", // Minimum age Low Age
        HAGE: "HAGE", // Highest age
        LCHR: "LCHR", // Lowest appearance value Low Charm
        HCHR: "HCHR", // Highest appearance value High Charm
        LINT: "LINT", // Minimum intelligence
        HINT: "HINT", // Highest intelligence
        LSTR: "LSTR", // Low Strength
        HSTR: "HSTR", // High Strength
        LMNY: "LMNY", // Low Money
        HMNY: "HMNY", // High Money
        LSPR: "LSPR", // Lowest spirit
        HSPR: "HSPR", // Highest spirit

        SUM: "SUM", // summary SUM

        EXT: "EXT", // Inherited talent

        // total
        // Achievement Total
        ATLT: "ATLT", // Have a talent achievement talent
        AEVT: "AEVT", // Triggered event achievement event
        ACHV: "ACHV", // Achievement s achieved

        CTLT: "CTLT", // Talent count
        CEVT: "CEVT", // Event collection Count Event
        CACHV: "CACHV", // Number of achievements

        // total
        TTLT: "TTLT", // Total Talent
        TEVT: "TEVT", // Total events
        TACHV: "TACHV", // Total Achievement

        // ratio
        REVT: "REVT", // Event collection rate
        RTLT: "RTLT", // Talent selection Rate Talent
        RACHV: "RACHV", // Rate Achievement

        // SPECIAL
        RDM: 'RDM', // Random attribute RDM

    };

    // Special type
    SPECIAL = {
        RDM: [ // Random attribute RDM
            this.TYPES.CHR,
            this.TYPES.INT,
            this.TYPES.STR,
            this.TYPES.MNY,
            this.TYPES.SPR,
        ]
    }

    #system;
    #ageData;
    #data = {};
    #total;
    #judge;

	//Later strategy

change() is a method of a class

    change(prop, value) {
        if(Array.isArray(value)) {
            for(const v of value)
                this.change(prop, Number(v));
            return;
        }
        switch(prop) {
            case this.TYPES.AGE:
            case this.TYPES.CHR:
            case this.TYPES.INT:
            case this.TYPES.STR:
            case this.TYPES.MNY:
            case this.TYPES.SPR:
            case this.TYPES.LIF:
                this.hl(prop, this.#data[prop] += Number(value));
                return;
            case this.TYPES.TLT:
            case this.TYPES.EVT:
                const v = this.#data[prop];
                if(value<0) {
                    const index = v.indexOf(value);
                    if(index!=-1) v.splice(index,1);
                }
                if(!v.includes(value)) v.push(value);
                this.achieve(prop, value);
                return;
            case this.TYPES.TMS:
                this.set(
                    prop,
                    this.get(prop) + parseInt(value)
                );
                return;
            default: return;
        }
    }

If you enter a value as a vector, foreach the vector, and then use change for prop for each vector element, so that you can use a function to deal with 1-to-1 to deal with 1-to-1 pairs of vectors. That's great
Different attributes have different ways of adding. No matter how detailed they are, they won't be seen

Go back to ageNext(), and then get

    get(prop) {
        const util = this.#util;
        switch(prop) {
            case this.TYPES.AGE:
            case this.TYPES.CHR:
            case this.TYPES.INT:
            case this.TYPES.STR:
            case this.TYPES.MNY:
            case this.TYPES.SPR:
            case this.TYPES.LIF:
            case this.TYPES.TLT:
            case this.TYPES.EVT:
                return util.clone(this.#data[prop]);
            case this.TYPES.LAGE:
            case this.TYPES.LCHR:
            case this.TYPES.LINT:
            case this.TYPES.LSTR:
            case this.TYPES.LMNY:
            case this.TYPES.LSPR:
                return util.min(
                    this.#data[prop],
                    this.get(this.fallback(prop))
                );
            case this.TYPES.HAGE:
            case this.TYPES.HCHR:
            case this.TYPES.HINT:
            case this.TYPES.HSTR:
            case this.TYPES.HMNY:
            case this.TYPES.HSPR:
                return util.max(
                    this.#data[prop],
                    this.get(this.fallback(prop))
                );
            case this.TYPES.SUM:
                const HAGE = this.get(this.TYPES.HAGE);
                const HCHR = this.get(this.TYPES.HCHR);
                const HINT = this.get(this.TYPES.HINT);
                const HSTR = this.get(this.TYPES.HSTR);
                const HMNY = this.get(this.TYPES.HMNY);
                const HSPR = this.get(this.TYPES.HSPR);
                return Math.floor(util.sum(HCHR, HINT, HSTR, HMNY, HSPR)*2 + HAGE/2);
            case this.TYPES.TMS:
                return this.lsget('times') || 0;
            case this.TYPES.EXT:
                return this.lsget('extendTalent') || null;
            case this.TYPES.ATLT:
            case this.TYPES.AEVT:
            case this.TYPES.ACHV:
                return this.lsget(prop) || [];
            case this.TYPES.CTLT:
            case this.TYPES.CEVT:
            case this.TYPES.CACHV:
                return this.get(
                    this.fallback(prop)
                ).length;
            case this.TYPES.TTLT:
            case this.TYPES.TEVT:
            case this.TYPES.TACHV:
                return this.#total[prop];
            case this.TYPES.RTLT:
            case this.TYPES.REVT:
            case this.TYPES.RACHV:
                const fb = this.fallback(prop);
                return this.get(fb[0]) / this.get(fb[1]);
            default: return 0;
        }
    }

Some attributes return clone and some return fallback. HMM... I don't know why it's not callback. Shouldn't it mean callback

clone() in util js

function clone(value) {
    switch(typeof value) {
        case 'object':
            if(Array.isArray(value)) return value.map(v=>clone(v));
            const newObj = {};
            for(const key in value) newObj[key] = clone(value[key]);
            return newObj;
        default: return value;
    }
}

If the argument to be cloned is an object
If the argument is a vector, as usual, a vector argument is divided into vector element arguments by traversal
Then copy the key value pair of the argument of this object type. The value may be a vector, so you need to use a clone() to return the value
Then return the copied object

Oh... Is a function that can copy objects

    fallback(prop) {
        switch(prop) {
            case this.TYPES.LAGE:
            case this.TYPES.HAGE: return this.TYPES.AGE;
            case this.TYPES.LCHR:
            case this.TYPES.HCHR: return this.TYPES.CHR;
            case this.TYPES.LINT:
            case this.TYPES.HINT: return this.TYPES.INT;
            case this.TYPES.LSTR:
            case this.TYPES.HSTR: return this.TYPES.STR;
            case this.TYPES.LMNY:
            case this.TYPES.HMNY: return this.TYPES.MNY;
            case this.TYPES.LSPR:
            case this.TYPES.HSPR: return this.TYPES.SPR;
            case this.TYPES.CTLT: return this.TYPES.ATLT;
            case this.TYPES.CEVT: return this.TYPES.AEVT;
            case this.TYPES.CACHV: return this.TYPES.ACHV;
            case this.TYPES.LIF: return this.TYPES.LIF;
            case this.TYPES.RTLT: return [this.TYPES.CTLT, this.TYPES.TTLT];
            case this.TYPES.REVT: return [this.TYPES.CEVT, this.TYPES.TEVT];
            case this.TYPES.RACHV: return [this.TYPES.CACHV, this.TYPES.TACHV];
            default: return;
        }
    }

Originally, fallback means fallback... It should back up some properties? I don't understand. I don't read it

Go back to ageNext(), followed by getAgeData()

getAgeData(age) {
        return this.#system.clone(this.#ageData[age]);
    }

Then an ageData is used

I don't understand... Why is ageData initialized with = age, and then [age] can be used without any objects
I don't understand. Skip

Back to next(), the next line is doTalent()

    doTalent(talents) {
        if(talents) this.#property.change(this.PropertyTypes.TLT, talents);
        talents = this.#property.get(this.PropertyTypes.TLT)
            .filter(talentId => this.getTalentCurrentTriggerCount(talentId) < this.#talent.get(talentId).max_triggers);

        const contents = [];
        for(const talentId of talents) {
            const result = this.#talent.do(talentId);
            if(!result) continue;
            this.#triggerTalents[talentId] = this.getTalentCurrentTriggerCount(talentId) + 1;
            const { effect, name, description, grade } = result;
            contents.push({
                type: this.PropertyTypes.TLT,
                name,
                grade,
                description,
            })
            if(!effect) continue;
            this.#property.effect(effect);
        }
        return contents;
    }

The first line is to use change() to change talents

            case this.TYPES.TLT:
            case this.TYPES.EVT:
                const v = this.#data[prop];
                if(value<0) {
                    const index = v.indexOf(value);
                    if(index!=-1) v.splice(index,1);
                }
                if(!v.includes(value)) v.push(value);
                this.achieve(prop, value);
                return;

This input is talent. Although I don't know what talent is, it should also be an object. Why can the object be less than 0... It's strange
Estimated to deal with anomalies? I don't know. Skip
The const in js is the underlying const, so you can modify the value pointed to. If this# If there is no talent in the data [prop], add it. HMM, it's reasonable
Then achieve()

    achieve(prop, newData) {
        let key;
        switch(prop) {
            case this.TYPES.ACHV:
                const lastData = this.lsget(prop);
                this.lsset(
                    prop,
                    (lastData || []).concat([[newData, Date.now()]])
                );
                return;
            case this.TYPES.TLT: key = this.TYPES.ATLT; break;
            case this.TYPES.EVT: key = this.TYPES.AEVT; break;
            default: return;
        }
        const lastData = this.lsget(key) || [];
        this.lsset(
            key,
            Array.from(
                new Set(
                    lastData
                        .concat(newData||[])
                        .flat()
                )
            )
        )
    }

This came in at this time PropertyTypes. TLT, get key = this TYPES. Atlt, pass in lsget()

    lsget(key) {
        const data = localStorage.getItem(key);
        if(data === null || data === 'undefined') return;
        return JSON.parse(data);
    }

It seems that we are going to start reading the meter. First go back and have a look at ATLT

ATLT: "ATLT", // Have a talent achievement talent

He fetches the ATLT from the local storage, which should be saved during initialization
But when I set the breakpoint, I didn't see this ATLT... Embarrassed
Forget it, whatever

AEVT: "AEVT", // Triggered event achievement event
ACHV: "ACHV", // Achievement s achieved

Then it parses the JSON string, nb

Well, I still haven't read my watch
Or is there a watch in the local storage?
I don't understand. Skip

Back to achieve

        const lastData = this.lsget(key) || [];
        this.lsset(
            key,
            Array.from(
                new Set(
                    lastData
                        .concat(newData||[])
                        .flat()
                )
            )
        )

Use array From() passes the second argument to lsset()
The new Set is array From () is the only argument, so here is array The only job of from () is to convert this Set into an array
Set is the splicing of lastData and newData. When a value participates in a logical or operation, the result is true, and the first value that is true will be returned; If the result is false, the second false value is returned. [] is false, so an empty array will be returned when newData is false. Finally, flat() cancels the depth
In fact, the new data is transmitted to lsset() together with the old data

    lsset(key, value) {
        localStorage.setItem(
            key,
            JSON.stringify(value)
        );
    }

Convert to JSON and save to local storage
That is, the function of achievement () is to take the corresponding old data from the local and store it locally together with the new data
Maybe property The function of change() is to update the storage according to the attribute?

Go back to doTalent(), property Get() is passed into TLT, so lsget() is still used. I've seen it

Array. The filtering condition set by filter () is that the number of trigger s corresponding to this id is less than the maximum value

    getTalentCurrentTriggerCount(talentId) {
        return this.#triggerTalents[talentId] || 0;
    }

It seems that a trigger will be set in one place. Skip it first

Wow, I'll let every talent work next... Maybe there's really game logic?

talent.do in talent js

   do(talentId) {
        const { effect, condition, grade, name, description } = this.get(talentId);
        if(condition && !this.#system.check(condition))
            return null;
        return { effect, grade, name, description };
    }

get is to directly obtain the talents attribute. This attribute has initialization, um... Then skip

    get(talentId) {
        const talent = this.#talents[talentId];
        if(!talent) throw new Error(`[ERROR] No Talent[${talentId}]`);
        return this.#system.clone(talent);
    }

Back to talent do
system.check() returns true talent to talent Do returns true

class Talent {
    constructor(system) {
        this.#system = system;
    }

    #system;
    #talents;
    #talentPullCount;
    #talentRate;
    #additions;

I don't know what kind of system this is. It's numb
Set a breakpoint

You can't get there in one step... Forget it

If this talent has running results, put its trigger + 1, and then take out the effect to property effect()

    effect(effects) {
        for(let prop in effects)
            this.change(
                this.hookSpecial(prop),
                Number(effects[prop])
            );
    }

For each item in the effect, the corresponding attribute and value should be extracted

    hookSpecial(prop) {
        switch(prop) {
            case this.TYPES.RDM:
                return this.#util.listRandom(this.SPECIAL.RDM);
            default: return prop;
        }
    }

hookSpecial() is a special attribute special handling

function listRandom(list) {
    return list[Math.floor(Math.random() * list.length)];
}

listRandom() returns a random element in the list

		// SPECIAL
        RDM: 'RDM', // Random attribute RDM
    // Special type
    SPECIAL = {
        RDM: [ // Random attribute RDM
            this.TYPES.CHR,
            this.TYPES.INT,
            this.TYPES.STR,
            this.TYPES.MNY,
            this.TYPES.SPR,
        ]
    }

Random attributes are a list

That's actually property The function of effect () is to apply the attribute value in effect to change()
In fact, doTalent() is talent Apply after do() finds the effect

Return to next(). The next line is doEvent()

    doEvent(eventId) {
        const { effect, next, description, postEvent, grade } = this.#event.do(eventId);
        this.#property.change(this.PropertyTypes.EVT, eventId);
        this.#property.effect(effect);
        const content = {
            type: this.PropertyTypes.EVT,
            description,
            postEvent,
            grade,
        }
        if(next) return [content, this.doEvent(next)].flat();
        return [content];
    }

Look at event Do() in event js

    do(eventId) {
        const { effect, branch, event: description, postEvent, grade } = this.get(eventId);
        if(branch)
            for(const [cond, next] of branch)
                if(this.#system.check(cond))
                    return { effect, next, description, grade };
        return { effect, postEvent, description, grade };
    }

Take out so many things according to an id... I really want to understand that the logic lies in this get()

Then it is whether there is a branch. If there is a branch, check the conditions of the branch. If the conditions are met, it returns the next event pointed to by the branch. Otherwise, it returns the end of the event

Go back to doEvent(), if it is event Do() returns the next event, so do doEvent() recursion again for this event

Go back to next(), and then judge whether it is over

    isEnd() {
        return this.get(this.TYPES.LIF) < 1;
    }
        LIF: "LIF", // life LIFE

This is based on life

Then judge whether you have achieved success Achievement() in achievement JS

    achieve(opportunity) {
        this.list()
            .filter(({isAchieved})=>!isAchieved)
            .filter(({opportunity: o})=>o==opportunity)
            .filter(({id})=>this.check(id, this.#prop))
            .forEach(({id})=>{
                this.#prop.achieve(this.#prop.TYPES.ACHV, id)
                $$event('achievement', this.get(id))
            });
    }
    get(achievementId) {
        const achievement = this.#achievements[achievementId];
        if(!achievement) throw new Error(`[ERROR] No Achievement[${achievementId}]`);
        return this.#system.clone(achievement);
    }

Unfinished
Have a chance
The id of the attribute cannot be found

property. Achievement () in JS

    achieve(prop, newData) {
        let key;
        switch(prop) {
            case this.TYPES.ACHV:
                const lastData = this.lsget(prop);
                this.lsset(
                    prop,
                    (lastData || []).concat([[newData, Date.now()]])
                );
                return;
            case this.TYPES.TLT: key = this.TYPES.ATLT; break;
            case this.TYPES.EVT: key = this.TYPES.AEVT; break;
            default: return;
        }
        const lastData = this.lsget(key) || [];
        this.lsset(
            key,
            Array.from(
                new Set(
                    lastData
                        .concat(newData||[])
                        .flat()
                )
            )
        )
    }

If it is a completed achievement, add the current time and return
If it is a talent or event, return the triggered talent or event
Return to other situations
Then add new data to the triggered talent or event and return
Well... Well, I don't understand

Ah... That's all about next()... I still don't understand

So I think the most important thing is these two sentences

const { effect, condition, grade, name, description } = this.get(talentId);
const { effect, branch, event: description, postEvent, grade } = this.get(eventId);
    get(talentId) {
        const talent = this.#talents[talentId];
        if(!talent) throw new Error(`[ERROR] No Talent[${talentId}]`);
        return this.#system.clone(talent);
    }
    get(eventId) {
        const event = this.#events[eventId];
        if(!event) throw new Error(`[ERROR] No Event[${eventId}]`);
        return this.#system.clone(event);
    }

But this get gets their own initialized properties... So it should also depend on how this thing is initialized

    initial({talents}) {
        this.#talents = talents;
        const emt = this.#system.function(this.#system.Function.CONDITION).extractMaxTriggers;
        for(const id in talents) {
            const talent = talents[id];
            talent.id= Number(id);
            talent.grade = Number(talent.grade);
            talent.max_triggers = emt(talent.condition);
            if(talent.replacement) {
                for(let key in talent.replacement) {
                    const obj = {};
                    for(let value of talent.replacement[key]) {
                        value = `${value}`.split('*');
                        obj[value[0]||0] = Number(value[1]) || 1;
                    }
                    talent.replacement[key] = obj;
                }
            }
        }
        return this.count;
    }
    initial({events}) {
        this.#events = events;
        for(const id in events) {
            const event = events[id];
            if(!event.branch) continue;
            event.branch = event.branch.map(b=>{
                b = b.split(':');
                b[1] = Number(b[1]);
                return b;
            });
        }
        return this.count;
    }

Then I feel that her initialization is not constructed from scratch. There is also an input value, which depends on what is input
Then you'll see that it's located in lief initial in JS

    async initial(i18nLoad, commonLoad) {
        const [age, talents, events, achievements, characters, specialThanks] = await Promise.all([
            i18nLoad('age'),
            i18nLoad('talents'),
            i18nLoad('events'),
            i18nLoad('achievement'),
            i18nLoad('character'),
            commonLoad('specialthanks'),
        ]);
        this.#specialThanks = specialThanks;

        const total = {
            [this.PropertyTypes.TACEV]: this.#achievement.initial({achievements}),
            [this.PropertyTypes.TEVT]: this.#event.initial({events}),
            [this.PropertyTypes.TTLT]: this.#talent.initial({talents}),
        };
        this.#property.initial({age, total});
        this.#character.initial({characters});
    }

The value used for initialization is promise All(), where i18nload and commonload are the input functions. It depends on who called the initial

That's strange. I can't find anyone who called this initial
Single step debugging

At first, run index

const core = new Life();
const game = new App();
globalThis.core = core;
globalThis.game = game;

Then the constructor of life

class Life {
    constructor() {
        this.#property = new Property(this);
        this.#event = new Event(this);
        this.#talent = new Talent(this);
        this.#achievement = new Achievement(this);
        this.#character = new Character(this);
    }

A series of
Finally

game.start(query);

Then I click to skip to the end of initial... Strange
Then you can only look at variables




Well, then I found it again

Ah, but I can't see what parameters are entered. Why
If you skip this, it's the only place you haven't seen

I just started to see this core. I thought it wasn't Life, so I didn't care
Later, I thought, well, since it's looking for references, it's really life initial()

		await core.initial(
            dataSet=>Laya.promises.loader.load(`data/${this.#language}/${dataSet}.json`, null, Laya.Loader.JSON),
            dataSet=>Laya.promises.loader.load(`data/${dataSet}.json`, null, Laya.Loader.JSON),
        );

What I found on the Internet seems to be LAYA loader. There is no LAYA promises. loader. Yes, it's amazing

Laya.promises = {
	...//Front strategy
    loader: {
        load: async function (url, progress, type) {
            return new Promise(function (resolve, reject) {
                try {
                    Laya.loader.load(url, Laya.Handler.create(null, ret=>resolve(ret), null, true), progress, type);
                } catch (e) {
                    reject(e);
                }
            });
        }

    }
};

Ah... It looks like a tool. I won't delve into it

Well, then go back to talent Initial() and event initial()

const emt = this.#system.function(this.#system.Function.CONDITION).extractMaxTriggers;

Here we use the system attribute, which is initialized in the constructor

class Talent {
    constructor(system) {
        this.#system = system;
    }

Talent is constructed in Life

class Life {
    constructor() {
        this.#property = new Property(this);
        this.#event = new Event(this);
        this.#talent = new Talent(this);
        this.#achievement = new Achievement(this);
        this.#character = new Character(this);
    }

So Talent's system is Life
Then life is called here Function

class Life {
    Function = {
        CONDITION: 'CONDITION',
        UTIL: 'UTIL',
    }

The last is a string CONDITION

The outer layer is life function

class Life {
    function(type) {
        switch (type) {
            case this.Function.CONDITION: return fCondition;
            case this.Function.UTIL: return util;
        }
    }

fCondition is called

import * as fCondition from '../functions/condition.js';

So I see condition JS, where

function extractMaxTriggers(condition) {
    // Assuming only age related talents can be triggered multiple times.
    const RE_AGE_CONDITION = /AGE\?\[([0-9\,]+)\]/;
    const match_object = RE_AGE_CONDITION.exec(condition);
    if (match_object == null) {
        // Not age related, single trigger.
        return 1;
    }

    const age_list = match_object[1].split(",");
    return age_list.length;
}

/AGE\?\[([0-9 \,] +) \] / is a regular expression, age \ [([0-9 \,] +) \] is the content, \? Is an escaped question mark character, \ [is an escaped parenthesis character, ([0-9 \,] +) saves the search results of [0-9 \,] +, + modifier represents at least one, [] represents the range, and the range is numbers 0 to 9 and commas. Then ([0-9 \,] +) is at least one number 0 to 9 and commas. The result of the whole expression is age? [at least one number 0 to 9 and comma]
This is the age condition obtained, and then separated by commas, return the number of age conditions... I didn't understand it, so I jumped

Back to talent Initial(), now emt becomes this function
The entered talents is a large table. For each element in the table, re assign id, grade and max_triggers attribute
Next, this talent should be replaced by other talents

To event Initial (), handle the branch

Then there was no
In fact, it still depends on the json of talent and event

Maybe the last thing I should look for is how to parse the agreed string

Ah, finally, other people's trade secrets are confused
reflection

2. Summary

It seems that the only thing we can do now is to stroke it again
Every year, increase one year, first check the talent, then check the event, and finally judge the achievement
When checking talents, select those talents that do not reach the upper limit of use times and judge whether they meet the effective conditions of talents. If they can take effect, apply the effect of talents and add 1 to the use times of this talent
When checking an event, judge whether the event has a branch. If there is no branch, the event will take effect directly. If there is a branch, judge whether the effective conditions of the branch event are met. If so, return a branch when the event takes effect, and then recursively check the branch event
It seems that the only thing I can understand is this hhh

Topics: Javascript JSON regex Game Development