refactor: mass refactoring
This commit is contained in:
parent
d0d051f547
commit
6740584fc8
|
|
@ -1,312 +0,0 @@
|
||||||
import { signal, Signal, computed } from '@preact/signals-core';
|
|
||||||
import type { Part } from './Part';
|
|
||||||
import type { Placement } from './Placement';
|
|
||||||
import type { Region } from './Region';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 游戏状态
|
|
||||||
*/
|
|
||||||
export interface GameStateData {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
phase?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 游戏状态类
|
|
||||||
* 统一管理所有 Parts, Regions, Placements
|
|
||||||
*/
|
|
||||||
export class GameState {
|
|
||||||
/** 游戏基本信息 */
|
|
||||||
data: Signal<GameStateData>;
|
|
||||||
|
|
||||||
/** Parts 存储 */
|
|
||||||
parts: Signal<Map<string, Part>>;
|
|
||||||
|
|
||||||
/** Regions 存储 */
|
|
||||||
regions: Signal<Map<string, Region>>;
|
|
||||||
|
|
||||||
/** Placements 存储 */
|
|
||||||
placements: Signal<Map<string, Placement>>;
|
|
||||||
|
|
||||||
constructor(gameData: GameStateData) {
|
|
||||||
this.data = signal(gameData);
|
|
||||||
this.parts = signal(new Map());
|
|
||||||
this.regions = signal(new Map());
|
|
||||||
this.placements = signal(new Map());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Part 相关方法 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Part
|
|
||||||
*/
|
|
||||||
addPart(part: Part): void {
|
|
||||||
const parts = new Map(this.parts.value);
|
|
||||||
parts.set(part.id, part);
|
|
||||||
this.parts.value = parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Part
|
|
||||||
*/
|
|
||||||
getPart(partId: string): Part | undefined {
|
|
||||||
return this.parts.value.get(partId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除 Part
|
|
||||||
*/
|
|
||||||
removePart(partId: string): void {
|
|
||||||
const parts = new Map(this.parts.value);
|
|
||||||
parts.delete(partId);
|
|
||||||
this.parts.value = parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Part
|
|
||||||
*/
|
|
||||||
updatePart<T extends Part>(partId: string, updates: Partial<T>): void {
|
|
||||||
const part = this.parts.value.get(partId);
|
|
||||||
if (part) {
|
|
||||||
const updated = { ...part, ...updates } as T;
|
|
||||||
const parts = new Map(this.parts.value);
|
|
||||||
parts.set(partId, updated);
|
|
||||||
this.parts.value = parts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Region 相关方法 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Region
|
|
||||||
*/
|
|
||||||
addRegion(region: Region): void {
|
|
||||||
const regions = new Map(this.regions.value);
|
|
||||||
regions.set(region.id, region);
|
|
||||||
this.regions.value = regions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Region
|
|
||||||
*/
|
|
||||||
getRegion(regionId: string): Region | undefined {
|
|
||||||
return this.regions.value.get(regionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除 Region
|
|
||||||
*/
|
|
||||||
removeRegion(regionId: string): void {
|
|
||||||
const regions = new Map(this.regions.value);
|
|
||||||
regions.delete(regionId);
|
|
||||||
this.regions.value = regions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Placement 相关方法 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Placement
|
|
||||||
*/
|
|
||||||
addPlacement(placement: Placement): void {
|
|
||||||
const placements = new Map(this.placements.value);
|
|
||||||
placements.set(placement.id, placement);
|
|
||||||
this.placements.value = placements;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Placement
|
|
||||||
*/
|
|
||||||
getPlacement(placementId: string): Placement | undefined {
|
|
||||||
return this.placements.value.get(placementId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除 Placement
|
|
||||||
*/
|
|
||||||
removePlacement(placementId: string): void {
|
|
||||||
const placement = this.placements.value.get(placementId);
|
|
||||||
if (placement) {
|
|
||||||
// 从 Region 中移除
|
|
||||||
const region = this.regions.value.get(placement.regionId);
|
|
||||||
if (region) {
|
|
||||||
const current = region.placements.value;
|
|
||||||
const index = current.indexOf(placementId);
|
|
||||||
if (index !== -1) {
|
|
||||||
const updated = [...current];
|
|
||||||
updated.splice(index, 1);
|
|
||||||
region.placements.value = updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是 keyed region,清理 slot
|
|
||||||
if (region.type === 'keyed' && region.slots) {
|
|
||||||
const slots = new Map(region.slots.value);
|
|
||||||
for (const [key, value] of slots.entries()) {
|
|
||||||
if (value === placementId) {
|
|
||||||
slots.set(key, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
region.slots.value = slots;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const placements = new Map(this.placements.value);
|
|
||||||
placements.delete(placementId);
|
|
||||||
this.placements.value = placements;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Placement
|
|
||||||
*/
|
|
||||||
updatePlacement(placementId: string, updates: Partial<Placement>): void {
|
|
||||||
const placement = this.placements.value.get(placementId);
|
|
||||||
if (placement) {
|
|
||||||
const updated = { ...placement, ...updates };
|
|
||||||
const placements = new Map(this.placements.value);
|
|
||||||
placements.set(placementId, updated);
|
|
||||||
this.placements.value = placements;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Placement 的 Part 引用
|
|
||||||
*/
|
|
||||||
updatePlacementPart(placementId: string, part: Part | null): void {
|
|
||||||
const placement = this.placements.value.get(placementId);
|
|
||||||
if (placement) {
|
|
||||||
const updated = { ...placement, part };
|
|
||||||
const placements = new Map(this.placements.value);
|
|
||||||
placements.set(placementId, updated);
|
|
||||||
this.placements.value = placements;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移动 Placement 到另一个 Region
|
|
||||||
*/
|
|
||||||
movePlacement(placementId: string, targetRegionId: string, key?: string): void {
|
|
||||||
const placement = this.placements.value.get(placementId);
|
|
||||||
if (!placement) {
|
|
||||||
throw new Error(`Placement ${placementId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceRegion = this.regions.value.get(placement.regionId);
|
|
||||||
const targetRegion = this.regions.value.get(targetRegionId);
|
|
||||||
|
|
||||||
if (!targetRegion) {
|
|
||||||
throw new Error(`Region ${targetRegionId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从源 Region 移除
|
|
||||||
if (sourceRegion) {
|
|
||||||
const current = sourceRegion.placements.value;
|
|
||||||
const index = current.indexOf(placementId);
|
|
||||||
if (index !== -1) {
|
|
||||||
const updated = [...current];
|
|
||||||
updated.splice(index, 1);
|
|
||||||
sourceRegion.placements.value = updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理源 keyed region 的 slot
|
|
||||||
if (sourceRegion.type === 'keyed' && sourceRegion.slots) {
|
|
||||||
const slots = new Map(sourceRegion.slots.value);
|
|
||||||
for (const [k, value] of slots.entries()) {
|
|
||||||
if (value === placementId) {
|
|
||||||
slots.set(k, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceRegion.slots.value = slots;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到目标 Region
|
|
||||||
if (targetRegion.type === 'keyed') {
|
|
||||||
if (key === undefined) {
|
|
||||||
throw new Error('Key is required for keyed regions');
|
|
||||||
}
|
|
||||||
if (targetRegion.slots) {
|
|
||||||
const slots = new Map(targetRegion.slots.value);
|
|
||||||
slots.set(key, placementId);
|
|
||||||
targetRegion.slots.value = slots;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPlacements = [...targetRegion.placements.value, placementId];
|
|
||||||
targetRegion.placements.value = targetPlacements;
|
|
||||||
|
|
||||||
// 更新 Placement 的 regionId
|
|
||||||
const updated = { ...placement, regionId: targetRegionId };
|
|
||||||
if (key !== undefined) {
|
|
||||||
updated.metadata = { ...updated.metadata, key };
|
|
||||||
}
|
|
||||||
const placements = new Map(this.placements.value);
|
|
||||||
placements.set(placementId, updated);
|
|
||||||
this.placements.value = placements;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 计算属性 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Region 中的所有 Placements
|
|
||||||
*/
|
|
||||||
getPlacementsInRegion(regionId: string): Placement[] {
|
|
||||||
const region = this.regions.value.get(regionId);
|
|
||||||
if (!region) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const placementIds = region.placements.value;
|
|
||||||
return placementIds
|
|
||||||
.map((id) => this.placements.value.get(id))
|
|
||||||
.filter((p): p is Placement => p !== undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Part 的所有 Placements
|
|
||||||
*/
|
|
||||||
getPlacementsOfPart(partId: string): Placement[] {
|
|
||||||
const allPlacements = Array.from(this.placements.value.values());
|
|
||||||
return allPlacements.filter((p) => p.partId === partId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建计算信号:获取 Region 中的 Placement 数量
|
|
||||||
*/
|
|
||||||
createPlacementCountSignal(regionId: string): Signal<number> {
|
|
||||||
const region = this.regions.value.get(regionId);
|
|
||||||
if (!region) {
|
|
||||||
return signal(0);
|
|
||||||
}
|
|
||||||
return computed(() => region.placements.value.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 游戏状态管理 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新游戏阶段
|
|
||||||
*/
|
|
||||||
setPhase(phase: string): void {
|
|
||||||
this.data.value = { ...this.data.value, phase };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新游戏元数据
|
|
||||||
*/
|
|
||||||
updateMetadata(updates: Record<string, unknown>): void {
|
|
||||||
this.data.value = {
|
|
||||||
...this.data.value,
|
|
||||||
metadata: { ...this.data.value.metadata, ...updates },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建游戏状态
|
|
||||||
*/
|
|
||||||
export function createGameState(data: GameStateData): GameState {
|
|
||||||
return new GameState(data);
|
|
||||||
}
|
|
||||||
103
src/core/Part.ts
103
src/core/Part.ts
|
|
@ -1,103 +0,0 @@
|
||||||
import { signal } from '@preact/signals-core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Part 类型枚举
|
|
||||||
*/
|
|
||||||
export enum PartType {
|
|
||||||
Meeple = 'meeple',
|
|
||||||
Card = 'card',
|
|
||||||
Tile = 'tile',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Part 的基础属性
|
|
||||||
*/
|
|
||||||
export interface PartBase {
|
|
||||||
id: string;
|
|
||||||
type: PartType;
|
|
||||||
name?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meeple 特有属性
|
|
||||||
*/
|
|
||||||
export interface MeeplePart extends PartBase {
|
|
||||||
type: PartType.Meeple;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Card 特有属性
|
|
||||||
*/
|
|
||||||
export interface CardPart extends PartBase {
|
|
||||||
type: PartType.Card;
|
|
||||||
suit?: string;
|
|
||||||
value?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tile 特有属性
|
|
||||||
*/
|
|
||||||
export interface TilePart extends PartBase {
|
|
||||||
type: PartType.Tile;
|
|
||||||
pattern?: string;
|
|
||||||
rotation?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Part 联合类型
|
|
||||||
*/
|
|
||||||
export type Part = MeeplePart | CardPart | TilePart;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Part 信号类型
|
|
||||||
*/
|
|
||||||
export type PartSignal = ReturnType<typeof signal<Part>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Part
|
|
||||||
*/
|
|
||||||
export function createPart<T extends Part>(part: T): T {
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Meeple Part
|
|
||||||
*/
|
|
||||||
export function createMeeple(id: string, color: string, options?: { name?: string; metadata?: Record<string, unknown> }): MeeplePart {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: PartType.Meeple,
|
|
||||||
color,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Card Part
|
|
||||||
*/
|
|
||||||
export function createCard(
|
|
||||||
id: string,
|
|
||||||
options?: { suit?: string; value?: number | string; name?: string; metadata?: Record<string, unknown> }
|
|
||||||
): CardPart {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: PartType.Card,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Tile Part
|
|
||||||
*/
|
|
||||||
export function createTile(
|
|
||||||
id: string,
|
|
||||||
options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record<string, unknown> }
|
|
||||||
): TilePart {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: PartType.Tile,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import { signal, Signal } from '@preact/signals-core';
|
|
||||||
import type { Part } from './Part';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placement 的位置信息
|
|
||||||
*/
|
|
||||||
export interface Position {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placement 属性
|
|
||||||
*/
|
|
||||||
export interface PlacementProperties {
|
|
||||||
id: string;
|
|
||||||
partId: string;
|
|
||||||
regionId: string;
|
|
||||||
position?: Position;
|
|
||||||
rotation?: number;
|
|
||||||
faceUp?: boolean;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placement 类型
|
|
||||||
*/
|
|
||||||
export interface Placement extends PlacementProperties {
|
|
||||||
part: Part | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placement 信号类型
|
|
||||||
*/
|
|
||||||
export type PlacementSignal = Signal<Placement>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Placement
|
|
||||||
*/
|
|
||||||
export function createPlacement(properties: {
|
|
||||||
id: string;
|
|
||||||
partId: string;
|
|
||||||
regionId: string;
|
|
||||||
part: Part;
|
|
||||||
position?: Position;
|
|
||||||
rotation?: number;
|
|
||||||
faceUp?: boolean;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}): Placement {
|
|
||||||
return {
|
|
||||||
id: properties.id,
|
|
||||||
partId: properties.partId,
|
|
||||||
regionId: properties.regionId,
|
|
||||||
part: properties.part,
|
|
||||||
position: properties.position,
|
|
||||||
rotation: properties.rotation ?? 0,
|
|
||||||
faceUp: properties.faceUp ?? true,
|
|
||||||
metadata: properties.metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Placement 的 Part 引用
|
|
||||||
*/
|
|
||||||
export function updatePlacementPart(placement: Placement, part: Part | null): void {
|
|
||||||
placement.part = part;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Placement 的位置
|
|
||||||
*/
|
|
||||||
export function updatePlacementPosition(placement: Placement, position: Position): void {
|
|
||||||
placement.position = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新 Placement 的旋转角度
|
|
||||||
*/
|
|
||||||
export function updatePlacementRotation(placement: Placement, rotation: number): void {
|
|
||||||
placement.rotation = rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻转 Placement
|
|
||||||
*/
|
|
||||||
export function flipPlacement(placement: Placement): void {
|
|
||||||
placement.faceUp = !placement.faceUp;
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { signal, Signal } from '@preact/signals-core';
|
|
||||||
import type { Placement } from './Placement';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Region 类型
|
|
||||||
*/
|
|
||||||
export enum RegionType {
|
|
||||||
/**
|
|
||||||
* Keyed Region - 子元素通过 key 索引
|
|
||||||
* 适用于:玩家手牌、版图格子等有固定位置的区域
|
|
||||||
*/
|
|
||||||
Keyed = 'keyed',
|
|
||||||
/**
|
|
||||||
* Unkeyed Region - 子元素按顺序排列
|
|
||||||
* 适用于:牌库、弃牌堆等堆叠区域
|
|
||||||
*/
|
|
||||||
Unkeyed = 'unkeyed',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Region 属性
|
|
||||||
*/
|
|
||||||
export interface RegionProperties {
|
|
||||||
id: string;
|
|
||||||
type: RegionType;
|
|
||||||
name?: string;
|
|
||||||
capacity?: number;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keyed Region 的槽位
|
|
||||||
*/
|
|
||||||
export interface Slot {
|
|
||||||
key: string;
|
|
||||||
placementId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Region 类型
|
|
||||||
*/
|
|
||||||
export interface Region extends RegionProperties {
|
|
||||||
placements: Signal<string[]>; // Placement ID 列表
|
|
||||||
slots?: Signal<Map<string, string | null>>; // Keyed Region 专用:key -> placementId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Region
|
|
||||||
*/
|
|
||||||
export function createRegion(properties: RegionProperties): Region {
|
|
||||||
const region: Region = {
|
|
||||||
...properties,
|
|
||||||
placements: signal<string[]>([]),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (properties.type === RegionType.Keyed) {
|
|
||||||
region.slots = signal<Map<string, string | null>>(new Map());
|
|
||||||
}
|
|
||||||
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加 Placement ID 到 Region (unkeyed)
|
|
||||||
*/
|
|
||||||
export function addPlacementToRegion(region: Region, placementId: string): void {
|
|
||||||
if (region.type === RegionType.Keyed) {
|
|
||||||
throw new Error('Cannot use addPlacementToRegion on a keyed region. Use setSlot instead.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = region.placements.value;
|
|
||||||
if (region.capacity !== undefined && current.length >= region.capacity) {
|
|
||||||
throw new Error(`Region ${region.id} has reached its capacity of ${region.capacity}`);
|
|
||||||
}
|
|
||||||
region.placements.value = [...current, placementId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Region 移除 Placement ID
|
|
||||||
*/
|
|
||||||
export function removePlacementFromRegion(region: Region, placementId: string): void {
|
|
||||||
const current = region.placements.value;
|
|
||||||
const index = current.indexOf(placementId);
|
|
||||||
if (index !== -1) {
|
|
||||||
const updated = [...current];
|
|
||||||
updated.splice(index, 1);
|
|
||||||
region.placements.value = updated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 Keyed Region 的槽位
|
|
||||||
*/
|
|
||||||
export function setSlot(region: Region, key: string, placementId: string | null): void {
|
|
||||||
if (region.type !== RegionType.Keyed || !region.slots) {
|
|
||||||
throw new Error('Cannot use setSlot on an unkeyed region.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const slots = new Map(region.slots.value);
|
|
||||||
|
|
||||||
// 如果是放置新 placement,需要更新 placements 列表
|
|
||||||
if (placementId !== null) {
|
|
||||||
const currentPlacements = region.placements.value;
|
|
||||||
if (!currentPlacements.includes(placementId)) {
|
|
||||||
region.placements.value = [...currentPlacements, placementId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slots.set(key, placementId);
|
|
||||||
region.slots.value = slots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Keyed Region 的槽位
|
|
||||||
*/
|
|
||||||
export function getSlot(region: Region, key: string): string | null {
|
|
||||||
if (region.type !== RegionType.Keyed || !region.slots) {
|
|
||||||
throw new Error('Cannot use getSlot on an unkeyed region.');
|
|
||||||
}
|
|
||||||
return region.slots.value.get(key) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空 Region
|
|
||||||
*/
|
|
||||||
export function clearRegion(region: Region): void {
|
|
||||||
region.placements.value = [];
|
|
||||||
if (region.slots) {
|
|
||||||
region.slots.value = new Map();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Region 中 Placement 的数量
|
|
||||||
*/
|
|
||||||
export function getPlacementCount(region: Region): number {
|
|
||||||
return region.placements.value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查 Region 是否为空
|
|
||||||
*/
|
|
||||||
export function isRegionEmpty(region: Region): boolean {
|
|
||||||
return region.placements.value.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查 Region 是否已满
|
|
||||||
*/
|
|
||||||
export function isRegionFull(region: Region): boolean {
|
|
||||||
if (region.capacity === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return region.placements.value.length >= region.capacity;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import {createModel, Signal, signal} from '@preact/signals-core';
|
||||||
|
import {createEntityCollection} from "../utils/entity";
|
||||||
|
import {Part} from "./part";
|
||||||
|
import {Region} from "./region";
|
||||||
|
|
||||||
|
export type Context = {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GameContext = createModel((root: Context) => {
|
||||||
|
const parts = createEntityCollection<Part>();
|
||||||
|
const regions = createEntityCollection<Region>();
|
||||||
|
const contexts = signal([signal(root)]);
|
||||||
|
function pushContext(context: Context) {
|
||||||
|
const ctxSignal = signal(context);
|
||||||
|
contexts.value = [...contexts.value, ctxSignal];
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
function popContext() {
|
||||||
|
contexts.value = contexts.value.slice(0, -1);
|
||||||
|
}
|
||||||
|
function latestContext<T extends Context>(type: T['type']){
|
||||||
|
for(let i = contexts.value.length - 1; i >= 0; i--){
|
||||||
|
if(contexts.value[i].value.type === type){
|
||||||
|
return contexts.value[i] as Signal<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parts,
|
||||||
|
regions,
|
||||||
|
contexts,
|
||||||
|
pushContext,
|
||||||
|
popContext,
|
||||||
|
latestContext,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {Entity, EntityAccessor} from "../utils/entity";
|
||||||
|
import {Region} from "./region";
|
||||||
|
import {RNG} from "../utils/rng";
|
||||||
|
|
||||||
|
export type Part = Entity & {
|
||||||
|
// cards have 2 sides, dices have multiple, tokens have 1
|
||||||
|
sides: number;
|
||||||
|
|
||||||
|
// mostly rotations, if relevant
|
||||||
|
alignments?: string[];
|
||||||
|
|
||||||
|
// current side
|
||||||
|
side: number;
|
||||||
|
// current alignment
|
||||||
|
alignment?: string;
|
||||||
|
|
||||||
|
// current region
|
||||||
|
region: EntityAccessor<Region>;
|
||||||
|
// current position in region
|
||||||
|
position: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flip(part: Part) {
|
||||||
|
part.side = (part.side + 1) % part.sides;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flipTo(part: Part, side: number) {
|
||||||
|
part.side = side;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roll(part: Part, rng: RNG) {
|
||||||
|
part.side = rng.nextInt(part.sides);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {Entity, EntityAccessor} from "../utils/entity";
|
||||||
|
import {Part} from "./part";
|
||||||
|
import {RNG} from "../utils/rng";
|
||||||
|
|
||||||
|
export type Region = Entity & {
|
||||||
|
// aligning axes of the region
|
||||||
|
axes: RegionAxis[];
|
||||||
|
|
||||||
|
// current children; expect no overlapped positions
|
||||||
|
children: EntityAccessor<Part>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegionAxis = {
|
||||||
|
name: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
align?: 'start' | 'end' | 'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for each axis, try to remove gaps in positions.
|
||||||
|
* - if min exists and align is start, and there are parts at (for example) min+2 and min+4, then move them to min and min+1
|
||||||
|
* - if max exists and align is end, and there are parts at (for example) max-2 and max-4, then move them to max-1 and max-3
|
||||||
|
* - for center, move parts to the center, possibly creating parts placed at 0.5 positions
|
||||||
|
* - sort children so that they're in ascending order on each axes.
|
||||||
|
* @param region
|
||||||
|
*/
|
||||||
|
export function applyAlign(region: Region){
|
||||||
|
for (const axis of region.axes) {
|
||||||
|
// TODO implement this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shuffle on each axis. for each axis, try to swap position.
|
||||||
|
* @param region
|
||||||
|
* @param rng
|
||||||
|
*/
|
||||||
|
export function shuffle(region: Region, rng: RNG){
|
||||||
|
// TODO implement this
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {Context} from "./context";
|
||||||
|
import {Command} from "../utils/command";
|
||||||
|
import {effect} from "@preact/signals-core";
|
||||||
|
|
||||||
|
export type RuleContext<T> = Context & {
|
||||||
|
actions: Command[];
|
||||||
|
handledActions: number;
|
||||||
|
invocations: RuleContext<unknown>[];
|
||||||
|
resolution?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeRuleContext<T>(pushContext: (context: Context) => void, type: string, rule: Generator<string, T, Command>){
|
||||||
|
const ctx: RuleContext<T> = {
|
||||||
|
type,
|
||||||
|
actions: [],
|
||||||
|
handledActions: 0,
|
||||||
|
invocations: [],
|
||||||
|
resolution: undefined,
|
||||||
|
}
|
||||||
|
const dispose = effect(() => {
|
||||||
|
if(ctx.resolution) {
|
||||||
|
dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pushContext(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* rule(){
|
||||||
|
const play: Command = yield 'play';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export type Command = {
|
||||||
|
name: string;
|
||||||
|
flags: Record<string, true>;
|
||||||
|
options: Record<string, string>;
|
||||||
|
params: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO implement this
|
||||||
|
export function parseCommand(input: string): Command {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {Signal, signal} from "@preact/signals-core";
|
||||||
|
|
||||||
|
export type Entity = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntityAccessor<T extends Entity> = {
|
||||||
|
id: string;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEntityCollection<T extends Entity>() {
|
||||||
|
const collection = signal({} as Record<string, Signal<T>>);
|
||||||
|
const remove = (...ids: string[]) => {
|
||||||
|
collection.value = Object.fromEntries(
|
||||||
|
Object.entries(collection.value).filter(([id]) => !ids.includes(id)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const add = (...entities: T[]) => {
|
||||||
|
collection.value = {
|
||||||
|
...collection.value,
|
||||||
|
...Object.fromEntries(entities.map((entity) => [entity.id, signal(entity)])),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = (id: string) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
get value(){
|
||||||
|
return collection.value[id]?.value;
|
||||||
|
},
|
||||||
|
set value(value: T){
|
||||||
|
const signal = collection.value[id];
|
||||||
|
if(signal)signal.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
collection,
|
||||||
|
remove,
|
||||||
|
add,
|
||||||
|
get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export interface RNG {
|
||||||
|
/** 设置随机数种子 */
|
||||||
|
(seed: number): void;
|
||||||
|
|
||||||
|
/** 获取一个[0,1)随机数 */
|
||||||
|
next(max?: number): number;
|
||||||
|
|
||||||
|
/** 获取一个[0,max)随机整数 */
|
||||||
|
nextInt(max: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create a RNG implementation with the alea library
|
||||||
Loading…
Reference in New Issue