点光源跟随勇士,动画路径

This commit is contained in:
unanmed 2023-04-15 20:21:29 +08:00
parent b0bfbfa0a4
commit c39dd87556
16 changed files with 1436 additions and 845 deletions

View File

@ -16,37 +16,37 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"ant-design-vue": "^3.2.15", "ant-design-vue": "^3.2.17",
"axios": "^1.3.4", "axios": "^1.3.5",
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"mutate-animate": "^1.0.2", "mutate-animate": "^1.1.1",
"three": "^0.149.0", "three": "^0.149.0",
"vue": "^3.2.47" "vue": "^3.2.47"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.21.0", "@babel/cli": "^7.21.0",
"@babel/core": "^7.21.0", "@babel/core": "^7.21.4",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.21.4",
"@types/babel__core": "^7.20.0",
"@types/fontmin": "^0.9.0", "@types/fontmin": "^0.9.0",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.194",
"@types/lz-string": "^1.3.34", "@types/node": "^18.15.11",
"@types/node": "^18.14.6", "@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-legacy": "^2.3.1", "@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue-jsx": "^3.0.1",
"@vitejs/plugin-vue-jsx": "^2.1.1", "compressing": "^1.9.0",
"compressing": "^1.8.0",
"fontmin": "^0.9.9", "fontmin": "^0.9.9",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"less": "^4.1.3", "less": "^4.1.3",
"terser": "^5.16.5", "terser": "^5.16.9",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"unplugin-vue-components": "^0.22.12", "unplugin-vue-components": "^0.22.12",
"vite": "^3.2.5", "vite": "^4.2.1",
"vue-tsc": "^1.2.0" "vue-tsc": "^1.2.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -320,7 +320,7 @@ core.prototype._loadPlugin = async function () {
core.plugin.showMarkedEnemy.value = true; core.plugin.showMarkedEnemy.value = true;
} }
if (main.pluginUseCompress) { if (main.pluginUseCompress) {
await main.loadScript(`project/plugin.min.js?v=${main.version}`); await main.loadScript(`project/plugin.m.js?v=${main.version}`);
} else { } else {
for await (const plugin of mainData.plugin) { for await (const plugin of mainData.plugin) {
await main.loadScript( await main.loadScript(

View File

@ -41,7 +41,7 @@ var enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80 =
"elemental": {"name":"元素生物","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "elemental": {"name":"元素生物","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"steelGuard": {"name":"铁守卫","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[18],"value":20}, "steelGuard": {"name":"铁守卫","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[18],"value":20},
"evilBat": {"name":"邪恶蝙蝠","hp":1000,"atk":800,"def":350,"money":1,"exp":40,"point":0,"special":[2]}, "evilBat": {"name":"邪恶蝙蝠","hp":1000,"atk":800,"def":350,"money":1,"exp":40,"point":0,"special":[2]},
"frozenSkeleton": {"name":"冻死骨","hp":7500,"atk":2500,"def":1000,"money":2,"exp":90,"point":0,"special":[1,20],"crit":500,"ice":10}, "frozenSkeleton": {"name":"冻死骨","hp":7500,"atk":2500,"def":1000,"money":2,"exp":90,"point":0,"special":[1,20],"crit":500,"ice":10,"description":"弱小,无助,哀嚎,这就是残酷的现实。哪怕你身处极寒之中,也难有人对你伸出援手。人类总会帮助他人,但在这绝望的环境下,人类的本性便暴露无遗。这一个个精致却又无情的骷髅,便是那些在极寒之中死去的冤魂。"},
"silverSlimelord": {"name":"银怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "silverSlimelord": {"name":"银怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"goldSlimelord": {"name":"金怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "goldSlimelord": {"name":"金怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"skeletonWarrior": {"name":"骷髅士兵","hp":500,"atk":100,"def":20,"money":0,"exp":12,"point":0,"special":[1],"crit":500,"description":"看来未来的机器人并不满足与赤手空拳,他们也拿上了武器。"}, "skeletonWarrior": {"name":"骷髅士兵","hp":500,"atk":100,"def":20,"money":0,"exp":12,"point":0,"special":[1],"crit":500,"description":"看来未来的机器人并不满足与赤手空拳,他们也拿上了武器。"},
@ -68,13 +68,13 @@ var enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80 =
"bowman": {"name":"猎人","hp":500,"atk":100,"def":50,"money":0,"exp":16,"point":0,"special":[24],"value":75,"description":"没人知道这些人怎么做出的弓,也没人知道他们怎么收集的这么多剑。而其他人唯一能做的事,便是远离他们。"}, "bowman": {"name":"猎人","hp":500,"atk":100,"def":50,"money":0,"exp":16,"point":0,"special":[24],"value":75,"description":"没人知道这些人怎么做出的弓,也没人知道他们怎么收集的这么多剑。而其他人唯一能做的事,便是远离他们。"},
"liteBowman": {"name":"山间猎手","hp":1200,"atk":200,"def":60,"money":1,"exp":27,"point":0,"special":[24],"description":"这箭,或许就是那些败于他弓下的那些不知好歹的莽夫的骨头吧。或许,绕开他的视野才是躲避他的攻击的最好办法。"}, "liteBowman": {"name":"山间猎手","hp":1200,"atk":200,"def":60,"money":1,"exp":27,"point":0,"special":[24],"description":"这箭,或许就是那些败于他弓下的那些不知好歹的莽夫的骨头吧。或许,绕开他的视野才是躲避他的攻击的最好办法。"},
"crimsonZombie": {"name":"勇气之兽","hp":1800,"atk":2000,"def":-100,"money":1,"exp":35,"point":0,"special":[],"description":"没人会愿意跟这些野兽一起吧?至少我是不想。上天赋予的勇气,却让他们更加渴望鲜血,这不是很可悲吗?"}, "crimsonZombie": {"name":"勇气之兽","hp":1800,"atk":2000,"def":-100,"money":1,"exp":35,"point":0,"special":[],"description":"没人会愿意跟这些野兽一起吧?至少我是不想。上天赋予的勇气,却让他们更加渴望鲜血,这不是很可悲吗?"},
"watcherSlime": {"name":"邪眼史莱姆","hp":5000,"atk":1200,"def":600,"money":1,"exp":50,"point":0,"special":[17]}, "watcherSlime": {"name":"邪眼史莱姆","hp":5000,"atk":1200,"def":600,"money":1,"exp":50,"point":0,"special":[17],"description":"成群结队地出现在森林中,看遍百花齐放,经历万物凋零。他们守在这森林中,将那些企图突破这里的人置于死地。"},
"mutantSlimeman": {"name":"变异史莱姆人","hp":350,"atk":70,"def":27,"money":0,"exp":13,"point":0,"special":[]}, "mutantSlimeman": {"name":"变异史莱姆人","hp":350,"atk":70,"def":27,"money":0,"exp":13,"point":0,"special":[],"description":"据说,史莱姆人也会基因突变,这样就产生了这种变异史莱姆人。"},
"devilKnight": {"name":"恶灵骑士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "devilKnight": {"name":"恶灵骑士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"grayPriest": {"name":"智慧法王","hp":3000,"atk":600,"def":250,"money":1,"exp":40,"point":0,"special":[13]}, "grayPriest": {"name":"智慧法王","hp":3000,"atk":600,"def":250,"money":1,"exp":40,"point":0,"special":[13],"description":"法杖?他已经不需要了。没有人知道他长什么样,只知道他的智慧已非常人能及,只知道他的法术能让一个人在瞬间化为灰烬。"},
"greenGateKeeper": {"name":"睿智雕像","hp":5000,"atk":1250,"def":900,"money":1,"exp":65,"point":0,"special":[1],"crit":1000}, "greenGateKeeper": {"name":"睿智雕像","hp":5000,"atk":1250,"def":900,"money":1,"exp":65,"point":0,"special":[1],"crit":1000},
"ghostSoldier": {"name":"山间骷髅","hp":750,"atk":180,"def":40,"money":0,"exp":18,"point":0,"special":[],"description":"这次,他们穿上了盔甲。"}, "ghostSoldier": {"name":"山间骷髅","hp":750,"atk":180,"def":40,"money":0,"exp":18,"point":0,"special":[],"description":"这次,他们穿上了盔甲。"},
"frostBat": {"name":"寒霜蝙蝠","hp":20000,"atk":3200,"def":2000,"money":2,"exp":2000,"point":0,"special":[4,20],"ice":90}, "frostBat": {"name":"寒霜蝙蝠","hp":20000,"atk":3200,"def":2000,"money":2,"exp":2000,"point":0,"special":[4,20],"ice":90,"description":"“穿梭于寒风里,行走在锋芒中”,寒霜蝙蝠将这句话运用到了极致。别看那小小的身体,它足以将你拆的七零八落。在它面前,任何小把戏都会被它看得一清二楚。它那凶恶的眼神,是否在哪里见过呢?"},
"blackKing": {"name":"黑衣魔王","hp":1000,"atk":500,"def":0,"money":1000,"exp":1000,"point":0,"special":[],"notBomb":true}, "blackKing": {"name":"黑衣魔王","hp":1000,"atk":500,"def":0,"money":1000,"exp":1000,"point":0,"special":[],"notBomb":true},
"yellowKing": {"name":"黄衣魔王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "yellowKing": {"name":"黄衣魔王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"greenKing": {"name":"青衣武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "greenKing": {"name":"青衣武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},

View File

@ -59,6 +59,13 @@ main.floors.MT42=
0, 0,
13 13
] ]
},
"8,12": {
"floorId": "MT48",
"loc": [
3,
7
]
} }
}, },
"beforeBattle": {}, "beforeBattle": {},

View File

@ -49,18 +49,18 @@ main.floors.MT44=
"cannotMoveIn": {}, "cannotMoveIn": {},
"map": [ "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,617, 0, 0, 0,600, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0,617, 0,403, 0,600, 0, 0],
[ 92, 0,584,584,584, 0,543, 0, 0,604,604,604, 0, 0, 0], [ 92, 0,584,584,584, 0,543, 0, 0,604,604,604, 0, 0, 0],
[584, 0,584, 0, 0, 0, 0, 0, 0, 0,492, 0, 0, 0, 0], [584, 0,584, 0, 0, 0, 0, 0,378, 0,492, 0,378, 0, 0],
[584, 0,643, 0, 0, 0, 0,595, 0,602,604,610,70176,70177,70177], [584, 0,643, 0, 0, 0, 0,595, 0,602,604,610,70176,70177,70177],
[584,584,584,584,584, 0,584,584,494, 0, 0, 0,608, 0, 94], [584,584,584,584,584, 0,584,584,494, 0, 0, 0,608, 0, 94],
[ 0, 0, 0, 0, 0,613, 0, 0,584,614,584,584,70208,70209,70209], [ 0, 0, 0, 0, 0,274, 0, 0,584,614,584,584,70208,70209,70209],
[586, 0, 0, 0, 0,588, 0, 0,584, 0, 0,584,584, 0, 0], [586, 0, 0, 0, 0,588, 0, 0,584, 0, 0,584,584, 0, 0],
[586, 0,588,588,588,588,602,584,584,584, 0,584,584, 0, 0], [586,617,588,588,588,588,602,584,584,584, 0,584,584, 0, 0],
[586, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [586, 0, 0, 0,274, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[586,586,586, 0,587,587, 0, 0, 0, 0, 0,585,585, 0, 0], [586,586,586,610,587,587,595, 0,376, 0, 0,585,585, 0, 0],
[586, 0, 0, 0, 0,587, 0,617,585,585,492, 33, 33, 0, 0], [586, 0,600, 0, 0,587, 33,617,585,585,492, 33, 33, 0, 0],
[586, 0,586, 0,587,587,587, 0,584,584,584,584,584, 0, 0], [586, 0,586,614,587,587,587, 0,584,584,584,584,584, 0, 0],
[ 92, 0,586, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 92, 0,586, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[584,584,584,584,584,584,584, 93, 0, 0, 0, 0, 0, 0, 0] [584,584,584,584,584,584,584, 93, 0, 0, 0, 0, 0, 0, 0]
], ],

View File

@ -1,45 +1,71 @@
main.floors.MT48= main.floors.MT48=
{ {
"floorId": "MT48", "floorId": "MT48",
"title": "冰封高原", "title": "冰封高原",
"name": "48", "name": "48",
"width": 15, "width": 15,
"height": 15, "height": 15,
"canFlyTo": true, "canFlyTo": true,
"canFlyFrom": true, "canFlyFrom": true,
"canUseQuickShop": true, "canUseQuickShop": true,
"cannotViewMap": false, "cannotViewMap": false,
"images": [], "images": [],
"ratio": 8, "ratio": 8,
"defaultGround": "T580", "defaultGround": "T580",
"bgm": "winter.mp3", "bgm": "winter.mp3",
"firstArrive": [], "firstArrive": [],
"eachArrive": [], "eachArrive": [],
"parallelDo": "", "parallelDo": "",
"events": {}, "events": {},
"changeFloor": {}, "changeFloor": {},
"beforeBattle": {}, "beforeBattle": {},
"afterBattle": {}, "afterBattle": {},
"afterGetItem": {}, "afterGetItem": {},
"afterOpenDoor": {}, "afterOpenDoor": {},
"autoEvent": {}, "autoEvent": {},
"cannotMove": {}, "cannotMove": {},
"cannotMoveIn": {}, "cannotMoveIn": {},
"map": [ "map": [
[ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[ 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, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0],
[ 0, 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, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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": [
]
} }

View File

@ -3,6 +3,7 @@ import fss from 'fs';
import fse from 'fs-extra'; import fse from 'fs-extra';
import Fontmin from 'fontmin'; import Fontmin from 'fontmin';
import { exec } from 'child_process'; import { exec } from 'child_process';
import * as babel from '@babel/core';
(async function () { (async function () {
// 1. 去除未使用的文件 // 1. 去除未使用的文件
@ -100,7 +101,7 @@ import { exec } from 'child_process';
exec( exec(
`babel ${data.main.plugin `babel ${data.main.plugin
.map(v => `./dist/project/plugin/${v}.js`) .map(v => `./dist/project/plugin/${v}.js`)
.join(' ')} --out-file ./dist/project/plugin.min.js` .join(' ')} --out-file ./dist/project/plugin.m.js`
).on('close', async () => { ).on('close', async () => {
const main = await fs.readFile('./dist/main.js', 'utf-8'); const main = await fs.readFile('./dist/main.js', 'utf-8');
await fs.writeFile( await fs.writeFile(

View File

@ -15,6 +15,7 @@ import shadow from './plugin/shadow/shadow';
import gameShadow from './plugin/shadow/gameShadow'; import gameShadow from './plugin/shadow/gameShadow';
import achievement from './plugin/ui/achievement'; import achievement from './plugin/ui/achievement';
import completion, { floors } from './plugin/completion'; import completion, { floors } from './plugin/completion';
import path from './plugin/fx/path';
function forward() { function forward() {
const toForward: any[] = [ const toForward: any[] = [
@ -34,7 +35,8 @@ function forward() {
shadow(), shadow(),
gameShadow(), gameShadow(),
achievement(), achievement(),
completion() completion(),
path()
]; ];
// 初始化所有插件并转发到core上 // 初始化所有插件并转发到core上

View File

@ -3,7 +3,7 @@
<div id="enemy-desc"> <div id="enemy-desc">
<span>怪物描述</span> <span>怪物描述</span>
<Scroll id="enemy-desc-scroll"> <Scroll id="enemy-desc-scroll">
<span>{{ enemy.description }}</span> <span>&nbsp;&nbsp;&nbsp;&nbsp;{{ enemy.description }}</span>
</Scroll> </Scroll>
</div> </div>
<a-divider dashed style="border-color: #ddd4"></a-divider> <a-divider dashed style="border-color: #ddd4"></a-divider>
@ -40,7 +40,7 @@ function mark() {
<style lang="less" scoped> <style lang="less" scoped>
#enemy-target { #enemy-target {
width: 100%; width: 100%;
font-size: 2.5vh; font-size: 2.8vh;
} }
#enemy-desc { #enemy-desc {

View File

@ -1,4 +1,4 @@
import { Animation, sleep, TimingFn } from 'mutate-animate'; import { Animation, circle, hyper, sleep, TimingFn } from 'mutate-animate';
import { completeAchievement } from '../ui/achievement'; import { completeAchievement } from '../ui/achievement';
import { has } from '../utils'; import { has } from '../utils';
import { ChaseCameraData, ChasePath, getChaseDataByIndex } from './data'; import { ChaseCameraData, ChasePath, getChaseDataByIndex } from './data';

496
src/plugin/fx/path.ts Normal file
View File

@ -0,0 +1,496 @@
import {
TimingFn,
linear,
bezierPath,
Animation,
hyper,
power,
sleep
} from 'mutate-animate';
import { has } from '../utils';
interface AnimatedPathShadow {
offsetX: number | TimingFn;
offsetY: number | TimingFn;
blur: number | TimingFn;
color: string | TimingFn<4>;
}
type AnimatedPathShadowEntry = [
keyof AnimatedPathShadow,
ValueOf<AnimatedPathShadow>
][];
type AnimatedPathFilterKey =
| 'blur'
| 'brightness'
| 'contrast'
| 'grayscale'
| 'hueRotate'
| 'opacity'
| 'saturate'
| 'sepia';
type AnimatedPathFilter = Record<AnimatedPathFilterKey, number | TimingFn>;
interface Path {
path: TimingFn<2>;
length: number;
}
export default function init() {
return { AnimatedPath, pathTest: test };
}
export class AnimatedPath {
/** 不同线条间是否连接起来,不连接的话中间可能会有短暂的中断 */
join: boolean = true;
/** 路径信息 */
linePath: Path[] = [];
/** 绘制画布 */
ctx: CanvasRenderingContext2D;
private dashStatus: number = 0;
private dashLength: number = 0;
private dashMode: (number | TimingFn)[] = [];
private lineWidth: number | TimingFn = 1;
private lineColor: string | TimingFn<4> = '#fff';
private lineShadow: Partial<AnimatedPathShadow> = {};
private lineFilter: Partial<AnimatedPathFilter> = {};
private pathClose: boolean = false;
constructor(ctx: CanvasRenderingContext2D) {
this.ctx = ctx;
}
/**
* 线
* @param mode 线
*
* input线
* @example path.dash([5, 10]); // 表示绘制时会先绘制5像素的实线之后10像素不绘制然后再绘制5像素实线以此类推。
* @example path.dash([5, (input) => Math.round(input * 10)]);
* // 表示绘制时先绘制5像素的实线然后会有一段不绘制不绘制的长度是动画完成度乘10以此类推。
*/
dash(mode: (number | TimingFn)[]): this {
const res = mode.slice();
if (mode.length % 2 === 1) res.push(...mode);
this.dashMode = mode;
return this;
}
/**
* 线
* @param width 线
* input
* @example path.width(2); // 设置线条宽度为2像素
* @example path.width((input) => input * 5); // 设置线条宽度为动画完成度的5倍
*/
width(width: number | TimingFn): this {
this.lineWidth = width;
return this;
}
/**
* 线
* @param color css颜色字符串或函数线
* input4rgba值
* @example path.color('#fff'); // 设置线条为白色
* @example path.color((input) => [input * 100, input * 50, input * 255, input * 0.8]);
* // 设置颜色的红色值为动画完成度的100倍绿色为动画完成度的50倍蓝色为动画完成度的255倍不透明度为动画完成度的0.8倍
*/
color(color: string | TimingFn<4>): this {
this.lineColor = color;
return this;
}
/**
* 线
* @param shadow offsetX(), offsetY(), blur(), color()
* color可传入字符串或函数
* ```ts
* path.shadow({
* offsetX: 3, // 横向偏移量为3
* offsetY: input => input * 10, // 纵向偏移量为动画完成度的10倍
* color: '#0008', // 颜色为半透明的黑色
* blur: 4 // 虚化程度为4
* })
* ```
*/
shadow(shadow: Partial<AnimatedPathShadow>): this {
this.lineShadow = shadow;
return this;
}
/**
* 线
* @param filter
* 1. `blur`:
* 2. `brightness`: `0-Infinity`
* 3. `contrast`: `0-Infinity`
* 4. `grayscale`: `0-100`
* 5. `hueRotate`: `0-360`
* 6. `invert`: `0-100`
* 7. `opacity`: `0-100`
* 8. `saturate`: `0-Infinity`
* 9. `sepia`: (怀)`0-100`
*
* ```ts
* path.filter({
* blur: 3, // 虚化程度为3
* contrast: input => 100 + input * 50 // 对比度增加动画完成度的50倍
* })
* ```
*/
filter(filter: Partial<AnimatedPathFilter>): this {
this.lineFilter = filter;
return this;
}
/**
*
*/
clear(): this {
this.linePath = [];
return this;
}
/**
* 线
* @param x1
* @param y1
* @param x2
* @param y2
* @returns
*/
line(x1: number, y1: number, x2: number, y2: number): this {
const dx = x2 - x1;
const dy = y2 - y1;
this.add(x => [x1 + dx * x, y1 + dy * x], Math.sqrt(dx ** 2 + dy ** 2));
return this;
}
/**
*
* @param x
* @param y
* @param r
* @param start 0
* @param end
*/
circle(x: number, y: number, r: number, start: number, end: number): this {
const dt = end - start;
this.add(
input => [
x + r * Math.cos(dt * input + start),
y + r * Math.sin(dt * input + start)
],
r * dt
);
return this;
}
/**
*
* @param x
* @param y
* @param a
* @param b
* @param start
* @param end
*/
ellipse(
x: number,
y: number,
a: number,
b: number,
start: number,
end: number
): this {
const dt = end - start;
this.add(input => [
x + a * Math.cos(dt * input + start),
y + b * Math.sin(dt * input + start)
]);
return this;
}
/**
*
* @param x
* @param y
* @param w
* @param h
* @param lineWidth 线
*/
rect(
x: number,
y: number,
w: number,
h: number,
lineWidth: number = 0
): this {
const x2 = x + w;
const y2 = y + h;
this.line(x, y, x2, y)
.line(x2, y, x2, y2)
.line(x2, y2, x, y2)
.line(x, y2, x, y - lineWidth / 2);
return this;
}
/**
* 线
* @param point
*/
bezier(...point: [number, number][]): this {
if (point.length < 2) {
throw new Error(`The point number of bezier must larger than 2.`);
}
const start = point[0];
const end = point.at(-1)!;
const cps = point.slice(1, -1);
this.add(bezierPath(start, end, ...cps));
return this;
}
/**
*
* @param path
* @param length calLength进行计算
*
* @example path.add(input => [input * 100, (input * 100) ** 2]); // 添加一个抛物线路径
*/
add(path: TimingFn<2>, length: number = this.calLength(path)): this {
this.linePath.push({
path,
length
});
return this;
}
/**
*
* @param close
*/
close(close: boolean): this {
this.pathClose = close;
return this;
}
/**
* 线使
* @param path
* @returns
*/
calLength(path: TimingFn<2>): number {
let [lastX, lastY] = path(0);
let length = 0;
for (let i = 1; i <= 1000; i++) {
const [x, y] = path(i * 0.001);
length += Math.sqrt((x - lastX) ** 2 + (y - lastY) ** 2);
lastX = x;
lastY = y;
}
return length;
}
/**
* 使
*/
drawImmediate(): this {
const totalLength = this.linePath.reduce(
(pre, cur) => pre + cur.length,
0
);
let drawed = 0;
this.linePath.forEach(v => {
this.drawFrom(v.path, 0, 1, drawed, v.length);
drawed += v.length / totalLength;
});
return this;
}
/**
*
* @param time 0drawImmediate函数齿
* @param timing
*/
draw(time: number, timing: TimingFn = linear()): this {
const totalLength = this.linePath.reduce(
(pre, cur) => pre + cur.length,
0
);
const ratio = this.linePath.map(v => v.length / totalLength);
let drawed = 0;
let now = 0;
let nowEnd = ratio[0];
let lastComplete = 0;
this.ctx.beginPath();
this.ctx.moveTo(...this.linePath[0].path(0));
const findNext = (input: number) => {
if (input >= 1 || nowEnd > input) return [];
const skipped: number[] = [];
while (1) {
drawed += ratio[now];
now++;
nowEnd += ratio[now];
if (input < nowEnd) {
lastComplete = drawed;
break;
} else skipped.push(now);
}
return skipped;
};
const ani = new Animation();
ani.register('path', 0);
ani.mode(timing).time(time).absolute().apply('path', 1);
ani.all().then(() => {
ani.ticker.destroy();
if (this.pathClose) {
this.ctx.beginPath();
this.ctx.moveTo(...this.linePath.at(-1)!.path(0.999));
this.ctx.lineTo(...this.linePath[0].path(0.001));
this.ctx.stroke();
}
this.ctx.closePath();
});
ani.ticker.add(() => {
const complete = ani.value.path;
if (complete >= nowEnd) {
const d = nowEnd - drawed;
const from = (lastComplete - drawed) / d;
this.drawFrom(
this.linePath[now].path,
from,
1,
lastComplete,
ratio[now]
);
const skipped = findNext(complete);
skipped.forEach(v => {
const path = this.linePath[v];
this.drawFrom(path.path, 0, 1, lastComplete, ratio[v]);
});
}
const fn = this.linePath[now].path;
const d = nowEnd - drawed;
const from = (lastComplete - drawed) / d;
const to = (complete - drawed) / d;
this.drawFrom(fn, from, to, lastComplete, ratio[now]);
lastComplete = complete;
});
return this;
}
private drawFrom(
path: TimingFn<2>,
from: number,
to: number,
complete: number,
ratio: number,
length?: number
) {
const [fx, fy] = path(from);
const [tx, ty] = path(to);
const l =
length ?? Math.ceil(Math.sqrt((tx - fx) ** 2 + (ty - fy) ** 2));
const step = (to - from) / l;
const ctx = this.ctx;
let [lastX, lastY] = path(from);
for (let i = 1; i <= l; i++) {
this.handleFx(complete + (ratio * i * step) / l);
const [x, y] = path(from + step * i);
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
lastX = x;
lastY = y;
}
}
private handleFx(complete: number) {
const ctx = this.ctx;
const width =
typeof this.lineWidth === 'number'
? this.lineWidth
: this.lineWidth(complete);
ctx.lineWidth = width;
let color;
if (typeof this.lineColor === 'string') {
color = this.lineColor;
} else {
const c = this.lineColor(complete);
color = `rgba(${c[0]},${c[1]},${c[2]},${c[3]})`;
}
ctx.strokeStyle = color;
const shadow: Partial<
Record<keyof AnimatedPathShadow, string | number>
> = {};
for (const [key, value] of Object.entries(
this.lineShadow
) as AnimatedPathShadowEntry) {
if (typeof value === 'function') {
const n = value(complete);
if (typeof n === 'number') {
shadow[key as Exclude<keyof AnimatedPathShadow, 'color'>] =
n;
} else {
shadow.color = `rgba(${n[0]},${n[1]},${n[2]},${n[3]})`;
}
} else {
// @ts-ignore
shadow[key] = value;
}
}
if (has(shadow.blur)) ctx.shadowBlur = shadow.blur as number;
if (has(shadow.offsetX)) ctx.shadowOffsetX = shadow.offsetX as number;
if (has(shadow.offsetY)) ctx.shadowOffsetY = shadow.offsetY as number;
if (has(shadow.color)) ctx.shadowColor = shadow.color as string;
let filter = '';
for (const [key, value] of Object.entries(this.lineFilter) as [
AnimatedPathFilterKey,
number | TimingFn
][]) {
let v;
if (typeof value === 'number') {
v = value;
} else {
v = value(complete);
}
if (key === 'blur') filter += `blur(${v}px)`;
else filter += `${key}(${v}%)`;
}
ctx.filter = filter;
}
}
async function test() {
const ctx = core.createCanvas('test', 0, 0, 480, 480, 100);
ctx.canvas.style.backgroundColor = '#000d';
const path = new AnimatedPath(ctx);
path.color('#fff')
.width(2)
.rect(100, 100, 280, 280, 2)
.close(true)
.draw(1000, power(5, 'in-out'));
await sleep(1050);
path.clear()
.bezier([200, 200], [280, 200], [280, 280])
.bezier([280, 280], [200, 280], [200, 200])
.draw(1000, power(5, 'in-out'));
await sleep(1050);
path.clear()
.bezier([280, 200], [200, 200], [200, 280])
.bezier([200, 280], [280, 280], [280, 200])
.draw(1000, power(5, 'in-out'));
}

View File

@ -1,6 +1,8 @@
import { Polygon } from './polygon'; import { Polygon } from './polygon';
import { import {
Light, Light,
getAllLights,
refreshLight,
removeAllLights, removeAllLights,
setBackground, setBackground,
setBlur, setBlur,
@ -9,29 +11,44 @@ import {
} from './shadow'; } from './shadow';
export default function init() { export default function init() {
const origin4 = control.prototype.drawHero;
control.prototype.drawHero = function () {
origin4.apply(core.control, arguments);
if (core.getFlag('__heroOpacity__') !== 0) {
getAllLights().forEach(v => {
if (!v.followHero) return;
v._offset ??= { x: v.x, y: v.y };
v.x = core.status.heroCenter.px + v._offset.x;
v.y = core.status.heroCenter.py + v._offset.y;
refreshLight();
});
}
};
return { updateShadow, clearShadowCache, setCalShadow }; return { updateShadow, clearShadowCache, setCalShadow };
} }
const shadowInfo: Partial<Record<FloorIds, Light[]>> = { const shadowInfo: Partial<Record<FloorIds, Light[]>> = {
MT43: [ MT48: [
{ {
id: 'mt42_1', id: 'mt48_1',
x: 280, x: 0,
y: 220, y: 0,
decay: 100, decay: 1000,
r: 300, r: 2000,
color: '#0000' color: '#0000',
followHero: true
} }
] ]
}; };
const backgroundInfo: Partial<Record<FloorIds, Color>> = { const backgroundInfo: Partial<Record<FloorIds, Color>> = {
MT43: '#0008' MT48: '#0008'
}; };
const blurInfo: Partial<Record<FloorIds, number>> = { const blurInfo: Partial<Record<FloorIds, number>> = {
MT43: 4 MT48: 4
}; };
const immersionInfo: Partial<Record<FloorIds, number>> = { const immersionInfo: Partial<Record<FloorIds, number>> = {
MT43: 8 MT48: 4
}; };
const shadowCache: Partial<Record<FloorIds, Polygon[]>> = {}; const shadowCache: Partial<Record<FloorIds, Polygon[]>> = {};

View File

@ -24,12 +24,16 @@ export interface Light {
color: Color; color: Color;
/** 是否可以被物体遮挡 */ /** 是否可以被物体遮挡 */
noShelter?: boolean; noShelter?: boolean;
/** 是否跟随勇士 */
followHero?: boolean;
/** 正在动画的属性 */ /** 正在动画的属性 */
_animating?: Record<string, boolean>; _animating?: Record<string, boolean>;
/** 执行渐变的属性 */ /** 执行渐变的属性 */
_transition?: Record<string, TransitionInfo>; _transition?: Record<string, TransitionInfo>;
/** 表示是否是代理只有设置渐变后才会变为true */ /** 表示是否是代理只有设置渐变后才会变为true */
_isProxy?: boolean; _isProxy?: boolean;
/** 跟随勇士的时候的偏移量 */
_offset?: Loc;
} }
export default function init() { export default function init() {
@ -49,7 +53,8 @@ export default function init() {
animateLight, animateLight,
transitionLight, transitionLight,
moveLightAs, moveLightAs,
getAllLights getAllLights,
refreshLight
}; };
} }
@ -171,6 +176,13 @@ export function setBackground(color: Color) {
needRefresh = true; needRefresh = true;
} }
/**
*
*/
export function refreshLight() {
needRefresh = true;
}
/** /**
* *
* @param id id * @param id id
@ -416,12 +428,14 @@ export function drawShadow() {
// 绘制阴影一个光源一个光源地绘制然后source-out获得光然后把光叠加再source-out获得最终阴影 // 绘制阴影一个光源一个光源地绘制然后source-out获得光然后把光叠加再source-out获得最终阴影
for (let i = 0; i < lights.length; i++) { for (let i = 0; i < lights.length; i++) {
const { x, y, r, decay, color, noShelter } = lights[i]; const { x, y, r, decay, color, noShelter } = lights[i];
const rx = x + 32;
const ry = y + 32;
// 绘制阴影 // 绘制阴影
ct1.clearRect(0, 0, w, h); ct1.clearRect(0, 0, w, h);
ct2.clearRect(0, 0, w, h); ct2.clearRect(0, 0, w, h);
if (!noShelter) { if (!noShelter) {
for (const polygon of shadowNodes) { for (const polygon of shadowNodes) {
const area = polygon.shadowArea(x + 32, y + 32, r); const area = polygon.shadowArea(rx, ry, r);
area.forEach(v => { area.forEach(v => {
ct1.beginPath(); ct1.beginPath();
ct1.moveTo(v[0][0], v[0][1]); ct1.moveTo(v[0][0], v[0][1]);
@ -439,21 +453,21 @@ export function drawShadow() {
ct2.globalCompositeOperation = 'source-over'; ct2.globalCompositeOperation = 'source-over';
ct2.drawImage(temp1, 0, 0, w, h); ct2.drawImage(temp1, 0, 0, w, h);
ct2.globalCompositeOperation = 'source-out'; ct2.globalCompositeOperation = 'source-out';
const gra = ct2.createRadialGradient(x, y, decay, x, y, r); const gra = ct2.createRadialGradient(rx, ry, decay, rx, ry, r);
gra.addColorStop(0, core.arrayToRGBA(color)); gra.addColorStop(0, core.arrayToRGBA(color));
gra.addColorStop(1, 'transparent'); gra.addColorStop(1, 'transparent');
ct2.fillStyle = gra; ct2.fillStyle = gra;
ct2.beginPath(); ct2.beginPath();
ct2.arc(x, y, r, 0, Math.PI * 2); ct2.arc(rx, ry, r, 0, Math.PI * 2);
ct2.fill(); ct2.fill();
ctx.drawImage(temp2, 0, 0, w, h); ctx.drawImage(temp2, 0, 0, w, h);
// 再绘制ct1的阴影然后绘制到ct3叠加 // 再绘制ct1的阴影然后绘制到ct3叠加
ct1.globalCompositeOperation = 'source-out'; ct1.globalCompositeOperation = 'source-out';
const gra2 = ct1.createRadialGradient(x, y, decay, x, y, r); const gra2 = ct1.createRadialGradient(rx, ry, decay, rx, ry, r);
gra2.addColorStop(0, '#fff'); gra2.addColorStop(0, '#fff');
gra2.addColorStop(1, '#fff0'); gra2.addColorStop(1, '#fff0');
ct1.beginPath(); ct1.beginPath();
ct1.arc(x, y, r, 0, Math.PI * 2); ct1.arc(rx, ry, r, 0, Math.PI * 2);
ct1.fillStyle = gra2; ct1.fillStyle = gra2;
ct1.fill(); ct1.fill();
// 绘制到ct3上 // 绘制到ct3上

View File

@ -751,8 +751,8 @@ interface Control {
* @example core.getHeroLoc(); // 获取主角的位置和朝向 * @example core.getHeroLoc(); // 获取主角的位置和朝向
* @param name * @param name
*/ */
getHeroLoc(): Loc; getHeroLoc(): DiredLoc;
getHeroLoc<K extends keyof Loc>(name: K): Loc[K]; getHeroLoc<K extends keyof DiredLoc>(name: K): DiredLoc[K];
/** /**
* *

2
src/types/util.d.ts vendored
View File

@ -890,3 +890,5 @@ type NonObjectOf<T> = SelectType<T, NonObject>;
type EndsWith<T extends string> = `${string}${T}`; type EndsWith<T extends string> = `${string}${T}`;
type KeyExcludesUnderline<T> = Exclude<keyof T, `_${string}`>; type KeyExcludesUnderline<T> = Exclude<keyof T, `_${string}`>;
type ValueOf<T> = T[keyof T];