Compare commits

...

21 Commits

Author SHA1 Message Date
f29c76ffc9 fix: rectr 标签 2025-01-02 13:52:43 +08:00
77dfbb5a62 Merge branch 'dev-strawberry' of github.com:unanmed/HumanBreak into dev-strawberry 2025-01-02 13:51:52 +08:00
7ae1eb1e21 fix: graphic元素错位问题 2025-01-02 13:51:34 +08:00
285dc2e48f feat: 圆角矩形接口 2025-01-02 13:50:44 +08:00
dbe633290e feat: Icon\winskin完成 2025-01-02 13:48:58 +08:00
d61beada0b fix: strokeAndFill in checkMode 2025-01-02 13:48:58 +08:00
2aa0ffe12a feat: patchProp for GraphicItemBase 2025-01-02 13:48:58 +08:00
5c9419280e style: graphics.ts 2025-01-02 13:48:58 +08:00
29f1f00e23 feat: graphic的patchProp写完了 2025-01-02 13:48:58 +08:00
1650865a6a feat:graphics完成 2025-01-02 13:48:58 +08:00
8024a39e39 style: eslint 2025-01-02 13:48:15 +08:00
0c399a03fe feat: add eslint 2024-12-30 22:34:17 +08:00
08f5d49e00 pref: 圆圆交叉判断 2024-12-29 16:12:53 +08:00
fa7b415c31 style: 将jsx元素的props拆分出来,提高可读性 2024-12-28 19:48:33 +08:00
8b5f94b65b fix: 分裂弹幕在摧毁后应该分裂 2024-12-28 16:45:05 +08:00
fc3a8fb81f refactor: delete pop.ts 2024-12-26 21:51:39 +08:00
4102893916 feat: 第二章特殊战第一种弹幕 2024-12-24 18:11:18 +08:00
44276183bb feat: ui控制器接口设计 2024-12-24 18:10:49 +08:00
e99320c52d feat: 修改文本框样式 2024-12-23 23:14:39 +08:00
734980eb6a feat: 文本框接入样板 2024-12-23 23:09:50 +08:00
e5e0458891 fix: 视角同步问题 & delegateTicker执行时机 2024-12-23 21:07:00 +08:00
33 changed files with 2002 additions and 322 deletions

1
.gitignore vendored
View File

@ -48,3 +48,4 @@ script/special.ts
script/people.ts
docs/
user.ts
.antlr

View File

@ -13,4 +13,4 @@ public/editor.html
keyCodes.ts
src/core/main/setting.ts
src/core/fx/shadow.ts
src/core/fx/shadow_upload.js
dist/

View File

@ -1,3 +1,9 @@
{
"recommendations": ["Vue.volar"]
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vue.volar",
"slevesque.shader",
"tobermory.es6-string-html"
]
}

70
eslint.config.js Normal file
View File

@ -0,0 +1,70 @@
import eslint from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import eslintPluginVue from 'eslint-plugin-vue';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
export default tseslint.config(
{
ignores: ['node_modules', 'dist', 'public']
},
eslint.configs.recommended,
...tseslint.configs.recommended,
...eslintPluginVue.configs['flat/recommended'],
{
files: ['**/*.{js,mjs,cjs,vue}'],
rules: {
'no-console': 'warn'
}
},
{
languageOptions: {
globals: {
...globals.browser,
wx: true
}
}
},
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
ecmaVersion: 'latest',
ecmaFeatures: {
jsx: true
}
}
},
rules: {
'vue/no-mutating-props': [
'error',
{
shallowOnly: true
}
]
}
},
{
files: ['**/*.{ts,tsx,vue}'],
rules: {
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-this-alias': 'off'
}
},
eslintPluginPrettierRecommended
);

View File

@ -17,6 +17,7 @@
"@ant-design/icons-vue": "^6.1.0",
"@emotion/css": "^11.13.0",
"@vueuse/core": "^10.11.1",
"@wasm-audio-decoders/ogg-vorbis": "^0.1.16",
"anon-tokyo": "0.0.0-alpha.0",
"ant-design-vue": "^3.2.20",
"axios": "^1.7.4",
@ -52,15 +53,20 @@
"@vitejs/plugin-vue-jsx": "^3.1.0",
"chokidar": "^3.6.0",
"compressing": "^1.10.1",
"eslint": "^9.17.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"fontmin": "^0.9.9",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"globals": "^15.14.0",
"less": "^4.2.0",
"postcss-preset-env": "^9.6.0",
"rollup": "^3.29.4",
"terser": "^5.31.6",
"tsx": "^4.17.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.18.2",
"unplugin-vue-components": "^0.22.12",
"vite": "^4.5.3",
"vue-tsc": "^2.1.6",

File diff suppressed because it is too large Load Diff

View File

