refactor: reorg
This commit is contained in:
parent
4deebf67c3
commit
3a135a4ad1
|
|
@ -1,5 +1,5 @@
|
||||||
import {Part} from "./part";
|
import {Part} from "./part";
|
||||||
import {RNG} from "@/utils/rng";
|
import {ReadonlyRNG} from "@/utils/rng";
|
||||||
|
|
||||||
export type Region = {
|
export type Region = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -107,7 +107,7 @@ export function applyAlign<TMeta>(region: Region, parts: Record<string, Part<TMe
|
||||||
region.partMap = buildPartMap(region, parts);
|
region.partMap = buildPartMap(region, parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shuffle<TMeta>(region: Region, parts: Record<string, Part<TMeta>>, rng: RNG){
|
export function shuffle<TMeta>(region: Region, parts: Record<string, Part<TMeta>>, rng: ReadonlyRNG){
|
||||||
if (region.childIds.length <= 1) return;
|
if (region.childIds.length <= 1) return;
|
||||||
|
|
||||||
const childIds = [...region.childIds];
|
const childIds = [...region.childIds];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {CombatEntity, EffectTable} from "./types";
|
import {CombatEntity, EffectTable} from "./types";
|
||||||
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import {PlayerEntity} from "@/samples/slay-the-spire-like/system/combat/types";
|
import {PlayerEntity} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
|
import {CombatState} from "./types";
|
||||||
|
|
||||||
export function addEffect(effects: EffectTable, effect: EffectData, stacks: number){
|
export function addEffect(effects: EffectTable, effect: EffectData, stacks: number){
|
||||||
let current = effects[effect.id];
|
let current = effects[effect.id];
|
||||||
|
|
@ -43,7 +44,10 @@ export function onPlayerItemEffectUpkeep(entity: PlayerEntity){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
export function* getAliveEnemies(state: CombatState) {
|
||||||
export function onDraw(entity: PlayerEntity, cardId:string ){}
|
for (let enemy of state.enemies) {
|
||||||
// TODO
|
if (enemy.isAlive) {
|
||||||
export function onDiscard(entity: PlayerEntity, cardId: string){}
|
yield enemy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export async function promptMainAction(game: CombatGameContext){
|
||||||
action: 'end-turn' as 'end-turn'
|
action: 'end-turn' as 'end-turn'
|
||||||
};
|
};
|
||||||
|
|
||||||
const exists = game.value.player.deck.hand.includes(cardId);
|
const exists = game.value.player.deck.regions.hand.childIds.includes(cardId);
|
||||||
if(!exists) throw `卡牌"${cardId}"不在手牌中`;
|
if(!exists) throw `卡牌"${cardId}"不在手牌中`;
|
||||||
|
|
||||||
const card = game.value.player.deck.cards[cardId];
|
const card = game.value.player.deck.cards[cardId];
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,132 @@
|
||||||
import {createMiddlewareChain} from "../utils/middleware";
|
|
||||||
import {CombatGameContext} from "./types";
|
import {CombatGameContext} from "./types";
|
||||||
import {getAliveEnemies} from "@/samples/slay-the-spire-like/system/combat/utils";
|
|
||||||
import {
|
import {
|
||||||
onDiscard, onDraw,
|
addEntityEffect,
|
||||||
|
addItemEffect,
|
||||||
|
getAliveEnemies,
|
||||||
onEntityEffectUpkeep,
|
onEntityEffectUpkeep,
|
||||||
onPlayerItemEffectUpkeep
|
onPlayerItemEffectUpkeep
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
import {promptMainAction} from "@/samples/slay-the-spire-like/system/combat/prompts";
|
import {promptMainAction} from "@/samples/slay-the-spire-like/system/combat/prompts";
|
||||||
|
import {moveToRegion, shuffle} from "@/core/region";
|
||||||
|
import {createMiddlewareChain} from "@/utils/middleware";
|
||||||
|
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
import {getAdjacentItems} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||||
|
import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress";
|
||||||
|
|
||||||
type TriggerTypes = {
|
type TriggerTypes = {
|
||||||
onCombatStart: {},
|
onCombatStart: {},
|
||||||
onTurnStart: { entityKey: "player" | string, },
|
onTurnStart: { entityKey: "player" | string, },
|
||||||
onTurnEnd: { entityKey: "player" | string, },
|
onTurnEnd: { entityKey: "player" | string, },
|
||||||
onShuffle: { entityKey: "player" | string, },
|
onShuffle: {},
|
||||||
onCardPlayed: { cardId: string, targetId?: string },
|
onCardPlayed: { cardId: string, targetId?: string },
|
||||||
onCardDiscarded: { cardId: string, },
|
onCardDiscarded: { cardId: string, },
|
||||||
onCardDrawn: { cardId: string, },
|
onCardDrawn: { cardId: string, },
|
||||||
onEffectApplied: { effectId: string, entityKey: "player" | string, stacks: number, },
|
onDraw: {count: number},
|
||||||
|
onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string },
|
||||||
|
onHpChange: { entityKey: "player" | string, amount: number},
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTriggers(){
|
function createTriggers(){
|
||||||
return {
|
const triggers = {
|
||||||
onCombatStart: createTrigger("onCombatStart"),
|
onCombatStart: createTrigger("onCombatStart"),
|
||||||
onTurnStart: createTrigger("onTurnStart"),
|
onTurnStart: createTrigger("onTurnStart", async ctx => {
|
||||||
onTurnEnd: createTrigger("onTurnEnd"),
|
await ctx.game.produceAsync(draft => {
|
||||||
onShuffle: createTrigger("onShuffle"),
|
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
||||||
onCardPlayed: createTrigger("onCardPlayed"),
|
if(entity) onEntityEffectUpkeep(entity);
|
||||||
onCardDiscarded: createTrigger("onCardDiscarded"),
|
if(entity === draft.player)
|
||||||
onCardDrawn: createTrigger("onCardDrawn"),
|
onPlayerItemEffectUpkeep(draft.player);
|
||||||
onEffectApplied: createTrigger("onEffectApplied"),
|
})
|
||||||
|
}),
|
||||||
|
onTurnEnd: createTrigger("onTurnEnd", async ctx => {
|
||||||
|
if(ctx.entityKey !== "player")return;
|
||||||
|
const {regions} = ctx.game.value.player.deck;
|
||||||
|
for(const cardId of Object.values(regions.hand.childIds)){
|
||||||
|
await triggers.onCardDiscarded.execute(ctx.game,{cardId});
|
||||||
}
|
}
|
||||||
|
await ctx.game.produceAsync(draft => draft.player.energy = draft.player.maxEnergy);
|
||||||
|
await triggers.onDraw.execute(ctx.game,{count: 5});
|
||||||
|
}),
|
||||||
|
onShuffle: createTrigger("onShuffle", async ctx => {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const {cards, regions} = draft.player.deck;
|
||||||
|
for(const cardId of Object.values(regions.discardPile.childIds))
|
||||||
|
moveToRegion(cards[cardId], regions.discardPile, regions.drawPile);
|
||||||
|
shuffle(regions.drawPile, cards, ctx.game.rng);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onCardPlayed: createTrigger("onCardPlayed", async ctx => {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const {cards, regions} = draft.player.deck;
|
||||||
|
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const {cards, regions} = draft.player.deck;
|
||||||
|
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onCardDrawn: createTrigger("onCardDrawn", async ctx => {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const {cards, regions} = draft.player.deck;
|
||||||
|
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onDraw: createTrigger("onDraw", async ctx => {
|
||||||
|
let toDraw = ctx.count;
|
||||||
|
while(toDraw > 0){
|
||||||
|
let inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
|
if(inDraw <= 0) await triggers.onShuffle.execute(ctx.game,{});
|
||||||
|
|
||||||
|
inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
|
if(inDraw <= 0) break;
|
||||||
|
|
||||||
|
const children = ctx.game.value.player.deck.regions.drawPile.childIds;
|
||||||
|
const cardId = children[children.length - 1];
|
||||||
|
await triggers.onCardDrawn.execute(ctx.game,{cardId});
|
||||||
|
toDraw--;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onEffectApplied: createTrigger("onEffectApplied", async ctx => {
|
||||||
|
if(ctx.effect.lifecycle === 'instant') return;
|
||||||
|
|
||||||
|
if(ctx.effect.lifecycle.startsWith("item")) {
|
||||||
|
if(ctx.cardId){
|
||||||
|
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
||||||
|
const nearby = getAdjacentItems<GameItemMeta>(ctx.game.value.inventory, card.itemId);
|
||||||
|
for(const itemId of nearby.keys()){
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.effect.lifecycle.startsWith('card')){
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
||||||
|
if(entity) addEntityEffect(entity, ctx.effect, ctx.stacks);
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
onHpChange: createTrigger("onHpChange", async ctx => {
|
||||||
|
await ctx.game.produceAsync(draft => {
|
||||||
|
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
||||||
|
if(!entity) return;
|
||||||
|
entity.hp += ctx.amount;
|
||||||
|
entity.isAlive = entity.hp > 0;
|
||||||
|
draft.result = !draft.player.isAlive ? "defeat" : draft.enemies.every(e => !e.isAlive) ? "victory" : null;
|
||||||
|
});
|
||||||
|
if(ctx.game.value.result) throw ctx.game.value;
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return triggers;
|
||||||
}
|
}
|
||||||
export type Triggers = ReturnType<typeof createTriggers>
|
export type Triggers = ReturnType<typeof createTriggers>
|
||||||
export function createStartWith(build: (triggers: Triggers) => void){
|
export function createStartWith(build: (triggers: Triggers) => void){
|
||||||
|
|
@ -38,56 +135,43 @@ export function createStartWith(build: (triggers: Triggers) => void){
|
||||||
return async function(game: CombatGameContext){
|
return async function(game: CombatGameContext){
|
||||||
await triggers.onCombatStart.execute(game,{});
|
await triggers.onCombatStart.execute(game,{});
|
||||||
|
|
||||||
// TODO at the end of a damage effect, if win/loss is achieved, break the loop with a throw
|
try {
|
||||||
// catch the throw and return the result here
|
while (true) {
|
||||||
while(true){
|
await triggers.onTurnStart.execute(game, {entityKey: "player"});
|
||||||
await triggers.onTurnStart.execute(game,{entityKey: "player"});
|
while (true) {
|
||||||
await game.produceAsync(draft => {
|
|
||||||
onEntityEffectUpkeep(draft.player);
|
|
||||||
onPlayerItemEffectUpkeep(draft.player);
|
|
||||||
});
|
|
||||||
while(true){
|
|
||||||
const action = await promptMainAction(game);
|
const action = await promptMainAction(game);
|
||||||
if(action.action === "end-turn") break;
|
if (action.action === "end-turn") break;
|
||||||
if(action.action === "play"){
|
if (action.action === "play") {
|
||||||
await game.produceAsync(draft => onDiscard(draft.player, action.cardId));
|
// TODO: energy/use consumption handling
|
||||||
await triggers.onCardPlayed.execute(game, action);
|
await triggers.onCardPlayed.execute(game, action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(const cardId of [...game.value.player.deck.hand]){
|
await triggers.onTurnEnd.execute(game, {entityKey: "player"});
|
||||||
await game.produceAsync(draft => onDiscard(draft.player, cardId));
|
|
||||||
await triggers.onCardDiscarded.execute(game,{cardId});
|
|
||||||
}
|
|
||||||
await triggers.onTurnEnd.execute(game,{entityKey: "player"});
|
|
||||||
await game.produceAsync(draft => draft.player.energy = draft.player.maxEnergy);
|
|
||||||
for(let i = 0; i < 5; i++){
|
|
||||||
const cardId = game.value.player.deck.drawPile[0]; // TODO: should this be drawPile[-1] ?
|
|
||||||
if(!cardId) break;
|
|
||||||
await game.produceAsync(draft => onDraw(draft.player, cardId));
|
|
||||||
await triggers.onCardDrawn.execute(game,{cardId});
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const enemy of getAliveEnemies(game.value)){
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
await triggers.onTurnStart.execute(game,{entityKey: enemy.id});
|
await triggers.onTurnStart.execute(game, {entityKey: enemy.id});
|
||||||
}
|
}
|
||||||
await game.produceAsync(draft => {
|
|
||||||
for(const enemy of getAliveEnemies(game.value)){
|
|
||||||
onEntityEffectUpkeep(enemy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// TODO execute enemy intent, then update with new one here
|
// TODO execute enemy intent, then update with new one here
|
||||||
for(const enemy of getAliveEnemies(game.value)){
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
await triggers.onTurnEnd.execute(game,{entityKey: enemy.id});
|
await triggers.onTurnEnd.execute(game, {entityKey: enemy.id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}catch(e){
|
||||||
|
if(e === game.value) return game.value.result;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrigger<TKey extends keyof TriggerTypes>(event: TKey) {
|
type TriggerContext<TKey extends keyof TriggerTypes> = TriggerTypes[TKey] & { event: TKey, game: CombatGameContext };
|
||||||
type Ctx = TriggerTypes[TKey] & { event: TKey, game: CombatGameContext };
|
function createTrigger<TKey extends keyof TriggerTypes>(event: TKey, fallback?: (ctx: TriggerContext<TKey>) => Promise<void>) {
|
||||||
const {use, execute} = createMiddlewareChain<Ctx>();
|
const {use, execute} = createMiddlewareChain<TriggerContext<TKey>,void>(fallback);
|
||||||
return {
|
return {
|
||||||
use,
|
use,
|
||||||
execute: (game: CombatGameContext, ctx: TriggerTypes[TKey]) => execute({...ctx, event, game}),
|
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
|
||||||
|
const param = {...ctx, game, event};
|
||||||
|
await execute(param);
|
||||||
|
return param;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import {CombatState} from "./types";
|
|
||||||
|
|
||||||
export function* getAliveEnemies(state: CombatState) {
|
|
||||||
for (let enemy of state.enemies) {
|
|
||||||
if (enemy.isAlive) {
|
|
||||||
yield enemy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -197,7 +197,7 @@ export function getItemAtCell<TMeta = Record<string, unknown>>(
|
||||||
* Gets all items adjacent to the given item (orthogonally, not diagonally).
|
* Gets all items adjacent to the given item (orthogonally, not diagonally).
|
||||||
* Returns a Map of itemId -> item for deduplication.
|
* Returns a Map of itemId -> item for deduplication.
|
||||||
*/
|
*/
|
||||||
export function getAdjacentItems<TMeta extends Record<string, unknown> = Record<string, unknown>>(
|
export function getAdjacentItems<TMeta>(
|
||||||
inventory: GridInventory<TMeta>,
|
inventory: GridInventory<TMeta>,
|
||||||
itemId: string
|
itemId: string
|
||||||
): Map<string, InventoryItem<TMeta>> {
|
): Map<string, InventoryItem<TMeta>> {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export interface CellCoordinate {
|
||||||
* An item placed on the grid inventory.
|
* An item placed on the grid inventory.
|
||||||
* @template TMeta - Optional metadata type for game-specific data
|
* @template TMeta - Optional metadata type for game-specific data
|
||||||
*/
|
*/
|
||||||
export interface InventoryItem<TMeta = Record<string, unknown>> {
|
export interface InventoryItem<TMeta> {
|
||||||
/** Unique item identifier */
|
/** Unique item identifier */
|
||||||
id: string;
|
id: string;
|
||||||
/** Reference to the item's shape definition */
|
/** Reference to the item's shape definition */
|
||||||
|
|
@ -44,7 +44,7 @@ export type MutationResult = { success: true } | { success: false; reason: strin
|
||||||
* Designed to be mutated directly inside a `mutative .produce()` callback.
|
* Designed to be mutated directly inside a `mutative .produce()` callback.
|
||||||
* @template TMeta - Optional metadata type for items
|
* @template TMeta - Optional metadata type for items
|
||||||
*/
|
*/
|
||||||
export interface GridInventory<TMeta = Record<string, unknown>> {
|
export interface GridInventory<TMeta> {
|
||||||
/** Board width in cells */
|
/** Board width in cells */
|
||||||
width: number;
|
width: number;
|
||||||
/** Board height in cells */
|
/** Board height in cells */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue