feat: 将 HumanBreak 新内容移植入该样板

This commit is contained in:
AncTe committed 2026-02-28 22:56:59 +08:00
1 parent 6bf32174d8
commit bea2725d6a
128 files changed
+11984 -573

No files matched your search

@@ -6,7 +6,8 @@ import {
saveLoad,
openSettings,
openViewMap,
openReplay
openReplay,
openStatistics
} from './ui';
import { ElementLocator } from '@motajs/render-core';
@@ -15,6 +16,9 @@ export function createAction() {
.realize('save', () => {
saveSave(mainUIController, FULL_LOC);
})
.realize('statistics', () => {
openStatistics(mainUIController);
})
.realize('load', () => {
saveLoad(mainUIController, FULL_LOC);
})
@@ -0,0 +1,22 @@
import { state } from '@user/data-state';
import { materials } from '@user/client-base';
import { MapRenderer, MapExtensionManager } from './map';
/** 主地图渲染器,用于渲染游戏画面 */
export const mainMapRenderer = new MapRenderer(materials, state.layer);
/** 主地图渲染器拓展 */
export const mainMapExtension = new MapExtensionManager(mainMapRenderer);
/** 副地图渲染器,用于渲染缩略图、浏览地图等 */
// export const expandMapRenderer = new MapRenderer(materials, state.layer);
export async function createMainExtension() {
// 算是一种妥协吧,等之后加载系统重构之后应该会清晰很多
await materials.trackedAsset.then();
mainMapRenderer.useAsset(materials.trackedAsset);
const layer = state.layer.getLayerByAlias('event');
if (layer) {
mainMapExtension.addHero(state.hero, layer);
mainMapExtension.addDoor(layer);
}
}
@@ -33,6 +33,8 @@ import {
import { SetupComponentOptions } from '@motajs/system-ui';
import { texture } from '../elements';
// todo: TextContent 应该改成渲染元素?
//#region TextContent
export interface TextContentProps
@@ -817,7 +817,7 @@ export class TextContentParser {
return pointer;
}
const time = parseInt(param);
this.addWaitRenderable(end, time);
this.addWaitRenderable(end + 1, time);
return end;
}
@@ -830,19 +830,19 @@ export class TextContentParser {
}
if (/^\d+$/.test(param)) {
const num = Number(param);
this.addIconRenderable(end, num as AllNumbers);
this.addIconRenderable(end + 1, num as AllNumbers);
} else {
if (/^X\d+$/.test(param)) {
// 额外素材
const num = Number(param.slice(1));
this.addIconRenderable(end, num as AllNumbers);
this.addIconRenderable(end + 1, num as AllNumbers);
} else {
const num = texture.idNumberMap[param as AllIds];
if (num === void 0) {
logger.warn(59, param);
return end;
}
this.addIconRenderable(end, num);
this.addIconRenderable(end + 1, num);
}
}
return end;
@@ -960,6 +960,7 @@ export class TextContentParser {
break;
case 'n':
// 在这里预先将换行处理为多个 node,会比在分行时再处理更方便
pointer++;
this.addTextNode(pointer + 1, true);
break;
}
@@ -985,7 +986,9 @@ export class TextContentParser {
this.resolved += char;
}
this.addTextNode(text.length, false);
if (this.nodePointer < text.length) {
this.addTextNode(text.length, false);
}
return this.splitLines(width);
}
@@ -1,8 +1,6 @@
import { logger } from '@motajs/common';
import {
MotaOffscreenCanvas2D,
SizedCanvasImageSource
} from '@motajs/render-core';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
@@ -1,4 +1,5 @@
import { RenderAdapter, SizedCanvasImageSource } from '@motajs/render-core';
import { RenderAdapter } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
import { logger } from '@motajs/common';
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
import EventEmitter from 'eventemitter3';
@@ -6,6 +6,10 @@ import { createViewport } from './viewport';
import { Icon, Winskin } from './misc';
import { Animate } from './animate';
import { createItemDetail } from './itemDetail';
import { logger } from '@motajs/common';
import { MapRender, MapRenderer } from '../map';
import { state } from '@user/data-state';
import { materials } from '@user/client-base';
export function createElements() {
createCache();
@@ -66,6 +70,31 @@ export function createElements() {
return new Animate();
});
tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('map-render', (_0, _1, props) => {
if (!props) {
logger.error(42);
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
const { layerState, renderer } = props;
if (!layerState) {
logger.error(42, 'layerState');
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
if (!renderer) {
logger.error(42, 'renderer');
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
return new MapRender(layerState, renderer);
});
}
export * from './animate';
@@ -4,9 +4,9 @@ import {
RenderItem,
RenderItemPosition,
MotaOffscreenCanvas2D,
Transform,
SizedCanvasImageSource
Transform
} from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
import { isNil } from 'lodash-es';
import { RenderableData, AutotileRenderable, texture } from './cache';
import { IAnimateFrame, renderEmits } from './frame';
@@ -1,5 +1,6 @@
import { BaseProps, TagDefine } from '@motajs/render-vue';
import { CanvasStyle, Transform } from '@motajs/render-core';
import { ERenderItemEvent, Transform } from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import {
ILayerGroupRenderExtends,
FloorLayer,
@@ -10,6 +11,8 @@ import {
import { EAnimateEvent } from './animate';
import { EIconEvent, EWinskinEvent } from './misc';
import { IEnemyCollection } from '@motajs/types';
import { ILayerState } from '@user/data-state';
import { IMapRenderer } from '../map';
export interface AnimateProps extends BaseProps {}
@@ -59,6 +62,11 @@ export interface LayerProps extends BaseProps {
ex?: readonly ILayerRenderExtends[];
}
export interface MapRenderProps extends BaseProps {
layerState: ILayerState;
renderer: IMapRenderer;
}
declare module 'vue/jsx-runtime' {
namespace JSX {
export interface IntrinsicElements {
@@ -67,6 +75,7 @@ declare module 'vue/jsx-runtime' {
animation: TagDefine<AnimateProps, EAnimateEvent>;
icon: TagDefine<IconProps, EIconEvent>;
winskin: TagDefine<WinskinProps, EWinskinEvent>;
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
}
}
}
@@ -7,7 +7,7 @@ import { EffectBase } from './base';
export class Image3DEffect
extends EffectBase<void>
implements ITransformUpdatable
implements ITransformUpdatable<Transform3D>
{
/** 图片的模型变换 */
private model: Transform3D = new Transform3D();
@@ -65,7 +65,7 @@ export class Image3DEffect
* @param model 模型变换
*/
setModel(model: Transform3D) {
this.model.bind();
this.model.unbind(this);
this.model = model;
model.bind(this);
}
@@ -75,7 +75,7 @@ export class Image3DEffect
* @param model 视角变换
*/
setView(view: Transform3D) {
this.view.bind();
this.view.unbind(this);
this.view = view;
view.bind(this);
}
@@ -85,7 +85,7 @@ export class Image3DEffect
* @param model 投影变换
*/
setProj(proj: Transform3D) {
this.proj.bind();
this.proj.unbind(this);
this.proj = proj;
proj.bind(this);
}
@@ -9,21 +9,19 @@ import { createAction } from './action';
import { sceneController } from './scene';
import { GameTitleUI } from './ui/title';
import { createWeather } from './weather';
import { createMainExtension } from './commonIns';
export function createGameRenderer() {
const App = defineComponent(_props => {
return () => (
<container noanti width={MAIN_WIDTH} height={MAIN_HEIGHT}>
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
{sceneController.render()}
</container>
);
});
mainRenderer.setAntiAliasing(false);
mainRenderer.hide();
createApp(App).mount(mainRenderer);
console.log(mainRenderer);
}
export function createRender() {
@@ -32,11 +30,15 @@ export function createRender() {
createAction();
createWeather();
loading.on('loaded', () => {
loading.once('loaded', () => {
sceneController.open(GameTitleUI, {});
mainRenderer.show();
});
loading.once('assetBuilt', () => {
createMainExtension();
});
hook.on('restart', () => {
sceneController.closeAll();
sceneController.open(GameTitleUI, {});
@@ -45,6 +47,7 @@ export function createRender() {
Font.setDefaults(DEFAULT_FONT);
}
export * from './commonIns';
export * from './components';
export * from './elements';
export * from './fx';
@@ -0,0 +1,351 @@
import { clamp } from 'lodash-es';
import {
IBlockData,
IBlockIndex,
IBlockInfo,
IBlockSplitter,
IBlockSplitterConfig
} from './types';
export class BlockSplitter<T> implements IBlockSplitter<T> {
blockWidth: number = 0;
blockHeight: number = 0;
dataWidth: number = 0;
dataHeight: number = 0;
width: number = 1;
height: number = 1;
/** 分块映射 */
readonly blockMap: Map<number, IBlockData<T>> = new Map();
/** 数据宽度配置 */
private splitDataWidth: number = 0;
/** 数据高度配置 */
private splitDataHeight: number = 0;
/** 单个分块的宽度配置 */
private splitBlockWidth: number = 0;
/** 单个分块的高度配置 */
private splitBlockHeight: number = 0;
/**
* 检查坐标范围
* @param x 分块横坐标
* @param y 分块纵坐标
*/
private checkLocRange(x: number, y: number) {
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
getBlockByLoc(x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
return this.blockMap.get(index) ?? null;
}
getBlockByIndex(index: number): IBlockData<T> | null {
return this.blockMap.get(index) ?? null;
}
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
setBlockByIndex(data: T, index: number): IBlockData<T> | null {
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
*iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void> {
if (!this.checkLocRange(x, y)) return;
const index = y * this.width + x;
yield* this.iterateBlockByIndex(index);
}
*iterateBlockByIndex(index: number): Generator<IBlockIndex, void> {
const block = this.blockMap.get(index);
if (!block) return;
const startX = block.x * this.blockWidth;
const startY = block.y * this.blockHeight;
const endX = startX + block.width;
const endY = startY + block.height;
for (let ny = startY; ny < endY; ny++) {
for (let nx = startX; nx < endX; nx++) {
const index: IBlockIndex = {
x: nx,
y: ny,
dataX: nx * this.blockWidth,
dataY: ny * this.blockHeight,
index: ny * this.dataWidth + nx
};
yield index;
}
}
}
*iterateBlockByIndices(
indices: Iterable<number>
): Generator<IBlockIndex, void> {
for (const index of indices) {
yield* this.iterateBlockByIndex(index);
}
}
iterateBlocks(): Iterable<IBlockData<T>> {
return this.blockMap.values();
}
*iterateBlocksOfDataArea(
x: number,
y: number,
width: number,
height: number
): Generator<IBlockData<T>> {
const r = this.width - 1;
const b = this.height - 1;
const rx = x + width;
const by = y + height;
const left = clamp(Math.floor(x / this.blockWidth), 0, r);
const top = clamp(Math.floor(y / this.blockHeight), 0, b);
const right = clamp(Math.floor(rx / this.blockWidth), 0, r);
const bottom = clamp(Math.floor(by / this.blockHeight), 0, b);
for (let ny = left; ny <= right; ny++) {
for (let nx = top; nx <= bottom; nx++) {
const index = ny * this.width + nx;
const block = this.blockMap.get(index);
if (!block) continue;
yield block;
}
}
}
getIndexByLoc(x: number, y: number): number {
if (!this.checkLocRange(x, y)) return -1;
return y * this.width + x;
}
getLocByIndex(index: number): Loc | null {
if (index >= this.width * this.height) return null;
return {
x: index % this.width,
y: Math.floor(index / this.width)
};
}
getIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
const res: number[] = [];
for (const { x, y } of list) {
res.push(this.getIndexByLoc(x, y));
}
return res;
}
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null> {
const res: (Loc | null)[] = [];
for (const index of list) {
res.push(this.getLocByIndex(index));
}
return res;
}
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) return null;
const index = by * this.width + bx;
return this.blockMap.get(index) ?? null;
}
getBlockByDataIndex(index: number): IBlockData<T> | null {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
return this.getBlockByDataLoc(x, y);
}
getIndicesByDataLocList(list: Iterable<Loc>): Set<number> {
const res = new Set<number>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getIndicesByDataIndices(list: Iterable<number>): Set<number> {
const res = new Set<number>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const index = bx + by * this.width;
const data = this.blockMap.get(index);
if (data) res.add(data);
}
return res;
}
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const blockIndex = bx + by * this.width;
const data = this.blockMap.get(blockIndex);
if (data) res.add(data);
}
return res;
}
configSplitter(config: IBlockSplitterConfig): void {
this.splitDataWidth = config.dataWidth;
this.splitDataHeight = config.dataHeight;
this.splitBlockWidth = config.blockWidth;
this.splitBlockHeight = config.blockHeight;
}
private mapBlock(
x: number,
y: number,
realWidth: number,
width: number,
height: number,
fn: (block: IBlockInfo) => T
) {
const index = y * realWidth + x;
const block: IBlockInfo = {
index,
x,
y,
dataX: x * this.blockWidth,
dataY: y * this.blockHeight,
width,
height
};
const data = fn(block);
const blockData = new SplittedBlockData(this, block, data);
this.blockMap.set(index, blockData);
}
splitBlocks(mapFn: (block: IBlockInfo) => T): void {
this.blockMap.clear();
this.blockWidth = this.splitBlockWidth;
this.blockHeight = this.splitBlockHeight;
this.dataWidth = this.splitDataWidth;
this.dataHeight = this.splitDataHeight;
const restX = this.splitDataWidth % this.splitBlockWidth;
const restY = this.splitDataHeight % this.splitBlockHeight;
const width = Math.floor(this.splitDataWidth / this.splitBlockWidth);
const height = Math.floor(this.splitDataHeight / this.splitBlockHeight);
const hasXRest = restX > 0;
const hasYRest = restY > 0;
const realWidth = hasXRest ? width + 1 : width;
const bw = this.blockWidth;
const bh = this.blockHeight;
this.width = realWidth;
this.height = hasYRest ? height + 1 : height;
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, ny, realWidth, bw, bh, mapFn);
}
}
if (hasXRest) {
for (let ny = 0; ny < height; ny++) {
this.mapBlock(width, ny, realWidth, restX, bh, mapFn);
}
}
if (hasYRest) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, height, realWidth, bw, restY, mapFn);
}
}
if (hasXRest && hasYRest) {
this.mapBlock(width, height, realWidth, restX, restY, mapFn);
}
}
}
class SplittedBlockData<T> implements IBlockData<T> {
width: number;
height: number;
x: number;
y: number;
dataX: number;
dataY: number;
index: number;
data: T;
constructor(
readonly splitter: BlockSplitter<T>,
info: IBlockInfo,
data: T
) {
this.width = info.width;
this.height = info.height;
this.x = info.x;
this.y = info.y;
this.dataX = info.dataX;
this.dataY = info.dataY;
this.index = info.index;
this.data = data;
}
left(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y);
}
right(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y);
}
up(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y - 1);
}
down(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y + 1);
}
leftUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y - 1);
}
leftDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y + 1);
}
rightUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y - 1);
}
rightDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y + 1);
}
next(): IBlockData<T> | null {
return this.splitter.getBlockByIndex(this.index + 1);
}
}
@@ -0,0 +1,2 @@
/** 单个图块的实例化数据数量 */
export const INSTANCED_COUNT = 4 + 4 + 4 + 4;
@@ -0,0 +1,70 @@
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
import { ILayerState } from '@user/data-state';
import { IMapRenderer } from './types';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
export class MapRender extends RenderItem {
/**
* @param layerState 地图状态对象
* @param renderer 地图渲染器对象
*/
constructor(
readonly layerState: ILayerState,
readonly renderer: IMapRenderer
) {
super('static', false, false);
renderer.setLayerState(layerState);
renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT);
renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT);
this.delegateTicker(time => {
this.renderer.tick(time);
if (this.renderer.needUpdate()) {
this.update();
}
});
}
private sizeGL(width: number, height: number) {
const ratio = this.highResolution ? devicePixelRatio : 1;
const scale = ratio * this.scale;
const w = width * scale;
const h = height * scale;
this.renderer.setCanvasSize(w, h);
this.renderer.setViewport(0, 0, w, h);
}
onResize(scale: number): void {
super.onResize(scale);
this.sizeGL(this.width, this.height);
}
size(width: number, height: number): void {
super.size(width, height);
this.sizeGL(width, height);
}
protected render(canvas: MotaOffscreenCanvas2D): void {
this.renderer.clear(true, true);
const map = this.renderer.render();
canvas.ctx.drawImage(map, 0, 0, canvas.width, canvas.height);
}
patchProp(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) {
case 'layerState': {
this.renderer.setLayerState(nextValue);
break;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
}
}
@@ -0,0 +1,73 @@
import {
IMapLayer,
IMapLayerHookController,
IMapLayerHooks
} from '@user/data-state';
import { IMapDoorRenderer } from './types';
import { IMapRenderer } from '../types';
import { sleep } from 'mutate-animate';
import { DOOR_ANIMATE_INTERVAL } from '../../shared';
export class MapDoorRenderer implements IMapDoorRenderer {
/** 钩子控制器 */
readonly controller: IMapLayerHookController;
/** 动画间隔 */
private interval: number = DOOR_ANIMATE_INTERVAL;
constructor(
readonly renderer: IMapRenderer,
readonly layer: IMapLayer
) {
this.controller = layer.addHook(new MapDoorHook(this));
this.controller.load();
}
setAnimateInterval(interval: number): void {
this.interval = interval;
}
async openDoor(x: number, y: number): Promise<void> {
const status = this.renderer.getBlockStatus(this.layer, x, y);
if (!status) return;
const array = this.layer.getMapRef().array;
const index = y * this.layer.width + x;
const num = array[index];
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const frames = data.frames;
for (let i = 0; i < frames; i++) {
status.useSpecifiedFrame(i);
await sleep(this.interval);
}
}
async closeDoor(num: number, x: number, y: number): Promise<void> {
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const moving = this.renderer.addMovingBlock(this.layer, num, x, y);
const frames = data.frames;
for (let i = frames - 1; i >= 0; i--) {
moving.useSpecifiedFrame(i);
await sleep(this.interval);
}
moving.destroy();
}
destroy(): void {
this.controller.unload();
}
}
class MapDoorHook implements Partial<IMapLayerHooks> {
constructor(readonly renderer: MapDoorRenderer) {}
onOpenDoor(x: number, y: number): Promise<void> {
return this.renderer.openDoor(x, y);
}
onCloseDoor(num: number, x: number, y: number): Promise<void> {
return this.renderer.closeDoor(num, x, y);
}
}
@@ -0,0 +1,518 @@
import {
degradeFace,
FaceDirection,
getFaceMovement,
HeroAnimateDirection,
IHeroState,
IHeroStateHooks,
IMapLayer,
nextFaceDirection,
state
} from '@user/data-state';
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
import { isNil } from 'lodash-es';
import { IHookController, logger } from '@motajs/common';
import { BlockCls, IMaterialFramedData } from '@user/client-base';
import {
ITexture,
ITextureSplitter,
TextureRowSplitter
} from '@motajs/render-assets';
import { IMapHeroRenderer } from './types';
import { TimingFn } from 'mutate-animate';
/** 默认的移动时长 */
const DEFAULT_TIME = 100;
interface HeroRenderEntity {
/** 移动图块对象 */
readonly block: IMovingBlock;
/** 标识符,用于判定跟随者 */
readonly identifier: string;
/** 目标横坐标,移动时有效 */
targetX: number;
/** 目标纵坐标,移动时有效 */
targetY: number;
/** 当前的移动朝向 */
direction: FaceDirection;
/** 下一个跟随者的移动方向 */
nextDirection: FaceDirection;
/** 当前是否正在移动 */
moving: boolean;
/** 当前是否正在动画,移动跟动画要分开,有的操作比如跳跃就是在移动中但是没动画 */
animating: boolean;
/** 帧动画间隔 */
animateInterval: number;
/** 当一次动画时刻 */
lastAnimateTime: number;
/** 当前的动画帧数 */
animateFrame: number;
/** 移动的 `Promise`,移动完成时兑现,如果停止,则一直是兑现状态 */
promise: Promise<void>;
/** 勇士移动的动画方向 */
animateDirection: HeroAnimateDirection;
}
export class MapHeroRenderer implements IMapHeroRenderer {
private static readonly splitter: ITextureSplitter<number> =
new TextureRowSplitter();
/** 勇士钩子 */
readonly controller: IHookController<IHeroStateHooks>;
/** 勇士每个朝向的贴图对象 */
readonly textureMap: Map<FaceDirection, IMaterialFramedData> = new Map();
/** 勇士渲染实体,与 `entities[0]` 同引用 */
readonly heroEntity: HeroRenderEntity;
/**
* 渲染实体,索引 0 表示勇士,后续索引依次表示跟随的跟随者。
* 整体是一个状态机,而且下一个跟随者只与上一个跟随者有关,下一个跟随者移动的方向就是上一个跟随者上一步移动后指向的方向。
*/
readonly entities: HeroRenderEntity[] = [];
/** 每帧执行的帧动画对象 */
readonly ticker: IMapRendererTicker;
constructor(
readonly renderer: IMapRenderer,
readonly layer: IMapLayer,
readonly hero: IHeroState
) {
this.controller = hero.addHook(new MapHeroHook(this));
this.controller.load();
const moving = this.addHeroMoving(renderer, layer, hero);
const heroEntity: HeroRenderEntity = {
block: moving,
identifier: '',
targetX: hero.x,
targetY: hero.y,
direction: hero.direction,
nextDirection: FaceDirection.Unknown,
moving: false,
animating: false,
animateInterval: 0,
lastAnimateTime: 0,
animateFrame: 0,
promise: Promise.resolve(),
animateDirection: HeroAnimateDirection.Forward
};
this.heroEntity = heroEntity;
this.entities.push(heroEntity);
this.ticker = renderer.requestTicker(time => this.tick(time));
}
/**
* 添加勇士对应的移动图块
* @param renderer 渲染器
* @param layer 图块所属图层
* @param hero 勇士状态对象
*/
private addHeroMoving(
renderer: IMapRenderer,
layer: IMapLayer,
hero: IHeroState
) {
if (isNil(hero.image)) {
logger.warn(88);
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
const image = this.renderer.manager.getImageByAlias(hero.image);
if (!image) {
logger.warn(89, hero.image);
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
this.updateHeroTexture(image);
const tex = this.textureMap.get(degradeFace(hero.direction));
if (!tex) {
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
const block = renderer.addMovingBlock(layer, tex, hero.x, hero.y);
block.useSpecifiedFrame(0);
return block;
}
/**
* 更新勇士贴图
* @param image 勇士使用的贴图,包含四个方向
*/
private updateHeroTexture(image: ITexture) {
const textures = [
...image.split(MapHeroRenderer.splitter, image.height / 4)
];
if (textures.length !== 4) {
logger.warn(90, hero.image);
return;
}
const faceList = [
FaceDirection.Down,
FaceDirection.Left,
FaceDirection.Right,
FaceDirection.Up
];
faceList.forEach((v, i) => {
const dirImage = textures[i];
const data: IMaterialFramedData = {
offset: dirImage.width / 4,
texture: dirImage,
cls: BlockCls.Unknown,
frames: 4,
defaultFrame: 0
};
this.textureMap.set(v, data);
});
}
private tick(time: number) {
this.entities.forEach(v => {
if (v.animating) {
const dt = time - v.lastAnimateTime;
if (dt > v.animateInterval) {
if (v.animateDirection === HeroAnimateDirection.Forward) {
v.animateFrame++;
} else {
v.animateFrame--;
if (v.animateFrame < 0) {
// 小于 0,则加上帧数的整数倍,就写个 10000 倍吧
v.animateFrame += v.block.texture.frames * 10000;
}
}
v.lastAnimateTime = time;
v.block.useSpecifiedFrame(v.animateFrame);
}
} else {
if (v.animateFrame !== 0) {
v.animateFrame = 0;
v.block.useSpecifiedFrame(0);
}
}
});
}
setImage(image: ITexture): void {
this.updateHeroTexture(image);
const tex = this.textureMap.get(degradeFace(this.hero.direction));
if (!tex) return;
this.heroEntity.block.setTexture(tex);
}
setAlpha(alpha: number): void {
this.heroEntity.block.setAlpha(alpha);
}
setPosition(x: number, y: number): void {
this.entities.forEach(v => {
v.block.setPos(x, y);
v.nextDirection = FaceDirection.Unknown;
});
}
/**
* 移动指定渲染实体,不会影响其他渲染实体。多次调用时会按顺序依次移动
* @param entity 渲染实体
* @param direction 移动方向
* @param time 移动时长
*/
private moveEntity(
entity: HeroRenderEntity,
direction: FaceDirection,
time: number
) {
const { x: dx, y: dy } = getFaceMovement(direction);
if (dx === 0 && dy === 0) return;
const block = entity.block;
const tx = block.x + dx;
const ty = block.y + dy;
const nextTile = state.roleFace.getFaceOf(block.tile, direction);
const nextTex = this.renderer.manager.getIfBigImage(
nextTile?.identifier ?? block.tile
);
entity.animateInterval = time;
entity.promise = entity.promise.then(async () => {
entity.moving = true;
entity.animating = true;
entity.direction = direction;
if (nextTex) block.setTexture(nextTex);
await block.lineTo(tx, ty, time);
entity.nextDirection = entity.direction;
});
}
/**
* 生成跳跃曲线
* @param dx 横向偏移量
* @param dy 纵向偏移量
*/
private generateJumpFn(dx: number, dy: number): TimingFn<2> {
const distance = Math.hypot(dx, dy);
const peak = 3 + distance;
return (progress: number) => {
const x = dx * progress;
const y = progress * dy + (progress ** 2 - progress) * peak;
return [x, y];
};
}
/**
* 将指定渲染实体跳跃至目标点,多次调用时会按顺序依次执行,可以与 `moveEntity` 混用
* @param entity 渲染实体
* @param x 目标横坐标
* @param y 目标纵坐标
* @param time 跳跃时长
*/
private jumpEntity(
entity: HeroRenderEntity,
x: number,
y: number,
time: number
) {
const block = entity.block;
entity.promise = entity.promise.then(async () => {
const dx = block.x - x;
const dy = block.y - y;
const fn = this.generateJumpFn(dx, dy);
entity.moving = true;
entity.animating = false;
entity.animateFrame = 0;
await block.moveRelative(fn, time);
});
}
startMove(): void {
this.heroEntity.moving = true;
this.heroEntity.animating = true;
this.heroEntity.lastAnimateTime = this.ticker.timestamp;
}
private endEntityMoving(entity: HeroRenderEntity) {
entity.moving = false;
entity.animating = false;
entity.animateFrame = 0;
entity.block.useSpecifiedFrame(0);
}
async waitMoveEnd(): Promise<void> {
await Promise.all(this.entities.map(v => v.promise));
this.entities.forEach(v => this.endEntityMoving(v));
}
stopMove(): void {
this.entities.forEach(v => {
v.block.endMoving();
this.endEntityMoving(v);
});
}
async move(direction: FaceDirection, time: number): Promise<void> {
this.moveEntity(this.heroEntity, direction, time);
for (let i = 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
this.moveEntity(this.entities[i], last.nextDirection, time);
}
await Promise.all(this.entities.map(v => v.promise));
}
async jumpTo(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void> {
// 首先要把所有的跟随者移动到勇士所在位置
for (let i = 1; i < this.entities.length; i++) {
// 对于每一个跟随者,需要向前遍历每一个跟随者,然后朝向移动,这样就可以聚集在一起了
const now = this.entities[i];
for (let j = i - 1; j >= 0; j--) {
const last = this.entities[j];
this.moveEntity(now, last.nextDirection, DEFAULT_TIME);
}
}
this.entities.forEach(v => {
this.jumpEntity(v, x, y, time);
});
if (waitFollower) {
await Promise.all(this.entities.map(v => v.promise));
} else {
return this.heroEntity.promise;
}
}
addFollower(image: number, id: string): void {
const last = this.entities[this.entities.length - 1];
if (last.moving) {
logger.warn(92);
return;
}
const nowFace = degradeFace(last.nextDirection, FaceDirection.Down);
const faced = state.roleFace.getFaceOf(image, nowFace);
const tex = this.renderer.manager.getIfBigImage(faced?.face ?? image);
if (!tex) {
logger.warn(91, image.toString());
return;
}
const { x: dxn, y: dyn } = getFaceMovement(last.nextDirection);
const { x: dx, y: dy } = getFaceMovement(last.direction);
const x = last.block.x - dxn;
const y = last.block.y - dyn;
const moving = this.renderer.addMovingBlock(this.layer, tex, x, y);
const entity: HeroRenderEntity = {
block: moving,
identifier: id,
targetX: last.targetX - dx,
targetY: last.targetY - dy,
direction: nowFace,
nextDirection: FaceDirection.Unknown,
moving: false,
animating: false,
animateInterval: 0,
lastAnimateTime: 0,
animateFrame: 0,
promise: Promise.resolve(),
animateDirection: HeroAnimateDirection.Forward
};
moving.useSpecifiedFrame(0);
this.entities.push(entity);
}
async removeFollower(follower: string, animate: boolean): Promise<void> {
const index = this.entities.findIndex(v => v.identifier === follower);
if (index === -1) return;
if (this.entities[index].moving) {
logger.warn(93);
return;
}
if (index === this.entities.length - 1) {
this.entities[index].block.destroy();
this.entities.splice(index, 1);
return;
}
// 展示动画
if (animate) {
for (let i = index + 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
const moving = this.entities[i];
this.moveEntity(moving, last.nextDirection, DEFAULT_TIME);
}
this.entities[index].block.destroy();
this.entities.splice(index, 1);
await Promise.all(this.entities.map(v => v.promise));
return;
}
// 不展示动画
for (let i = index + 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
const moving = this.entities[i];
moving.block.setPos(last.block.x, last.block.y);
const nextFace = state.roleFace.getFaceOf(
moving.block.tile,
last.nextDirection
);
if (!nextFace) continue;
const tile = this.renderer.manager.getIfBigImage(
nextFace.identifier
);
if (!tile) continue;
moving.block.setTexture(tile);
moving.direction = last.nextDirection;
moving.nextDirection = moving.direction;
}
this.entities[index].block.destroy();
this.entities.splice(index, 1);
}
removeAllFollowers(): void {
for (let i = 1; i < this.entities.length; i++) {
this.entities[i].block.destroy();
}
this.entities.length = 1;
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.entities.find(v => v.identifier === identifier);
if (!follower) return;
follower.block.setAlpha(alpha);
}
setHeroAnimateDirection(direction: HeroAnimateDirection): void {
this.heroEntity.animateDirection = direction;
}
turn(direction?: FaceDirection): void {
const next = isNil(direction)
? nextFaceDirection(this.heroEntity.direction)
: direction;
const tex = this.textureMap.get(next);
if (tex) {
this.heroEntity.block.setTexture(tex);
this.heroEntity.direction = next;
}
}
destroy() {
this.controller.unload();
}
}
class MapHeroHook implements Partial<IHeroStateHooks> {
constructor(readonly hero: MapHeroRenderer) {}
onSetImage(image: ImageIds): void {
const texture = this.hero.renderer.manager.getImageByAlias(image);
if (!texture) {
logger.warn(89, hero.image);
return;
}
this.hero.setImage(texture);
}
onSetPosition(x: number, y: number): void {
this.hero.setPosition(x, y);
}
onTurnHero(direction: FaceDirection): void {
this.hero.turn(direction);
}
onStartMove(): void {
this.hero.startMove();
}
onMoveHero(direction: FaceDirection, time: number): Promise<void> {
return this.hero.move(direction, time);
}
onEndMove(): Promise<void> {
return this.hero.waitMoveEnd();
}
onJumpHero(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void> {
return this.hero.jumpTo(x, y, time, waitFollower);
}
onSetAlpha(alpha: number): void {
this.hero.setAlpha(alpha);
}
onAddFollower(follower: number, identifier: string): void {
this.hero.addFollower(follower, identifier);
}
onRemoveFollower(identifier: string, animate: boolean): void {
this.hero.removeFollower(identifier, animate);
}
onRemoveAllFollowers(): void {
this.hero.removeAllFollowers();
}
onSetFollowerAlpha(identifier: string, alpha: number): void {
this.hero.setFollowerAlpha(identifier, alpha);
}
}
@@ -0,0 +1,3 @@
export * from './hero';
export * from './manager';
export * from './types';
@@ -0,0 +1,60 @@
import { IHeroState, IMapLayer } from '@user/data-state';
import {
IMapDoorRenderer,
IMapExtensionManager,
IMapHeroRenderer
} from './types';
import { IMapRenderer } from '../types';
import { MapHeroRenderer } from './hero';
import { logger } from '@motajs/common';
import { MapDoorRenderer } from './door';
export class MapExtensionManager implements IMapExtensionManager {
/** 勇士状态至勇士渲染器的映射 */
readonly heroMap: Map<IHeroState, IMapHeroRenderer> = new Map();
/** 地图图层到门渲染器的映射 */
readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map();
constructor(readonly renderer: IMapRenderer) {}
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null {
if (this.heroMap.has(state)) {
logger.error(45, 'hero renderer');
return null;
}
const heroRenderer = new MapHeroRenderer(this.renderer, layer, state);
this.heroMap.set(state, heroRenderer);
return heroRenderer;
}
removeHero(state: IHeroState): void {
const renderer = this.heroMap.get(state);
if (!renderer) return;
renderer.destroy();
this.heroMap.delete(state);
}
addDoor(layer: IMapLayer): IMapDoorRenderer | null {
if (this.doorMap.has(layer)) {
logger.error(45, 'door renderer');
return null;
}
const doorRenderer = new MapDoorRenderer(this.renderer, layer);
this.doorMap.set(layer, doorRenderer);
return doorRenderer;
}
removeDoor(layer: IMapLayer): void {
const renderer = this.doorMap.get(layer);
if (!renderer) return;
renderer.destroy();
this.doorMap.delete(layer);
}
destroy(): void {
this.heroMap.forEach(v => void v.destroy());
this.doorMap.forEach(v => void v.destroy());
this.heroMap.clear();
this.doorMap.clear();
}
}
@@ -0,0 +1,63 @@
import { IMapRenderer } from '../types';
import { IMapTextArea, IMapTextRenderable, IOnMapTextRenderer } from './types';
export class OnMapTextRenderer implements IOnMapTextRenderer {
/** 画布元素 */
readonly canvas: HTMLCanvasElement;
/** 画布 Canvas2D 上下文 */
readonly ctx: CanvasRenderingContext2D;
/** 图块索引到图块文本对象的映射 */
readonly areaMap: Map<number, MapTextArea> = new Map();
private dirty: boolean = false;
constructor(readonly renderer: IMapRenderer) {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
}
render(): HTMLCanvasElement {
return this.canvas;
}
requireBlockArea(x: number, y: number): Readonly<IMapTextArea> {
const index = y * this.renderer.mapWidth + x;
const exist = this.areaMap.get(index);
if (exist) return exist;
const area = new MapTextArea(this, x, y);
this.areaMap.set(index, area);
return area;
}
needUpdate(): boolean {
return this.dirty;
}
clear(): void {}
destroy(): void {}
}
class MapTextArea implements IMapTextArea {
index: number;
constructor(
readonly renderer: OnMapTextRenderer,
public mapX: number,
public mapY: number
) {
this.index = mapY * renderer.renderer.mapWidth + mapX;
}
addTextRenderable(renderable: IMapTextRenderable): void {
throw new Error('Method not implemented.');
}
removeTextRenderable(renderable: IMapTextRenderable): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
}
@@ -0,0 +1,246 @@
import { ITexture } from '@motajs/render-assets';
import {
FaceDirection,
HeroAnimateDirection,
IHeroState,
IMapLayer
} from '@user/data-state';
import { Font } from '@motajs/render-style';
export interface IMapExtensionManager {
/**
* 添加勇士渲染拓展
* @param state 勇士状态
* @param layer 勇士所在图层
*/
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null;
/**
* 移除勇士渲染拓展
* @param state 勇士状态
*/
removeHero(state: IHeroState): void;
/**
* 添加开门动画拓展
*/
addDoor(layer: IMapLayer): IMapDoorRenderer | null;
/**
* 移除开门动画拓展
*/
removeDoor(layer: IMapLayer): void;
/**
* 摧毁这个拓展管理对象,释放相关资源
*/
destroy(): void;
}
export interface IMapHeroRenderer {
/**
* 设置勇士图片
* @param image 勇士使用的图片
*/
setImage(image: ITexture): void;
/**
* 添加跟随者
* @param image 跟随者图块数字
* @param id 跟随者的 id,用于删除操作
*/
addFollower(image: number, id: string): void;
/**
* 取消跟随者
* @param follower 跟随者的 id
* @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格
*/
removeFollower(follower: string, animate: boolean): Promise<void>;
/**
* 移除所有跟随者
*/
removeAllFollowers(): void;
/**
* 设置勇士位置
*/
setPosition(x: number, y: number): void;
/**
* 开始移动,在移动前需要调用此方法切换勇士状态
*/
startMove(): void;
/**
* 等待勇士移动停止后,将移动状态切换为停止
* @param waitFollower 是否也等待跟随者移动结束
*/
waitMoveEnd(waitFollower: boolean): Promise<void>;
/**
* 立刻停止移动,勇士和跟随者瞬移到目标点
*/
stopMove(): void;
/**
* 勇士朝某个方向移动
* @param direction 移动方向
*/
move(direction: FaceDirection, time: number): Promise<void>;
/**
* 跳跃勇士至目标点
* @param x 目标点横坐标
* @param y 目标点纵坐标
* @param time 跳跃时长
* @param waitFollower 是否等待跟随者也跳跃完毕
*/
jumpTo(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void>;
/**
* 设置勇士不透明度
* @param alpha 不透明度
*/
setAlpha(alpha: number): void;
/**
* 设置跟随者的不透明度
* @param identifier 跟随者标识符
* @param alpha 跟随者不透明度
*/
setFollowerAlpha(identifier: string, alpha: number): void;
/**
* 设置勇士移动的动画播放方向,一般后退会使用反向播放的动画,前进使用正向播放的动画
* @param direction 动画方向
*/
setHeroAnimateDirection(direction: HeroAnimateDirection): void;
/**
* 设置勇士朝向
* @param direction 勇士朝向,不填表示顺时针旋转
*/
turn(direction?: FaceDirection): void;
/**
* 摧毁这个勇士渲染拓展,释放相关资源
*/
destroy(): void;
}
export interface IMapDoorRenderer {
/**
* 开启指定位置的门,播放开门动画
* @param x 门横坐标
* @param y 门纵坐标
*/
openDoor(x: number, y: number): Promise<void>;
/**
* 在指定位置执行关门动画
* @param num 门图块数字
* @param x 门横坐标
* @param y 门纵坐标
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
/**
* 设置开关门动画两帧之间的间隔
* @param interval 开门动画间隔
*/
setAnimateInterval(interval: number): void;
/**
* 摧毁这个门动画拓展,释放相关资源
*/
destroy(): void;
}
export interface IMapTextRenderable {
/** 文本内容 */
readonly text: string;
/** 文本字体 */
readonly font: Font;
/** 文本填充样式 */
readonly fillStyle: CanvasStyle;
/** 文本描边样式 */
readonly strokeStyle: CanvasStyle;
/** 文本横坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
readonly px: number;
/** 文本纵坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
readonly py: number;
/** 文本横向对齐方式 */
readonly textAlign: CanvasTextAlign;
/** 文本纵向对齐方式 */
readonly textBaseline: CanvasTextBaseline;
}
export interface IMapTextRequested {
/**
* 申请更新指定的图块
* @param blocks 需要更新数据的图块列表
*/
requestBlocks(blocks: IMapTextArea[]): void;
}
export interface IMapTextArea {
/** 图块在地图上的索引 */
index: number;
/** 图块横坐标 */
mapX: number;
/** 图块纵坐标 */
mapY: number;
/**
* 添加文字可渲染对象。可渲染对象的坐标相对于图块,而非地图。
* @param renderable 可渲染对象
*/
addTextRenderable(renderable: IMapTextRenderable): void;
/**
* 移除指定的文字可渲染对象
* @param renderable 可渲染对象
*/
removeTextRenderable(renderable: IMapTextRenderable): void;
/**
* 清除本图块的所有文字可渲染对象
*/
clear(): void;
}
export interface IOnMapTextRenderer {
/**
* 渲染地图文字,返回的画布就是文字画到的画布
*/
render(): HTMLCanvasElement;
/**
* 申请指定图块坐标的文字管理对象
* @param x 图块横坐标
* @param y 图块纵坐标
*/
requireBlockArea(x: number, y: number): Readonly<IMapTextArea>;
/**
* 判断是否需要更新
*/
needUpdate(): boolean;
/**
* 清空所有文字内容
*/
clear(): void;
/**
* 摧毁这个文字渲染对象,释放相关资源
*/
destroy(): void;
}
@@ -0,0 +1,11 @@
export * from './extension';
export * from './block';
export * from './constant';
export * from './element';
export * from './moving';
export * from './renderer';
export * from './status';
export * from './types';
export * from './vertex';
export * from './viewport';
@@ -0,0 +1,268 @@
import { linear, TimingFn } from 'mutate-animate';
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
import { logger } from '@motajs/common';
import { IMapLayer } from '@user/data-state';
import { DynamicBlockStatus } from './status';
export interface IMovingRenderer {
/** 素材管理器 */
readonly manager: IMaterialManager;
/** 顶点数组生成器 */
readonly vertex: IMapVertexGenerator;
/**
* 获得当前时间戳
*/
getTimestamp(): number;
/**
* 删除指定的移动图块对象
* @param block 移动图块对象
*/
deleteMoving(block: IMovingBlock): void;
}
export class MovingBlock extends DynamicBlockStatus implements IMovingBlock {
readonly tile: number;
readonly renderer: IMovingRenderer;
readonly layer: IMapLayer;
texture: IMaterialFramedData;
index: number;
x: number = 0;
y: number = 0;
/** 当前动画开始的时刻 */
private startTime: number = 0;
/** 是否是直线动画 */
private line: boolean = false;
/** 是否是相对模式 */
private relative: boolean = false;
/** 目标横坐标 */
private targetX: number = 0;
/** 目标纵坐标 */
private targetY: number = 0;
/** 直线移动的横坐标增量 */
private dx: number = 0;
/** 直线移动的纵坐标增量 */
private dy: number = 0;
/** 动画时长 */
private time: number = 0;
/** 速率曲线 */
private timing: TimingFn = () => 0;
/** 移动轨迹曲线 */
private curve: TimingFn<2> = () => [0, 0];
/** 动画开始时横坐标 */
private startX: number = 0;
/** 动画开始时纵坐标 */
private startY: number = 0;
/** 当前动画是否已经结束 */
private end: boolean = true;
/** 是否通过 `setPos` 设置了位置 */
private posUpdated: boolean = false;
/** 兑现函数 */
private promiseFunc: () => void = () => {};
constructor(
renderer: IMovingRenderer & IMapRenderer,
index: number,
layer: IMapLayer,
block: number | IMaterialFramedData
) {
super(layer, renderer.vertex, index);
this.renderer = renderer;
this.index = index;
this.layer = layer;
if (typeof block === 'number') {
this.texture = renderer.manager.getTile(block)!;
this.tile = block;
} else {
if (!renderer.manager.assetContainsTexture(block.texture)) {
logger.error(34);
}
if (renderer.getOffsetIndex(block.offset) === -1) {
logger.error(41);
}
this.texture = block;
this.tile = -1;
}
}
setPos(x: number, y: number): void {
if (!this.end) return;
this.x = x;
this.y = y;
this.posUpdated = true;
}
setTexture(texture: IMaterialFramedData): void {
if (texture === this.texture) return;
this.texture = texture;
this.renderer.vertex.updateMoving(this, true);
}
lineTo(
x: number,
y: number,
time: number,
timing?: TimingFn
): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.startX = this.x;
this.startY = this.y;
this.targetX = x;
this.targetY = y;
this.dx = x - this.x;
this.dy = y - this.y;
this.time = time;
this.relative = false;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
this.x = x;
this.y = y;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.line = true;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = false;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx;
this.y = ty;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveRelative(
curve: TimingFn<2>,
time: number,
timing?: TimingFn
): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = true;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx + this.startX;
this.y = ty + this.startY;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
stepMoving(timestamp: number): boolean {
if (this.end) {
if (this.posUpdated) {
this.posUpdated = false;
return true;
}
return false;
}
const dt = timestamp - this.startTime;
if (this.line) {
if (dt > this.time) {
this.x = this.targetX;
this.y = this.targetY;
this.end = true;
this.promiseFunc();
return true;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
this.x = this.startX + progress * this.dx;
this.y = this.startY + progress * this.dy;
}
} else {
if (dt > this.time) {
const [tx, ty] = this.curve(1);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
this.end = true;
this.promiseFunc();
return true;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
const [tx, ty] = this.curve(progress);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
}
}
return true;
}
endMoving(): void {
this.end = true;
if (this.line) {
this.x = this.targetX;
this.y = this.targetY;
} else {
const [x, y] = this.curve(1);
if (this.relative) {
this.x = x + this.startX;
this.y = y + this.startY;
} else {
this.x = x;
this.y = y;
}
}
this.promiseFunc();
this.posUpdated = true;
}
useDefaultFrame(): void {
if (!this.renderer.manager.getTile(this.tile)) return;
const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
this.useSpecifiedFrame(defaultFrame);
}
destroy(): void {
this.renderer.deleteMoving(this);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,12 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
outColor = texture(u_sampler, v_texCoord);
}
@@ -0,0 +1,16 @@
#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec3 v_texCoord;
uniform float u_nowFrame;
uniform mat3 u_transform;
void main() {
vec3 transformed = u_transform * vec3(a_position, 1.0);
v_texCoord = vec3(a_texCoord, u_nowFrame);
gl_Position = vec4(transformed.xy, 0.95, 1.0);
}
@@ -0,0 +1,17 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec4 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
float alpha = texColor.a * v_texCoord.a;
// todo: 透明像素应该如何解决??
if (alpha < 0.1) discard;
outColor = vec4(texColor.rgb, alpha);
}
@@ -0,0 +1,36 @@
#version 300 es
precision highp float;
precision mediump int;
// 顶点坐标
layout(location = 0) in vec4 a_position;
// 实例化数据
// 图块坐标
layout(location = 1) in vec4 a_tilePos;
// 贴图坐标
layout(location = 2) in vec4 a_texCoord;
// x: 纵深,y: 不透明度
layout(location = 3) in vec4 a_tileData;
// x: 当前帧数,负数表示使用 u_nowFrame,y: 最大帧数,z: 偏移池索引,w: 纹理数组索引
layout(location = 4) in vec4 a_texData;
// x,y,z: 纹理坐标,w: 不透明度
out vec4 v_texCoord;
uniform float u_offsetPool[$1];
uniform float u_nowFrame;
uniform mat3 u_transform;
void main() {
// 坐标
vec2 pos = a_position.xy * a_tilePos.zw + a_tilePos.xy;
vec2 texCoord = a_position.zw * a_texCoord.zw + a_texCoord.xy;
// 偏移量
float offset = mod(a_texData.x < 0.0 ? u_nowFrame : a_texData.x, a_texData.y);
int offsetIndex = int(a_texData.z);
// 贴图偏移
texCoord.x += u_offsetPool[offsetIndex] * offset;
v_texCoord = vec4(texCoord.xy, a_texData.w, a_tileData.y);
vec3 transformed = u_transform * vec3(pos.xy, 1.0);
gl_Position = vec4(transformed.xy, a_tileData.x, 1.0);
}
@@ -0,0 +1,70 @@
import { IMapLayer } from '@user/data-state';
import { IBlockStatus, IMapVertexStatus } from './types';
export class StaticBlockStatus implements IBlockStatus {
/**
* @param layer 图层对象
* @param vertex 顶点数组生成器对象
* @param x 图块横坐标
* @param y 图块纵坐标
*/
constructor(
readonly layer: IMapLayer,
readonly vertex: IMapVertexStatus,
readonly x: number,
readonly y: number
) {}
setAlpha(alpha: number): void {
this.vertex.setStaticAlpha(this.layer, alpha, this.x, this.y);
}
getAlpha(): number {
return this.vertex.getStaticAlpha(this.layer, this.x, this.y);
}
useGlobalFrame(): void {
this.vertex.setStaticFrame(this.layer, this.x, this.y, -1);
}
useSpecifiedFrame(frame: number): void {
this.vertex.setStaticFrame(this.layer, this.x, this.y, frame);
}
getFrame(): number {
return this.vertex.getStaticFrame(this.layer, this.x, this.y);
}
}
export class DynamicBlockStatus implements IBlockStatus {
/**
* @param layer 图层对象
* @param vertex 顶点数组生成器对象
* @param index 图块索引
*/
constructor(
readonly layer: IMapLayer,
readonly vertex: IMapVertexStatus,
readonly index: number
) {}
setAlpha(alpha: number): void {
this.vertex.setDynamicAlpha(this.index, alpha);
}
getAlpha(): number {
return this.vertex.getDynamicAlpha(this.index);
}
useGlobalFrame(): void {
this.vertex.setDynamicFrame(this.index, -1);
}
useSpecifiedFrame(frame: number): void {
this.vertex.setDynamicFrame(this.index, frame);
}
getFrame(): number {
return this.vertex.getDynamicFrame(this.index);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,138 @@
import { Transform } from '@motajs/render-core';
import {
IBlockData,
IMapRenderArea,
IMapRenderData,
IMapRenderer,
IMapVertexBlock,
IMapVertexGenerator,
IMapViewportController
} from './types';
import { clamp } from 'lodash-es';
export class MapViewport implements IMapViewportController {
transform: Transform = new Transform();
/** 顶点生成器 */
readonly vertex: IMapVertexGenerator;
constructor(readonly renderer: IMapRenderer) {
this.vertex = renderer.vertex;
}
private pushBlock(
list: IMapRenderArea[],
start: IBlockData<IMapVertexBlock>,
end: IBlockData<IMapVertexBlock>
) {
const startIndex = start.data.startIndex;
const endIndex = end.data.endIndex;
list.push({
startIndex,
endIndex,
count: endIndex - startIndex
});
}
private checkDynamic(
list: IMapRenderArea[],
dynamicStart: number,
dynamicCount: number
) {
const last = list[list.length - 1];
if (!last || last.endIndex < dynamicStart) {
list.push({
startIndex: dynamicStart,
endIndex: dynamicStart + dynamicCount,
count: dynamicCount
});
} else {
last.endIndex = dynamicStart + dynamicCount;
last.count += dynamicCount;
}
}
getRenderArea(): IMapRenderData {
const { cellWidth, cellHeight, renderWidth, renderHeight } =
this.renderer;
const { blockWidth, blockHeight, width, height } = this.vertex.block;
// 其实只需要算左上角和右下角就行了
const [left, top] = this.transform.untransformed(-1, -1);
const [right, bottom] = this.transform.untransformed(1, 1);
const cl = (left * renderWidth) / cellWidth;
const ct = (top * renderHeight) / cellHeight;
const cr = (right * renderWidth) / cellWidth;
const cb = (bottom * renderHeight) / cellHeight;
const blockLeft = clamp(Math.floor(cl / blockWidth), 0, width - 1);
const blockRight = clamp(Math.floor(cr / blockWidth), 0, width - 1);
const blockTop = clamp(Math.floor(ct / blockHeight), 0, height - 1);
const blockBottom = clamp(Math.floor(cb / blockHeight), 0, height - 1);
const renderArea: IMapRenderArea[] = [];
const updateArea: IMapRenderArea[] = [];
const blockList: IBlockData<IMapVertexBlock>[] = [];
// 内层横向外层纵向的话,索引在换行之前都是连续的,方便整合
for (let ny = blockTop; ny <= blockBottom; ny++) {
for (let nx = blockLeft; nx <= blockRight; nx++) {
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
blockList.push(block);
}
}
if (blockList.length > 0) {
if (blockList.length === 1) {
const block = blockList[0];
if (block.data.renderDirty) {
this.pushBlock(updateArea, block, block);
}
this.pushBlock(renderArea, block, block);
} else {
// 更新区域
let updateStart: IBlockData<IMapVertexBlock> = blockList[0];
let updateEnd: IBlockData<IMapVertexBlock> = blockList[0];
let renderStart: IBlockData<IMapVertexBlock> = blockList[0];
let renderEnd: IBlockData<IMapVertexBlock> = blockList[0];
for (let i = 1; i < blockList.length; i++) {
const block = blockList[i];
const { renderDirty } = block.data;
// 连续则合并
// 渲染区域
if (block.index === renderEnd.index + 1) {
renderEnd = block;
} else {
this.pushBlock(renderArea, renderStart, renderEnd);
renderStart = block;
renderEnd = block;
}
// 缓冲区更新区域
if (renderDirty && block.index === updateEnd.index + 1) {
updateEnd = block;
} else {
this.pushBlock(updateArea, updateStart, updateEnd);
updateStart = block;
updateEnd = block;
}
}
this.pushBlock(updateArea, updateStart, updateEnd);
this.pushBlock(renderArea, renderStart, renderEnd);
}
}
const dynamicStart = this.vertex.dynamicStart;
const dynamicCount = this.vertex.dynamicCount;
this.checkDynamic(renderArea, dynamicStart, dynamicCount);
if (this.vertex.dynamicRenderDirty) {
this.checkDynamic(updateArea, dynamicStart, dynamicCount);
}
return {
render: renderArea,
dirty: updateArea,
blockList: blockList
};
}
bindTransform(transform: Transform): void {
this.transform = transform;
}
}
@@ -5,6 +5,10 @@ import { Font } from '@motajs/render-style';
//#region 地图
/** 每个格子的默认宽度,现阶段用处不大 */
export const CELL_WIDTH = 32;
/** 每个格子的默认高度,现阶段用处不大 */
export const CELL_HEIGHT = 32;
/** 每个格子的宽高 */
export const CELL_SIZE = 32;
/** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
@@ -19,6 +23,18 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
/** 地图高度的一半 */
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
/**
* 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小,
* 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。
*/
export const DYNAMIC_RESERVE = 16;
/**
* 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。
* 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。
*/
export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50;
//#region 状态栏
@@ -8,6 +8,7 @@ export * from './controller';
export * from './main';
export * from './save';
export * from './settings';
export * from './statistics';
export * from './statusBar';
export * from './toolbar';
export * from './viewmap';
@@ -241,10 +241,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
};
onMounted(() => {
const startIndex = getPosIndex(core.saves.saveIndex - 1);
selected.value = startIndex;
const startIndex = getPosIndex(core.saves.saveIndex);
selected.value = startIndex - 1;
pageRef.value?.changePage(
Math.floor((core.saves.saveIndex - 1) / (grid.value.count - 1))
Math.floor(core.saves.saveIndex / (grid.value.count - 1))
);
updateDataList(now.value);
});
@@ -380,10 +380,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
'@save_right',
() => {
const count = grid.value.count;
if (selected.value < count - 1) {
if (selected.value < count) {
selected.value++;
} else {
selected.value = 1;
selected.value = 0;
pageRef.value?.movePage(1);
}
},
@@ -20,6 +20,7 @@ import { generateKeyboardEvent } from '@motajs/system-action';
import { getVitualKeyOnce } from '@motajs/legacy-ui';
import { getAllSavesData, getSaveData, syncFromServer } from '../utils';
import { getInput } from '../components';
import { openStatistics } from './statistics';
import { saveWithExist } from './save';
import { compressToBase64 } from 'lz-string';
import { ViewMapUI } from './viewmap';
@@ -126,7 +127,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
choices={choices}
width={POP_BOX_WIDTH}
onChoose={choose}
maxHeight={MAIN_HEIGHT - 32}
maxHeight={MAIN_HEIGHT - 64}
interval={8}
scope={scope}
/>
@@ -245,7 +246,6 @@ export const ReplaySettings = defineComponent<MainSettingsProps>(props => {
onChoose={choose}
interval={8}
scope={scope}
maxHeight={MAIN_HEIGHT - 32}
/>
);
}, mainSettingsProps);
@@ -275,12 +275,7 @@ export const GameInfo = defineComponent<MainSettingsProps>(props => {
const choose = async (key: ChoiceKey) => {
switch (key) {
case GameInfoChoice.Statistics: {
getConfirm(
props.controller,
'数据统计尚未完工',
CENTER_LOC,
POP_BOX_WIDTH
);
openStatistics(props.controller);
break;
}
case GameInfoChoice.Project: {
@@ -1,5 +1,5 @@
import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
export class CloudWeather extends CloudLike {
getImage(): SizedCanvasImageSource | null {
@@ -1,6 +1,6 @@
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
import { Weather } from '../weather';
import { SizedCanvasImageSource } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
export abstract class CloudLike extends Weather<Sprite> {
/** 不透明度 */
@@ -1,6 +1,6 @@
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
export class FogWeather extends CloudLike {
/** 雾天气的图像比较小,因此将四个进行合并 */