diff --git a/idea.md b/idea.md
index f6939f9..498d56d 100644
--- a/idea.md
+++ b/idea.md
@@ -90,6 +90,11 @@ dam4.png ---- 存档 59
 [] 优化路径显示,瞬移可以闪一下再熄灭
 [] 勇士身上显示攻防血
 [] 优化地图拖动
-[] 楼层转换假如随机小贴士
+[] 楼层转换加入随机小贴士
 [] ui 中如果元素发生改变,那么做出背景亮一下再熄灭的效果
 [] 双击怪物手册拐点可以直接在拖动条上定位
+[] 重构技能树结构
+[] 技能树允许自动升级
+[] 重构装备系统
+[] 野外地图加入平行光
+[] 弹幕系统
diff --git a/src/core/main/custom/ui.ts b/src/core/main/custom/ui.ts
new file mode 100644
index 0000000..9359219
--- /dev/null
+++ b/src/core/main/custom/ui.ts
@@ -0,0 +1,145 @@
+import { Component, reactive } from 'vue';
+import { EmitableEvent, EventEmitter } from '../../common/eventEmitter';
+import { KeyCode } from '../../../plugin/keyCodes';
+import { Hotkey } from './hotkey';
+
+interface FocusEvent<T> extends EmitableEvent {
+    focus: (before: T | null, after: T) => void;
+    unfocus: (before: T | null) => void;
+    add: (item: T) => void;
+    pop: (item: T | null) => void;
+    register: (item: T[]) => void;
+    splice: (spliced: T[]) => void;
+}
+
+export class Focus<T = any> extends EventEmitter<FocusEvent<T>> {
+    targets: Set<T> = new Set();
+    /** 显示列表 */
+    stack: T[];
+    focused: T | null = null;
+
+    constructor(react?: boolean) {
+        super();
+        this.stack = react ? reactive([]) : [];
+    }
+
+    /**
+     * 聚焦于一个目标
+     * @param target 聚焦目标
+     * @param add 如果聚焦目标不在显示列表里面,是否自动追加
+     */
+    focus(target: T, add: boolean = false) {
+        if (target === this.focused) return;
+        const before = this.focused;
+        if (!this.stack.includes(target)) {
+            if (add) {
+                this.add(target);
+                this.focused = target;
+            } else {
+                console.warn(
+                    `聚焦于一个不存在的目标,同时没有传入自动追加的参数`,
+                    `聚焦目标:${target}`
+                );
+                return;
+            }
+        } else {
+            this.focused = target;
+        }
+        this.emit('focus', before, this.focused);
+    }
+
+    /**
+     * 取消聚焦
+     */
+    unfocus() {
+        const before = this.focused;
+        this.focused = null;
+        this.emit('unfocus', before);
+    }
+
+    /**
+     * 向显示列表中添加物品
+     * @param item 添加的物品
+     */
+    add(item: T) {
+        if (!this.targets.has(item)) {
+            console.warn(`向显示列表里面添加了不在物品集合里面的物品`);
+            return;
+        }
+        this.stack.push(item);
+        this.emit('add', item);
+    }
+
+    /**
+     * 弹出显示列表中的最后一个物品
+     */
+    pop() {
+        const item = this.stack.pop() ?? null;
+        this.emit('pop', item);
+        return item;
+    }
+
+    /**
+     * 从一个位置开始删除显示列表
+     * @param item 从哪开始删除,包括此项
+     */
+    splice(item: T) {
+        const index = this.stack.indexOf(item);
+        if (index === -1) {
+            this.emit('splice', []);
+            return;
+        }
+        this.emit('splice', this.stack.splice(index));
+    }
+
+    /**
+     * 注册一个物品
+     * @param item 要注册的物品
+     */
+    register(...item: T[]) {
+        item.forEach(v => {
+            this.targets.add(v);
+        });
+        this.emit('register', item);
+    }
+}
+
+interface GameUiEvent extends EmitableEvent {
+    close: () => void;
+    open: () => void;
+}
+
+export class GameUi extends EventEmitter<GameUiEvent> {
+    static uiList: GameUi[] = [];
+
+    component: Component;
+    hotkey?: Hotkey;
+
+    constructor(component: Component, hotkey?: Hotkey) {
+        super();
+        this.component = component;
+        this.hotkey = hotkey;
+        GameUi.uiList.push(this);
+    }
+}
+
+export class UiController extends Focus<GameUi> {
+    constructor() {
+        super(true);
+        this.on('splice', spliced => {
+            spliced.forEach(v => {
+                v.emit('close');
+            });
+        });
+        this.on('add', item => item.emit('open'));
+    }
+
+    /**
+     * 执行按键操作
+     * @param key 按键的KeyCode
+     * @param e 按键操作事件
+     */
+    emitKey(key: KeyCode, e: KeyboardEvent) {
+        this.focused?.hotkey?.emitKey(key, e);
+    }
+}
diff --git a/src/core/main/setting.ts b/src/core/main/setting.ts
index b5b752f..8a93a7d 100644
--- a/src/core/main/setting.ts
+++ b/src/core/main/setting.ts
@@ -3,7 +3,6 @@ import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
 import { transition } from '../../plugin/uiController';
 import { loading } from '../loader/load';
 import { hook } from './game';
