Cloak of Darkness WIP in Salet

This commit is contained in:
Alexander Yakovlev 2017-05-15 17:28:53 +07:00
commit c72d6bad55
17 changed files with 815 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
build
dist
dist.zip

130
Gulpfile.coffee Normal file
View File

@ -0,0 +1,130 @@
browserSync = require('browser-sync')
gulp = require('gulp')
gutil = require('gulp-util')
coffee = require("gulp-coffee")
sass = require('gulp-sass')
uglify = require('gulp-uglify')
zip = require('gulp-zip')
concat = require('gulp-concat')
rename = require('gulp-rename')
fs = require 'fs'
CSON = require 'cson'
reload = browserSync.reload
html = (target) ->
return () ->
gulp.src(['html/*.html'])
.pipe(gulp.dest(target))
gulp.src(['node_modules/salet/lib/index.min.js'])
.pipe(rename('salet.min.js'))
.pipe(gulp.dest(target+"/game"))
# 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'))
# SCSS styles
gulp.task('sass', () ->
gulp.src('sass/main.scss')
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(gulp.dest('./build/css'))
)
gulp.task('concatCoffee', () ->
if !fs.existsSync('./build/game/translations')
fs.mkdirSync('./build/game/translations')
for language in ['ru', 'en']
data = CSON.parseCSONFile('game/translations/'+language+'.cson')
json = JSON.stringify(data) + '\n'
fs.writeFileSync("./build/game/translations/"+language+".json", json)
gulp.src([
## additional functions
'./game/dialogue.coffee',
'./game/phrase.coffee',
## the actual game
'./game/begin.coffee',
'./game/story.coffee',
]).pipe(concat('./main.coffee'))
.pipe(gulp.dest('./build/game'))
)
gulp.task('coffee', ['concatCoffee'], () ->
gulp.src('./build/game/main.coffee')
.pipe(coffee({bare: true}))
.pipe(gulp.dest('./build/game/'))
)
gulp.task('build', ['html', 'img', 'sass', 'coffee', 'audio'])
gulp.task('serve', ['build'], () ->
browserSync({
server: {
baseDir: 'build'
}
online: true
browser: []
ghostMode: false
})
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'))
)
gulp.task('coffee-dist', ['concatCoffee'], () ->
gulp.src('./build/game/main.coffee', {sourcemaps: false})
.pipe(coffee())
.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('.'))
)

123
game/begin.coffee Normal file
View File

@ -0,0 +1,123 @@
salet.game_id = "8b0c371c-57f4-49b3-ae3c-cba07d1a9733"
salet.game_version = "1.0"
salet.beforeAction = (roomId, actionId) ->
verbRe = /verb\_(?:\w+)\_(?:\w+)/
match = verbRe.exec(actionId)
if match? and match[1] and match[2]
verb = match[1]
unit = match[2]
salet.view.write(salet.rooms[roomId].units[unit][verb].fcall(salet.rooms[roomId]))
return true # consume the action
return false
$.holdReady( true )
$.getJSON('game/translations/'+i18n.lang+'.json', (data) ->
i18n.push(i18n.lang, data)
$.holdReady( false )
)
switchTab = (tabid) ->
$(".tab").removeClass("active")
$("#"+tabid).addClass("active")
$(document).ready(() ->
window.addEventListener('popstate', (event) ->
salet.goBack()
)
$("body").on("click", '#night', () ->
if (window.night)
$("body").removeClass("night")
$("#night").removeClass("active")
window.night = false
else
$("body").addClass("night")
$("#night").addClass("active")
window.night = true
)
$("body").on("click", "#storytab", (event) ->
event.preventDefault()
if not salet.here().canSave
salet.goBack()
return false
)
$("body").on("click", ".tab", (event) ->
switchTab(event.target.id)
return true
)
salet.beginGame()
)
updateverb = (unit, verb) ->
if unit[verb]? or salet.character.displayAll
$("##{verb}list").append("<li><a href='./verb_#{verb}_#{unit.name}'>#{unit.display()}</a></li>")
###
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>"
sysroom = (name, options) ->
options.canSave = false
options.enter = () ->
$("#inventory").hide()
options.exit = () ->
$("#inventory").show()
options.dsc = () ->
return @text.fcall()+"\n\n"+"""
<div class="center"><a href="./exit"><button class="btn btn-lg btn-outline-primary">Go back</button></a></div>
"""
options.actions = {
exit: () ->
return salet.goBack()
}
return room(name, options)
croom = (name, spec) ->
spec.clear ?= true
spec.optionColor ?= ""
spec.optionText ?= () ->
retval = """
<div class="#{spec.optionColor}">
<div class="title">#{spec.title.fcall()}</div>
"""
if (spec.subtitle?)
retval += """
<div class="subtitle">#{spec.subtitle.fcall()}</div>
"""
retval += '</div>'
spec.enter = () ->
salet.character.update_sidebar()
if @onEnter?
@onEnter()
return room(name, spec)
sysroom "inventory",
text: () ->
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"
sysroom "settings",
text: () ->
nightclass = ""
if window.night
nightclass = "active"
return "credits".l() + """
<button id="filter" class="btn btn-outline-primary}">#{"showall".l()}</button>
<button id="night" class="btn btn-outline-primary #{nightclass}">#{"night".l()}</button>
<button onclick="TogetherJS(this); return false;" class="btn btn-outline-primary">#{"multiplayer".l()}</button>
"""

