From 5e0d8ac7d45a8bdc5418e7ad9fead4c98d961841 Mon Sep 17 00:00:00 2001
From: unanmed <1319491857@qq.com>
Date: Sun, 5 Feb 2023 18:17:10 +0800
Subject: [PATCH] =?UTF-8?q?=E7=82=B9=E5=85=89=E6=BA=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
idea.md | 4 +
public/libs/maps.js | 2 +
public/libs/ui.js | 3 +-
public/project/floors/MT42.js | 7 +
public/project/floors/MT46.js | 108 +++++---
public/project/functions.js | 2 +
public/project/plugins.js | 8 +-
src/initPlugin.ts | 8 +-
src/plugin/utils.ts | 3 +-
src/plugin/webgl/canvas.ts | 123 +++++++++
src/plugin/webgl/gameShadow.ts | 131 +++++++++
src/plugin/webgl/martrix.ts | 160 +++++++++++
src/plugin/webgl/polygon.ts | 89 +++++++
src/plugin/webgl/shadow.ts | 468 +++++++++++++++++++++++++++++++++
src/plugin/webgl/utils.ts | 8 +
src/types/map.d.ts | 2 +-
src/types/util.d.ts | 7 +
17 files changed, 1082 insertions(+), 51 deletions(-)
create mode 100644 src/plugin/webgl/canvas.ts
create mode 100644 src/plugin/webgl/gameShadow.ts
create mode 100644 src/plugin/webgl/martrix.ts
create mode 100644 src/plugin/webgl/polygon.ts
create mode 100644 src/plugin/webgl/shadow.ts
create mode 100644 src/plugin/webgl/utils.ts
diff --git a/idea.md b/idea.md
index 3542621..34f9a8b 100644
--- a/idea.md
+++ b/idea.md
@@ -26,6 +26,10 @@
### 第三章 战争
+#### 技能
+
+闪避:每 M 回合闪避一次,减少 N%的伤害
+
## 机制
### 通用
diff --git a/public/libs/maps.js b/public/libs/maps.js
index dd32a1f..139c689 100644
--- a/public/libs/maps.js
+++ b/public/libs/maps.js
@@ -3202,6 +3202,7 @@ maps.prototype.removeBlock = function (x, y, floorId) {
const block = blocks[i];
this.removeBlockByIndex(i, floorId);
this._removeBlockFromMap(floorId, block);
+ core.updateShadow(true);
return true;
}
return false;
@@ -3364,6 +3365,7 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) {
}
}
}
+ core.updateShadow(true);
};
maps.prototype.animateSetBlock = function (
diff --git a/public/libs/ui.js b/public/libs/ui.js
index 5b28e9b..716dd67 100644
--- a/public/libs/ui.js
+++ b/public/libs/ui.js
@@ -4226,7 +4226,8 @@ ui.prototype.deleteCanvas = function (name) {
////// 删除所有动态canvas //////
ui.prototype.deleteAllCanvas = function () {
- return this.deleteCanvas(function () {
+ this.deleteCanvas(function () {
return true;
});
+ if (main.mode === 'play' && !core.isReplaying()) core.initShadowCanvas();
};
diff --git a/public/project/floors/MT42.js b/public/project/floors/MT42.js
index 910c99a..0f33595 100644
--- a/public/project/floors/MT42.js
+++ b/public/project/floors/MT42.js
@@ -31,6 +31,13 @@ main.floors.MT42=
5,
0
]
+ },
+ "8,12": {
+ "floorId": "MT46",
+ "loc": [
+ 7,
+ 8
+ ]
}
},
"beforeBattle": {},
diff --git a/public/project/floors/MT46.js b/public/project/floors/MT46.js
index f55080e..c6b28bd 100644
--- a/public/project/floors/MT46.js
+++ b/public/project/floors/MT46.js
@@ -1,45 +1,71 @@
main.floors.MT46=
{
-"floorId": "MT46",
-"title": "冰封高原",
-"name": "46",
-"width": 15,
-"height": 15,
-"canFlyTo": true,
-"canFlyFrom": true,
-"canUseQuickShop": true,
-"cannotViewMap": false,
-"images": [],
-"ratio": 8,
-"defaultGround": "T580",
-"bgm": "winter.mp3",
-"firstArrive": [],
-"eachArrive": [],
-"parallelDo": "",
-"events": {},
-"changeFloor": {},
-"beforeBattle": {},
-"afterBattle": {},
-"afterGetItem": {},
-"afterOpenDoor": {},
-"autoEvent": {},
-"cannotMove": {},
-"cannotMoveIn": {},
-"map": [
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ "floorId": "MT46",
+ "title": "冰封高原",
+ "name": "46",
+ "width": 15,
+ "height": 15,
+ "canFlyTo": true,
+ "canFlyFrom": true,
+ "canUseQuickShop": true,
+ "cannotViewMap": false,
+ "images": [],
+ "ratio": 8,
+ "defaultGround": "T580",
+ "bgm": "winter.mp3",
+ "firstArrive": [],
+ "eachArrive": [],
+ "parallelDo": "",
+ "events": {},
+ "changeFloor": {},
+ "beforeBattle": {},
+ "afterBattle": {},
+ "afterGetItem": {},
+ "afterOpenDoor": {},
+ "autoEvent": {},
+ "cannotMove": {},
+ "cannotMoveIn": {},
+ "map": [
+ [ 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
+ [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
+ [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
+ [ 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
+ [ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
+ [ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1],
+ [ 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0],
+ [ 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0],
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
+ [ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
+ [ 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
+ [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
+ [ 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
+ [ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
+ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0]
],
+ "bgmap": [
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300],
+ [300,300,300,300,300,300,300,300,300,300,300,300,300,300,300]
+],
+ "fgmap": [
+
+],
+ "bg2map": [
+
+],
+ "fg2map": [
+
+]
}
\ No newline at end of file
diff --git a/public/project/functions.js b/public/project/functions.js
index 98166f4..b362973 100644
--- a/public/project/functions.js
+++ b/public/project/functions.js
@@ -150,6 +150,8 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
// ---------- 重绘新地图;这一步将会设置core.status.floorId ---------- //
core.drawMap(floorId);
+ core.updateShadow();
+
// 切换楼层BGM
if (core.status.maps[floorId].bgm) {
var bgm = core.status.maps[floorId].bgm;
diff --git a/public/project/plugins.js b/public/project/plugins.js
index 3a392b9..e439faf 100644
--- a/public/project/plugins.js
+++ b/public/project/plugins.js
@@ -2,6 +2,7 @@
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
init: function () {
+ // 只看插件没用,插件是与vite样板高度融合的,所以要看的话就在游戏内的百科全书-关于游戏内点那个开源地址吧
this._afterLoadResources = function () {
if (!main.replayChecking && main.mode === 'play') {
main.forward();
@@ -770,12 +771,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
core.status.hero = new Proxy(hero, handler);
core.status.maps[floorId].blocks.forEach(function (block) {
- if (
- block.event.cls !== 'items' ||
- block.event.id === 'superPotion' ||
- block.disable
- )
- return;
+ if (block.event.cls !== 'items' || block.disable) return;
const x = block.x,
y = block.y;
// v2优化,只绘制范围内的部分
diff --git a/src/initPlugin.ts b/src/initPlugin.ts
index ed5a5d2..baaac03 100644
--- a/src/initPlugin.ts
+++ b/src/initPlugin.ts
@@ -11,6 +11,9 @@ import chapter from './plugin/ui/chapter';
import fly from './plugin/ui/fly';
import chase from './plugin/chase/chase';
import fixed from './plugin/ui/fixed';
+import webglUtils from './plugin/webgl/utils';
+import shadow from './plugin/webgl/shadow';
+import gameShadow from './plugin/webgl/gameShadow';
function forward() {
// 每个引入的插件都要在这里执行,否则不会被转发
@@ -26,7 +29,10 @@ function forward() {
chapter(),
fly(),
chase(),
- fixed()
+ fixed(),
+ webglUtils(),
+ shadow(),
+ gameShadow()
];
// 初始化所有插件,并转发到core上
diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts
index 62d9e2f..5339489 100644
--- a/src/plugin/utils.ts
+++ b/src/plugin/utils.ts
@@ -1,3 +1,4 @@
+///
import { message } from 'ant-design-vue';
import { MessageApi } from 'ant-design-vue/lib/message';
import { isNil } from 'lodash';
@@ -69,7 +70,7 @@ export function keycode(key: number) {
* @param css 要解析的css字符串
*/
export function parseCss(css: string): Partial> {
- const str = css.replace(/[\n\s\t]*/g, '').replace(/[;,]*/g, ';');
+ const str = css.replace(/[\n\s\t]*/g, '').replace(/;*/g, ';');
const styles = str.split(';');
const res: Partial> = {};
diff --git a/src/plugin/webgl/canvas.ts b/src/plugin/webgl/canvas.ts
new file mode 100644
index 0000000..b4a70c2
--- /dev/null
+++ b/src/plugin/webgl/canvas.ts
@@ -0,0 +1,123 @@
+const glMap: Record = {};
+
+/**
+ * 创建一个以webgl为绘制上下文的画布
+ * @param id 画布id
+ * @param x 横坐标
+ * @param y 纵坐标
+ * @param w 宽度
+ * @param h 高度
+ * @param z 纵深
+ */
+export function createWebGLCanvas(
+ id: string,
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ z: number
+) {
+ if (id in glMap) {
+ deleteWebGLCanvas(id);
+ }
+ const canvas = document.createElement('canvas');
+ const gl = canvas.getContext('webgl')!;
+ const s = core.domStyle.scale;
+ canvas.style.left = `${x * s}px`;
+ canvas.style.top = `${y * s}px`;
+ canvas.style.width = `${w * s}px`;
+ canvas.style.height = `${h * s}px`;
+ canvas.style.zIndex = `${z}`;
+ canvas.width = w * s * devicePixelRatio;
+ canvas.height = h * s * devicePixelRatio;
+ core.dom.gameDraw.appendChild(canvas);
+ return gl;
+}
+
+/**
+ * 删除一个webgl画布
+ * @param id 画布id
+ */
+export function deleteWebGLCanvas(id: string) {
+ const gl = glMap[id];
+ if (!gl) return;
+ const canvas = gl.canvas as HTMLCanvasElement;
+ canvas.remove();
+ delete glMap[id];
+}
+
+/**
+ * 获取webgl画布上下文
+ * @param id 画布id
+ */
+export function getWebGLCanvas(id: string): WebGLRenderingContext | null {
+ return glMap[id];
+}
+
+/**
+ * 创建webgl程序对象
+ * @param gl 画布webgl上下文
+ * @param vshader 顶点着色器
+ * @param fshader 片元着色器
+ */
+export function createProgram(
+ gl: WebGLRenderingContext,
+ vshader: string,
+ fshader: string
+) {
+ // 创建着色器
+ const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
+ const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
+
+ // 创建program
+ const program = gl.createProgram();
+ if (!program) {
+ throw new Error(`Create webgl program fail!`);
+ }
+
+ // 分配和连接program
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+
+ // 检查连接是否成功
+ const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
+ if (!linked) {
+ const err = gl.getProgramInfoLog(program);
+ throw new Error(`Program link fail: ${err}`);
+ }
+
+ return program;
+}
+
+/**
+ * 加载着色器
+ * @param gl 画布的webgl上下文
+ * @param type 着色器类型,顶点着色器还是片元着色器
+ * @param source 着色器源码
+ */
+export function loadShader(
+ gl: WebGLRenderingContext,
+ type: number,
+ source: string
+) {
+ // 创建着色器
+ const shader = gl.createShader(type);
+ if (!shader) {
+ throw new ReferenceError(
+ `Your device or browser does not support webgl!`
+ );
+ }
+ // 引入并编译着色器
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ // 检查是否编译成功
+ const compiled = gl.getShaderParameter(gl, gl.COMPILE_STATUS);
+ if (!compiled) {
+ const err = gl.getShaderInfoLog(shader);
+ throw new Error(`Shader compile fail: ${err}`);
+ }
+
+ return shader;
+}
diff --git a/src/plugin/webgl/gameShadow.ts b/src/plugin/webgl/gameShadow.ts
new file mode 100644
index 0000000..e5f2e21
--- /dev/null
+++ b/src/plugin/webgl/gameShadow.ts
@@ -0,0 +1,131 @@
+import { Polygon } from './polygon';
+import {
+ Light,
+ removeAllLights,
+ setBackground,
+ setBlur,
+ setLightList,
+ setShadowNodes
+} from './shadow';
+
+export default function init() {
+ return { updateShadow, clearShadowCache, setCalShadow };
+}
+
+const shadowInfo: Partial> = {
+ MT46: [
+ {
+ id: 'mt42_1',
+ x: 85,
+ y: 85,
+ decay: 100,
+ r: 300,
+ color: '#0000'
+ }
+ ]
+};
+const backgroundInfo: Partial> = {
+ MT46: '#0008'
+};
+const blurInfo: Partial> = {
+ MT46: 4
+};
+const immersionInfo: Partial> = {
+ MT46: 8
+};
+const shadowCache: Partial> = {};
+
+let calMapShadow = true;
+
+export function updateShadow(nocache: boolean = false) {
+ // 需要优化,优化成bfs
+ const floor = core.status.floorId;
+ if (!shadowInfo[floor] || !backgroundInfo[floor]) {
+ removeAllLights();
+ setShadowNodes([]);
+ setBackground('#0000');
+ return;
+ }
+ const f = core.status.thisMap;
+ const w = f.width;
+ const h = f.height;
+ const nodes: Polygon[] = [];
+ if (calMapShadow) {
+ if (shadowCache[floor] && !nocache) {
+ setShadowNodes(shadowCache[floor]!);
+ } else {
+ core.extractBlocks();
+ const blocks = core.getMapBlocksObj();
+ core.status.maps[floor].blocks.forEach(v => {
+ if (
+ !['terrains', 'autotile', 'tileset', 'animates'].includes(
+ v.event.cls
+ )
+ ) {
+ return;
+ }
+
+ if (v.event.noPass) {
+ const immerse = immersionInfo[floor] ?? 4;
+ const x = v.x;
+ const y = v.y;
+ let left = x * 32 + immerse;
+ let top = y * 32 + immerse;
+ let right = left + 32 - immerse * 2;
+ let bottom = top + 32 - immerse * 2;
+ const l: LocString = `${x - 1},${y}`;
+ const r: LocString = `${x + 1},${y}`;
+ const t: LocString = `${x},${y - 1}`;
+ const b: LocString = `${x},${y + 1}`;
+
+ if (x === 0 || (blocks[l] && blocks[l].event.noPass)) {
+ left -= immerse;
+ }
+ if (x + 1 === w || (blocks[r] && blocks[r].event.noPass)) {
+ right += immerse;
+ }
+ if (y === 0 || (blocks[t] && blocks[t].event.noPass)) {
+ top -= immerse;
+ }
+ if (y + 1 === h || (blocks[b] && blocks[b].event.noPass)) {
+ bottom += immerse;
+ }
+ nodes.push(
+ new Polygon([
+ [left, top],
+ [right, top],
+ [right, bottom],
+ [left, bottom]
+ ])
+ );
+ return;
+ }
+ });
+ shadowCache[floor] = nodes;
+ setShadowNodes(nodes);
+ }
+ } else {
+ setShadowNodes([]);
+ setBlur(0);
+ }
+ setLightList(shadowInfo[floor]!);
+ setBackground(backgroundInfo[floor]!);
+ setBlur(blurInfo[floor] ?? 3);
+}
+
+/**
+ * 清除某一层的墙壁缓存
+ * @param floorId 楼层id
+ */
+export function clearShadowCache(floorId: FloorIds) {
+ delete shadowCache[floorId];
+}
+
+/**
+ * 设置是否不计算墙壁遮挡,对所有灯光有效
+ * @param n 目标值
+ */
+export function setCalShadow(n: boolean) {
+ calMapShadow = n;
+ updateShadow();
+}
diff --git a/src/plugin/webgl/martrix.ts b/src/plugin/webgl/martrix.ts
new file mode 100644
index 0000000..d7d6b41
--- /dev/null
+++ b/src/plugin/webgl/martrix.ts
@@ -0,0 +1,160 @@
+import { has } from '../utils';
+
+export class Matrix extends Array {
+ constructor(...n: number[][]) {
+ if (n.length !== n[0]?.length) {
+ throw new TypeError(
+ `The array delivered to Matrix must has the same length of its item and itself.`
+ );
+ }
+ super(...n);
+ }
+
+ /**
+ * 加上某个方阵
+ * @param matrix 要加上的方阵
+ */
+ add(matrix: number[][]): Matrix {
+ if (matrix.length !== this.length) {
+ throw new TypeError(
+ `To add a martrix, the be-added-matrix's size must equal to the to-add-matrix's.`
+ );
+ }
+ const length = matrix.length;
+ for (let i = 0; i < length; i++) {
+ for (let j = 0; j < length; j++) {
+ this[i][j] += matrix[i][j];
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 让该方阵与另一个方阵相乘
+ * @param matrix 要相乘的方阵
+ */
+ multipy(matrix: number[][]): Matrix {
+ if (matrix.length !== this.length) {
+ throw new TypeError(
+ `To multipy a martrix, the be-multipied-matrix's size must equal to the to-multipy-matrix's.`
+ );
+ }
+ const n = this.length;
+ const arr = this.map(v => v.slice());
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ for (let k = 0; k < n; k++) {
+ this[i][j] = arr[i][k] * matrix[k][j];
+ }
+ }
+ }
+ return this;
+ }
+}
+
+export class Matrix4 extends Matrix {
+ constructor(...n: number[][]) {
+ n ??= [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1]
+ ];
+ if (n.length !== 4) {
+ throw new TypeError(`The length of delivered array must be 4.`);
+ }
+ super(...n);
+ }
+
+ /**
+ * 平移变换
+ * @param x 平移横坐标
+ * @param y 平移纵坐标
+ * @param z 平移竖坐标
+ */
+ translation(x: number, y: number, z: number) {
+ this.multipy([
+ [1, 0, 0, x],
+ [0, 1, 0, y],
+ [0, 0, 1, z],
+ [0, 0, 0, 1]
+ ]);
+ }
+
+ /**
+ * 缩放变换
+ * @param x 沿x轴的缩放比例
+ * @param y 沿y轴的缩放比例
+ * @param z 沿z轴的缩放比例
+ */
+ scale(x: number, y: number, z: number) {
+ this.multipy([
+ [x, 0, 0, 0],
+ [0, y, 0, 0],
+ [0, 0, z, 0],
+ [0, 0, 0, 1]
+ ]);
+ }
+
+ /**
+ * 旋转变换
+ * @param x 绕x轴的旋转角度
+ * @param y 绕y轴的旋转角度
+ * @param z 绕z轴的旋转角度
+ */
+ rotate(x?: number, y?: number, z?: number): Matrix4 {
+ if (has(x) && x !== 0) {
+ const sin = Math.sin(x);
+ const cos = Math.cos(x);
+ this.multipy([
+ [1, 0, 0, 0],
+ [0, cos, sin, 0],
+ [0, -sin, cos, 0],
+ [0, 0, 0, 1]
+ ]);
+ }
+ if (has(y) && y !== 0) {
+ const sin = Math.sin(y);
+ const cos = Math.cos(y);
+ this.multipy([
+ [cos, 0, -sin, 0],
+ [0, 1, 0, 0],
+ [sin, 0, cos, 0],
+ [0, 0, 0, 1]
+ ]);
+ }
+ if (has(z) && z !== 0) {
+ const sin = Math.sin(z);
+ const cos = Math.cos(z);
+ this.multipy([
+ [cos, sin, 0, 0],
+ [-sin, cos, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1]
+ ]);
+ }
+ return this;
+ }
+
+ /**
+ * 转置矩阵
+ * @param target 转置目标,是赋给原矩阵还是新建一个矩阵
+ */
+ transpose(target: 'this' | 'new' = 'new'): Matrix4 {
+ const t = target === 'this' ? this : new Matrix4();
+ const arr = this.map(v => v.slice());
+ for (let i = 0; i < 4; i++) {
+ for (let j = 0; j < 4; j++) {
+ t[i][j] = arr[j][i];
+ }
+ }
+ return t;
+ }
+
+ /**
+ * 转换成列主序的Float32Array,用于webgl
+ */
+ toWebGLFloat32Array(): Float32Array {
+ return new Float32Array(this.transpose().flat());
+ }
+}
diff --git a/src/plugin/webgl/polygon.ts b/src/plugin/webgl/polygon.ts
new file mode 100644
index 0000000..4248458
--- /dev/null
+++ b/src/plugin/webgl/polygon.ts
@@ -0,0 +1,89 @@
+export class Polygon {
+ /**
+ * 多边形的节点
+ */
+ nodes: LocArr[];
+
+ private cache: Record = {};
+
+ static from(...polygons: LocArr[][]) {
+ return polygons.map(v => new Polygon(v));
+ }
+
+ constructor(nodes: LocArr[]) {
+ if (nodes.length < 3) {
+ throw new Error(`Nodes number delivered is less than 3!`);
+ }
+ this.nodes = nodes;
+ }
+
+ /**
+ * 获取一个点光源下的阴影
+ */
+ shadowArea(x: number, y: number, r: number): LocArr[][] {
+ const id = `${x},${y}`;
+ if (this.cache[id]) return this.cache[id];
+ const res: LocArr[][] = [];
+ const w = core._PX_ ?? core.__PIXELS__;
+ const h = core._PY_ ?? core.__PIXELS__;
+
+ const intersect = (nx: number, ny: number): LocArr => {
+ const k = (ny - y) / (nx - x);
+ if (k > 1 || k < -1) {
+ if (ny < y) {
+ const ix = x + y / k;
+ return [2 * x - ix, 0];
+ } else {
+ const ix = x + (h - y) / k;
+ return [ix, h];
+ }
+ } else {
+ if (nx < x) {
+ const iy = y + k * x;
+ return [0, 2 * y - iy];
+ } else {
+ const iy = y + k * (w - x);
+ return [w, iy];
+ }
+ }
+ };
+ const l = this.nodes.length;
+ let now = intersect(...this.nodes[0]);
+ for (let i = 0; i < l; i++) {
+ const next = (i + 1) % l;
+ const nextInter = intersect(...this.nodes[next]);
+ const start = [this.nodes[i], now];
+ const end = [nextInter, this.nodes[next]];
+ let path: LocArr[];
+ if (
+ (now[0] === 0 && nextInter[1] === 0) ||
+ (now[1] === 0 && nextInter[0] === 0)
+ ) {
+ path = [...start, [0, 0], ...end];
+ } else if (
+ (now[0] === 0 && nextInter[1] === h) ||
+ (now[1] === h && nextInter[0] === 0)
+ ) {
+ path = [...start, [0, h], ...end];
+ } else if (
+ (now[0] === w && nextInter[1] === 0) ||
+ (now[1] === 0 && nextInter[0] === w)
+ ) {
+ path = [...start, [w, 0], ...end];
+ } else if (
+ (now[0] === w && nextInter[1] === h) ||
+ (now[1] === h && nextInter[0] === w)
+ ) {
+ path = [...start, [w, h], ...end];
+ } else {
+ path = [...start, ...end];
+ }
+ res.push(path);
+ now = nextInter;
+ }
+
+ this.cache[id] = res;
+
+ return res;
+ }
+}
diff --git a/src/plugin/webgl/shadow.ts b/src/plugin/webgl/shadow.ts
new file mode 100644
index 0000000..e30242e
--- /dev/null
+++ b/src/plugin/webgl/shadow.ts
@@ -0,0 +1,468 @@
+import {
+ Animation,
+ linear,
+ PathFn,
+ TimingFn,
+ Transition
+} from 'mutate-animate';
+import { has } from '../utils';
+import { Polygon } from './polygon';
+
+interface TransitionInfo {
+ time: number;
+ mode: TimingFn;
+}
+
+export interface Light {
+ id: string;
+ x: number;
+ y: number;
+ r: number;
+ /** 衰减开始半径 */
+ decay: number;
+ /** 颜色,每个值的范围0.0~1.0 */
+ color: Color;
+ /** 是否可以被物体遮挡 */
+ noShelter?: boolean;
+ /** 正在动画的属性 */
+ _animating?: Record;
+ /** 执行渐变的属性 */
+ _transition?: Record;
+ /** 表示是否是代理,只有设置渐变后才会变为true */
+ _isProxy?: boolean;
+}
+
+export default function init() {
+ core.registerAnimationFrame('shadow', true, () => {
+ if (!needRefresh) return;
+ drawShadow();
+ });
+
+ return {
+ initShadowCanvas,
+ drawShadow,
+ addLight,
+ removeLight,
+ setLight,
+ setShadowNodes,
+ setBackground,
+ animateLight,
+ transitionLight,
+ moveLightAs,
+ getAllLights
+ };
+}
+
+let canvas: HTMLCanvasElement;
+let ctx: CanvasRenderingContext2D;
+let lights: Light[] = [];
+let needRefresh = false;
+let shadowNodes: Polygon[] = [];
+let background: Color;
+let blur = 3;
+const temp1 = document.createElement('canvas');
+const temp2 = document.createElement('canvas');
+const temp3 = document.createElement('canvas');
+const ct1 = temp1.getContext('2d')!;
+const ct2 = temp2.getContext('2d')!;
+const ct3 = temp3.getContext('2d')!;
+
+const animationList: Record = {};
+const transitionList: Record = {};
+
+/**
+ * 初始化阴影画布
+ */
+export function initShadowCanvas() {
+ const w = core._PX_ ?? core.__PIXELS__;
+ const h = core._PY_ ?? core.__PIXELS__;
+ ctx = core.createCanvas('shadow', 0, 0, w, h, 55);
+ canvas = ctx.canvas;
+ const s = core.domStyle.scale * devicePixelRatio;
+ temp1.width = w * s;
+ temp1.height = h * s;
+ temp2.width = w * s;
+ temp2.height = h * s;
+ temp3.width = w * s;
+ temp3.height = h * s;
+ ct1.scale(s, s);
+ ct2.scale(s, s);
+ ct3.scale(s, s);
+ canvas.style.filter = `blur(${blur}px)`;
+}
+
+/**
+ * 添加一个光源
+ * @param info 光源信息
+ */
+export function addLight(info: Light) {
+ lights.push(info);
+ needRefresh = true;
+}
+
+/**
+ * 移除一个光源
+ * @param id 光源id
+ */
+export function removeLight(id: string) {
+ const index = lights.findIndex(v => v.id === id);
+ if (index === -1) {
+ throw new ReferenceError(`You are going to remove nonexistent light!`);
+ }
+ lights.splice(index, 1);
+ needRefresh = true;
+}
+
+/**
+ * 设置一个光源的信息
+ * @param id 光源id
+ * @param info 光源信息
+ */
+export function setLight(id: string, info: Partial) {
+ if (has(info.id)) delete info.id;
+ const light = lights.find(v => v.id === id);
+ if (!light) {
+ throw new ReferenceError(`You are going to set nonexistent light!`);
+ }
+ for (const [p, v] of Object.entries(info)) {
+ light[p as SelectKey] = v as number;
+ }
+ needRefresh = true;
+}
+
+/**
+ * 设置当前的光源列表
+ * @param list 光源列表
+ */
+export function setLightList(list: Light[]) {
+ lights = list;
+ needRefresh = true;
+}
+
+/**
+ * 去除所有的光源
+ */
+export function removeAllLights() {
+ lights = [];
+ needRefresh = true;
+}
+
+/**
+ * 获取一个灯光
+ * @param id 灯光id
+ */
+export function getLight(id: string) {
+ return lights.find(v => v.id === id);
+}
+
+/**
+ * 获取所有灯光
+ */
+export function getAllLights() {
+ return lights;
+}
+
+/**
+ * 设置背景色
+ * @param color 背景色
+ */
+export function setBackground(color: Color) {
+ background = color;
+ needRefresh = true;
+}
+
+/**
+ * 动画改变一个属性的值
+ * @param id 灯光id
+ * @param key 动画属性,x,y,r,decay,颜色请使用animateLightColor(下个版本会加)
+ * @param n 目标值
+ * @param time 动画时间
+ * @param mode 动画方式,渐变函数,高级动画提供了大量内置的渐变函数
+ * @param relative 相对方式,是绝对还是相对
+ */
+export function animateLight>(
+ id: string,
+ key: K,
+ n: Light[K],
+ time: number = 1000,
+ mode: TimingFn = linear(),
+ relative: boolean = false
+) {
+ const light = getLight(id);
+ if (!has(light)) {
+ throw new ReferenceError(`You are going to animate nonexistent light`);
+ }
+ if (typeof n !== 'number') {
+ light[key] = n;
+ }
+ const ani = animationList[id] ?? (animationList[id] = new Animation());
+ if (typeof ani.value[key] !== 'number') {
+ ani.register(key, light[key] as number);
+ } else {
+ ani.time(0)
+ .mode(linear())
+ .absolute()
+ .apply(key, light[key] as number);
+ }
+ ani.time(time)
+ .mode(mode)
+ [relative ? 'relative' : 'absolute']()
+ .apply(key, n as number);
+ const start = Date.now();
+ const fn = () => {
+ if (Date.now() - start > time + 50) {
+ ani.ticker.remove(fn);
+ light._animating![key] = false;
+ }
+ needRefresh = true;
+ light[key as SelectKey] = ani.value[key];
+ };
+ ani.ticker.add(fn);
+ light._animating ??= {};
+ light._animating[key] = true;
+}
+
+/**
+ * 把一个属性设置为渐变模式
+ * @param id 灯光id
+ * @param key 渐变的属性
+ * @param time 渐变时长
+ * @param mode 渐变方式,渐变函数,高级动画提供了大量内置的渐变函数
+ */
+export function transitionLight>(
+ id: string,
+ key: K,
+ time: number = 1000,
+ mode: TimingFn = linear()
+) {
+ const index = lights.findIndex(v => v.id === id);
+ if (index === -1) {
+ throw new ReferenceError(`You are going to transite nonexistent light`);
+ }
+ const light = lights[index];
+ if (typeof light[key] !== 'number') return;
+ light._transition ??= {};
+ light._transition[key] = { time, mode };
+ const tran = transitionList[id] ?? (transitionList[id] = new Transition());
+ tran.value[key] = light[key] as number;
+ if (!light._isProxy) {
+ const handler: ProxyHandler = {
+ set(t, p, v) {
+ if (typeof p === 'symbol') return false;
+ const start = Date.now();
+ if (
+ !light._transition![p] ||
+ light._animating?.[key] ||
+ typeof v !== 'number'
+ ) {
+ t[p as SelectKey] = v;
+ return true;
+ }
+ // @ts-ignore
+ t[p] = light[p];
+ const info = light._transition![p];
+ tran.mode(info.mode).time(info.time);
+ const fn = () => {
+ if (Date.now() - start > info.time + 50) {
+ tran.ticker.remove(fn);
+ }
+ needRefresh = true;
+ t[p as SelectKey] = tran.value[key];
+ };
+ tran.ticker.add(fn);
+ tran.transition(p, v);
+ return true;
+ }
+ };
+ lights[index] = new Proxy(light, handler);
+ }
+}
+
+/**
+ * 移动一个灯光
+ * @param id 灯光id
+ * @param x 目标横坐标
+ * @param y 目标纵坐标
+ * @param time 移动时间
+ * @param mode 移动方式,渐变函数
+ * @param relative 相对模式,相对还是绝对
+ */
+export function moveLight(
+ id: string,
+ x: number,
+ y: number,
+ time: number = 1000,
+ mode: TimingFn = linear(),
+ relative: boolean = false
+) {
+ animateLight(id, 'x', x, time, mode, relative);
+ animateLight(id, 'y', y, time, mode, relative);
+}
+
+/**
+ * 以一个路径移动光源
+ * @param id 灯光id
+ * @param time 移动时长
+ * @param path 移动路径
+ * @param mode 移动方式,渐变函数,表示移动的完成度
+ * @param relative 相对模式,相对还是绝对
+ */
+export function moveLightAs(
+ id: string,
+ time: number,
+ path: PathFn,
+ mode: TimingFn = linear(),
+ relative: boolean = true
+) {
+ const light = getLight(id);
+ if (!has(light)) {
+ throw new ReferenceError(`You are going to animate nonexistent light`);
+ }
+ const ani = animationList[id] ?? (animationList[id] = new Animation());
+ ani.mode(linear()).time(0).move(light.x, light.y);
+ ani.time(time)
+ .mode(mode)
+ [relative ? 'relative' : 'absolute']()
+ .moveAs(path);
+ const start = Date.now();
+ const fn = () => {
+ if (Date.now() - start > time + 50) {
+ ani.ticker.remove(fn);
+ light._animating!.x = false;
+ light._animating!.y = false;
+ }
+ needRefresh = true;
+ light.x = ani.x;
+ light.y = ani.y;
+ };
+ ani.ticker.add(fn);
+ light._animating ??= {};
+ light._animating.x = true;
+ light._animating.y = true;
+}
+
+export function animateLightColor(
+ id: string,
+ target: Color,
+ time: number = 1000,
+ mode: TimingFn = linear()
+) {
+ // todo
+}
+
+/**
+ * 根据坐标数组设置物体节点
+ * @param nodes 坐标数组
+ */
+export function setShadowNodes(nodes: LocArr[][]): void;
+/**
+ * 根据多边形数组设置物体节点
+ * @param nodes 多边形数组
+ */
+export function setShadowNodes(nodes: Polygon[]): void;
+export function setShadowNodes(nodes: LocArr[][] | Polygon[]) {
+ if (nodes.length === 0) {
+ shadowNodes = [];
+ needRefresh = true;
+ }
+ if (nodes[0] instanceof Polygon) shadowNodes = nodes as Polygon[];
+ else shadowNodes = Polygon.from(...(nodes as LocArr[][]));
+ needRefresh = true;
+}
+
+/**
+ * 根据坐标数组添加物体节点
+ * @param polygons 坐标数组
+ */
+export function addPolygon(...polygons: LocArr[][]): void;
+/**
+ * 根据多边形数组添加物体节点
+ * @param polygons 多边形数组
+ */
+export function addPolygon(...polygons: Polygon[]): void;
+export function addPolygon(...polygons: Polygon[] | LocArr[][]) {
+ if (polygons.length === 0) return;
+ if (polygons[0] instanceof Polygon)
+ shadowNodes.push(...(polygons as Polygon[]));
+ else shadowNodes.push(...Polygon.from(...(polygons as LocArr[][])));
+ needRefresh = true;
+}
+
+/**
+ * 设置光源的虚化程度
+ * @param n 虚化程度
+ */
+export function setBlur(n: number) {
+ blur = n;
+ canvas.style.filter = `blur(${n}px)`;
+}
+
+/**
+ * 绘制阴影
+ */
+export function drawShadow() {
+ const w = core._PX_ ?? core.__PIXELS__;
+ const h = core._PY_ ?? core.__PIXELS__;
+ needRefresh = false;
+ ctx.clearRect(0, 0, w, h);
+ ct1.clearRect(0, 0, w, h);
+ ct2.clearRect(0, 0, w, h);
+ ct3.clearRect(0, 0, w, h);
+
+ const b = core.arrayToRGBA(background);
+ ctx.globalCompositeOperation = 'source-over';
+ ct3.globalCompositeOperation = 'source-over';
+
+ // 绘制阴影,一个光源一个光源地绘制,然后source-out获得光,然后把光叠加,再source-out获得最终阴影
+ for (let i = 0; i < lights.length; i++) {
+ const { x, y, r, decay, color, noShelter } = lights[i];
+ // 绘制阴影
+ ct1.clearRect(0, 0, w, h);
+ ct2.clearRect(0, 0, w, h);
+ if (!noShelter) {
+ for (const polygon of shadowNodes) {
+ const area = polygon.shadowArea(x, y, r);
+ area.forEach(v => {
+ ct1.beginPath();
+ ct1.moveTo(v[0][0], v[0][1]);
+ for (let i = 1; i < v.length; i++) {
+ ct1.lineTo(v[i][0], v[i][1]);
+ }
+ ct1.closePath();
+ ct1.fillStyle = '#000';
+ ct1.globalCompositeOperation = 'source-over';
+ ct1.fill();
+ });
+ }
+ }
+ // 存入ct2,用于绘制真实阴影
+ ct2.globalCompositeOperation = 'source-over';
+ ct2.drawImage(temp1, 0, 0, w, h);
+ ct2.globalCompositeOperation = 'source-out';
+ const gra = ct2.createRadialGradient(x, y, decay, x, y, r);
+ gra.addColorStop(0, core.arrayToRGBA(color));
+ gra.addColorStop(1, 'transparent');
+ ct2.fillStyle = gra;
+ ct2.beginPath();
+ ct2.arc(x, y, r, 0, Math.PI * 2);
+ ct2.fill();
+ ctx.drawImage(temp2, 0, 0, w, h);
+ // 再绘制ct1的阴影,然后绘制到ct3叠加
+ ct1.globalCompositeOperation = 'source-out';
+ const gra2 = ct1.createRadialGradient(x, y, decay, x, y, r);
+ gra2.addColorStop(0, '#fff');
+ gra2.addColorStop(1, '#fff0');
+ ct1.beginPath();
+ ct1.arc(x, y, r, 0, Math.PI * 2);
+ ct1.fillStyle = gra2;
+ ct1.fill();
+ // 绘制到ct3上
+ ct3.drawImage(temp1, 0, 0, w, h);
+ }
+ // 绘制真实阴影
+ ct3.globalCompositeOperation = 'source-out';
+ ct3.fillStyle = b;
+ ct3.fillRect(0, 0, w, h);
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.drawImage(temp3, 0, 0, w, h);
+}
diff --git a/src/plugin/webgl/utils.ts b/src/plugin/webgl/utils.ts
new file mode 100644
index 0000000..999219c
--- /dev/null
+++ b/src/plugin/webgl/utils.ts
@@ -0,0 +1,8 @@
+export default function init() {
+ return { isWebGLSupported };
+}
+
+export const isWebGLSupported = (function () {
+ const canvas = document.createElement('canvas');
+ return !!canvas.getContext('webgl');
+})();
diff --git a/src/types/map.d.ts b/src/types/map.d.ts
index beefa76..43c4fee 100644
--- a/src/types/map.d.ts
+++ b/src/types/map.d.ts
@@ -61,7 +61,7 @@ interface Block = Exclude> {
/**
* 图块是否不可通行
*/
- nopass: boolean;
+ noPass: boolean;
/**
* 图块高度
diff --git a/src/types/util.d.ts b/src/types/util.d.ts
index 35fbc14..f3a7697 100644
--- a/src/types/util.d.ts
+++ b/src/types/util.d.ts
@@ -862,6 +862,11 @@ type SelectType = {
[P in keyof R as R[P] extends T ? P : never]: R[P];
};
+/**
+ * 从一个对象中选择类型是目标属性的键名
+ */
+type SelectKey = keyof SelectType;
+
/**
* 获取一段字符串的第一个字符
*/
@@ -883,3 +888,5 @@ type NonObjectOf = SelectType;
* 以一个字符串结尾
*/
type EndsWith = `${string}${T}`;
+
+type KeyExcludesUnderline = Excluede;