Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Result orientated optimization target #173

Open
Kamii0909 opened this issue Mar 3, 2024 · 2 comments
Open

[Feature] Result orientated optimization target #173

Kamii0909 opened this issue Mar 3, 2024 · 2 comments

Comments

@Kamii0909
Copy link
Contributor

Motivation

Currently, the optimizer use an rudimentary method to sort builds, which is more or less: get all builds permutations -> sort them
This is limiting for various reasons:

  • Relies on ad-hoc and prebuilt index (Basic, Skill, Ultimate...): making it impossible to address complex compositions (optimizing for both FuA and Skill, for example).
  • Error-prone scoring system to filter out irrelevant relics: this system is rather arbitrary, it is impossible to prove the global optimality of a build. There is little need to stop at local optimality when global optimality will work, as evidenced by genshin-optimizer.

The result of such process is no better than manual eyeball.

Furthermore, the damage system is inherently flawed and ad-hoc, which currently is (and will continue to) be problematic to work with:

  • Sets and conditionals are hardcoded: with more and more sets being introduced overtime, it is going to be a problem eventually.
  • Hit splits are not handled well: things that seems unchanging at first glance, for example, IL 3SP Basic, in fact, is not. IL has 7 hits, each hit increasing the damage of the later one by adding the talent DMG% buff and skill Crit DMG buff. This also holds true for other hit-related buff like The Ash Blazing Grand Duke or Swordplay.

Personally, code for the current optimizer lacks proper documentation and is rather confusing. Without significant time investment, I can't even verify things like "does BS EHR -> DMG% work?".

I raised this issue to discuss about the aforementioned concerns. Before I produce a full-fledged pull requests, I want to use this issue to first gather some feedback, before I did something that is not on the vision of the site.

Proposal

Stat API

As part of the current damage calculation stack, the stat collection system need a redesign. A stat modification (buff, debuff, whatever) may come from many source, with their own conditional on top of that. We need a mechanism to effectively model them with an intuitive game-like API, this will greatly simplify game version update. I reckon the upperbound of the scope should be "everything can be changed after you pick an action and enemy". So things like "IL gains 24% CD when attacking enemies with Imaginary Weakness" should be out of scope.

For example, Ting Yun provides an unconditional 50% DMG Boost with her Ultimate. Topaz Skill with 50% FuA DMG Boost.

