diff --git a/public/libs/actions.js b/public/libs/actions.js index 247af90..422db39 100644 --- a/public/libs/actions.js +++ b/public/libs/actions.js @@ -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); diff --git a/public/libs/events.js b/public/libs/events.js index 61a327d..590f098 100644 --- a/public/libs/events.js +++ b/public/libs/events.js @@ -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; - } - core.ui.drawTextBox(data.text, data); - if (!data.showAll) { - core.ui._animateUI('show', ctx, function () { - if (data.async) core.doAction(); - }); + 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; + } + 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) { diff --git a/public/project/floors/snowShop.js b/public/project/floors/snowShop.js index d349a07..f80f89f 100644 --- a/public/project/floors/snowShop.js +++ b/public/project/floors/snowShop.js @@ -22,7 +22,7 @@ main.floors.snowShop= "而且,一共就只有三件装备(" ], "7,5": [ - "\t[商店老板,N636]\b[up,7,5]请随意挑选", + "\t[商店老板]请随意挑选", { "type": "openShop", "id": "snowShop", diff --git a/src/core/render/components/textbox.tsx b/src/core/render/components/textbox.tsx index 7ffca3c..3a260e4 100644 --- a/src/core/render/components/textbox.tsx +++ b/src/core/render/components/textbox.tsx @@ -16,11 +16,11 @@ 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'; +import { Text } from '../preset'; export const enum WordBreak { /** 不换行 */ @@ -46,7 +46,7 @@ Mota.require('var', 'loading').once('coreInit', () => { testCanvas.freeze(); }); -export interface TextContentProps { +interface TextContentRenderData { text: string; x?: number; y?: number; @@ -80,8 +80,14 @@ export interface TextContentProps { fill?: boolean; /** 是否描边 */ stroke?: boolean; + /** 是否无视打字机,强制全部显示 */ + showAll?: boolean; } +export interface TextContentProps + extends ContainerProps, + TextContentRenderData {} + export type TextContentEmits = { typeEnd: () => void; typeStart: () => void; @@ -103,9 +109,8 @@ interface TextContentData { interface TextContentRenderable { x: number; - y: number; /** 行高,为0时表示两行间为默认行距 */ - height: number; + lineHeight: number; /** 这一行文字的高度,即 measureText 算出的高度 */ textHeight: number; /** 这一行的文字 */ @@ -138,7 +143,8 @@ const textContentOptions = { 'fillStyle', 'strokeStyle', 'strokeWidth', - 'stroke' + 'stroke', + 'showAll' ], emits: ['typeEnd', 'typeStart'] } satisfies SetupComponentOptions< @@ -155,7 +161,7 @@ export const TextContent = defineComponent< if (props.width && props.width <= 0) { logger.warn(41, String(props.width)); } - const renderData: Required = { + const renderData: Required = shallowReactive({ text: props.text, textAlign: props.textAlign ?? TextAlign.Left, x: props.x ?? 0, @@ -174,8 +180,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)) { @@ -242,9 +249,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 +268,7 @@ export const TextContent = defineComponent< break; } } + if (linePointer >= dirtyIndex.length) { needUpdate = false; renderable.forEach(v => (v.pointer = v.text.length)); @@ -282,21 +292,23 @@ export const TextContent = defineComponent< 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; }); }; @@ -323,7 +335,7 @@ export const TextContent = defineComponent< needUpdate = true; let startY = renderable.reduce( - (prev, curr) => prev + curr.textHeight + curr.height, + (prev, curr) => prev + curr.textHeight + curr.lineHeight, 0 ); // 第一个比较特殊,需要特判 @@ -335,12 +347,11 @@ export const TextContent = defineComponent< renderable.push({ text: text.slice(start, end), x: 0, - y: startY, - height: renderData.lineHeight!, + lineHeight: renderData.lineHeight!, textHeight: height, pointer: startPointer, from: start, - to: end + to: end ?? text.length }); for (let i = index + 1; i < lines.length; i++) { @@ -353,12 +364,11 @@ export const TextContent = defineComponent< renderable.push({ text: text.slice(start, end), x: 0, - y: startY, - height: renderData.lineHeight!, + lineHeight: renderData.lineHeight!, textHeight: height, pointer: 0, from: start, - to: end + to: end ?? text.length }); } emit('typeStart'); @@ -443,9 +453,8 @@ export const TextContent = defineComponent< return () => { return ( ; @@ -494,24 +528,110 @@ 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(); + 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 ( - ); diff --git a/src/core/render/item.ts b/src/core/render/item.ts index ebd39f5..0dedf17 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -446,10 +446,9 @@ export abstract class RenderItem 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); } @@ -616,6 +615,7 @@ export abstract class RenderItem 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 } } -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); diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts index fcb3847..4310b14 100644 --- a/src/core/render/preset/misc.ts +++ b/src/core/render/preset/misc.ts @@ -7,7 +7,9 @@ import { AutotileRenderable, RenderableData } from '../cache'; type CanvasStyle = string | CanvasGradient | CanvasPattern; -export interface ETextEvent extends ERenderItemEvent {} +export interface ETextEvent extends ERenderItemEvent { + setText: [text: string]; +} export class Text extends RenderItem { text: string; @@ -67,6 +69,7 @@ export class Text extends RenderItem { this.text = text; this.calBox(); if (this.parent) this.update(this); + this.emit('setText', text); } /** @@ -120,10 +123,10 @@ export class Text extends RenderItem { 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; diff --git a/src/core/render/renderer/index.ts b/src/core/render/renderer/index.ts index 4f3ec9d..56a8d28 100644 --- a/src/core/render/renderer/index.ts +++ b/src/core/render/renderer/index.ts @@ -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({ patchProp: function ( @@ -48,7 +48,7 @@ export const { createApp, render } = createRenderer({ return onCreate(namespace, isCustomizedBuiltIn, vnodeProps); }, - createText: function (text: string): RenderItem { + createText: function (text: string): RenderItem { if (!/^\s*$/.test(text)) logger.warn(38); return new Text(text); }, diff --git a/src/core/render/renderer/map.ts b/src/core/render/renderer/map.ts index b3b76b2..f85689f 100644 --- a/src/core/render/renderer/map.ts +++ b/src/core/render/renderer/map.ts @@ -4,7 +4,14 @@ import { ElementNamespace, VNodeProps } from 'vue'; import { Container } from '../container'; import { MotaRenderer } from '../render'; import { Sprite } from '../sprite'; -import { Comment, Icon, Image, Text, Winskin } from '../preset/misc'; +import { + Comment, + ETextEvent, + Icon, + Image, + Text, + Winskin +} from '../preset/misc'; import { Shader } from '../shader'; import { Animate, Damage, EDamageEvent, Layer, LayerGroup } from '../preset'; import { @@ -90,6 +97,40 @@ const standardElementNoCache = ( }; }; +const enum ElementState { + None = 0, + Cache = 1, + Fall = 2 +} + +/** + * standardElementFor + */ +const se = ( + Item: new ( + type: RenderItemPosition, + cache?: boolean, + fall?: boolean + ) => RenderItem, + position: RenderItemPosition, + state: ElementState +) => { + const defaultCache = !!(state & ElementState.Cache); + const defautFall = !!(state & ElementState.Fall); + + return (_0: any, _1: any, props?: any) => { + if (!props) return new Item('absolute'); + else { + const { + type = position, + cache = defaultCache, + fall = defautFall + } = props; + return new Item(type, cache, fall); + } + }; +}; + // Default elements tagMap.register('container', standardElement(Container)); tagMap.register('template', standardElement(Container)); @@ -97,7 +138,7 @@ tagMap.register('mota-renderer', (_0, _1, props) => { return new MotaRenderer(props?.id); }); tagMap.register('sprite', standardElement(Sprite)); -tagMap.register('text', (_0, _1, props) => { +tagMap.register('text', (_0, _1, props) => { if (!props) return new Text(); else { const { type = 'static', text = '' } = props; @@ -182,13 +223,13 @@ tagMap.register('damage', (_0, _1, props) => { tagMap.register('animation', (_0, _1, props) => { return new Animate(); }); -tagMap.register('g-rect', standardElementNoCache(Rect)); -tagMap.register('g-circle', standardElementNoCache(Circle)); -tagMap.register('g-ellipse', standardElementNoCache(Ellipse)); -tagMap.register('g-line', standardElementNoCache(Line)); -tagMap.register('g-bezier', standardElementNoCache(BezierCurve)); -tagMap.register('g-quad', standardElementNoCache(QuadraticCurve)); -tagMap.register('g-path', standardElementNoCache(Path)); +tagMap.register('g-rect', se(Rect, 'absolute', ElementState.None)); +tagMap.register('g-circle', se(Circle, 'absolute', ElementState.None)); +tagMap.register('g-ellipse', se(Ellipse, 'absolute', ElementState.None)); +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('icon', standardElementNoCache(Icon)); tagMap.register('winskin', (_0, _1, props) => { if (!props) return new Winskin(core.material.images.images['winskin.png']); diff --git a/src/data/logger.json b/src/data/logger.json index 1ebb723..ae0a740 100644 --- a/src/data/logger.json +++ b/src/data/logger.json @@ -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." } diff --git a/src/game/system.ts b/src/game/system.ts index 6edf198..7620022 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -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; diff --git a/vite.config.ts b/vite.config.ts index 5450e54..df06f72 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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/