23
game/dialogue.coffee Normal file
View File

@ -0,0 +1,23 @@
###
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, startTag, endTag, text, effect) ->
retval = room("dialogue_"+Object.keys(salet.rooms).length, {
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

26
game/phrase.coffee Normal file
View File

@ -0,0 +1,26 @@
###
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, tag, text, effect) ->
retval = room("phrase_"+salet.rooms.length, {
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

54
game/story.coffee Normal file
View File

@ -0,0 +1,54 @@
salet.init = () ->
@character.displayAll = false
@character.update_sidebar = () ->
$(".objects").empty()
for obj in document.querySelectorAll(".objects")
for u in salet.here().units
updateverb(u, $(obj).data("verb"))
for u in salet.character.inventory
updateverb(u, $(obj).data("verb"))
cloak = unit "cloak",
dsc: () -> "cloak".l()
display: () -> "cloak_disp".l()
drop: () ->
if (salet.currentRoom != 'cloakroom')
return "drop_cloak".l()
return "hang_cloak".l()
wear: () ->
if (salet.here().has('cloak'))
salet.here().drop('cloak')
salet.character.take('cloak')
return "wear_cloak".l()
else # no cloak in the room, maybe in the inventory?
if salet.character.has('cloak')
return "wear_cloak".l()
else
return "no_cloak".l()
@character.take(cloak)
croom "start",
before: () -> "start".l()
dsc: () -> "foyer".l()
ways: ["entrance", "cloakroom", "bar"]
croom "foyer",
clear: false
dsc: () -> "foyer".l()
ways: ["entrance", "cloakroom", "bar"]
title: () -> "foyer_title".l()
croom "cloakroom",
dsc: () -> "cloakroom".l()
title: () -> "cloakroom_title".l()
ways: ["foyer"]
croom "entrance",
dsc: () -> "entrance".l()
after: () ->
salet.goTo('foyer')
title: () -> "entrance_title".l()
croom "bar",
dsc: () -> "bar".l()
title: () -> "bar_title".l()
ways: ["foyer"]

75
game/translations/en.cson Normal file
View File

@ -0,0 +1,75 @@
foyer_title: "Foyer of the Opera House"
start: """
Hurrying through the rainswept November night, you're glad to see the bright
lights of the Opera House.
It's surprising that there aren't more people about but, hey, what do you
expect in a cheap demo game...?
### Cloak of Darkness
#### A basic IF demonstration
##### Roger Filth, remade in Salet by Alexander Yakovlev
"""
foyer: """
You are standing in a spacious hall, splendidly decorated in red
and gold, with glittering chandeliers overhead.
The entrance from the street is to the [north,](north) and there are doorways
[south](south) and [west.](west)
"""
credits: """
The game uses code licensed by MIT license.
Code contributors:
* Alexander Yakovlev
* Andrew Plotkin
* Bruno Dias
* David Eyk
* Dmitry Eliseev
* Ian Millington
* Ivan Narozhny
* Juhana Leinonen
* Reactive Sets
* Michael Neal Tenuis
* Selene
"""
entrance: """
You've only just arrived, and besides, the weather outside seems to be getting worse.
"""
cloakroom: """
The walls of this small room were clearly once lined with hooks,
though now [only one](./hook) remains. The exit is a door to the [east.](cloakroom)
"""
cloakroom_title: "Cloakroom"
entrance_title: "Street entrance"
bar_title: "Foyer bar"
bar: """
The bar, much rougher than you'd have guessed after the opulence
of the foyer to the north, is completely empty.
"""
message: """
There seems to be some sort of {{message}} scrawled in the sawdust on the floor.
"""
message_x: """
The message, neatly marked in the sawdust, reads...
### You have won
"""
message_ruined: """
The message has been carelessly trampled, making it difficult to read.
You can just distinguish the words...
### You have lost
"""
cloak: """
A handsome cloak, of velvet trimmed with satin, and slightly spattered with raindrops.
Its blackness is so deep that it almost seems to suck light from the room.
"""
cloak_disp: "cloak"
hook: "It's just a small brass hook,"
hook_empty: "screwed to the wall."
hook_full: "with {list} hanging on it."
dark: "Blundering around in the dark isn't a good idea!"
drop_cloak: "This isn't the best place to leave a smart cloak lying around."
hang_cloak: "There's a hook here, so you hang the cloak on it."
night: "Night mode"
multiplayer: "Multiplayer mode"
showall: "Filter list of objects"

17
game/translations/ru.cson Normal file
View File

@ -0,0 +1,17 @@
credits: """
Фоновое изображение: нет
Игра использует код по лицензии MIT. К коду приложили руку:
* Alexander Yakovlev
* Andrew Plotkin
* Bruno Dias
* David Eyk
* Dmitry Eliseev
* Ian Millington
* Ivan Narozhny
* Juhana Leinonen
* Reactive Sets
* Michael Neal Tenuis
* Selene
"""

70
html/en.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cloak of Darkness</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">
<div class="container">
<div class="row">
<div class='ways'>
<ul class="nav nav-pills" id="ways">
</ul>
</div>
</div> <!-- End of div.tools_wrapper -->
<div id="content_wrapper">
<div id="content" class="content narrow">
<noscript>This game requires Javascript.</noscript>
</div>
<div class="sidebar">
<div class="ui">
<a href="#" id="storytab" class="tab active">
<img src="img/white-book.png"> Story
</a>
<a href="inventory" id="chartab" class="tab">
<img src="img/light-backpack.png"> Inventory
</a>
<a href="map" id="maptab" class="tab">
<img src="img/compass.png"> Map
</a>
</div>
<div class="action">
<div class="verb">Examine</div>
<ul class="objects" id="examinelist" data-verb="examine">
</ul>
<div class="verb">Take</div>
<ul class="objects" id="takelist" data-verb="take">
</ul>
<div class="verb">Drop</div>
<ul class="objects" id="droplist" data-verb="drop">
</ul>
<div class="verb">Wear</div>
<ul class="objects" id="wearlist" data-verb="wear">
</ul>
</div>
</div>
<a name="end_of_content"></a>
</div>
<div class="row">
<div class="footer">
<a href="settings">
<button class="btn btn-outline-primary">Options</button>
</a>
<button id="erase" class="btn btn-outline-danger">Restart</button>
</div>
</div>
</div> <!-- End of div.page -->
<!-- CDN JS Libraries -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.1.1.min.js" crossorigin="anonymous"></script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
<script type="text/javascript" src="game/salet.min.js"></script>
<script type="text/javascript" defer="defer" src="game/main.js"></script>
</body>
</html>

66
html/index.html Normal file
View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Инженер</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">
<div class="container">
<div class="row">
<div class='ways'>
<ul class="nav nav-pills" id="ways">
<li class='nav-item'><a class='nav-link' href="djsd">Жестиана</a></li>
<li class='nav-item'><a class='nav-link' href="fff">Дом на окраине</a></li>
<li class='nav-item'><a class='nav-link' href="33">Заброшенный дом</a></li>
</ul>
</div>
</div> <!-- End of div.tools_wrapper -->
<div id="content_wrapper">
<div id="content" class="content narrow">
<noscript>Эта игра требует включённого Javascript.</noscript>
</div>
<div class="sidebar">
<div class="ui">
<a href="#" id="storytab" class="tab active">
<img src="img/white-book.png"> Повествование
</a>
<a href="character" id="chartab" class="tab">
<img src="img/light-backpack.png"> Инвентарь
</a>
<a href="map" id="maptab" class="tab">
<img src="img/compass.png"> Карта
</a>
</div>
<div class="action">
<div class="verb">ГОВОРИТЬ</div>
<ul>
<li><a href="talk-npc1">NPC 1</a></li>
<li><a href="talk-npc2">NPC 2</a></li>
</ul>
</div>
</div>
<a name="end_of_content"></a>
</div>
<div class="row">
<div class="footer">
<a href="settings">
<button class="btn btn-outline-primary">Настройки</button>
</a>
<button id="erase" class="btn btn-outline-danger">Заново</button>
</div>
</div>
</div> <!-- End of div.page -->
<!-- CDN JS Libraries -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.1.1.min.js" crossorigin="anonymous"></script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
<script type="text/javascript" src="game/salet.min.js"></script>
<script type="text/javascript" defer="defer" src="game/main.js"></script>
</body>
</html>

BIN
img/compass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
img/double-face-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
img/light-backpack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

BIN
img/white-book.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"dependencies": {
"salet": "^1.7.5"
},
"private": true,
"devDependencies": {
"bootstrap": "^4.0.0-alpha.5",
"browser-sync": "^2.18.8",
"coffee-script": "^1.12.6",
"cson": "^4.1.0",
"gulp": "^3.8.11",
"gulp-coffee": "^2.3.4",
"gulp-concat": "^2.6.1",
"gulp-rename": "^1.2.2",
"gulp-sass": "^3.1.0",
"gulp-uglify": "^2.1.2",
"gulp-util": "^3.0.8",
"gulp-zip": "^4.0.0"
}
}

31
sass/_variables.scss Normal file
View File

@ -0,0 +1,31 @@
$font-family-sans-serif: 'Scada', 'PT Sans', sans-serif;
$font-family-serif: 'PT Serif', serif;
$headings-font-family: $font-family-serif;
//$headings-font-family: "PT Sans Caption",$font-family-sans-serif;
$font-family-base: $font-family-serif;
$body-bg: #eee;
$body-color: #000;
$link-color: darkviolet;
$btn-bg: grey;
$btn-color: lighten($btn-bg, 50%);
$secondary-bg: #F1EED9;
$brand-primary: lighten($body-color, 20%);
$brand-danger: darken(#fff, 30%);
$waycolor: $link-color;
$text_background: transparent;
$animation_duration: 2s;
$enable-rounded: true;
$enable-shadows: false;
$enable-gradients: false;
$enable-transitions: false;
$enable-hover-media-query: false;
$enable-grid-classes: false;
$enable-print-styles: true;
$ok-color: $link-color;
$neutral-color: brown;
$warning-color: darkred;

176
sass/main.scss Normal file
View File

@ -0,0 +1,176 @@
@import "variables";
@import "../node_modules/bootstrap/scss/bootstrap.scss";
.fixed {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 1000;
width: 100%;
}
body {
// background-image: url('../img/background.png');
background-image: radial-gradient(circle,rgba(0,0,0,.0),rgba(0,0,0,.3));
}
.container {
@include make-container();
@include make-container-max-widths();
border-radius: 5px;
// border-style: solid;
// border-width: 81px 103px 77px 66px;
// border-image: url(../img/border.png) 81 103 77 66 repeat;
}
#ways_wrapper {
@include make-row();
}
#ways {
@include media-breakpoint-up(md) {
@include make-col(6);
@include make-col-offset(3);
}
text-align: center;
.nav-item {
list-style-type: none;
display: inline-block;
@extend .btn;
@extend .btn-outline-primary;
a {
color: $body-color;
}
&:hover a {
color: #fff;
}
}
}
#content_wrapper {
@include make-row();
background: $text_background;
}
.sidebar {
@include media-breakpoint-up(md) {
@include make-col(3);
}
.verb {
margin-top: 1em;
text-align: center;
font-size: larger;
text-transform: uppercase;
}
li {
@extend .btn;
@extend .btn-outline-danger;
display: block;
text-align: left;
}
}
.content {
@include make-col(12);
@include media-breakpoint-up(md) {
@include make-col(10);
@include make-col-offset(1);
}
&.narrow {
@include media-breakpoint-up(md) {
@include make-col(9);
margin-left: 0;
}
}
padding: 1em;
ul {
margin: 0;
padding: 0 0 0 1em;
}
ul.options {
padding: 0;
margin-top: 0.5em;
margin-bottom: 0.7em;
list-style-type: none;
li {
display: inline;
}
li a {
display: block;
margin-bottom: 0.5em;
font-family: $headings-font-family;
text-decoration: none;
> div {
border-radius: 5px;
border: 1px solid #000;
padding: 1em;
background-image: linear-gradient( 45deg, #ccc, #fff );
color: $ok-color;
&:hover {
background-color: rgba(153,136,119,0.2);
background-image: none;
}
}
}
.warning {
background-image: linear-gradient( 45deg, #ddd, #fff );
color: $warning-color;
}
.neutral {
background-image: linear-gradient( 90deg, #ccc, #fff );
color: $neutral-color;
}
}
.room-start {
border-top: none;
}
h3, h4, h5 {
text-align: center;
}
blockquote {
font-family: "EB Garamond", serif;
margin: 1em 2em;
line-height: 1.45;
color: #383838;
font-size: $font-size-base* 1.4;
}
}
.cycle {
color: darkgreen;
border-bottom: darkgreen dashed 1px;
}
hr {
width: 50%;
border-color: $body-color;
}
.btn-outline-primary,
.btn-outline-danger {
border: none;
border-color: transparent;
}
.center {
text-align: center;
}
.gothic {
font-family: "Germania One", cursive;
}
.footer {
@include media-breakpoint-up(md) {
@include make-col(6);
@include make-col-offset(3);
}
text-align: center;
}
.tab {
width: 100%;
position: relative;
text-align: left;
display: block;
padding: 14px 21px;
border-radius: 2px 2px 0 0;
font-size: $font-size-base;
font-weight: normal;
top: 4px;
&:hover {
background: lighten($brand-primary, 10);
}
&.active {
top: 0;
padding-top: 17px;
background: darken($body-bg, 15);
}
}