@ -855,7 +855,7 @@ actions.prototype._sys_keyDownCtrl = function () {
core.status.event.id == 'action' &&
core.status.event.data.type == 'text'
) {
core.doAction();
this._clickAction_text();
return true;
}
if (
@ -1117,16 +1117,25 @@ actions.prototype._clickAction_text = function () {
// 正在淡入淡出的话不执行
if (core.status.event.animateUI) return;
var data = core.clone(core.status.event.data.current);
if (typeof data == 'string') data = { type: 'text', text: data };
const Store = Mota.require('module', 'Render').TextboxStore;
const store = Store.get('main-textbox');
// var data = core.clone(core.status.event.data.current);
// if (typeof data == 'string') data = { type: 'text', text: data };
// 打字机效果显示全部文字
if (core.status.event.interval != null) {
data.showAll = true;
core.insertAction(data);
core.doAction();
if (store.typing) {
store.endType();
return;
} else {
store.hide();
}
// if (core.status.event.interval != null) {
// data.showAll = true;
// core.insertAction(data);
// core.doAction();
// return;
// }
if (!data.code) {
core.ui._animateUI('hide', null, core.doAction);

View File

@ -1553,24 +1553,70 @@ events.prototype.__action_doAsyncFunc = function (isAsync, func) {
events.prototype._action_text = function (data, x, y, prefix) {
if (this.__action_checkReplaying()) return;
data.text = core.replaceText(data.text, prefix);
var ctx = data.code ? '__text__' + data.code : null;
data.ctx = ctx;
if (core.getContextByName(ctx) && !data.showAll) {
core.ui._animateUI('hide', ctx, function () {
core.ui.drawTextBox(data.text, data);
core.ui._animateUI('show', ctx, function () {
if (data.async) core.doAction();
});
});
return;
const Store = Mota.require('module', 'Render').TextboxStore;
const store = Store.get('main-textbox');
const { text } = data;
let title = '';
let inTitle = false;
let titleStartIndex = 0;
let titleEndIndex = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (inTitle) {
if (char === '\\' && text[i + 1] === ']') {
title += ']';
i++;
} else if (char === ']') {
inTitle = false;
titleEndIndex = i + 1;
break;
} else {
title += char;
}
core.ui.drawTextBox(data.text, data);
if (!data.showAll) {
core.ui._animateUI('show', ctx, function () {
if (data.async) core.doAction();
});
continue;
}
if (char === '\t' && text[i + 1] === '[') {
inTitle = true;
titleStartIndex = i;
// 跳转至方括号内
i++;
continue;
}
if (char === '\\' && text[i + 1] === 't' && text[i + 2] === '[') {
inTitle = true;
titleStartIndex = i;
// 跳转至方括号内
i += 2;
continue;
}
}
const showTitle =
text.slice(0, titleStartIndex) + text.slice(titleEndIndex);
store.show();
store.modify({ text: showTitle, title });
// data.text = core.replaceText(data.text, prefix);
// var ctx = data.code ? '__text__' + data.code : null;
// data.ctx = ctx;
// if (core.getContextByName(ctx) && !data.showAll) {
// core.ui._animateUI('hide', ctx, function () {
// core.ui.drawTextBox(data.text, data);
// core.ui._animateUI('show', ctx, function () {
// if (data.async) core.doAction();
// });
// });
// return;
// }
// core.ui.drawTextBox(data.text, data);
// if (!data.showAll) {
// core.ui._animateUI('show', ctx, function () {
// if (data.async) core.doAction();
// });
// }
};
events.prototype._action_moveTextBox = function (data, x, y, prefix) {

View File

@ -22,7 +22,7 @@ main.floors.snowShop=
"而且,一共就只有三件装备("
],
"7,5": [
"\t[商店老板,N636]\b[up,7,5]请随意挑选",
"\t[商店老板]请随意挑选",
{
"type": "openShop",
"id": "snowShop",

View File

@ -76,6 +76,7 @@ import './render/index';
import * as RenderUtils from './render/utils';
import '@/module';
import { MotaOffscreenCanvas2D } from './fx/canvas2d';
import { TextboxStore } from './render/index';
// ----- 类注册
Mota.register('class', 'AudioPlayer', AudioPlayer);
@ -163,7 +164,8 @@ Mota.register('module', 'Render', {
LayerGroupFloorBinder,
Camera,
MotaOffscreenCanvas2D,
Utils: RenderUtils
Utils: RenderUtils,
TextboxStore
});
Mota.register('module', 'Action', {
HeroKeyMover

View File

@ -16,26 +16,17 @@ import { Transform } from '../transform';
import { isSetEqual } from '../utils';
import { logger } from '@/core/common/logger';
import { Sprite } from '../sprite';
import { onTick } from '../renderer';
import { ContainerProps, onTick } from '../renderer';
import { isNil } from 'lodash-es';
import { SetupComponentOptions } from './types';
import EventEmitter from 'eventemitter3';
import { Container } from '../container';
export const enum WordBreak {
/** 不换行 */
None,
/** 仅空格和连字符等可换行CJK 字符可任意换行,默认值 */
Space,
/** 所有字符都可以换行 */
All
}
export const enum TextAlign {
Left,
Center,
End
}
import { Text } from '../preset';
import {
buildFont,
TextAlign,
ITextContentRenderData,
WordBreak
} from './textboxHelper';
let testCanvas: MotaOffscreenCanvas2D;
Mota.require('var', 'loading').once('coreInit', () => {
@ -46,41 +37,9 @@ Mota.require('var', 'loading').once('coreInit', () => {
testCanvas.freeze();
});
export interface TextContentProps {
text: string;
x?: number;
y?: number;
width?: number;
height?: number;
/** 字体 */
font?: string;
/** 是否持续上一次的文本,开启后,如果修改后的文本以修改前的文本为开头,那么会继续播放而不会从头播放 */
keepLast?: boolean;
/** 打字机时间间隔,即两个字出现之间相隔多长时间 */
interval?: number;
/** 行高 */
lineHeight?: number;
/** 分词规则 */
wordBreak?: WordBreak;
/** 文字对齐方式 */
textAlign?: TextAlign;
/** 行首忽略字符,即不会出现在行首的字符 */
ignoreLineStart?: Iterable<string>;
/** 行尾忽略字符,即不会出现在行尾的字符 */
ignoreLineEnd?: Iterable<string>;
/** 会被分词规则识别的分词字符 */
breakChars?: Iterable<string>;
/** 填充样式 */
fillStyle?: CanvasStyle;
/** 描边样式 */
strokeStyle?: CanvasStyle;
/** 线宽 */
strokeWidth?: number;
/** 是否填充 */
fill?: boolean;
/** 是否描边 */
stroke?: boolean;
}
export interface TextContentProps
extends ContainerProps,
ITextContentRenderData {}
export type TextContentEmits = {
typeEnd: () => void;
@ -103,9 +62,8 @@ interface TextContentData {
interface TextContentRenderable {
x: number;
y: number;
/** 行高为0时表示两行间为默认行距 */
height: number;
lineHeight: number;
/** 这一行文字的高度,即 measureText 算出的高度 */
textHeight: number;
/** 这一行的文字 */
@ -121,7 +79,10 @@ interface TextContentRenderable {
const textContentOptions = {
props: [
'breakChars',
'font',
'fontFamily',
'fontSize',
'fontBold',
'fontItalic',
'height',
'ignoreLineEnd',
'ignoreLineStart',
@ -138,7 +99,8 @@ const textContentOptions = {
'fillStyle',
'strokeStyle',
'strokeWidth',
'stroke'
'stroke',
'showAll'
],
emits: ['typeEnd', 'typeStart']
} satisfies SetupComponentOptions<
@ -155,14 +117,18 @@ export const TextContent = defineComponent<
if (props.width && props.width <= 0) {
logger.warn(41, String(props.width));
}
const renderData: Required<TextContentProps> = {
const renderData: Required<ITextContentRenderData> = shallowReactive({
text: props.text,
textAlign: props.textAlign ?? TextAlign.Left,
x: props.x ?? 0,
y: props.y ?? 0,
width: !props.width || props.width <= 0 ? 200 : props.width,
height: props.height ?? 200,
font: props.font ?? core.status.globalAttribute?.font ?? '16px Verdana',
fontFamily:
props.fontFamily ?? core.status.globalAttribute?.font ?? 'Verdana',
fontSize: props.fontSize ?? 16,
fontBold: props.fontWeight ?? false,
fontItalic: props.fontItalic ?? false,
ignoreLineEnd: props.ignoreLineEnd ?? new Set(),
ignoreLineStart: props.ignoreLineStart ?? new Set(),
keepLast: props.keepLast ?? false,
@ -174,8 +140,9 @@ export const TextContent = defineComponent<
strokeStyle: props.strokeStyle ?? 'transparent',
fill: props.fill ?? true,
stroke: props.stroke ?? false,
strokeWidth: props.strokeWidth ?? 2
};
strokeWidth: props.strokeWidth ?? 2,
showAll: props.showAll ?? false
});
const ensureProps = () => {
for (const [key, value] of Object.entries(props)) {
@ -188,7 +155,7 @@ export const TextContent = defineComponent<
renderData.width = value;
}
} else {
// @ts-ignore
// @ts-expect-error may use spread?
renderData[key] = value;
}
}
@ -200,7 +167,12 @@ export const TextContent = defineComponent<
return {
text: renderData.text,
width: renderData.width!,
font: renderData.font!,
font: buildFont(
renderData.fontFamily,
renderData.fontSize,
renderData.fontWeight,
renderData.fontItalic
),
wordBreak: renderData.wordBreak!,
ignoreLineStart: new Set(renderData.ignoreLineStart),
ignoreLineEnd: new Set(renderData.ignoreLineEnd),
@ -242,9 +214,11 @@ export const TextContent = defineComponent<
const time = Date.now();
const char =
Math.floor((time - startTime) / renderData.interval!) + fromChar;
if (!isFinite(char)) {
if (!isFinite(char) || renderData.showAll) {
renderable.forEach(v => (v.pointer = v.text.length));
needUpdate = false;
linePointer = dirtyIndex.length;
emit('typeEnd');
return;
}
while (linePointer < dirtyIndex.length) {
@ -259,6 +233,7 @@ export const TextContent = defineComponent<
break;
}
}
if (linePointer >= dirtyIndex.length) {
needUpdate = false;
renderable.forEach(v => (v.pointer = v.text.length));
@ -277,26 +252,28 @@ export const TextContent = defineComponent<
transform: Transform
) => {
const ctx = canvas.ctx;
ctx.font = renderData.font;
// ctx.font = renderData.font;
ctx.fillStyle = renderData.fillStyle;
ctx.strokeStyle = renderData.strokeStyle;
ctx.lineWidth = renderData.strokeWidth;
let y = renderable[0]?.textHeight ?? 0;
renderable.forEach(v => {
if (v.pointer === 0) return;
const text = v.text.slice(0, v.pointer);
if (renderData.textAlign === TextAlign.Left) {
if (renderData.stroke) ctx.strokeText(text, v.x, v.y);
if (renderData.fill) ctx.fillText(text, v.x, v.y);
if (renderData.stroke) ctx.strokeText(text, v.x, y);
if (renderData.fill) ctx.fillText(text, v.x, y);
} else if (renderData.textAlign === TextAlign.Center) {
const x = (renderData.width - v.x) / 2 + v.x;
if (renderData.stroke) ctx.strokeText(text, x, v.y);
if (renderData.fill) ctx.fillText(text, x, v.y);
if (renderData.stroke) ctx.strokeText(text, x, y);
if (renderData.fill) ctx.fillText(text, x, y);
} else {
const x = renderData.width;
if (renderData.stroke) ctx.strokeText(text, x, v.y);
if (renderData.fill) ctx.fillText(text, x, v.y);
if (renderData.stroke) ctx.strokeText(text, x, y);
if (renderData.fill) ctx.fillText(text, x, y);
}
y += v.textHeight + v.lineHeight;
});
};
@ -313,54 +290,52 @@ export const TextContent = defineComponent<
index: number,
from: number
) => {
renderable.splice(index);
dirtyIndex.splice(0);
dirtyIndex.push(index);
// 初始化渲染
linePointer = 0;
startTime = Date.now();
fromChar = from;
needUpdate = true;
// renderable.splice(index);
// dirtyIndex.splice(0);
// dirtyIndex.push(index);
// // 初始化渲染
// linePointer = 0;
// startTime = Date.now();
// fromChar = from;
// needUpdate = true;
let startY = renderable.reduce(
(prev, curr) => prev + curr.textHeight + curr.height,
0
);
// 第一个比较特殊,需要特判
const start = lines[index - 1] ?? 0;
const end = lines[index];
const startPointer = from > start && from < end ? from - start : 0;
const height = testHeight(text, renderData.font!);
startY += height;
renderable.push({
text: text.slice(start, end),
x: 0,
y: startY,
height: renderData.lineHeight!,
textHeight: height,
pointer: startPointer,
from: start,
to: end
});
// let startY = renderable.reduce(
// (prev, curr) => prev + curr.textHeight + curr.lineHeight,
// 0
// );
// // 第一个比较特殊,需要特判
// const start = lines[index - 1] ?? 0;
// const end = lines[index];
// const startPointer = from > start && from < end ? from - start : 0;
// const height = testHeight(text, renderData.font!);
// startY += height;
// renderable.push({
// text: text.slice(start, end),
// x: 0,
// lineHeight: renderData.lineHeight!,
// textHeight: height,
// pointer: startPointer,
// from: start,
// to: end ?? text.length
// });
for (let i = index + 1; i < lines.length; i++) {
dirtyIndex.push(i);
const start = lines[i - 1] ?? 0;
const end = lines[i];
const height = testHeight(text, renderData.font!);
startY += height;
// for (let i = index + 1; i < lines.length; i++) {
// dirtyIndex.push(i);
// const start = lines[i - 1] ?? 0;
// const end = lines[i];
// const height = testHeight(text, renderData.font!);
// startY += height;
renderable.push({
text: text.slice(start, end),
x: 0,
y: startY,
height: renderData.lineHeight!,
textHeight: height,
pointer: 0,
from: start,
to: end
});
}
// renderable.push({
// text: text.slice(start, end),
// x: 0,
// lineHeight: renderData.lineHeight!,
// textHeight: height,
// pointer: 0,
// from: start,
// to: end ?? text.length
// });
// }
emit('typeStart');
};
@ -443,9 +418,8 @@ export const TextContent = defineComponent<
return () => {
return (
<sprite
{...renderData}
ref={spriteElement}
hd
antiAliasing={true}
x={renderData.x}
y={renderData.y}
width={renderData.width}
@ -456,24 +430,52 @@ export const TextContent = defineComponent<
};
}, textContentOptions);
export interface TextboxProps extends TextContentProps {
id?: string;
export interface TextboxProps extends TextContentProps, ContainerProps {
/** 背景颜色 */
backColor?: CanvasStyle;
/** 背景 winskin */
winskin?: string;
/** 边框与文字间的距离默认为8 */
padding?: number;
/** 标题 */
title?: string;
/** 标题字体 */
titleFont?: string;
/** 标题填充样式 */
titleFill?: CanvasStyle;
/** 标题描边样式 */
titleStroke?: CanvasStyle;
/** 标题文字与边框间的距离默认为4 */
titlePadding?: number;
}
type TextboxEmits = TextContentEmits;
type TextboxSlots = SlotsType<{ default: (data: TextboxProps) => VNode[] }>;
type TextboxSlots = SlotsType<{
default: (data: TextboxProps) => VNode[];
title: (data: TextboxProps) => VNode[];
}>;
const textboxOptions = {
props: (textContentOptions.props as (keyof TextboxProps)[]).concat([
'backColor',
'winskin',
'id'
'id',
'padding',
'alpha',
'hidden',
'anchorX',
'anchorY',
'antiAliasing',
'cache',
'composite',
'fall',
'hd',
'transform',
'type',
'zIndex',
'titleFill',
'titleStroke',
'titleFont'
]),
emits: textContentOptions.emits
} satisfies SetupComponentOptions<TextboxProps, {}, string, TextboxSlots>;
@ -494,24 +496,113 @@ export const Textbox = defineComponent<
data.width ??= 200;
data.height ??= 200;
data.id ??= '';
data.alpha ??= 1;
data.titleFill ??= '#000';
data.titleStroke ??= 'transparent';
data.titleFont ??= '16px Verdana';
data.titlePadding ??= 4;
const store = TextboxStore.use(props.id ?? getNextTextboxId(), data);
const hidden = ref(false);
const titleElement = ref<Text>();
const titleWidth = ref(data.titlePadding * 2);
const titleHeight = ref(data.titlePadding * 2);
const contentY = computed(() => {
const height = titleHeight.value;
return data.title ? height : 0;
});
const contentWidth = computed(() => data.width! - data.padding! * 2);
const contentHeight = computed(
() => data.height! - data.padding! * 2 - contentY.value
);
const calTitleSize = (text: string) => {
if (!titleElement.value) return;
const { width, height } = titleElement.value;
titleWidth.value = width + data.titlePadding! * 2;
titleHeight.value = height + data.titlePadding! * 2;
data.title = text;
};
watch(titleElement, (value, old) => {
old?.off('setText', calTitleSize);
value?.on('setText', calTitleSize);
if (value) calTitleSize(value?.text);
});
onUnmounted(() => {
titleElement.value?.off('setText', calTitleSize);
});
// ----- store
/** 结束打字机 */
const storeEmits: TextboxStoreEmits = {
endType() {
data.showAll = true;
}
};
const store = TextboxStore.use(
props.id ?? getNextTextboxId(),
data,
storeEmits
);
const hidden = ref(data.hidden);
store.on('hide', () => (hidden.value = true));
store.on('show', () => (hidden.value = false));
onUpdated(() => {
for (const [key, value] of Object.entries(props)) {
// @ts-ignore
if (!isNil(value)) data[key] = value;
store.on('update', value => {
if (value.title) {
titleElement.value?.requestBeforeFrame(() => {
const { width, height } = titleElement.value!;
titleWidth.value = width + data.padding! * 2;
titleHeight.value = height + data.padding! * 2;
});
}
});
const contentWidth = computed(() => data.width! - data.padding! * 2);
const contentHeight = computed(() => data.height! - data.padding! * 2);
const onTypeStart = () => {
store.emitTypeStart();
};
const onTypeEnd = () => {
data.showAll = false;
store.emitTypeEnd();
};
return () => {
return (
<container hidden={hidden.value} id="11111">
<container {...data} hidden={hidden.value} alpha={data.alpha}>
{data.title ? (
<container
zIndex={10}
width={titleWidth.value}
height={titleHeight.value}
>
{slots.title ? (
slots.title(data)
) : props.winskin ? (
<winskin></winskin>
) : (
<g-rect
x={0}
y={0}
width={titleWidth.value}
height={titleHeight.value}
fillStyle={data.backColor}
></g-rect>
)}
<text
ref={titleElement}
text={data.title}
x={data.titlePadding}
y={data.titlePadding}
fillStyle={data.titleFill}
strokeStyle={data.titleStroke}
font={data.titleFont}
></text>
</container>
) : (
''
)}
{slots.default ? (
slots.default(data)
) : props.winskin ? (
@ -521,25 +612,37 @@ export const Textbox = defineComponent<
// todo
<g-rect
x={0}
y={0}
y={contentY.value}
width={data.width ?? 200}
height={data.height ?? 200}
height={(data.height ?? 200) - contentY.value}
fill
fillStyle={data.backColor}
></g-rect>
)}
<TextContent
{...data}
x={data.padding}
y={data.padding}
hidden={false}
x={data.padding!}
y={contentY.value + data.padding!}
width={contentWidth.value}
height={contentHeight.value}
onTypeEnd={onTypeEnd}
onTypeStart={onTypeStart}
zIndex={0}
showAll={data.showAll}
></TextContent>
</container>
);
};
}, textboxOptions);
interface LineSplitData {
text: string;
font: string;
wait: number;
icon: AllIds | AllNumbers;
}
const fontSizeGuessScale = new Map<string, number>([
['px', 1],
['%', 0.2],
@ -569,7 +672,7 @@ function splitLines(data: TextContentData) {
// 对文字二分,然后计算长度
const text = data.text;
const res: number[] = [];
const fontSize = data.font.match(/\s*[\d\.-]+[a-zA-Z%]*\s*/)?.[0].trim();
const fontSize = data.font.match(/\s*[\d\.-]+[a-zA-Z%]+\s*/)?.[0].trim();
const unit = fontSize?.match(/[a-zA-Z%]+/)?.[0];
const guessScale = fontSizeGuessScale.get(unit ?? '') ?? 0.2;
const guessSize = parseInt(fontSize ?? '1') * guessScale;
@ -721,19 +824,53 @@ function testHeight(text: string, font: string) {
return ctx.measureText(text).fontBoundingBoxAscent;
}
interface TextboxStoreEmits {
endType: () => void;
}
interface TextboxStoreEvent {
update: [value: TextboxProps];
show: [];
hide: [];
typeStart: [];
typeEnd: [];
}
export class TextboxStore extends EventEmitter<TextboxStoreEvent> {
static list: Map<string, TextboxStore> = new Map();
private constructor(private readonly data: TextboxProps) {
typing: boolean = false;
private constructor(
private readonly data: TextboxProps,
private readonly emits: TextboxStoreEmits
) {
super();
}
/**
*
*/
emitTypeStart() {
this.typing = true;
this.emit('typeStart');
}
/**
*
*/
emitTypeEnd() {
this.typing = false;
this.emit('typeEnd');
}
/**
*
*/
endType() {
this.emits.endType();
}
/**
*
*/
@ -772,8 +909,8 @@ export class TextboxStore extends EventEmitter<TextboxStoreEvent> {
* @param id id
* @param props
*/
static use(id: string, props: TextboxProps) {
const store = new TextboxStore(props);
static use(id: string, props: TextboxProps, emits: TextboxStoreEmits) {
const store = new TextboxStore(props, emits);
if (this.list.has(id)) {
logger.warn(42, id);
}

View File

@ -0,0 +1,111 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import EventEmitter from 'eventemitter3';
export const enum WordBreak {
/** 不换行 */
None,
/** 仅空格和连字符等可换行CJK 字符可任意换行,默认值 */
Space,
/** 所有字符都可以换行 */
All
}
export const enum TextAlign {
Left,
Center,
End
}
export interface ITextContentRenderData {
text: string;
x?: number;
y?: number;
width?: number;
height?: number;
/** 字体类型 */
fontFamily?: string;
/** 字体大小 */
fontSize?: number;
/** 字体线宽 */
fontWeight?: number;
/** 是否斜体 */
fontItalic?: boolean;
/** 是否持续上一次的文本,开启后,如果修改后的文本以修改前的文本为开头,那么会继续播放而不会从头播放 */
keepLast?: boolean;
/** 打字机时间间隔,即两个字出现之间相隔多长时间 */
interval?: number;
/** 行高 */
lineHeight?: number;
/** 分词规则 */
wordBreak?: WordBreak;
/** 文字对齐方式 */
textAlign?: TextAlign;
/** 行首忽略字符,即不会出现在行首的字符 */
ignoreLineStart?: Iterable<string>;
/** 行尾忽略字符,即不会出现在行尾的字符 */
ignoreLineEnd?: Iterable<string>;
/** 会被分词规则识别的分词字符 */
breakChars?: Iterable<string>;
/** 填充样式 */
fillStyle?: CanvasStyle;
/** 描边样式 */
strokeStyle?: CanvasStyle;
/** 线宽 */
strokeWidth?: number;
/** 是否填充 */
fill?: boolean;
/** 是否描边 */
stroke?: boolean;
/** 是否无视打字机,强制全部显示 */
showAll?: boolean;
}
export const enum TextContentType {
Text,
Wait,
Icon
}
export interface ITextContentRenderable {
type: TextContentType;
text: string;
wait?: number;
icon?: AllNumbers;
}
interface TextContentTyperEvent {
typeStart: [];
typeEnd: [];
}
export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
testCanvas: MotaOffscreenCanvas2D;
constructor(public readonly data: Required<ITextContentRenderData>) {
super();
this.testCanvas = new MotaOffscreenCanvas2D(false);
this.testCanvas.withGameScale(false);
this.testCanvas.setHD(false);
this.testCanvas.size(32, 32);
this.testCanvas.freeze();
}
/**
*
*/
setText(text: string) {
this.data.text = text;
}
private parse(text: string) {}
}
export function buildFont(
family: string,
size: number,
weight: number = 500,
italic: boolean = false
) {
return `${italic ? 'italic ' : ''}${weight} ${size}px ${family}`;
}

View File

@ -13,24 +13,17 @@ import { PopText } from '@/plugin/fx/pop';
import { FloorChange } from '@/plugin/fallback';
import { createApp } from './renderer';
import { defineComponent } from 'vue';
import { Textbox } from './components';
import { ILayerGroupRenderExtends, ILayerRenderExtends } from './preset';
import { Props } from './utils';
let main: MotaRenderer;
Mota.require('var', 'loading').once('coreInit', () => {
main = new MotaRenderer();
const Com = defineComponent(props => {
return () => (
<container
id="map-draw"
hd
antiAliasing={false}
width={core._PX_}
height={core._PY_}
>
<layer-group
id="layer-main"
ex={[
const App = defineComponent(_props => {
const layerGroupExtends: ILayerGroupRenderExtends[] = [
new FloorDamageExtends(),
new FloorItemDetail(),
new LayerGroupFilter(),
@ -38,30 +31,50 @@ Mota.require('var', 'loading').once('coreInit', () => {
new LayerGroupHalo(),
new LayerGroupAnimate(),
new FloorViewport()
]}
>
<layer layer="bg" zIndex={10}></layer>
<layer layer="bg2" zIndex={20}></layer>
<layer
layer="event"
zIndex={30}
ex={[
];
const eventExtends: ILayerRenderExtends[] = [
new HeroRenderer(),
new LayerDoorAnimate(),
new LayerShadowExtends()
]}
></layer>
];
const mapDrawProps: Props<'container'> = {
width: core._PX_,
height: core._PY_
};
const mainTextboxProps: Props<typeof Textbox> = {
text: '',
hidden: true,
width: 480,
height: 150,
y: 330,
zIndex: 30,
fillStyle: '#fff',
titleFill: 'gold',
fontFamily: 'normal',
titleFont: '700 20px normal',
winskin: 'winskin2.png',
interval: 25,
lineHeight: 6
};
return () => (
<container id="map-draw" {...mapDrawProps}>
<layer-group id="layer-main" ex={layerGroupExtends}>
<layer layer="bg" zIndex={10}></layer>
<layer layer="bg2" zIndex={20}></layer>
<layer layer="event" zIndex={30} ex={eventExtends}></layer>
<layer layer="fg" zIndex={40}></layer>
<layer layer="fg2" zIndex={50}></layer>
<PopText id="pop-main" zIndex={80}></PopText>
</layer-group>
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
<FloorChange id="floor-change" zIndex={50}></FloorChange>
</container>
);
});
main.hide();
createApp(Com).mount(main);
createApp(App).mount(main);
// render(<Com></Com>, main);
console.log(main);

View File

@ -446,10 +446,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
endFn: end
};
RenderItem.tickerMap.set(id, delegation);
RenderItem.ticker.add(fn);
if (typeof time === 'number' && time < 2147438647 && time > 0) {
delegation.timeout = window.setTimeout(() => {
RenderItem.ticker.remove(fn);
RenderItem.tickerMap.delete(id);
end?.();
}, time);
}
@ -541,7 +540,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
* RenderItem继承类中复写它使
* @param child
*/
appendChild(...child: RenderItem<any>[]): void {
appendChild(..._child: RenderItem<any>[]): void {
logger.warn(35);
}
@ -549,7 +548,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
* RenderItem继承类中复写它使
* @param child
*/
removeChild(...child: RenderItem<any>[]): void {
removeChild(..._child: RenderItem<any>[]): void {
logger.warn(36);
}
@ -613,9 +612,10 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
_namespace?: ElementNamespace,
_parentComponent?: ComponentInternalInstance | null
): void {
if (isNil(prevValue) && isNil(nextValue)) return;
switch (key) {
case 'x': {
if (!this.assertType(nextValue, 'number', key)) return;
@ -722,13 +722,16 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
}
}
RenderItem.ticker.add(() => {
RenderItem.ticker.add(time => {
// slice 是为了让函数里面的 request 进入下一帧执行
if (beforeFrame.length > 0) {
const arr = beforeFrame.slice();
beforeFrame.splice(0);
arr.forEach(v => v());
}
RenderItem.tickerMap.forEach(v => {
v.fn(time);
});
if (renderFrame.length > 0) {
const arr = renderFrame.slice();
renderFrame.splice(0);

View File

@ -76,7 +76,7 @@ export class LayerGroupAnimate implements ILayerGroupRenderExtends {
this.listen();
}
onDestroy(group: LayerGroup): void {
onDestroy(_group: LayerGroup): void {
if (this.checkHero()) {
this.hero!.off('moveTick', this.onMoveTick);
LayerGroupAnimate.animateList.delete(this);

View File

@ -16,7 +16,9 @@ import { logger } from '@/core/common/logger';
type CanvasStyle = string | CanvasGradient | CanvasPattern;
export interface ETextEvent extends ERenderItemEvent {}
export interface ETextEvent extends ERenderItemEvent {
setText: [text: string];
}
export class Text extends RenderItem<ETextEvent> {
text: string;
@ -76,6 +78,7 @@ export class Text extends RenderItem<ETextEvent> {
this.text = text;
this.calBox();
if (this.parent) this.update(this);
this.emit('setText', text);
}
/**
@ -129,10 +132,10 @@ export class Text extends RenderItem<ETextEvent> {
this.setText(nextValue);
return;
case 'fillStyle':
this.setStyle(nextValue);
this.setStyle(nextValue, this.strokeStyle);
return;
case 'strokeStyle':
this.setStyle(void 0, nextValue);
this.setStyle(this.fillStyle, nextValue);
return;
case 'font':
if (!this.assertType(nextValue, 'string', key)) return;

View File

@ -67,8 +67,10 @@ export class FloorViewport implements ILayerGroupRenderExtends {
const halfHeight = core._PY_ / 2;
const cell = this.group.cellSize;
const half = cell / 2;
this.nx = -(nx - halfWidth + half) / this.group.cellSize;
this.ny = -(ny - halfHeight + half) / this.group.cellSize;
this.applyPosition(
-(nx - halfWidth + half) / this.group.cellSize,
-(ny - halfHeight + half) / this.group.cellSize
);
this.mutateTo(x, y);
}
@ -116,8 +118,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.group.removeTicker(this.transition, false);
this.nx = nx;
this.ny = ny;
this.applyPosition(nx, ny);
}
/**
@ -198,8 +199,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
const { x, y } = this.hero!.renderable;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.nx = nx;
this.ny = ny;
this.applyPosition(nx, ny);
if (ending) {
if (this.ox === xTarget && this.oy == yTarget) {
@ -264,41 +264,32 @@ export class FloorViewport implements ILayerGroupRenderExtends {
const progress = fn((now - start) / time);
const tx = dx * progress;
const ty = dy * progress;
this.nx = tx + sx;
this.ny = ty + sy;
this.applyPosition(tx + sx, ty + sy);
},
time,
() => {
this.nx = x;
this.ny = y;
this.applyPosition(x, y);
this.inTransition = false;
}
);
}
private create() {
let nx = this.nx;
let ny = this.ny;
private applyPosition(x: number, y: number) {
if (!this.enabled) return;
if (x === this.nx && y === this.ny) return;
const halfWidth = core._PX_ / 2;
const halfHeight = core._PY_ / 2;
this.delegation = this.group.delegateTicker(() => {
if (!this.enabled) return;
if (this.nx === nx && this.ny === ny) {
return;
}
const cell = this.group.cellSize;
const half = cell / 2;
nx = this.nx;
ny = this.ny;
const { x: bx, y: by } = this.getBoundedPosition(nx, ny);
this.nx = x;
this.ny = y;
const { x: bx, y: by } = this.getBoundedPosition(x, y);
const rx = bx * cell - halfWidth + half;
const ry = by * cell - halfHeight + half;
core.bigmap.offsetX = rx;
core.bigmap.offsetY = ry;
this.group.camera.setTranslate(-rx, -ry);
this.group.update(this.group);
});
// this.createMoving();
}
private checkDependency() {
@ -319,7 +310,6 @@ export class FloorViewport implements ILayerGroupRenderExtends {
awake(group: LayerGroup): void {
this.group = group;
this.create();
adapter.add(this);
}

View File

@ -7,7 +7,7 @@ import {
import { ERenderItemEvent, RenderItem } from '../item';
import { tagMap } from './map';
import { logger } from '@/core/common/logger';
import { Comment, Text } from '../preset/misc';
import { Comment, ETextEvent, Text } from '../preset/misc';
export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
patchProp: function (
@ -24,7 +24,7 @@ export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
insert: function (
el: RenderItem<ERenderItemEvent>,
parent: RenderItem,
anchor?: RenderItem<ERenderItemEvent> | null
_anchor?: RenderItem<ERenderItemEvent> | null
): void {
parent.appendChild(el);
},
@ -48,7 +48,7 @@ export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
return onCreate(namespace, isCustomizedBuiltIn, vnodeProps);
},
createText: function (text: string): RenderItem<ERenderItemEvent> {
createText: function (text: string): RenderItem<ETextEvent> {
if (!/^\s*$/.test(text)) logger.warn(38);
return new Text(text);
},

View File

@ -21,9 +21,9 @@ import {
Line,
Path,
QuadraticCurve,
Rect
Rect,
RectR
} from '../preset/graphics';
import { BaseProps } from './props';
type OnItemCreate<
E extends ERenderItemEvent = ERenderItemEvent,
@ -217,10 +217,10 @@ tagMap.register('layer-group', (_0, _1, props) => {
return l;
}
});
tagMap.register<EDamageEvent, Damage>('damage', (_0, _1, props) => {
tagMap.register<EDamageEvent, Damage>('damage', (_0, _1, _props) => {
return new Damage();
});
tagMap.register('animation', (_0, _1, props) => {
tagMap.register('animation', (_0, _1, _props) => {
return new Animate();
});
tagMap.register('g-rect', se(Rect, 'absolute', ElementState.None));
@ -230,7 +230,7 @@ tagMap.register('g-line', se(Line, 'absolute', ElementState.None));
tagMap.register('g-bezier', se(BezierCurve, 'absolute', ElementState.None));
tagMap.register('g-quad', se(QuadraticCurve, 'absolute', ElementState.None));
tagMap.register('g-path', se(Path, 'absolute', ElementState.None));
tagMap.register('g-rectr', se(Path, 'absolute', ElementState.None));
tagMap.register('g-rectr', se(RectR, 'absolute', ElementState.None));
tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('winskin', (_0, _1, props) => {
if (!props) return new Winskin(core.material.images.images['winskin.png']);

View File

@ -1,6 +1,21 @@
import { Ticker, TimingFn } from 'mutate-animate';
import { RenderAdapter } from './adapter';
import { FloorViewport } from './preset/viewport';
import { JSX } from 'vue/jsx-runtime';
import { Component, DefineComponent, DefineSetupFnComponent } from 'vue';
export type Props<
T extends
| keyof JSX.IntrinsicElements
| DefineSetupFnComponent<any>
| DefineComponent
> = T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: T extends DefineSetupFnComponent<any>
? InstanceType<T>['$props']
: T extends DefineComponent
? InstanceType<T>['$props']
: unknown;
export function disableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport');

1
src/core/system/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './ui';

View File

@ -0,0 +1,55 @@
import { Component, VNodeProps } from 'vue';
export interface IUIControllerConfig<Element, UI> {
/**
* ui挂载至目标元素时的操作
* @param element
* @param ui ui对象
*/
insert(element: Element, ui: UI): void;
/**
* ui从目标元素上移除时的操作
* @param element ui的父元素
* @param ui ui元素
*/
remove(element: Element, ui: UI): void;
/**
* UI
* @param component UI组件
* @param props UI传递的props
*/
createUI(
component: Component,
props?: (VNodeProps & { [key: string]: any }) | null
): UI;
}
export const enum OpenOption {
Push,
Unshift
}
export const enum CloseOption {
Splice,
Pop,
Shift
}
export class UIController<Element, UI> {
constructor(config: IUIControllerConfig<Element, UI>) {}
/**
* ui改变时控制器的行为
* @param open
* @param close
*/
setChangeMode(open: OpenOption, close: CloseOption) {}
/**
* UI控制器挂载至容器上
* @param container
*/
mount(container: Element) {}
}

View File

@ -0,0 +1 @@
export * from './controller';

View File

@ -69,7 +69,7 @@
"39": "Plain text is not supported outside Text element.",
"40": "Cannot return canvas that is not provided by this pool.",
"41": "Width of text content components must be positive. receive: $1",
"42": "Repeat Textbox id: '$1'.",
"42": "Repeated Textbox id: '$1'.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
}

View File

@ -41,6 +41,7 @@ import type * as Animation from 'mutate-animate';
import type * as RenderUtils from '@/core/render/utils';
import type { WeatherController } from '@/module/weather/weather';
import type { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import type { TextboxStore } from '@/core/render';
interface ClassInterface {
// 渲染进程与游戏进程通用
@ -123,6 +124,7 @@ interface ModuleInterface {
Camera: typeof Camera;
MotaOffscreenCanvas2D: typeof MotaOffscreenCanvas2D;
Utils: typeof RenderUtils;
TextboxStore: typeof TextboxStore;
};
State: {
ItemState: typeof ItemState;
@ -155,7 +157,6 @@ type InterfaceType = keyof SystemInterfaceMap;
interface PluginInterface {
// 渲染进程定义的插件
pop_r: typeof import('../plugin/pop');
use_r: typeof import('../plugin/use');
fly_r: typeof import('../plugin/ui/fly');
chase_r: typeof import('../plugin/chase');

View File

@ -17,8 +17,11 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
/** 开始时刻 */
private startTime: number = 0;
/** 当前帧数 */
frame: number = 0;
/** 上一帧的时刻 */
lastTime: number = 0;
/** 这个boss战的主渲染元素所有弹幕都会在此之上渲染 */
abstract readonly main: BossSprite;
@ -31,17 +34,19 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
* boss的ai
* @param time
* @param frame
* @param dt
*/
abstract ai(time: number, frame: number): void;
abstract ai(time: number, frame: number, dt: number): void;
private tick = () => {
const now = Date.now();
this.ai(now - this.startTime, this.frame);
const dt = now - this.lastTime;
this.ai(now - this.startTime, this.frame, dt);
this.frame++;
this.projectiles.forEach(v => {
const time = now - v.startTime;
v.time = time;
v.ai(this, time, v.frame);
v.ai(this, time, v.frame, dt);
v.frame++;
if (time > 60_000) {
this.destroyProjectile(v);
@ -50,6 +55,7 @@ export abstract class BarrageBoss extends EventEmitter<BarrageBossEvent> {
v.doDamage(this.state);
}
});
this.lastTime = now;
};
/**
@ -230,8 +236,9 @@ export abstract class Projectile<T extends BarrageBoss = BarrageBoss> {
* @param boss boss
* @param time
* @param frame
* @param dt
*/
abstract ai(boss: T, time: number, frame: number): void;
abstract ai(boss: T, time: number, frame: number, dt: number): void;
/**
* boss的弹幕应该全部画在同一层
@ -409,8 +416,8 @@ export namespace Hitbox {
export function checkCircleCircle(circle1: Circle, circle2: Circle) {
const dx = circle1.x - circle2.x;
const dy = circle1.y - circle2.y;
const dis = Math.hypot(dx, dy);
return dis <= circle1.radius + circle2.radius;
const dis = dx ** 2 + dy ** 2;
return dis <= (circle1.radius + circle2.radius) ** 2;
}
/**

View File

@ -0,0 +1,131 @@
import { IStateDamageable } from '@/game/state/interface';
import { BarrageBoss, BossSprite, Hitbox } from './barrage';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import {
Container,
HeroRenderer,
LayerGroup,
MotaRenderer,
RenderItem,
Shader,
Transform
} from '@/core/render';
import { Pop } from '../fx/pop';
import { SplittableBall } from './palaceBossProjectile';
import { PointEffect } from '../fx/pointShader';
Mota.require('var', 'loading').once('coreInit', () => {
const shader = new Shader();
shader.size(480, 480);
shader.setHD(true);
shader.setZIndex(120);
PalaceBoss.shader = shader;
PalaceBoss.effect.create(shader, 40);
});
const enum BossStage {
Prologue,
Stage1,
Stage2,
Stage3,
Stage4,
End
}
export class PalaceBoss extends BarrageBoss {
static effect: PointEffect = new PointEffect();
static shader: Shader;
main: BossSprite<BarrageBoss>;
hitbox: Hitbox.Circle;
state: IStateDamageable;
private stage: BossStage = BossStage.Prologue;
/** 用于展示傅里叶频谱的背景元素 */
private back: SonicBack;
/** 楼层渲染元素 */
private group: LayerGroup;
/** 楼层渲染容器 */
private mapDraw: Container;
/** 伤害弹出 */
pop: Pop;
private heroHp: number = 0;
constructor() {
super();
const render = MotaRenderer.get('render-main')!;
this.group = render.getElementById('layer-main') as LayerGroup;
this.mapDraw = render.getElementById('map-draw') as Container;
this.pop = render.getElementById('pop-main') as Pop;
this.state = core.status.hero;
this.main = new BossEffect('static', this);
this.back = new SonicBack('static');
const { x, y } = core.status.hero.loc;
const cell = 32;
this.hitbox = new Hitbox.Circle(x + cell / 2, y + cell / 2, cell / 3);
}
override start(): void {
super.start();
PalaceBoss.shader.append(this.mapDraw);
this.main.append(this.group);
// const event = this.group.getLayer('event');
// const hero = event?.getExtends('floor-hero') as HeroRenderer;
// hero?.on('moveTick', this.moveTick);
SplittableBall.init({});
this.heroHp = core.status.hero.hp;
}
override end(): void {
super.end();
PalaceBoss.shader.remove();
this.main.remove();
this.back.remove();
this.main.destroy();
this.back.destroy();
// const event = this.group.getLayer('event');
// const hero = event?.getExtends('floor-hero') as HeroRenderer;
// hero?.off('moveTick', this.moveTick);
SplittableBall.end();
PalaceBoss.effect.end();
core.status.hero.hp = this.heroHp;
Mota.Plugin.require('replay_g').clip('choices:0');
}
ai(time: number, frame: number): void {}
}
class BossEffect extends BossSprite<PalaceBoss> {
protected preDraw(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): boolean {
return true;
}
protected postDraw(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {}
}
class SonicBack extends RenderItem {
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {}
}

View File

@ -0,0 +1,271 @@
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { Transform } from '@/core/render';
import { IStateDamageable } from '@/game/state/interface';
import { Hitbox, Projectile } from './barrage';
import type { PalaceBoss } from './palaceBoss';
import { clamp } from '../utils';
function popDamage(damage: number, boss: PalaceBoss, color: string) {
const { x, y } = core.status.hero.loc;
boss.pop.addPop(
(-damage).toString(),
1000,
x * 32 + 16,
y * 32 + 16,
color
);
}
export interface ISplitData {
split: boolean;
/** 分裂时刻,以弹幕被创建时刻为基准 */
time: number;
/** 分裂起始角度,以该弹幕朝向方向为 0 */
startAngle: number;
/** 分裂终止角度,以该弹幕朝向方向为 0 */
endAngle: number;
/** 每秒加速度 */
acc: number;
/** 初始速度 */
startVel: number;
/** 终止速度 */
endVel: number;
/** 持续时长 */
lastTime: number;
/** 分裂数量 */
count: number;
/** 这个弹幕分裂产生的弹幕的分裂信息,不填则表示产生的弹幕不会分裂 */
data?: ISplitData;
}
export class SplittableBall extends Projectile<PalaceBoss> {
damage: number = 10000;
hitbox: Hitbox.Circle = new Hitbox.Circle(0, 0, 8);
static ball: Map<string, MotaOffscreenCanvas2D> = new Map();
private damaged: boolean = false;
private splitData?: ISplitData;
private last: number = 60_000;
/** 角度,水平向右为 0顺时针旋转一圈为 Math.PI * 2 */
private angle: number = 0;
/** 每秒加速度 */
private acc: number = 0;
/** 初始速度,每秒多少像素 */
private startVel: number = 0;
/** 终止速度 */
private endVel: number = 0;
/** 弹幕颜色 */
private color?: string;
private startVelX: number = 0;
private startVelY: number = 0;
private endVelX: number = 0;
private endVelY: number = 0;
private vx: number = 0;
private vy: number = 0;
// 加速度
private ax: number = 0;
private ay: number = 0;
/** 是否已经分裂过 */
private splitted: boolean = false;
static init(colors: Record<string, string[]>) {
this.ball.clear();
for (const [key, color] of Object.entries(colors)) {
const canvas = new MotaOffscreenCanvas2D();
canvas.size(32, 32);
canvas.withGameScale(true);
canvas.setHD(true);
const ctx = canvas.ctx;
const gradient = ctx.createRadialGradient(16, 16, 8, 16, 16, 16);
const step = 1 / (color.length - 1);
for (let i = 0; i < color.length; i++) {
gradient.addColorStop(i * step, color[i]);
}
ctx.fillStyle = gradient;
ctx.arc(16, 16, 16, 0, Math.PI * 2);
ctx.fill();
canvas.freeze();
this.ball.set(key, canvas);
}
}
static end() {
this.ball.forEach(v => {
v.clear();
v.delete();
});
this.ball.clear();
}
/**
*
* @param time
*/
setLastTime(time: number) {
this.last = time;
}
/**
*
* @param data
*/
setSplitData(data?: ISplitData) {
this.splitData = data;
}
/**
*
*/
private calVel() {
const sin = Math.sin(this.angle);
const cos = Math.cos(this.angle);
const vel = Math.hypot(this.vx, this.vy);
this.startVelX = this.startVel * cos;
this.startVelY = this.startVel * sin;
this.endVelX = this.endVel * cos;
this.endVelY = this.endVel * sin;
this.ax = this.acc * cos;
this.ay = this.acc * sin;
this.vx = vel * cos;
this.vy = vel * sin;
}
/**
*
* @param angle
*/
setAngle(angle: number) {
this.angle = angle;
this.calVel();
}
/**
*
* @param start
* @param end
*/
setVel(start: number, end: number) {
this.startVel = start;
this.endVel = end;
this.calVel();
}
/**
*
* @param acc
*/
setAcc(acc: number) {
this.acc = acc;
this.calVel();
}
/**
*
* @param color
*/
setColor(color: string) {
this.color = color;
}
isIntersect(hitbox: Hitbox.HitboxType): boolean {
if (hitbox instanceof Hitbox.Circle) {
return Hitbox.checkCircleCircle(hitbox, this.hitbox);
} else {
return false;
}
}
updateHitbox(x: number, y: number): void {
this.hitbox.setCenter(x, y);
}
doDamage(target: IStateDamageable): boolean {
if (this.damaged) return false;
target.hp -= this.damage;
this.damaged = true;
core.drawHeroAnimate('hand');
popDamage(this.damage, this.boss, '#ff8180');
return true;
}
private split(boss: PalaceBoss) {
if (!this.splitData?.split) return;
if (this.splitted) return;
this.splitted = true;
const {
startAngle,
endAngle,
startVel,
endVel,
acc,
lastTime,
count,
data
} = this.splitData;
const sa = this.angle + startAngle;
const ea = this.angle + endAngle;
const step = (ea - sa - 1) / count;
const { x, y } = this.hitbox;
for (let i = 0; i < count; i++) {
const proj = boss.createProjectile(SplittableBall, x, y);
proj.setAngle(sa + step * i);
proj.setAcc(acc);
proj.setVel(startVel, endVel);
proj.setLastTime(lastTime);
proj.setSplitData(data);
}
}
ai(boss: PalaceBoss, time: number, frame: number, dt: number): void {
if (this.splitData?.split) {
if (time > this.splitData.time) {
this.split(boss);
}
}
if (time > this.last) {
this.destroy();
return;
}
const p = dt / 1000;
this.vx += this.ax * p;
this.vy += this.ay * p;
const sx = Math.sign(this.vx);
const sy = Math.sign(this.vy);
const cx = clamp(
Math.abs(this.vx),
Math.abs(this.startVelX),
Math.abs(this.endVelX)
);
const cy = clamp(
Math.abs(this.vy),
Math.abs(this.startVelY),
Math.abs(this.endVelY)
);
this.vx = cx * sx;
this.vy = cy * sy;
const { x, y } = this.hitbox;
this.setPosition(x + this.vx * p, y + this.vy * p);
}
render(canvas: MotaOffscreenCanvas2D, transform: Transform): void {
if (!this.color) return;
const texture = SplittableBall.ball.get(this.color);
if (!texture) return;
const ctx = canvas.ctx;
ctx.drawImage(texture.canvas, this.x - 16, this.y - 16, 32, 32);
}
destroy(): void {
this.split(this.boss);
super.destroy();
}
}

View File

@ -179,6 +179,8 @@ export class TowerBoss extends BarrageBoss {
this.healthBar.remove();
this.word.remove();
this.main.remove();
this.main.destroy();
this.healthBar.destroy();
const event = this.group.getLayer('event');
const hero = event?.getExtends('floor-hero') as HeroRenderer;

View File

@ -1,7 +1,6 @@
import * as fly from './ui/fly';
import * as chase from './chase';
import * as completion from './completion';
import * as pop from './pop';
import * as use from './use';
import * as gameCanvas from './fx/gameCanvas';
import * as animateController from './animateController';
@ -13,7 +12,6 @@ import './loopMap';
Mota.Plugin.register('fly_r', fly);
Mota.Plugin.register('chase_r', chase);
Mota.Plugin.register('completion_r', completion, completion.init);
Mota.Plugin.register('pop_r', pop, pop.init);
Mota.Plugin.register('use_r', use);
Mota.Plugin.register('gameCanvas_r', gameCanvas);
Mota.Plugin.register(

View File

@ -1,60 +0,0 @@
// 示例插件:文字弹出
// todo: 重写
let pop: any[] = [];
let time = 0;
export function init() {
// core.registerAnimationFrame('pop', true, popValue);
}
/**
*
*/
function popValue(t: number) {
if (t - time < 15) return;
let ctx = core.getContextByName('pop')!;
if (!ctx) ctx = core.createCanvas('pop', 0, 0, core._PX_, core._PY_, 90);
core.clearMap(ctx);
let count = 0;
pop.forEach(function (one) {
// 由frame计算出dy
const dy = 6 - one.frame * 0.2;
const dx = 1;
one.py -= dy;
one.px += dx;
one.frame++;
// 绘制
if (one.frame >= 60) core.setAlpha(ctx, 3 - one.frame / 30);
else core.setAlpha(ctx, 1);
core.fillBoldText(
ctx,
one.value,
one.px,
one.py,
'#f22',
'#000',
'24px normal'
);
if (one.frame >= 90) count++;
});
if (count > 0) pop.splice(0, count);
time = t;
}
/**
*
* @param px
* @param py
* @param value
*/
export function addPop(px: number, py: number, value: string) {
var data = {
px: px,
py: py,
value: value,
frame: 0
};
pop.push(data);
}

View File

@ -517,3 +517,11 @@ export function calStringSize(str: string) {
return size;
}
export function clamp(num: number, start: number, end: number) {
const s = Math.min(start, end);
const e = Math.max(start, end);
if (num < s) return s;
else if (num > e) return e;
return num;
}

View File

@ -11,7 +11,7 @@ const FSHOST = 'http://127.0.0.1:3000/';
const custom = [
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon'
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin'
]
// https://vitejs.dev/config/