File size: 6,183 Bytes
b76daae
 
 
 
2452398
6ab7697
c255694
5825182
2452398
 
b76daae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5825182
 
 
2452398
b76daae
 
2452398
c255694
 
17a347d
5825182
 
b76daae
5825182
 
17a347d
 
5825182
 
 
17a347d
6e6388e
5825182
17a347d
 
5825182
17a347d
 
c255694
 
 
17a347d
 
 
2452398
17a347d
5825182
17a347d
 
5825182
 
 
6e6388e
5825182
6e6388e
5825182
6e6388e
c255694
 
 
17a347d
 
2452398
 
17a347d
 
b76daae
 
 
 
 
3ed500d
b76daae
2452398
b76daae
6ab7697
2452398
 
5825182
 
b76daae
 
55f0dce
 
b76daae
 
 
 
 
17a347d
b76daae
3ed500d
b76daae
5825182
 
 
17a347d
b76daae
17a347d
 
 
 
 
b76daae
 
 
 
 
 
 
 
 
 
 
 
 
6e6388e
 
60d173b
 
 
6e6388e
 
 
 
6d09f00
 
 
6e6388e
6d09f00
b76daae
60d173b
b76daae
2452398
 
 
 
 
 
 
b76daae
2452398
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from dataclasses import dataclass
from collections import defaultdict
from typing import Dict

from base.attribute import Attribute
from base.constant import FRAME_PER_SECOND
from base.skill import Skill, DotDamage, PetDamage
from utils.parser import School


@dataclass
class Detail:
    damage: int = 0
    critical_damage: int = 0
    expected_damage: float = 0.
    critical_strike: float = 0.

    gradients: Dict[str, float] = None

    critical_count: int = 0
    count: int = 0

    def __post_init__(self):
        self.gradients = defaultdict(float)

    @property
    def actual_critical_strike(self):
        if self.count:
            return self.critical_count / self.count
        return 0


def filter_status(status, school: School, skill_id):
    buffs = []
    for buff_id, buff_level, buff_stack in status:
        buff = school.buffs[buff_id]
        if not buff.activate:
            continue
        buff.buff_level, buff.buff_stack = buff_level, buff_stack
        if buff.gain_attributes:
            buffs.append(buff)
        elif skill_id in buff.gain_skills:
            buffs.append(buff)

    return tuple(sorted(buffs, key=lambda x: x.buff_id))


def add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
    final_buffs = []
    if not snapshot_buffs:
        for buff in current_buffs:
            buff.add_all(attribute, skill)
            final_buffs.append(buff)
    elif isinstance(skill, DotDamage):
        for buff in snapshot_buffs:
            if buff.add_dot(attribute, skill, True):
                final_buffs.append(buff)
        for buff in current_buffs:
            if buff.add_dot(attribute, skill, False):
                final_buffs.append(buff)
    elif isinstance(skill, PetDamage):
        for buff in snapshot_buffs:
            buff.add_all(attribute, skill)
            final_buffs.append(buff)
    for buff in target_buffs:
        buff.add_all(attribute, skill)

    return final_buffs + list(target_buffs)


def sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
    if not snapshot_buffs:
        for buff in current_buffs:
            buff.sub_all(attribute, skill)
    elif isinstance(skill, DotDamage):
        for buff in snapshot_buffs:
            buff.sub_dot(attribute, skill, True)
        for buff in current_buffs:
            buff.sub_dot(attribute, skill, False)
    elif isinstance(skill, PetDamage):
        for buff in snapshot_buffs:
            buff.sub_all(attribute, skill)
    for buff in target_buffs:
        buff.sub_all(attribute, skill)


def concat_buffs(buffs):
    buffs = ",".join(buff.display_name for buff in buffs)
    if not buffs:
        buffs = "~"
    return buffs


def analyze_details(record, duration: int, attribute: Attribute, school: School):
    total = Detail()
    details = {}
    summary = {}
    duration *= FRAME_PER_SECOND

    for skill, status in record.items():
        skill_id, skill_level, skill_stack = skill
        skill: Skill = school.skills[skill_id]
        if not skill.activate:
            continue
        skill.skill_level, skill.skill_stack = skill_level, skill_stack
        skill_name = skill.skill_name

        skill_detail = details[skill.display_name] = {}
        if not (skill_summary := summary.get(skill_name)):
            skill_summary = summary[skill_name] = Detail()
        skill_total = skill_detail[""] = Detail()
        for (current_status, snapshot_status, target_status), timeline in status.items():
            if not (timeline := [t for t in timeline if t[0] < duration]):
                continue
            critical_timeline = [t for t in timeline if t[1]]

            current_buffs = filter_status(current_status, school, skill_id)
            snapshot_buffs = filter_status(snapshot_status, school, skill_id)
            target_buffs = filter_status(target_status, school, skill_id)

            buffs = add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)
            buffs = concat_buffs(buffs)
            detail = skill_detail[buffs] = Detail(*skill(attribute))
            detail.gradients = analyze_gradients(skill, attribute)
            sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)

            detail.critical_count += len(critical_timeline)
            detail.count += len(timeline)
            skill_total.critical_count += len(critical_timeline)
            skill_total.count += len(timeline)

            skill_total.damage += detail.damage * len(timeline)
            skill_total.critical_damage += detail.critical_damage * len(timeline)
            skill_total.expected_damage += detail.expected_damage * len(timeline)
            skill_total.critical_strike += detail.critical_strike * len(timeline)
            for attr, residual_damage in detail.gradients.items():
                skill_total.gradients[attr] += residual_damage * len(timeline)

        if skill_total.count:
            total.expected_damage += skill_total.expected_damage
            skill_summary.expected_damage += skill_total.expected_damage
            skill_summary.critical_count += skill_total.critical_strike
            skill_summary.count += skill_total.count
            skill_total.damage /= skill_total.count
            skill_total.critical_damage /= skill_total.count
            skill_total.expected_damage /= skill_total.count
            skill_total.critical_strike /= skill_total.count
            for attr, residual_damage in skill_total.gradients.items():
                total.gradients[attr] += residual_damage
                skill_total.gradients[attr] /= skill_total.count
        else:
            details.pop(skill.display_name)

    summary = {skill: detail for skill, detail in summary.items() if detail.count}
    return total, summary, details


def analyze_gradients(skill, attribute):
    results = {}
    for attr, value in attribute.grad_attrs.items():
        origin_value = getattr(attribute, attr)
        setattr(attribute, attr, origin_value + value)
        _, _, results[attr], _ = skill(attribute)
        setattr(attribute, attr, origin_value)
    return results