mirror of
https://gitlab.com/Oreolek/black_phone.git
synced 2024-06-28 21:05:09 +03:00
Salet conversion WIP
This commit is contained in:
parent
f497b78e27
commit
074f78d1ab
|
@ -10,25 +10,23 @@ sass = require('gulp-sass')
|
|||
uglify = require('gulp-uglify')
|
||||
buffer = require('vinyl-buffer')
|
||||
zip = require('gulp-zip')
|
||||
_ = require('lodash')
|
||||
concat = require('gulp-concat')
|
||||
|
||||
reload = browserSync.reload
|
||||
|
||||
html = (target) ->
|
||||
return () ->
|
||||
return gulp.src(['html/index.html','html/en.html'])
|
||||
.pipe(gulp.dest(target));
|
||||
return gulp.src(['html/index.html']).pipe(gulp.dest(target))
|
||||
|
||||
# Images
|
||||
img = (target) ->
|
||||
return () ->
|
||||
return gulp.src(['img/*.png', 'img/*.jpeg', 'img/*.jpg'])
|
||||
.pipe(gulp.dest(target));
|
||||
return gulp.src(['img/*.png', 'img/*.jpeg', 'img/*.jpg']).pipe(gulp.dest(target))
|
||||
|
||||
# Audio assets
|
||||
audio = (target) ->
|
||||
return () ->
|
||||
return gulp.src(['audio/*.mp3'])
|
||||
.pipe(gulp.dest(target));
|
||||
return gulp.src(['audio/*.mp3']).pipe(gulp.dest(target))
|
||||
|
||||
gulp.task('html', html('./build'))
|
||||
gulp.task('img', img('./build/img'))
|
||||
|
@ -37,21 +35,14 @@ gulp.task('audio', audio('./build/audio'))
|
|||
gulp.task('sass', () ->
|
||||
gulp.src('sass/main.scss')
|
||||
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
|
||||
.pipe(gulp.dest('./build/css'));
|
||||
.pipe(gulp.dest('./build/css'))
|
||||
)
|
||||
|
||||
sources = [
|
||||
'./game/begin.coffee',
|
||||
'./game/story.coffee',
|
||||
'./game/end.coffee',
|
||||
]
|
||||
|
||||
opts = _.assign({}, watchify.args, {
|
||||
bundler = watchify(browserify({
|
||||
entries: ["./build/game/main.coffee"]
|
||||
debug: true
|
||||
transform: [coffeify]
|
||||
});
|
||||
bundler = watchify(browserify(opts))
|
||||
}))
|
||||
|
||||
bundle = () ->
|
||||
return bundler.bundle()
|
||||
|
@ -60,15 +51,16 @@ bundle = () ->
|
|||
.pipe(gulp.dest('./build/game'))
|
||||
|
||||
gulp.task('concatCoffee', () ->
|
||||
return gulp.src(sources)
|
||||
.pipe(concat('./main.coffee'))
|
||||
.pipe(gulp.dest('./build/game'))
|
||||
return gulp.src([
|
||||
'./game/begin.coffee',
|
||||
'./game/story.coffee',
|
||||
]).pipe(concat('./main.coffee')).pipe(gulp.dest('./build/game'))
|
||||
)
|
||||
|
||||
gulp.task('coffee', ['concatCoffee'], bundle)
|
||||
|
||||
bundler.on('update', bundle);
|
||||
bundler.on('log', gutil.log);
|
||||
bundler.on('update', bundle)
|
||||
bundler.on('log', gutil.log)
|
||||
|
||||
gulp.task('build', ['html', 'img', 'sass', 'coffee', 'audio'])
|
||||
|
||||
|
@ -85,7 +77,7 @@ gulp.task('serve', ['build'], () ->
|
|||
gulp.watch(['./html/*.html'], ['html'])
|
||||
gulp.watch(['./sass/*.scss'], ['sass'])
|
||||
gulp.watch(['./img/*.png', './img/*.jpeg', './img/*.jpg'], ['img'])
|
||||
gulp.watch(['./lib/*.coffee', './lib/*.js', './game/*.coffee'], ['coffee'])
|
||||
gulp.watch(['./game/*.coffee'], ['coffee']);
|
||||
|
||||
gulp.watch(['./build/css/main.css'], sassListener)
|
||||
gulp.watch(
|
||||
|
@ -93,25 +85,25 @@ gulp.task('serve', ['build'], () ->
|
|||
browserSync.reload)
|
||||
)
|
||||
|
||||
gulp.task('html-dist', html('./dist'));
|
||||
gulp.task('img-dist', img('./dist/img'));
|
||||
gulp.task('audio-dist', audio('./dist/audio'));
|
||||
gulp.task('html-dist', html('./dist'))
|
||||
gulp.task('img-dist', img('./dist/img'))
|
||||
gulp.task('audio-dist', audio('./dist/audio'))
|
||||
gulp.task('legal-dist', () ->
|
||||
return gulp.src(['LICENSE.txt'])
|
||||
.pipe(gulp.dest("./dist"));
|
||||
);
|
||||
.pipe(gulp.dest("./dist"))
|
||||
)
|
||||
|
||||
gulp.task('sass-dist', () ->
|
||||
return gulp.src('./sass/main.scss')
|
||||
.pipe(sass({outputStyle: 'compressed'}))
|
||||
.pipe(gulp.dest('./dist/css'));
|
||||
);
|
||||
.pipe(gulp.dest('./dist/css'))
|
||||
)
|
||||
|
||||
distBundler = browserify({
|
||||
debug: false,
|
||||
entries: ['./build/game/main.coffee'],
|
||||
transform: ['coffeeify']
|
||||
});
|
||||
})
|
||||
|
||||
gulp.task('coffee-dist', ['concatCoffee'], () ->
|
||||
return distBundler.bundle()
|
||||
|
@ -119,13 +111,20 @@ gulp.task('coffee-dist', ['concatCoffee'], () ->
|
|||
.pipe(buffer())
|
||||
.pipe(uglify())
|
||||
.on('error', gutil.log)
|
||||
.pipe(gulp.dest('./dist/game'));
|
||||
);
|
||||
.pipe(gulp.dest('./dist/game'))
|
||||
)
|
||||
|
||||
gulp.task('dist', ['html-dist', 'img-dist', 'sass-dist', 'coffee-dist', 'audio-dist', 'legal-dist']);
|
||||
gulp.task('dist', [
|
||||
'html-dist',
|
||||
'img-dist',
|
||||
'sass-dist',
|
||||
'coffee-dist',
|
||||
'audio-dist',
|
||||
'legal-dist'
|
||||
])
|
||||
|
||||
gulp.task('zip', ['dist'], () ->
|
||||
return gulp.src('dist/**')
|
||||
.pipe(zip('dist.zip'))
|
||||
.pipe(gulp.dest('.'));
|
||||
);
|
||||
.pipe(gulp.dest('.'))
|
||||
)
|
||||
|
|
28
README.md
28
README.md
|
@ -2,31 +2,3 @@
|
|||
### A walk through someone else's apartment
|
||||
|
||||
*Provided under the terms of MIT license, see LICENSE.txt*
|
||||
|
||||
#### Hacks and whistles
|
||||
|
||||
"Black phone" boasts a new technical feature: additional room exits.
|
||||
|
||||
This is something I greatly missed coming from INSTEAD.
|
||||
I had to hack Undum and expose ("export") its `processClick` function so I could make an additional interface block.
|
||||
|
||||
The technical side is simple: my [Undum-commonjs](https://github.com/oreolek/undum-commonjs) fork exports `processClick` function.
|
||||
Every situation that is a room (there are non-room situations, mind) has an array of `ways`.
|
||||
This array has every other room this one connects to.
|
||||
On entering a room, the `before` function calls for `update_ways` function that reads `ways` array and updates the UI.
|
||||
It's not automatic but the game is small enough for this to work.
|
||||
|
||||
Undum has a bug: every "once" link becomes clickable again when you visit the same situation.
|
||||
For example, you can save the game, load it again and click everything the second time.
|
||||
|
||||
The game exploits this because you can visit any room again and examine everything you missed.
|
||||
|
||||
There is also a slightly debatable UI hack: as every link is "once" link (it gets deactivated once it's called),
|
||||
I've sped up every click by 250ms or so like this:
|
||||
|
||||
document.onmousedown = (e) ->
|
||||
e.target.click()
|
||||
|
||||
A "click" in Javascript is an event of clicking the mouse button and releasing it.
|
||||
The "mousedown" event is just clicking the button, not waiting for the release.
|
||||
I didn't bother with intricacies, so the right mouse click is treated the same as the left one.
|
||||
|
|
|
@ -2,73 +2,78 @@
|
|||
# This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
# To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0
|
||||
|
||||
markdown = require('../../lib/markdown.coffee')
|
||||
room = require("../../lib/room.coffee")
|
||||
obj = require('../../lib/obj.coffee')
|
||||
dialogue = require('../../lib/dialogue.coffee')
|
||||
oneOf = require('../../lib/oneOf.coffee')
|
||||
require('../../lib/interface.coffee')
|
||||
undum = require('../../lib/undum.js')
|
||||
Salet = require('../../lib/salet.coffee')
|
||||
|
||||
undum.game.id = "6a9909a4-586a-4089-bd18-26da684d1c8d"
|
||||
undum.game.version = "2.0"
|
||||
salet = new Salet
|
||||
salet.view.init(salet)
|
||||
salet.game_id = "6a9909a4-586a-4089-bd18-26da684d1c8d"
|
||||
salet.game_version = "2.0"
|
||||
salet.init = () ->
|
||||
_paq.push(['setCustomDimension', 1, false])
|
||||
|
||||
@character.view_smash = 1
|
||||
@character.money = 0
|
||||
@character.code = ""
|
||||
@character.knows_the_code = 0
|
||||
@character.box_opened = 0
|
||||
$(document).ready(() ->
|
||||
salet.beginGame()
|
||||
)
|
||||
|
||||
way_to = (content, ref) ->
|
||||
return "<a href='#{ref}' class='way'>#{content}</a>"
|
||||
textlink = (content, ref) ->
|
||||
return "<a href='./_writer_#{ref}' class='once'>#{content}</a>"
|
||||
actlink = (content, ref) ->
|
||||
return "<a href='./#{ref}' class='once'>#{content}</a>"
|
||||
textcycle = (content, ref) ->
|
||||
return "<a href='./_replacer_#{ref}' class='cycle' id='#{ref}'>#{content}</a>"# usage: writemd( system, "Text to write")
|
||||
objlink = (content, ref) ->
|
||||
return "<a href='./_act_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
writemd = (system, text) ->
|
||||
text = markdown(text)
|
||||
system.write(text)
|
||||
money = (salet, amount) ->
|
||||
salet.character.money = salet.character.money + amount
|
||||
|
||||
money = (character, amount) ->
|
||||
character.sandbox.money = character.sandbox.money + amount
|
||||
code_can_input = (salet) ->
|
||||
return salet.character.code.length < 8
|
||||
|
||||
code_can_input = (character) ->
|
||||
return character.sandbox.code.length < 8
|
||||
|
||||
code_print = (character) ->
|
||||
mask = 8 - character.sandbox.code.length
|
||||
retval = character.sandbox.code
|
||||
code_print = (salet) ->
|
||||
mask = 8 - salet.character.code.length
|
||||
retval = salet.character.code
|
||||
if mask > 0
|
||||
for i in [1..mask]
|
||||
retval += "_"
|
||||
return retval
|
||||
|
||||
code_input = (character, digit) ->
|
||||
if code_can_input(character)
|
||||
character.sandbox.code = character.sandbox.code + digit
|
||||
code_input = (salet, digit) ->
|
||||
if code_can_input(salet)
|
||||
salet.character.code = salet.character.code + digit
|
||||
|
||||
code_reset = (character) ->
|
||||
character.sandbox.code = ""
|
||||
code_reset = (salet) ->
|
||||
salet.character.code = ""
|
||||
|
||||
code_check = (character, system) ->
|
||||
if character.sandbox.code.length >= 8
|
||||
code_check = (salet) ->
|
||||
output = ""
|
||||
if salet.character.code.length >= 8
|
||||
# There is an Undum.. let's call it a feature
|
||||
# that prevents the player from entering "3112".
|
||||
# You see, you can't select the situation 1 when you are
|
||||
# already in this situation, so you can't input 1 twice.
|
||||
if character.sandbox.code == "01012017"
|
||||
character.sandbox.box_opened = 1
|
||||
if character.sandbox.knows_the_code == 0
|
||||
writemd(system, """
|
||||
if salet.character.code == "01012017"
|
||||
salet.character.box_opened = 1
|
||||
if salet.character.knows_the_code == 0
|
||||
output += """
|
||||
Is he an extraordinary puzzle cracker or was it a sheer luck, but Ronald manages to *guess* the code.
|
||||
""")
|
||||
"""
|
||||
else
|
||||
writemd(system, """
|
||||
output += """
|
||||
New Year 2017.
|
||||
L. Y. must be Leonard Yakovlev, a famous painter.
|
||||
Some tabloids tried to connect him with Ana but it seemed like a weak link.
|
||||
|
||||
By that logic, his sketch is worth more than all the cash here.
|
||||
Ronald thinks about it, but decides to "let the woman have her memories".
|
||||
""")
|
||||
writemd(system, """
|
||||
"""
|
||||
output += """
|
||||
\n\n
|
||||
Something clicks and box opens.
|
||||
|
||||
The phone is slick, black and light in Ronald's hand.
|
||||
|
@ -77,48 +82,21 @@ code_check = (character, system) ->
|
|||
|
||||
Ronald has no other business here.
|
||||
It's time to go.
|
||||
""")
|
||||
system.doLink("bedroom")
|
||||
"""
|
||||
salet.goTo("bedroom")
|
||||
else
|
||||
writemd(system, "Something clicks and the display resets, but the box stays locked.")
|
||||
output += "Something clicks and the display resets, but the box stays locked."
|
||||
if character.sandbox.code == "000000"
|
||||
writemd(system, "Of course, Ronald didn't hope it would be that easy.")
|
||||
output += "\n\nOf course, Ronald didn't hope it would be that easy."
|
||||
|
||||
character.sandbox.code = ""
|
||||
code_reset(salet)
|
||||
return output
|
||||
|
||||
room "start",
|
||||
room "start", salet,
|
||||
before: () ->
|
||||
dsc: """
|
||||
Peter opened his door to find an empty silent corridor.
|
||||
He went to the neighbor's door and met a closed door.
|
||||
Ronald was working inside, quietly walking around the apartment.
|
||||
He began the inspection from #{way_to('the living room.', 'living-room')}
|
||||
"""
|
||||
|
||||
is_visited = (situation) ->
|
||||
situations = undum.game.situations[situation]
|
||||
if situations
|
||||
return Boolean situations.visited
|
||||
return 0
|
||||
|
||||
# N-th level examine function
|
||||
level = (text, mark) ->
|
||||
$("#content .#{mark}").fadeOut()
|
||||
return markdown(text, mark)
|
||||
|
||||
lvl1 = (text) ->
|
||||
$("#content .lvl2").fadeOut()
|
||||
$("#content .lvl3").fadeOut()
|
||||
$("#content .lvl4").fadeOut()
|
||||
level(text, "lvl1")
|
||||
|
||||
lvl2 = (text) ->
|
||||
$("#content .lvl3").fadeOut()
|
||||
$("#content .lvl4").fadeOut()
|
||||
level(text, "lvl2")
|
||||
|
||||
lvl3 = (text) ->
|
||||
$("#content .lvl4").fadeOut()
|
||||
level(text, "lvl3")
|
||||
|
||||
lvl4 = (text) ->
|
||||
level(text, "lvl4")
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
undum.game.init = (character, system) ->
|
||||
_paq.push(['setCustomDimension', 1, false])
|
||||
|
||||
character.sandbox.view_smash = 1
|
||||
character.sandbox.money = 0
|
||||
character.sandbox.code = ""
|
||||
character.sandbox.knows_the_code = 0
|
||||
character.sandbox.box_opened = 0
|
||||
|
||||
window.onload = undum.begin
|
|
@ -1,4 +1,6 @@
|
|||
room "living-room",
|
||||
# This room has no description.
|
||||
# The description is constructed from the objects within.
|
||||
room "living-room", salet,
|
||||
title: "Living room"
|
||||
enter: (character, system, from) ->
|
||||
if (from == "start")
|
||||
|
@ -7,17 +9,15 @@ room "living-room",
|
|||
audio.volume = 0.5
|
||||
audio.play()
|
||||
ways: ["bedroom", "kitchen", "balcony"]
|
||||
dsc: """
|
||||
#{textlink("A book stand", "bookcase")} is hanging above #{textlink("a television set.", "tv")}
|
||||
"""
|
||||
objects:
|
||||
window: obj "window",
|
||||
objects: [
|
||||
obj "window",
|
||||
act: """
|
||||
The moon is full today.
|
||||
It illuminates the apartment, makes the things stand out in some weird angles.
|
||||
"""
|
||||
dsc: "Ronald is standing in a dark room with a big {{window}}"
|
||||
walls: obj "walls",
|
||||
order: 0
|
||||
dsc: "Ronald is standing in a dark room with a big {{window}}."
|
||||
obj "walls",
|
||||
dsc: "{{The walls}} are covered with a dingy rose wallpaper."
|
||||
act: """
|
||||
There are colorful photographs on the walls.
|
||||
|
@ -27,28 +27,26 @@ room "living-room",
|
|||
A sunset burning in a vast ocean.
|
||||
A black monolith standing on sand dunes.
|
||||
"""
|
||||
door: obj "door",
|
||||
obj "door",
|
||||
dsc: "Oh, and {{the door Ronald came into}} the apartment is there, too."
|
||||
act: (character, system) ->
|
||||
if character.sandbox.box_opened == 0
|
||||
writemd(system, lvl1("Ronald has a job here. It's still on."))
|
||||
order: 99
|
||||
act: (salet) ->
|
||||
if salet.character.box_opened == 0
|
||||
return "Ronald has a job here. It's still on."
|
||||
else
|
||||
system.doLink("exitdoor")
|
||||
writers:
|
||||
bookcase: (character, system) ->
|
||||
lvl1("""
|
||||
Either Anastasia has a very conflicting taste in books, or she has no taste at all. Let's see...
|
||||
#{textlink("“Soviet Black Magic: a Lost Art”,", "magic")}
|
||||
#{textlink("”My Dinner with Zane”,", "zane")}
|
||||
#{textlink("The Scientific Dictionary of Everything,", "dictionary")}
|
||||
#{textlink("“Silent Things”,", "silent")}
|
||||
#{textlink("”Also sprach Zarathustra”,", "zaratustra")}
|
||||
#{textlink("”Pepperoni poker”,", "pepper")}
|
||||
#{textlink("”Jazz sauce. Culinary collection”", "culinary")}
|
||||
and #{textlink("Classico.", "classico")}
|
||||
""")
|
||||
magic: (character, system) ->
|
||||
lvl2("""
|
||||
salet.goTo("exitdoor")
|
||||
obj "zane",
|
||||
level: 1,
|
||||
act: """
|
||||
A three-part romantic urban fantasy detective about a girl who solves murders while dining
|
||||
with her best werewolf friend in an Orient café.
|
||||
|
||||
It looks rather new, the pages are still white and straight.
|
||||
Maybe she didn't catch the right moment to read this.. or maybe just forgot about it.
|
||||
"""
|
||||
obj "magic",
|
||||
level: 1,
|
||||
act: """
|
||||
”Soviet Black Magic: a Lost Art”
|
||||
|
||||
The author, who calls himself The Master, describes the lost art of Soviet Union *magija.*
|
||||
|
@ -57,230 +55,277 @@ room "living-room",
|
|||
This is a cheap paperback edition, but she read this at least a couple of times.
|
||||
The pages have dogged ears, and coffee stains too.
|
||||
Ronald can even see a hint of lipstick smearing the variety show retelling.
|
||||
""")
|
||||
zane: (character, system) ->
|
||||
lvl2("""
|
||||
A three-part romantic urban fantasy detective about a girl who solves murders while dining
|
||||
with her best werewolf friend in an Orient café.
|
||||
|
||||
It looks rather new, the pages are still white and straight.
|
||||
Maybe she didn't catch the right moment to read this.. or maybe just forgot about it.
|
||||
""")
|
||||
dictionary: (character, system) ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "dictionary",
|
||||
level: 1,
|
||||
act: """
|
||||
A big fat Dictionary of Everything, issued in 1989.
|
||||
Nobody reads every page of these.
|
||||
Ronald doubts Anastasia got to read at least one page.
|
||||
""")
|
||||
silent: (character, system) ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "silent",
|
||||
level: 1,
|
||||
act: """
|
||||
Silent Things, a story where nothing speaks and nothing happens.
|
||||
*And she actually read that.*
|
||||
""")
|
||||
zaratustra: (character, system) ->
|
||||
if character.sandbox.seen_zaratustra?
|
||||
lvl2("""
|
||||
This book already gave Ronald everything he wanted.
|
||||
No need to read it, not a bit.
|
||||
""")
|
||||
else
|
||||
money(character, 20000)
|
||||
lvl2("""
|
||||
Nietsche's four-part novel about The Man, The Superman and everything in-between.
|
||||
It's surprisingly worn down.
|
||||
She took this book out a lot.
|
||||
*And she actually read that.*
|
||||
"""
|
||||
obj "zaratustra",
|
||||
level: 1,
|
||||
act: (salet) ->
|
||||
if salet.character.seen_zaratustra?
|
||||
return """
|
||||
This book already gave Ronald everything he wanted.
|
||||
No need to read it, not a bit.
|
||||
"""
|
||||
else
|
||||
money(salet, 20000)
|
||||
return """
|
||||
Nietsche's four-part novel about The Man, The Superman and everything in-between.
|
||||
It's surprisingly worn down.
|
||||
She took this book out a lot.
|
||||
|
||||
Was she secretly a philosophy nut?
|
||||
An Übermensch dreamer?
|
||||
Was she secretly a philosophy nut?
|
||||
An Übermensch dreamer?
|
||||
|
||||
No, of course not.
|
||||
Ronald opens the book and finds a stash of money inside.
|
||||
""")
|
||||
pepper: (character, system) ->
|
||||
lvl2("""
|
||||
No, of course not.
|
||||
Ronald opens the book and finds a stash of money inside.
|
||||
"""
|
||||
obj "pepper",
|
||||
level: 1,
|
||||
act: """
|
||||
An "ironic woman detective" who plays harp and solves murders!
|
||||
It's a trash book filled with blatant product placement. Looks untouched.
|
||||
""")
|
||||
culinary: (character, system) ->
|
||||
lvl2("An old culinary book. Nothing about it.")
|
||||
classico: (character, system) ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "culinary",
|
||||
level: 1,
|
||||
act: "An old culinary book. Nothing about it."
|
||||
obj "classico",
|
||||
level: 1,
|
||||
act: """
|
||||
A history of classical music, from the prehistoric times to the 1970s, a brand new edition with passages about psychedelic rock.
|
||||
The Bach section is bookmarked.
|
||||
""")
|
||||
tv: (character, system) ->
|
||||
lvl1("""
|
||||
"""
|
||||
obj "tv",
|
||||
dsc: "#{objlink("A book stand", "bookcase")} is hanging above {{a television set.}}"
|
||||
order: 1
|
||||
act: """
|
||||
An expensive 40-something inch TV standing on a stylish black stand. The room looks kinda small for that monster.
|
||||
""")
|
||||
"""
|
||||
obj "bookcase",
|
||||
order: 0
|
||||
act: """
|
||||
Either Anastasia has a very conflicting taste in books, or she has no taste at all. Let's see...
|
||||
#{objlink("“Soviet Black Magic: a Lost Art”,", "magic")}
|
||||
#{objlink("”My Dinner with Zane”,", "zane")}
|
||||
#{objlink("The Scientific Dictionary of Everything,", "dictionary")}
|
||||
#{objlink("“Silent Things”,", "silent")}
|
||||
#{objlink("”Also sprach Zarathustra”,", "zaratustra")}
|
||||
#{objlink("”Pepperoni poker”,", "pepper")}
|
||||
#{objlink("”Jazz sauce. Culinary collection”", "culinary")}
|
||||
and #{objlink("Classico.", "classico")}
|
||||
"""
|
||||
]
|
||||
|
||||
room "bedroom",
|
||||
room "bedroom", salet,
|
||||
title: "Bedroom"
|
||||
ways: ["living-room", "kitchen", "bathroom"]
|
||||
dsc: (character, system) ->
|
||||
dsc: (salet) ->
|
||||
return """
|
||||
The bedroom is spacious; its walls are lavender green, almost white in the moonlight.
|
||||
|
||||
#{textlink("A massive wardrobe", "wardrobe")} occupies one of the walls.
|
||||
On the wall across #{objlink("a big bed", "bed")} hangs #{objlink("a full sized mirror.", "mirror")}
|
||||
|
||||
On the wall across #{textlink("a big bed", "bed")} hangs #{textlink("a full sized mirror.", "mirror")}
|
||||
|
||||
#{if character.sandbox.box_opened == 0
|
||||
#{if salet.character.box_opened == 0
|
||||
"On a small table near the bed is an ornate #{way_to("wooden box.", "box")}"
|
||||
else ""}
|
||||
"""
|
||||
writers:
|
||||
bed: () -> lvl1("""
|
||||
A double bed with flower-embroidered sheets.
|
||||
She left several days ago.
|
||||
The sheets are still fresh.
|
||||
""")
|
||||
wardrobe: () -> lvl1("""
|
||||
Anastasia's wardrobe is very high-maintenance.
|
||||
It has a built-in ironing board (with an iron hanged nearby), with 5 drawer rows for #{textlink("lingerie,", "lingerie")} #{textlink("accessories", "accessories")}, #{textlink("shoes.", "shoes")}, #{textlink("hats", "hats")} and.. #{textlink("audio players.", "mp3")}
|
||||
objects: [
|
||||
obj "bed",
|
||||
dsc: ""
|
||||
order: 2
|
||||
act: """
|
||||
A double bed with flower-embroidered sheets.
|
||||
She left several days ago.
|
||||
The sheets are still fresh.
|
||||
"""
|
||||
obj "wardrobe",
|
||||
dsc: "{{A massive wardrobe}} occupies one of the walls.",
|
||||
order: 1,
|
||||
act: """
|
||||
Anastasia's wardrobe is very high-maintenance.
|
||||
It has a built-in ironing board (with an iron hanged nearby), with 5 drawer rows for #{objlink("lingerie,", "lingerie")} #{objlink("accessories", "accessories")}, #{objlink("shoes.", "shoes")}, #{objlink("hats", "hats")} and.. #{objlink("audio players.", "mp3")}
|
||||
|
||||
On the hangers are #{textlink("cashmere coat,", "coat")} #{textlink("sport jacket,","jacket")} #{textlink("jeans,", "jeans")} #{textlink("green shirt,", "gshirt")} #{textlink("a red sleeveless shirt", "rshirt")}, #{textlink("an orange vest,", "vest")} #{textlink("knee-length flower dress,", "dress")} #{textlink("another flower dress,", "adress")} #{textlink("alpaca coat,","coat")} #{textlink("a short skirt", "skirt")} and #{textlink("a big collection of dancing costumes.", "costumes")}
|
||||
""")
|
||||
coat: () ->
|
||||
lvl2("A warm coat for the cold winter.")
|
||||
jacket: () ->
|
||||
lvl2("""
|
||||
On the hangers are #{objlink("cashmere coat,", "coat")} #{objlink("sport jacket,","jacket")} #{objlink("jeans,", "jeans")} #{objlink("green shirt,", "gshirt")} #{objlink("a red sleeveless shirt", "rshirt")}, #{objlink("an orange vest,", "vest")} #{objlink("knee-length flower dress,", "dress")} #{objlink("another flower dress,", "adress")} #{objlink("alpaca coat,","coat")} #{objlink("a short skirt", "skirt")} and #{objlink("a big collection of dancing costumes.", "costumes")}
|
||||
"""
|
||||
obj "coat",
|
||||
level: 1
|
||||
act: "A warm coat for the cold winter."
|
||||
obj "jacket",
|
||||
level: 1,
|
||||
act: """
|
||||
A light expensive jacket.
|
||||
An unusual material, must be something high-tech.
|
||||
It's slightly used.
|
||||
""")
|
||||
jeans: () ->
|
||||
lvl2("Just a pair of women jeans, nothing special.")
|
||||
gshirt: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "jeans",
|
||||
level: 1,
|
||||
act: "Just a pair of women jeans, nothing special."
|
||||
obj "gshirt",
|
||||
level: 1,
|
||||
act: """
|
||||
A green shirt, looks very worn.
|
||||
It's not remarkable in any way but maybe she just loves it very much.
|
||||
""")
|
||||
rshirt: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "rshirt",
|
||||
level: 1,
|
||||
act: """
|
||||
A red women-cut shirt.
|
||||
She didn't wear it much.
|
||||
""")
|
||||
vest: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "vest",
|
||||
level: 1,
|
||||
act: """
|
||||
It looks like a life vest but actually it's a fashionable piece of warm clothing.
|
||||
It was trendy last year.
|
||||
*Why do I know this, is something wrong with me?*
|
||||
""")
|
||||
dress: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "dress",
|
||||
level: 1,
|
||||
act: """
|
||||
Just a dress.
|
||||
Lots of flower embroidery, no pockets.
|
||||
*Impractical.*
|
||||
""")
|
||||
adress: () ->
|
||||
lvl2("These flowers are not like that flowers.")
|
||||
coat: (character, system) ->
|
||||
if character.sandbox.seen_coat?
|
||||
return lvl2("""
|
||||
A warm grey alpaca coat for the bleak autumn times.
|
||||
It's one of her favorites.
|
||||
""")
|
||||
else
|
||||
character.sandbox.seen_coat = 1
|
||||
money(character, 4000)
|
||||
return lvl2("""
|
||||
A warm coat.. hey, what's this?
|
||||
One of the pockets is loaded with cash!
|
||||
""")
|
||||
skirt: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "adress",
|
||||
level: 1,
|
||||
act: "These flowers are not like that flowers."
|
||||
obj "coat",
|
||||
level: 1,
|
||||
act: (salet) ->
|
||||
if salet.character.seen_coat?
|
||||
return """
|
||||
A warm grey alpaca coat for the bleak autumn times.
|
||||
It's one of her favorites.
|
||||
"""
|
||||
else
|
||||
salet.character.seen_coat = 1
|
||||
money(salet, 4000)
|
||||
return """
|
||||
A warm coat.. hey, what's this?
|
||||
One of the pockets is loaded with cash!
|
||||
"""
|
||||
obj "skirt",
|
||||
level: 1,
|
||||
act: """
|
||||
This hanger has only a short skirt.
|
||||
Maybe there was something else on it?
|
||||
Who knows.
|
||||
""")
|
||||
costumes: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "costumes",
|
||||
level: 1,
|
||||
act: """
|
||||
Ana is an exotic dancer.
|
||||
She has her own dance style, and these exquisite costumes are made just for her moves and motions.
|
||||
""")
|
||||
mp3: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "mp3",
|
||||
level: 1,
|
||||
act: """
|
||||
Wow, this woman LOVES her players!
|
||||
|
||||
There are MP3 players, CD players, portable DVD, walk-on clips, sport hands-free players, underwater ones.
|
||||
|
||||
These are all rather cheap, though, compared to *something else* in this room.
|
||||
""")
|
||||
hats: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "hats",
|
||||
level: 1,
|
||||
act: """
|
||||
These look very old-style, very Mary Poppins-like.
|
||||
Maybe that's just a trend or whatever.
|
||||
""")
|
||||
lingerie: () ->
|
||||
lvl2("Ronald won't be digging in that.")
|
||||
accessories: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "lingerie",
|
||||
level: 1,
|
||||
act: "Ronald won't be digging in that."
|
||||
obj "accessories",
|
||||
level: 1,
|
||||
act: """
|
||||
A cross necklace, three metal bracelets and lots of uncomplicated earrings and hair pins.
|
||||
A dozen of scarfs or so.
|
||||
No diamonds, no rings, no *jewelry.*
|
||||
|
||||
On the other hand, her Instagram nickname is *bareboned mane shaker.*
|
||||
""")
|
||||
shoes: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "shoes",
|
||||
level: 1,
|
||||
act: """
|
||||
Anastasia doesn't care for the footwear fashion.
|
||||
These 4 pairs of combat boots and 13 pairs of ballet flats can attest that.
|
||||
|
||||
Of course, there are sandals, loafers, flat dress shoes.. That's a strong dislike for heels... or maybe it's a medical problem?
|
||||
""")
|
||||
mirror: () ->
|
||||
lvl1("""
|
||||
"""
|
||||
obj "mirror"
|
||||
order: 3
|
||||
dsc: ""
|
||||
act: """
|
||||
The mirror looks directly at the bed.
|
||||
Kinky, though not very much *Feng Shui* in it.
|
||||
|
||||
#{textlink("The frame","frame")} depicts various artists (all women) making sculptures of men.
|
||||
#{objlink("The frame","frame")} depicts various artists (all women) making sculptures of men.
|
||||
It's a very *unusual* art.
|
||||
""")
|
||||
frame: () ->
|
||||
lvl2("""
|
||||
"""
|
||||
obj "frame"
|
||||
level: 1,
|
||||
act: """
|
||||
On a close examination, the frame isn't attached to the wall.
|
||||
There is #{textlink("a safe", "safe")} behind the mirror!
|
||||
""")
|
||||
safe: (character) ->
|
||||
if character.sandbox.seen_safe?
|
||||
return lvl3("""
|
||||
The safe is locked with a regular lock.
|
||||
Ronald tries two keys.
|
||||
The first of them opens the door.
|
||||
There is #{objlink("a safe", "safe")} behind the mirror!
|
||||
"""
|
||||
obj "safe"
|
||||
level: 2
|
||||
act: (salet) ->
|
||||
if salet.character.seen_safe?
|
||||
return """
|
||||
The safe is locked with a regular lock.
|
||||
Ronald tries two keys.
|
||||
The first of them opens the door.
|
||||
|
||||
There are #{textlink("money", "money")} inside, and #{textlink("a rough sketch.", "sketch")}
|
||||
""")
|
||||
else
|
||||
return lvl3("""
|
||||
The safe is open now.
|
||||
There are #{objlink("money", "money")} inside, and #{objlink("a rough sketch.", "sketch")}
|
||||
"""
|
||||
else
|
||||
return """
|
||||
The safe is open now.
|
||||
|
||||
There is #{textlink("a rough sketch", "sketch")} inside.
|
||||
""")
|
||||
money: (character, system) ->
|
||||
character.sandbox.seen_safe = 1
|
||||
money(character, 50000)
|
||||
lvl4("""
|
||||
It's a big cash.
|
||||
Odd that she didn't take this when she left.
|
||||
But someone's fault just makes Ronald's payday now.
|
||||
""")
|
||||
sketch: () ->
|
||||
lvl4("""
|
||||
There is #{objlink("a rough sketch", "sketch")} inside.
|
||||
"""
|
||||
obj "money",
|
||||
level: 3
|
||||
act: (salet) ->
|
||||
salet.character.seen_safe = 1
|
||||
money(salet, 50000)
|
||||
return """
|
||||
It's a big cash pile.
|
||||
Odd that she didn't take this when she left.
|
||||
But someone's fault just makes Ronald's payday now.
|
||||
"""
|
||||
obj "sketch",
|
||||
level: 3
|
||||
act: """
|
||||
It's a portrait of Anastasia.
|
||||
She bites her lower lip slightly.
|
||||
Her eyes are sad, or maybe concerned with something.
|
||||
The sketch is signed: *"L. Y. - 2017"*
|
||||
""")
|
||||
"""
|
||||
]
|
||||
|
||||
room "kitchen",
|
||||
room "kitchen", salet,
|
||||
title: "Kitchen"
|
||||
ways: ["living-room", "bedroom"]
|
||||
dsc: """
|
||||
The white, perfectly clean kitchen could be called spartan: #{textlink("a fridge,", "fridge")} a microwave and #{textlink("a big table", "table")} where one can eat whatever she "cooked" that way.
|
||||
The white, perfectly clean kitchen could be called spartan: #{objlink("a fridge,", "fridge")} a microwave
|
||||
and #{objlink("a big table", "table")} where one can eat whatever she "cooked" that way.
|
||||
"""
|
||||
writers:
|
||||
fridge: () ->
|
||||
lvl1("""
|
||||
objects: [
|
||||
obj "fridge",
|
||||
dsc: ""
|
||||
act: """
|
||||
No magnets or stickers on the outside.
|
||||
The door opens easily.
|
||||
*If only the hinges on the apartment doors were as good as refrigerator ones.*
|
||||
|
@ -296,30 +341,35 @@ room "kitchen",
|
|||
A jar of raspberry preserve, half-finished.
|
||||
|
||||
And enough frozen pizzas to last a month.
|
||||
""")
|
||||
table: (character, system) ->
|
||||
if character.sandbox.seen_table?
|
||||
return lvl1("A letter's still there. Nothing new about it.")
|
||||
else
|
||||
character.sandbox.seen_table = 1
|
||||
return lvl1("""
|
||||
There's something on the table.
|
||||
"""
|
||||
obj "table",
|
||||
dsc: ""
|
||||
act: (salet) ->
|
||||
if salet.character.seen_table?
|
||||
return "A letter's still there. Nothing new about it."
|
||||
else
|
||||
salet.character.seen_table = 1
|
||||
return """
|
||||
There's something on the table.
|
||||
|
||||
It looks like a formal letter.
|
||||
It's in French, though, so Ronald won't be able to read it.
|
||||
He's sure it's recent (`24.03.2018`) and it's about something-something QUANTUM AUDIO.. armement?
|
||||
""")
|
||||
It looks like a formal letter.
|
||||
It's in French, though, so Ronald won't be able to read it.
|
||||
He's sure it's recent (`24.03.2018`) and it's about something-something QUANTUM AUDIO.. armement?
|
||||
"""
|
||||
]
|
||||
|
||||
room "bathroom",
|
||||
before: (character,system) ->
|
||||
writemd(system,"Ronald doesn't want to search the bathroom. It's too private a room to enter.")
|
||||
index = undum.game.situations["bedroom"].ways.indexOf("bathroom")
|
||||
undum.game.situations["bedroom"].ways.splice(index, 1)
|
||||
return false
|
||||
room "bathroom", salet,
|
||||
before: (salet) ->
|
||||
bedroom = salet.getRoom("bedroom")
|
||||
index = bedroom.ways.indexOf("bathroom")
|
||||
bedroom.ways.splice(index, 1)
|
||||
return "Ronald doesn't want to search the bathroom. It's too private a room to enter."
|
||||
enter: (salet) ->
|
||||
return salet.goTo("bedroom")
|
||||
title: "Bathroom"
|
||||
ways: ["bedroom"]
|
||||
|
||||
room "balcony",
|
||||
room "balcony", salet,
|
||||
title: "Balcony"
|
||||
ways: ["living-room"]
|
||||
dsc: """
|
||||
|
@ -327,17 +377,19 @@ room "balcony",
|
|||
It's an amazing night.
|
||||
The whole town is lit by moonlight, standing perfectly still.
|
||||
|
||||
On a short stand is #{textlink("an ashtray","ashtray")} with some ash in it.
|
||||
On a short stand is #{objlink("an ashtray","ashtray")} with some ash in it.
|
||||
"""
|
||||
writers:
|
||||
ashtray: (character) ->
|
||||
character.sandbox.knows_the_code = 1
|
||||
return lvl1("""
|
||||
She completely smoked out two cigarettes here.
|
||||
There's also a #{textlink("piece of paper nearby,", "paper")} half-burnt.
|
||||
""")
|
||||
paper: () ->
|
||||
lvl2("""
|
||||
objects: [
|
||||
obj "ashtray",
|
||||
act: (salet) ->
|
||||
salet.character.knows_the_code = 1
|
||||
return """
|
||||
She completely smoked out two cigarettes here.
|
||||
There's also a #{objlink("piece of paper nearby,", "paper")} half-burnt.
|
||||
"""
|
||||
obj "paper",
|
||||
level: 1,
|
||||
act: """
|
||||
It's a letter, written by hand on a thick sheet of what must be an A4 paper.
|
||||
The handwriting is wobbly and the first three quarters of the sheet is gone, but the ending is legible.
|
||||
|
||||
|
@ -346,16 +398,17 @@ room "balcony",
|
|||
I will fly to you no matter what.*
|
||||
|
||||
*L. Y.*
|
||||
""")
|
||||
"""
|
||||
]
|
||||
|
||||
room "box",
|
||||
room "box", salet,
|
||||
ways: ["bedroom"]
|
||||
choices: "#box"
|
||||
dsc: (character, system) ->
|
||||
dsc: (salet) ->
|
||||
return """
|
||||
It's a red wood, very expensive.
|
||||
And this box is locked with a digital code key.
|
||||
#{if is_visited(this) == 0
|
||||
#{if salet.isVisited(this.name) == 0
|
||||
"""
|
||||
Ronald takes out a vial from his pocket. He coats the keys with a bright white powder.
|
||||
|
||||
|
@ -366,47 +419,47 @@ room "box",
|
|||
}
|
||||
"""
|
||||
|
||||
room "smash",
|
||||
canView: (character) ->
|
||||
character.sandbox.view_smash == 1
|
||||
room "smash", salet,
|
||||
canView: (salet) ->
|
||||
salet.character.view_smash == 1
|
||||
optionText: "Smash the box"
|
||||
before: (character) ->
|
||||
character.sandbox.view_smash = 0
|
||||
before: (salet) ->
|
||||
salet.character.view_smash = 0
|
||||
choices: "#box"
|
||||
tags: ["box"]
|
||||
dsc: "Ronald still needs the phone in this box. A very high-tech fragile phone. Smashing isn't an option."
|
||||
|
||||
safe_button = (number) ->
|
||||
room "put#{number}",
|
||||
safe_button = (number, salet) ->
|
||||
room "put#{number}", salet,
|
||||
choices: "#box"
|
||||
tags: ["box"]
|
||||
optionText: "Enter #{number}"
|
||||
before: (character) ->
|
||||
code_input(character, number)
|
||||
canChoose: (character) ->
|
||||
code_can_input(character)
|
||||
after: (character, system) ->
|
||||
code_check(character, system)
|
||||
dsc: (character) -> """
|
||||
Ronald presses button #{number}. The display is #{code_print(character)} now.
|
||||
before: (salet) ->
|
||||
code_input(salet, number)
|
||||
canChoose: (salet) ->
|
||||
code_can_input(salet)
|
||||
after: (salet) ->
|
||||
code_check(salet)
|
||||
dsc: (salet) -> """
|
||||
Ronald presses button #{number}. The display is #{code_print(salet)} now.
|
||||
"""
|
||||
|
||||
safe_button(1)
|
||||
safe_button(2)
|
||||
safe_button(7)
|
||||
safe_button(0)
|
||||
safe_button(1, salet)
|
||||
safe_button(2, salet)
|
||||
safe_button(7, salet)
|
||||
safe_button(0, salet)
|
||||
|
||||
room "reset",
|
||||
room "reset", salet,
|
||||
choices: "#box"
|
||||
tags: ["box"]
|
||||
optionText: "Reset the display"
|
||||
before: (character) ->
|
||||
code_reset(character)
|
||||
before: (salet) ->
|
||||
code_reset(salet)
|
||||
dsc: """
|
||||
Ronald presses Backspace until the display is empty.
|
||||
"""
|
||||
|
||||
room "exitdoor",
|
||||
room "exitdoor", salet,
|
||||
ways: ["living-room"]
|
||||
choices: "#door"
|
||||
dsc: """
|
||||
|
@ -417,14 +470,14 @@ room "exitdoor",
|
|||
Someone's shadow is under the doorframe.
|
||||
"""
|
||||
|
||||
room "finale",
|
||||
room "finale", salet,
|
||||
before: () ->
|
||||
_paq.push(['setCustomDimension', 1, true])
|
||||
$("#tools_wrapper").hide()
|
||||
optionText: "Use the Phone"
|
||||
tags: ["door"]
|
||||
ways: []
|
||||
dsc: (character, system) -> """
|
||||
dsc: (salet) -> """
|
||||
"LOADING... 100%"
|
||||
|
||||
Ronald opens the door and presses his finger to the phone screen.
|
||||
|
@ -443,8 +496,8 @@ room "finale",
|
|||
|
||||
“Well, that was a good night.”
|
||||
|
||||
#{if character.sandbox.money > 0
|
||||
"The pocket is heavy with #{character.sandbox.money} rubles and the phone."
|
||||
#{if salet.character.money > 0
|
||||
"The pocket is heavy with #{salet.character.money} rubles and the phone."
|
||||
else
|
||||
"The phone is heavy in the pocket."
|
||||
}
|
||||
|
|
|
@ -70,22 +70,6 @@
|
|||
</div>
|
||||
</div> <!-- End of div.page -->
|
||||
|
||||
<div id="ui_library">
|
||||
<div id="quality" class="quality">
|
||||
<span class="name" data-attr="name"></span>
|
||||
<span class="value" data-attr="value"></span>
|
||||
</div>
|
||||
|
||||
<div id="quality_group" class="quality_group">
|
||||
<h2 data-attr="title"></h2>
|
||||
<div class="qualities_in_group">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr id="turn_separator">
|
||||
</div>
|
||||
|
||||
<div id="content_library"></div>
|
||||
<audio id="bgsound" preload="auto"><source src="audio/bgr.mp3" type='audio/mpeg; codecs="mp3"'></audio>
|
||||
<!-- CDN JS Libraries -->
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# Cycling interface.
|
||||
# Rooms: cycle through this.cycle_gallery
|
||||
# Objects: cycle through this.cycle_gallery
|
||||
|
||||
cyclelink = (content) ->
|
||||
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
|
||||
|
||||
cycle = (responses, name, character) ->
|
||||
if typeof responses == "function"
|
||||
responses = responses()
|
||||
character.sandbox.cycle_index ?= [] # initialize with empty array
|
||||
character.sandbox.cycle_index[name] ?= 0 # initialize with 0
|
||||
response = responses[character.sandbox.cycle_index[name]]
|
||||
character.sandbox.cycle_index[name]++
|
||||
if character.sandbox.cycle_index[name] == responses.length
|
||||
character.sandbox.cycle_index[name] = 0
|
||||
return cyclelink(response)
|
||||
|
||||
module.exports = cycle
|
|
@ -15,8 +15,8 @@ Usage:
|
|||
Point out a thing in her purse (mildly)
|
||||
""", "character.sandbox.mild = true"
|
||||
###
|
||||
dialogue = (title, startTag, endTag, text, effect) ->
|
||||
retval = room(randomid(), {
|
||||
dialogue = (title, salet, startTag, endTag, text, effect) ->
|
||||
retval = room(randomid(), salet, {
|
||||
optionText: title
|
||||
dsc: text
|
||||
clear: false # backlog is useful in dialogues
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
###
|
||||
Salet interface configuration.
|
||||
###
|
||||
undum = require('./undum.js')
|
||||
$(document).ready(() ->
|
||||
$("#ways").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
undum.processClick($(this).attr("href"))
|
||||
)
|
||||
$("#inventory").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
)
|
||||
$("#load").on("click", "a", (event) ->
|
||||
window.location.reload()
|
||||
)
|
||||
)
|
|
@ -4,7 +4,7 @@ Implies that you don't mix up your tabs and spaces.
|
|||
Copyright 2015 Bruno Dias
|
||||
###
|
||||
normaliseTabs = (text) ->
|
||||
unless text?
|
||||
unless text? and typeof(text) == "string"
|
||||
return ""
|
||||
lines = text.split('\n');
|
||||
indents = lines
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
undum = require('./undum.js')
|
||||
require('./salet.coffee')
|
||||
objlink = (content, ref) ->
|
||||
return "<a href='./_act_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
|
@ -22,26 +22,29 @@ class SaletObj
|
|||
return null
|
||||
for key, value of spec
|
||||
this[key] = value
|
||||
level: 0
|
||||
look: (character, system, f) =>
|
||||
if @dsc
|
||||
text = markdown(@dsc.fcall(this, character, system, f))
|
||||
text = "<span class='look lvl#{@level}'>" + text + "</span>"
|
||||
level: 0 # if > 0 it's hidden
|
||||
order: 0 # you can use this to sort the descriptions
|
||||
look: (system, f) =>
|
||||
if @dsc and @dsc != ""
|
||||
text = markdown(@dsc.fcall(this, system, f).toString())
|
||||
text = system.view.wrapLevel(text, @level)
|
||||
# replace braces {{}} with link to _act_
|
||||
return parsedsc(text, @name)
|
||||
takeable: false
|
||||
take: (character, system) => "You take the #{@name}." # taking to inventory
|
||||
act: (character, system) => "You don't find anything extraordinary about the #{@name}." # object action
|
||||
dsc: (character, system) => "You see a {{#{@name}}} here." # object description
|
||||
inv: (character, system) => "It's a {{#{@name}.}}" # inventory description
|
||||
take: (system) => "You take the #{@name}." # taking to inventory
|
||||
act: (system) => "You don't find anything extraordinary about the #{@name}." # object action
|
||||
dsc: (system) => "You see a {{#{@name}}} here." # object description
|
||||
inv: (system) => "It's a {{#{@name}.}}" # inventory description
|
||||
location: ""
|
||||
put: (location) =>
|
||||
@level = 0 # this is scenery
|
||||
if undum.game.situations[location]?
|
||||
undum.game.situations[location].take(this)
|
||||
if salet.rooms[location]?
|
||||
salet.rooms[location].take(this)
|
||||
@location = location
|
||||
delete: () =>
|
||||
undum.game.situations[@location].objects.remove(this)
|
||||
delete: (location = false) =>
|
||||
if location == false
|
||||
location = @location
|
||||
salet.rooms[location].drop(this)
|
||||
|
||||
obj = (name, spec) ->
|
||||
spec ?= {}
|
||||
|
|
219
lib/room.coffee
219
lib/room.coffee
|
@ -1,79 +1,24 @@
|
|||
# I confess that this world model heavily borrows from INSTEAD engine. - A.Y.
|
||||
|
||||
undum = require('./undum.js')
|
||||
require('./salet.coffee')
|
||||
obj = require('./obj.coffee')
|
||||
markdown = require('./markdown.coffee')
|
||||
cycle = require('./cycle.coffee')
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
Function.prototype.fcall = Function.prototype.call;
|
||||
Boolean.prototype.fcall = () ->
|
||||
return this
|
||||
String.prototype.fcall = () ->
|
||||
return this
|
||||
|
||||
way_to = (content, ref) ->
|
||||
return "<a href='#{ref}' class='way' id='waylink-#{ref}'>#{content}</a>"
|
||||
|
||||
# jQuery was confused by this point where's the context so I did it vanilla-way
|
||||
print = (content) ->
|
||||
if content == ""
|
||||
return
|
||||
if typeof content == "function"
|
||||
content = content()
|
||||
block = document.getElementById("current-situation")
|
||||
if block
|
||||
block.innerHTML = block.innerHTML + markdown(content)
|
||||
else
|
||||
console.error("No current situation found.")
|
||||
|
||||
Array::remove = (e) -> @[t..t] = [] if (t = @indexOf(e)) > -1
|
||||
|
||||
addClass = (element, className) ->
|
||||
if (element.classList)
|
||||
element.classList.add(className)
|
||||
else
|
||||
element.className += ' ' + className
|
||||
|
||||
cls = (system) ->
|
||||
system.clearContent()
|
||||
system.clearContent("#intro")
|
||||
|
||||
update_ways = (ways, name) ->
|
||||
content = ""
|
||||
distances = []
|
||||
if ways
|
||||
document.querySelector(".ways h2").style.display = "block"
|
||||
for way in ways
|
||||
if undum.game.situations[way]?
|
||||
title = undum.game.situations[way].title.fcall(this, name)
|
||||
content += way_to(title, way)
|
||||
distances.push({
|
||||
key: way
|
||||
distance: undum.game.situations[way].distance
|
||||
})
|
||||
else
|
||||
document.querySelector(".ways h2").style.display = "none"
|
||||
document.getElementById("ways").innerHTML = content
|
||||
min = Infinity
|
||||
min_key = []
|
||||
for node in distances
|
||||
if node.distance < min
|
||||
min = node.distance
|
||||
min_key = [node.key]
|
||||
if node.distance == min
|
||||
min_key.push(node.key)
|
||||
if min < Infinity
|
||||
for node in min_key
|
||||
addClass(document.getElementById("waylink-#{node}"), "destination")
|
||||
|
||||
picture_tag = (picture) ->
|
||||
extension = picture.substr((~-picture.lastIndexOf(".") >>> 0) + 2)
|
||||
if (extension == "webm")
|
||||
return """
|
||||
<video src="#{picture}" controls>
|
||||
Your browser does not support the video tag for some reason.
|
||||
You won't be able to view this video in this browser.
|
||||
</video>
|
||||
"""
|
||||
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
|
||||
|
||||
class SaletRoom extends undum.Situation
|
||||
class SaletRoom
|
||||
constructor: (spec) ->
|
||||
undum.Situation.call(this, spec)
|
||||
for index, value of spec
|
||||
this[index] = value
|
||||
return this
|
||||
|
@ -84,17 +29,27 @@ class SaletRoom extends undum.Situation
|
|||
# room illustration image, VN-style. Can be a GIF or WEBM. Can be a function.
|
||||
pic: false
|
||||
|
||||
canView: true
|
||||
canChoose: true
|
||||
priority: 1
|
||||
displayOrder: 1
|
||||
tags: []
|
||||
choices: ""
|
||||
optionText: "Choice"
|
||||
|
||||
dsc: false # room description
|
||||
extendSection: false
|
||||
distance: Infinity # distance to the destination
|
||||
clear: true # clear the screen on entering the room?
|
||||
|
||||
entering: (system, from) =>
|
||||
|
||||
###
|
||||
I call SaletRoom.exit every time the player exits to another room.
|
||||
Unlike @after this gets called after the section is closed.
|
||||
It's a styling difference.
|
||||
###
|
||||
exit: (character, system, to) =>
|
||||
exit: (system, to) =>
|
||||
return true
|
||||
|
||||
###
|
||||
|
@ -105,7 +60,7 @@ class SaletRoom extends undum.Situation
|
|||
The upstream Undum version does not allow you to redefine @enter function easily but allows custom @exit one.
|
||||
It was renamed as @entering to achieve API consistency.
|
||||
###
|
||||
enter: (character, system, from) =>
|
||||
enter: (system, from) =>
|
||||
return true
|
||||
|
||||
###
|
||||
|
@ -117,62 +72,79 @@ class SaletRoom extends undum.Situation
|
|||
My version of `enter` splits the location description from the effects.
|
||||
Also if f == this.name (we're in the same location) the `before` and `after` callbacks are ignored.
|
||||
###
|
||||
entering: (character, system, f) =>
|
||||
entering: (system, f) =>
|
||||
if @clear and f?
|
||||
cls(system)
|
||||
system.view.clearContent()
|
||||
|
||||
if f != @name and f?
|
||||
@visited++
|
||||
if undum.game.situations[f].exit?
|
||||
undum.game.situations[f].exit(character, system, @name)
|
||||
if system.rooms[f].exit?
|
||||
system.rooms[f].exit system, @name
|
||||
|
||||
if @enter
|
||||
@enter character, system, f
|
||||
@enter system, f
|
||||
|
||||
current_situation = ""
|
||||
room_content = ""
|
||||
if not @extendSection
|
||||
classes = if @classes then ' ' + @classes.join(' ') else ''
|
||||
situation = document.getElementById('current-situation')
|
||||
if situation?
|
||||
situation.removeAttribute('id')
|
||||
room = document.getElementById('current-room')
|
||||
if room?
|
||||
room.removeAttribute('id')
|
||||
# Javascript DOM manipulation functions like jQuery's append() or document.createElement
|
||||
# don't work like a typical printLn - they create *DOM nodes*.
|
||||
# You can't leave an unclosed tag just like that. So we have to buffer the output.
|
||||
current_situation = "<section id='current-situation' data-situation='#{@name}' class='situation-#{@name}#{classes}'>"
|
||||
room_content = "<section id='current-room' data-room='#{@name}' class='room-#{@name}#{classes}'>"
|
||||
|
||||
if f != @name and @before?
|
||||
current_situation += markdown(@before.fcall(this, character, system, f))
|
||||
room_content += markdown(@before.fcall(this, system, f))
|
||||
|
||||
current_situation += @look character, system, f
|
||||
room_content += @look system, f
|
||||
|
||||
if f != @name and @after?
|
||||
current_situation += markdown(@after.fcall(this, character, system, f))
|
||||
room_content += markdown(@after.fcall(this, system, f))
|
||||
|
||||
if not @extendSection
|
||||
current_situation += "</section>"
|
||||
room_content += "</section>"
|
||||
|
||||
system.write(current_situation)
|
||||
system.view.write(room_content)
|
||||
|
||||
if @choices
|
||||
system.writeChoices(system.getSituationIdChoices(@choices, @minChoices, @maxChoices))
|
||||
system.view.writeChoices(system, system.getSituationIdChoices(@choices, @maxChoices))
|
||||
|
||||
if system.autosave
|
||||
system.saveGame()
|
||||
|
||||
###
|
||||
An internal function to get the room's description and the descriptions of
|
||||
every object in this room.
|
||||
###
|
||||
look: (character, system, f) =>
|
||||
update_ways(@ways, @name)
|
||||
look: (system, f) =>
|
||||
system.view.updateWays(system, @ways, @name)
|
||||
retval = ""
|
||||
|
||||
if @pic
|
||||
retval += '<div class="pic">'+picture_tag(@pic.fcall(this, character, system, f))+'</div>'
|
||||
retval += '<div class="pic">'+system.view.pictureTag(@pic.fcall(this, system, f))+'</div>'
|
||||
|
||||
# Print the room description
|
||||
if @dsc
|
||||
retval += markdown(@dsc.fcall(this, character, system, f))
|
||||
if @dsc and @dsc != ""
|
||||
dsc = @dsc.fcall(this, system, f).toString()
|
||||
retval += markdown(dsc)
|
||||
|
||||
for name, thing of @objects
|
||||
retval += thing.look()
|
||||
objDescriptions = []
|
||||
for thing in @objects
|
||||
console.log thing
|
||||
if thing.name and typeof(thing.look) == "function" and thing.level == 0 and thing.look(system, f)
|
||||
objDescriptions.push ({
|
||||
order: thing.order,
|
||||
content: thing.look(system, f)
|
||||
})
|
||||
|
||||
objDescriptions.sort((a, b) ->
|
||||
return a.order - b.order
|
||||
)
|
||||
|
||||
for description in objDescriptions
|
||||
retval += description.content
|
||||
|
||||
return retval
|
||||
|
||||
|
@ -183,7 +155,7 @@ class SaletRoom extends undum.Situation
|
|||
@objects[thing.name] = thing
|
||||
# BUG: for some really weird reason if the call is made in init function or
|
||||
# during the initialization, this ALSO puts the thing in the start room.
|
||||
undum.game.situations["start"].objects = {}
|
||||
salet.rooms["start"].objects = {}
|
||||
|
||||
drop: (name) =>
|
||||
delete @objects[name]
|
||||
|
@ -192,25 +164,29 @@ class SaletRoom extends undum.Situation
|
|||
Object action. A function or a string which comes when you click on the object link.
|
||||
You could interpret this as an EXAMINE verb or USE one, it's your call.
|
||||
###
|
||||
act: (character, system, action) =>
|
||||
act: (system, action) =>
|
||||
if (link = action.match(/^_(act|cycle)_(.+)$/)) #object action
|
||||
for name, thing of @objects
|
||||
if name == link[2]
|
||||
for thing in @objects
|
||||
if thing.name == link[2]
|
||||
if link[1] == "act"
|
||||
# If it's takeable, the player can take this object.
|
||||
# If not, we check the "act" function.
|
||||
if thing.takeable
|
||||
character.sandbox.inventory.push thing
|
||||
system.character.inventory.push thing
|
||||
@drop name
|
||||
cls(system)
|
||||
@entering.fcall(this, character, system, @name)
|
||||
return print(thing.take.fcall(thing, character, system))
|
||||
system.view.clearContent()
|
||||
@entering.fcall(this, system, @name)
|
||||
return system.view.write(thing.take.fcall(thing, system).toString())
|
||||
if thing.act
|
||||
return print(thing.act.fcall(thing, character, system))
|
||||
elseif link[1] == "cycle"
|
||||
# TODO object cyclewriter
|
||||
system.view.changeLevel(thing.level)
|
||||
return system.view.write(
|
||||
system.view.wrapLevel(
|
||||
thing.act.fcall(thing, system).toString(),
|
||||
thing.level
|
||||
)
|
||||
)
|
||||
# the loop is done but no return came - match not found
|
||||
console.error("Could not find #{link[1]} in current room.")
|
||||
console.error("Could not find #{link[2]} in current room.")
|
||||
|
||||
# we're done with objects, now check the regular actions
|
||||
actionClass = action.match(/^_(\w+)_(.+)$/)
|
||||
|
@ -218,17 +194,16 @@ class SaletRoom extends undum.Situation
|
|||
|
||||
responses = {
|
||||
writer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, character, system, action)
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.writeInto(output, '#current-situation')
|
||||
system.view.write(output)
|
||||
replacer: (ref) ->
|
||||
content = that.writers[ref].fcall(that, character, system, action)
|
||||
output = "<span>"+content+"</span>" # <p> tags are usually bad for replacers
|
||||
system.replaceWith(output, '#'+ref)
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
system.view.replace(content, '#'+ref)
|
||||
inserter: (ref) ->
|
||||
content = that.writers[ref].fcall(that, character, system, action)
|
||||
content = that.writers[ref].fcall(that, system, action)
|
||||
output = markdown(content)
|
||||
system.writeInto(output, '#'+ref)
|
||||
system.view.write(output, '#'+ref)
|
||||
}
|
||||
|
||||
if (actionClass)
|
||||
|
@ -239,7 +214,7 @@ class SaletRoom extends undum.Situation
|
|||
throw new Error("Tried to call undefined writer: #{action}");
|
||||
responses[responder](ref);
|
||||
else if (@actions.hasOwnProperty(action))
|
||||
@actions[action].call(this, character, system, action);
|
||||
@actions[action].call(this, system, action);
|
||||
else
|
||||
throw new Error("Tried to call undefined action: #{action}");
|
||||
|
||||
|
@ -256,22 +231,30 @@ class SaletRoom extends undum.Situation
|
|||
node.distance = current_room.distance + 1
|
||||
candidates.push(node)
|
||||
|
||||
register: () =>
|
||||
register: (salet) =>
|
||||
if not @name?
|
||||
console.error("Situation has no name")
|
||||
return this
|
||||
undum.game.situations[@name] = this
|
||||
salet.rooms[@name] = this
|
||||
return this
|
||||
|
||||
writers:
|
||||
cyclewriter: (character) ->
|
||||
cycle(this.cycle, this.name, character)
|
||||
cyclewriter: (salet) ->
|
||||
responses = @cycle
|
||||
if typeof responses == "function"
|
||||
responses = responses()
|
||||
cycleIndex = window.localStorage.getItem("cycleIndex")
|
||||
cycleIndex ?= 0
|
||||
response = responses[cycleIndex]
|
||||
cycleIndex++
|
||||
if cycleIndex == responses.length
|
||||
cycleIndex = 0
|
||||
window.localStorage.setItem("cycleIndex", cycleIndex)
|
||||
return salet.view.cycleLink(response)
|
||||
|
||||
room = (name, spec) ->
|
||||
room = (name, salet, spec) ->
|
||||
spec ?= {}
|
||||
spec.name = name
|
||||
retval = new SaletRoom(spec)
|
||||
retval.register()
|
||||
return retval
|
||||
return new SaletRoom(spec).register(salet)
|
||||
|
||||
module.exports = room
|
||||
|
|
445
lib/salet.coffee
Normal file
445
lib/salet.coffee
Normal file
|
@ -0,0 +1,445 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
SaletView = require('./view.coffee')
|
||||
Random = require('./random.js')
|
||||
languages = require('./localize.coffee')
|
||||
|
||||
###
|
||||
fcall() (by analogy with fmap) is added to the prototypes of both String and
|
||||
Function. When called on a Function, it's an alias for Function#call();
|
||||
when called on a String, it only returns the string itself, discarding any input.
|
||||
###
|
||||
|
||||
Function.prototype.fcall = Function.prototype.call;
|
||||
Boolean.prototype.fcall = () ->
|
||||
return this
|
||||
String.prototype.fcall = () ->
|
||||
return this
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
class Character
|
||||
inventory: []
|
||||
|
||||
###
|
||||
This is the control structure, it has minimal amount of data and
|
||||
this data is volatile anyway (as in, it won't get saved).
|
||||
###
|
||||
class Salet
|
||||
# REDEFINE THIS IN YOUR GAME
|
||||
game_id: null
|
||||
game_version: "1.0"
|
||||
autosave: true
|
||||
|
||||
rnd: null
|
||||
time: 0
|
||||
|
||||
# Corresponding room names to room objects.
|
||||
rooms: {}
|
||||
|
||||
# The unique id of the starting room.
|
||||
start: "start"
|
||||
|
||||
# Regular expression to catch every link action.
|
||||
# Salet's default is a general URL-safe expression.
|
||||
linkRe: /^([0-9A-Za-z_-]+|\.)(\/([0-9A-Za-z_-]+))?$/
|
||||
|
||||
character: new Character
|
||||
|
||||
###
|
||||
This function is called at the start of the game. It is
|
||||
normally overridden to provide initial character creation
|
||||
(setting initial quality values, setting the
|
||||
character-text. This is optional, however, as set-up
|
||||
processing could also be done by the first situation's
|
||||
enter function.
|
||||
###
|
||||
init: () ->
|
||||
|
||||
###
|
||||
This function is called before entering any new
|
||||
situation. It is called before the corresponding situation
|
||||
has its `enter` method called.
|
||||
###
|
||||
enter: (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
Hook for when the situation has already been carried out
|
||||
and printed.
|
||||
###
|
||||
afterEnter: (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
This function is called before carrying out any action in
|
||||
any situation. It is called before the corresponding
|
||||
situation has its `act` method called.
|
||||
|
||||
If the function returns true, then it is indicating that it
|
||||
has consumed the action, and the action will not be passed
|
||||
on to the situation. Note that this is the only one of
|
||||
these global handlers that can consume the event.
|
||||
###
|
||||
beforeAction: (situationId, actionId) ->
|
||||
|
||||
###
|
||||
This function is called after carrying out any action in
|
||||
any situation. It is called after the corresponding
|
||||
situation has its `act` method called.
|
||||
###
|
||||
afterAction: (situationId, actionId) ->
|
||||
|
||||
###
|
||||
This function is called after leaving any situation. It is
|
||||
called after the corresponding situation has its `exit`
|
||||
method called.
|
||||
###
|
||||
exit: (oldSituationId, newSituationId) ->
|
||||
|
||||
###
|
||||
Returns a list of situation ids to choose from, given a set of
|
||||
specifications.
|
||||
|
||||
This function is a complex and powerful way of compiling
|
||||
implicit situation choices. You give it a list of situation ids
|
||||
and situation tags (if a single id or tag is needed just that
|
||||
string can be given, it doesn't need to be wrapped in a
|
||||
list). Tags should be prefixed with a hash # to differentiate
|
||||
them from situation ids. The function then considers all
|
||||
matching situations in descending priority order, calling their
|
||||
canView functions and filtering out any that should not be
|
||||
shown, given the current state. Without additional parameters
|
||||
the function returns a list of the situation ids at the highest
|
||||
level of priority that has any valid results. So, for example,
|
||||
if a tag #places matches three situations, one with priority 2,
|
||||
and two with priority 3, and all of them can be viewed in the
|
||||
current context, then only the two with priority 3 will be
|
||||
returned. This allows you to have high-priority situations that
|
||||
trump any lower situations when they are valid, such as
|
||||
situations that force the player to go to one destination if
|
||||
the player is out of money, for example.
|
||||
|
||||
If a maxChoices value is given, then the function will not
|
||||
return any more than the given number of results. If there are
|
||||
more than this number of results possible, then the highest
|
||||
priority resuls will be guaranteed to be returned, but the
|
||||
lowest priority group will have to fight it out for the
|
||||
remaining places. In this case, a random sample is chosen,
|
||||
taking into account the frequency of each situation. So a
|
||||
situation with a frequency of 100 will be chosen 100 times more
|
||||
often than a situation with a frequency of 1, if there is one
|
||||
space available. Often these frequencies have to be taken as a
|
||||
guideline, and the actual probabilities will only be
|
||||
approximate. Consider three situations with frequencies of 1,
|
||||
1, 100, competing for two spaces. The 100-frequency situation
|
||||
will be chosen almost every time, but for the other space, one
|
||||
of the 1-frequency situations must be chosen. So the actual
|
||||
probabilities will be roughly 50%, 50%, 100%. When selecting
|
||||
more than one result, frequencies can only be a guide.
|
||||
|
||||
Before this function returns its result, it sorts the
|
||||
situations in increasing order of their displayOrder values.
|
||||
###
|
||||
getSituationIdChoices: (listOfOrOneIdsOrTags, maxChoices) ->
|
||||
datum = null
|
||||
i = 0
|
||||
|
||||
# First check if we have a single string for the id or tag.
|
||||
if (typeof(listOfOrOneIdsOrTags) == 'string')
|
||||
listOfOrOneIdsOrTags = [listOfOrOneIdsOrTags]
|
||||
|
||||
# First we build a list of all candidate ids.
|
||||
allIds = []
|
||||
for tagOrId in listOfOrOneIdsOrTags
|
||||
if (tagOrId.substr(0, 1) == '#')
|
||||
ids = @getRoomsTagged(tagOrId.substr(1))
|
||||
for id in ids
|
||||
allIds.push(id)
|
||||
else #it's an id, not a tag
|
||||
allIds.push(tagOrId)
|
||||
|
||||
#Filter out anything that can't be viewed right now.
|
||||
currentRoom = @getCurrentRoom()
|
||||
viewableRoomData = []
|
||||
for roomId in allIds
|
||||
room = @rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
|
||||
if (room.canView.fcall(this, currentRoom))
|
||||
viewableRoomData.push({
|
||||
priority: room.priority
|
||||
id: roomId
|
||||
displayOrder: room.displayOrder
|
||||
})
|
||||
|
||||
# Then we sort in descending priority order.
|
||||
viewableRoomData.sort((a, b) ->
|
||||
return b.priority - a.priority
|
||||
)
|
||||
|
||||
committed = []
|
||||
|
||||
# if we need to filter out the results
|
||||
if (maxChoices? && viewableRoomData.length > maxChoices)
|
||||
viewableRoomData = viewableRoomData[-maxChoices..]
|
||||
|
||||
for candidateRoom in viewableRoomData
|
||||
committed.push({
|
||||
id: candidateRoom.id
|
||||
displayOrder: candidateRoom.displayOrder
|
||||
})
|
||||
|
||||
# Now sort in ascending display order.
|
||||
committed.sort((a, b) ->
|
||||
return a.displayOrder - b.displayOrder
|
||||
)
|
||||
|
||||
# And return as a list of ids only.
|
||||
result = []
|
||||
for i in committed
|
||||
result.push(i.id)
|
||||
|
||||
return result
|
||||
|
||||
# This is the data on the player's progress that gets saved.
|
||||
progress: {
|
||||
# A random seed string, used internally to make random
|
||||
# sequences predictable.
|
||||
seed: null
|
||||
# Keeps track of the links clicked, and when.
|
||||
sequence: [],
|
||||
# The time when the progress was saved.
|
||||
saveTime: null
|
||||
}
|
||||
|
||||
# The Id of the current situation the player is in.
|
||||
current: null;
|
||||
|
||||
# Tracks whether we're in interactive mode or batch mode.
|
||||
interactive: true
|
||||
|
||||
# The system time when the game was initialized.
|
||||
startTime: null
|
||||
|
||||
# The stack of links, resulting from the last action, still be to resolved.
|
||||
linkStack: null
|
||||
|
||||
getCurrentRoom: () =>
|
||||
if (@current)
|
||||
return @rooms[@current]
|
||||
return null
|
||||
|
||||
# Gets the unique id used to identify saved games.
|
||||
getSaveId: (slot = "") ->
|
||||
return 'salet_'+@game_id+'_'+@game_version#+'_'+slot
|
||||
|
||||
# This gets called when a link needs to be followed, regardless
|
||||
# of whether it was user action that initiated it.
|
||||
processLink: (code) =>
|
||||
# Check if we should do this now, or if processing is already underway.
|
||||
if @linkStack != null
|
||||
@linkStack.push(code)
|
||||
return
|
||||
|
||||
@view.mark_all_links_old
|
||||
|
||||
# We're processing, so make the stack available.
|
||||
@linkStack = []
|
||||
|
||||
# Handle each link in turn.
|
||||
@processOneLink(code);
|
||||
while (@linkStack.length > 0)
|
||||
code = @linkStack.shift()
|
||||
@processOneLink(code)
|
||||
|
||||
# We're done, so remove the stack to prevent future pushes.
|
||||
@linkStack = null;
|
||||
|
||||
# Scroll to the top of the new content.
|
||||
@view.endOutputTransaction()
|
||||
|
||||
# We're able to save, if we weren't already.
|
||||
@view.enableSaving()
|
||||
|
||||
goTo: (roomId) ->
|
||||
return @processLink(roomId)
|
||||
|
||||
###
|
||||
This gets called to actually do the work of processing a code.
|
||||
When one doLink is called (or a link is clicked), this may set call
|
||||
code that further calls doLink, and so on. This method processes
|
||||
each one, and processLink manages this.
|
||||
###
|
||||
processOneLink: (code) ->
|
||||
match = code.match(@linkRe)
|
||||
assert(match, "link_not_valid".l({link:code}))
|
||||
|
||||
situation = match[1]
|
||||
action = match[3]
|
||||
|
||||
# Change the situation
|
||||
if situation != '.' and situation != @current_room
|
||||
@doTransitionTo(situation)
|
||||
else
|
||||
# We should have an action if we have no situation change.
|
||||
assert(action, "link_no_action".l())
|
||||
|
||||
# Carry out the action
|
||||
if (action)
|
||||
room = @getCurrentRoom()
|
||||
if (room and @beforeAction)
|
||||
# Try the global act handler
|
||||
consumed = @beforeAction(room, action)
|
||||
if (consumed != true)
|
||||
room.act(this, action)
|
||||
|
||||
if (@afterAction)
|
||||
@afterAction(this, room, action)
|
||||
|
||||
# This gets called when the user clicks a link to carry out an action.
|
||||
processClick: (code) ->
|
||||
now = (new Date()).getTime() * 0.001
|
||||
@time = now - @startTime
|
||||
@progress.sequence.push({link:code, when:@time})
|
||||
@processLink(code)
|
||||
|
||||
# Transitions between situations.
|
||||
doTransitionTo: (newRoomId) ->
|
||||
oldRoomId = @current
|
||||
oldRoom = @getCurrentRoom()
|
||||
newRoom = @rooms[newRoomId]
|
||||
|
||||
assert(newRoom, "unknown_situation".l({id:newRoomId}))
|
||||
|
||||
# We might not have an old situation if this is the start of the game.
|
||||
if (oldRoom and @exit)
|
||||
@exit(oldRoomId, newRoomId)
|
||||
|
||||
@current = newRoomId
|
||||
|
||||
# Remove links and transient sections.
|
||||
@view.removeTransient(@interactive)
|
||||
|
||||
# Notify the incoming situation.
|
||||
if (@enter)
|
||||
@enter(oldRoomId, newRoomId)
|
||||
newRoom.entering(this, oldRoomId)
|
||||
|
||||
# additional hook for when the situation text has already been printed
|
||||
if (@afterEnter)
|
||||
@afterEnter(oldRoomId, newRoomId)
|
||||
|
||||
###
|
||||
Erases the character in local storage. This is permanent!
|
||||
To restart the game afterwards, we perform a simple page refresh.
|
||||
This guarantees authors don't have to care about "tainting" the
|
||||
game state across save/erase cycles, meaning that character.sandbox
|
||||
no longer has to be the end-all be-all repository of game state.
|
||||
###
|
||||
eraseSave: (force = false) =>
|
||||
saveId = @getSaveId() # save slot
|
||||
if (localStorage.getItem(saveId) and (force or confirm("erase_message".l())))
|
||||
localStorage.removeItem(saveId)
|
||||
window.location.reload()
|
||||
|
||||
# Find and return a list of ids for all situations with the given tag.
|
||||
getRoomsTagged: (tag) =>
|
||||
result = []
|
||||
for id, room of @rooms
|
||||
for i in room.tags
|
||||
if (i == tag)
|
||||
result.push(id)
|
||||
break
|
||||
return result
|
||||
|
||||
# Saves the character and the walking history to local storage.
|
||||
saveGame: () ->
|
||||
# Store when we're saving the game, to avoid exploits where a
|
||||
# player loads their file to gain extra time.
|
||||
now = (new Date()).getTime() * 0.001
|
||||
@progress.saveTime = now - @startTime
|
||||
|
||||
# Save the game.
|
||||
window.localStorage.setItem(@getSaveId(), JSON.stringify({
|
||||
progress: @progress,
|
||||
character: @character
|
||||
}))
|
||||
|
||||
# Switch the button highlights.
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
@view.enableLoading()
|
||||
|
||||
# Loads the game from the given data
|
||||
loadGame: (saveFile) ->
|
||||
@progress = saveFile.progress
|
||||
@character = saveFile.character
|
||||
|
||||
@rnd = new Random(@progress.seed)
|
||||
|
||||
# Don't load the save if it's an autosave at the first room (start).
|
||||
# We don't need to clear the screen.
|
||||
if saveFile.progress? and saveFile.progress.sequence.length > 1
|
||||
@view.clearContent()
|
||||
|
||||
# Now play through the actions so far:
|
||||
if (@init)
|
||||
@init()
|
||||
|
||||
# Run through all the player's history.
|
||||
interactive = false
|
||||
for step in @progress.sequence
|
||||
# The action must be done at the recorded time.
|
||||
@time = step.when
|
||||
@processLink(step.link)
|
||||
interactive = true
|
||||
|
||||
# Reverse engineer the start time.
|
||||
now = new Date().getTime() * 0.001
|
||||
startTime = now - @progress.saveTime
|
||||
|
||||
view: new SaletView
|
||||
|
||||
beginGame: () ->
|
||||
@view.fixClicks()
|
||||
|
||||
# Handle storage.
|
||||
saveFile = false
|
||||
if (@view.hasLocalStorage())
|
||||
saveFile = localStorage.getItem(@getSaveId())
|
||||
|
||||
if (saveFile)
|
||||
try
|
||||
@loadGame(JSON.parse(saveFile))
|
||||
@view.disableSaving()
|
||||
@view.enableErasing()
|
||||
catch err
|
||||
console.log "There was an error loading your save. The save is deleted."
|
||||
console.error err
|
||||
@eraseSave(true)
|
||||
else
|
||||
@progress.seed = new Date().toString()
|
||||
|
||||
character = new Character()
|
||||
@rnd = new Random(@progress.seed)
|
||||
@progress.sequence = [{link:@start, when:0}]
|
||||
|
||||
# Start the game
|
||||
@startTime = new Date().getTime() * 0.001
|
||||
if (@init)
|
||||
@init(character)
|
||||
|
||||
# Do the first state.
|
||||
@doTransitionTo(@start)
|
||||
|
||||
getRoom: (name) ->
|
||||
return @rooms[name]
|
||||
|
||||
# Just an alias for getCurrentRoom
|
||||
here: () -> @getCurrentRoom()
|
||||
|
||||
isVisited: (name) ->
|
||||
place = @getRoom(name)
|
||||
if place
|
||||
return Boolean place.visited
|
||||
return 0
|
||||
|
||||
module.exports = Salet
|
1617
lib/undum.js
1617
lib/undum.js
File diff suppressed because it is too large
Load diff
303
lib/view.coffee
Normal file
303
lib/view.coffee
Normal file
|
@ -0,0 +1,303 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
###
|
||||
Salet interface configuration.
|
||||
In a typical MVC structure, this is the View.
|
||||
Only it knows about the DOM structure.
|
||||
Other modules just use its API or prepare the HTML for insertion.
|
||||
You don't need to call this module from the game directly.
|
||||
|
||||
The abstraction goal here is to provide the author with a freedom to style his
|
||||
game as he wants to. The save and erase buttons are not necessary buttons,
|
||||
but they could be something else entirely. (That's why IDs are hardcoded.)
|
||||
###
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
way_to = (content, ref) ->
|
||||
return "<a href='#{ref}' class='way'>#{content}</a>"
|
||||
|
||||
addClass = (element, className) ->
|
||||
if (element.classList)
|
||||
element.classList.add(className)
|
||||
else
|
||||
element.className += ' ' + className
|
||||
|
||||
class SaletView
|
||||
init: (salet) ->
|
||||
$("#content, #ways").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
a = $(this)
|
||||
href = a.attr('href')
|
||||
if (href.match(salet.linkRe))
|
||||
if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
|
||||
salet.view.clearLinks(href)
|
||||
salet.processClick(href)
|
||||
)
|
||||
$("#inventory").on("click", "a", (event) ->
|
||||
event.preventDefault()
|
||||
alert("Not done yet")
|
||||
)
|
||||
$("#load").on("click", "a", (event) ->
|
||||
window.location.reload()
|
||||
)
|
||||
if (@hasLocalStorage())
|
||||
$("#erase").click((event) ->
|
||||
event.preventDefault()
|
||||
return salet.eraseSave()
|
||||
)
|
||||
$("#save").click((event) ->
|
||||
event.preventDefault()
|
||||
return salet.saveGame()
|
||||
)
|
||||
|
||||
disableSaving: () ->
|
||||
$("#save").addClass('disabled')
|
||||
enableSaving: () ->
|
||||
$("#save").removeClass('disabled')
|
||||
enableErasing: () ->
|
||||
$("#erase").removeClass('disabled')
|
||||
disableErasing: () ->
|
||||
$("#erase").addClass('disabled')
|
||||
enableLoading: () ->
|
||||
$("#load").removeClass('disabled')
|
||||
disableLoading: () ->
|
||||
$("#load").addClass('disabled')
|
||||
|
||||
# Scrolls the top of the screen to the specified point
|
||||
scrollTopTo: (value) ->
|
||||
$('html,body').animate({scrollTop: value}, 500)
|
||||
|
||||
# Scrolls the bottom of the screen to the specified point
|
||||
scrollBottomTo: (value) ->
|
||||
scrollTopTo(value - $(window).height())
|
||||
|
||||
# Scrolls all the way to the bottom of the screen
|
||||
scrollToBottom: () ->
|
||||
scrollTopTo($('html').height() - $(window).height());
|
||||
|
||||
###
|
||||
Removes all content from the page, clearing the main content area.
|
||||
|
||||
If an elementSelector is given, then only that selector will be
|
||||
cleared. Note that all content from the cleared element is removed,
|
||||
but the element itself remains, ready to be filled again using @write.
|
||||
###
|
||||
clearContent: (elementSelector = "#content") ->
|
||||
if (elementSelector == "#content") # empty the intro with the content
|
||||
document.getElementById("intro").innerHTML = ""
|
||||
document.querySelector(elementSelector).innerHTML = ""
|
||||
|
||||
prepareContent: (content) ->
|
||||
if typeof content == "function"
|
||||
content = content()
|
||||
if content instanceof jQuery
|
||||
content = content[0].outerHTML
|
||||
return content.toString()
|
||||
|
||||
# Write content to current room
|
||||
write: (content, elementSelector = "#current-room") ->
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
block = document.querySelector(elementSelector)
|
||||
if block
|
||||
block.innerHTML = block.innerHTML + markdown(content)
|
||||
else
|
||||
# most likely this is the starting room
|
||||
block = document.getElementById("content")
|
||||
block.innerHTML = content
|
||||
|
||||
# Replaces the text in the given block with the given text.
|
||||
# !! Does not call markdown on the provided text. !!
|
||||
replace: (content, elementSelector) ->
|
||||
if content == ""
|
||||
return
|
||||
content = @prepareContent(content)
|
||||
block = document.querySelector(elementSelector)
|
||||
block.innerHTML = content
|
||||
###
|
||||
Turns any links that target the given href into plain
|
||||
text. This can be used to remove action options when an action
|
||||
is no longer available. It is used automatically when you give
|
||||
a link the 'once' class.
|
||||
###
|
||||
clearLinks: (code) ->
|
||||
for a in $("#content").find("a[href='" + code + "']")
|
||||
html = a.innerHTML
|
||||
a = $(a)
|
||||
a.replaceWith($("<span>").addClass("ex_link").html(html))
|
||||
return true
|
||||
|
||||
###
|
||||
Given a list of situation ids, this outputs a standard option
|
||||
block with the situation choices in the given order.
|
||||
|
||||
The contents of each choice will be a link to the situation,
|
||||
the text of the link will be given by the situation's
|
||||
outputText property. Note that the canChoose function is
|
||||
called, and if it returns false, then the text will appear, but
|
||||
the link will not be clickable.
|
||||
|
||||
Although canChoose is honored, canView and displayOrder are
|
||||
not. If you need to honor these, you should either do so
|
||||
manually, ot else use the `getSituationIdChoices` method to
|
||||
return an ordered list of valid viewable situation ids.
|
||||
###
|
||||
writeChoices: (salet, listOfIds) ->
|
||||
if (not listOfIds? or listOfIds.length == 0)
|
||||
return
|
||||
|
||||
currentRoom = salet.getCurrentRoom();
|
||||
$options = $("<ul>").addClass("options");
|
||||
for roomId in listOfIds
|
||||
room = salet.rooms[roomId]
|
||||
assert(room, "unknown_situation".l({id:roomId}))
|
||||
if (room == currentRoom)
|
||||
continue
|
||||
|
||||
optionText = room.optionText.fcall(salet, currentRoom)
|
||||
if (!optionText)
|
||||
optionText = "choice".l({number:i+1})
|
||||
$option = $("<li>")
|
||||
$a = $("<span>")
|
||||
if (room.canChoose.fcall(this, salet, currentRoom))
|
||||
$a = $("<a>").attr({href: roomId})
|
||||
$a.html(optionText)
|
||||
$option.html($a)
|
||||
$options.append($option)
|
||||
@write($options)
|
||||
|
||||
# Marks all links as old. This gets called in a `processLink` function.
|
||||
mark_all_links_old: () ->
|
||||
$('.new').removeClass('new')
|
||||
|
||||
# Removes links and transient sections.
|
||||
# Arguments:
|
||||
# interactive - if we're working in interactive mode (or we're loading a save)
|
||||
removeTransient: (interactive = false) ->
|
||||
for a in $('#content').find('a')
|
||||
a = $(a)
|
||||
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
|
||||
return;
|
||||
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
|
||||
contentToHide = $('#content .transient, #content ul.options')
|
||||
contentToHide.add($("#content a").filter(() ->
|
||||
return $(this).attr("href").match(/[?&]transient[=&]?/)
|
||||
))
|
||||
if (interactive)
|
||||
contentToHide.animate({opacity: 0}, 350).
|
||||
slideUp(500, () ->
|
||||
$(this).remove()
|
||||
)
|
||||
else
|
||||
contentToHide.remove()
|
||||
|
||||
# Remove every section marked as a different level.
|
||||
# For a link level 0, we hide every link of level 1 and above.
|
||||
# It's for the player to focus.
|
||||
changeLevel: (level) ->
|
||||
maxLevel = 6
|
||||
if level < maxLevel
|
||||
i = level + 1
|
||||
hideArray = []
|
||||
while i <= maxLevel
|
||||
hideArray.push("#content .lvl"+i)
|
||||
i++
|
||||
directive = hideArray.join(", ")
|
||||
$(directive).fadeOut("slow")
|
||||
|
||||
wrapLevel: (text, level) ->
|
||||
return "<span class='lvl#{level}'>"+text+'</span>'
|
||||
|
||||
# At last, we scroll the view so that .new objects are in view.
|
||||
endOutputTransaction: () =>
|
||||
if !@interactive
|
||||
return; # We're loading a save; do nothing at all.
|
||||
$new = $('.new')
|
||||
viewHeight = $(window).height()
|
||||
newTop = newBottom = newHeight = optionHeight = 0
|
||||
|
||||
if $new.length == 0
|
||||
return; # Somehow, there's nothing new.
|
||||
|
||||
newTop = $new.first().offset().top
|
||||
newBottom = $new.last().offset().top + $new.last().height()
|
||||
newHeight = newBottom - newTop
|
||||
|
||||
# We take the options list into account, because we don't want the new
|
||||
# content to scroll offscreen when the list disappears. So we calculate
|
||||
# scroll points as though the option list was already gone.
|
||||
if ($('.options').not('.new').length)
|
||||
optionHeight = $('.options').not('new').height()
|
||||
|
||||
if (newHeight > (viewHeight - optionHeight - 50))
|
||||
# The new content is too long for our viewport, so we scroll the
|
||||
# top of the new content to roughly 75% of the way up the viewport's
|
||||
# height.
|
||||
scrollTopTo(newTop-(viewHeight*0.25) - optionHeight);
|
||||
else
|
||||
if (newTop > $('body').height() - viewHeight)
|
||||
# If we scroll right to the bottom, the new content will be in
|
||||
# view. So we do that.
|
||||
scrollToBottom();
|
||||
else
|
||||
# Our new content is too far up the page. So we scroll to place
|
||||
# it somewhere near the bottom.
|
||||
scrollBottomTo(newBottom+100 - optionHeight);
|
||||
|
||||
# Feature detection
|
||||
hasLocalStorage: () ->
|
||||
return window.localStorage?
|
||||
|
||||
# Any point that an option list appears, its options are its first links.
|
||||
fixClicks: () ->
|
||||
$("body").on('click', "ul.options li", (event) ->
|
||||
# Make option clicks pass through to their first link.
|
||||
link = $("a", this)
|
||||
if (link.length > 0)
|
||||
$(link.get(0)).click()
|
||||
)
|
||||
|
||||
updateWays: (salet, ways, name) ->
|
||||
content = ""
|
||||
distances = []
|
||||
if ways then for way in ways
|
||||
if salet.rooms[way]?
|
||||
title = salet.rooms[way].title.fcall(this, name)
|
||||
content += way_to(title, way)
|
||||
distances.push({
|
||||
key: way
|
||||
distance: salet.rooms[way].distance
|
||||
})
|
||||
else
|
||||
document.querySelector(".ways h2").style.display = "none"
|
||||
document.getElementById("ways").innerHTML = content
|
||||
min = Infinity
|
||||
min_key = []
|
||||
for node in distances
|
||||
if node.distance < min
|
||||
min = node.distance
|
||||
min_key = [node.key]
|
||||
if node.distance == min
|
||||
min_key.push(node.key)
|
||||
if min < Infinity
|
||||
for node in min_key
|
||||
waylink = document.getElementById("waylink-#{node}")
|
||||
if waylink
|
||||
addClass(waylink, "destination")
|
||||
|
||||
pictureTag: (picture) ->
|
||||
extension = picture.substr((~-picture.lastIndexOf(".") >>> 0) + 2)
|
||||
if (extension == "webm")
|
||||
return """
|
||||
<video src="#{picture}" controls>
|
||||
Your browser does not support the video tag for some reason.
|
||||
You won't be able to view this video in this browser.
|
||||
</video>
|
||||
"""
|
||||
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
|
||||
|
||||
cycleLink: (content) ->
|
||||
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
|
||||
|
||||
module.exports = SaletView
|
|
@ -8,6 +8,7 @@
|
|||
"browserify": "^9.0.8",
|
||||
"browserify-shim": "^3.8.8",
|
||||
"coffeeify": "^1.0.0",
|
||||
"coffee-script": "^1.10.0",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-coffee": "^2.3.1",
|
||||
|
@ -15,7 +16,6 @@
|
|||
"gulp-zip": "^3.0.2",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-sass": "^2.1.1",
|
||||
"lodash": "^3.6.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.1.0"
|
||||
|
|
Loading…
Reference in a new issue