// we can reuse all of these
const tyUlt: StatModification = { dmgBoost: 0.5 }
const riverFlowsInSpring = {
    basic: {
        percent: {
            speed: 0.12
        }
    },
    dmgBoost: 0.24
}
// ConditionalStat.trait(trait: Trait, stat: StatModification)
const topazSkillBuff = ConditionalStat.trait(Trait.FOLLOW_UP, { dmgBoost: 0.5 });
// ConditionalStat.stat((currentStat: Readonly<CurrentStat>) => StatModification | null)
// Why not group both into a single API? 
// The above type can be applied very early (before relic stat calculation), the below cannot.
// Note that we actually risk circular dependency here, but whatever, it's Hoyoverse's fault. 
// When the time comes, we will deal with it.  
// For example, Kazuha buff cannot scale with other percentage EM buff, a surprisingly logical yet arbitrary limitation.
const blackSwanEhrToDmg = ConditionalStat.stat(curr => { dmgBoost: Math.max(curr.effectHitRate * 0.6, 0.72) };
const hertaSpaceStation = ConditionalStat.stat(curr => curr.speed >= 120 ? { basic: { percent: { atk: 12 }}}: null);
const rutilantArena = ConditionalStat.stat(curr => 
    curr.traits in [Traits.NORMAL_ATTACK, Traits.SKILL] && curr.critRate >= 0.7 
    ? { dmgBoost: 0.2 } : null);
/*
calculateStats(
    unconditional: StatModification[],
    element: ConditionalStat<Element>[],
    traits: ConditionalStat<Trait[]>[],
    stats: ConditionalStat<CurrentStat>
): FinalStats
*/

This should also close #153 since FinalStats can trace all its element, traits, and stats dependencies.
I'm honestly debating whether Element ConditionalStat is even relevant at all, because then the stat system should also track which element the attack is, which is an extremely useless feature, because such a thing cannot be changed after "the player chose an action". But then, it will simplify a fair bit for downstream, so well?

Optimization request

The aforementioned stat system also provide the base for complex/custom result-orientated optimization. For example, we can provide the formula for the optimization of IL 3SP and then Ult:

// Some are not valid JS/TS, but is used to provide type information. 
// It's unlikely that the actual API will be in builder/factory format,.
// I used it here because it's rather easy to read
const target: OptimizationRequest = OptimizationRequest.builder()
    // These stats cannot be changed between each step
    .with(stat: StatBuilder => stat
        .unconditional(mods: StatModification[])
        .element(eleConds: ElementConditional[])
        .trait(trConds: TraitConditional[])
        .stat(statConds: StatConditional[])
        .build(): ModifyingStats)
    .element(ele: Element) // Element.IMAGINARY
    // IL Hit split is 14.2 x6, 14.8
    // 2nd -> 7th hit will get 10% DMG Bonus (stack)
    // 4th -> 7th hit will get 12% Crit DMG (stack)
    .addStep(step: StepBuilder => step
        .damage() // calculate for damage
        .traits(traits: Trait[]) // [Trait.NORMAL_ATTACK]
        // multiplier = 5 * 0.142, flat (additional damage) = 0
        // formula = (base) => base.atk * multiplier + flat
        .baseMultiplier(formula: (base: BaseStat) => number))
        // This attack can Crit, assume average crit
        // .dot(): this attack can't crit
        .averageCrit(): Step) 
    .addStep(step => step.damage().traits([Trait.NORMAL_ATTACK])
        // should we even allow element/trait/stat conditionals here? 
        // IMPORTANT: will it simplify downstream code?
        .with(stat: StepStatBuilder => stat
            // each step can know stat modifications by previous steps
            // prev won't contain ones applied at the start
            // done through a key so that they don't interfere with each other needlessly
            .unconditional(
                key: string, 
                prev: StatModification | null => StatModification | null)
            .unconditional("Talent", _ => { dmgBoost: 0.1 }) 
            .build(): ModifyingStats)
        .baseMultiplier(GenericAtkFormula.with(5 * 0.142, 0))
        .averageCrit())
    // A generic one
    .addStep(step => step...
        .with(stat => stat
            .unconditional(
                "Talent", 
                prev => { 
                    dmgBoost: Math.max(prev.dmgBoost + 0.1, 0.6)
                })
            .unconditional(
                "Talent",
                prev => {
                    crit: {
                        critDmg: Math.max(prev.crit.critDmg + 0.12, 0.48)
                    }
                })
            .build())
        .averageCrit())
    // similarly, Skill buff
    .build();
    
const result: OptimizationResult = target.optimize(relics: RelicInformation);
result.getCurrentProgress();

There are still multiple problems with this approach, for example, parallelization, performance, flexibility, but you get the idea. This is just an API draft anyway. We probably want some composition for OptimizationTarget, but that should take a fair bit of work.

Personal note

Personally, I'm not familliar with React technology, so it's unlikely that I can contribute to UI, so this is as much sugar that I can personally provide. I'm from a strongly typed language background, so I'm comfortable (in fact, I can't work with vanilla JS at all lol) working everything with Typescript.

@fribbels
Copy link
Owner

fribbels commented Mar 3, 2024

Hi @Kamii0909 I appreciate the feedback! I agree the optimization system needs an overhaul - the project is still young and we could use a hand in designing the api. Its late here but please drop by the discord server's #dev channel for more discussion tmrw/next week, I will be able to respond better on discord than in a ticket.

@fribbels
Copy link
Owner

fribbels commented Mar 3, 2024

There's a lot of backlog tasks related to this one that we could use help with:

#153

#115

#41

#8

Kamii0909 pushed a commit to Kamii0909/engine-beta that referenced this issue Mar 4, 2024
@Kamii0909 Kamii0909 mentioned this issue Mar 4, 2024
10 tasks
@fribbels fribbels changed the title Feature: Result orientated optimization target [Feature] Result orientated optimization target Mar 9, 2024
Kamii0909 pushed a commit to Kamii0909/engine-beta that referenced this issue Mar 16, 2024
Kamii0909 pushed a commit to Kamii0909/engine-beta that referenced this issue Mar 25, 2024
Kamii0909 pushed a commit to Kamii0909/engine-beta that referenced this issue Mar 25, 2024
Kamii0909 pushed a commit to Kamii0909/engine-beta that referenced this issue Mar 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In progress
Development

No branches or pull requests

2 participants