-import { isMobile } from '../../plugin/use';
 import { GameStorage } from './storage';
 import { triggerFullscreen } from '../../plugin/utils';
 
@@ -352,7 +351,6 @@ mainSetting
         new MotaSetting()
             .register('fullscreen', '全屏游戏', false)
             .register('halo', '光环显示', true)
-            .register('frag', '打怪特效', true)
             .register('itemDetail', '宝石血瓶显伤', true)
             .register('transition', '界面动画', false)
             .register('antiAlias', '抗锯齿', false)
@@ -376,10 +374,17 @@ mainSetting
     )
     .register(
         'utils',
-        '功能设置',
+        '系统设置',
         new MotaSetting()
             .register('betterLoad', '优化加载', true)
             .register('autoScale', '自动放缩', true)
+    )
+    .register(
+        'fx',
+        '特效设置',
+        new MotaSetting()
+            .register('paraLight', '野外阴影', true)
+            .register('frag', '打怪特效', true)
     );
 
 interface SettingStorage {
@@ -394,6 +399,7 @@ interface SettingStorage {
     fixed: boolean;
     betterLoad: boolean;
     autoScale: boolean;
+    paraLight: boolean;
 }
 
 const storage = new GameStorage<SettingStorage>(
@@ -404,7 +410,6 @@ loading.once('coreInit', () => {
     mainSetting.reset({
         'screen.fullscreen': !!document.fullscreenElement,
         'screen.halo': !!storage.getValue('showHalo', true),
-        'screen.frag': !!storage.getValue('frag', true),
         'screen.itemDetail': !!storage.getValue('itemDetail', true),
         'screen.transition': !!storage.getValue('transition', false),
         'screen.antiAlias': !!storage.getValue('antiAlias', false),
@@ -413,7 +418,9 @@ loading.once('coreInit', () => {
         'screen.criticalGem': !!storage.getValue('criticalGem', false),
         'action.fixed': !!storage.getValue('fixed', true),
         'utils.betterLoad': !!storage.getValue('betterLoad', true),
-        'utils.autoScale': !!storage.getValue('autoScale', true)
+        'utils.autoScale': !!storage.getValue('autoScale', true),
+        'fx.paraLight': !!storage.getValue('paraLight', true),
+        'fx.frag': !!storage.getValue('frag', true)
     });
 });
 
diff --git a/src/data/settings.json b/src/data/settings.json
index 144ba27..ccf6533 100644
--- a/src/data/settings.json
+++ b/src/data/settings.json
@@ -5,7 +5,6 @@
             "请按下方的按钮打开。进入或退出全屏后请存读档一下,以解决一部分绘制问题。"
         ],
         "halo": ["开启后,会在地图上显示范围光环。"],
-        "frag": ["开启后,在打败怪物后会触发怪物碎裂特效。"],
         "itemDetail": ["是否在地图上显示宝石血瓶装备等增加的属性值"],
         "transition": [
             "是否展示当一个ui界面,如怪物手册等的打开与关闭时的动画。当此项开启时,",
@@ -14,7 +13,6 @@
         "antiAlias": [
             "是否开启抗锯齿。开启后,画面会变得不那么锐利,观感更加舒适;关闭后,可以更好地展现出像素感,同时部分像素错误也不会出现。"
         ],
-
         "fontSize": [
             "在各种 ui 界面中显示的文字大小,范围为 8 - 28。注意,字体过大可能会引起 ui 布局发生错误"
         ],
@@ -57,5 +55,11 @@
             "<br>",
             "2. 如果缩放后游戏画面高度高于页面高度的95%,那么缩小一个缩放比例,否则保持最大比例"
         ]
+    },
+    "fx": {
+        "paraLight": [
+            "是否开启野外的平行光阴影,在野外将会显示平行光阴影,模拟太阳光,拥有不错的视觉效果"
+        ],
+        "frag": ["开启后,在打败怪物后会触发怪物碎裂特效。"]
     }
 }