Compare commits

...

10 Commits

@ -0,0 +1,26 @@
# I Can't Write Jam Entry, "Drowning Among Stars"
A short narrative and puzzle driven text adventure about disaster in the final frontier. Repair your spacecraft and get to safety before you run out of breathable air!
This game was created for the [I Can't Write (But Want to Tell a Story)](https://itch.io/jam/i-cant-write-but-want-to-tell-a-story) jam, and built with TypeScript and React.
Tested in Firefox and Chrome; it's playable in Edge but I don't recommend it (practically all the visual effects are broken, and the way it renders the background makes text very hard to read); I don't have a machine capable of testing it with Safari so your mileage may vary.
### Features:
- Classic command-driven text adventure
- Space!
- Ray-traced graphics\*
- In-game map, inventory, and help UI
- Potential sabotage?
- Built in hint system
- Did I mention it's in space?
### Known Issues:
- The "last seen location" for items sometimes does not update correctly.
- When long sections of text are printed, the beginning of the text can be pushed out the top of the screen (although you should still be able to read it by scrolling up).
\* This isn't even a joke! Okay no it is a joke, but it's not a lie. The background image was modeled in Blender and then rendered in the raytracing engine Cycles. The resulting image is used as a static background, but it was raytraced and is the largest of three graphics in the game so it counts.
What? Don't look at me like that - I'm not a wizard, and RTX definitely isn't supported in Javascript yet so this is as good as you're getting for now.

@ -50,7 +50,12 @@ export default function Help({parentRef}) {
<li>take [thing]</li>
<li>drop [thing]</li>
<li>open [thing]</li>
<li>turn on [thing]</li>
<li>take apart [thing]</li>
</ul>
<p>
Finally, if you get stuck on how to solve a puzzle, try the <Cmd>hint</Cmd> command.
</p>
</>
}

@ -55,11 +55,11 @@ export default function Map() {
<h3>{currentRoom.printableName}</h3>
{currentRoom.visited
? (<>
<ReactMarkdown>{currentRoom.description}</ReactMarkdown>
<ReactMarkdown escapeHtml={false}>{currentRoom.description}</ReactMarkdown>
{itemsInRoom.length > 0 && (<>
<p>You {(playerLocation === roomName) ? 'see' : 'recall seeing'} the following items here:</p>
<ul>
{itemsInRoom.map(({name, printableName}) => <li>{printableName || name}</li>)}
{itemsInRoom.map(({name, printableName}) => <li><ReactMarkdown escapeHtml={false}>{printableName || name}</ReactMarkdown></li>)}
</ul>
</>)}
</>)

@ -16,7 +16,7 @@ export default function Text({promptVisible, messages, currentInput, currentScro
<div ref={outputRef} className={styles.output}>
{messages.map((message, i) => {
if(message.type === 'message')
return <ReactMarkdown key={i}>{message.message}</ReactMarkdown>
return <ReactMarkdown escapeHtml={false} key={i}>{message.message}</ReactMarkdown>
if(message.type === 'command')
return <p key={i} className={styles.command}>{message.command}</p>

@ -87,6 +87,8 @@ export default function Text({promptVisible: promptEnabled, handleCommand, showR
if(ev.key !== ' ' && ev.key !== 'Enter') return;
ev.preventDefault()
inputRef.current.value = ''
setCurrentInput('')
game.getState().messages[currentPause].resolved = true
game.saveDraft()
forceRender()

@ -29,7 +29,7 @@ export default class Game {
constructor() {
let state = this.getState()
state.directions.set('fore', {type: ObjectType.Direction, name: 'fore', printableName: 'fore', aliases: ['north', 'f', 'n'], opposite: 'aft'})
state.directions.set('fore', {type: ObjectType.Direction, name: 'fore', printableName: 'fore', aliases: ['north', 'f', 'n', 'forward', 'foreward'], opposite: 'aft'})
state.directions.set('starboard', {type: ObjectType.Direction, name: 'starboard', printableName: 'starboard', aliases: ['east', 'sb', 'e'], opposite: 'port'})
state.directions.set('aft', {type: ObjectType.Direction, name: 'aft', printableName: 'aft', aliases: ['south', 'a', 's'], opposite: 'fore'})
state.directions.set('port', {type: ObjectType.Direction, name: 'port', printableName: 'port', aliases: ['west', 'p', 'w'], opposite: 'starboard'})
@ -227,11 +227,11 @@ export default class Game {
b.neighbors.set(opposite, a.name)
}
findObjectByName(name : string | undefined | null, type : ObjectType) : GameObject | null {
if(!name) return null;
findObjectsByName(name : string | undefined | null, type : ObjectType) : GameObject[] {
if(!name) return [];
if(/^the /.test(name))
return this.findObjectByName(name.replace(/^the /, ''), type)
return this.findObjectsByName(name.replace(/^the /, ''), type)
let lowerCaseName = name.toLocaleLowerCase()
@ -254,17 +254,36 @@ export default class Game {
break
}
const objects = [...collection!.values()]
const objects : GameObject[] = [...collection!.values()]
const matching = [
// Exact name
...objects.filter((object) => lowerCaseName === object.name.toLocaleLowerCase()),
// Aliases
...objects.filter(({aliases}) => aliases.map(a => a.toLocaleLowerCase()).includes(lowerCaseName))
]
// Filter duplicates
const seenDict : any = {}
return matching.filter(({name}) => {
if(seenDict[name])
return false
seenDict[name] = name
return true
})
}
findObjectByName(name : string | undefined | null, type : ObjectType) : GameObject | null {
const candidates = this.findObjectsByName(name, type)
const exactMatch = objects.find((object) => lowerCaseName === object.name.toLocaleLowerCase())
if(exactMatch)
return exactMatch
if(candidates.length < 1)
return null
const aliasMatch = objects.find(({aliases}) => aliases.map(a => a.toLocaleLowerCase()).includes(lowerCaseName))
if(aliasMatch)
return aliasMatch
if(candidates.length > 1)
console.warn(`Duplicate objects found for name ${name}`)
return null
return candidates[0]
}
findObjectsInRoom(name : string | undefined) : Draft<Item> [] {

@ -21,7 +21,8 @@ export default class Parser {
this.game.outputCommand(rawCommand)
this.game.saveDraft()
const timer = delay(200)
const timerDelay = 100
const timer = delay(timerDelay)
try {
renderer.hidePrompt()
@ -29,8 +30,9 @@ export default class Parser {
await this.runCommand(rawCommand)
let end = window.performance.now()
if(end - start > 200)
if(end - start > timerDelay)
console.warn(`Command execution took ${end - start}ms`)
await timer
} catch (err) {
await timer
@ -39,7 +41,6 @@ export default class Parser {
else if(err.message)
game.say(err.message)
else{
game.say('An unknown error occured')
console.error(err)
}
} finally {

@ -18,6 +18,7 @@ export default class Renderer {
private target : HTMLElement | null = null
private promptVisible : boolean = true
private videoSettingsSet : boolean = !!window.localStorage.getItem('video')
private gameEnded : boolean = false
constructor(parser : Parser, game : Game, rules : RulesEngine) {
this.parser = parser
@ -25,6 +26,12 @@ export default class Renderer {
this.rules = rules
}
endGame() {
this.gameEnded = true
this.promptVisible = false
this.render()
}
start(target : HTMLElement | null) {
this.target = target
this.rules.gameStart()
@ -37,7 +44,8 @@ export default class Renderer {
}
public showPrompt() {
this.promptVisible = true;
if(!this.gameEnded)
this.promptVisible = true;
this.render()
}

@ -4,11 +4,13 @@ export default [
require('./lookAt'),
require('./go'),
require('./open'),
require('./unlockDoor'),
// require('./unlockDoor'),
require('./take-drop'),
require('./inventory'),
require('./help'),
require('./options'),
require('./map'),
require('./hint')
require('./hint'),
require('./start'),
require('./take-apart')
]

@ -5,6 +5,17 @@ import { ObjectType, Door } from "../types/GameState";
import { Draft } from "immer";
export default function(parser : Parser, rules : RulesEngine, game : Game) {
parser.understand('openItem')
.as('open [item]')
.as('open [item] with [item|object]')
.as('pry open [item] with [item|object]')
.as('unlock [item] with [item|object]')
.as('use [item|object] on [item]')
rules.onCommand('openItem', () => {
game.say(`You don't believe that can be opened!`)
})
parser.understand('openDoor')
.as('open [door]')
@ -23,13 +34,4 @@ export default function(parser : Parser, rules : RulesEngine, game : Game) {
const mutable = game.findObjectByName(door.name, ObjectType.Door);
(mutable as Draft<Door>).open = true
})
parser.understand('openItem')
.as('open [item]')
.as('open [item] with [item|object]')
rules.onCommand('openItem', () => {
game.say(`You don't believe that can be opened!`)
})
}

@ -0,0 +1,14 @@
import Parser from "../Parser";
import RulesEngine from "../RulesEngine";
import Game from "../Game";
export default function(parser : Parser, rules : RulesEngine, game : Game) {
parser.understand('start')
.as('start [item]')
.as('restart [item]')
.as('turn on [item]')
rules.onCommand('start', command => {
game.say(`That isn't something you can turn on.`)
})
}

@ -0,0 +1,15 @@
import Parser from "../Parser";
import RulesEngine from "../RulesEngine";
import Game from "../Game";
export default function(parser : Parser, rules : RulesEngine, game : Game) {
parser.understand('take apart')
.as('take apart [item]')
.as('take [item] apart')
.as('disassemble [item]')
.as('dissassemble [item]')
rules.onCommand('take apart', command => {
game.say(`That isn't something you can take apart.`)
})
}

@ -75,8 +75,8 @@ export default class ParsedCommand {
let object : GameObject | null = null
for(const noun of nouns) {
let gameObject = game.findObjectByName(noun.name, noun.itemType)
if(!gameObject || (gameObject.type === ObjectType.Item && !((gameObject as Item).seen)))
let possibleObjects = game.findObjectsByName(noun.name, noun.itemType).filter(gameObject => (gameObject.type !== ObjectType.Item || ((gameObject as Item).seen)))
if(possibleObjects.length < 1)
return {
isValid: false,
command: this,
@ -84,10 +84,10 @@ export default class ParsedCommand {
severity: ParsingErrorSeverity.NoSuchObject
}
// TODO: Optionally print "the" depending on if the original
// command name had one at the beginning (don't print the the book)
// (but also don't do "you cannot see heart of the cards")
if(!game.isVisible(gameObject))
let visibleObjects = possibleObjects.filter(gameObject => game.isVisible(gameObject))
if(visibleObjects.length < 1) {
const gameObject = possibleObjects[0]
return {
isValid: false,
command: this,
@ -98,7 +98,9 @@ export default class ParsedCommand {
})()}`,
severity: ParsingErrorSeverity.NotVisible
}
}
const gameObject = visibleObjects[0]
if(noun.sentencePosition === NounPosition.Subject)
subject = gameObject
else

@ -1,17 +1,19 @@
import {game} from '../engine'
/**
* Cabin
*/
game.addRoom('cabin', 'Crew Cabin', `
A dark and dingy room with a single bunk bed along the starboard side.
A dark and dingy room with a single bunk bed along the starboard side, and a few
piles of discarded clothes strewn about the room.
The washroom is to the aft, with the comms room to port.
`)
/**
* Washroom
*/
const cabinDoor = game.addItem('cabin door', `
Large, metal retractable doors designed to keep air from escaping through them.
`, 'cabin')
cabinDoor.aliases = ['security doors', 'door', 'doors']
cabinDoor.printableName = 'security doors'
game.addRoom('bathroom', 'Washroom', `
Tight, cramped, but serviceable. The _Dawn_ was really only meant for a crew
of one or two, and that is no more evident than here.
@ -30,10 +32,6 @@ cupboard.aliases.push('cupboard')
cupboard.aliases.push('cupboard door')
cupboard.aliases.push('cabinet door')
/**
* Comms
*/
game.addRoom('comms', 'Comms Room', `
A wide room with pipes and cabling running thick through the floor.
@ -41,6 +39,13 @@ The bridge is to the fore, the medbay to port, and the crew cabin to starboard.
There is a large storage locker on the aft wall of the room.
`)
game.addItem('locker', `
Recessed into the aft wall, you use this locker to store various odds and
ends. Spare parts, cleaning supplies, etc.
It is locked.
`, 'comms')
game.addRoom('bridge', 'Bridge', `
A lone chair sits in the center of the room, surrounded with computer
consoles and flight instruments. Nowadays most aspects of spaceflight are
@ -58,6 +63,22 @@ event it would still save your life . . . at least you hope it would.
The stairwell is to the aft, with the comms room to starboard.
`)
const medDoor = game.addItem('med door', `
Large, metal retractable doors designed to keep air from escaping through them.
`, 'medbay')
medDoor.aliases = ['security doors', 'door', 'doors']
medDoor.printableName = 'security doors'
const filter = game.addItem('filter', `
Largest of the medical bay equipment, the CO<sub>2</sub> filter looks sort of
like a large air conditioning unit. It is currently switched off.
`, 'medbay')
filter.aliases = ['co2 filter', 'co2', 'life support', 'scrubber']
filter.printableName = 'CO<sub>2</sub> filter'
game.addRoom('stairupper', 'Upper Stairwell', `
A large window in the aft wall shows the view of an unknown star system
in the distance. It's almost peaceful to gaze at, if it didn't remind
@ -75,7 +96,6 @@ The mainframe is to the fore, and above you the stairs curl up and
out of sight.
`)
game.addRoom('mainframe', 'Mainframe', `
The mainframe fills the room with its soft humming, lights blinking
on and off in a disorderly pattern.
@ -83,6 +103,17 @@ on and off in a disorderly pattern.
The stairwell is to the aft, with the engine room to starboard.
`)
const computer = game.addItem('mainframe', `
Filling almost the entire room, the mainframe serves as the ship's primary control
computer. It regulates engine power, controls the navigation and piloting systems,
and even has a few games built in!
Of course, with the engine not running the mainframe is currently in low power mode - you'll have to start the engine up again to restore full functionality.
`, 'mainframe')
computer.aliases.push('computer')
computer.aliases.push('engine controller')
computer.aliases.push('control circuit')
game.addRoom('engine', 'Engine Room', `
The usual deep rumble of the ship's engines is missing, leaving a
disconcerting silence.
@ -90,6 +121,21 @@ disconcerting silence.
The mainframe is to port, with the docking bay to starboard.
`)
game.addItem('engine', `
Even running on standby power, your view through the window of the engine is overwhelmingly bright.
At full power the windows have to be polarized to block the blinding glow of the engine's warp singularity - as it is, it's relatively safe to look at.
`, 'engine')
const chair = game.addItem('chair', `
An odd three-legged construction, made of one large beam curving all the way from the
back of the chair to the floor, with two side legs affixed at the sides. Your
experience has shown that this model of chair is **very** sturdy.
`, 'engine')
chair.carryable = true
game.addRoom('docking', 'Docking Bay', `
A long and wide room for loading and unloading cargo. The
dock hatch is sealed, and you definitely shouldn't go opening
@ -107,8 +153,8 @@ game.setNeighbor('stairupper', 'down', 'stairlower')
game.setNeighbor('stairlower', 'fore', 'mainframe')
game.setNeighbor('mainframe', 'starboard', 'engine')
game.setNeighbor('engine', 'starboard', 'docking')
game.setNeighbor('cabin', 'port', 'comms')
game.setNeighbor('medbay', 'starboard', 'comms')
// DEBUG hallways
// game.setNeighbor('bathroom', 'down', 'docking')
// game.setNeighbor('cabin', 'port', 'comms')
// game.setNeighbor('comms', 'port', 'medbay')

@ -22,26 +22,25 @@ export enum Phase {
unlockedLocker
}
// TODO: Replace [thing]
export const hints : Map<Phase, string> = new Map()
hints.set(Phase.wakeUp, 'You may be able to assess your situation better if you find a light.')
hints.set(Phase.hasFlashlight, 'With the security door shut and power cut off, you\'ll have to find another way into the rest of the ship.')
hints.set(Phase.checkedUnderSink, `There is a panel under the sink that you might be able to fit through - you'll need a wrench to get it open though.`)
hints.set(Phase.gotWrench, `You have a wrench and can open the panel under the sink.`)
hints.set(Phase.openedSinkPanel, 'You can get to the lower deck through the panel under the sink, but be sure not to leave anything behind!')
hints.set(Phase.droppedBelow, 'You need to re-start the CO2 scrubber before you run out of clean air.')
hints.set(Phase.droppedBelow, 'You need to re-start the CO<sub>2</sub> scrubber before you run out of clean air.')
hints.set(Phase.fixedLifeSupport, 'While the immediate threat to your life has been solved, you need to bring the engine back on so you can restore power to your ship.')
hints.set(Phase.examinedEngine, 'The engine itself seems to be in good repair, time to go to the mainframe and start up its control systems.')
hints.set(Phase.examinedMainframe, 'The engine control systems are missing [thing]. There\'s a spare in the comm room locker, but you\'ll have to find a way to get there.')
hints.set(Phase.examinedDoor, 'You need to find a way into the comms room to retrieve [thing] - the door looks like it could be pried open with enough leverage.')
hints.set(Phase.examinedMainframe, 'The engine control systems are missing a capacitor. There\'s a spare in the comm room locker, but you\'ll have to find a way to get there.')
hints.set(Phase.examinedDoor, 'You need to find a way into the comms room to retrieve the capacitor for your engine controls - the door looks like it could be pried open with enough leverage.')
hints.set(Phase.examinedChair, 'The chair looks sturdy enough to work as a lever to get in the door, but it will have to be disassembled first.')
hints.set(Phase.destroyedChair, 'You have a bar that should be strong enough to open the door to the comms room - go retrieve the [thing] so you can start the engine again!')
hints.set(Phase.openedDoor, 'You found a way into the comms room - retrieve the [thing] from the comms room locker so you can restart the engine.')
hints.set(Phase.destroyedChair, 'You have a bar that should be strong enough to open the door to the comms room - go retrieve the the capacitor so you can start the engine again!')
hints.set(Phase.openedDoor, 'You found a way into the comms room - retrieve the capacitor from the comms room locker so you can restart the engine.')
hints.set(Phase.examinedLocker, 'Someone locked the comms room locker. There\'s a spare key in your overalls - they\'re back in your cabin.')
hints.set(Phase.examinedHoleCannotGetUp, 'You can\'t reach up into the bathroom any more - you\'ll have to find something else to use to climb up')
hints.set(Phase.hasNewChair, 'You found another chair you can use to reach the bathroom - go get the spare locker key from your cabin.')
hints.set(Phase.returnedUpToBathroom, 'Someone locked the comms room locker. There\'s a spare key in your overalls - they\'re back in your cabin.')
hints.set(Phase.hasKey, `You've retrieved the spare key to the comms locker, and can finally get the [thing] to repair the mainframe.`)
hints.set(Phase.hasKey, `You've retrieved the spare key to the comms locker, and can finally get the capacitor to repair the mainframe.`)
hints.set(Phase.unlockedLocker, 'Locker is empty - whoever was in your ship made sure you wouldn\'t be able to repair it.')
setImmediate(() => {

@ -1,4 +1,4 @@
import {game, rules} from '../engine'
import {game, rules, parser, renderer} from '../engine'
import { ObjectType, Item } from '../engine/types/GameState'
import { Draft } from 'immer'
import { Phase } from './2-phases-and-hints'
@ -15,6 +15,67 @@ rules.on('beforePrintItems', () => {
throw new Error('You cannot make out much more without light.')
})
/**
* Do not allow going west from cabin
*/
rules.onBeforeCommand(command => {
const playerLocation = game.getCurrentRoom()?.name
if(command.verb.name !== 'go' || playerLocation !== 'cabin' || command.subject?.name !== 'port')
return;
throw new Error(`The security doors have sealed - you're either going to need to restart the mainframe or find a way to force these open before you can access the comms room.`)
})
/**
* Do not allow going east from comms
*/
rules.onBeforeCommand(command => {
const playerLocation = game.getCurrentRoom()?.name
if(command.verb.name !== 'go' || playerLocation !== 'comms' || command.subject?.name !== 'starboard')
return;
throw new Error(`The security doors have sealed - you're either going to need to restart the mainframe or find a way to force these open before you can access the comms room.`)
})
/**
* Do not allow going east from medbay until opened
*/
rules.onBeforeCommand(command => {
const playerLocation = game.getCurrentRoom()?.name
if(command.verb.name !== 'go' || playerLocation !== 'medbay' || command.subject?.name !== 'starboard')
return;
if((game.findObjectByName('chair leg', ObjectType.Item) as Item)?.location === 'inventory') {
try {
parser.runCommand(`open door with chair leg`)
} catch {
game.pause()
game.clear()
}
}
if(game.getProperty('gamePhase') < Phase.openedDoor)
throw new Error(`The security doors have sealed - you're either going to need to restart the mainframe or find a way to force these open before you can access the comms room.`)
})
/**
* Do not allow opening security doors
*/
rules.onBeforeCommand(command => {
const playerLocation = game.getCurrentRoom()?.name
if(command.verb.name !== 'openItem' || !command.subject?.aliases.includes('security doors') || command.object !== null)
return;
if(playerLocation === 'cabin')
parser.runCommand('go port')
if(playerLocation === 'medbay')
parser.runCommand('go starboard')
// Do not print regular command output
throw new Error()
})
/**
* Update hint after getting the flashlight
*/
@ -52,6 +113,17 @@ rules.onBeforeCommand(command => {
throw new Error(`If you put down the flashlight, you will likely not be able to find it again in the dark.`)
})
/**
* If we get "take apart" cabinet, try opening it instead
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'take apart') return;
if(command.subject?.name === 'cabinet' || command.subject?.name === 'floor panel'){
parser.runCommand(`open ${command.subject?.name}`)
throw new Error()
}
})
/**
* When opening the cabinet, insert floor panel
@ -102,3 +174,239 @@ rules.onBeforeCommand(command => {
throw new Error('It takes you a few minutes, but eventually you pull up the floor panel. You should be able to get down to the docking bay from here.')
})
/**
* Add to engine description when CO2 not fixed
*/
rules.onAfterCommand(command => {
if(command.verb.name !== 'lookAt' || command.subject?.name !== 'engine') return;
if(game.getProperty('gamePhase') < Phase.fixedLifeSupport)
game.say(`_Focus_, you remind yourself. _The engine is pretty but I've gotta fix that CO<sub>2</sub> filter before I'll have time to bother with this._`)
})
/**
* Add to mainframe description when CO2 not fixed
*/
rules.onAfterCommand(command => {
if(command.verb.name !== 'lookAt' || command.subject?.name !== 'mainframe') return;
if(game.getProperty('gamePhase') < Phase.fixedLifeSupport)
game.say(`Luckily the CO<sub>2</sub> filter operates independently to the rest of the ship's systems, so you can worry about that fixing first and then come back to the mainframe.`)
})
/**
* Turn on flashlight
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'start' || command.subject?.name !== 'flashlight') return;
const light = game.findObjectByName('flashlight', ObjectType.Item) as Item
if(light.location === 'inventory')
throw new Error('It is already on')
else{
parser.runCommand(`take ${light.name}`)
throw new Error()
}
})
/**
* Cannot turn on engine
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'start' || command.subject?.name !== 'engine') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase < Phase.fixedLifeSupport)
throw new Error(`You probably should restart the CO<sub>2</sub> filter before worrying about the engine.`)
if(currentPhase < Phase.examinedMainframe){
game.setProperty('gamePhase', Phase.examinedEngine)
throw new Error(`As far as you can tell the engine _itself_ is fine, perhaps something is wrong with the mainframe's control systems?`)
}
throw new Error(`The mainframe's engine control systems have been damaged and will have to be repaired before the engine can be started.`)
})
/**
* Cannot start mainframe
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'start' || command.subject?.name !== 'mainframe') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase < Phase.fixedLifeSupport)
throw new Error(`You probably should restart the CO<sub>2</sub> filter before worrying about the mainframe.`)
if(currentPhase < Phase.examinedMainframe){
parser.runCommand(`take apart mainframe`)
throw new Error()
}
throw new Error(`The mainframe's engine control system is damaged, and will have to be repaired before you can bring it online. You believe you have a replacement part in the comms room.`)
})
/**
* Cannot start filter again
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'start' || command.subject?.name !== 'filter') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase >= Phase.fixedLifeSupport)
throw new Error(`The CO<sub>2</sub> filter is already running.`)
})
/**
* Taking apart engine: error saying that's dangerous, and not something you can do without a drydock
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'take apart' || command.subject?.name !== 'engine') return;
throw new Error(`Even in non-emergency conditions, the engine is not something you can safely service without bringing the _Dawn_ down at a drydock.`)
})
/**
* Taking apart mainframe: Explain damaged part - after that, say you need the part
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'take apart' || command.subject?.name !== 'mainframe') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase < Phase.fixedLifeSupport)
throw new Error(`You probably should restart the CO<sub>2</sub> filter before worrying about the mainframe.`)
if(currentPhase >= Phase.examinedMainframe)
throw new Error(`You won't be able to repair the mainframe's control system without the replacement capacitor from the comms room.`)
})
/**
* Hint for examining door
*/
rules.onAfterCommand(command => {
if(!(((command.verb.name === 'lookAt' || command.verb.name === 'openDoor')) && (command.subject?.name === 'med door' || command.subject?.name === 'cabin door')))
return;
const currentPhase = game.getProperty('gamePhase')
if(Phase.examinedMainframe <= currentPhase && currentPhase < Phase.examinedDoor)
game.setProperty('gamePhase', Phase.examinedDoor)
})
/**
* Hint for examining chair
*/
rules.onAfterCommand(command => {
if(!(command.verb.name === 'lookAt' && command.subject?.name === 'chair'))
return;
const currentPhase = game.getProperty('gamePhase')
if(Phase.examinedMainframe <= currentPhase && currentPhase < Phase.examinedChair){
game.setProperty('gamePhase', Phase.examinedChair)
}
})
/**
* Taking apart chair
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'take apart' || command.subject?.name !== 'chair') return
const wrench = game.findObjectByName('wrench', ObjectType.Item) as Draft<Item>
if(wrench?.location !== 'inventory')
throw new Error(`You do not have the proper tools to take this apart.`)
const currentPhase = game.getProperty('gamePhase')
if(currentPhase < Phase.fixedLifeSupport)
throw new Error(`You should take care of the CO<sub>2</sub> scrubber before you get distracted taking things apart.`)
if(currentPhase < Phase.destroyedChair){
if(game.hasProperty('examinedMainframe')){
game.say(`The chair's center leg should give you enough leverage to force open the security door, but as you finish taking it apart you remember something that bothers you: if you're not mistaken, that regulator board was brand new. Even when they've gone bad in the past you've usually been able to get a good week or two of use out of them before they failed completely, so seeing one fail this early is definitely unusual.`)
game.say(`You can't remember for sure if that board was replaced in the last refit though, and you'd have to check the work log Wren gave you to be sure. You try to push that idea aside for now - no sense in worrying about it now.`)
} else {
game.say(`Twisting the bolts out of place, you are soon left with one long metal rod, and a small pile of miscelaneous pieces.`)
}
game.setProperty('gamePhase', Phase.destroyedChair)
const leg = game.addItem('chair leg', 'A sturdy, curved piece of metal about a meter and a half long.', 'inventory')
leg.aliases = ['leg', 'prybar', 'stick', 'rod', 'piece of metal']
leg.seen = true
const chair = game.findObjectByName('chair', ObjectType.Item) as Draft<Item>
chair.location = 'bridge'
chair.description += `\n\nHopefully you won't have to destroy this one - they're actually rather expensive.`
chair.lastKnownLocation = undefined
game.say(`(You place the chair leg in your inventory)`)
throw new Error()
}
})
/**
* Prevent going back up if chair is not in docking room
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'go' || command.subject?.name !== 'up' || game.getCurrentRoom()?.name !== 'docking')
return
const chair = game.findObjectByName('chair', ObjectType.Item) as Draft<Item>
if(chair.location === 'inventory'){
game.say(`(First putting the chair down)`)
chair.location = game.getCurrentRoom()!.name
}
if(chair.location !== game.getCurrentRoom()!.name) {
throw new Error(`You cannot reach the hole in the ceiling - you might need to find something to stand on.`)
}
game.say(`Climbing on the chair, you just barely manage to reach up to the hole in the ceiling.`)
})
/**
* Open door with leg
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'openItem' || command.subject?.name !== 'med door' || game.getCurrentRoom()?.name !== 'medbay')
return
game.say(`With an uncomfortable grinding noise, and much effort, the security doors slide open allowing you access to the comms room.`)
game.say(`Unfortunately it looks like your chair leg got caught in the mechanism - the good news is this door will stay open, the bad news is you're not getting that back.`)
game.getState().items.delete('med door')
game.getState().items.delete('chair leg')
game.setProperty('gamePhase', Phase.openedDoor)
throw new Error()
})
/**
* Prevent locker from opening
*/
rules.onBeforeCommand(command => {
if(command.verb.name !== 'openItem' || command.subject?.name !== 'locker')
return
if(game.getProperty('gamePhase') === Phase.hasKey) {
renderer.endGame()
setImmediate(() => rules.emit('gameEnd'))
throw new Error()
}
game.say(`
The locker door rattles but will not open.
"That's odd," you say, "I don't remember locking this..." but luckily you have
a spare key. You think for a moment and remember the key is in your other
pants - those should be back in your cabin.
`)
if(game.getProperty('gamePhase') < Phase.examinedLocker)
game.setProperty('gamePhase', Phase.examinedLocker)
throw new Error()
})

@ -1,5 +1,7 @@
import {game, rules} from '../engine'
import { Phase } from './2-phases-and-hints'
import { ObjectType, Item } from '../engine/types/GameState'
import { Draft } from 'immer'
/**
* Intro
@ -31,7 +33,7 @@ rules.onAfterCommand(command => {
game.clear()
game.say(`You click the light on and glance around, sighing. The Dawn has certainly seen better days. She flies just fine, but assuming you survive you'll definitely have to use the money from this job to finally replace all the rusty patches with new parts.`)
game.say(`You click the light on and glance around, sighing. The _Dawn_ has certainly seen better days. She flies just fine, but assuming you survive you'll definitely have to use the money from this job to finally replace all the rusty patches with new parts.`)
game.say(`It figures that the easy delivery would turn out to be the one where you have the most problems. All you had to do was take a few sealed boxes to the far side of the galaxy and then fly back - no sneaking, no lying, no fighting or anything!`)
game.pause()
game.clear()
@ -58,13 +60,13 @@ rules.onAfterCommand(command => {
game.clear()
game.say(`You suppose this kind of equipment failure is inevitable, although it's a bit strange since you had the engine checked last week. It was the same cousin who checked the engine as who told you about this job actually.`)
game.say(`"Listen, I know you don't want to get involved in the synth trade but it's just one supply run and they can pay you a lot for it." Wren had said. "I'd do it myself but the Aurora's a flying collection of scrap at this point, and you know how I hate having to find new pilots."`)
game.say(`"Listen, I know you don't want to get involved in the synth trade but it's just one supply run and they can pay you a lot for it." Wren had said. "I'd do it myself but the _Aurora's_ a flying collection of scrap at this point, and you know how I hate having to find new pilots."`)
game.pause()
game.clear()
game.say(`You had groaned at that. "And you think the Ashen Dawn is in any better condition? That's why I brought her to you, numbskull."`)
game.say(`Wren's grin was wide, but knowing. "Listen, after I fix her up let me introduce you to my client Rosalyn - it's a simple supply run, and would be a good shakedown for Dawn after the repairs."`)
game.say(`You had groaned at that. "And you think the _Ashen Dawn_ is in any better condition? That's why I brought her to you, numbskull."`)
game.say(`Wren's grin was wide, but knowing. "Listen, after I fix her up let me introduce you to my client Rosalyn - it's a simple supply run, and would be a good shakedown for _Dawn_ after the repairs."`)
game.pause()
game.clear()
@ -81,8 +83,7 @@ rules.onAfterCommand(command => {
* Dropping from bathroom to docking bay for the first time
*/
rules.onAfterCommand(command => {
// When going down from the bathroom . . .
if(command.verb.name !== 'go' || command.subject?.name !== 'down' || game.getCurrentRoom()?.name !== 'bathroom')
if(command.verb.name !== 'go' || command.subject?.name !== 'down' || game.getCurrentRoom()?.name !== 'docking')
return
// Only do once
@ -95,9 +96,147 @@ rules.onAfterCommand(command => {
game.say(`As you lower yourself down into the cargo bay you're struck by how empty it looks. Normally this storage bay would have a lot more general-purpose supplies, but you had to clear it out to make room for this delivery. Since you've dropped the cargo and were on your way to get paid, they're no longer here to fill that space.`)
game.pause()
game.clear()
game.say(`You weren't permitted to know what was in the boxes you delivered. "Wares to be sold in the outer markets," Rosalyn said when you asked. "The less you know the less you have to worry." `)
game.say(`You weren't even sure how heavy or light the boxes were - she arranged crews for loading and unloading ahead of time, and the time requirements for delivery were just close enough you hadn't dared stop to investigate along the way. It really was as easy as Wren had said it would be...`)
game.pause()
game.clear()
rules.printArea()
})
/**
* Entering lower stairwell for first time
*/
rules.onAfterCommand(command => {
if(command.verb.name !== 'go' || command.subject?.name !== 'aft' || game.getCurrentRoom()?.name !== 'stairlower')
return
// Only do once
if(game.hasProperty('printedLowerStairwell'))
return
else
game.createProperty('printedLowerStairwell', true)
game.say(`As you look up the stairway you catch a glimpse of light filtering down from above - must be near a star cluster you suppose. Not really any good way to know which one until you can bring the navigation systems back online.`)
})
/**
* Starting CO2 filter
*/
rules.onAfterCommand(command => {
if(command.verb.name !== 'start' || command.subject?.name !== 'filter') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase >= Phase.fixedLifeSupport)
throw new Error(`The CO<sub>2</sub> filter is already running.`)
const filter = game.findObjectByName('filter', ObjectType.Item) as Draft<Item>
filter.description = filter.description?.replace('switched off', 'running')
game.setProperty('gamePhase', Phase.fixedLifeSupport)
game.clear()
game.say(`Okay, with the CO2 scrubber running again, the filter should last for at least a week - plenty of time to get the _Dawn_ back to a spaceport. If the engine issue isn't that bad, maybe she'll last you all the way back to Earth so you can collect your paycheck and do a proper diagnostic.`)
game.say(`Speaking of which, examining the engine is probably the next order of business - if it's gonna keep dropping out of hyperspace on you then this is going to be a long trip home.`)
game.pause()
game.clear()
rules.printArea()
})
rules.onAfterCommand(command => {
if(command.verb.name !== 'take apart' || command.subject?.name !== 'mainframe') return;
const currentPhase = game.getProperty('gamePhase')
if(currentPhase < Phase.fixedLifeSupport)
throw new Error(`You really should worry about the CO<sub>2</sub> filter before doing anything with the mainframe.`)
if(currentPhase >= Phase.examinedMainframe)
throw new Error(`You've already taken the mainframe pretty well apart - you need that replacement capacitor.`)
game.setProperty('gamePhase', Phase.examinedMainframe)
const mainframe = game.findObjectByName('mainframe', ObjectType.Item) as Draft<Item>
mainframe.description = mainframe.description?.replace(/Of course.*/, `While the bulk of the mainframe is still there, you've currently taken apart the engine control systems. Looks like there's a faulty capacitor in the engine regulator board, and you'll have to get a replacement from the comms room locker.`)
if(!game.hasProperty('examinedMainframe'))
game.createProperty('examinedMainframe', true)
game.clear()
game.say(`At first glance the mainframe is working as expected - you can bring up the internal atmospheric readings, the operation logs, and anything else that doesn't need engine power. The mainframe even kindly informs you there was some sort of anomalous power output from the engine before it cut from hyperjump, but when you try to bring the engine back online you hear a loud ***pop*** from behind the terminal and start to smell smoke.`)
game.pause()
game.clear()
game.say(`Upon further inspection, it looks like there's a faulty capacitor in the engine regulator board. That would explain the power anomalies - these sort of caps always act a bit weird before they blow completely.`)
game.say(`The _Dawn_ is old enough (and this issue is common with her model of ship) so you luckily have a spare capacitor or two in the comms room locker - although finding a way through the security doors might be a bit tricky.`)
game.pause()
game.clear()
rules.printArea()
})
rules.onBeforeCommand(command => {
if(command.verb.name !== 'go' || command.subject?.name !== 'fore' || game.getCurrentRoom()?.name !== 'mainframe')
return;
if(game.getProperty('gamePhase') < Phase.examinedLocker)
return;
game.say(`As you work your way back to your cabin you start to get a growing sense of unease.`)
game.say(`You don't want to believe it, but something is definitely suspicious about this engine failure - the part was brand new, and now a locker you've never locked in your life is shut tight.`)
game.pause()
game.clear()
game.say(`You don't think about it. Rosalyn and the rest of her synth trade clients would definitely be willing to sabotage your ship, but they wouldn't have known enough to pull it off that quickly they had Wren's help.`)
game.say(`"Come on, Wren," you say to yourself, "don't let me down. The spare will be where I left it, and everything will be all right."`)
game.pause()
game.clear()
})
/**
* Give player key after entering cabin with the proper game phase
*/
rules.onAfterCommand(command => {
if(command.verb.name !== 'go' || command.subject?.name !== 'fore' || game.getCurrentRoom()?.name !== 'cabin')
return;
if(game.getProperty('gamePhase') < Phase.examinedLocker)
return;
game.say(`Upon entering your cabin you quickly find your pants and retrieve the key.`)
const key = game.addItem('key', 'A small steel locker key - cheap, but easier than picking the lock every time.', 'inventory')
key.seen = true
game.say(`(You place the key in your inventory)`)
game.setProperty('gamePhase', Phase.hasKey)
})
/**
* Triggers on opening the locker
*/
rules.on('gameEnd', () => {
game.clear()
game.say(`The key turns in the lock with a sharp ***click***. As you swing open the locker your heart drops.`)
game.say(`"No Wren," you mutter, "you didn't!" But the empty locker before you is evidence that he did.`)
game.pause()
game.clear()
game.say(`It figures that the easy job was too good to be true.`)
game.pause()
game.clear()
game.say(`<div style="margin-top: 122px; text-align: center">[GAME OVER]</div>`)
game.say(`<div style="text-align:center; opacity: .6">(Reload the page to play again)</div>`)
})

@ -3,7 +3,6 @@ import Game from "../engine/Game"
export function printLocDescription(game : Game) {
const location = game.getCurrentRoom()!
console.log('printing')
game.say(`**${capitalize(location.printableName)}**`)
game.say(location.description!)

Loading…
Cancel
Save