From f497b78e27c0ad66269db1a4acd7a3ae649b8e79 Mon Sep 17 00:00:00 2001 From: Alexander Yakovlev Date: Wed, 20 Jan 2016 23:48:30 +0700 Subject: [PATCH] Salet conversion WIP --- Gulpfile.coffee | 131 ++++ Gulpfile.js | 184 ----- LICENSE.txt | 4 +- game/begin.coffee | 98 +-- game/end.coffee | 18 +- game/story.coffee | 119 ++-- html/index.html | 41 +- lib/cycle.coffee | 19 + lib/dialogue.coffee | 34 + lib/interface.coffee | 16 + lib/localize.coffee | 60 ++ lib/markdown.coffee | 36 + lib/obj.coffee | 50 ++ lib/oneOf.coffee | 170 +++++ lib/random.js | 207 ++++++ lib/room.coffee | 277 ++++++++ lib/undum.js | 1617 ++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +- sass/main.scss | 27 +- 19 files changed, 2729 insertions(+), 392 deletions(-) create mode 100644 Gulpfile.coffee delete mode 100644 Gulpfile.js create mode 100644 lib/cycle.coffee create mode 100644 lib/dialogue.coffee create mode 100644 lib/interface.coffee create mode 100644 lib/localize.coffee create mode 100644 lib/markdown.coffee create mode 100644 lib/obj.coffee create mode 100644 lib/oneOf.coffee create mode 100644 lib/random.js create mode 100644 lib/room.coffee create mode 100644 lib/undum.js diff --git a/Gulpfile.coffee b/Gulpfile.coffee new file mode 100644 index 0000000..19807d9 --- /dev/null +++ b/Gulpfile.coffee @@ -0,0 +1,131 @@ +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') +_ = 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)); + +img = (target) -> + return () -> + return gulp.src(['img/*.png', 'img/*.jpeg', 'img/*.jpg']) + .pipe(gulp.dest(target)); + +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')); +) + +sources = [ + './game/begin.coffee', + './game/story.coffee', + './game/end.coffee', +] + +opts = _.assign({}, watchify.args, { + entries: ["./build/game/main.coffee"] + debug: true + transform: [coffeify] +}); +bundler = watchify(browserify(opts)) + +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(sources) + .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(['./lib/*.coffee', './lib/*.js', './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('.')); +); diff --git a/Gulpfile.js b/Gulpfile.js deleted file mode 100644 index 0415f22..0000000 --- a/Gulpfile.js +++ /dev/null @@ -1,184 +0,0 @@ -'use strict'; - -/* Imports */ -var 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'), - sass = require('gulp-sass'), - minifyCSS = require('gulp-minify-css'), - uglify = require('gulp-uglify'), - buffer = require('vinyl-buffer'), - zip = require('gulp-zip'), - _ = require('lodash'), - concat = require('gulp-concat'); - -var reload = browserSync.reload; - -/* Tasks */ - -/* Trivial file copies */ - -function html (target) { - return function () { - return gulp.src(['html/index.html','html/en.html']) - .pipe(gulp.dest(target)); - }; -} - -function img (target) { - return function () { - return gulp.src(['img/*.png', 'img/*.jpeg', 'img/*.jpg']) - .pipe(gulp.dest(target)); - }; -} - -function audio (target) { - return function () { - 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')); - -/* Less */ - -gulp.task('sass', function () { - gulp.src('sass/main.scss') - .pipe(sass().on('error', sass.logError)) - .pipe(gulp.dest('./build/css')); -}); - -/* Bundle libraries */ - -var undumBundler = browserify({debug: true}); -undumBundler.require('undum-commonjs'); - -gulp.task('buildUndum', function () { - return undumBundler.bundle().pipe(source('undum.js')).pipe(gulp.dest('./build/game')); -}); - -/* Generate JavaScript with browser sync. */ - -var sources = [ - './game/begin.coffee', - './game/story.coffee', - './game/end.coffee', -] - -var opts = _.assign({}, watchify.args, { - entries: ["./build/game/main.coffee"], - debug: true, - transform: [coffeify] -}); -var bundler = watchify(browserify(opts)); -bundler.external('undum-commonjs'); - -function bundle () { - return bundler.bundle() - .on('error', gutil.log.bind(gutil, 'Browserify Error')) - .pipe(source('bundle.js')) - .pipe(gulp.dest('./build/game')); -}; - -gulp.task('concatCoffee', function() { - return gulp.src(sources) - .pipe(concat('./main.coffee')) - .pipe(gulp.dest('./build/game')); -}); - -// `gulp coffee` will generate bundle -gulp.task('coffee', ['buildUndum', 'concatCoffee'], bundle); - -bundler.on('update', bundle); // Re-bundle on dep updates -bundler.on('log', gutil.log); // Output build logs to terminal - -/* Make a development build */ - -gulp.task('build', ['html', 'img', 'sass', 'coffee', 'audio'], function () { - -}); - -/* Start a development server */ - -gulp.task('serve', ['build'], function () { - browserSync({ - server: { - baseDir: 'build' - } - }); - - var sassListener = function () { - 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); -}); - -/* Distribution tasks */ - -var undumDistBundler = browserify(); -undumDistBundler.require('undum-commonjs'); - -gulp.task('undum-dist', function () { - return undumDistBundler.bundle().pipe(source('undum.js')) - .pipe(buffer()) - .pipe(uglify()) - .pipe(gulp.dest('./dist/game')); -}); - -gulp.task('html-dist', html('./dist')); -gulp.task('img-dist', img('./dist/img')); -gulp.task('audio-dist', audio('./dist/audio')); -gulp.task('legal-dist', function () { - return gulp.src(['LICENSE.txt']) - .pipe(gulp.dest("./dist")); -}); - -gulp.task('sass-dist', function () { - return gulp.src('./sass/main.scss') - .pipe(sass({outputStyle: 'compressed'})) - .pipe(gulp.dest('./dist/css')); -}); - -var distBundler = browserify({ - debug: false, - entries: ['./build/game/main.coffee'], - transform: ['coffeeify'] -}); - -distBundler.external('undum-commonjs'); - -gulp.task('coffee-dist', ['undum-dist', 'concatCoffee'], function () { - 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'], - function () { - return; -}); - -gulp.task('zip', ['dist'], function () { - return gulp.src('dist/**') - .pipe(zip('dist.zip')) - .pipe(gulp.dest('.')); -}); diff --git a/LICENSE.txt b/LICENSE.txt index 7c5f2d2..828576b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,6 @@ -Copyright (c) 2015-2016 Alexander Yakovlev, https://oreolek.ru/ +Copyright (c) 2016 Alexander Yakovlev, https://oreolek.ru/ +Raconteur is copyright (c) 2015 Bruno Dias, released under the similar license terms. +Undum is copyright (c) 2009-2015 Ian Millington, released under the similar license terms. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/game/begin.coffee b/game/begin.coffee index b91df35..df15015 100644 --- a/game/begin.coffee +++ b/game/begin.coffee @@ -2,54 +2,32 @@ # 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 -situation = require('raconteur') -undum = require('undum-commonjs') -oneOf = require('raconteur/lib/oneOf.js') -qualities = require('raconteur/lib/qualities.js') -$ = require("jquery") - -Array.prototype.oneOf = () -> - oneOf.apply(null, this) - -md = require('markdown-it') -markdown = new md({ - typographer: true, - html: true -}) - -shortid = require('shortid') -# you have to alter linkRe in Undum core to use that. -# Undum doesn't allow using uppercase letters in situation names by default. +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') undum.game.id = "6a9909a4-586a-4089-bd18-26da684d1c8d" -undum.game.version = "1.0" +undum.game.version = "2.0" + +way_to = (content, ref) -> + return "#{content}" +textlink = (content, ref) -> + return "#{content}" +actlink = (content, ref) -> + return "#{content}" +textcycle = (content, ref) -> + return "#{content}"# usage: writemd( system, "Text to write") -a = require('raconteur/lib/elements.js').a -way_to = (content, ref) -> a(content).class('way').ref(ref) -textlink = (content, ref) -> a(content).once().writer(ref) -actlink = (content, ref) -> a(content).once().action(ref) -textcycle = (content, ref) -> a(content).replacer(ref).class("cycle").id(ref).toString() -# usage: writemd( system, "Text to write") writemd = (system, text) -> - if typeof text is Function - text = text() - text = markdown.render(text) + text = markdown(text) system.write(text) -preparemd = (text, mark) -> - if typeof text is Function - text = text() - text = markdown.render(text) - if mark? - text = """ -
- #{text} -
- """ - return text - -money = (character, system, amount) -> - system.setQuality("money", character.qualities.money + amount) +money = (character, amount) -> + character.sandbox.money = character.sandbox.money + amount code_can_input = (character) -> return character.sandbox.code.length < 8 @@ -108,40 +86,12 @@ code_check = (character, system) -> character.sandbox.code = "" -update_ways = (ways) -> - content = "" - for way in ways - if undum.game.situations[way]? - content += way_to(undum.game.situations[way].title, way) - $("#ways").html(content) - -situation "start", - content: """ - Peter had so much trouble sleeping he had to drown his pills in at least an hour of thoughts. - - A violent ringing of the bell awakened him. - He rose from the bed, grumbling: - “Crazy neighbors and their guests. It must be three o'clock!” - - The visitor entered the hallway. - It was him ringing the bell, but he was not going to meet Peter. - In fact, he wasn't looking for meeting anybody here. - - Fourth floor, apartment 406. - There, he tried two keys. - The second of them fitted the lock. - - Burglary is a curious line of employment. - Befittedly, Ronald Chernoff was very curious about a black phone behind the door of apartment 406 in a wooden box on a small table no farther than two meters from the bed. - A gift, a prototype, a valuable treasure left by Anastasia Kozlowa when she fled the country. - Of course, one had to be reasonably au fait with her *Instagram* to notice that. - +room "start", + 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 [the living room.](living-room) - -
+ He began the inspection from #{way_to('the living room.', 'living-room')} """ is_visited = (situation) -> @@ -153,7 +103,7 @@ is_visited = (situation) -> # N-th level examine function level = (text, mark) -> $("#content .#{mark}").fadeOut() - return preparemd(text, mark) + return markdown(text, mark) lvl1 = (text) -> $("#content .lvl2").fadeOut() diff --git a/game/end.coffee b/game/end.coffee index 71e69dc..b30cd97 100644 --- a/game/end.coffee +++ b/game/end.coffee @@ -1,24 +1,8 @@ -qualities - general: - money: qualities.integer('Money'), - undum.game.init = (character, system) -> - $("#ways").on("click", "a", (event) -> - event.preventDefault() - undum.processClick($(this).attr("href")) - ) _paq.push(['setCustomDimension', 1, false]) - # If you use only once() links you can use this "hack": - document.onmousedown = (e) -> - e.target.click() - # It makes every click slightly faster because the game responds after the user presses the mouse button, - # not after he presses and releases it. - # - # Another thing to bear in mind: this game is not a typical Undum game. - # It deliberately repeats the situations, so you can repeat "once" links once you re-enter the situation. - # So this game has no need in repeatable links at all and this hack is useful. character.sandbox.view_smash = 1 + character.sandbox.money = 0 character.sandbox.code = "" character.sandbox.knows_the_code = 0 character.sandbox.box_opened = 0 diff --git a/game/story.coffee b/game/story.coffee index 0c7bbb1..b0b4f58 100644 --- a/game/story.coffee +++ b/game/story.coffee @@ -1,44 +1,40 @@ -situation "living-room", +room "living-room", title: "Living room" - before: () -> - if not $(".ways h2").is(':visible') - $(".ways h2").fadeIn() - update_ways(this.ways) - audio = document.getElementById("bgsound") - audio.currentTime=0 - audio.volume = 0.5 - audio.play() + enter: (character, system, from) -> + if (from == "start") + audio = document.getElementById("bgsound") + audio.currentTime=0 + audio.volume = 0.5 + audio.play() ways: ["bedroom", "kitchen", "balcony"] - content: """ - Ronald is standing in a dark room with a big #{textlink("window.", "window")} - - #{textlink("The walls", "walls")} are covered with a dingy rose wallpaper. - + dsc: """ #{textlink("A book stand", "bookcase")} is hanging above #{textlink("a television set.", "tv")} - - Oh, and #{actlink("the door Ronald came into", "door")} the apartment is there, too. """ - actions: - door: (character, system) -> - if character.sandbox.box_opened == 0 - writemd(system, lvl1("Ronald has a job here. It's still on.")) - else - system.doLink("exitdoor") - writers: - walls: (character, system) -> - lvl1(""" + objects: + window: 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", + dsc: "{{The walls}} are covered with a dingy rose wallpaper." + act: """ There are colorful photographs on the walls. A wooden house in a forest. A village on a mountaintop. A family sitting around a fire. A sunset burning in a vast ocean. A black monolith standing on sand dunes. - """) - window: (character, system) -> - lvl1(""" - The moon is full today. - It illuminates the apartment, makes the things stand out in some weird angles. - """) + """ + 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.")) + 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... @@ -88,7 +84,7 @@ situation "living-room", No need to read it, not a bit. """) else - money(character, system, 20000) + money(character, 20000) lvl2(""" Nietsche's four-part novel about The Man, The Superman and everything in-between. It's surprisingly worn down. @@ -117,12 +113,10 @@ situation "living-room", An expensive 40-something inch TV standing on a stylish black stand. The room looks kinda small for that monster. """) -situation "bedroom", - before: () -> - update_ways(this.ways) +room "bedroom", title: "Bedroom" ways: ["living-room", "kitchen", "bathroom"] - content: (character, system) -> + dsc: (character, system) -> return """ The bedroom is spacious; its walls are lavender green, almost white in the moonlight. @@ -188,7 +182,7 @@ situation "bedroom", """) else character.sandbox.seen_coat = 1 - money(character, system, 4000) + money(character, 4000) return lvl2(""" A warm coat.. hey, what's this? One of the pockets is loaded with cash! @@ -264,7 +258,7 @@ situation "bedroom", """) money: (character, system) -> character.sandbox.seen_safe = 1 - money(character, system, 50000) + money(character, 50000) lvl4(""" It's a big cash. Odd that she didn't take this when she left. @@ -278,12 +272,10 @@ situation "bedroom", The sketch is signed: *"L. Y. - 2017"* """) -situation "kitchen", - before: () -> - update_ways(this.ways) +room "kitchen", title: "Kitchen" ways: ["living-room", "bedroom"] - content: """ + 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. """ writers: @@ -318,22 +310,19 @@ situation "kitchen", He's sure it's recent (`24.03.2018`) and it's about something-something QUANTUM AUDIO.. armement? """) -situation "bathroom", +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) - update_ways(undum.game.situations["bedroom"].ways) return false title: "Bathroom" ways: ["bedroom"] -situation "balcony", - before: () -> - update_ways(this.ways) +room "balcony", title: "Balcony" ways: ["living-room"] - content: """ + dsc: """ A small glazed-in empty balcony. It's an amazing night. The whole town is lit by moonlight, standing perfectly still. @@ -359,12 +348,10 @@ situation "balcony",     *L. Y.* """) -situation "box", - before: () -> - update_ways(this.ways) +room "box", ways: ["bedroom"] choices: "#box" - content: (character, system) -> + dsc: (character, system) -> return """ It's a red wood, very expensive. And this box is locked with a digital code key. @@ -379,8 +366,7 @@ situation "box", } """ -# no need to call update_ways, it's the same location -situation "smash", +room "smash", canView: (character) -> character.sandbox.view_smash == 1 optionText: "Smash the box" @@ -388,10 +374,10 @@ situation "smash", character.sandbox.view_smash = 0 choices: "#box" tags: ["box"] - content: "Ronald still needs the phone in this box. A very high-tech fragile phone. Smashing isn't an option." + dsc: "Ronald still needs the phone in this box. A very high-tech fragile phone. Smashing isn't an option." safe_button = (number) -> - situation "put#{number}", + room "put#{number}", choices: "#box" tags: ["box"] optionText: "Enter #{number}" @@ -401,7 +387,7 @@ safe_button = (number) -> code_can_input(character) after: (character, system) -> code_check(character, system) - content: (character) -> """ + dsc: (character) -> """ Ronald presses button #{number}. The display is #{code_print(character)} now. """ @@ -410,22 +396,20 @@ safe_button(2) safe_button(7) safe_button(0) -situation "reset", +room "reset", choices: "#box" tags: ["box"] optionText: "Reset the display" before: (character) -> code_reset(character) - content: """ + dsc: """ Ronald presses Backspace until the display is empty. """ -situation "exitdoor", - before: () -> - update_ways(this.ways) +room "exitdoor", ways: ["living-room"] choices: "#door" - content: """ + dsc: """ Ronald is ready to go. Maybe he's satisfied with his explorations or just wants to finish this. But then a new problem arrives. @@ -433,15 +417,14 @@ situation "exitdoor", Someone's shadow is under the doorframe. """ -situation "finale", +room "finale", before: () -> _paq.push(['setCustomDimension', 1, true]) $("#tools_wrapper").hide() - update_ways(this.ways) optionText: "Use the Phone" tags: ["door"] ways: [] - content: (character, system) -> """ + dsc: (character, system) -> """ "LOADING... 100%" Ronald opens the door and presses his finger to the phone screen. @@ -460,8 +443,8 @@ situation "finale", “Well, that was a good night.” - #{if character.qualities.money > 0 - "The pocket is heavy with #{character.qualities.money} rubles and the phone." + #{if character.sandbox.money > 0 + "The pocket is heavy with #{character.sandbox.money} rubles and the phone." else "The phone is heavy in the pocket." } diff --git a/html/index.html b/html/index.html index 36e4343..ac6a84f 100644 --- a/html/index.html +++ b/html/index.html @@ -16,24 +16,37 @@ -

click to begin

-
+
+
+

Peter had so much trouble sleeping he had to drown his pills in at least an hour of thoughts.

+ +

A violent ringing of the bell awakened him. + He rose from the bed, grumbling: + “Crazy neighbors and their guests. It must be three o'clock!”

+ +

The visitor entered the hallway. + It was him ringing the bell, but he was not going to meet Peter. + In fact, he wasn't looking for meeting anybody here.

+ +

Fourth floor, apartment 406. + There, he tried two keys. + The second of them fitted the lock.

+ +

Burglary is a curious line of employment. + Befittedly, Ronald Chernoff was very curious about a black phone behind the door of apartment 406 in a wooden box on a small table no farther than two meters from the bed. + A gift, a prototype, a valuable treasure left by Anastasia Kozlowa when she fled the country. + Of course, one had to be reasonably au fait with her Instagram to notice that.

+ +
+
-
-
-
-
-
- -
-

Other rooms

@@ -46,9 +59,8 @@