mirror of
https://gitlab.com/Oreolek/salet-module.git
synced 2024-06-26 03:50:49 +03:00
NPM module (WIP)
This commit is contained in:
parent
bda0b84260
commit
bcf84bf95b
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
node_modules
|
||||
build
|
||||
dist
|
||||
dist.zip
|
||||
lib
|
||||
|
|
31
Cakefile
Normal file
31
Cakefile
Normal file
|
@ -0,0 +1,31 @@
|
|||
fs = require 'fs'
|
||||
{exec,spawn} = require 'child_process'
|
||||
util = require 'util'
|
||||
sources = [
|
||||
'markdown.coffee'
|
||||
'view.coffee'
|
||||
'unit.coffee'
|
||||
'character.coffee'
|
||||
'room.coffee'
|
||||
'localize.coffee'
|
||||
'salet.coffee'
|
||||
]
|
||||
sourcestring = ""
|
||||
for source in sources
|
||||
sourcestring += 'src/'+source+' '
|
||||
|
||||
task 'watch', 'Watch source files and build changes', ->
|
||||
watch = spawn "coffee", ['-c', '-w', '--no-header', '-j', 'lib/index.js', sourcestring]
|
||||
watch.stdout.on 'data', (data) -> console.log data.toString().trim()
|
||||
|
||||
task 'build', 'Compile all CoffeeScript files', ->
|
||||
# prepare lib directory
|
||||
if not fs.existsSync 'lib'
|
||||
fs.mkdirSync 'lib'
|
||||
|
||||
# run coffee-script compile
|
||||
exec "coffee -c --no-header -j lib/index.js #{sourcestring}", (err, stdout, stderr) ->
|
||||
if err
|
||||
util.log err
|
||||
process.exit 1 # abort npm packaging
|
||||
util.log "Compiled CoffeeScript."
|
130
Gulpfile.coffee
130
Gulpfile.coffee
|
@ -1,130 +0,0 @@
|
|||
watchify = require('watchify')
|
||||
browserify = require('browserify')
|
||||
browserSync = require('browser-sync')
|
||||
gulp = require('gulp')
|
||||
source = require('vinyl-source-stream')
|
||||
gutil = require('gulp-util')
|
||||
coffeify = require('coffeeify')
|
||||
coffee = require("gulp-coffee")
|
||||
sass = require('gulp-sass')
|
||||
uglify = require('gulp-uglify')
|
||||
buffer = require('vinyl-buffer')
|
||||
zip = require('gulp-zip')
|
||||
concat = require('gulp-concat')
|
||||
|
||||
reload = browserSync.reload
|
||||
|
||||
html = (target) ->
|
||||
return () ->
|
||||
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))
|
||||
|
||||
# Audio assets
|
||||
audio = (target) ->
|
||||
return () ->
|
||||
return gulp.src(['audio/*.mp3']).pipe(gulp.dest(target))
|
||||
|
||||
gulp.task('html', html('./build'))
|
||||
gulp.task('img', img('./build/img'))
|
||||
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'))
|
||||
)
|
||||
|
||||
bundler = watchify(browserify({
|
||||
entries: ["./build/game/main.coffee"]
|
||||
debug: true
|
||||
transform: [coffeify]
|
||||
}))
|
||||
|
||||
bundle = () ->
|
||||
return bundler.bundle()
|
||||
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe(gulp.dest('./build/game'))
|
||||
|
||||
gulp.task('concatCoffee', () ->
|
||||
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)
|
||||
|
||||
gulp.task('build', ['html', 'img', 'sass', 'coffee', 'audio'])
|
||||
|
||||
gulp.task('serve', ['build'], () ->
|
||||
browserSync({
|
||||
server: {
|
||||
baseDir: 'build'
|
||||
}
|
||||
})
|
||||
|
||||
sassListener = () ->
|
||||
reload('./build/css/main.css')
|
||||
|
||||
gulp.watch(['./html/*.html'], ['html'])
|
||||
gulp.watch(['./sass/*.scss'], ['sass'])
|
||||
gulp.watch(['./img/*.png', './img/*.jpeg', './img/*.jpg'], ['img'])
|
||||
gulp.watch(['./game/*.coffee'], ['coffee']);
|
||||
|
||||
gulp.watch(['./build/css/main.css'], sassListener)
|
||||
gulp.watch(
|
||||
['./build/game/bundle.js', './build/img/*', './build/index.html'],
|
||||
browserSync.reload)
|
||||
)
|
||||
|
||||
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"))
|
||||
)
|
||||
|
||||
gulp.task('sass-dist', () ->
|
||||
return gulp.src('./sass/main.scss')
|
||||
.pipe(sass({outputStyle: 'compressed'}))
|
||||
.pipe(gulp.dest('./dist/css'))
|
||||
)
|
||||
|
||||
distBundler = browserify({
|
||||
debug: false,
|
||||
entries: ['./build/game/main.coffee'],
|
||||
transform: ['coffeeify']
|
||||
})
|
||||
|
||||
gulp.task('coffee-dist', ['concatCoffee'], () ->
|
||||
return distBundler.bundle()
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(uglify())
|
||||
.on('error', gutil.log)
|
||||
.pipe(gulp.dest('./dist/game'))
|
||||
)
|
||||
|
||||
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('.'))
|
||||
)
|
|
@ -1,62 +0,0 @@
|
|||
room = require("../../lib/room.coffee")
|
||||
unit = require('../../lib/unit.coffee')
|
||||
dialogue = require('../../lib/util/dialogue.coffee')
|
||||
phrase = require('../../lib/util/phrase.coffee')
|
||||
oneOf = require('../../lib/oneOf.coffee')
|
||||
Salet = require('../../lib/salet.coffee')
|
||||
|
||||
salet = Salet({
|
||||
game_id: "your-game-id-here"
|
||||
game_version: "1.2"
|
||||
})
|
||||
$(document).ready(() ->
|
||||
window.addEventListener('popstate', (event) ->
|
||||
salet.goBack()
|
||||
)
|
||||
|
||||
salet.beginGame()
|
||||
)
|
||||
|
||||
###
|
||||
Element helpers. There is no real need to build monsters like a().id("hello")
|
||||
because you won't use them as is. It does not make sense in context, the
|
||||
author has Markdown and all utilities to *forget* about the markup.
|
||||
###
|
||||
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>"
|
||||
|
||||
# The first room of the game.
|
||||
# For accessibility reasons the text is provided in HTML, not here.
|
||||
room "start", salet,
|
||||
enter: (salet) ->
|
||||
salet.character.bought_lamp = false
|
||||
dsc: """
|
||||
""",
|
||||
choices: "#start"
|
||||
|
||||
# This is a special inventory room.
|
||||
# The inventory button is a regular link to this room.
|
||||
# You may alter these as much as you like or scrap it along with the button.
|
||||
room "inventory", salet,
|
||||
canSave: false # saving the game here is forbidden. Aautosaving too.
|
||||
enter: () ->
|
||||
$("#inventory").hide()
|
||||
exit: () ->
|
||||
$("#inventory").show()
|
||||
dsc: (salet) ->
|
||||
if salet.character.inventory.length == 0
|
||||
text = "You are carrying nothing."
|
||||
else
|
||||
text = "You are carrying:\n\n"
|
||||
for thing in salet.character.inventory
|
||||
text += "* #{salet.character.listinv(thing.name)}\n"
|
||||
return text+"\n\n"+"""
|
||||
<div class="center"><a href="./exit"><button class="btn btn-lg btn-outline-primary">Go back</button></a></div>
|
||||
"""
|
||||
actions:
|
||||
exit: (salet) ->
|
||||
return salet.goBack()
|
|
@ -1,124 +0,0 @@
|
|||
room "world", salet,
|
||||
tags: ["start"],
|
||||
optionText: "Enter the world",
|
||||
ways: ["plaza"]
|
||||
dsc: """
|
||||
### Rhinestone Room
|
||||
|
||||
You're in a large room carved inside a giant milky rock mountain.
|
||||
The floor and walls are littered with signs and signatures of the previous visitors.
|
||||
"""
|
||||
units: [
|
||||
unit "well",
|
||||
dsc: "A steep narrow {{well}} proceeds upward."
|
||||
act: "There is only one passage out. See the „Other rooms“ block popped up? Click it."
|
||||
]
|
||||
|
||||
room "plaza", salet,
|
||||
title: (from) ->
|
||||
if from == "world"
|
||||
return "Upwards"
|
||||
else
|
||||
return "Town plaza"
|
||||
cycle: ["quirky", "distinct", "kooky", "crazy", "quaint"]
|
||||
ways: ["shop"]
|
||||
before: (system, from) ->
|
||||
if from == 'world'
|
||||
"""
|
||||
You climb up the well and come out to a central plaza of a #{system.view.cycleLink("quaint")} little town.
|
||||
A plaque nearby says it's the town of *Innsmouth,* wherever that is.
|
||||
"""
|
||||
else
|
||||
"You quickly find the central plaza."
|
||||
units: [
|
||||
unit "policeman",
|
||||
dsc: "There is a policeman nearby. You could ask him {{for directions.}}"
|
||||
act: (salet) ->
|
||||
if salet.character.has_mark?
|
||||
return "You already talked to him, no need to bug the man twice."
|
||||
salet.character.has_mark ?= true
|
||||
salet.getRoom("lair").destination()
|
||||
"""
|
||||
“Here, let me mark it on your map.”
|
||||
"""
|
||||
unit "people",
|
||||
dsc: "There are {{people shouting}} nearby."
|
||||
act: 'Just some weirdos shouting "Viva la Cthulhu!". Typical.'
|
||||
]
|
||||
|
||||
room "shop", salet,
|
||||
title: "The Shop"
|
||||
#pic: "http://loremflickr.com/640/300/room,shop"
|
||||
ways: ["plaza", "shop-inside", "lair"]
|
||||
dsc: """
|
||||
Being the only shop in town, this trendy establishment did not need a name.
|
||||
It's an open question why it had one, especially because its name was "Hung Crossing".
|
||||
|
||||
You are standing in front of a picturesque sign. It's cold here.
|
||||
"""
|
||||
|
||||
room "lair", salet,
|
||||
title: "The Lair"
|
||||
before: "Finding The Lair is easy. Leaving it is impossible. Your game ends here."
|
||||
dsc: """
|
||||
The Lair of Yog-Sothoth is a very *n'gai* cave, full of *buggs-shoggogs* and *n'ghaa ng'aa*.
|
||||
"""
|
||||
ways: ["shop"]
|
||||
units: [
|
||||
unit "bugg",
|
||||
dsc: "You see a particularly beautiful slimy {{bugg.}}"
|
||||
takeable: false
|
||||
act: (salet) =>
|
||||
salet.rooms[salet.current].drop("bugg")
|
||||
return "You eat the bugg mass. Delicious and raw. Perhaps it's a good lair to live in."
|
||||
]
|
||||
|
||||
phrase "Yes", salet, "merchant", """
|
||||
Yes.
|
||||
"""
|
||||
dialogue "No", salet, "merchant", "merchant", """
|
||||
No.
|
||||
"""
|
||||
room "sell-lamp", salet,
|
||||
ways: ["shop"]
|
||||
tags: ["merchant"]
|
||||
choices: ["#merchant"]
|
||||
optionText: "May I buy this lamp?"
|
||||
title: "Talking with merchant"
|
||||
canView: (salet) ->
|
||||
return salet.character.has("lamp") and salet.character.bought_lamp == false
|
||||
enter: (salet) ->
|
||||
salet.character.bought_lamp = true
|
||||
dsc: """
|
||||
"That'll be 30 pieces of your time."
|
||||
|
||||
You quickly pay the price and take the lamp as a rightful owner.
|
||||
"""
|
||||
|
||||
room "shop-inside", salet,
|
||||
ways: ["shop"]
|
||||
tags: ["merchant"]
|
||||
optionText: "End the conversation"
|
||||
title: "Inside the Shop"
|
||||
dsc: """
|
||||
The insides are painted pastel white, honouring The Great Milk Spill of 1985.
|
||||
"""
|
||||
units: [
|
||||
unit "merchant",
|
||||
dsc: "A {{merchant}} eyes you warily."
|
||||
takeable: false
|
||||
act: (salet) =>
|
||||
salet.processClick("merchdialogue")
|
||||
return ""
|
||||
]
|
||||
|
||||
lamp = unit "lamp",
|
||||
takeable: true
|
||||
lamp.put(salet, "shop-inside")
|
||||
|
||||
# The dialogue entry point has to be a room, in order to have an ID to go to.
|
||||
room "merchdialogue", salet,
|
||||
choices: "#merchant",
|
||||
dsc: """
|
||||
Nice day, isn't it?
|
||||
"""
|
|
@ -1,85 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Salet showcase</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href='https://fonts.googleapis.com/css?family=PT+Sans:400,400italic|PT+Sans+Caption' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="page" class="container">
|
||||
<div class="row">
|
||||
<div id="title" class="title">
|
||||
<div class="label">
|
||||
<h1>Salet</h1>
|
||||
<h2>A general cybertext IF engine</h2>
|
||||
<noscript>
|
||||
<p class="noscript_message">This game requires Javascript.</p>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content_wrapper" class="row">
|
||||
<div id="intro" class="content">
|
||||
<section>
|
||||
<!-- For the accessibility reasons, you really should begin your game here.
|
||||
Write a short intro the player will read while the Javascript is loading and executing.
|
||||
It has to be HTML but that shouldn't be a BIG problem if you're reading this.
|
||||
On the other hand this means the first intro passages will be non-interactive. -->
|
||||
<p><em>Salet</em> is an offspring of Undum and Raconteur.</p>
|
||||
<p>The project is still a work-in-progress. This "game" will show you some of the new features.</p>
|
||||
<p>It's supposed to be relatively painless for the author without sacrificing the reader's experience.
|
||||
For example, the game still loads while you're reading this.</p>
|
||||
<p><em>Salet</em> is <em>not</em> a library or a framework you can slap something on.
|
||||
It's more a game you can hack into your game. (Like Undum, yeah.)</p>
|
||||
<p>There are no either cool editor or clear instructions.
|
||||
You'll have to copy the source code and edit the CoffeeScript files in the <code>game</code> folder.
|
||||
Then <em>compile</em> them.</p>
|
||||
<p>This is neither a technical documentation nor UI tutorial, this is a <em>game.</em>
|
||||
You're supposed to note all the cool and curious features yourself.
|
||||
If you want some technical info, there are the <a href="http://git.oreolek.ru/oreolek/salet">source code</a> and a <a href="http://git.oreolek.ru/oreolek/salet/wikis/home">wiki.</a></p>
|
||||
<p>I'm just here to point out that you are playing this in the web browser, online.
|
||||
And you don't need to learn <em>The Complete Javascript, Volumes I-III</em> to write in this style.</p>
|
||||
<p>So let me show you... The World.</p>
|
||||
|
||||
<noscript>You need to turn on Javascript to play this game.</noscript>
|
||||
</section>
|
||||
</div>
|
||||
<div id="content" class="content">
|
||||
</div>
|
||||
<a name="end_of_content"></a>
|
||||
</div>
|
||||
<div id="tools_wrapper" class="row">
|
||||
<div class='ways'>
|
||||
<small class="text-muted" id="ways_hint">Click these links to explore other rooms</small>
|
||||
<ul class="nav nav-pills" id="ways">
|
||||
</ul>
|
||||
</div>
|
||||
</div> <!-- End of div.tools_wrapper -->
|
||||
|
||||
<div id="legal" class="row">
|
||||
<div id="footleft">
|
||||
<p>
|
||||
If you want to know how this game was made, check out <a
|
||||
href="https://git.oreolek.ru/oreolek/salet" target="_blank">the
|
||||
source code.</a>
|
||||
</p>
|
||||
</div>
|
||||
<div id="footright">
|
||||
<p id='buttons'>
|
||||
<a href="inventory"><button id="inventory" class="btn btn-outline-primary">Inventory</button></a>
|
||||
<button id="erase" class="btn btn-outline-danger">Restart</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End of div.page -->
|
||||
|
||||
<!-- CDN JS Libraries -->
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" integrity="sha384-QXBtGc4014gU26HdCwzgy8TVO+FHSSE4+EvPPiSTpdE9w0KyJy1ocfiIbBl1HLq7" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="//code.jquery.com/jquery-3.0.0.min.js"
|
||||
integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
|
||||
|
||||
<script type="text/javascript" src="game/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
172
lib/oneOf.coffee
172
lib/oneOf.coffee
|
@ -1,172 +0,0 @@
|
|||
###
|
||||
oneOf.js
|
||||
|
||||
Copyright (c) 2015 Bruno Dias
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
###
|
||||
|
||||
###
|
||||
Undularity Tools
|
||||
|
||||
Those functions are not a core part of Undularity, but provide some
|
||||
general functionality that relates to adaptive text generation.
|
||||
|
||||
This is provided partly as a helper to less technical users, and as
|
||||
a convenience for authors.
|
||||
###
|
||||
|
||||
# Monkey patching
|
||||
|
||||
###
|
||||
Shuffles an array. It can use Undum's random number generator implementation,
|
||||
so it expects a System.rnd object to be passed into it. If one isn't
|
||||
supplied, it will use Math.Random instead.
|
||||
|
||||
This is an implementation of the Fischer-Yates (Knuth) shuffle.
|
||||
|
||||
Returns the shuffled array.
|
||||
###
|
||||
|
||||
Array.prototype.shuffle = (system) ->
|
||||
rng = if system then system.rnd.random else Math.random
|
||||
# slice() clones the array. Object members are copied by reference, beware.
|
||||
newArr = this.slice()
|
||||
m = newArr.length
|
||||
|
||||
while (m)
|
||||
i = Math.floor(rng() * m--)
|
||||
t = newArr[m]
|
||||
newArr[m] = newArr[i]
|
||||
newArr[i] = t
|
||||
|
||||
return newArr
|
||||
|
||||
###
|
||||
oneOf()
|
||||
|
||||
Takes an array and returns an object with several methods. Each method
|
||||
returns an iterator which iterates over the array in a specific way:
|
||||
|
||||
inOrder()
|
||||
Returns the array items in order.
|
||||
|
||||
cycling()
|
||||
Returns the array items in order, cycling back to the first item when
|
||||
it runs out.
|
||||
|
||||
stopping()
|
||||
Returns the array items in order, then repeats the last item when it
|
||||
runs out.
|
||||
|
||||
randomly()
|
||||
Returns the array items at random. Takes a system object, for consistent
|
||||
randomness. Will never return the same item twice in a row.
|
||||
|
||||
trulyAtRandom()
|
||||
Returns the array items purely at random. Takes a system object, for
|
||||
consistent randomness.
|
||||
|
||||
inRandomOrder()
|
||||
Returns the array items in a random order. Takes a system object, for
|
||||
consistent randomness.
|
||||
###
|
||||
|
||||
###
|
||||
Takes a function and gives it a toString() property that calls itself and
|
||||
returns its value, allowing for ambiguous use of the closure object
|
||||
as a text snippet.
|
||||
|
||||
Returns the modified function.
|
||||
###
|
||||
stringish = (callback) ->
|
||||
callback.toString = () ->
|
||||
return '' + this.call()
|
||||
return callback
|
||||
|
||||
oneOf = (ary...) ->
|
||||
if ary.length == 0
|
||||
throw new Error(
|
||||
"tried to create a oneOf iterator with a 0-length array");
|
||||
|
||||
return {
|
||||
inOrder: () ->
|
||||
i = 0
|
||||
return stringish(() ->
|
||||
if i >= ary.length
|
||||
return null
|
||||
return ary[i++]
|
||||
)
|
||||
|
||||
cycling: () ->
|
||||
i = 0
|
||||
return stringish(() ->
|
||||
if (i >= ary.length)
|
||||
i = 0
|
||||
return ary[i++]
|
||||
)
|
||||
|
||||
stopping: () ->
|
||||
i = 0
|
||||
return stringish(() ->
|
||||
if (i >= ary.length)
|
||||
i = ary.length - 1
|
||||
return ary[i++]
|
||||
)
|
||||
|
||||
randomly: (system) ->
|
||||
rng = if system then system.rnd.random else Math.random
|
||||
last = null
|
||||
|
||||
if (ary.length<2)
|
||||
throw new Error("attempted to make randomly() iterator with a 1-length array")
|
||||
return stringish( () ->
|
||||
i = null
|
||||
offset = null
|
||||
if not last?
|
||||
i = Math.floor(rng() * ary.length)
|
||||
else
|
||||
###
|
||||
Let offset be a random number between 1 and the length of the
|
||||
array, minus one. We jump offset items ahead on the array,
|
||||
wrapping around to the beginning. This gives us a random item
|
||||
other than the one we just chose.
|
||||
###
|
||||
|
||||
offset = Math.floor(rng() * (ary.length -1) + 1);
|
||||
i = (last + offset) % ary.length;
|
||||
|
||||
last = i
|
||||
return ary[i]
|
||||
)
|
||||
|
||||
trulyAtRandom: (system) ->
|
||||
rng = if system then system.rnd.random else Math.random
|
||||
return stringish(() ->
|
||||
return ary[Math.floor(rng() * ary.length)];
|
||||
)
|
||||
|
||||
inRandomOrder: (system) ->
|
||||
shuffled = ary.shuffle(system)
|
||||
i = 0
|
||||
return stringish(() ->
|
||||
if (i >= ary.length)
|
||||
i = 0
|
||||
return shuffled[i++]
|
||||
)
|
||||
}
|
||||
|
||||
Array.prototype.oneOf = () ->
|
||||
oneOf.apply(null, this)
|
||||
String.prototype.oneOf = () ->
|
||||
return this
|
||||
|
||||
module.exports = oneOf;
|
207
lib/random.js
207
lib/random.js
|
@ -1,207 +0,0 @@
|
|||
// Random Number generation based on seedrandom.js code by David Bau.
|
||||
// Copyright 2010 David Bau, all rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or
|
||||
// without modification, are permitted provided that the following
|
||||
// conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above
|
||||
// copyright notice, this list of conditions and the
|
||||
// following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the
|
||||
// following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of this module nor the names of its
|
||||
// contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written
|
||||
// permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
var width = 256;
|
||||
var chunks = 6;
|
||||
var significanceExponent = 52;
|
||||
var startdenom = Math.pow(width, chunks);
|
||||
var significance = Math.pow(2, significanceExponent);
|
||||
var overflow = significance * 2;
|
||||
|
||||
var Random = (function () {
|
||||
var Random = function(seed) {
|
||||
this.random = null;
|
||||
if (!seed) throw {
|
||||
name: "RandomSeedError",
|
||||
message: "random_seed_error".l()
|
||||
};
|
||||
var key = [];
|
||||
mixkey(seed, key);
|
||||
var arc4 = new ARC4(key);
|
||||
this.random = function() {
|
||||
var n = arc4.g(chunks);
|
||||
var d = startdenom;
|
||||
var x = 0;
|
||||
while (n < significance) {
|
||||
n = (n + x) * width;
|
||||
d *= width;
|
||||
x = arc4.g(1);
|
||||
}
|
||||
while (n >= overflow) {
|
||||
n /= 2;
|
||||
d /= 2;
|
||||
x >>>= 1;
|
||||
}
|
||||
return (n + x) / d;
|
||||
};
|
||||
};
|
||||
// Helper type.
|
||||
var ARC4 = function(key) {
|
||||
var t, u, me = this, keylen = key.length;
|
||||
var i = 0, j = me.i = me.j = me.m = 0;
|
||||
me.S = [];
|
||||
me.c = [];
|
||||
if (!keylen) { key = [keylen++]; }
|
||||
while (i < width) { me.S[i] = i++; }
|
||||
for (i = 0; i < width; i++) {
|
||||
t = me.S[i];
|
||||
j = lowbits(j + t + key[i % keylen]);
|
||||
u = me.S[j];
|
||||
me.S[i] = u;
|
||||
me.S[j] = t;
|
||||
}
|
||||
me.g = function getnext(count) {
|
||||
var s = me.S;
|
||||
var i = lowbits(me.i + 1); var t = s[i];
|
||||
var j = lowbits(me.j + t); var u = s[j];
|
||||
s[i] = u;
|
||||
s[j] = t;
|
||||
var r = s[lowbits(t + u)];
|
||||
while (--count) {
|
||||
i = lowbits(i + 1); t = s[i];
|
||||
j = lowbits(j + t); u = s[j];
|
||||
s[i] = u;
|
||||
s[j] = t;
|
||||
r = r * width + s[lowbits(t + u)];
|
||||
}
|
||||
me.i = i;
|
||||
me.j = j;
|
||||
return r;
|
||||
};
|
||||
me.g(width);
|
||||
};
|
||||
// Helper functions.
|
||||
var mixkey = function(seed, key) {
|
||||
seed += '';
|
||||
var smear = 0;
|
||||
for (var j = 0; j < seed.length; j++) {
|
||||
var lb = lowbits(j);
|
||||
smear ^= key[lb];
|
||||
key[lb] = lowbits(smear*19 + seed.charCodeAt(j));
|
||||
}
|
||||
seed = '';
|
||||
for (j in key) {
|
||||
seed += String.fromCharCode(key[j]);
|
||||
}
|
||||
return seed;
|
||||
};
|
||||
var lowbits = function(n) {
|
||||
return n & (width - 1);
|
||||
};
|
||||
|
||||
return Random;
|
||||
})();
|
||||
|
||||
/* Returns a random floating point number between zero and
|
||||
* one. NB: The prototype implementation below just throws an
|
||||
* error, it will be overridden in each Random object when the
|
||||
* seed has been correctly configured. */
|
||||
Random.prototype.random = function() {
|
||||
throw {
|
||||
name:"RandomError",
|
||||
message: "random_error".l()
|
||||
};
|
||||
};
|
||||
/* Returns an integer between the given min and max values,
|
||||
* inclusive. */
|
||||
Random.prototype.randomInt = function(min, max) {
|
||||
return min + Math.floor((max-min+1)*this.random());
|
||||
};
|
||||
/* Returns the result of rolling n dice with dx sides, and adding
|
||||
* plus. */
|
||||
Random.prototype.dice = function(n, dx, plus) {
|
||||
var result = 0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
result += this.randomInt(1, dx);
|
||||
}
|
||||
if (plus) result += plus;
|
||||
return result;
|
||||
};
|
||||
/* Returns the result of rolling n averaging dice (i.e. 6 sided dice
|
||||
* with sides 2,3,3,4,4,5). And adding plus. */
|
||||
Random.prototype.aveDice = (function() {
|
||||
var mapping = [2,3,3,4,4,5];
|
||||
return function(n, plus) {
|
||||
var result = 0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
result += mapping[this.randomInt(0, 5)];
|
||||
}
|
||||
if (plus) result += plus;
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
/* Returns a dice-roll result from the given string dice
|
||||
* specification. The specification should be of the form xdy+z,
|
||||
* where the x component and z component are optional. This rolls
|
||||
* x dice of with y sides, and adds z to the result, the z
|
||||
* component can also be negative: xdy-z. The y component can be
|
||||
* either a number of sides, or can be the special values 'F', for
|
||||
* a fudge die (with 3 sides, +,0,-), '%' for a 100 sided die, or
|
||||
* 'A' for an averaging die (with sides 2,3,3,4,4,5).
|
||||
*/
|
||||
|
||||
Random.prototype.diceString = (function () {
|
||||
var diceRe = /^([1-9][0-9]*)?d([%FA]|[1-9][0-9]*)([-+][1-9][0-9]*)?$/;
|
||||
return function(def) {
|
||||
var match = def.match(diceRe);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"dice_string_error".l({string:def})
|
||||
);
|
||||
}
|
||||
|
||||
var num = match[1]?parseInt(match[1], 10):1;
|
||||
var sides;
|
||||
var bonus = match[3]?parseInt(match[3], 10):0;
|
||||
|
||||
switch (match[2]) {
|
||||
case 'A':
|
||||
return this.aveDice(num, bonus);
|
||||
case 'F':
|
||||
sides = 3;
|
||||
bonus -= num*2;
|
||||
break;
|
||||
case '%':
|
||||
sides = 100;
|
||||
break;
|
||||
default:
|
||||
sides = parseInt(match[2], 10);
|
||||
break;
|
||||
}
|
||||
return this.dice(num, sides, bonus);
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = Random;
|
|
@ -1,2 +0,0 @@
|
|||
These functions are not used by Salet core.
|
||||
But you can `require` them in your game and use thusly.
|
|
@ -1,26 +0,0 @@
|
|||
room = require("../room.coffee")
|
||||
###
|
||||
A dialogue shortcut.
|
||||
Usage:
|
||||
|
||||
dialogue "Point out a thing in her purse (mildly)", "start", "mild", """
|
||||
Point out a thing in her purse (mildly)
|
||||
""", "character.mild = true"
|
||||
###
|
||||
dialogue = (title, salet, startTag, endTag, text, effect) ->
|
||||
retval = room("dialogue_"+Object.keys(salet.rooms).length, salet, {
|
||||
optionText: title
|
||||
dsc: text
|
||||
clear: false # backlog is useful in dialogues
|
||||
choices: "#"+endTag
|
||||
})
|
||||
if typeof(startTag) == "string"
|
||||
retval.tags = [startTag]
|
||||
else if typeof(startTag) == "object"
|
||||
retval.tags = startTag
|
||||
if effect?
|
||||
retval.before = (character, system) ->
|
||||
eval(effect)
|
||||
return retval
|
||||
|
||||
module.exports = dialogue
|
|
@ -1,29 +0,0 @@
|
|||
room = require("../room.coffee")
|
||||
###
|
||||
A phrase shortcut.
|
||||
Usage:
|
||||
|
||||
phrase "Point out a thing in her purse (mildly)", "start", """
|
||||
Point out a thing in her purse (mildly)
|
||||
""", "character.sandbox.mild = true"
|
||||
|
||||
@param title phrase Phrase (question)
|
||||
@param salet Salet core
|
||||
@param string tag tag marking viewing condition
|
||||
@param string text Response
|
||||
@param string effect an optional parameter, eval'd code
|
||||
###
|
||||
phrase = (title, salet, tag, text, effect) ->
|
||||
retval = room("phrase_"+salet.rooms.length, salet, {
|
||||
optionText: title
|
||||
dsc: text
|
||||
clear: false # backlog is useful in dialogues
|
||||
choices: "#"+tag
|
||||
tags: [tag]
|
||||
})
|
||||
if effect?
|
||||
retval.before = (character, system) ->
|
||||
eval(effect)
|
||||
return retval
|
||||
|
||||
module.exports = phrase
|
|
@ -1,19 +0,0 @@
|
|||
@mixin halfcolumn() {
|
||||
@include make-col();
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-span(6);
|
||||
}
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
@include make-col-span(12);
|
||||
}
|
||||
}
|
||||
@mixin col($sm-width, $xs-width) {
|
||||
@include make-col();
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-span($sm-width);
|
||||
}
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
@include make-col-span($xs-width);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
$font-family-sans-serif: 'PT Sans','Open Sans',"Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$headings-font-family: "PT Sans Caption",$font-family-sans-serif;
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
|
||||
$body-bg: #F1EED9;
|
||||
$body-color: #58351A;
|
||||
$link-color: #382313;
|
||||
$btn-bg: #C33601;
|
||||
$btn-color: lighten($btn-bg, 50%);
|
||||
$secondary-bg: #F1EED9;
|
||||
|
||||
$waycolor: $link-color;
|
||||
$text_background: $body-bg; // can be btn-bg
|
209
sass/main.scss
209
sass/main.scss
|
@ -1,209 +0,0 @@
|
|||
@import "mixins";
|
||||
@import "variables";
|
||||
// Bootstrap v4 stripped core
|
||||
@import "../node_modules/bootstrap/scss/variables";
|
||||
@import "../node_modules/bootstrap/scss/mixins";
|
||||
@import "../node_modules/bootstrap/scss/normalize";
|
||||
@import "../node_modules/bootstrap/scss/print";
|
||||
@import "../node_modules/bootstrap/scss/reboot";
|
||||
@import "../node_modules/bootstrap/scss/type";
|
||||
@import "../node_modules/bootstrap/scss/images";
|
||||
@import "../node_modules/bootstrap/scss/grid";
|
||||
@import "../node_modules/bootstrap/scss/buttons";
|
||||
|
||||
@import "../node_modules/bootstrap/scss/nav";
|
||||
@import "../node_modules/bootstrap/scss/responsive-embed";
|
||||
@import "../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: $body-bg;
|
||||
}
|
||||
// The title block
|
||||
.title {
|
||||
margin-top: 2em;
|
||||
@include col(10,12);
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-offset(1);
|
||||
}
|
||||
.label {
|
||||
margin: 1em auto;
|
||||
max-width: 18em;
|
||||
text-align: center;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: smaller;
|
||||
color: #aaa;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
text-shadow: rgba(255,255,255,0.5) 2px 2px 2px,
|
||||
rgba(0,0,0,0.1) -1px -1px 2px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.warnings {
|
||||
font-size: small;
|
||||
font-style: italic;
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
.noscript_message {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: #943;
|
||||
}
|
||||
}
|
||||
#tools_wrapper {
|
||||
.ways {
|
||||
padding: 0.5em;
|
||||
@include col(8, 9);
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-offset(1);
|
||||
}
|
||||
}
|
||||
.destination {
|
||||
font-weight: bold;
|
||||
}
|
||||
.menu {
|
||||
@include col(3, 4);
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
#content_wrapper {
|
||||
background: $text_background;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.content {
|
||||
@include col(10, 12);
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-offset(1);
|
||||
}
|
||||
.pic {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
p {
|
||||
hyphens: auto;
|
||||
}
|
||||
padding: 1em;
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
ul.options {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.7em;
|
||||
list-style-type: none;
|
||||
border-radius: 4px;
|
||||
li {
|
||||
padding: 0.5em;
|
||||
}
|
||||
li:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
section {
|
||||
border-top: 1px dashed #bbb;
|
||||
}
|
||||
.situation-start {
|
||||
border-top: none;
|
||||
}
|
||||
img.right {
|
||||
float: right;
|
||||
margin: 1.1em 0 1.1em 1.1em;
|
||||
}
|
||||
img.left {
|
||||
float: left;
|
||||
margin: 1.1em 1.1em 1.1em 0;
|
||||
}
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
#legal {
|
||||
margin-top: 1em;
|
||||
color: darken($body-color, 10%);
|
||||
font-size: smaller;
|
||||
#footleft {
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col-offset(2);
|
||||
@include make-col(5);
|
||||
}
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
@include make-col(12);
|
||||
}
|
||||
}
|
||||
#footright {
|
||||
text-align: right;
|
||||
@media (min-width: breakpoint-min(sm)) {
|
||||
@include make-col(3);
|
||||
@include make-col-offset(2);
|
||||
}
|
||||
@media (max-width: breakpoint-max(xs)) {
|
||||
@include make-col(12);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.way {
|
||||
color: $waycolor;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.cycle {
|
||||
color: darkgreen;
|
||||
border-bottom: darkgreen dashed 1px;
|
||||
}
|
||||
ul.options {
|
||||
border: 1px solid #876;
|
||||
li {
|
||||
border-bottom: 1px solid #876;
|
||||
}
|
||||
li:hover {
|
||||
background-color: rgba(153,136,119,0.2);
|
||||
}
|
||||
}
|
||||
#legal {
|
||||
.muted {
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
width: 50%;
|
||||
border-color: $body-color;
|
||||
}
|
||||
|
||||
// fade-in animation
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
|
@ -36,5 +36,3 @@ class Character
|
|||
character = (spec) ->
|
||||
spec ?= {}
|
||||
return( new Character(spec) )
|
||||
|
||||
module.exports = character
|
|
@ -58,5 +58,3 @@ String.prototype.l = (args) ->
|
|||
new RegExp("\\{"+name+"\\}"), args[name]
|
||||
)
|
||||
return localized
|
||||
|
||||
module.exports = languages;
|
|
@ -35,5 +35,3 @@ markdown = (text) ->
|
|||
return marked(normaliseTabs(text), {
|
||||
smartypants: true
|
||||
})
|
||||
|
||||
module.exports = markdown
|
|
@ -1,6 +1,3 @@
|
|||
unit = require('./unit.coffee')
|
||||
markdown = require('./markdown.coffee')
|
||||
|
||||
assert = (msg, assertion) -> console.assert assertion, msg
|
||||
|
||||
Function.prototype.fcall = Function.prototype.call
|
||||
|
@ -255,5 +252,3 @@ room = (name, salet, spec) ->
|
|||
spec ?= {}
|
||||
spec.name = name
|
||||
return new SaletRoom(spec).register(salet)
|
||||
|
||||
module.exports = room
|
|
@ -1,9 +1,3 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
SaletView = require('./view.coffee')
|
||||
Random = require('./random.js')
|
||||
Character = require('./character.coffee')
|
||||
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();
|
||||
|
@ -36,6 +30,8 @@ class Salet
|
|||
@rnd = null
|
||||
@time = 0
|
||||
|
||||
@languages = languages
|
||||
|
||||
# Corresponding room names to room objects.
|
||||
@rooms = {}
|
||||
|
||||
|
@ -457,4 +453,10 @@ salet = (spec) ->
|
|||
retval.view.init(retval)
|
||||
return retval
|
||||
|
||||
module.exports = salet
|
||||
module.exports = {
|
||||
salet: salet
|
||||
room: room
|
||||
unit: unit
|
||||
markdown: markdown
|
||||
character: character
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
#require('./salet.coffee')
|
||||
unitlink = (content, ref) ->
|
||||
return "<a href='./_act_#{ref}' class='once'>#{content}</a>"
|
||||
|
||||
|
@ -53,4 +51,3 @@ unit = (name, spec) ->
|
|||
spec ?= {}
|
||||
spec.name = name
|
||||
return new SaletUnit(spec)
|
||||
module.exports = unit
|
|
@ -1,4 +1,3 @@
|
|||
markdown = require('./markdown.coffee')
|
||||
###
|
||||
Salet interface configuration.
|
||||
In a typical MVC structure, this is the View.
|
||||
|
@ -301,5 +300,3 @@ class SaletView
|
|||
|
||||
cycleLink: (content) ->
|
||||
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
|
||||
|
||||
module.exports = SaletView
|
Loading…
Reference in a new issue