diff --git a/core/character.py b/core/character.py
index 77f7f09..d706503 100644
--- a/core/character.py
+++ b/core/character.py
@@ -2,6 +2,7 @@ from .config_manager import Config
 from .constant import Constant
 from .error import ArcError, InputError, ItemNotEnough, NoData
 from .item import CollectionItemMixin, ItemCore
+from .sql import UserKVTable
 
 
 class Level:
@@ -55,6 +56,7 @@ class Skill:
 class CharacterValue:
     def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
         self.set_parameter(start, mid, end)
+        self.addition: float = 0
 
     @staticmethod
     def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
@@ -87,9 +89,9 @@ class CharacterValue:
 
     def get_value(self, level: Level):
         if level.min_level <= level.level <= level.mid_level:
-            return self._calc_char_value_20_math(level.level, self.start, self.mid)
+            return self._calc_char_value_20_math(level.level, self.start, self.mid) + self.addition
         if level.mid_level < level.level <= level.max_level:
-            return self._calc_char_value_30(level.level, self.mid, self.end)
+            return self._calc_char_value_30(level.level, self.mid, self.end) + self.addition
         return 0
 
 
@@ -231,6 +233,8 @@ class UserCharacter(Character):
 
         self.skill_flag: bool = None
 
+        self.fatalis_is_limited: bool = False
+
     @property
     def skill_id_displayed(self) -> str:
         '''对外显示的技能id'''
@@ -295,6 +299,22 @@ class UserCharacter(Character):
         if self.character_id in (21, 46):
             self.voice = [0, 1, 2, 3, 100, 1000, 1001]
 
+        if self.character_id == 55:
+            # fatalis 提升数值
+            # prog & overdrive += 世界模式中完成的所有非无限地图的台阶数之和 / 30
+            if Config.CHARACTER_FULL_UNLOCK:
+                addition = Constant.FATALIS_MAX_VALUE
+                self.fatalis_is_limited = True
+            else:
+                kvd = UserKVTable(self.c, self.user.user_id, 'world')
+                steps = kvd['total_step_count'] or 0
+                addition = steps / 30
+                if addition >= Constant.FATALIS_MAX_VALUE:
+                    addition = Constant.FATALIS_MAX_VALUE
+                    self.fatalis_is_limited = True
+            self.prog.addition = addition
+            self.overdrive.addition = addition
+
         self.select_character_core()
         if self.character_id == 72:
             self.update_insight_state()
@@ -323,7 +343,7 @@ class UserCharacter(Character):
         if self.voice:
             r['voice'] = self.voice
         if self.character_id == 55:
-            r['fatalis_is_limited'] = False  # emmmmmmm
+            r['fatalis_is_limited'] = self.fatalis_is_limited
         if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
             r['base_character_id'] = 1
 
diff --git a/core/constant.py b/core/constant.py
index ffdbe70..a553359 100644
--- a/core/constant.py
+++ b/core/constant.py
@@ -1,6 +1,6 @@
 from .config_manager import Config
 
-ARCAEA_SERVER_VERSION = 'v2.12.0.3'
+ARCAEA_SERVER_VERSION = 'v2.12.0.4'
 ARCAEA_DATABASE_VERSION = 'v2.12.0.4'
 ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
 
@@ -32,6 +32,7 @@ class Constant:
     SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
     SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble', 'breakbreak', 'straightintolights', 'virtus', 'yomibitoshirazu',
                         'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
+    FATALIS_MAX_VALUE = 100
 
     MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
 
@@ -68,7 +69,6 @@ class Constant:
     LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
     LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
 
-
     LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4  # Units: seconds
     LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000]
     LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1]
diff --git a/core/score.py b/core/score.py
index 4772ccb..fd91636 100644
--- a/core/score.py
+++ b/core/score.py
@@ -377,6 +377,7 @@ class UserPlay(UserScore):
         if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
             raise StaminaNotEnough('Stamina is not enough.')
 
+        fatalis_stamina_multiply = 1
         self.user.select_user_about_character()
         if not self.user.is_skill_sealed:
             self.user.character.select_character_info()
@@ -387,16 +388,13 @@ class UserPlay(UserScore):
                 self.invasion_flag = _flag
             elif self.user.character.skill_id_displayed == 'skill_fatalis':
                 # 特殊判断hikari fatalis的双倍体力消耗
