Return defeated parties to dormant state

This commit is contained in:
2026-08-17 20:49:04 -07:00
parent e24b1b4a3f
commit db19a89957
10 changed files with 67 additions and 66 deletions
+7 -6
View File
@@ -61,7 +61,7 @@ export class Game {
return true
}
forceEndPlayerPhase():boolean {if(this.state.phase.kind!=='player')return false;this.endPlayerPhase();return true}
resetRunByAdmin():void {this.resetRun('admin-reset','An administrator reset the run.')}
resetRunByAdmin():void {this.resetRun()}
spawn(message: SpawnMessage): SpawnResult {
if(this.externalEvents.has(message.externalEventId)) return {accepted:false,reason:'DUPLICATE',message:'That spawn request was already handled.'}
@@ -85,6 +85,7 @@ export class Game {
if(p.lifeState!=='dead') return {accepted:false,reason:'PLAYER_ALIVE',message:'That character is already alive.'}
p.lifeState='alive'; p.hp=3; p.ap=0; p.guard=0; p.eligibleThisPhase=false; p.diedOnFloor=null; p.position=copy(this.state.floor.spawnTiles[0]!)
this.log('player-resurrected',p.twitchUserId,null,`${p.displayName} rose again at the spawn!`)
if(this.state.phase.kind==='dormant')this.startPlayerPhase()
return {accepted:true,reason:null,message:`${p.displayName} resurrected.`}
}
@@ -180,7 +181,7 @@ export class Game {
if(adjacent(g.position,target.position)){
const hit=this.deps.random.next()<0.5
if(hit && target.guard>0){target.guard--;this.log('guard-blocked','goblin',target.twitchUserId,`${target.displayName}'s Guard blocks the hit.`)}
else if(hit){target.hp--;this.log('goblin-hit','goblin',target.twitchUserId,`The Goblin hits ${target.displayName} for 1 damage.`);if(target.hp<=0)this.killPlayer(target)}
else if(hit){target.hp--;this.log('goblin-hit','goblin',target.twitchUserId,`The Goblin hits ${target.displayName} for 1 damage.`);if(target.hp<=0){this.killPlayer(target);if(this.state.phase.kind==='dormant')return}}
else this.log('goblin-missed','goblin',target.twitchUserId,`The Goblin misses ${target.displayName}.`)
} else { const step=this.nextPathStep(g.position,target.position);if(!step)break;g.position=step;this.log('goblin-moved','goblin',target.twitchUserId,'The Goblin closes in.') }
} else if(g.mode==='returning'){
@@ -195,17 +196,17 @@ export class Game {
player.hp=0;player.lifeState='dead';player.ap=0;player.guard=0;player.eligibleThisPhase=false;player.diedOnFloor=this.state.floorNumber
this.log('player-died','goblin',player.twitchUserId,`${player.displayName} has fallen.`)
if(this.state.goblin.targetPlayerId===player.twitchUserId){this.state.goblin.mode='returning';this.state.goblin.targetPlayerId=null}
const all=Object.values(this.state.players);if(all.length>0 && all.every(p=>p.lifeState==='dead'))this.resetRun()
const all=Object.values(this.state.players);if(all.length>0 && all.every(p=>p.lifeState==='dead')){this.state.phase={kind:'dormant'};this.log('party-defeated',null,null,'The party has fallen. The dungeon is dormant until an adventurer returns.')}
}
private advanceFloor(): void {
this.state.phase={kind:'transition',reason:'floor-advance'};this.state.floorNumber++
const floor=this.deps.generateFloor(`${this.state.runId}-floor-${this.state.floorNumber}`);this.state.floor=floor;this.state.goblin=this.newGoblin(floor)
this.restorePlayers();this.log('floor-advanced',null,null,`The party reaches Floor ${this.state.floorNumber}!`);this.startPlayerPhase()
}
private resetRun(logType='party-wipe',message='The party has fallen. A new run begins on Floor 1.'): void {
this.state.phase={kind:'transition',reason:'party-wipe'};this.state.runId=this.deps.ids.next('run');this.state.floorNumber=1
private resetRun(): void {
this.state.phase={kind:'transition',reason:'admin-reset'};this.state.runId=this.deps.ids.next('run');this.state.floorNumber=1
const floor=this.deps.generateFloor(`${this.state.runId}-floor-1`);this.state.floor=floor;this.state.goblin=this.newGoblin(floor)
this.restorePlayers();this.log(logType,null,null,message);this.startPlayerPhase()
this.restorePlayers();this.log('admin-reset',null,null,'An administrator reset the run.');this.startPlayerPhase()
}
private restorePlayers(): void { for(const p of Object.values(this.state.players)){p.hp=3;p.lifeState='alive';p.ap=0;p.guard=0;p.healAvailable=true;p.diedOnFloor=null;p.eligibleThisPhase=false;p.participatingFloor=this.state.floorNumber;p.position=copy(this.state.floor.spawnTiles[0]!)} }
private nextPathStep(start:TilePosition,goal:TilePosition):TilePosition|null {
+1 -1
View File
@@ -21,7 +21,7 @@ export type PhaseState = { kind: 'dormant' } | {
kind: 'player'; phaseId: string; startedAt: number; deadlineAt: number
initialEligiblePlayerIds: string[]
} | { kind: 'enemy'; phaseId: string } | {
kind: 'transition'; reason: 'floor-advance' | 'party-wipe'
kind: 'transition'; reason: 'floor-advance' | 'admin-reset'
}
export interface ActionLogEntry {
sequence: number; runId: string; floorNumber: number; phaseId: string | null