-                self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
-                    self.stamina_multiply * 2
-                self.user.stamina.update()
-                return None
+                fatalis_stamina_multiply = 2
 
         self.clear_play_state()
         self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h,:i,:j)''', {
             'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 'i': self.skill_chinatsu_flag, 'j': self.invasion_flag, 't': self.song_token})
 
-        self.user.stamina.stamina -= self.user.current_map.stamina_cost * self.stamina_multiply
+        self.user.stamina.stamina -= self.user.current_map.stamina_cost * self.stamina_multiply * fatalis_stamina_multiply
         self.user.stamina.update()
 
     def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
diff --git a/core/sql.py b/core/sql.py
index 6852427..a0a2dbf 100644
--- a/core/sql.py
+++ b/core/sql.py
@@ -525,3 +525,35 @@ class MemoryDatabase:
 @register
 def atexit():
     MemoryDatabase.conn.close()
+
+
+class UserKVTable:
+    '''用户键值对表'''
+
+    def __init__(self, c=None, user_id: int = None, class_name: str = None) -> None:
+        self.c = c
+        self.user_id = user_id
+        self.class_name = class_name
+
+    def get(self, key: str, idx: int = 0):
+        '''获取键值对'''
+        x = self.c.execute(
+            '''select value from user_kvdata where user_id = ? and class = ? and key = ? and idx = ?''', (self.user_id, self.class_name, key, idx)).fetchone()
+        return x[0] if x else None
+
+    def set(self, key: str, value, idx: int = 0) -> None:
+        '''设置键值对'''
+        self.c.execute('''insert or replace into user_kvdata values(?,?,?,?,?)''',
+                       (self.user_id, self.class_name, key, idx, value))
+
+    def __getitem__(self, args):
+        if isinstance(args, tuple):
+            return self.get(*args)
+        else:
+            return self.get(args)
+
+    def __setitem__(self, args, value):
+        if isinstance(args, tuple):
+            self.set(args[0], value, args[1])
+        else:
+            self.set(args, value)
diff --git a/core/user.py b/core/user.py
index dfc6bc6..c2e3d67 100644
--- a/core/user.py
+++ b/core/user.py
@@ -13,8 +13,8 @@ from .item import UserItemList
 from .limiter import ArcLimiter
 from .mission import UserMissionList
 from .score import Score
-from .sql import Query, Sql
-from .world import Map, UserMap, UserStamina
+from .sql import Query, Sql, UserKVTable
+from .world import Map, MapParser, UserMap, UserStamina
 
 
 def code_get_id(c, user_code: str) -> int:
@@ -741,6 +741,35 @@ class UserInfo(User):
             '''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id))
         self.world_rank_score = x[0]
 
+    def update_user_world_complete_info(self) -> None:
+        '''
+            更新用户的世界模式完成信息,包括两个部分
+
+            1. 每个章节的完成地图数量,为了 salt 技能
+            2. 全世界模式完成台阶数之和,为了 fatalis 技能
+        '''
+        kvd = UserKVTable(self.c, self.user_id, 'world')
+
+        for chapter_id, map_ids in MapParser.chapter_info_without_repeatable.items():
+            self.c.execute(
+                f'''select map_id, curr_position from user_world where user_id = ? and map_id in ({','.join(['?']*len(map_ids))})''',
+                (self.user_id, *map_ids)
+            )
+            x = self.c.fetchall()
+            n = 0
+            for map_id, curr_position in x:
+                step_count = MapParser.world_info[map_id]['step_count']
+                if curr_position == step_count - 1:
+                    n += 1
+            kvd['chapter_complete_count', chapter_id] = n
+
+        self.c.execute(
+            '''select sum(curr_position) + count(*) from user_world where user_id = ?''', (self.user_id,)
+        )
+        x = self.c.fetchone()
+        if x is not None:
+            kvd['total_step_count'] = x[0] or 0
+
     def select_user_one_column(self, column_name: str, default_value=None, data_type=None) -> None:
         '''
             查询user表的某个属性
diff --git a/database/init/arc_data.py b/database/init/arc_data.py
index 589930b..c3c28d0 100644
--- a/database/init/arc_data.py
+++ b/database/init/arc_data.py
@@ -30,13 +30,13 @@ class InitData:
                    46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 75, 54, 90, 41, 34, 30, 55, 66, 55, 62, 81, 44, 46]
 
     frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
-              65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 115, 80, 50, 61.6, 48, 37, 90, 60, 50, 102, 76, 0, 89]
+              65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 115, 80, 50, 61.6, 48, 37, 90, 60, 50, 102, 76, 44, 89]
 
     prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
-              80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 80, 90, 67, 41, 55, 50, 103, 66, 35, 62, 75, 0, 53]
+              80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 80, 90, 67, 41, 55, 50, 103, 66, 35, 62, 75, 50, 53]
 
     overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
-                   56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 75, 64, 90, 41, 34, 30, 55, 66, 55, 72, 91, 0, 56]
+                   56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 75, 64, 90, 41, 34, 30, 55, 66, 55, 72, 91, 44, 56]
 
     char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
                  0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 4, 2, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 2]
diff --git a/database/init/tables.sql b/database/init/tables.sql
index a9af045..e5c74aa 100644
--- a/database/init/tables.sql
+++ b/database/init/tables.sql
@@ -283,6 +283,17 @@ status int,
 primary key(user_id, mission_id)
 );
 
+-- value 无类型
+create table if not exists user_kvdata(
+user_id int,
+class text,
+key text,
+idx int,
+value,
+primary key(user_id, class, key, idx)
+);
+
+
 create index if not exists best_score_1 on best_score (song_id, difficulty);
 
 PRAGMA journal_mode = WAL;
diff --git a/web/index.py b/web/index.py
index 5a679a8..565efc8 100644
--- a/web/index.py
+++ b/web/index.py
@@ -443,7 +443,7 @@ def all_character():
 def change_character():
     # 修改角色数据
     skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
-                 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene']
+                 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid']
     return render_template('web/changechar.html', skill_ids=skill_ids)