├── .babelrc
├── .editorconfig
├── .flowconfig
├── .github
└── FUNDING.yml
├── .gitignore
├── .jshintrc
├── .vscode
└── settings.json
├── Gemfile
├── Gemfile.lock
├── LICENSE.md
├── NOTES
├── PATCH_NOTES
├── README.md
├── assets.psd
├── client
├── behaviors
│ ├── Activatable.js
│ ├── BattleBehaviour.js
│ ├── CharacterController.js
│ ├── Chat.js
│ ├── DangerousThing.js
│ ├── GameObject.js
│ ├── HUDController.js
│ ├── HasLifebar.js
│ ├── InventoryBehaviour.js
│ ├── LightOscillator.js
│ ├── NearPlayerOpacity.js
│ ├── Openable.js
│ ├── Pickable.js
│ ├── QuestIndicator.js
│ ├── Raycaster.js
│ ├── Shadow.js
│ └── Stretchable.js
├── config.js
├── core
│ ├── App.js
│ ├── PlayerPrefs.js
│ ├── network.js
│ ├── sound.js
│ └── utils.js
├── css
│ ├── ads.styl
│ ├── credentials.styl
│ ├── fonts.styl
│ ├── fonts
│ │ ├── 5x5_pixel.eot
│ │ ├── 5x5_pixel.otf
│ │ ├── 5x5_pixel.svg
│ │ ├── 5x5_pixel.ttf
│ │ ├── 5x5_pixel.woff
│ │ ├── 5x5_pixel.woff2
│ │ ├── enchanted_land-webfont.otf
│ │ ├── enchanted_land-webfont.ttf
│ │ ├── enchanted_land-webfont.woff
│ │ └── enchanted_land-webfont.woff2
│ ├── hint.styl
│ ├── main.styl
│ ├── modal.styl
│ ├── sprites
│ │ └── index.styl
│ └── tutorial.styl
├── dungeon_viewer.js
├── elements
│ ├── Aesthetic.js
│ ├── Character.js
│ ├── CheckPoint.js
│ ├── Chest.js
│ ├── Door.js
│ ├── Enemy.js
│ ├── Fountain.js
│ ├── Item.js
│ ├── Jail.js
│ ├── Leaderboard.js
│ ├── Lever.js
│ ├── Lifebar.js
│ ├── LightPole.js
│ ├── NPC.js
│ ├── Portal.js
│ ├── Rock.js
│ ├── StunTile.js
│ ├── TeleportTile.js
│ ├── TextEvent.js
│ ├── TileSelectionPreview.js
│ ├── character
│ │ ├── Composition.js
│ │ └── HeroSkinBuilder.js
│ ├── controls
│ │ ├── Button.js
│ │ ├── Checkbox.js
│ │ ├── ColorPicker.js
│ │ ├── SelectBox.js
│ │ └── ToggleButton.js
│ ├── effects
│ │ ├── Highlight.js
│ │ ├── LevelUp.js
│ │ ├── Projectile.js
│ │ └── SkillUse.js
│ ├── hud
│ │ ├── Character.js
│ │ ├── CheckPointSelector.js
│ │ ├── Cursor.js
│ │ ├── Hint.js
│ │ ├── HorizontalBar.js
│ │ ├── InventoryBagIcon.js
│ │ ├── LeaderboardOverlay.js
│ │ ├── LevelUpButton.js
│ │ ├── Minibar.js
│ │ ├── NewQuestOverlay.js
│ │ ├── QuestsButton.js
│ │ ├── Resources.js
│ │ ├── SettingsOverlay.js
│ │ ├── SkillButton.js
│ │ └── VerticalBar.js
│ └── inventory
│ │ ├── ConsumableShortcut.js
│ │ ├── EquipedItems.js
│ │ ├── Inventory.js
│ │ ├── ItemSlot.js
│ │ ├── OpenButton.js
│ │ ├── SlotStrip.js
│ │ └── components
│ │ └── DraggableItem.js
├── game
│ ├── Game.js
│ ├── HUD.js
│ ├── character
│ │ └── Builder.js
│ └── level
│ │ ├── Factory.js
│ │ ├── Level.js
│ │ └── Minimap.js
├── lang
│ ├── en.js
│ ├── index.js
│ ├── pt.js
│ └── tr.js
├── main.js
├── resource
│ ├── manager.js
│ └── sounds
│ │ ├── boss
│ │ ├── boss.mp3
│ │ └── kill-boss.mp3
│ │ ├── chest.wav
│ │ ├── coin.mp3
│ │ ├── death1.mp3
│ │ ├── death2.mp3
│ │ ├── death3.mp3
│ │ ├── door-2.wav
│ │ ├── door.mp3
│ │ ├── effects
│ │ ├── checkpoint-2.mp3
│ │ ├── checkpoint.mp3
│ │ ├── lever.mp3
│ │ ├── open-portal-inferno.mp3
│ │ ├── open-portal.mp3
│ │ ├── pending.mp3
│ │ ├── portal.mp3
│ │ ├── stun.mp3
│ │ └── teleport.mp3
│ │ ├── enemies
│ │ ├── default.mp3
│ │ ├── mimic.mp3
│ │ └── snake.mp3
│ │ ├── fountain.wav
│ │ ├── hit1.mp3
│ │ ├── hit2.mp3
│ │ ├── hit3.mp3
│ │ ├── hit4.mp3
│ │ ├── inventory
│ │ ├── buy.wav
│ │ ├── close.ogg
│ │ ├── open.ogg
│ │ └── sell.wav
│ │ ├── levelup.aif
│ │ ├── music
│ │ ├── higure-forest.mp3
│ │ ├── moonlight-forest.mp3
│ │ └── plague-of-nighterrors.mp3
│ │ ├── potion.wav
│ │ ├── skills
│ │ ├── attack-speed-1.mp3
│ │ ├── attack-speed-2.mp3
│ │ ├── attack-speed-3.mp3
│ │ ├── movement-speed-1.mp3
│ │ ├── movement-speed-2.mp3
│ │ └── movement-speed-3.mp3
│ │ ├── spells
│ │ └── generic.mp3
│ │ ├── step1.mp3
│ │ ├── step2.mp3
│ │ ├── step3.mp3
│ │ ├── step4.mp3
│ │ ├── stingers
│ │ ├── checkpointStinger.mp3
│ │ ├── death.mp3
│ │ └── mapkind.mp3
│ │ ├── voices
│ │ ├── approve-1.mp3
│ │ ├── approve-2.mp3
│ │ ├── approve-3.mp3
│ │ ├── approve-4.mp3
│ │ ├── approve-5.mp3
│ │ ├── potion-seller-1.wav
│ │ ├── potion-seller-2.wav
│ │ └── potion-seller-3.wav
│ │ ├── weapons
│ │ ├── bow-1.mp3
│ │ ├── bow-2.mp3
│ │ ├── staff-1.mp3
│ │ └── staff-2.mp3
│ │ ├── woosh1.wav
│ │ ├── woosh2.wav
│ │ └── woosh3.wav
├── utils
│ ├── device.js
│ └── index.js
└── web
│ ├── login.js
│ └── tutorial.js
├── color-reference.png
├── conver.pe
├── ecosystem.config.js
├── export_layers.rb
├── faviconDescription.json
├── gulpfile.js
├── hud.psd
├── logo.png
├── logo.psd
├── package-lock.json
├── package.json
├── press
├── legionary-course.png
└── magicavoxel
│ └── heros.vox
├── public
├── ads.txt
├── fb-share.png
├── images
│ ├── background.jpg
│ ├── github-icon.png
│ ├── icons
│ │ └── original.png
│ ├── logo.png
│ ├── portal.gif
│ ├── sprites
│ │ └── .gitkeep
│ ├── tutorial
│ │ ├── .DS_Store
│ │ ├── chests.png
│ │ ├── enemies.png
│ │ ├── inventory-1.png
│ │ ├── inventory-2.png
│ │ ├── inventory-3.png
│ │ ├── inventory-4.png
│ │ ├── level-up-1.png
│ │ ├── level-up-2.png
│ │ ├── move.png
│ │ ├── portal-1.png
│ │ ├── portal-2.png
│ │ └── potion.png
│ └── youtube-icon.png
├── index.html
└── privacy.txt
├── server
├── .env
├── StatsDocs.ts
├── actions
│ ├── Action.ts
│ └── BattleAction.ts
├── controllers
│ └── hero.ts
├── core
│ ├── Bar.ts
│ ├── EquipedItems.ts
│ ├── EquipmentSlot.ts
│ ├── Inventory.ts
│ ├── Movement.ts
│ └── Position.ts
├── db-migrations
│ └── update_checkpoints.ts
├── db
│ ├── ChatLog.ts
│ ├── Hero.ts
│ ├── Quest.ts
│ ├── Report.ts
│ └── Season.ts
├── entities
│ ├── Boss.ts
│ ├── Enemy.ts
│ ├── Entity.ts
│ ├── Interactive.ts
│ ├── Item.ts
│ ├── NPC.ts
│ ├── Player.ts
│ ├── Tower.ts
│ ├── Unit.ts
│ ├── behaviours
│ │ ├── AttackNearestPlayer.ts
│ │ ├── Behaviour.ts
│ │ ├── UnitSpawner.ts
│ │ └── bosses
│ │ │ └── SlimeBoss.ts
│ ├── ephemeral
│ │ └── TextEvent.ts
│ ├── interactive
│ │ ├── CheckPoint.ts
│ │ ├── Chest.ts
│ │ ├── Door.ts
│ │ ├── Fountain.ts
│ │ ├── InfernoPortal.ts
│ │ ├── Jail.ts
│ │ ├── Leaderboard.ts
│ │ ├── Lever.ts
│ │ ├── Portal.ts
│ │ ├── StunTile.ts
│ │ └── TeleportTile.ts
│ ├── items
│ │ ├── CastableItem.ts
│ │ ├── ConsumableItem.ts
│ │ ├── Diamond.ts
│ │ ├── EquipableItem.ts
│ │ ├── Gold.ts
│ │ ├── consumable
│ │ │ ├── HpPotion.ts
│ │ │ ├── Key.ts
│ │ │ ├── MpPotion.ts
│ │ │ ├── Potion.ts
│ │ │ ├── PotionPoints.ts
│ │ │ ├── Scroll.ts
│ │ │ ├── ScrollTeleport.ts
│ │ │ └── XpPotion.ts
│ │ ├── createItem.ts
│ │ └── equipable
│ │ │ ├── ArmorItem.ts
│ │ │ ├── BootItem.ts
│ │ │ ├── HelmetItem.ts
│ │ │ ├── ShieldItem.ts
│ │ │ └── WeaponItem.ts
│ └── skills
│ │ ├── AttackSpeedSkill.ts
│ │ ├── MovementSpeedSkill.ts
│ │ └── Skill.ts
├── helpers
│ └── Math.ts
├── index.ts
├── maps
│ └── truehell.ts
├── package-lock.json
├── package.json
├── pdf
│ ├── ProgressionTable.ts
│ └── random-seed.js
├── rooms
│ ├── ChatRoom.ts
│ ├── DungeonRoom.ts
│ └── states
│ │ └── DungeonState.ts
├── test
│ └── SchemaTest.ts
├── tsconfig.json
├── utils
│ ├── Debug.ts
│ ├── GridUtils.ts
│ ├── MapTemplateParser.ts
│ ├── ProgressionConfig.ts
│ └── RoomUtils.ts
└── yarn.lock
├── shared
├── Dungeon.js
└── helpers.js
├── webpack.config.js
└── webpack
└── SpritesheetPlugin.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "browsers": [
8 | "last 1 version",
9 | "not ie <= 9",
10 | "not dead",
11 | ]
12 | }
13 | }
14 | ],
15 | "@babel/preset-react"
16 | ],
17 | "plugins": [
18 | "@babel/plugin-transform-runtime",
19 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
20 | ["@babel/plugin-proposal-class-properties", { "loose": true }],
21 | "@babel/plugin-proposal-optional-chaining"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/test/.*
3 | .*/node_modules/.*
4 |
5 | [include]
6 |
7 | [libs]
8 |
9 | [options]
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: endel # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: # Replace with a single custom sponsorship URL
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | .tmp
5 | .sass-cache
6 | bower_components
7 | test/bower_components
8 | npm-debug.log
9 | app/images/sprites/*.png
10 | db.sqlite3
11 | /public
12 | /server/src
13 |
14 | public/images/spritesheet.png
15 | public/images/spritesheet.json
16 | public/images/sprites/*.png
17 | press/
18 | faviconData.json
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "esnext": true,
4 | "bitwise": true,
5 | "camelcase": true,
6 | "curly": true,
7 | "eqeqeq": true,
8 | "immed": true,
9 | "indent": 2,
10 | "latedef": true,
11 | "newcap": true,
12 | "noarg": true,
13 | "quotmark": "single",
14 | "undef": true,
15 | "unused": true,
16 | "strict": true
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "server/node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 |
3 | gem 'psd', '~> 3.5.0'
4 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: http://rubygems.org/
3 | specs:
4 | chunky_png (1.3.5)
5 | hashie (3.4.3)
6 | nokogiri (1.5.11)
7 | psd (3.5.0)
8 | chunky_png
9 | psd-enginedata (~> 1)
10 | rake
11 | xmp
12 | psd-enginedata (1.1.1)
13 | hashie
14 | rake (10.4.2)
15 | xmp (0.2.0)
16 | nokogiri (~> 1.5.0)
17 |
18 | PLATFORMS
19 | ruby
20 |
21 | DEPENDENCIES
22 | psd (~> 3.5.0)
23 |
24 | BUNDLED WITH
25 | 1.10.6
26 |
--------------------------------------------------------------------------------
/NOTES:
--------------------------------------------------------------------------------
1 | TODO:
2 |
3 | - leaving and entering rooms
4 | - enemies are not following properly
5 | - quests (talking to people)
6 | - picking items / potions / weapons
7 | - using potions
8 | - equiping items
9 | - inventory system!
10 | - keep player data between rooms
11 |
12 | - enemies that blocks walking
13 |
14 | Class select:
15 | - Wizard
16 | - Warrior
17 | - Archer
18 |
19 | ADD FEATURES:
20 | - passive / orbiting items
21 |
22 | Background
23 | - Every player has it's own kingdom
24 | -
25 |
26 | Advertising
27 | - Explore unique dungeons with other players
28 |
29 | Who doesn't want to kill bunnies in the forest to level up with your friends? lol #roguelike #mmo #indiedev
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # [Mazmorra.io](https://mazmorra.io): Online Multiplayer Dungeon Crawler
4 |
5 | [Mazmorra.io](https://mazmorra.io) has a long history with Colyseus. This project has started to validate the very first versions of Colyseus, as you can see in [commits dating back from 2015](https://github.com/endel/mazmorra/commit/7d2f631a48f8907f5031a3c9a1936d012bbe2090), when the Colyseus version was **0.2**
6 |
7 | I've decided to make the **source code available** for mazmorra.io, for educational purposes. It is not open-sourced under a permissive license, though. You are **not allowed** to host your own version, or a modified version of [Mazmorra.io](https://mazmorra.io).
8 |
9 | ## LICENSE
10 |
11 | Copyright © Endel Dreyer. See [LICENSE.md](LICENSE.md) for more details.
12 |
--------------------------------------------------------------------------------
/assets.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/assets.psd
--------------------------------------------------------------------------------
/client/behaviors/Activatable.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 | import { activatableSound, playSound3D } from "../core/sound";
3 |
4 | export default class Activatable extends Behaviour {
5 |
6 | onAttach () {
7 | this.onMouseOver = this.onMouseOver.bind(this);
8 | this.onMouseOut = this.onMouseOut.bind(this);
9 |
10 | this.on('active', this.onActiveChange.bind(this));
11 |
12 | this.on('mouseover', this.onMouseOver);
13 | this.on('mouseout', this.onMouseOut);
14 | }
15 |
16 | onMouseOver (tileSelection) {
17 | if (this.isActive) {
18 | tileSelection.setColor(config.COLOR_GREEN)
19 | App.cursor.dispatchEvent({ type: 'cursor', kind: 'activate' })
20 | }
21 | }
22 |
23 | onMouseOut (tileSelection) {
24 | tileSelection.setColor()
25 | }
26 |
27 | onActiveChange (value) {
28 | var canPlaySound = value;
29 |
30 | if (this.object.userData.type === "fountain" ) {
31 | canPlaySound = (value === false);
32 | }
33 |
34 | var sound = activatableSound[this.object.userData.type];
35 | if (sound && canPlaySound) {
36 | playSound3D(sound, this.object);
37 | }
38 | }
39 |
40 | update () {
41 | this.object.activeSprite.visible = this.isActive
42 | this.object.inactiveSprite.visible = !this.isActive
43 | }
44 |
45 | get isActive () {
46 | return this.object.userData.active
47 | }
48 |
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/client/behaviors/DangerousThing.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | export default class DangerousThing extends Behaviour {
4 |
5 | onAttach (amount = 0.05, duration) {
6 | this.object.userData.interactive = true;
7 |
8 | this.initY = this.object.position.y
9 | this.destY = this.initY + amount
10 | this.duration = (duration) ? duration : 400 + (Math.random() * 200)
11 |
12 | this.goUp = this.goUp.bind(this);
13 | this.goDown = this.goDown.bind(this);
14 |
15 | this.tween = null
16 | setTimeout(() => this.goUp(), Math.random() * 1500)
17 |
18 | this.on('died', this.detach.bind(this))
19 | }
20 |
21 | goUp () {
22 | this.tween = App.tweens.
23 | add(this.object.position).
24 | to({ y: this.destY }, this.duration, Tweener.ease.cubicInOut).
25 | then(this.goDown)
26 | }
27 |
28 | goDown () {
29 | this.tween = App.tweens.
30 | add(this.object.position).
31 | to({ y: this.initY }, this.duration, Tweener.ease.cubicInOut).
32 | then(this.goUp)
33 | }
34 |
35 | onDetach () {
36 | delete this.object.userData.interactive;
37 | if (this.tween) {
38 | this.tween.then(null);
39 | this.tween = null;
40 | }
41 | }
42 |
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/client/behaviors/HasLifebar.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | import Lifebar from '../elements/Lifebar'
4 |
5 | export default class HasLifebar extends Behaviour {
6 |
7 | onAttach (factory) {
8 | // lifebar
9 | this.lifebar = new Lifebar()
10 | this.lifebar.position.x = 0
11 |
12 | this.isPVPAllowed = factory && factory.level.isPVPAllowed;
13 |
14 | // position lifebar on top of enemy's variable height
15 | this.lifebar.position.y = this.object.sprite.scale.y + 0.2;
16 |
17 | this.lifebar.position.z = 0
18 | this.lifebar.visible = false
19 | this.object.add(this.lifebar)
20 |
21 | this.onMouseOver = this.onMouseOver.bind(this);
22 | this.onMouseOut = this.onMouseOut.bind(this)
23 | this.detach = this.detach.bind(this);
24 |
25 | this.on('mouseover', this.onMouseOver);
26 | this.on('mouseout', this.onMouseOut);
27 |
28 | this.on('died', this.detach);
29 | }
30 |
31 | update () {
32 | this.lifebar.progress = (this.object.userData.hp.current / this.object.userData.hp.max)
33 | }
34 |
35 | onMouseOver (tileSelection) {
36 | this.lifebar.visible = true
37 |
38 | if (this.object.userData.kind || this.isPVPAllowed) {
39 | // update cursor to 'attack'
40 | App.cursor.dispatchEvent({ type: 'cursor', kind: 'attack' })
41 | tileSelection.setColor(config.COLOR_RED)
42 |
43 | } else {
44 | tileSelection.setColor(config.COLOR_GREEN)
45 | }
46 | }
47 |
48 | onMouseOut (tileSelection) {
49 | // only hide on mouseout if enemy didn't took any damage
50 | if (this.lifebar.progress == 1) {
51 | this.lifebar.visible = false
52 | }
53 |
54 | tileSelection.setColor()
55 | }
56 |
57 | onDetach () {
58 | if (this.lifebar.parent) {
59 | this.lifebar.parent.remove(this.lifebar)
60 | }
61 |
62 | // remove listeners
63 | this.off('mouseover', this.onMouseOver);
64 | this.off('mouseout', this.onMouseOut);
65 | this.off('died', this.detach);
66 |
67 | this.lifebar.destroy();
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/client/behaviors/InventoryBehaviour.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | let player = new WeakMap()
4 |
5 | export default class InventoryBehaviour extends Behaviour {
6 |
7 | onAttach (playerObject) {
8 | player.set(this, playerObject)
9 |
10 | this.on('toggle', this.onToggle.bind(this))
11 | }
12 |
13 | onToggle (isOpen) {
14 | if (isOpen) {
15 | this.object.slots.updateItems();
16 | }
17 | }
18 |
19 | onDetach () { }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/client/behaviors/LightOscillator.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | import lerp from 'lerp'
4 |
5 | export default class LightOscillator extends Behaviour {
6 |
7 | onAttach (min, max, lerpRatio = 0.2) {
8 | this.min = min
9 | this.max = max
10 |
11 | this.distance = this.object.distance
12 | this.decay = 1
13 | this.lerpRatio = lerpRatio
14 |
15 | this.oscillateInterval = setInterval(this.oscillate.bind(this), 100)
16 | this.oscillate()
17 | }
18 |
19 | oscillate () {
20 | this.targetIntensity = this.min + (Math.random() * (this.max - this.min))
21 | this.targetDistance = this.distance + this.distance * (Math.random() * (this.max - this.min))
22 | this.targetDecay = this.decay - (Math.random() * (this.max - this.min))
23 | }
24 |
25 | update () {
26 | this.object.intensity = lerp(this.object.intensity, this.targetIntensity, this.lerpRatio)
27 | this.object.distance = lerp(this.object.distance, this.targetDistance, this.lerpRatio)
28 | this.object.decay = lerp(this.object.decay, this.targetDecay, this.lerpRatio)
29 | }
30 |
31 | onDetach () {
32 | clearInterval(this.oscillateInterval)
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/client/behaviors/NearPlayerOpacity.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 | import Activatable from './Activatable';
3 |
4 | export default class NearPlayerOpacity extends Behaviour {
5 |
6 | onAttach () {
7 | if (
8 | IS_DAY ||
9 | typeof(this.object.userData.hp) !== "undefined" && // if unit is already dead, dettach imediatelly.
10 | this.object.userData.hp.current <= 0
11 | ) {
12 | this.detach();
13 | return;
14 | }
15 |
16 | this.on('died', this.detach.bind(this))
17 | }
18 |
19 | update () {
20 | // no need to apply opacity on daylight
21 | if (global.player) {
22 | var v1 = this.object.position
23 | , v2 = global.player.position
24 |
25 | , dx = v1.x - v2.x
26 | , dy = v1.y - v2.y
27 | , dz = v1.z - v2.z
28 | , distance = Math.sqrt(dx*dx+dy*dy+dz*dz)
29 |
30 | // TODO: improve me
31 | // make it dynamic accouding to player illumination
32 | , opacity = 5 / distance;
33 |
34 | if (this.entity.getBehaviour("Activatable")) {
35 | this.object.activeSprite.material.opacity = opacity;
36 | this.object.inactiveSprite.material.opacity = opacity;
37 |
38 | // } else if (this.entity.getBehaviour("Openable")) {
39 | // this.object.body.material.opacity = opacity;
40 | // this.object.head.material.opacity = opacity;
41 |
42 | } else {
43 | (this.object.material || this.object.children[0].material).opacity = opacity;
44 | }
45 | }
46 | }
47 |
48 | onDetach () { }
49 |
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/client/behaviors/Pickable.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | import Shadow from './Shadow'
4 |
5 | export default class Pickable extends Behaviour {
6 |
7 | onAttach () {
8 | this.tween = null
9 | // this.object.addBehaviour(new Shadow)
10 |
11 | this.initY = this.object.position.y
12 | this.destY = this.initY + 0.2
13 | this.duration = 1700 + (Math.random() * 300)
14 |
15 | this.object.position.y = this.initY - 1
16 |
17 | this.goUp = this.goUp.bind(this);
18 | this.goDown = this.goDown.bind(this);
19 |
20 | App.tweens.
21 | add(this.object.scale).
22 | from({x: 0.1, y: 0.1, z: 0.1}, 600, Tweener.ease.quartOut)
23 |
24 | App.tweens.
25 | add(this.object.position).
26 | to({ y: this.initY + 1 }, 700, Tweener.ease.quartOut).
27 | to({ y: this.initY }, 1600, Tweener.ease.quartInOut).
28 | then(() => {
29 | this.initTimeout = setTimeout(() => this.goUp(), Math.random() * 1500)
30 | })
31 | }
32 |
33 | goUp () {
34 | this.tween = App.tweens.
35 | add(this.object.position).
36 | to({ y: this.destY }, this.duration, Tweener.ease.cubicInOut).
37 | then(this.goDown)
38 | }
39 |
40 | goDown () {
41 | this.tween = App.tweens.
42 | add(this.object.position).
43 | to({ y: this.initY }, this.duration, Tweener.ease.cubicInOut).
44 | then(this.goUp)
45 | }
46 |
47 | onDetach () {
48 | App.tweens.remove(this.object.position);
49 | clearTimeout(this.initTimeout)
50 | if (this.tween) { this.tween.dispose(); }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/client/behaviors/QuestIndicator.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | export default class QuestIndicator extends Behaviour {
4 |
5 | onAttach () {
6 | this.tween = null
7 |
8 | const map = ResourceManager.get('hud-quest-indicator');
9 | this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map }))
10 | this.sprite.scale.normalizeWithTexture(map);
11 |
12 | this.initY = 3.2;
13 | this.destY = this.initY + 0.2
14 | this.duration = 1500
15 |
16 | this.sprite.position.y = this.initY;
17 |
18 | this.object.add(this.sprite);
19 |
20 | this.goUp = this.goUp.bind(this);
21 | this.goDown = this.goDown.bind(this);
22 |
23 | this.goUp();
24 | }
25 |
26 | goUp () {
27 | this.tween = App.tweens.
28 | add(this.sprite.position).
29 | to({ y: this.destY }, this.duration, Tweener.ease.cubicInOut).
30 | then(this.goDown)
31 | }
32 |
33 | goDown () {
34 | this.tween = App.tweens.
35 | add(this.sprite.position).
36 | to({ y: this.initY }, this.duration, Tweener.ease.cubicInOut).
37 | then(this.goUp)
38 | }
39 |
40 | onDetach () {
41 | this.object.remove(this.sprite);
42 | App.tweens.remove(this.sprite.position);
43 | clearTimeout(this.initTimeout)
44 | if (this.tween) { this.tween.dispose(); }
45 | delete this.sprite;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/client/behaviors/Shadow.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | var material = null, geometry = null
4 |
5 | export default class Shadow extends Behaviour {
6 |
7 | onAttach () {
8 | if (!material) {
9 | material = new THREE.MeshBasicMaterial( {
10 | map: ResourceManager.get('effects-shadow'),
11 | transparent: true,
12 | side: THREE.FrontSide
13 | } )
14 | geometry = new THREE.PlaneGeometry(1, 1)
15 | }
16 |
17 | this.initialY = this.object.position.y
18 |
19 | this.shadow = new THREE.Mesh(geometry, material)
20 | this.shadow.scale.normalizeWithTexture(material.map)
21 | this.shadow.rotateX(-Math.PI / 2)
22 | this.shadow.position.y = -0.4999
23 | // this.object.add(this.shadow)
24 | }
25 |
26 | update () {
27 | var fixedScale = this.scale - (this.initialY - this.object.position.y)
28 | // console.log(this.initialY, this.initialY - this.object.position.y)
29 | // console.log(this.object.position.y, fixedScale)
30 | this.shadow.scale.set(fixedScale, fixedScale, fixedScale)
31 | }
32 |
33 | onDetach () {
34 | // this.object.remove(this.shadow)
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/client/behaviors/Stretchable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { Behaviour } from 'behaviour.js'
4 |
5 | export default class Strechable extends Behaviour {
6 |
7 | onAttach () {
8 | this.tween = null
9 |
10 | this.dest = this.object.scale.x + 0.2
11 | this.init = this.object.scale.x - 0.2
12 |
13 | this.shrink()
14 | }
15 |
16 | shrink () {
17 | this.tween = App.tweens.
18 | add(this.object.scale).
19 | to({ x: this.dest }, 1500, Tweener.ease.cubicInOut).
20 | then(this.grow.bind(this))
21 | }
22 |
23 | grow () {
24 | this.tween = App.tweens.
25 | add(this.object.scale).
26 | to({ x: this.init }, 1500, Tweener.ease.cubicInOut).
27 | then(this.shrink.bind(this))
28 | }
29 |
30 | onDetach () {
31 | if (this.tween) this.tween.dispose()
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/client/config.js:
--------------------------------------------------------------------------------
1 | const isMobile = require('./utils/device').isMobile;
2 | const devicePixelRatio = Math.min(2, window.devicePixelRatio);
3 |
4 | module.exports = {
5 | TILE_SIZE: 3,
6 | WALL_THICKNESS: 0.7,
7 |
8 | COLOR_RED: new THREE.Color(0xd00000),
9 | COLOR_GREEN: new THREE.Color(0x7cac20),
10 | COLOR_YELLOW: new THREE.Color(0xfcf458),
11 | COLOR_WHITE: new THREE.Color(0xffffff),
12 | COLOR_BLUE: new THREE.Color(0x1c80e4),
13 |
14 | colors: {
15 | dark: 0x000000, // black
16 | grass: 0x002a0d, // green
17 | rock: 0x1e2129, // gray
18 | ice: 0x387493, // blue
19 | inferno: 0x440000, // red
20 | castle: 0x443434 // brown
21 | },
22 |
23 | // ZOOM: 23
24 | // ZOOM: 32 / window.devicePixelRatio,
25 | // ZOOM: 42 / window.devicePixelRatio,
26 | ZOOM: 42,
27 | // ZOOM: 45 / window.devicePixelRatio,
28 | IS_DAY: true,
29 |
30 | devicePixelRatio,
31 |
32 | HUD_MARGIN: 2.5,
33 | HUD_SCALE: (isMobile)
34 | ? (7 / devicePixelRatio)
35 | : (6 / devicePixelRatio),
36 |
37 | DEFAULT_FONT: (Math.floor((7.5 / devicePixelRatio) * 5)) + "px primary",
38 | FONT_TITLE: (Math.floor((9 / devicePixelRatio) * 5)) + "px primary",
39 | SMALL_FONT: (Math.floor((5.5 / devicePixelRatio) * 5)) + "px primary",
40 |
41 | // player preferences
42 | classes: [
43 | 'strength',
44 | 'intelligence',
45 | 'agility',
46 | ],
47 | hairs: [
48 | "boy",
49 | "young",
50 | "ancient",
51 | "lumberman",
52 | "viking",
53 | "girl",
54 | "woman",
55 | "lady",
56 | "queen",
57 | "cowlick",
58 | "punk",
59 | "bald"
60 | ]
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/client/core/App.js:
--------------------------------------------------------------------------------
1 | import Tweener from "tweener";
2 | import Clock from "clock-timer.js";
3 | import { createComponentSystem } from "behaviour.js";
4 | import { isMobile } from "../utils/device";
5 |
6 | // const Tweener = require("tweener");
7 | // const Clock = require("clock-timer.js").default;
8 | // const createComponentSystem = require("behaviour.js").createComponentSystem;
9 | // const isMobile = require("../utils/device").isMobile;
10 |
11 | class App {
12 |
13 | constructor () {
14 | this.tweens = new Tweener()
15 | this.clock = new Clock()
16 | this.componentSystem = createComponentSystem(THREE.Object3D)
17 | this.mouse = new THREE.Vector2();
18 |
19 | if (isMobile) {
20 | window.addEventListener('touchmove', (e) => {
21 | e.preventDefault();
22 | e.stopPropagation();
23 | this.onMouseMove({
24 | clientX: e.touches[0].clientX,
25 | clientY: e.touches[0].clientY,
26 | })
27 | }, false);
28 |
29 | } else {
30 | window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
31 | }
32 | }
33 |
34 | update () {
35 | this.clock.tick()
36 | this.tweens.update(this.clock.deltaTime)
37 | this.componentSystem.update()
38 | }
39 |
40 |
41 | onMouseMove (e) {
42 | this.mouse.clientX = e.clientX;
43 | this.mouse.clientY = e.clientY;
44 |
45 | this.mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1
46 | this.mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1
47 | }
48 |
49 | }
50 |
51 | export default new App();
52 | // module.exports = new App();
53 |
--------------------------------------------------------------------------------
/client/core/PlayerPrefs.js:
--------------------------------------------------------------------------------
1 | import { getHeroId } from "./network";
2 |
3 | const localStorage = window.localStorage || {
4 | //
5 | // some browser configurations may block `localStorage` usage from an iframe
6 | // e.g.: "Failed to read the 'localStorage' property from 'Window': Access is denied for this document."
7 | //
8 | _data: {},
9 | setItem(k, v) { this._data[k] = v },
10 | getItem(k) { return this._data[k] },
11 | removeItem(k) { delete this._data[k] },
12 | clear() { this._data = {}; }
13 | }
14 |
15 | export class PlayerPrefs {
16 |
17 | static set (key, value, isGlobal = true) {
18 | const k = (isGlobal) ? key : this.heroKey(key);
19 | localStorage.setItem(k, value);
20 | }
21 |
22 | static get (key, isGlobal = true) {
23 | const k = (isGlobal) ? key : this.heroKey(key);
24 | return localStorage.getItem(k);
25 | }
26 |
27 | static getNumber (key, fallback = "0", isGlobal = true) {
28 | const k = (isGlobal) ? key : this.heroKey(key);
29 | return parseFloat(localStorage.getItem(k) || fallback);
30 | }
31 |
32 | static remove (key, isGlobal = true) {
33 | const k = (isGlobal) ? key : this.heroKey(key)
34 | localStorage.removeItem(k);
35 | }
36 |
37 | static clear () {
38 | localStorage.clear();
39 | }
40 |
41 | static hasSeenBoss(entity, bool) {
42 | if (bool) {
43 | return localStorage.setItem(this.heroKey(`boss-${entity.kind}-seen`), bool);
44 |
45 | } else {
46 | return localStorage.getItem(this.heroKey(`boss-${entity.kind}-seen`));
47 | }
48 | }
49 |
50 | static hasKilledBoss(entity, bool) {
51 | if (bool) {
52 | return localStorage.setItem(this.heroKey(`boss-${entity.kind}-killed`), bool);
53 |
54 | } else {
55 | return localStorage.getItem(this.heroKey(`boss-${entity.kind}-killed`));
56 | }
57 | }
58 |
59 | static heroKey(id) {
60 | return `${getHeroId()}${id}`;
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/client/core/network.js:
--------------------------------------------------------------------------------
1 | import { Client } from 'colyseus.js'
2 | import credentials from '../web/login'
3 | import { PlayerPrefs } from './PlayerPrefs';
4 |
5 | const protocol = window.location.protocol.replace("http", "ws");
6 | const endpoint = (process.env.NODE_ENV === "production")
7 | ? `wss://mazmorra.io`
8 | : `${protocol}//${ window.location.hostname }:3553`;
9 |
10 | export const client = new Client(endpoint);
11 | export let room = null;
12 |
13 | // export const client = new Client(`ws://${ window.location.hostname }`);
14 | global.client = client;
15 |
16 | let heroId;
17 | export function setHeroId(_heroId) {
18 | heroId = _heroId;
19 | }
20 |
21 | export function getHeroId() {
22 | return heroId;
23 | }
24 |
25 | export function getRoomId() {
26 | return room.id;
27 | }
28 |
29 | export function enterRoom (name, options = {}) {
30 | App.cursor.dispatchEvent({ type: "cursor", kind: "loading" });
31 |
32 | options.token = credentials.token
33 |
34 | // Assign current hero id
35 | if (heroId) { options.heroId = heroId; }
36 |
37 | // Troll adblock users
38 | if (window.adBlock) { options.adBlock = true; }
39 |
40 | return client.joinOrCreate(name, options).then(r => {
41 | room = r;
42 | App.cursor.dispatchEvent({ type: "cursor", kind: "pointer" });
43 | return room;
44 | });
45 | }
46 |
47 | export function getClientId () {
48 | return room && room.sessionId;
49 | }
50 |
51 | export function enterChat() {
52 | return client.joinOrCreate("chat");
53 | }
54 |
--------------------------------------------------------------------------------
/client/core/utils.js:
--------------------------------------------------------------------------------
1 | export function distance(pointA, pointB) {
2 | return Math.abs(pointA.x - pointB.x) + Math.abs(pointA.z - pointB.z)
3 | }
4 |
5 | export function distanceFromPlayer (object) {
6 | if (!global.player) {
7 | return { x: 0, y: 0, z: 0 };
8 |
9 | } else {
10 | return {
11 | x: Math.abs(object.position.x - player.position.x),
12 | y: Math.abs(object.position.z - player.position.z),
13 | z: distance(object.position, player.position)
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/css/ads.styl:
--------------------------------------------------------------------------------
1 | #crashracing-com_300x250
2 | position: absolute
3 |
--------------------------------------------------------------------------------
/client/css/fonts.styl:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-family 'primary'
3 | src url('./fonts/enchanted_land-webfont.woff2') format('woff2'),
4 | url('./fonts/enchanted_land-webfont.woff') format('woff'),
5 | url('./fonts/enchanted_land-webfont.ttf') format('truetype');
6 | font-weight normal
7 | font-style normal
8 |
9 | // font-family 'primary'
10 | // src url('./fonts/5x5_pixel.eot')
11 | // src url('./fonts/5x5_pixel.eot?#iefix') format('embedded-opentype'),
12 | // url('./fonts/5x5_pixel.woff') format('woff'),
13 | // url('./fonts/5x5_pixel.ttf') format('truetype'),
14 | // url('./fonts/5x5_pixel.svg#5x5_pixel.svg') format('svg')
15 | // // src url('./fonts/pixel_noir_skinny_short.eot')
16 | // // src url('./fonts/pixel_noir_skinny_short.ttf') format('truetype')
17 | // font-weight normal
18 | // font-style normal
19 |
20 | p#font-preload
21 | position absolute
22 | font-family 'primary'
23 | visibility hidden
24 |
--------------------------------------------------------------------------------
/client/css/fonts/5x5_pixel.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/5x5_pixel.eot
--------------------------------------------------------------------------------
/client/css/fonts/5x5_pixel.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/5x5_pixel.otf
--------------------------------------------------------------------------------
/client/css/fonts/5x5_pixel.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/5x5_pixel.ttf
--------------------------------------------------------------------------------
/client/css/fonts/5x5_pixel.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/5x5_pixel.woff
--------------------------------------------------------------------------------
/client/css/fonts/5x5_pixel.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/5x5_pixel.woff2
--------------------------------------------------------------------------------
/client/css/fonts/enchanted_land-webfont.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/enchanted_land-webfont.otf
--------------------------------------------------------------------------------
/client/css/fonts/enchanted_land-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/enchanted_land-webfont.ttf
--------------------------------------------------------------------------------
/client/css/fonts/enchanted_land-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/enchanted_land-webfont.woff
--------------------------------------------------------------------------------
/client/css/fonts/enchanted_land-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/fonts/enchanted_land-webfont.woff2
--------------------------------------------------------------------------------
/client/css/modal.styl:
--------------------------------------------------------------------------------
1 | .modal
2 | pointer-events none
3 | position absolute
4 | text-align center
5 | width 100vw
6 | height 100vh
7 | top 0
8 | bottom 0
9 | right 0
10 | left 0
11 | transition opacity 0.2s
12 | z-index -10
13 |
14 | &.checkpoint-modal
15 | padding 10vh 10vw
16 | h2
17 | color #986c1b
18 | text-shadow -2px 0 #2c1800, 0 -2px #2c1800, 2px 0 #2c1800, 0px 3px #2c1800
19 | ul
20 | margin-top 1vh
21 | list-style none
22 | padding 1vh 1vw
23 | li
24 | display inline-block
25 | width 60px
26 | height 60px
27 | background #cca874
28 | color #2e1100
29 | a
30 | display inline-block
31 |
32 | opacity 0
33 | background rgba(0, 0, 0, 0.95)
34 |
35 | // display none
36 | &.active
37 | pointer-events all
38 | display block
39 | opacity 1
40 | z-index 10
41 |
--------------------------------------------------------------------------------
/client/css/sprites/index.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/css/sprites/index.styl
--------------------------------------------------------------------------------
/client/css/tutorial.styl:
--------------------------------------------------------------------------------
1 | #tutorial
2 | h2
3 | margin 8vh 0 4vh 0
4 | font-size 1.2em
5 | // text-decoration underline
6 |
7 | .contents
8 | position relative
9 | margin auto
10 | font-size 28px
11 | text-align center
12 | width 600px
13 | background #3c2404
14 | color #cca874
15 | border-radius 4px
16 |
17 | for n in (1..9)
18 | &.step-{n}
19 | >div:not(.step-{n})
20 | display none
21 |
22 | // first step
23 | &.step-1
24 | a.nav.left
25 | display none
26 |
27 | // last step
28 | &.step-9
29 | a.nav.right
30 | display none
31 |
32 | &:not(.step-9)
33 | a.nav.last
34 | display none
35 |
36 |
37 | > div
38 | pointer-events none
39 |
40 | a.nav
41 | position absolute
42 | top 0
43 | color inherit
44 | padding 18px 12px
45 | z-index 1
46 | font-size 22px
47 | cursor pointer
48 | &.left
49 | left 0
50 | &.right
51 | right 0
52 | &.last
53 | right 0
54 |
55 | img
56 | width 100%
57 | display block
58 |
59 | p
60 | padding 14px
61 | text-shadow -1px 0 #000, 0 -1px #000, 1px 0 #000, 0px 1px #000, 0px 2px #000
62 | color #fff
63 |
--------------------------------------------------------------------------------
/client/dungeon_viewer.js:
--------------------------------------------------------------------------------
1 | import Dungeon from "../shared/Dungeon";
2 | import helpers from "../shared/helpers";
3 |
4 | const rand = {
5 | intBetween: (min, max) =>Math.floor(Math.random() * (max - min + 1) + min)
6 | }
7 |
8 | const TILE_SIZE = 12;
9 |
10 | function generate() {
11 | const [grid, rooms] = Dungeon.generate(
12 | rand,
13 | {
14 | x: parseInt(gridSizeX.value),
15 | y: parseInt(gridSizeY.value )
16 | }, {
17 | x: parseInt(minRoomSizeX.value),
18 | y: parseInt(minRoomSizeY.value)
19 | }, {
20 | x: parseInt(maxRoomSizeX.value),
21 | y: parseInt(maxRoomSizeY.value),
22 | }, parseInt(numRooms.value),
23 | parseInt(oneDirection.value),
24 | parseInt(hasConnections.value),
25 | parseInt(hasObstacles.value),
26 | );
27 | const canvas = document.getElementById("viewer");
28 | canvas.width = gridSizeX.value * TILE_SIZE;
29 | canvas.height = gridSizeY.value * TILE_SIZE;
30 | canvas.style.width = `${canvas.width}px`;
31 | canvas.style.height = `${canvas.height}px`;
32 | canvas.style.zIndex = 999;
33 |
34 | const ctx = canvas.getContext('2d');
35 | ctx.fillStyle = "#ffffff";
36 | ctx.fillRect(0, 0, canvas.width, canvas.height);
37 |
38 | for (let x = 0; x < grid.length; x++) {
39 | for (let y = 0; y < grid[x].length; y++) {
40 | if (grid[x][y] & helpers.TILE_TYPE.FLOOR) {
41 | ctx.fillStyle = "#00ff00";
42 | ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
43 |
44 | } else if (grid[x][y] & helpers.TILE_TYPE.WALL) {
45 | ctx.fillStyle = "#ff0000";
46 | ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
47 | }
48 | }
49 | }
50 |
51 | function drawRoom(room) {
52 | ctx.fillStyle = "#0000ff";
53 | ctx.fillRect(room.position.x * TILE_SIZE, room.position.y * TILE_SIZE, room.size.x * TILE_SIZE, room.size.y * TILE_SIZE);
54 | }
55 |
56 | // drawRoom(rooms[0]);
57 | // drawRoom(rooms[rooms.length - 1]);
58 | // drawRoom(rooms[rooms.length-2]);
59 | // drawRoom(rooms[rooms.length-3]);
60 | }
61 |
62 | var options = {
63 | gridSizeX: 40,
64 | gridSizeY: 40,
65 | minRoomSizeX: 10,
66 | minRoomSizeY: 10,
67 | maxRoomSizeX: 10,
68 | maxRoomSizeY: 10,
69 | numRooms: 10,
70 | oneDirection: 0,
71 | hasConnections: 1,
72 | hasObstacles: 1,
73 | }
74 |
75 | for (let field in options) {
76 | var option = document.createElement("div");
77 |
78 | var label = document.createElement("label");
79 | label.innerText = field;
80 | option.appendChild(label)
81 |
82 | var input = document.createElement("input");
83 | input.id = field;
84 | input.type = "text";
85 | input.value = options[field].toString();
86 | option.appendChild(input)
87 |
88 | document.body.appendChild(option);
89 | }
90 |
91 | window.generate = generate;
92 |
93 | generate();
94 |
--------------------------------------------------------------------------------
/client/elements/Aesthetic.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import NearPlayerOpacity from '../behaviors/NearPlayerOpacity'
4 |
5 | export default class Aesthetic extends THREE.Object3D {
6 |
7 | constructor (mapkind) {
8 | super()
9 |
10 | var i = Math.floor(Math.random() * 2) + 1;
11 |
12 | this.sprite = ResourceManager.getSprite(`aesthetics-${mapkind}-${i}`);
13 | this.sprite.position.y = 0.099
14 | this.add(this.sprite)
15 |
16 | this.addBehaviour(new NearPlayerOpacity)
17 | }
18 |
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/client/elements/Character.js:
--------------------------------------------------------------------------------
1 | import Composition from './character/Composition'
2 |
3 | export default class Character extends THREE.Object3D {
4 |
5 | constructor (data) {
6 | super()
7 |
8 | this.userData = data
9 |
10 | this.composition = new Composition(data.properties)
11 | this.composition.position.y = 0.5
12 |
13 | this.isTypingSprite = ResourceManager.getSprite(`hud-talk-indicator`);
14 | this.isTypingSprite.position.y = 3;
15 |
16 | this.initialScale = this.isTypingSprite.scale.clone();
17 |
18 | this.add(this.composition)
19 | }
20 |
21 | get sprite () {
22 | return this.composition.sprite;
23 | }
24 |
25 | get label () {
26 | return (this !== global.player)
27 | ? `${ this.userData.name } - lvl ${ this.userData.lvl }`
28 | : undefined;
29 | }
30 |
31 | set direction (direction) {
32 | this.composition.direction = direction
33 | }
34 |
35 | set typing (isTyping) {
36 | if (isTyping) {
37 | App.tweens.remove(this.isTypingSprite.scale);
38 |
39 | this.isTypingSprite.scale.set(0.1, 0.1, 0.1);
40 | App.tweens.add(this.isTypingSprite.scale).to({
41 | x: this.initialScale.x,
42 | y: this.initialScale.y,
43 | z: this.initialScale.z
44 | }, 200, Tweener.ease.quintOut);
45 |
46 | this.add(this.isTypingSprite);
47 |
48 | } else {
49 | this.remove(this.isTypingSprite);
50 | }
51 | }
52 |
53 | destroy () {
54 | this.composition.destroy()
55 | delete this.isTypingSprite;
56 | super.destroy();
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/client/elements/CheckPoint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { Behaviour } from 'behaviour.js'
4 | import LightOscillator from '../behaviors/LightOscillator'
5 | import { getLightFromPool, removeLight } from '../utils';
6 | import QuestIndicator from '../behaviors/QuestIndicator';
7 | import { i18n } from '../lang';
8 |
9 | class CheckPointBehaviour extends Behaviour {
10 | onAttach(level) {
11 |
12 | // TUTORIAL: show quest indicator for first check point
13 | if (level.progress !== 1 && level.progress < 10) {
14 | if (!global.player || global.player.userData.latestProgress < level.progress) {
15 | this.questIndicator = new QuestIndicator();
16 | this.object.addBehaviour(this.questIndicator);
17 | }
18 | }
19 |
20 | this.on("activate", (data) => {
21 | if (!this.light) {
22 | this.light = getLightFromPool();
23 | }
24 |
25 | if (this.questIndicator) { this.questIndicator.detach(); }
26 |
27 | this.light.intensity = 0.5;
28 | this.light.distance = 7;
29 | this.light.color = new THREE.Color(0xfe1313);
30 | this.light.position.set(0, 0, 0);
31 |
32 | this.object.material.map = this.object.getTexture();
33 |
34 | App.tweens.
35 | add(this.light).
36 | from({ intensity: 0 }, 300, Tweener.ease.quartOut).
37 | then(() => this.light.addBehaviour(new LightOscillator, 0.4, 0.6));
38 |
39 | this.object.add(this.light);
40 | })
41 |
42 | }
43 |
44 | onDetach() {
45 | if (this.light) {
46 | App.tweens.remove(this.light);
47 | removeLight(this.light);
48 | }
49 | }
50 |
51 | }
52 |
53 | export default class CheckPoint extends THREE.Object3D {
54 |
55 | constructor (data, level) {
56 | super()
57 |
58 | this.material = new THREE.MeshPhongMaterial({
59 | flatShading: true,
60 | map: this.getTexture(),
61 | side: THREE.FrontSide,
62 | transparent: true
63 | });
64 |
65 | const geometry = new THREE.PlaneGeometry(config.TILE_SIZE, config.TILE_SIZE)
66 | const mesh = new THREE.Mesh(geometry, this.material)
67 | mesh.scale.normalizeWithTexture(this.material.map, true)
68 | mesh.rotateX(-Math.PI / 2);
69 | mesh.position.y -= 0.49;
70 |
71 | this.add(mesh);
72 | this.addBehaviour(new CheckPointBehaviour(), level);
73 |
74 | // Announcement
75 | this.addEventListener("added", () => {
76 | this.dispatchEvent({
77 | bubbles: true,
78 | type: "announcement",
79 | id: "checkpoint",
80 | title: i18n('checkpointArea'),
81 | sound: "checkpointStinger"
82 | });
83 | })
84 | }
85 |
86 | get label () {
87 | return i18n('checkpoint');
88 | }
89 |
90 | getTexture() {
91 | const resourceName = (this.userData.active) ? "checkpoint-active" : "checkpoint";
92 | return ResourceManager.get(`interactive-${resourceName}`);
93 | }
94 |
95 | }
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/client/elements/Chest.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Openable from '../behaviors/Openable'
4 | import NearPlayerOpacity from '../behaviors/NearPlayerOpacity'
5 | import { i18n } from '../lang';
6 |
7 | export default class Chest extends THREE.Object3D {
8 |
9 | constructor (data) {
10 | super()
11 | this.userData = data
12 |
13 | this.body = ResourceManager.getSprite(`interactive-${data.kind}-body`);
14 | this.add(this.body)
15 |
16 | this.head = ResourceManager.getSprite(`interactive-${data.kind}-head`);
17 | this.add(this.head)
18 |
19 | this.openableBehaviour = new Openable
20 | this.addBehaviour(this.openableBehaviour);
21 |
22 | // this.addBehaviour(new NearPlayerOpacity);
23 | }
24 |
25 | get label () {
26 | return (this.openableBehaviour.isOpen)
27 | ? i18n('openedChest')
28 | : i18n('chest')
29 | }
30 |
31 | destroy () {
32 | delete this.openableBehaviour;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/client/elements/Fountain.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Activatable from '../behaviors/Activatable'
4 | import { i18n } from '../lang';
5 |
6 | export default class Fountain extends THREE.Object3D {
7 |
8 | constructor (data) {
9 | super()
10 | this.userData = data
11 |
12 | this.activeSprite = ResourceManager.getSprite( `interactive-fountain` )
13 | this.add(this.activeSprite)
14 |
15 | this.inactiveSprite = ResourceManager.getSprite( `interactive-fountain-dry` )
16 | this.add(this.inactiveSprite)
17 |
18 | this.activeSprite.position.y += 0.1
19 | this.inactiveSprite.position.y += 0.1
20 |
21 | this.activateable = new Activatable()
22 | this.addBehaviour(this.activateable)
23 | }
24 |
25 | get label () {
26 | return ((this.activateable.isActive) ? i18n('fountain') : i18n('dryFountain'))
27 | }
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/client/elements/Item.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Highlight from './effects/Highlight'
4 | import Pickable from '../behaviors/Pickable'
5 | import Stretchable from '../behaviors/Stretchable'
6 | import NearPlayerOpacity from '../behaviors/NearPlayerOpacity'
7 | import { humanize } from '../utils';
8 | import { i18n } from '../lang';
9 |
10 | export default class Item extends THREE.Object3D {
11 |
12 | constructor (data) {
13 | super()
14 |
15 | this.userData = data
16 |
17 | this.sprite = ResourceManager.getSprite( "items-" + data.type )
18 | this.sprite.position.y = 0.5
19 | this.add(this.sprite)
20 |
21 | //
22 | // TODO: add "rarity" to rare items.
23 | //
24 | if (data.isRare || data.isMagical) {
25 | this.highlight = new Highlight(data.isMagical ? 'magical' : 'rare');
26 | this.highlight.position.y = 0.8;
27 | this.highlight.addBehaviour(new Stretchable);
28 | this.add(this.highlight)
29 |
30 | // var light = new THREE.SpotLight(0xffffff, 0.5, 50);
31 | // light.penumbra = 1
32 | // light.addBehaviour(new LightOscillator, 0.5, 0.6, 0.05)
33 | // light.position.set(0, 4, 0)
34 | // light.target = this.item
35 | // this.add(light)
36 |
37 | } else {
38 | this.addBehaviour(new NearPlayerOpacity)
39 | }
40 |
41 | this.sprite.addBehaviour(new Pickable)
42 |
43 | this.onMouseOver = this.onMouseOver.bind(this);
44 | this.onMouseOut = this.onMouseOut.bind(this);
45 |
46 | this.getEntity().on('destroy', () => {
47 | this.sprite.getEntity().destroy();
48 | this.remove(this.sprite);
49 | })
50 |
51 | this.getEntity().on('mouseover', this.onMouseOver);
52 | this.getEntity().on('mouseout', this.onMouseOut);
53 | }
54 |
55 | get label () {
56 | return humanize(i18n(this.userData.type));
57 | }
58 |
59 | onMouseOver (tileSelection) {
60 | tileSelection.setColor(config.COLOR_GREEN)
61 | }
62 |
63 | onMouseOut (tileSelection) {
64 | tileSelection.setColor()
65 | }
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/client/elements/Jail.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import helpers from '../../shared/helpers'
3 | import { i18n } from '../lang';
4 |
5 | export default class Jail extends THREE.Object3D {
6 |
7 | constructor (data, currentProgress, mapkind) {
8 | super()
9 |
10 | this.userData = data
11 | this.mapkind = this.userData.mapkind || mapkind;
12 | this.currentProgress = currentProgress
13 |
14 | this.material = new THREE.MeshPhongMaterial({
15 | flatShading: true,
16 | map: ResourceManager.get('billboards-jail'),
17 | side: THREE.FrontSide,
18 | transparent: true
19 | });
20 |
21 | const geometry = new THREE.PlaneGeometry(config.TILE_SIZE, config.TILE_SIZE)
22 | const mesh = new THREE.Mesh(geometry, this.material)
23 |
24 | if (this.userData.direction === helpers.DIRECTION.SOUTH) {
25 | this.position.y = 0.5;
26 | mesh.position.y = 0.5;
27 | mesh.position.z -= 1.499;
28 |
29 | } else if (this.userData.direction === helpers.DIRECTION.NORTH) {
30 | this.position.y = 0.5;
31 | mesh.position.y = 0.5;
32 | mesh.position.z = 1.5;
33 |
34 | } else if (this.userData.direction === helpers.DIRECTION.WEST) {
35 | this.position.x = 0.5;
36 | mesh.position.x = 1.499;
37 | mesh.position.y = 1;
38 | mesh.rotateY(Math.PI/2);
39 |
40 | } else if (this.userData.direction === helpers.DIRECTION.EAST) {
41 | this.position.x = 0.5;
42 | mesh.position.x = -1.499;
43 | mesh.position.y = 1;
44 | mesh.rotateY(Math.PI/2);
45 | }
46 |
47 | this.onUpdate();
48 |
49 | mesh.scale.normalizeWithTexture(this.material.map, true)
50 | this.add(mesh);
51 |
52 | this.getEntity().on('mouseover', this.onMouseOver.bind(this))
53 | this.getEntity().on('mouseout', this.onMouseOut.bind(this))
54 |
55 | this.getEntity().on('update', this.onUpdate.bind(this));
56 | }
57 |
58 | get isLocked () {
59 | return this.userData.isLocked;
60 | }
61 |
62 | onUpdate () {
63 | if (!this.isLocked) {
64 | App.tweens.add(this.position).to({ y: 3.2 }, 1500, Tweener.ease.quartOut);
65 | }
66 | }
67 |
68 | get label () {
69 | return (this.isLocked) ? i18n('lockedJail') : i18n('openedJail');
70 | }
71 |
72 | onMouseOver (tileSelection) {
73 | tileSelection.setColor(config.COLOR_GREEN)
74 | }
75 |
76 | onMouseOut (tileSelection) {
77 | tileSelection.setColor()
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/client/elements/Leaderboard.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { i18n } from "../lang";
4 |
5 | export default class Leaderboard extends THREE.Object3D {
6 |
7 | constructor (data) {
8 | super()
9 |
10 | this.sprite = ResourceManager.getSprite(`interactive-leaderboard`);
11 | this.sprite.position.y = 0.5;
12 | this.add(this.sprite)
13 | }
14 |
15 | get label () {
16 | return i18n('hallOfFame');
17 | }
18 |
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/client/elements/Lever.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Activatable from '../behaviors/Activatable'
4 | import { i18n } from '../lang';
5 |
6 | export default class Lever extends THREE.Object3D {
7 |
8 | constructor (data, mapkind) {
9 | super()
10 | this.userData = data
11 |
12 | const leverSkin = (mapkind === "inferno") ? "lever-1" : "lever-2";
13 |
14 | this.activeSprite = ResourceManager.getSprite(`interactive-${leverSkin}-active`)
15 | this.add(this.activeSprite)
16 |
17 | this.inactiveSprite = ResourceManager.getSprite(`interactive-${leverSkin}`)
18 | this.add(this.inactiveSprite)
19 |
20 | this.activateable = new Activatable()
21 | this.addBehaviour(this.activateable)
22 | }
23 |
24 | get label () {
25 | return (this.activateable.isActive) ? i18n("activatedLever") : i18n('lever');
26 | }
27 |
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/client/elements/Lifebar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default class Lifebar extends THREE.Object3D {
4 |
5 | constructor () {
6 | super()
7 |
8 | this.colors = {
9 | red: { bg: 0x740000, fg: 0xfc6018 },
10 | yellow: { bg: 0x886408, fg: 0xfcf458 },
11 | green: { bg: 0x183400, fg: 0x7cac20 }
12 | }
13 |
14 | this.background = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xffffffff }))
15 | this.background.scale.x = 1;
16 | this.add(this.background)
17 |
18 | this.bar = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xffffffff }))
19 | // depthWrite: false, depthTest: false
20 | // this.bar.renderOrder = 0
21 | this.add(this.bar)
22 |
23 | this.color = 'green'
24 |
25 | this.scale.set(2, 0.2, 1)
26 | }
27 |
28 | set color (color) {
29 | this.bar.material.color = new THREE.Color(this.colors[ color ].fg)
30 | this.background.material.color = new THREE.Color(this.colors[ color ].bg)
31 | }
32 |
33 | set progress (value) {
34 | if (value > 0.6) {
35 | this.color = 'green'
36 | } else if (value > 0.3) {
37 | this.color = 'yellow'
38 | } else {
39 | this.color = 'red'
40 | }
41 |
42 | this.bar.scale.x = value
43 | // this.bar.position.x = value/2 - 0.5
44 |
45 | // this.background.scale.x = 1
46 | // this.background.position.x = 0.5 + (value/2 - 0.5)
47 | }
48 |
49 | get progress () {
50 | return this.bar.scale.x
51 | }
52 |
53 | }
54 |
55 |
56 |
--------------------------------------------------------------------------------
/client/elements/LightPole.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import LightOscillator from '../behaviors/LightOscillator'
4 | import { getLightFromPool, removeLight } from '../utils';
5 |
6 | export default class LightPole extends THREE.Object3D {
7 |
8 | constructor () {
9 | super()
10 |
11 | this.sprite = ResourceManager.getSprite("billboards-light-pole")
12 | this.sprite.position.y = 0.55;
13 | this.add(this.sprite)
14 |
15 | this.light = getLightFromPool();
16 | this.light.color = new THREE.Color(0xfc6018);
17 | this.light.intensity = 1;
18 | this.light.distance = 8;
19 | this.light.position.set(0, 2, 0);
20 |
21 | // var light = new THREE.PointLight(this.colors[0], 0.3, 50);
22 | this.light.addBehaviour(new LightOscillator, 0.7, 1.1);
23 | this.add(this.light)
24 | }
25 |
26 | destroy () {
27 | removeLight(this.light);
28 | super.destroy();
29 | }
30 |
31 | }
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client/elements/NPC.js:
--------------------------------------------------------------------------------
1 | // Enemy and and NPC share a lot
2 |
3 | import NearPlayerOpacity from '../behaviors/NearPlayerOpacity'
4 | import DangerousThing from '../behaviors/DangerousThing';
5 | import { humanize } from '../utils';
6 | import { i18n } from '../lang';
7 |
8 | export default class NPC extends THREE.Object3D {
9 |
10 | constructor (data) {
11 | super()
12 |
13 | this.userData = data
14 | this._direction = 'bottom'
15 |
16 | this.textures = {
17 | top: ResourceManager.get( 'npc-' + this.userData.kind + '-top' ),
18 | bottom: ResourceManager.get( 'npc-' + this.userData.kind + '-bottom' ),
19 | left: ResourceManager.get( 'npc-' + this.userData.kind + '-left' ),
20 | right: ResourceManager.get( 'npc-' + this.userData.kind + '-right' )
21 | }
22 |
23 | this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({
24 | map: this.textures[ this._direction ],
25 | // fog: true
26 | }))
27 | this.originalColor = this.sprite.material.color.getHex()
28 |
29 | this.sprite.scale.normalizeWithTexture(this.sprite.material.map)
30 | this.sprite.position.y = 0.18
31 |
32 | this.add(this.sprite)
33 |
34 | this.addBehaviour(new DangerousThing)
35 | this.addBehaviour(new NearPlayerOpacity)
36 | }
37 |
38 | get label () {
39 | var text = humanize(i18n(this.userData.kind));
40 |
41 | if (this.userData.hp.current <= 0) {
42 | text = `(${i18n('dead')}) ${ text }`;
43 | }
44 |
45 | return text
46 | }
47 |
48 | set direction (direction) {
49 | this._direction = direction
50 |
51 | var texture = this.textures[ this._direction ]
52 | this.sprite.material.map = texture
53 |
54 | this.sprite.scale.normalizeWithTexture(texture)
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/client/elements/Rock.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export default class Chest extends THREE.Object3D {
4 |
5 | constructor () {
6 | super()
7 |
8 | var i = Math.floor(Math.random() * 3)
9 | // grass = mapkind
10 |
11 | this.sprite = ResourceManager.getSprite( 'aesthetics-grass-rock' + i )
12 | this.sprite.position.y = 0.099
13 | this.add(this.sprite)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/client/elements/StunTile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { i18n } from '../lang';
4 |
5 | export default class StunTile extends THREE.Object3D {
6 |
7 | constructor (data) {
8 | super();
9 |
10 | this.material = new THREE.MeshPhongMaterial({
11 | flatShading: true,
12 | map: ResourceManager.get(`traps-${data.type}`),
13 | side: THREE.FrontSide,
14 | transparent: true
15 | });
16 |
17 | const geometry = new THREE.PlaneGeometry(config.TILE_SIZE * 0.75, config.TILE_SIZE * 0.75);
18 | this.sprite = new THREE.Mesh(geometry, this.material)
19 | this.sprite.scale.normalizeWithTexture(this.material.map, true)
20 | this.sprite.rotateX(-Math.PI / 2);
21 | this.sprite.position.y -= 0.49;
22 |
23 | this.add(this.sprite);
24 | }
25 |
26 | get label () {
27 | return i18n('trap');
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/client/elements/TeleportTile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { i18n } from '../lang';
4 |
5 | export default class TeleportTile extends THREE.Object3D {
6 |
7 | constructor (data) {
8 | super();
9 |
10 | this.material = new THREE.MeshPhongMaterial({
11 | flatShading: true,
12 | map: ResourceManager.get(`interactive-teleport-tile`),
13 | side: THREE.FrontSide,
14 | transparent: true
15 | });
16 |
17 | const geometry = new THREE.PlaneGeometry(config.TILE_SIZE * 0.75, config.TILE_SIZE * 0.75);
18 | this.sprite = new THREE.Mesh(geometry, this.material)
19 | this.sprite.scale.normalizeWithTexture(this.material.map, true)
20 | this.sprite.rotateX(-Math.PI / 2);
21 | this.sprite.position.y -= 0.49;
22 |
23 | this.add(this.sprite);
24 | }
25 |
26 | get label () {
27 | return i18n('teleport');
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/client/elements/TextEvent.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { SpriteText2D, textAlign } from 'three-text2d'
4 | import { i18n } from '../lang';
5 |
6 | const TEXT_MAX_LENGTH = 32;
7 |
8 | export default class TextEvent extends THREE.Object3D {
9 |
10 | constructor (data) {
11 | super()
12 |
13 | let offsetY = 3.5
14 |
15 | let color = config.COLOR_WHITE
16 |
17 | if (data.kind === 'warn' || data.kind === 'yellow') {
18 | color = config.COLOR_YELLOW
19 |
20 | } else if (data.kind === 'attention' || data.kind === 'red') {
21 | color = config.COLOR_RED
22 |
23 | } else if (data.kind === 'blue') {
24 | color = config.COLOR_BLUE
25 | }
26 |
27 | // differentiate drinking potions from damage in battle.
28 | if ((color === config.COLOR_RED || color === config.COLOR_BLUE) && data.text[0] === "+") {
29 | offsetY += 1.5;
30 | }
31 |
32 | this.movingTime = 500
33 | this.waitTime = (typeof(data.ttl)==="undefined") ? 1500 : data.ttl
34 | this.fadeTime = 300
35 |
36 | this.userData = data
37 |
38 | const text = (data.text.length > TEXT_MAX_LENGTH) ? `${data.text.substr(0, TEXT_MAX_LENGTH) }...` : data.text;
39 | this.text = new SpriteText2D(i18n(text), {
40 | font: "40px primary",
41 | fillStyle: `#${color.getHexString()}`,
42 | antialias: false,
43 | align: textAlign.center,
44 | shadowColor: "#000000",
45 | shadowOffsetY: 2,
46 | shadowBlur: 0
47 | })
48 | // this.text.material.alphaTest = 0.5
49 | this.text.material.opacity = 0
50 | this.add(this.text)
51 |
52 | // this.position.y = data.z || 0;
53 |
54 | if (!data.small) {
55 | this.text.scale.set(0.03, 0.03, 0.03)
56 | } else {
57 | offsetY += 1
58 | this.text.scale.set(0.02, 0.02, 0.02)
59 | }
60 |
61 | App.tweens.
62 | add(this.text.material).
63 | to({ opacity: 1 }, this.fadeTime).
64 | wait(this.movingTime + this.waitTime).
65 | to({ opacity: 0 }, this.fadeTime).
66 | then(this.removeFromParent.bind(this))
67 |
68 | if (data.special) {
69 | offsetY += 1.5
70 | App.tweens.add(this.scale).
71 | to({ x: 1.5, y: 1.5 }, this.movingTime, Tweener.ease.cubicOut)
72 | }
73 |
74 | App.tweens.
75 | add(this.position).
76 | to({ y: this.position.y + offsetY }, this.movingTime, Tweener.ease.cubicOut)
77 | }
78 |
79 | removeFromParent () {
80 | if (this.parent) {
81 | this.remove(this.text);
82 | this.text = null;
83 | this.parent.remove(this);
84 | }
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/client/elements/TileSelectionPreview.js:
--------------------------------------------------------------------------------
1 |
2 | export default class TileSelectionPreview extends THREE.Object3D {
3 |
4 | constructor (light, hud) {
5 | super()
6 |
7 | this._target = null
8 |
9 | this.light = light
10 | this.lightColors = {}
11 |
12 | // TODO: decouple cursor reference
13 | this.cursor = hud.cursor
14 | this.selectionText = hud.selectionText
15 |
16 | this.material = new THREE.MeshPhongMaterial( {
17 | map: ResourceManager.get('effects-tile-selection'),
18 | transparent: true,
19 | side: THREE.DoubleSide,
20 | // fog: true
21 | })
22 | this.material.opacity = 0.8
23 | this.geometry = new THREE.PlaneGeometry(config.TILE_SIZE, config.TILE_SIZE)
24 |
25 | this.mesh = new THREE.Mesh(this.geometry, this.material)
26 | this.add(this.mesh)
27 |
28 | this.setColor()
29 | }
30 |
31 | set target (target) {
32 |
33 | if (this._target !== target) {
34 |
35 | if (this._target) {
36 | this.setLabel( null )
37 | this._target.forEach(e => e.__ENTITY__ && e.__ENTITY__.emit('mouseout', this))
38 | }
39 |
40 | if (target) {
41 | // apply HUD label
42 | let availableLabels = target.filter(t => t.label)
43 | , lastLabel = availableLabels.length-1
44 |
45 | if (lastLabel !== -1) {
46 | this.setLabel(availableLabels[lastLabel].label);
47 | }
48 |
49 | target.forEach(e => e.__ENTITY__ && e.__ENTITY__.emit('mouseover', this))
50 | }
51 | }
52 |
53 | // TODO: improve me
54 | // clear highlight color if there's nothing on target
55 | if (target.length == 0) {
56 |
57 | // set pointer cursor
58 | App.cursor.dispatchEvent({ type: 'cursor', kind: 'pointer' })
59 |
60 | this.setColor()
61 |
62 | }
63 |
64 | this._target = target
65 | }
66 |
67 | setColor (hex = 0xffffff) {
68 |
69 | if (hex instanceof THREE.Color) this.lightColors[hex] = hex
70 |
71 | if (!this.lightColors[hex]) {
72 | this.lightColors[hex] = new THREE.Color(hex)
73 | }
74 |
75 | this.material.color = this.lightColors[hex]
76 | this.light.color = this.lightColors[hex]
77 | }
78 |
79 | setLabel (label) {
80 | this.selectionText.visible = !!label
81 |
82 | if (label !== this.selectionText.text) {
83 | this.selectionText.text = label
84 | }
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/client/elements/character/Composition.js:
--------------------------------------------------------------------------------
1 | import { HeroSkinBuilder } from "./HeroSkinBuilder";
2 |
3 | export default class Composition extends THREE.Object3D {
4 |
5 | constructor (props = {}) {
6 | super()
7 |
8 | this.textureOffset = HeroSkinBuilder.getCurrentOffset()
9 |
10 | this.properties = {
11 | cape: props.klass || 0,
12 | cloth: props.klass || 0,
13 | hair: props.hair || 0,
14 | eye: props.eye || 0,
15 | body: props.body || 0
16 | }
17 |
18 | this.colors = {
19 | hair: HeroSkinBuilder.colors.hair[props.hairColor || 0],
20 | eye: HeroSkinBuilder.colors.eye[props.eye || 0],
21 | body: HeroSkinBuilder.colors.body[props.body || 0]
22 | }
23 |
24 | this._direction = 'bottom'
25 | this.updateTexture()
26 | }
27 |
28 | updateTexture () {
29 | HeroSkinBuilder.updateTexture(this)
30 | var map = HeroSkinBuilder.get(this)
31 |
32 | if (!this.sprite) {
33 | this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({
34 | map: map
35 | }))
36 | this.sprite.scale.normalizeWithTexture(map)
37 | this.add(this.sprite)
38 |
39 | } else {
40 | this.sprite.material.map = map
41 | }
42 | }
43 |
44 | updateClass (value) {
45 | this.updateProperty('cloth', value)
46 | this.updateProperty('cape', value)
47 | }
48 |
49 | updateProperty(property, value = null) {
50 | this.properties[property] = value
51 | }
52 |
53 | updateColor (property, color) {
54 | this.colors[ property ] = color
55 | }
56 |
57 | set direction (direction) {
58 | this._direction = direction
59 | this.updateDirection()
60 | }
61 |
62 | updateDirection () {
63 | this.sprite.material.map = HeroSkinBuilder.get(this)
64 | }
65 |
66 | get material () {
67 | return this.sprite.material;
68 | }
69 |
70 | destroy () {
71 | HeroSkinBuilder.deleteTexture(this)
72 | super.destroy();
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/client/elements/controls/Button.js:
--------------------------------------------------------------------------------
1 | export default class Button extends THREE.Object3D {
2 |
3 | constructor (kind = 'button-left') {
4 | super()
5 |
6 | this.userData.hud = true;
7 |
8 | this.hoverScale = 1.04
9 | this.mouseOutScale = 1
10 |
11 | this.standby = new THREE.Sprite(new THREE.SpriteMaterial({
12 | map: ResourceManager.get(`gui-${ kind }`),
13 | transparent: true
14 | }))
15 | this.standby.scale.normalizeWithHUDTexture(this.standby.material.map)
16 | this.add(this.standby)
17 |
18 | this.over = new THREE.Sprite(new THREE.SpriteMaterial({
19 | map: ResourceManager.get(`gui-${ kind }-over`),
20 | transparent: true
21 | }))
22 | this.over.scale.normalizeWithHUDTexture(this.over.material.map)
23 | // this.add(this.over)
24 |
25 | this.width = this.standby.material.map.frame.w * config.HUD_SCALE
26 | this.height = this.standby.material.map.frame.h * config.HUD_SCALE
27 |
28 | this.addEventListener('click', this.onClick.bind(this))
29 | this.addEventListener('mouseover', this.onMouseOver.bind(this))
30 | this.addEventListener('mouseout', this.onMouseOut.bind(this))
31 | }
32 |
33 | colorize (color) {
34 | this.standby.material.color = color
35 | this.over.material.color = color
36 | }
37 |
38 | onClick () {
39 | App.tweens.remove(this.scale)
40 | this.scale.set(1.5, 1.5, 1.5)
41 |
42 | App.tweens.add(this.scale).
43 | to({ x: this.hoverScale, y: this.hoverScale, z: this.hoverScale }, 200, Tweener.ease.cubicOut)
44 | }
45 |
46 | onMouseOut () {
47 | App.tweens.add(this.scale).
48 | to({ x: this.mouseOutScale, y: this.mouseOutScale, z: this.mouseOutScale }, 300, Tweener.ease.cubicInOut)
49 |
50 | this.over.renderOrder = null
51 | if (this.over.parent) this.over.parent.remove(this.over)
52 | this.add(this.standby)
53 | }
54 |
55 | onMouseOver () {
56 | App.tweens.add(this.scale).
57 | to({ x: this.hoverScale, y: this.hoverScale, z: this.hoverScale }, 300, Tweener.ease.cubicInOut)
58 |
59 | this.over.renderOrder = 100
60 | if (this.standby.parent) this.standby.parent.remove(this.standby)
61 | this.add(this.over)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/client/elements/controls/Checkbox.js:
--------------------------------------------------------------------------------
1 | import Button from './Button'
2 |
3 | export default class Checkbox extends Button {
4 |
5 | constructor (asset = 'color-picker') {
6 | super(asset)
7 |
8 | this.checked = false
9 | this.hoverScale = 1.05
10 | this.checkedScale = 1.25
11 | }
12 |
13 | onMouseOut () {
14 | this.mouseOutScale = (this.checked) ? this.checkedScale : 1
15 | super.onMouseOut()
16 | }
17 |
18 | onClick () {
19 | this.checked = true
20 |
21 | App.tweens.remove(this.scale)
22 | this.scale.set(1.5, 1.5, 1.5)
23 |
24 | App.tweens.add(this.scale).
25 | to({ x: this.checkedScale, y: this.checkedScale, z: this.checkedScale }, 200, Tweener.ease.cubicOut)
26 | }
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/client/elements/controls/ColorPicker.js:
--------------------------------------------------------------------------------
1 | import Checkbox from './Checkbox'
2 | import { SpriteText2D, textAlign } from 'three-text2d'
3 |
4 | export default class ColorPicker extends THREE.Object3D {
5 |
6 | constructor (options, title = false) {
7 | super()
8 | var container = new THREE.Object3D()
9 |
10 | this.options = options
11 | this._selectedIndex = -1
12 |
13 | this.buttons = []
14 |
15 | for (let i=0; i this.onMouseOver());
24 | this.addEventListener("mouseout", () => this.onMouseOut());
25 |
26 | this.addEventListener("click", () => {
27 | this.scale.x = this.initialScale.x - 5;
28 | this.scale.y = this.initialScale.y - 5;
29 | this.onMouseOver();
30 | });
31 | }
32 |
33 | get isActive () {
34 | return this.userData.hud === true;
35 | }
36 |
37 | onMouseOver () {
38 | if (this.userData.hint) { Hint.show(this.userData.hint, this.sprite); }
39 |
40 | App.tweens.remove(this.scale);
41 | App.tweens.add(this.scale).to({
42 | x: this.initialScale.x + 4,
43 | y: this.initialScale.y + 4,
44 | }, 300, Tweener.ease.quadOut);
45 | }
46 |
47 | onMouseOut () {
48 | if (this.userData.hint) { Hint.hide(); }
49 | App.tweens.remove(this.scale);
50 | App.tweens.add(this.scale).to(this.initialScale, 200, Tweener.ease.quadOut);
51 | }
52 |
53 | show () {
54 | // skip if already active.
55 | if (this.isActive) { return; }
56 |
57 | this.userData.hud = true;
58 |
59 | if (this.initialX === undefined) {
60 | this.initialX = this.position.x;
61 | }
62 |
63 | this.position.x = this.initialX - 20;
64 | App.tweens.add(this.position).to({ x: this.initialX }, 300, Tweener.ease.cubicOut);
65 | App.tweens.add(this.sprite.material).to({ opacity: 1 }, 500, Tweener.ease.quadOut);
66 | }
67 |
68 | hide () {
69 | // skip if not active already.
70 | if (!this.isActive) { return; }
71 |
72 | this.userData.hud = undefined;
73 |
74 | App.tweens.remove(this.position);
75 | App.tweens.remove(this.sprite.material);
76 | App.tweens.add(this.sprite.material).to({ opacity: 0 }, 200, Tweener.ease.quadOut);
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/client/elements/hud/Minibar.js:
--------------------------------------------------------------------------------
1 | export default class Minibar extends THREE.Object3D {
2 |
3 | constructor (color = 'red') {
4 | super()
5 |
6 | this.colors = {
7 | red: { bg: 0x740000, fg: 0xd00000 },
8 | blue: { bg: 0x000c4c, fg: 0x1c80e4 },
9 | gray: { bg: 0x282828, fg: 0xe0e0e0 },
10 | // yellow: { bg: 0x886408, fg: 0xfcf458 },
11 | // green: { bg: 0x183400, fg: 0x7cac20 },
12 | }
13 |
14 | this.bar = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xffffffff }))
15 | this.add(this.bar)
16 |
17 | this.background = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xffffffff }))
18 | this.add(this.background)
19 |
20 | this.color = color
21 |
22 | this.height = 1 * config.HUD_SCALE
23 | this.width = 10 * config.HUD_SCALE
24 | this.scale.set(this.width, this.height, 1)
25 | }
26 |
27 | set color (color) {
28 | this.bar.material.color = new THREE.Color(this.colors[ color ].fg)
29 | this.background.material.color = new THREE.Color(this.colors[ color ].bg)
30 | }
31 |
32 | set progress (value) {
33 | this.bar.scale.x = value
34 | this.bar.position.x = value/2 - 0.5
35 |
36 | this.background.scale.x = 1 - value
37 | this.background.position.x = 0.5 + (value/2 - 0.5)
38 | }
39 |
40 | get progress () {
41 | return this.bar.scale.x
42 | }
43 |
44 | }
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/client/elements/hud/NewQuestOverlay.js:
--------------------------------------------------------------------------------
1 | import { SpriteText2D, textAlign, MeshText2D } from 'three-text2d'
2 | import { checkpoint } from '../../core/sound';
3 |
4 | export default class NewQuestOverlay extends THREE.Object3D {
5 |
6 | constructor () {
7 | super()
8 |
9 | this.isOpen = false
10 |
11 | this.title = ResourceManager.getHUDElement('hud-big-title-red');
12 | this.title.position.y = this.title.height * 2;
13 | this.add(this.title);
14 |
15 | this.titleText = new MeshText2D("Quests", {
16 | align: textAlign.center ,
17 | font: config.FONT_TITLE,
18 | fillStyle: "#ffffff",
19 | shadowColor: "#000000",
20 | shadowOffsetY: 3,
21 | shadowBlur: 0
22 | });
23 | this.titleText.position.y = this.title.position.y + this.title.height - this.titleText.height - 6;
24 | this.add(this.titleText);
25 |
26 | this.options = new THREE.Object3D();
27 | this.add(this.options);
28 |
29 | this.width = this.title.width;
30 | this.height = this.title.height;
31 | }
32 |
33 | toggleOpen (cb) {
34 | this.isOpen = !this.isOpen
35 | this.visible = true;
36 |
37 | this.options.visible = this.isOpen;
38 |
39 | const scaleFrom = ((this.isOpen) ? 0.5 : 1);
40 | const scaleTo = ((this.isOpen) ? 1 : 0.85);
41 |
42 | this.scale.set(scaleFrom, scaleFrom, scaleFrom);
43 |
44 | if (this.isOpen) {
45 | this.title.materialopacity = 0;
46 | App.tweens.remove(this.title.material);
47 | App.tweens.add(this.title.material).to({ opacity: 1 }, 500, Tweener.ease.quintOut);
48 |
49 | App.tweens.remove(this.titleText.material);
50 | App.tweens.add(this.titleText.material).to({ opacity: 1 }, 500, Tweener.ease.quintOut);
51 |
52 | } else {
53 | App.tweens.remove(this.title.material);
54 | App.tweens.add(this.title.material).to({ opacity: 0 }, 500, Tweener.ease.quintOut);
55 |
56 | App.tweens.remove(this.titleText.material);
57 | App.tweens.add(this.titleText.material).to({ opacity: 0 }, 500, Tweener.ease.quintOut);
58 | }
59 |
60 | // scale container
61 | App.tweens.remove(this.scale)
62 | App.tweens.add(this.scale).to({ x: scaleTo, y: scaleTo, z: scaleTo }, 500, Tweener.ease.quintOut).then(() => {
63 | if (!this.isOpen) this.visible = false;
64 | if (cb) cb();
65 | });
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/client/elements/hud/QuestsButton.js:
--------------------------------------------------------------------------------
1 | import Hint from "../hud/Hint";
2 |
3 | export default class QuestsButton extends THREE.Object3D {
4 |
5 | constructor () {
6 | super()
7 |
8 | this.userData.hud = true;
9 |
10 | var questsMaterial = ResourceManager.get("hud-quests");
11 | this.regular = new THREE.Sprite(new THREE.SpriteMaterial({ map: questsMaterial, transparent: true }));
12 | this.regular.scale.set(questsMaterial.frame.w * config.HUD_SCALE, questsMaterial.frame.h * config.HUD_SCALE, 1);
13 | this.add(this.regular);
14 |
15 | var newQuestMaterial = ResourceManager.get("hud-quests-new");
16 | this.hasNew = new THREE.Sprite(new THREE.SpriteMaterial({ map: newQuestMaterial, transparent: true }));
17 | this.hasNew.scale.set(newQuestMaterial.frame.w * config.HUD_SCALE, newQuestMaterial.frame.h * config.HUD_SCALE, 1);
18 |
19 | this.width = newQuestMaterial.frame.w * config.HUD_SCALE;
20 | this.height = newQuestMaterial.frame.h * config.HUD_SCALE;
21 |
22 | this.addEventListener('mouseover', this.onMouseOver.bind(this));
23 | this.addEventListener('mouseout', this.onMouseOut.bind(this));
24 | this.addEventListener('click', this.onClick.bind(this));
25 | }
26 |
27 | onClick () {
28 | this.parent.openQuests();
29 | }
30 |
31 | onUpdate() {
32 | }
33 |
34 | onMouseOver () {
35 | Hint.show(`Quests`, this);
36 |
37 | App.tweens.remove(this.scale)
38 | App.tweens.add(this.scale).to({ x: 1.1, y: 1.1 }, 200, Tweener.ease.quadOut)
39 | }
40 |
41 | onMouseOut () {
42 | Hint.hide();
43 |
44 | App.tweens.remove(this.scale)
45 | App.tweens.add(this.scale).to({ x: 1, y: 1 }, 200, Tweener.ease.quadOut)
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/client/elements/hud/Resources.js:
--------------------------------------------------------------------------------
1 | import { MeshText2D, textAlign } from 'three-text2d'
2 |
3 | export default class Resources extends THREE.Object3D {
4 |
5 | constructor () {
6 | super()
7 |
8 | // Gold
9 | this.gold = new THREE.Sprite(new THREE.SpriteMaterial({ map: ResourceManager.get("items-gold") }))
10 | this.gold.scale.set(this.gold.material.map.frame.w * config.HUD_SCALE, this.gold.material.map.frame.h * config.HUD_SCALE, 1)
11 | this.add(this.gold)
12 |
13 | this.goldAmount = new MeshText2D("0", {
14 | font: config.DEFAULT_FONT,
15 | align: textAlign.right,
16 | fillStyle: '#fcf458',
17 | antialias: false ,
18 | shadowColor: "#000000",
19 | shadowOffsetY: 3,
20 | shadowBlur: 0
21 | })
22 | this.goldAmount.position.y = config.HUD_SCALE * ( config.HUD_MARGIN * 1.3)
23 | this.goldAmount.position.x = - config.HUD_SCALE * ( config.HUD_MARGIN * 1.5)
24 | this.add(this.goldAmount)
25 |
26 | // Diamond
27 | this.diamond = new THREE.Sprite(new THREE.SpriteMaterial({ map: ResourceManager.get("items-diamond") }))
28 | this.diamond.scale.set(this.diamond.material.map.frame.w * config.HUD_SCALE, this.diamond.material.map.frame.h * config.HUD_SCALE, 1)
29 | this.diamond.position.set(-config.HUD_SCALE/10, - this.diamond.material.map.frame.h * (config.HUD_SCALE + config.HUD_MARGIN), 1)
30 | this.add(this.diamond)
31 |
32 | this.diamondAmount = new MeshText2D("0", {
33 | font: config.DEFAULT_FONT,
34 | align: textAlign.right,
35 | fillStyle: '#4480b0',
36 | antialias: false,
37 | shadowColor: "#000000",
38 | shadowOffsetY: 3,
39 | shadowBlur: 0
40 | })
41 | this.diamondAmount.position.x = - config.HUD_SCALE * ( config.HUD_MARGIN * 1.5)
42 | this.diamondAmount.position.y = this.diamond.position.y + (config.HUD_SCALE * ( config.HUD_MARGIN * 1.3))
43 | this.add(this.diamondAmount)
44 |
45 | this.width = (this.gold.material.map.frame.w * config.HUD_SCALE) / 2
46 | this.height = (this.gold.material.map.frame.h * config.HUD_SCALE) / 2
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/client/elements/hud/SkillButton.js:
--------------------------------------------------------------------------------
1 | import Hint from "./Hint";
2 | import { i18n } from "../../lang";
3 |
4 | export default class SkillButton extends THREE.Object3D {
5 |
6 | constructor (skillName, hotkey) {
7 | super()
8 |
9 | this.userData.hud = true;
10 |
11 | this.userData.hint = `${hotkey} `;
12 |
13 | if (skillName === "attack-speed") {
14 | this.userData.hint += `${i18n('skillAttackSpeed')}
15 | ${i18n('skillAttackSpeedDescription')}
16 | ${i18n('duration')}: 2s
17 | ${i18n('mana_cost')}: 15`;
18 |
19 | } else if (skillName === "movement-speed") {
20 | this.userData.hint += `${i18n('skillMovementSpeed')}
21 | ${i18n('skillMovementSpeedDescription')}
22 | ${i18n('duration')}: 2.5s
23 | ${i18n('mana_cost')}: 10`;
24 | }
25 |
26 | this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: ResourceManager.get(`skills-${skillName}`) }))
27 | this.add(this.sprite)
28 |
29 | this.width = (this.sprite.material.map.frame.w * config.HUD_SCALE) / 2
30 | this.height = (this.sprite.material.map.frame.h * config.HUD_SCALE) / 2
31 |
32 | this.initialScale = {
33 | x: this.sprite.material.map.frame.w * config.HUD_SCALE,
34 | y: this.sprite.material.map.frame.h * config.HUD_SCALE,
35 | };
36 | this.scale.set(this.initialScale.x, this.initialScale.y, 1);
37 |
38 | this.addEventListener("mouseover", () => this.onMouseOver());
39 | this.addEventListener("mouseout", () => this.onMouseOut());
40 |
41 | this.addEventListener("click", () => {
42 | this.dispatchEvent({
43 | type: "skill",
44 | skill: skillName,
45 | bubbles: true
46 | });
47 |
48 | this.scale.x = this.initialScale.x - 5;
49 | this.scale.y = this.initialScale.y - 5;
50 | this.onMouseOut();
51 | });
52 | }
53 |
54 | get isActive () {
55 | return this.userData.hud === true;
56 | }
57 |
58 | onMouseOver () {
59 | if (this.userData.hint) { Hint.show(this.userData.hint, this.sprite); }
60 |
61 | App.tweens.remove(this.scale);
62 | App.tweens.add(this.scale).to({
63 | x: this.initialScale.x + 4,
64 | y: this.initialScale.y + 4,
65 | }, 300, Tweener.ease.quadOut);
66 | }
67 |
68 | onMouseOut () {
69 | if (this.userData.hint) { Hint.hide(); }
70 | App.tweens.remove(this.scale);
71 | App.tweens.add(this.scale).to(this.initialScale, 200, Tweener.ease.quadOut);
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/client/elements/hud/VerticalBar.js:
--------------------------------------------------------------------------------
1 | import hint from "./Hint"
2 |
3 | export default class VerticalBar extends THREE.Object3D {
4 |
5 | constructor (type = 'hp') {
6 | super()
7 |
8 | this.userData.hud = true;
9 |
10 | this.offsetMultiplier = 1
11 | this.attribute = type;
12 |
13 | this.bg = new THREE.Sprite(new THREE.SpriteMaterial({
14 | map: ResourceManager.get(`hud-bar-bg`),
15 | transparent: true
16 | }));
17 | this.bg.material.opacity = 0.6;
18 | this.add(this.bg);
19 |
20 | this.fg = new THREE.Sprite(new THREE.SpriteMaterial({
21 | map: ResourceManager.get("hud-" + type + "-bar-fill"),
22 | transparent: true
23 | }))
24 | this.fg.scale.set(1, 2, 1);
25 | this.fg.material.opacity = 0.85;
26 | this.add(this.fg);
27 |
28 | this.scale.set(this.bg.material.map.frame.w * config.HUD_SCALE, this.bg.material.map.frame.h * config.HUD_SCALE, 1);
29 |
30 | this.width = this.bg.material.map.frame.w * config.HUD_SCALE;
31 | this.height = this.bg.material.map.frame.h * config.HUD_SCALE;
32 |
33 | this.initialOffset = this.fg.material.map.offset.y
34 |
35 | this.addEventListener('mouseover', this.onMouseOver.bind(this))
36 | this.addEventListener('mouseout', this.onMouseOut.bind(this))
37 |
38 | this.set(0)
39 | }
40 |
41 | onMouseOver () {
42 | const update = () => hint.show(player.userData[this.attribute].current + " / " + player.userData[this.attribute].max, this);
43 | update();
44 |
45 | this.updateHintInterval = setInterval(update, 300);
46 | }
47 |
48 | onMouseOut () {
49 | hint.hide();
50 |
51 | clearInterval(this.updateHintInterval);
52 | }
53 |
54 | set (percentage) {
55 | var totalHeight = this.bg.material.map.frame.h
56 | , imgHeight = this.fg.material.map.image.height
57 |
58 | // (1 - %)
59 | var finalPercentage = 1 - percentage
60 | this.fg.material.map.offset.y = this.initialOffset-((totalHeight/imgHeight)*finalPercentage)
61 | this.fg.position.y = -finalPercentage - (this.bg.material.map.frame.h/this.fg.material.map.frame.h)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/client/elements/inventory/EquipedItems.js:
--------------------------------------------------------------------------------
1 | import ItemSlot from './ItemSlot'
2 | import hint from "../hud/Hint"
3 |
4 | export default class EquipedItems extends THREE.Object3D {
5 |
6 | constructor () {
7 | super()
8 |
9 | this.head = new ItemSlot({ accepts: 'head' })
10 | this.left = new ItemSlot({ accepts: 'left' })
11 | this.right = new ItemSlot({ accepts: 'right' })
12 | this.body = new ItemSlot({ accepts: 'body' })
13 | this.feet = new ItemSlot({ accepts: 'feet' })
14 |
15 | this.inventoryType = "equipedItems";
16 |
17 | this.slots = [ this.head, this.left, this.right, this.body, this.feet ];
18 | this.slots.forEach(slot => this.add(slot));
19 |
20 | this.head.position.y = this.head.height - config.HUD_SCALE;
21 | this.feet.position.y = -this.feet.height + config.HUD_SCALE;
22 | this.left.position.x = -this.left.width + config.HUD_SCALE;
23 | this.right.position.x = this.right.width - config.HUD_SCALE;
24 |
25 | this.width = (this.head.width) * 3;
26 | this.height = ((this.head.height) * 3) + 7 * config.HUD_SCALE;
27 | }
28 |
29 | set enabled (bool) {
30 | this.slots.forEach(slot => slot.enabled = bool);
31 | this._enabled = bool;
32 | }
33 |
34 | get enabled () {
35 | return this._enabled;
36 | }
37 |
38 | updateItems () {
39 | const equipmentSlots = ['head', 'left', 'right', 'body', 'feet'];
40 |
41 | for (const slotName of equipmentSlots) {
42 | const item = this.userData.slots[slotName];
43 |
44 | // clear previous item
45 | if (this[slotName].item) {
46 | this[slotName].item.getEntity().destroy();
47 | this[slotName].item = null;
48 | }
49 |
50 | // re-add existing item
51 | if (item) {
52 | const itemIcon = ResourceManager.getHUDElement(`items-${item.type}`);
53 | itemIcon.userData.item = item;
54 | itemIcon.userData.itemId = slotName;
55 | itemIcon.userData.inventoryType = this.inventoryType;
56 | this[slotName].item = itemIcon
57 | this[slotName].enabled = this.enabled;
58 | }
59 | }
60 |
61 | // update hint when data has synched.
62 | hint.update();
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/client/elements/inventory/OpenButton.js:
--------------------------------------------------------------------------------
1 | import Hint from "../hud/Hint";
2 | import { i18n } from "../../lang";
3 |
4 | export default class OpenInventoryButton extends THREE.Object3D {
5 |
6 | constructor () {
7 | super()
8 |
9 | this.userData.hud = true;
10 |
11 | this.isOpen = false
12 |
13 | var closedMaterial = ResourceManager.get("hud-bag")
14 | this.closed = new THREE.Sprite(new THREE.SpriteMaterial({ map: closedMaterial, transparent: true }))
15 | this.closed.scale.set(closedMaterial.frame.w * config.HUD_SCALE, closedMaterial.frame.h * config.HUD_SCALE, 1)
16 | this.add(this.closed)
17 |
18 | var openMaterial = ResourceManager.get("hud-bag-open")
19 | this.open = new THREE.Sprite(new THREE.SpriteMaterial({ map: openMaterial, transparent: true }))
20 | this.open.scale.set(openMaterial.frame.w * config.HUD_SCALE, openMaterial.frame.h * config.HUD_SCALE, 1)
21 |
22 | this.openYOffset = openMaterial.frame.h - closedMaterial.frame.h - 2
23 |
24 | this.width = closedMaterial.frame.w * config.HUD_SCALE
25 | this.height = closedMaterial.frame.h * config.HUD_SCALE
26 |
27 | this.addEventListener('mouseover', this.onMouseOver.bind(this))
28 | this.addEventListener('mouseout', this.onMouseOut.bind(this))
29 | this.addEventListener('click', this.onClick.bind(this))
30 | }
31 |
32 | onClick () {
33 | // toggle open
34 |
35 | if (this.isOpen) {
36 | } else {
37 | }
38 |
39 | this.isOpen = !this.isOpen
40 | }
41 |
42 | onOpen() {
43 | App.clock.setTimeout(() => {
44 | this.add(this.open)
45 | this.remove(this.closed)
46 | }, 80)
47 | App.tweens.add(this.closed.position).to({ y: -this.openYOffset * config.HUD_SCALE }, 150, Tweener.ease.quintOut)
48 | }
49 |
50 | onClose() {
51 | this.add(this.closed)
52 | this.remove(this.open)
53 | App.tweens.add(this.closed.position).to({ y: 0 }, 200, Tweener.ease.quintOut)
54 | }
55 |
56 | onMouseOver () {
57 | Hint.show(`I B ${i18n('inventory')}`, this);
58 |
59 | App.tweens.remove(this.scale)
60 | App.tweens.add(this.scale).to({ x: 1.1, y: 1.1 }, 200, Tweener.ease.quadOut)
61 | }
62 |
63 | onMouseOut () {
64 | Hint.hide();
65 |
66 | App.tweens.remove(this.scale)
67 | App.tweens.add(this.scale).to({ x: 1, y: 1 }, 200, Tweener.ease.quadOut)
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/client/elements/inventory/components/DraggableItem.js:
--------------------------------------------------------------------------------
1 | import { Behaviour } from 'behaviour.js'
2 |
3 | export default class DraggableItem extends Behaviour {
4 |
5 | onAttach () {
6 | this.isDragging = false
7 |
8 | this.object.addEventListener('click', this.toggleDrag.bind(this))
9 | this.object.addEventListener('touchstart', this.startDrag.bind(this))
10 | this.object.addEventListener('touchend', this.stopDrag.bind(this))
11 | }
12 |
13 | toggleDrag () {
14 | if (!this.isDragging) {
15 | this.startDrag()
16 | } else {
17 | this.stopDrag()
18 | }
19 | }
20 |
21 | startDrag () {
22 | this.isDragging = true
23 | }
24 |
25 | stopDrag (e) {
26 | e.preventDefault();
27 | this.isDragging = false
28 | }
29 |
30 | onDetach () {
31 | }
32 |
33 |
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/client/game/level/Minimap.js:
--------------------------------------------------------------------------------
1 | // var canvas = document.createElement('canvas')
2 | // , ctx = canvas.getContext('2d')
3 | //
4 | // canvas.style.position = "absolute"
5 | // canvas.style.top = "50px"
6 | // canvas.style.left = "50px"
7 | // window.document.body.appendChild(canvas)
8 | //
9 | // canvas.width = gridSize.x * TILE_SIZE
10 | // canvas.height = gridSize.y * TILE_SIZE
11 | //
12 | // function drawTile(type, x, y) {
13 | // ctx.fillStyle = '#ffffff'
14 | // ctx.fillRect(
15 | // x * TILE_SIZE,
16 | // y * TILE_SIZE,
17 | // TILE_SIZE,
18 | // TILE_SIZE
19 | // );
20 | // }
21 | //
22 | // function drawGridMap(grid) {
23 | // var xlen = grid.length,
24 | // ylen = grid[0].length;
25 | //
26 | // //draw dungeon grid
27 | // for(var x = 0; x < xlen; ++x) {
28 | // for(var y = 0; y < ylen; ++y) {
29 | // var tile = grid[x][y];
30 | //
31 | // if(tile & helpers.TILE_TYPE.EMPTY)
32 | // continue;
33 | //
34 | // if(tile & helpers.TILE_TYPE.FLOOR) {
35 | // drawTile('floor', x, y);
36 | // //ctx.fillStyle = options.colors[helpers.TILE_TYPE.FLOOR];
37 | // }
38 | //
39 | // //ctx.fillRect(x * scale.x, y * scale.y, scale.x, scale.y);
40 | // }
41 | // }
42 | // }
43 | // drawGridMap(grid)
44 |
--------------------------------------------------------------------------------
/client/lang/index.js:
--------------------------------------------------------------------------------
1 | const LANGUAGES = {
2 | en: require('./en'),
3 | pt: require('./pt'),
4 | tr: require('./tr'),
5 | }
6 |
7 | let CURRENT_LANG = navigator.language.replace(/\-.+/i, "")
8 |
9 | export function setLanguage(lang) {
10 | CURRENT_LANG = lang;
11 | }
12 |
13 | export function i18n(id) {
14 | return (LANGUAGES[CURRENT_LANG] || LANGUAGES['en'])[id] || id;
15 | }
16 |
--------------------------------------------------------------------------------
/client/resource/sounds/boss/boss.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/boss/boss.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/boss/kill-boss.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/boss/kill-boss.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/chest.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/chest.wav
--------------------------------------------------------------------------------
/client/resource/sounds/coin.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/coin.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/death1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/death1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/death2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/death2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/death3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/death3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/door-2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/door-2.wav
--------------------------------------------------------------------------------
/client/resource/sounds/door.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/door.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/checkpoint-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/checkpoint-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/checkpoint.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/checkpoint.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/lever.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/lever.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/open-portal-inferno.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/open-portal-inferno.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/open-portal.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/open-portal.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/pending.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/pending.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/portal.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/portal.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/stun.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/stun.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/effects/teleport.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/effects/teleport.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/enemies/default.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/enemies/default.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/enemies/mimic.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/enemies/mimic.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/enemies/snake.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/enemies/snake.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/fountain.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/fountain.wav
--------------------------------------------------------------------------------
/client/resource/sounds/hit1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/hit1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/hit2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/hit2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/hit3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/hit3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/hit4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/hit4.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/inventory/buy.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/inventory/buy.wav
--------------------------------------------------------------------------------
/client/resource/sounds/inventory/close.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/inventory/close.ogg
--------------------------------------------------------------------------------
/client/resource/sounds/inventory/open.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/inventory/open.ogg
--------------------------------------------------------------------------------
/client/resource/sounds/inventory/sell.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/inventory/sell.wav
--------------------------------------------------------------------------------
/client/resource/sounds/levelup.aif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/levelup.aif
--------------------------------------------------------------------------------
/client/resource/sounds/music/higure-forest.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/music/higure-forest.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/music/moonlight-forest.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/music/moonlight-forest.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/music/plague-of-nighterrors.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/music/plague-of-nighterrors.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/potion.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/potion.wav
--------------------------------------------------------------------------------
/client/resource/sounds/skills/attack-speed-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/attack-speed-1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/skills/attack-speed-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/attack-speed-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/skills/attack-speed-3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/attack-speed-3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/skills/movement-speed-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/movement-speed-1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/skills/movement-speed-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/movement-speed-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/skills/movement-speed-3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/skills/movement-speed-3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/spells/generic.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/spells/generic.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/step1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/step1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/step2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/step2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/step3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/step3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/step4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/step4.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/stingers/checkpointStinger.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/stingers/checkpointStinger.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/stingers/death.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/stingers/death.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/stingers/mapkind.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/stingers/mapkind.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/approve-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/approve-1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/approve-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/approve-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/approve-3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/approve-3.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/approve-4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/approve-4.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/approve-5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/approve-5.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/voices/potion-seller-1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/potion-seller-1.wav
--------------------------------------------------------------------------------
/client/resource/sounds/voices/potion-seller-2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/potion-seller-2.wav
--------------------------------------------------------------------------------
/client/resource/sounds/voices/potion-seller-3.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/voices/potion-seller-3.wav
--------------------------------------------------------------------------------
/client/resource/sounds/weapons/bow-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/weapons/bow-1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/weapons/bow-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/weapons/bow-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/weapons/staff-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/weapons/staff-1.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/weapons/staff-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/weapons/staff-2.mp3
--------------------------------------------------------------------------------
/client/resource/sounds/woosh1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/woosh1.wav
--------------------------------------------------------------------------------
/client/resource/sounds/woosh2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/woosh2.wav
--------------------------------------------------------------------------------
/client/resource/sounds/woosh3.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/client/resource/sounds/woosh3.wav
--------------------------------------------------------------------------------
/client/utils/device.js:
--------------------------------------------------------------------------------
1 | import IsMobile from 'is-mobile'
2 | import fullscreen from 'fullscreen'
3 |
4 | export const isMobile = IsMobile();
5 |
6 | // if (isMobile()) {
7 | // let el = window.document.body
8 | // , fs = fullscreen(el)
9 | //
10 | // el.addEventListener('click', function() {
11 | // fs.request()
12 | // })
13 | // }
14 |
--------------------------------------------------------------------------------
/client/utils/index.js:
--------------------------------------------------------------------------------
1 | export function humanize (value) {
2 | if (!value) { value = ""; }
3 |
4 | const camelMatch = /([A-Z])/g;
5 | const underscoreMatch = /_/g;
6 |
7 | const camelCaseToSpaces = value.replace(camelMatch, " $1");
8 | const underscoresToSpaces = camelCaseToSpaces.replace(underscoreMatch, " ");
9 | const caseCorrected =
10 | underscoresToSpaces.charAt(0).toUpperCase() +
11 | underscoresToSpaces.slice(1).toLowerCase();
12 |
13 | return caseCorrected.replace(/\-/g, " ").replace(/([0-9]+)$/g, " - T$1");
14 | }
15 |
16 | export function trackEvent(name, options) {
17 | if (process.env.NODE_ENV === "production") {
18 | gtag('event', name, options);
19 | }
20 | }
21 |
22 | // 15 rotating lights to reuse
23 | const lightPool = [
24 | new THREE.PointLight(0x1c80e4, 0, 0),
25 | new THREE.PointLight(0x1c80e4, 0, 0),
26 | new THREE.PointLight(0x1c80e4, 0, 0),
27 | new THREE.PointLight(0x1c80e4, 0, 0),
28 | new THREE.PointLight(0x1c80e4, 0, 0),
29 | new THREE.PointLight(0x1c80e4, 0, 0),
30 | new THREE.PointLight(0x1c80e4, 0, 0),
31 | // More lights than 8 are causing WebGL issues on low end devices.
32 |
33 | // new THREE.PointLight(0x1c80e4, 0, 0),
34 | // new THREE.PointLight(0x1c80e4, 0, 0),
35 | // new THREE.PointLight(0x1c80e4, 0, 0),
36 | // new THREE.PointLight(0x1c80e4, 0, 0),
37 | // new THREE.PointLight(0x1c80e4, 0, 0),
38 | // new THREE.PointLight(0x1c80e4, 0, 0),
39 | // new THREE.PointLight(0x1c80e4, 0, 0),
40 | // new THREE.PointLight(0x1c80e4, 0, 0),
41 | ];
42 |
43 | let currentLight = 0;
44 | export function getLightFromPool() {
45 | let retries = 0;
46 |
47 | do {
48 | currentLight = (currentLight + 1) % lightPool.length;
49 | retries++;
50 | } while (lightPool[currentLight].intensity !== 0 && retries < lightPool.length);
51 |
52 | return lightPool[currentLight];
53 | }
54 |
55 | export function getLightPoolCount() {
56 | return lightPool.length;
57 | }
58 |
59 | export function removeLight(light) {
60 | light.getEntity().destroy();
61 | light.intensity = 0;
62 | light.distance = 0;
63 | window.scene.add(light);
64 | }
65 |
66 | export function toScreenPosition(camera, obj) {
67 | var vector = new THREE.Vector3();
68 |
69 | var widthHalf = 0.5 * window.innerWidth;
70 | var heightHalf = 0.5 * window.innerHeight;
71 |
72 | obj.updateMatrixWorld();
73 | vector.setFromMatrixPosition(obj.matrixWorld);
74 | vector.project(camera);
75 |
76 | vector.x = (vector.x * widthHalf) + widthHalf;
77 | vector.y = - (vector.y * heightHalf) + heightHalf;
78 |
79 | return {
80 | x: vector.x,
81 | y: vector.y
82 | };
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/color-reference.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/color-reference.png
--------------------------------------------------------------------------------
/conver.pe:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/fontforge
2 | Open($1)
3 | Generate($1:r + ".otf")
4 | Generate($1:r + ".svg")
5 | Generate($1:r + ".woff")
6 | Generate($1:r + ".woff2")
7 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | /* PM2 config file */
2 |
3 | module.exports = {
4 | apps : [{
5 | name : 'mazmorra.io',
6 | script : 'server/index.js',
7 | watch : true,
8 | instances : 'max',
9 | env: {
10 | NODE_ENV: 'development'
11 | },
12 | env_production : {
13 | NODE_ENV: 'production'
14 | }
15 | }],
16 |
17 | deploy : {
18 | production : {
19 | user : 'root',
20 | host : ['68.183.149.229'],
21 | ref : 'origin/colyseus-upgrade',
22 | repo : 'git@github.com:endel/mazmorra.git',
23 | path : '/root/mazmorra',
24 | 'post-deploy' : './node_modules/.bin/yarn install && npm install --prefix server && ./node_modules/.bin/yarn build && pm2 startOrRestart ecosystem.config.js --env production'
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/export_layers.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 | require 'psd'
3 |
4 | def is_power_of_two(x)
5 | n = x.to_i
6 | while (((x % 2) == 0) && x > 1)
7 | x = x/2
8 | end
9 | x == 1 && n != 1
10 | end
11 |
12 | file = ARGV[0] || './assets.psd'
13 | psd = PSD.new(file, parse_layer_images: true)
14 | psd.parse!
15 |
16 | file_list = []
17 | psd.tree.descendant_layers.each do |layer|
18 | original = layer.image.to_png
19 |
20 | next if !layer.path.index('ignore').nil? || original.width == 0 || original.height == 0
21 |
22 | filename = "images/sprites/#{layer.path.gsub('/', '-').strip}.png"
23 | # power_of_two.save("app/#{filename}")
24 | original.save("public/#{filename}")
25 | file_list << filename
26 | end
27 |
28 | File.open('client/resource/data.json', 'w+') do |f|
29 | f.write(file_list.to_json)
30 | end
31 |
--------------------------------------------------------------------------------
/faviconDescription.json:
--------------------------------------------------------------------------------
1 | {
2 | "masterPicture": "./public/images/icons/original.png",
3 | "iconsPath": "images/icons/",
4 | "design": {
5 | "ios": {
6 | "pictureAspect": "backgroundAndMargin",
7 | "backgroundColor": "#000000",
8 | "margin": "28%",
9 | "assets": {
10 | "ios6AndPriorIcons": false,
11 | "ios7AndLaterIcons": false,
12 | "precomposedIcons": false,
13 | "declareOnlyDefaultIcon": true
14 | }
15 | },
16 | "desktopBrowser": {},
17 | "windows": {
18 | "pictureAspect": "noChange",
19 | "backgroundColor": "#9f00a7",
20 | "onConflict": "override",
21 | "assets": {
22 | "windows80Ie10Tile": false,
23 | "windows10Ie11EdgeTiles": {
24 | "small": false,
25 | "medium": true,
26 | "big": false,
27 | "rectangle": false
28 | }
29 | }
30 | },
31 | "androidChrome": {
32 | "pictureAspect": "noChange",
33 | "themeColor": "#000000",
34 | "manifest": {
35 | "short_name": "Mazmorra",
36 | "name": "Mazmorra.io Beta",
37 | "start_url": "/",
38 | "display": "fullscreen",
39 | "orientation": "landscape",
40 | "theme_color": "#000000",
41 | "background_color": "#000000"
42 | },
43 | "assets": {
44 | "legacyIcon": true,
45 | "lowResolutionIcons": false
46 | }
47 | },
48 | "safariPinnedTab": {
49 | "pictureAspect": "blackAndWhite",
50 | "threshold": 50,
51 | "themeColor": "#5bbad5"
52 | }
53 | },
54 | "settings": {
55 | "scalingAlgorithm": "Mitchell",
56 | "errorOnImageTooSmall": false,
57 | "readmeFile": false,
58 | "htmlCodeFile": true,
59 | "usePathAsIs": false
60 | }
61 | }
--------------------------------------------------------------------------------
/hud.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/hud.psd
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/logo.png
--------------------------------------------------------------------------------
/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/logo.psd
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "./node_modules/.bin/webpack-dev-server",
5 | "build": "NODE_ENV=production ./node_modules/.bin/webpack --mode=production",
6 | "dev": "npm start --prefix server & npm start",
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "soundtracks": "audiosprite client/resource/sounds/music/*.mp3 --format howler --output soundtracks && sed 's/urls/src/g' soundtracks.json > public/soundtracks.json",
9 | "favicons": "real-favicon generate faviconDescription.json faviconData.json public/images/icons/"
10 | },
11 | "devDependencies": {
12 | "@babel/core": "^7.4.5",
13 | "@babel/plugin-proposal-class-properties": "^7.4.4",
14 | "@babel/plugin-proposal-decorators": "^7.4.4",
15 | "@babel/plugin-proposal-optional-chaining": "^7.2.0",
16 | "@babel/plugin-transform-runtime": "^7.4.4",
17 | "@babel/preset-env": "^7.4.5",
18 | "@babel/preset-react": "^7.0.0",
19 | "@babel/runtime": "^7.4.5",
20 | "babel-loader": "^8.0.6",
21 | "babel-plugin-transform-class-properties": "^6.24.1",
22 | "css-loader": "^0.28.11",
23 | "extract-text-webpack-plugin": "next",
24 | "file-loader": "^1.1.11",
25 | "spritesheet-js": "^1.0.5",
26 | "style-loader": "^0.21.0",
27 | "stylus": "^0.54.5",
28 | "stylus-loader": "^3.0.2",
29 | "terser-webpack-plugin": "^1.3.0",
30 | "url-loader": "^1.0.1",
31 | "val-loader": "^1.1.0",
32 | "webpack": "^4.8.3",
33 | "webpack-cli": "^3.1.2",
34 | "webpack-dev-server": "^3.1.4",
35 | "typescript": "^3.4.3",
36 | "cli-real-favicon": "0.0.8"
37 | },
38 | "dependencies": {
39 | "audiosprite-loader": "^0.1.5",
40 | "behaviour.js": "^0.2.4",
41 | "clock-timer.js": "^1.1.4",
42 | "colyseus.js": "^0.14.0",
43 | "core-js": "^3.1.4",
44 | "fullscreen": "^1.0.2",
45 | "howler": "^2.1.1",
46 | "is-mobile": "^0.2.2",
47 | "keycode.js": "0.0.1",
48 | "lerp": "^1.0.3",
49 | "random-seed": "^0.3.0",
50 | "regenerator-runtime": "^0.13.2",
51 | "three": "^0.106.2",
52 | "three-text2d": "^0.5.3",
53 | "throttle.js": "^1.0.0",
54 | "tiny-emitter": "^1.0.2",
55 | "tweener": "0.1.28"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/press/legionary-course.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/press/legionary-course.png
--------------------------------------------------------------------------------
/press/magicavoxel/heros.vox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/press/magicavoxel/heros.vox
--------------------------------------------------------------------------------
/public/fb-share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/fb-share.png
--------------------------------------------------------------------------------
/public/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/background.jpg
--------------------------------------------------------------------------------
/public/images/github-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/github-icon.png
--------------------------------------------------------------------------------
/public/images/icons/original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/icons/original.png
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/portal.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/portal.gif
--------------------------------------------------------------------------------
/public/images/sprites/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/sprites/.gitkeep
--------------------------------------------------------------------------------
/public/images/tutorial/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/.DS_Store
--------------------------------------------------------------------------------
/public/images/tutorial/chests.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/chests.png
--------------------------------------------------------------------------------
/public/images/tutorial/enemies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/enemies.png
--------------------------------------------------------------------------------
/public/images/tutorial/inventory-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/inventory-1.png
--------------------------------------------------------------------------------
/public/images/tutorial/inventory-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/inventory-2.png
--------------------------------------------------------------------------------
/public/images/tutorial/inventory-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/inventory-3.png
--------------------------------------------------------------------------------
/public/images/tutorial/inventory-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/inventory-4.png
--------------------------------------------------------------------------------
/public/images/tutorial/level-up-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/level-up-1.png
--------------------------------------------------------------------------------
/public/images/tutorial/level-up-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/level-up-2.png
--------------------------------------------------------------------------------
/public/images/tutorial/move.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/move.png
--------------------------------------------------------------------------------
/public/images/tutorial/portal-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/portal-1.png
--------------------------------------------------------------------------------
/public/images/tutorial/portal-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/portal-2.png
--------------------------------------------------------------------------------
/public/images/tutorial/potion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/tutorial/potion.png
--------------------------------------------------------------------------------
/public/images/youtube-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/endel/mazmorra/026071c83d402f9a87babf7c816b8b10687a7aa2/public/images/youtube-icon.png
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | DEBUG=colyseus:errors,mazmorra:*
2 |
3 | # NODE_ENV=production
4 | NODE_ENV=development
5 | MONITOR_PASSWORD=mazmorra
6 |
7 | MONGO_URI=mongodb://localhost:27017/mazmorra
8 | FACEBOOK_APP_TOKEN=423509175166200|f3d32a37a1650a4bde81c216ea198677
9 |
10 | WEBPUSH_SUBJECT=mailto:endel@gamestd.io
11 | WEBPUSH_PUBLIC_KEY=BGMKTbSGocUARpMHCJLqSJfs-Z7FcZq53SuRNzOZ2PD1qzJEXRZUiEiPoguj2OcyAAedmdOuCasI3g1626gHglE
12 | WEBPUSH_PRIVATE_KEY=nmzH-37p_8PJFzAxPOcUmeZCCob-kvNwNMPB_DBcjL4
13 |
14 |
15 | ## Development
16 | #FACEBOOK_APP_TOKEN=2370808886332893|b64a4359ca46db2bc4f4e1c464e7b6bd
17 |
--------------------------------------------------------------------------------
/server/StatsDocs.ts:
--------------------------------------------------------------------------------
1 | import * as Config from "./utils/ProgressionConfig";
2 |
3 | const labels = {
4 | "Total number of levels in the game": Config.MAX_LEVELS,
5 | "Number of levels between different maps": Config.NUM_LEVELS_PER_MAP,
6 | "Number levels between checkpoints": Config.NUM_LEVELS_PER_CHECKPOINT,
7 | "Max. Body Armor": Config.MAX_ARMOR_ARMOR,
8 | "Max. Shield Armor": Config.MAX_SHIELD_ARMOR,
9 | "Max. Boots Armor": Config.MAX_BOOTS_ARMOR,
10 | "Max. Helmet Armor": Config.MAX_HELMET_ARMOR,
11 | "Max. Boots Movement Speed": Config.MAX_BOOTS_MOVEMENT_SPEED,
12 | "Max. Melee Weapon Damage": Config.MAX_WEAPON_DAMAGE,
13 | "Max. Bow Damage": Config.MAX_BOW_DAMAGE,
14 | "Max. Staff Damage": Config.MAX_STAFF_DAMAGE,
15 | "Max. Bow Attack Distance": Config.MAX_BOW_ATTACK_DISTANCE,
16 | "Max. Bow Staff Distance": Config.MAX_STAFF_ATTACK_DISTANCE,
17 | }
18 |
19 | const markdown = Object.keys(labels).map((label => {
20 | return `- ${label}: ${labels[label]}`;
21 | })).join("\n");
22 |
23 | console.log(markdown);
24 |
--------------------------------------------------------------------------------
/server/actions/Action.ts:
--------------------------------------------------------------------------------
1 | import { Schema, type } from "@colyseus/schema";
2 |
3 | export class Action extends Schema {
4 | @type("string") type: string;
5 | @type("number") lastUpdateTime: number = Date.now();
6 | @type("boolean") active: boolean;
7 |
8 | constructor (type: string = "attack", active: boolean = false) {
9 | super();
10 | this.type = type;
11 | this.active = active;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/controllers/hero.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import helpers from "../../shared/helpers"
3 | import { jwtMiddleware } from "@colyseus/social/express";
4 |
5 | import { Hero, ATTRIBUTE_BASE_VALUE, DBItem } from "../db/Hero";
6 | import { Attribute } from "../entities/Unit";
7 |
8 | export const router = express.Router()
9 |
10 | router.get('/', jwtMiddleware, async (req: express.Request, res: express.Response) => {
11 | const heroes = await Hero.find({ userId: req.cauth._id, alive: true });
12 | res.json(heroes);
13 | });
14 |
15 | router.delete('/', jwtMiddleware, async (req, res) => {
16 | // delete previous user's heroes.
17 | const deleted = await Hero.deleteOne({
18 | userId: req.cauth._id,
19 | _id: req.body._id
20 | });
21 |
22 | if (deleted.n > 0) {
23 | const heroes = await Hero.find({ userId: req.cauth._id, alive: true });
24 | res.json(heroes);
25 |
26 | } else {
27 | res.status(403);
28 | res.json({ error: "invalid hero" });
29 | }
30 | });
31 |
32 |
33 | router.post('/', jwtMiddleware, async (req, res) => {
34 | // see client/config.js for ordering
35 | const primaryAttributes: Attribute[] = [
36 | 'strength',
37 | 'intelligence',
38 | 'agility',
39 | ];
40 |
41 | const initialWeapon: { [id: string]: DBItem } = {
42 | 'strength': {
43 | type: helpers.ENTITIES.WEAPON_1, modifiers: [{
44 | attr: "damage",
45 | modifier: 0
46 | }]
47 | },
48 | 'intelligence': {
49 | type: helpers.ENTITIES.WAND_1, modifiers: [{
50 | attr: "damage",
51 | modifier: 0
52 | }, {
53 | attr: "attackDistance",
54 | modifier: 1
55 | }],
56 | },
57 | 'agility': {
58 | type: helpers.ENTITIES.BOW_1, modifiers: [{
59 | attr: "damage",
60 | modifier: 0
61 | }, {
62 | attr: "attackDistance",
63 | modifier: 1
64 | }]
65 | },
66 | }
67 |
68 | const primaryAttribute = primaryAttributes[req.body.klass];
69 |
70 | // delete previous user's heroes.
71 | const numHeroes = await Hero.count({ userId: req.cauth._id });
72 |
73 | // each player can only have 3 heroes
74 | if (numHeroes < 3) {
75 | // (mongoose type definitions used to allow the call below.)
76 | // @ts-ignore
77 | res.json(await Hero.create({
78 | userId: req.cauth._id,
79 |
80 | name: req.body.name.substr(0, 20),
81 | klass: req.body.klass,
82 | hair: req.body.hair,
83 | hairColor: req.body.hairColor,
84 | eye: req.body.eye,
85 | body: req.body.body,
86 |
87 | primaryAttribute,
88 | [primaryAttribute]: ATTRIBUTE_BASE_VALUE + 3,
89 |
90 | equipedItems: [initialWeapon[primaryAttribute]]
91 | }));
92 |
93 | } else {
94 | res.status(403);
95 | res.json({error: "max heroes reached"});
96 | }
97 | });
98 |
--------------------------------------------------------------------------------
/server/core/Bar.ts:
--------------------------------------------------------------------------------
1 | import { Schema, type } from "@colyseus/schema";
2 | import { EventEmitter } from "events";
3 |
4 | export class Bar extends Schema {
5 | @type("number") current: number;
6 | @type("number") max: number;
7 |
8 | attribute: string;
9 | public events: EventEmitter;
10 |
11 | constructor (attribute: string, current?: number, max?: number) {
12 | super();
13 | this.current = current;
14 | this.max = max || current;
15 |
16 | this.attribute = attribute;
17 |
18 | if (this.attribute === "xp") {
19 | this.events = new EventEmitter();
20 | }
21 | }
22 |
23 | increment (value: number) {
24 | if (this.attribute === "xp") {
25 | // xp bar is re-set, and emits an event when max is reached.
26 | let xp: number = value
27 |
28 | while (this.current + xp > this.max) {
29 | xp = (this.current + xp) - this.max;
30 |
31 | this.events.emit('lvl-up');
32 | this.current = 0;
33 | }
34 |
35 | this.current += xp;
36 |
37 | } else {
38 | this.set(this.current + value);
39 | }
40 | }
41 |
42 | set (value: number, max?: number) {
43 | if (max) {
44 | this.max = max;
45 | }
46 |
47 | this.current = Math.max(0, Math.min(value, this.max));
48 | }
49 |
50 | dispose() {
51 | if (this.events) {
52 | this.events.removeAllListeners();
53 | delete this.events;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/server/core/EquipedItems.ts:
--------------------------------------------------------------------------------
1 | import { Inventory } from "./Inventory";
2 | import { EquipableItem } from "../entities/items/EquipableItem";
3 | import { EquipmentSlot } from "./EquipmentSlot";
4 | import { Item } from "../entities/Item";
5 | import { EventEmitter } from "events";
6 |
7 | export class EquipedItems extends Inventory {
8 | events = new EventEmitter();
9 |
10 | constructor () {
11 | super({ capacity: 5 })
12 | }
13 |
14 | isSlotAvailable(slot: EquipmentSlot) {
15 | return !this.slots[slot];
16 | }
17 |
18 | add (item: EquipableItem, force: boolean = false) {
19 | // FORCE is a workaround for a @colyseus/schema bug
20 | const hasAvailability = this.isSlotAvailable(item.slotName) || force;
21 |
22 | if (hasAvailability) {
23 | this.slots[item.slotName] = item;
24 | this.events.emit('change');
25 | }
26 |
27 | return hasAvailability;
28 | }
29 |
30 | getItem(itemIdOrSlotName: string) {
31 | if (this.slots[itemIdOrSlotName]) {
32 | // allow to get item by slot name
33 | return this.slots[itemIdOrSlotName];
34 |
35 | } else {
36 | // allow to get item by id
37 | return Array.from(this.slots.values()).find((item) => {
38 | return item.id === itemIdOrSlotName;
39 | });
40 | }
41 | }
42 |
43 | dropRandomItem() {
44 | const equippedSlots: EquipmentSlot[] = [
45 | EquipmentSlot.HEAD,
46 | EquipmentSlot.BODY,
47 | EquipmentSlot.LEFT,
48 | EquipmentSlot.RIGHT,
49 | EquipmentSlot.FEET
50 | ].filter(slotName => !this.isSlotAvailable(slotName));
51 |
52 | const dropItemFromSlot = equippedSlots[Math.floor(Math.random() * equippedSlots.length)];
53 |
54 | if (dropItemFromSlot) {
55 | const item = this.getItem(dropItemFromSlot);
56 | this.remove(dropItemFromSlot);
57 | return item;
58 | }
59 | }
60 |
61 | remove(itemIdOrSlotName: string) {
62 | if (this.slots[itemIdOrSlotName]) {
63 | delete this.slots[itemIdOrSlotName];
64 | this.events.emit('change');
65 | return true;
66 |
67 | } else {
68 | const entries = Array.from(this.slots.entries());
69 | for (const [slotName, itemInSlot] of entries) {
70 | if (itemInSlot.id === itemIdOrSlotName) {
71 | delete this.slots[slotName];
72 | this.events.emit('change');
73 | return true;
74 | }
75 | }
76 | }
77 |
78 | return false;
79 | }
80 |
81 | dispose() {
82 | this.events.removeAllListeners();
83 | delete this.events;
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/server/core/EquipmentSlot.ts:
--------------------------------------------------------------------------------
1 |
2 | export enum EquipmentSlot {
3 | HEAD = 'head',
4 | BODY = 'body',
5 | LEFT = 'left',
6 | RIGHT = 'right',
7 | FEET = 'feet'
8 | };
9 |
--------------------------------------------------------------------------------
/server/core/Inventory.ts:
--------------------------------------------------------------------------------
1 | import { Schema, type, MapSchema } from "@colyseus/schema";
2 | import { Item } from "../entities/Item";
3 | import { createItem } from "../entities/items/createItem";
4 | import { DBItem } from "../db/Hero";
5 |
6 | interface InventoryOptions {
7 | capacity?: number;
8 | }
9 |
10 | export class Inventory extends Schema {
11 | @type({ map: Item })
12 | slots = new MapSchema- ();
13 |
14 | @type("number")
15 | capacity: number;
16 |
17 | constructor (options: InventoryOptions = {}, items?: any) {
18 | super();
19 |
20 | this.capacity = options.capacity || 4
21 |
22 | if (items) {
23 | this.set(items)
24 | }
25 | }
26 |
27 | clear() {
28 | this.slots.clear();
29 | }
30 |
31 | set (items: Item[] | DBItem[]) {
32 | for (let i=0; i await perform())();
32 |
--------------------------------------------------------------------------------
/server/db/ChatLog.ts:
--------------------------------------------------------------------------------
1 | import { mongoose } from "@colyseus/social";
2 |
3 | /**
4 | * Hero
5 | */
6 | export interface DBChatLog extends mongoose.Document {
7 | name: string,
8 | lvl: number,
9 | progress: number,
10 |
11 | text?: string,
12 | say?: string,
13 | killed?: string,
14 | slain?: string,
15 |
16 | timestamp: number
17 | };
18 |
19 | export const ChatLog = mongoose.model('ChatLog', new mongoose.Schema({
20 | name: String,
21 | lvl: Number,
22 | progress: Number,
23 |
24 | text: String,
25 | say: String,
26 | killed: String,
27 | slain: String,
28 |
29 | timestamp: Number
30 | }, {
31 | versionKey: false
32 | }));
33 |
--------------------------------------------------------------------------------
/server/db/Quest.ts:
--------------------------------------------------------------------------------
1 | import { mongoose } from "@colyseus/social";
2 |
3 | /**
4 | * Hero
5 | */
6 | export interface DBQuest extends mongoose.Document {
7 | name: string,
8 | lvl: number,
9 | progress: number,
10 | text: string,
11 | timestamp: number
12 | };
13 |
14 | export const Quest = mongoose.model('Quest', new mongoose.Schema({
15 | name: String,
16 | lvl: Number,
17 | progress: Number,
18 | text: String,
19 | timestamp: Number
20 | }, {
21 | versionKey: false
22 | }));
23 |
--------------------------------------------------------------------------------
/server/db/Report.ts:
--------------------------------------------------------------------------------
1 | import { mongoose } from "@colyseus/social";
2 |
3 | /**
4 | * Hero
5 | */
6 | export interface DBReport extends mongoose.Document {
7 | userId: string,
8 | heroId: string,
9 | message: number,
10 | stack: number,
11 | debug: any,
12 | timestamp: number
13 | };
14 |
15 | export const Report = mongoose.model('Report', new mongoose.Schema({
16 | userId: String,
17 | heroId: String,
18 | message: String,
19 | stack: String,
20 | debug: mongoose.Schema.Types.Mixed,
21 | timestamp: Number,
22 | }, {
23 | versionKey: false
24 | }));
25 |
--------------------------------------------------------------------------------
/server/db/Season.ts:
--------------------------------------------------------------------------------
1 | import { mongoose } from "@colyseus/social";
2 |
3 | const Schema = mongoose.Schema
4 |
5 | /**
6 | * Hero
7 | */
8 | export interface DBSeason extends mongoose.Document {
9 | seed: string;
10 | until: number;
11 | };
12 |
13 | const SeasonSchema = new Schema({
14 | seed: String,
15 | until: Number
16 | });
17 |
18 | SeasonSchema.index({ until: -1 });
19 |
20 | export const Season = mongoose.model('Season', SeasonSchema);
21 |
--------------------------------------------------------------------------------
/server/entities/Boss.ts:
--------------------------------------------------------------------------------
1 | import { Enemy } from "./Enemy";
2 | import { DBHero } from "../db/Hero";
3 | import { StatsModifiers, Unit } from "./Unit";
4 | import { SlimeBoss } from "./behaviours/bosses/SlimeBoss";
5 |
6 | export class Boss extends Enemy {
7 | thingsToUnlockWhenDead: any[] = [];
8 |
9 | constructor (kind, data: Partial, modifiers: Partial = {}) {
10 | super(kind, data,modifiers);
11 |
12 | this.isBoss = true;
13 |
14 | // FIXME: improve this!
15 | if (kind === 'slime-boss') {
16 | this.addBehaviour(new SlimeBoss());
17 | }
18 | }
19 |
20 | get aiDistance () {
21 | return 4;
22 | }
23 |
24 | onDie () {
25 | // unlock chests and doors!
26 | this.thingsToUnlockWhenDead.forEach((thing) => {
27 | thing.isLocked = false;
28 |
29 | // FIXME: use .unlock instead.
30 | // thing.unlock();
31 | });
32 |
33 | // Announce boss killed only if user haven't progressed to next dungeons yet
34 | const playersWhoKilled = super.onDie().filter(player => (
35 | (player as any).latestProgress <= player.state.progress
36 | ));
37 |
38 | if (playersWhoKilled.length > 0) {
39 | // broadcast killed event for global chat.
40 | this.state.events.emit('event', {
41 | name: playersWhoKilled.map(p => (p as any).name).join(", "),
42 | progress: this.state.progress,
43 | killed: this.kind
44 | });
45 | }
46 |
47 | return playersWhoKilled;
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/server/entities/Enemy.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../shared/helpers";
2 |
3 | // Entities
4 | import { Unit, StatsModifiers } from "./Unit";
5 | import { type } from "@colyseus/schema";
6 | import { DBHero } from "../db/Hero";
7 | import { AttackNearestPlayer } from "./behaviours/AttackNearestPlayer";
8 |
9 | export class Enemy extends Unit {
10 | @type("string") kind: string;
11 | @type("boolean") isBoss?: boolean;
12 |
13 | lastUpdateTime = 0;
14 |
15 | constructor (kind, data: Partial, modifiers: Partial = {}) {
16 | super(undefined, data);
17 | this.type = helpers.ENTITIES.ENEMY
18 |
19 | this.kind = kind;
20 | this.lvl = data.lvl || 1;
21 |
22 | // give boost on provided modifiers.
23 | for (let statName in modifiers) {
24 | this.statsBoostModifiers[statName] = modifiers[statName];
25 | }
26 |
27 | this.addBehaviour(new AttackNearestPlayer());
28 |
29 | this.recalculateStatsModifiers();
30 | }
31 |
32 | onLevelUp () {
33 | super.onLevelUp();
34 |
35 | for (let i = this.pointsToDistribute; i > 0; i--) {
36 | this.attributes[this.primaryAttribute]++;
37 | this.pointsToDistribute--;
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/server/entities/Entity.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from "colyseus";
2 | import { Schema, type } from "@colyseus/schema";
3 | import { DungeonState } from "../rooms/states/DungeonState";
4 | import { Position } from "../core/Position";
5 | import { Behaviour } from "./behaviours/Behaviour";
6 |
7 | export class Entity extends Schema {
8 | @type("string") id: string;
9 | @type("string") type: string;
10 | @type(Position) position = new Position();
11 |
12 | walkable: boolean;
13 | state: DungeonState;
14 |
15 | behaviours?: Behaviour[]
16 |
17 | removed?: boolean;
18 |
19 | constructor (id?: string) {
20 | super();
21 |
22 | this.id = id || generateId();
23 | this.walkable = false;
24 | }
25 |
26 | update(currentTime?: number) {
27 | if (this.behaviours) {
28 | let i: number = this.behaviours.length;
29 | while (i--) {
30 | this.behaviours[i].update(this, this.state, currentTime);
31 | }
32 | }
33 | }
34 |
35 | addBehaviour(behaviour: Behaviour) {
36 | if (!this.behaviours) { this.behaviours = []; }
37 | this.behaviours.push(behaviour);
38 | }
39 |
40 | dispose() {
41 | this.behaviours = null;
42 | this.removed = true;
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/server/entities/Interactive.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "./Entity";
2 | import { Point, DungeonState } from "../rooms/states/DungeonState";
3 | import { type } from "@colyseus/schema";
4 | import { Action } from "../actions/Action";
5 | import { MoveEvent } from "../core/Movement";
6 | import { Unit } from "./Unit";
7 |
8 | export abstract class Interactive extends Entity {
9 | @type(Action) action: Action;
10 | activateOnWalkThrough = false;
11 |
12 | constructor (type, position: Point) {
13 | super()
14 |
15 | this.type = type
16 | this.position.set(position);
17 | }
18 |
19 | abstract interact (moveEvent: MoveEvent, unit: Unit, state: DungeonState);
20 | }
21 |
--------------------------------------------------------------------------------
/server/entities/Tower.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../shared/helpers";
2 |
3 | // Entities
4 | import { Enemy } from "./Enemy";
5 | import { DBHero } from "../db/Hero";
6 | import { StatsModifiers, Unit } from "./Unit";
7 | import { type } from "@colyseus/schema";
8 | import { MoveEvent } from "../core/Movement";
9 |
10 | export class Tower extends Enemy {
11 |
12 | constructor (kind, data: Partial, modifiers: Partial = {}) {
13 | super(undefined, data);
14 | this.type = helpers.ENTITIES.ENEMY;
15 |
16 | this.kind = kind;
17 | this.lvl = data.lvl || 1;
18 |
19 | // give boost on provided modifiers.
20 | for (let statName in modifiers) {
21 | this.statsBoostModifiers[statName] = modifiers[statName];
22 | }
23 |
24 | this.recalculateStatsModifiers();
25 | this.updateDirection();
26 | }
27 |
28 | updateDirection(x?: number, y?: number) {
29 | this.direction = "bottom";
30 | }
31 |
32 | update(currentTime) {
33 | super.update(currentTime);
34 |
35 | if (this.position.pending.length > 0) {
36 | this.clearPendingMovement();
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/server/entities/behaviours/AttackNearestPlayer.ts:
--------------------------------------------------------------------------------
1 | import { Behaviour } from "./Behaviour";
2 | import { DungeonState } from "../../rooms/states/DungeonState";
3 | import { Unit } from "../Unit";
4 | import { distance } from "../../helpers/Math";
5 | import { Player } from "../Player";
6 |
7 | export class AttackNearestPlayer implements Behaviour {
8 | lastUpdateTime: number = 0;
9 | aiUpdateTime = 500;
10 |
11 | update(unit: Unit, state: DungeonState, currentTime: number) {
12 | const timeDiff = currentTime - this.lastUpdateTime;
13 | const aiAllowed = timeDiff > this.aiUpdateTime;
14 |
15 | if (aiAllowed && (!unit.action || !unit.action.isEligible)) {
16 | const closePlayer: Player = state.findClosestPlayer(unit, unit.getAIDistance());
17 |
18 | if (closePlayer) {
19 | unit.state.move(unit, { x: closePlayer.position.y, y: closePlayer.position.x }, true)
20 |
21 | } else if (
22 | !unit.action &&
23 | (
24 | (
25 | unit.position.destiny &&
26 | (
27 | unit.position.destiny.x !== unit.position.initialPosition.x &&
28 | unit.position.destiny.y !== unit.position.initialPosition.y
29 | )
30 | ) || (
31 | unit.position.x !== unit.position.initialPosition.x &&
32 | unit.position.y !== unit.position.initialPosition.y
33 | )
34 | )
35 | ) {
36 | // Move back to initial position!
37 | state.move(unit, { x: unit.position.initialPosition.y, y: unit.position.initialPosition.x }, true);
38 | }
39 |
40 | this.lastUpdateTime = currentTime;
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/server/entities/behaviours/Behaviour.ts:
--------------------------------------------------------------------------------
1 | import { DungeonState } from "../../rooms/states/DungeonState";
2 | import { Entity } from "../Entity";
3 |
4 | export interface Behaviour {
5 | update(unit: Entity, state: DungeonState, currentTime: number);
6 | }
7 |
--------------------------------------------------------------------------------
/server/entities/behaviours/UnitSpawner.ts:
--------------------------------------------------------------------------------
1 | import { Behaviour } from "./Behaviour";
2 | import { DungeonState } from "../../rooms/states/DungeonState";
3 | import { Unit } from "../Unit";
4 | import { Enemy } from "../Enemy";
5 |
6 | export interface UnitSpawnerConfig {
7 | type: string[],
8 | lvl: number,
9 | giveXP?: boolean,
10 | interval?: number,
11 | surrounding?: boolean
12 | }
13 |
14 | export class UnitSpawner implements Behaviour {
15 | unitSpawner: UnitSpawnerConfig;
16 | lastUnitSpawnTime = Date.now() - 5800; // give some time for the spawn to appear during camera animation
17 |
18 | constructor (unitSpawner: UnitSpawnerConfig) {
19 | if (!unitSpawner.interval) {
20 | unitSpawner.interval = 6000;
21 | }
22 |
23 | if (unitSpawner.giveXP === undefined) {
24 | unitSpawner.giveXP = true;
25 | }
26 |
27 | if (unitSpawner.surrounding === undefined) {
28 | unitSpawner.surrounding = true;
29 | }
30 |
31 | this.unitSpawner = unitSpawner;
32 | }
33 |
34 | update(unit: Unit, state: DungeonState, currentTime: number) {
35 | if (this.unitSpawner) {
36 | const timeDiff = currentTime - this.lastUnitSpawnTime;
37 | const spawnAllowed = timeDiff > this.unitSpawner.interval
38 |
39 | if (spawnAllowed) {
40 | const checkPositions = (this.unitSpawner.surrounding) ? [
41 | [unit.position.y - 1, unit.position.x],
42 | [unit.position.y + 1, unit.position.x],
43 | [unit.position.y, unit.position.x + 1],
44 | [unit.position.y, unit.position.x - 1],
45 | [unit.position.y - 1, unit.position.x - 1],
46 | [unit.position.y - 1, unit.position.x + 1],
47 | [unit.position.y + 1, unit.position.x - 1],
48 | [unit.position.y + 1, unit.position.x + 1],
49 | ] : [
50 | [unit.position.y, unit.position.x],
51 | ];
52 |
53 | const availablePosition = checkPositions.find((position) => {
54 | return (
55 | !state.gridUtils.getEntityAt(position[0], position[1], Unit) &&
56 | state.pathgrid.isWalkableAt(position[1], position[0])
57 | )
58 | });
59 |
60 | if (availablePosition) {
61 | const enemyType = this.unitSpawner.type[state.roomUtils.realRand.intBetween(0, this.unitSpawner.type.length - 1)];
62 | const enemy = state.roomUtils.createEnemy(enemyType, Enemy, this.unitSpawner.lvl);
63 |
64 | enemy.position.set({
65 | x: availablePosition[1],
66 | y: availablePosition[0]
67 | });
68 |
69 | enemy.walkable = true;
70 |
71 | if (!this.unitSpawner.giveXP) {
72 | enemy.doNotGiveXP = true;
73 | }
74 |
75 | // disable drop for this unit.
76 | enemy.dropOptions = null;
77 |
78 | state.addEntity(enemy);
79 | }
80 |
81 | this.lastUnitSpawnTime = currentTime;
82 | }
83 | }
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/server/entities/behaviours/bosses/SlimeBoss.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { Behaviour } from "../Behaviour";
3 | import { DungeonState } from "../../../rooms/states/DungeonState";
4 | import { Unit } from "../../Unit";
5 | import { StunTile } from "../../interactive/StunTile";
6 |
7 | export class SlimeBoss implements Behaviour {
8 | lastStunTileTime: number = 0;
9 | stunTileInterval = 1000;
10 |
11 | update(unit: Unit, state: DungeonState, currentTime: number) {
12 | const stunTileDiff = currentTime - this.lastStunTileTime;
13 | const stunTileAllowed = stunTileDiff > this.stunTileInterval;
14 |
15 | if (stunTileAllowed) {
16 | if (!state.gridUtils.getEntityAt(unit.position.x, unit.position.y, StunTile)) {
17 | const newStunTile = new StunTile(unit.position, helpers.ENTITIES.STUN_TILE_GOO);
18 | newStunTile.state = state;
19 | state.addEntity(newStunTile);
20 | }
21 | this.lastStunTileTime = currentTime;
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/server/entities/ephemeral/TextEvent.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 |
3 | import { Entity } from "../Entity";
4 | import helpers from "../../../shared/helpers";
5 | import { Position } from "../../core/Position";
6 |
7 | export class TextEvent extends Entity {
8 | @type("string") text: string;
9 | @type("number") ttl: number;
10 |
11 | @type("string") kind: string;
12 | @type("boolean") small: boolean;
13 |
14 | creationTime: number;
15 |
16 | constructor (text, position: Position, kind: string, ttl = 3000, small: boolean = false) {
17 | super();
18 |
19 | this.type = helpers.ENTITIES.TEXT_EVENT;
20 | this.text = text;
21 | this.position = position.clone();
22 | this.ttl = ttl; // ttl on interface
23 |
24 | this.creationTime = Date.now();
25 | this.walkable = true;
26 |
27 | if (kind) { this.kind = kind; }
28 | if (small) { this.small = true; }
29 | }
30 |
31 | update (currentTime) {
32 | if (currentTime > this.creationTime + 3000) {
33 | this.state.removeEntity(this)
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/server/entities/interactive/CheckPoint.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 | import { Interactive } from "../Interactive";
3 | import helpers from "../../../shared/helpers";
4 | import { Action } from "../../actions/Action";
5 |
6 | export class CheckPoint extends Interactive {
7 | @type("boolean") active: boolean = false;
8 |
9 | activationTime: number = Date.now();
10 | fillTimeout: number = 10000; // 10 seconds to fill
11 |
12 | constructor (position) {
13 | super(helpers.ENTITIES.CHECK_POINT, position)
14 | this.walkable = true;
15 | }
16 |
17 | update (currentTime) {
18 | if (currentTime > this.activationTime + this.fillTimeout) {
19 | this.active = false
20 | }
21 | }
22 |
23 | interact (moveEvent, player, state) {
24 | this.action = new Action("activate", true);
25 | setTimeout(() => this.action = null, 2000);
26 |
27 | this.active = true;
28 | this.activationTime = Date.now();
29 |
30 | if (state.progress > 1) {
31 | player.checkPoint = state.progress;
32 | }
33 |
34 | if (player.hero.checkPoints.indexOf(state.progress) === -1) {
35 | player.hero.checkPoints.push(state.progress);
36 | }
37 |
38 | if (state.progress === 1 && player.hero.checkPoints.length === 1) {
39 | setTimeout(() => {
40 | state.createTextEvent(`noCheckpoints`, player.position, 'white', 1000);
41 | }, 1);
42 |
43 | } else {
44 | state.events.emit("send", player, ["checkpoints", player.hero.checkPoints]);
45 | }
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/server/entities/interactive/Chest.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 |
3 | import { Interactive } from "../Interactive";
4 | import helpers from "../../../shared/helpers";
5 | import { Action } from "../../actions/Action";
6 | import { ItemDropOptions } from "../../utils/RoomUtils";
7 |
8 | export class Chest extends Interactive {
9 | @type("string") kind: string;
10 |
11 | walkable = true;
12 | itemDropOptions: ItemDropOptions;
13 |
14 | isLocked: boolean = false;
15 |
16 | actionTimeout: NodeJS.Timeout;
17 |
18 | constructor (position, kind: string, isOpen = false) {
19 | super(helpers.ENTITIES.CHEST, position)
20 |
21 | this.kind = kind;
22 |
23 | if (isOpen) {
24 | this.action = new Action("open", false);
25 | }
26 | }
27 |
28 | unlock() {
29 | this.isLocked = false;
30 | }
31 |
32 | open(state) {
33 | this.action = new Action("open", true);
34 |
35 | if (this.itemDropOptions) {
36 | state.dropItemFrom(this, undefined, this.itemDropOptions);
37 |
38 | } else {
39 | state.dropItemFrom(this);
40 | }
41 | }
42 |
43 | interact (moveEvent, player, state) {
44 | if (this.isLocked) {
45 | state.createTextEvent(`Chest is locked!`, this.position, "white", 100);
46 | return;
47 | }
48 |
49 | if (!this.action) {
50 | moveEvent.cancel();
51 |
52 | if (state.progress > 9 && this.kind === "chest-mimic") {
53 | this.action = new Action("preopen", true);
54 | this.actionTimeout = setTimeout(() => {
55 | if (Math.random() > 0.5) {
56 | const mimic = state.roomUtils.createEnemy("mimic");
57 | mimic.position.set(this.position);
58 | mimic.direction = "bottom";
59 |
60 | mimic.dropOptions = this.itemDropOptions;
61 | mimic.dropOptions.progress++;
62 |
63 | state.removeEntity(this);
64 | state.addEntity(mimic);
65 |
66 | } else {
67 | this.open(state);
68 | }
69 | }, 500);
70 | return;
71 |
72 | } else {
73 | this.open(state);
74 | }
75 |
76 | }
77 | }
78 |
79 | dispose() {
80 | super.dispose();
81 | clearTimeout(this.actionTimeout);
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/server/entities/interactive/Door.ts:
--------------------------------------------------------------------------------
1 | import { Schema, type } from "@colyseus/schema";
2 | import { Interactive } from "../Interactive";
3 | import helpers from "../../../shared/helpers";
4 |
5 | import { Point, RoomType } from "../../rooms/states/DungeonState";
6 |
7 | export enum DoorProgress {
8 | BACK = -1,
9 | FORWARD = -2,
10 | HOME = 1,
11 | LATEST = -3,
12 | }
13 |
14 | interface DoorDestinyOptions {
15 | progress: number;
16 | room?: RoomType;
17 | }
18 |
19 | export class DoorDestiny extends Schema implements DoorDestinyOptions {
20 | // @type("string") identifier: string;
21 | // @type("string") mapkind: string;
22 | // @type("number") difficulty: number;
23 | @type("number") progress: DoorProgress;
24 | @type("string") room: RoomType;
25 |
26 | constructor(data: DoorDestinyOptions) {
27 | super();
28 | // this.identifier = data.identifier;
29 | // this.mapkind = data.mapkind;
30 | // this.difficulty = data.difficulty;
31 | this.progress = data.progress;
32 |
33 | if (data.room) {
34 | this.room = data.room;
35 | }
36 | }
37 | }
38 |
39 | export class Door extends Interactive {
40 | @type(DoorDestiny) destiny: DoorDestiny;
41 | @type("boolean") isLocked: boolean = false;
42 | @type("string") mapkind: string;
43 |
44 | walkable = true;
45 | lockedMessage?: string;
46 |
47 | constructor (position: Point, destiny: DoorDestiny, isLocked: boolean = false) {
48 | super(helpers.ENTITIES.DOOR, position);
49 | this.destiny = destiny;
50 | this.isLocked = isLocked;
51 | }
52 |
53 | unlock() {
54 | this.isLocked = false;
55 | }
56 |
57 | interact (moveEvent, player, state) {
58 | if (!player.isAlive || player.isSwitchingDungeons) {
59 | return;
60 | }
61 |
62 | if (this.isLocked) {
63 | const item = player.getItemByType('key-' + (this.mapkind || state.mapkind));
64 |
65 | // use key to unlock door
66 | if (!item) {
67 | state.createTextEvent(this.lockedMessage || `Door is locked!`, this.position, "white", 500);
68 | return;
69 |
70 | } else if (this.destiny.room === undefined) { // do not unlock special rooms!
71 | this.isLocked = false;
72 | }
73 |
74 | player.useItem('inventory', item.id, true);
75 | }
76 |
77 | player.isSwitchingDungeons = true;
78 | state.events.emit('goto', player, this.destiny, { isPortal: false });
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/server/entities/interactive/Fountain.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 | import { Interactive } from "../Interactive";
3 | import helpers from "../../../shared/helpers";
4 |
5 | export class Fountain extends Interactive {
6 | @type("boolean") active: boolean = (Math.random() > 0.5);
7 |
8 | activationTime: number = Date.now();
9 | fillTimeout: number = 5000; // 5 seconds to fill
10 |
11 | constructor (position) {
12 | super(helpers.ENTITIES.FOUNTAIN, position)
13 | }
14 |
15 | update (currentTime) {
16 | if (currentTime > this.activationTime + this.fillTimeout) {
17 | this.active = true
18 | }
19 | }
20 |
21 | interact (moveEvent, player, state) {
22 | moveEvent.cancel()
23 |
24 | // activate if player needs hp or mana
25 | if (
26 | this.active && (
27 | player.hp.current < player.hp.max ||
28 | player.mp.current < player.mp.max
29 | )
30 | ) {
31 | player.hp.current = player.hp.max
32 | player.mp.current = player.mp.max
33 |
34 | this.active = false
35 | this.activationTime = Date.now()
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/server/entities/interactive/InfernoPortal.ts:
--------------------------------------------------------------------------------
1 | import { Interactive } from "../Interactive";
2 | import helpers from "../../../shared/helpers";
3 | import { UnitSpawnerConfig, UnitSpawner } from "../behaviours/UnitSpawner";
4 | import { Action } from "../../actions/Action";
5 |
6 | export class InfernoPortal extends Interactive {
7 | activationTime: number;
8 | activeTimeout: number = 5000; // 5 seconds to destroy
9 |
10 | // Check to activate portal.
11 | lastUpdateTime: number = 0;
12 | aiUpdateTime = 500;
13 |
14 | unitSpawner: UnitSpawnerConfig;
15 |
16 | constructor (position, unitSpawner: UnitSpawnerConfig) {
17 | super(helpers.ENTITIES.PORTAL_INFERNO, position)
18 |
19 | this.unitSpawner = unitSpawner;
20 | }
21 |
22 | update (currentTime) {
23 | super.update(currentTime);
24 |
25 | if (!this.action) {
26 | const timeDiff = currentTime - this.lastUpdateTime;
27 | const aiAllowed = timeDiff > this.aiUpdateTime;
28 |
29 | if (aiAllowed) {
30 | if (this.state.findClosestPlayer(this, 4)) {
31 | this.activationTime = currentTime;
32 | this.action = new Action("active", true);
33 | this.addBehaviour(new UnitSpawner(this.unitSpawner));
34 | }
35 | this.lastUpdateTime = currentTime;
36 | }
37 |
38 | } else if (currentTime > this.activationTime + this.activeTimeout) {
39 | // remove itself after activeTimeout.
40 | this.state.removeEntity(this);
41 | }
42 | }
43 |
44 | interact (moveEvent, player, state) {
45 | moveEvent.cancel()
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/server/entities/interactive/Jail.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../shared/helpers";
2 | import { type } from "@colyseus/schema";
3 |
4 | import { Point } from "../../rooms/states/DungeonState";
5 | import { Interactive } from "../Interactive";
6 |
7 | export class Jail extends Interactive {
8 | @type("boolean") isLocked: boolean;
9 | @type("number") direction: number;
10 |
11 | constructor (position: Point, direction: number, isLocked: boolean = true) {
12 | super(helpers.ENTITIES.JAIL, position);
13 | this.isLocked = isLocked;
14 | this.direction = direction;
15 | }
16 |
17 | lockTiles(state) {
18 | this.getLockedTiles().forEach((position) => {
19 | state.pathgrid.setWalkableAt(position.x, position.y, false);
20 | });
21 | }
22 |
23 | getLockedTiles() {
24 | if (this.direction === helpers.DIRECTION.SOUTH) {
25 | return [{ x: this.position.x - 1, y: this.position.y }];
26 |
27 | } else if (this.direction === helpers.DIRECTION.NORTH) {
28 | return [{ x: this.position.x + 1, y: this.position.y }];
29 |
30 | } else if (this.direction === helpers.DIRECTION.WEST) {
31 | return [{ x: this.position.x, y: this.position.y + 1 }];
32 |
33 | } else if (this.direction === helpers.DIRECTION.EAST) {
34 | return [{ x: this.position.x, y: this.position.y - 1 }];
35 | }
36 | }
37 |
38 | unlock (state) {
39 | this.isLocked = false;
40 | this.walkable = true;
41 |
42 | this.getLockedTiles().forEach(position => {
43 | state.pathgrid.setWalkableAt(position.x, position.y, true);
44 | });
45 | }
46 |
47 | interact (moveEvent, player, state) {
48 | if (this.isLocked) {
49 | moveEvent.cancel();
50 |
51 | setTimeout(() => {
52 | state.createTextEvent(`It's locked!`, player.position, "white", 100);
53 | }, 1);
54 | return;
55 | }
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/server/entities/interactive/Leaderboard.ts:
--------------------------------------------------------------------------------
1 | import { Interactive } from "../Interactive";
2 | import helpers from "../../../shared/helpers";
3 | import { Hero, DBHero } from "../../db/Hero";
4 |
5 | export class Leaderboard extends Interactive {
6 | lastCacheTime: number = -1;
7 | cacheTime: number = 5 * 60 * 1000; // 5 minutes
8 | cachedLeaderboard: any;
9 |
10 | constructor (position) {
11 | super(helpers.ENTITIES.LEADERBOARD, position)
12 | }
13 |
14 | update (currentTime) {
15 | if (currentTime > this.lastCacheTime + this.cacheTime) {
16 | this.lastCacheTime = currentTime;
17 |
18 | const projection = "_id name lvl primaryAttribute latestProgress klass hair hairColor eye body";
19 | Hero.find({}, projection).
20 | sort({ lvl: -1 }).
21 | limit(12).
22 | then(heroes => {
23 | this.cachedLeaderboard = heroes.map((hero, i) => this.getHeroData(hero, i + 1));
24 | });
25 | }
26 | }
27 |
28 | async interact (moveEvent, player, state) {
29 | moveEvent.cancel();
30 |
31 | if (this.cachedLeaderboard && !player.leaderboardRequested) {
32 | const hasCurrentPlayer = this.cachedLeaderboard.find(hero => hero._id.toString() === player.hero._id.toString());
33 |
34 | // prevent hero from clicking multiple times and querying the database multiple times
35 | player.leaderboardRequested = true;
36 |
37 | if (!hasCurrentPlayer) {
38 | const position = await Hero.find({ lvl: { $gte: player.hero.lvl } }).count();
39 | const leaderboard = [...this.cachedLeaderboard];
40 | leaderboard.pop();
41 |
42 | const currentHero: any = this.getHeroData(player.hero, position);
43 | currentHero.current = true;
44 | leaderboard.push(currentHero);
45 | state.events.emit("send", player, ['leaderboard', leaderboard]);
46 | player.leaderboardRequested = false;
47 |
48 | } else {
49 | player.leaderboardRequested = false;
50 | state.events.emit("send", player, ['leaderboard', this.cachedLeaderboard.map(hero => {
51 | hero.current = (hero._id.toString() === player.hero._id.toString());
52 | return hero;
53 | })]);
54 |
55 | }
56 | }
57 | }
58 |
59 | getHeroData (hero: DBHero, position: number) {
60 | return {
61 | _id: hero._id,
62 | name: hero.name,
63 | lvl: hero.lvl,
64 | position,
65 | primaryAttribute: hero.primaryAttribute,
66 | latestProgress: hero.latestProgress,
67 | props: {
68 | klass: hero.klass,
69 | hair: hero.hair,
70 | hairColor: hero.hairColor,
71 | eye: hero.eye,
72 | body: hero.body
73 | }
74 | }
75 | }
76 |
77 | dispose () {
78 | super.dispose();
79 | delete this.cachedLeaderboard;
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/server/entities/interactive/Lever.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 | import { Interactive } from "../Interactive";
3 | import helpers from "../../../shared/helpers";
4 | import { Point } from "../../rooms/states/DungeonState";
5 |
6 | export class Lever extends Interactive {
7 | @type("boolean") active: boolean = false;
8 |
9 | numPlayersToUnlock: number = 1;
10 | playersInteracted: Set;
11 |
12 | _unlock?: any[];
13 | walkable = true;
14 |
15 | constructor (position: Point) {
16 | super(helpers.ENTITIES.LEVER, position)
17 | }
18 |
19 | set unlock (doors) {
20 | for (let i=0;i unlock.unlock(state));
36 | }
37 |
38 | this._unlock = null;
39 | }
40 |
41 | interact (moveEvent, player, state) {
42 | // skip if already active.
43 | if (this.active) { return; }
44 |
45 | moveEvent.cancel();
46 |
47 | if (!this.playersInteracted) { this.playersInteracted = new Set(); }
48 | this.playersInteracted.add(player.hero._id.toString());
49 |
50 | const playersMissing = this.numPlayersToUnlock - this.playersInteracted.size;
51 |
52 | // activate!
53 | if (playersMissing <= 0) {
54 | this.activate(state);
55 |
56 | } else {
57 | const isPlural = (playersMissing > 1);
58 | state.createTextEvent(`${playersMissing} more player${(isPlural) ? 's' : ''} to unlock`, this.position, "white", 500);
59 | state.events.emit("sound", "pending", player);
60 | }
61 | }
62 |
63 | dispose() {
64 | super.dispose();
65 |
66 | if (this.playersInteracted) {
67 | this.playersInteracted.clear();
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/server/entities/interactive/Portal.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from "@colyseus/social";
2 | import { Schema, type } from "@colyseus/schema";
3 | import helpers from "../../../shared/helpers";
4 |
5 | import { Point } from "../../rooms/states/DungeonState";
6 | import { Door, DoorDestiny } from "./Door";
7 |
8 | export class Portal extends Door {
9 | ownerId: ObjectId;
10 |
11 | creationTime: number;
12 | ttl: number = 3 * 60 * 1000; // 3 minutes
13 |
14 | constructor (position: Point, destiny: DoorDestiny) {
15 | super(position, destiny, false);
16 | this.type = helpers.ENTITIES.PORTAL;
17 | this.creationTime = Date.now();
18 | }
19 |
20 | update(currentTime?: number) {
21 | if (currentTime >= this.creationTime + this.ttl) {
22 | this.state.removeEntity(this);
23 | }
24 | }
25 |
26 | interact (moveEvent, player, state) {
27 | if (
28 | !player.isAlive ||
29 | player.isSwitchingDungeons ||
30 | this.ownerId !== player.hero._id.toString()
31 | ) {
32 | return;
33 | }
34 |
35 | // remove portal when using it.
36 | // only the portal owner can enter this portal.
37 | if (this.ownerId !== player.hero._id.toString()) {
38 | return;
39 | }
40 |
41 | if (state.progress > 1) {
42 | player.shouldSaveCoords = true;
43 |
44 | } else {
45 | state.removeEntity(this);
46 | }
47 |
48 | player.isSwitchingDungeons = true;
49 | state.events.emit('goto', player, this.destiny, { isPortal: true });
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/server/entities/interactive/StunTile.ts:
--------------------------------------------------------------------------------
1 | import { Interactive } from "../Interactive";
2 | import helpers from "../../../shared/helpers";
3 |
4 | export class StunTile extends Interactive {
5 | constructor (position, type: string = helpers.ENTITIES.STUN_TILE) {
6 | super(type, position)
7 |
8 | this.walkable = true;
9 | this.activateOnWalkThrough = true;
10 | }
11 |
12 | interact (moveEvent, player, state) {
13 | player.stun(1600);
14 | state.removeEntity(this);
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/server/entities/interactive/TeleportTile.ts:
--------------------------------------------------------------------------------
1 | import { Interactive } from "../Interactive";
2 | import helpers from "../../../shared/helpers";
3 |
4 | export class TeleportTile extends Interactive {
5 | destiny: TeleportTile;
6 |
7 | constructor (position) {
8 | super(helpers.ENTITIES.TELEPORT_TILE, position)
9 |
10 | this.walkable = true;
11 | this.activateOnWalkThrough = true;
12 | }
13 |
14 | interact (moveEvent, player, state) {
15 | if (this.destiny) {
16 | player.clearPendingMovement();
17 | setTimeout(() => {
18 | player.position.x = this.destiny.position.x;
19 | player.position.y = this.destiny.position.y;
20 | player.clearPendingMovement();
21 | }, player.getMovementSpeed() / 2);
22 |
23 | } else {
24 | state.createTextEvent(`Oops...`, player.position, 'white', 100);
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/server/entities/items/CastableItem.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 | import { Unit } from "../Unit";
3 | import { DungeonState, Point } from "../../rooms/states/DungeonState";
4 | import { ConsumableItem } from "./ConsumableItem";
5 |
6 | export abstract class CastableItem extends ConsumableItem {
7 | @type("boolean") isCastable = true;
8 |
9 | cast (unit: Unit, state: DungeonState, position?: Point): boolean {
10 | this.qty--;
11 | return this.qty === 0;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/entities/items/ConsumableItem.ts:
--------------------------------------------------------------------------------
1 | import { Item } from "../Item";
2 | import { Player } from "../Player";
3 | import { type, MapSchema } from "@colyseus/schema";
4 |
5 | const MAX_ITEM_STACK = 9;
6 |
7 | export class ConsumableItem extends Item {
8 | @type("number") qty: number = 1;
9 |
10 | use(player, state, force: boolean = false) {
11 | this.qty -= 1;
12 |
13 | // return true if ConsumableItem should be removed from Inventory
14 | return this.qty <= 0;
15 | }
16 |
17 | getSameItemToIncrement(slots: MapSchema, combineWith?: ConsumableItem) {
18 | const qtyToAdd = (combineWith && combineWith.qty) || 1;
19 | return Array.from(slots.values()).find((item) => {
20 | if (
21 | item.type === this.type &&
22 | item.qty + qtyToAdd <= MAX_ITEM_STACK
23 | ) {
24 | return item;
25 | }
26 | });
27 | }
28 |
29 | incrementQtyFromSlots(slots, combineWith?: ConsumableItem) {
30 | const itemToIncrement = this.getSameItemToIncrement(slots, combineWith);
31 |
32 | if (itemToIncrement) {
33 | itemToIncrement.qty += this.qty;
34 | return true;
35 | }
36 |
37 | return false;
38 | }
39 |
40 | pick (player: Player, state) {
41 | let success: boolean = this.incrementQtyFromSlots(player.inventory.slots, this);
42 |
43 | if (!success && player.inventory.hasAvailability()) {
44 | success = player.inventory.add(this);
45 | }
46 |
47 | if (success) {
48 | state.events.emit("sound", "pickItem", player);
49 | }
50 |
51 | return success;
52 | }
53 |
54 | getPrice() {
55 | return super.getPrice() * this.qty;
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/server/entities/items/Diamond.ts:
--------------------------------------------------------------------------------
1 | import { Item } from "../Item";
2 | import helpers from "../../../shared/helpers";
3 | import { Player } from "../Player";
4 |
5 | export class Diamond extends Item {
6 | amount: number;
7 |
8 | constructor (amount: number) {
9 | super()
10 | this.type = helpers.ENTITIES.DIAMOND;
11 | this.amount = amount;
12 | }
13 |
14 | // you cannot really use gold...
15 | use(player, state) { return true; }
16 |
17 | pick (player: Player, state) {
18 | //
19 | // FIXME!
20 | // This is necessary to preserve updated position of player.
21 | //
22 | setTimeout(() => {
23 | player.diamond += this.amount;
24 | state.createTextEvent("+" + this.amount, player.position, 'blue', 100);
25 | state.events.emit("sound", "coin", player);
26 | }, 1);
27 |
28 | return true;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/server/entities/items/EquipableItem.ts:
--------------------------------------------------------------------------------
1 | import { Item } from "../Item";
2 | import { DungeonState } from "../../rooms/states/DungeonState";
3 | import { EquipmentSlot } from "../../core/EquipmentSlot";
4 | import { type } from "@colyseus/schema";
5 | import { Unit } from "../Unit";
6 | import { Inventory } from "../../core/Inventory";
7 |
8 | export abstract class EquipableItem extends Item {
9 | @type("string") abstract slotName: EquipmentSlot;
10 | @type("number") progressRequired: number;
11 |
12 | use(player: Unit, state: DungeonState) {
13 | // prevent performing action on already equiped item.
14 | if (player.equipedItems.getItem(this.slotName) === this) {
15 | return false;
16 | }
17 |
18 | // is player allowed to equip?
19 | if (
20 | this.progressRequired &&
21 | this.progressRequired > Math.max((player as any).latestProgress, state.progress)
22 | ) {
23 | return false;
24 | }
25 |
26 | if (player.equipedItems.isSlotAvailable(this.slotName)) {
27 | // just equip!
28 | player.equipedItems.add(this);
29 | return true;
30 |
31 | } else {
32 | // swap with previously equiped item.
33 | const equipedItem = player.equipedItems.getItem(this.slotName);
34 |
35 | let inventoryFrom: Inventory;
36 | if (player.inventory.getItem(this.id)) {
37 | player.inventory.remove(this.id);
38 | inventoryFrom = player.inventory;
39 |
40 | // } else if (player.quickInventory.getItem(this.id)) {
41 | // player.quickInventory.remove(this.id);
42 | // inventoryFrom = player.quickInventory;
43 | }
44 |
45 | if (!inventoryFrom) {
46 | return false;
47 | }
48 |
49 | inventoryFrom.add(equipedItem);
50 | player.equipedItems.add(this, true);
51 | return false;
52 | }
53 |
54 | }
55 |
56 | pick (unit: Unit, state: DungeonState) {
57 | let success = false;
58 |
59 | if (
60 | unit.equipedItems.isSlotAvailable(this.slotName) &&
61 | ( // only auto-equip items if primaryAttribute matches player's primaryAttribute
62 | !(this as any).damageAttribute ||
63 | ((this as any).damageAttribute && unit.primaryAttribute === (this as any).damageAttribute)
64 | ) &&
65 | this.use(unit, state)
66 | ) {
67 | success = true;
68 | }
69 |
70 | if (!success) {
71 | success = super.pick(unit, state);
72 | }
73 |
74 | if (success) {
75 | state.events.emit("sound", "pickItem", unit);
76 | }
77 |
78 | return success
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/entities/items/Gold.ts:
--------------------------------------------------------------------------------
1 | import { Item } from "../Item";
2 | import helpers from "../../../shared/helpers";
3 |
4 | export class Gold extends Item {
5 | amount: number;
6 |
7 | constructor (amount: number) {
8 | super()
9 | this.type = (amount >= 100) ? helpers.ENTITIES.GOLD_BAG : helpers.ENTITIES.GOLD;
10 | this.amount = amount;
11 | }
12 |
13 | // you cannot really use gold...
14 | use(player, state) { return true; }
15 |
16 | pick (player, state) {
17 | //
18 | // FIXME!
19 | // This is necessary to preserve updated position of player.
20 | //
21 | setTimeout(() => {
22 | player.gold += this.amount;
23 | state.createTextEvent("+" + this.amount, player.position, 'yellow', 100);
24 | state.events.emit("sound", "coin", player);
25 | }, 1);
26 |
27 | return true;
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/HpPotion.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { ConsumableItem } from "../ConsumableItem";
3 | import { POTION_1_MODIFIER, POTION_2_MODIFIER, POTION_3_MODIFIER, POTION_4_MODIFIER } from "./Potion";
4 |
5 | const ATTRIBUTE = "hp";
6 |
7 | export class HpPotion extends ConsumableItem {
8 |
9 | constructor (tier: number = 1) {
10 | super();
11 |
12 | switch (tier) {
13 | case 1:
14 | this.type = helpers.ENTITIES.HP_POTION_1;
15 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_1_MODIFIER });
16 | break;
17 | case 2:
18 | this.type = helpers.ENTITIES.HP_POTION_2;
19 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_2_MODIFIER });
20 | break;
21 | case 3:
22 | this.type = helpers.ENTITIES.HP_POTION_3;
23 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_3_MODIFIER });
24 | break;
25 | case 4:
26 | this.type = helpers.ENTITIES.HP_POTION_4;
27 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_4_MODIFIER });
28 | break;
29 | }
30 | }
31 |
32 | use(player, state) {
33 | const shouldBeRemovedFromInventory = super.use(player, state);
34 |
35 | const amount = this.modifiers[0].modifier;
36 | player[ATTRIBUTE].increment(amount);
37 |
38 | state.createTextEvent(`+ ${amount} ${ATTRIBUTE}`, player.position, "red", 100);
39 | state.events.emit("sound", "potion", player);
40 |
41 | return shouldBeRemovedFromInventory;
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/Key.ts:
--------------------------------------------------------------------------------
1 | import { DungeonState, Point } from "../../../rooms/states/DungeonState";
2 | import { ConsumableItem } from "../ConsumableItem";
3 | import { Unit } from "../../Unit";
4 |
5 | export class Key extends ConsumableItem {
6 |
7 | constructor() {
8 | super();
9 | }
10 |
11 | cast(unit: Unit, state: DungeonState, position?: Point) {}
12 | use(player, state, force: boolean = false) {
13 | if (force) {
14 | return super.use(player, state, force);
15 |
16 | } else {
17 | return false;
18 | }
19 | }
20 |
21 | getSellPrice() {
22 | // players can't kill bosses to earn loads of money!
23 | return 10;
24 | }
25 |
26 | getPrice() {
27 | return 10000 * this.qty;
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/MpPotion.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { ConsumableItem } from "../ConsumableItem";
3 |
4 | const ATTRIBUTE = "mp";
5 | const POTION_1_MODIFIER = 15;
6 | const POTION_2_MODIFIER = 30;
7 | const POTION_3_MODIFIER = 50;
8 | const POTION_4_MODIFIER = 80;
9 |
10 | export class MpPotion extends ConsumableItem {
11 |
12 | constructor (tier: number = 1) {
13 | super();
14 |
15 | switch (tier) {
16 | case 1:
17 | this.type = helpers.ENTITIES.MP_POTION_1;
18 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_1_MODIFIER });
19 | break;
20 | case 2:
21 | this.type = helpers.ENTITIES.MP_POTION_2;
22 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_2_MODIFIER });
23 | break;
24 | case 3:
25 | this.type = helpers.ENTITIES.MP_POTION_3;
26 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_3_MODIFIER });
27 | break;
28 | case 4:
29 | this.type = helpers.ENTITIES.MP_POTION_4;
30 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_4_MODIFIER });
31 | break;
32 | }
33 | }
34 |
35 | use(player, state) {
36 | const shouldBeRemovedFromInventory = super.use(player, state);
37 |
38 | const amount = this.modifiers[0].modifier;
39 | player[ATTRIBUTE].increment(amount);
40 |
41 | state.createTextEvent(`+ ${amount} ${ATTRIBUTE}`, player.position, "blue", 100);
42 | state.events.emit("sound", "potion", player);
43 |
44 | return shouldBeRemovedFromInventory;
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/Potion.ts:
--------------------------------------------------------------------------------
1 | export const POTION_1_MODIFIER = 15;
2 | export const POTION_2_MODIFIER = 30;
3 | export const POTION_3_MODIFIER = 50;
4 | export const POTION_4_MODIFIER = 80;
5 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/PotionPoints.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { ConsumableItem } from "../ConsumableItem";
3 |
4 | export class PotionPoints extends ConsumableItem {
5 |
6 | constructor () {
7 | super();
8 |
9 | this.type = helpers.ENTITIES.POINTS_POTION_1;
10 | }
11 |
12 | getPrice() {
13 | return 500;
14 | }
15 |
16 | use(player, state) {
17 | const amount = 1;
18 |
19 | if (
20 | player.attributes.strength > amount &&
21 | player.attributes.agility > amount &&
22 | player.attributes.intelligence > amount
23 | ) {
24 | const shouldRemoveFromInventory = super.use(player, state);
25 |
26 | player.attributes.strength -= amount;
27 | player.attributes.agility -= amount;
28 | player.attributes.intelligence -= amount;
29 | player.pointsToDistribute += amount * 3;
30 | state.events.emit("sound", "potion", player);
31 |
32 | return shouldRemoveFromInventory;
33 |
34 | } else {
35 | state.createTextEvent(`Need ${amount} on each attribute.`, player.position, "white", 200);
36 | return false;
37 | }
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/Scroll.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { Unit } from "../../Unit";
3 | import { DungeonState, Point } from "../../../rooms/states/DungeonState";
4 | import { DoorDestiny, DoorProgress } from "../../interactive/Door";
5 | import { ConsumableItem } from "../ConsumableItem";
6 | import { Portal } from "../../interactive/Portal";
7 |
8 | export class Scroll extends ConsumableItem {
9 |
10 | constructor() {
11 | super();
12 |
13 | this.type = helpers.ENTITIES.SCROLL;
14 | }
15 |
16 | cast(unit: Unit, state: DungeonState, position?: Point) {
17 | }
18 |
19 | // you cannot use this.
20 | use(player, state) {
21 | if (state.progress === 1) {
22 | state.createTextEvent('notAllowedHere', player.position, 'white', 100);
23 | return false;
24 | }
25 |
26 | const availablePosition = this.getAvailablePosition(player, state);
27 |
28 | if (availablePosition) {
29 | const shouldRemoveFromInventory = super.use(player, state);
30 |
31 | //
32 | // remove previous portals from this player.
33 | //
34 | state.entities.forEach((entity, id) => {
35 | if (
36 | entity.type === helpers.ENTITIES.PORTAL &&
37 | entity.ownerId === player.hero._id.toString()
38 | ) {
39 | state.removeEntity(entity);
40 | }
41 | });
42 |
43 | const portal = new Portal({
44 | x: availablePosition[1],
45 | y: availablePosition[0]
46 | }, new DoorDestiny({ progress: DoorProgress.HOME }));
47 |
48 | portal.ownerId = player.hero._id.toString();
49 | portal.state = state;
50 |
51 | state.addEntity(portal);
52 |
53 | return shouldRemoveFromInventory;
54 |
55 | } else {
56 | state.createTextEvent(`I need more space`, player.position, 'white', 100);
57 | return false;
58 | }
59 |
60 |
61 | return false;
62 | }
63 |
64 | getAvailablePosition(player, state) {
65 | const checkPositions = [
66 | [player.position.y - 1, player.position.x],
67 | [player.position.y + 1, player.position.x],
68 | [player.position.y, player.position.x + 1],
69 | [player.position.y, player.position.x - 1],
70 | [player.position.y - 1, player.position.x - 1],
71 | [player.position.y - 1, player.position.x + 1],
72 | [player.position.y + 1, player.position.x - 1],
73 | [player.position.y + 1, player.position.x + 1],
74 | ];
75 |
76 | const availablePosition = checkPositions.find((position) => {
77 | return (
78 | !state.gridUtils.getEntityAt(position[0], position[1], Unit) &&
79 | state.pathgrid.isWalkableAt(position[1], position[0])
80 | )
81 | });
82 |
83 | return availablePosition;
84 | }
85 |
86 | getPrice() {
87 | // TODO: differentiate prices from portals and other magic
88 | return 50;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/ScrollTeleport.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { Unit } from "../../Unit";
3 | import { DungeonState, Point } from "../../../rooms/states/DungeonState";
4 | import { CastableItem } from "../CastableItem";
5 |
6 | export class ScrollTeleport extends CastableItem {
7 |
8 | constructor() {
9 | super();
10 |
11 | this.type = helpers.ENTITIES.SCROLL_MAGIC;
12 | }
13 |
14 | cast(unit: Unit, state: DungeonState, position?: Point) {
15 | if (state.roomUtils.isValidTile(position)) {
16 | unit.position.set(position);
17 | return super.cast(unit, state, position);
18 |
19 | } else {
20 | return false;
21 | }
22 | }
23 |
24 | // you cannot use this.
25 | use(player, state) { return false; }
26 |
27 | getPrice() {
28 | return 400;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/server/entities/items/consumable/XpPotion.ts:
--------------------------------------------------------------------------------
1 | import helpers from "../../../../shared/helpers";
2 | import { ConsumableItem } from "../ConsumableItem";
3 |
4 | const ATTRIBUTE = "xp";
5 | const POTION_1_MODIFIER = 75;
6 | const POTION_2_MODIFIER = 150;
7 | const POTION_3_MODIFIER = 300;
8 | const POTION_4_MODIFIER = 600;
9 |
10 | export class XpPotion extends ConsumableItem {
11 |
12 | constructor (tier: number = 1) {
13 | super();
14 |
15 | this.premium = true;
16 |
17 | switch (tier) {
18 | case 1:
19 | this.type = helpers.ENTITIES.XP_POTION_1;
20 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_1_MODIFIER });
21 | break;
22 | case 2:
23 | this.type = helpers.ENTITIES.XP_POTION_2;
24 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_2_MODIFIER });
25 | break;
26 | case 3:
27 | this.type = helpers.ENTITIES.XP_POTION_3;
28 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_3_MODIFIER });
29 | break;
30 | case 4:
31 | this.type = helpers.ENTITIES.XP_POTION_4;
32 | this.addModifier({ attr: ATTRIBUTE, modifier: POTION_4_MODIFIER });
33 | break;
34 | }
35 | }
36 |
37 | use(player, state) {
38 | const shouldBeRemovedFromInventory = super.use(player, state);
39 |
40 | const amount = this.modifiers[0].modifier;
41 | player[ATTRIBUTE].increment(amount);
42 |
43 | state.createTextEvent(`+ ${amount} ${ATTRIBUTE}`, player.position, "white", 100);
44 | state.events.emit("sound", "potion", player);
45 |
46 | return shouldBeRemovedFromInventory;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/server/entities/items/equipable/ArmorItem.ts:
--------------------------------------------------------------------------------
1 | import { EquipableItem } from "../EquipableItem";
2 | import { EquipmentSlot } from "../../../core/EquipmentSlot";
3 |
4 | export class ArmorItem extends EquipableItem {
5 | slotName = EquipmentSlot.BODY;
6 | }
7 |
--------------------------------------------------------------------------------
/server/entities/items/equipable/BootItem.ts:
--------------------------------------------------------------------------------
1 | import { EquipableItem } from "../EquipableItem";
2 | import { EquipmentSlot } from "../../../core/EquipmentSlot";
3 |
4 | export class BootItem extends EquipableItem {
5 | slotName = EquipmentSlot.FEET;
6 | }
7 |
--------------------------------------------------------------------------------
/server/entities/items/equipable/HelmetItem.ts:
--------------------------------------------------------------------------------
1 | import { EquipableItem } from "../EquipableItem";
2 | import { EquipmentSlot } from "../../../core/EquipmentSlot";
3 |
4 | export class HelmetItem extends EquipableItem {
5 | slotName = EquipmentSlot.HEAD;
6 | }
7 |
--------------------------------------------------------------------------------
/server/entities/items/equipable/ShieldItem.ts:
--------------------------------------------------------------------------------
1 | import { EquipableItem } from "../EquipableItem";
2 | import { EquipmentSlot } from "../../../core/EquipmentSlot";
3 |
4 | export class ShieldItem extends EquipableItem {
5 | slotName = EquipmentSlot.RIGHT;
6 | }
7 |
--------------------------------------------------------------------------------
/server/entities/items/equipable/WeaponItem.ts:
--------------------------------------------------------------------------------
1 | import { type } from "@colyseus/schema";
2 | import { EquipableItem } from "../EquipableItem";
3 | import { EquipmentSlot } from "../../../core/EquipmentSlot";
4 | import { Attribute } from "../../Unit";
5 |
6 | export class WeaponItem extends EquipableItem {
7 | @type("string") damageAttribute: Attribute;
8 | @type("number") manaCost: number;
9 |
10 | slotName = EquipmentSlot.LEFT;
11 | }
12 |
--------------------------------------------------------------------------------
/server/entities/skills/AttackSpeedSkill.ts:
--------------------------------------------------------------------------------
1 | import { Skill } from "./Skill";
2 | import { Unit } from "../Unit";
3 |
4 | export class AttackSpeedSkill extends Skill {
5 | name: string = "attack-speed";
6 | manaCost: number = 15;
7 | duration: number = 2000;
8 |
9 | increasedValue: number;
10 |
11 | activate(unit: Unit) {
12 | super.activate(unit);
13 |
14 | // this.increasedValue = Math.floor(unit.attributes.agility * 0.75);
15 | // unit.statsBoostModifiers.attackSpeed += this.increasedValue;
16 | }
17 |
18 | deactivate(unit: Unit) {
19 | super.deactivate(unit);
20 |
21 | // unit.statsBoostModifiers.attackSpeed -= this.increasedValue;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/entities/skills/MovementSpeedSkill.ts:
--------------------------------------------------------------------------------
1 | import { Skill } from "./Skill";
2 | import { Unit } from "../Unit";
3 |
4 | export class MovementSpeedSkill extends Skill {
5 | name: string = "movement-speed";
6 | manaCost: number = 10;
7 | duration: number = 2500;
8 |
9 | increasedValue: number;
10 |
11 | activate(unit: Unit) {
12 | super.activate(unit);
13 |
14 | // this.increasedValue = Math.floor(unit.attributes.agility * 0.5);
15 | // unit.statsBoostModifiers.movementSpeed += this.increasedValue;
16 | }
17 |
18 | deactivate(unit: Unit) {
19 | super.deactivate(unit);
20 |
21 | // unit.statsBoostModifiers.movementSpeed -= this.increasedValue;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/entities/skills/Skill.ts:
--------------------------------------------------------------------------------
1 | import { Unit } from "../Unit";
2 |
3 | export abstract class Skill {
4 | abstract name: string;
5 | abstract manaCost: number;
6 | abstract duration: number;
7 |
8 | activationTime: number;
9 |
10 | activate(unit: Unit) {
11 | this.activationTime = Date.now();
12 | }
13 |
14 | update(unit, currentTime) {
15 | const isActive = (this.activationTime + this.duration >= currentTime);
16 |
17 | // return is skill is still active or not
18 | return isActive;
19 | }
20 |
21 | deactivate(unit: Unit) {
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/helpers/Math.ts:
--------------------------------------------------------------------------------
1 | import { Point } from "../rooms/states/DungeonState";
2 |
3 | export function distance(pointA: Point, pointB: Point) {
4 | return Math.abs(pointA.x - pointB.x) + Math.abs(pointA.y - pointB.y)
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/server/maps/truehell.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../entities/Entity";
2 | import { defaultSymbols, SymbolsDictonary } from "../utils/MapTemplateParser";
3 |
4 | // Fake entities just to mock the map
5 | class Fence extends Entity {}
6 | class Fountain extends Entity {}
7 | class Chest extends Entity {}
8 | class Boss extends Entity {}
9 | class Monster1 extends Entity {}
10 | class Monster2 extends Entity {}
11 | class Lever extends Entity {constructor(a: any) {super()}}
12 |
13 | export const symbols: SymbolsDictonary = {
14 | ...defaultSymbols,
15 | [`🔷`]: new Lever({ unlock: [] }),
16 | [`⛔`]: new Fence(),
17 | [`🍔`]: new Fountain(),
18 | [`🎁`]: new Chest(),
19 | [`💀`]: new Boss(),
20 | [`👹`]: [new Monster1(), new Monster1()],
21 | [`👺`]: new Monster2(),
22 | };
23 |
24 | export const keys = Object.keys(symbols);
25 |
26 | /*
27 | WEST
28 | NORTH SOUTH
29 | EAST
30 | */
31 | export const mapTemplate = `
32 | ⬜⬜⬜⬜⬜⬜⬜🧱🧱🧱🧱🧱🧱🧱⬜⬜⬜⬜⬜🧱🧱🧱🧱🧱🧱🧱🧱🧱🧱⬜⬜⬜⬜⬜
33 | ⬜⬜⬜⬜⬜⬜⬜🧱⬛⬛⬛⬛⬛🧱⬜⬜⬜⬜🧱🧱⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜⬜⬜⬜
34 | ⬜⬜⬜⬜⬜⬜⬜🧱⬛⬛⬛⬛⬛🧱⬜⬜⬜🧱🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜⬜⬜
35 | ⬜⬜⬜⬜⬜⬜⬜🧱🧱🧱⬛🧱🧱🧱⬜⬜⬜🧱⬛⬛⬛⬛⬛⬛🧱⬛⬛⬛⬛⬛🧱⬜⬜⬜
36 | ⬜🧱🧱🧱🧱🧱🧱⬜⬜🧱⬛🧱⬜⬜⬜⬜🧱🧱⬛⬛⬛⬛⬛⬛🧱⬛⬛⬛⬛⬛🧱🧱⬜⬜
37 | ⬜🧱⬛⬛⬛⬛🧱🧱🧱🧱⬛🧱🧱🧱🧱🧱🧱⬛⬛⬛🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜
38 | ⬜🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱⬛⬛👺⬛🧱⬛⬛⬛⬛⬛⬛⬛🧱⬜
39 | ⬜🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱⬛⬛🧱⬛⬛🧱⬛⬛⬛⬛⬛⬛⬛🧱⬜
40 | ⬜🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱⬛⬛🧱👹⬛🧱⬛⬛⬛⬛⬛⬛⬛🧱⬜
41 | ⬜🧱⬛⬛⬛⬛🧱🧱🧱🧱🧱⬛🧱🧱🧱🧱🧱⬛⬛🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜
42 | ⬜🧱🧱🧱🧱🧱🧱⬜⬜⬜⬜⬛⬜⬜⬜⬜🧱🧱⬛⬛🧱⬛⬛⬛🧱⬛⬛⬛⬛⬛🧱🧱⬜⬜
43 | ⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬜⬜⬜⬜🧱⬛⬛⬛⬛⬛⬛🧱⬛⬛⬛⬛⬛🧱⬜⬜⬜
44 | ⬜⬜⬜⬜⬜⬜⬜⬜⬛⬛⬛⬛⬛⬛⬛⬜⬜🧱🧱⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜⬜⬜
45 | ⬜⬜⬜⬛⬜⬜⬜⬜⬜⬜⬜⬛⬜⬜⬜⬜⬜⬜🧱🧱⬛⬛⬛⬛⬛⬛⬛⬛🧱🧱⬜⬜⬜⬜
46 | ⬜⬜⬜⬛⬜⬜⬜⬜⬜⬜⬛⬛⬛⬜⬜⬜⬜⬜⬜🧱🧱🧱🧱🧱🧱🧱🧱🧱🧱⬜⬜⬜⬜⬜
47 | ⬜⬜⬛⬛⬛⬛⬛⬛⬛⬛⬛⬜⬛⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
48 | ⬜⬜⬜⬜⬜⬜⬛⬜⬜⬜⬜⬜⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛⬜⬜
49 | ⬜⬜⬜⬜⬜⬜⬛⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
50 | ⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
51 | `;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "server",
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "mocha --require ts-node/register test/**Test.ts test/**/**Test.ts",
9 | "start-production": "pm2 start index.js --node-args='--harmony --use-strict'",
10 | "start": "DEBUG=colyseus:matchmaking,mazmorra:* ts-node ./index.ts localhost 3553",
11 | "dev": "DEBUG=colyseus:* nodemon --watch '**/*.ts' --exec 'ts-node' ./index.ts localhost 3553",
12 | "compile": "tsc && cp .env ../server-compiled/server/"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@colyseus/monitor": "^0.14.0",
18 | "@colyseus/social": "^0.11.2",
19 | "clock-timer.js": "^1.1.4",
20 | "colyseus": "^0.14.1",
21 | "cors": "^2.7.1",
22 | "dotenv": "^2.0.0",
23 | "express": "^4.17.1",
24 | "express-basic-auth": "^1.2.0",
25 | "express-jwt": "^5.3.1",
26 | "pathfinding": "^0.4.17",
27 | "random-seed": "^0.3.0"
28 | },
29 | "devDependencies": {
30 | "@types/express": "^4.16.1",
31 | "@types/mocha": "^8.0.1",
32 | "@types/mongoose": "^5.5.32",
33 | "@types/pathfinding": "0.0.2",
34 | "@types/random-seed": "^0.3.3",
35 | "mocha": "^8.1.1",
36 | "nodemon": "^1.18.11",
37 | "ts-node": "^8.2.0",
38 | "typescript": "^3.4.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/server/pdf/ProgressionTable.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from "colyseus";
2 | import * as Config from "../utils/ProgressionConfig";
3 | import gen, { RandomSeed } from "random-seed";
4 |
5 | function mkify(value) {
6 | if (typeof (value) === "boolean") {
7 | return value ? "✅" : "❌";
8 |
9 | } else if (typeof (value) === "number") {
10 | return "`" + value + "`";
11 |
12 | } else if (typeof(value) === "object") {
13 | return `${value.x} x ${value.y}`;
14 |
15 | } else {
16 | return value;
17 | }
18 | }
19 |
20 | const seed = `${generateId()}-${generateId()}`;
21 |
22 | const header = 'Progress | Daylight | Kind | Width | Height | Rooms | One direction? | Checkpoint? | Boss?';
23 | // Min. Room Size | Max. Room Size |
24 | console.log(header);
25 | console.log(`${header.split("|").map(h => '---').join(" | ")}`);
26 |
27 | for (let progress = 1; progress <= 72; progress++) {
28 | const rand = gen.create(seed + progress);
29 |
30 | const config = Config.getMapConfig(progress);
31 | const props = [];
32 |
33 | const width = config.getMapWidth(progress);
34 | const height = config.getMapHeight(progress);
35 | const minRoomSize = config.minRoomSize;
36 | const maxRoomSize = config.maxRoomSize;
37 |
38 | const minRooms = (progress == 1 || progress === Config.MAX_LEVELS) ? 2 : 3;
39 | const maxRooms = (progress == 1 || progress === Config.MAX_LEVELS) ? 2
40 | : Math.min(
41 | Math.floor((width * height) / (maxRoomSize.x * maxRoomSize.y)),
42 | Math.floor(progress / 2)
43 | );
44 | const numRooms = Math.max(minRooms, maxRooms);
45 |
46 | props.push(progress);
47 | props.push(config.daylight);
48 | props.push(config.mapkind);
49 | props.push(width);
50 | props.push(height);
51 | props.push(numRooms);
52 | props.push(config.oneDirection && config.oneDirection(rand, progress) || false);
53 | // props.push(minRoomSize);
54 | // props.push(maxRoomSize);
55 | props.push(Config.isCheckPointMap(progress));
56 | props.push(Config.isBossMap(progress));
57 |
58 | console.log(`${props.map(p => mkify(p)).join(" | ")}`);
59 |
60 | rand.done();
61 | }
62 |
--------------------------------------------------------------------------------
/server/pdf/random-seed.js:
--------------------------------------------------------------------------------
1 | const gen = require('random-seed');
2 |
3 | const rand = gen.create("unique seed");
4 | console.log(rand.random()); // -> 0.6608899281692662
5 | console.log(rand.intBetween(0, 10)); // -> 10
6 | console.log(rand.floatBetween(0, 10)); // -> 0.8044230218003667
7 |
--------------------------------------------------------------------------------
/server/rooms/ChatRoom.ts:
--------------------------------------------------------------------------------
1 | import { Room, Client } from "colyseus";
2 | import { verifyToken } from "@colyseus/social";
3 | import { ChatLog } from "../db/ChatLog";
4 |
5 | export class ChatRoom extends Room {
6 | autoDispose = false;
7 |
8 | lastMessages: any[] = [];
9 |
10 | async onCreate() {
11 | this.setPatchRate(null);
12 |
13 | this.lastMessages = (await ChatLog.find().sort({_id: -1}).limit(30)).reverse();
14 |
15 | // listen to events from other rooms.
16 | this.presence.subscribe("events", (message) => this.broadcast("msg", this.appendMessage(message)));
17 |
18 | this.onMessage("*", (client, command, data) => {
19 | if (command == "msg") {
20 | const msg = {
21 | name: data.name,
22 | lvl: data.lvl,
23 | progress: data.progress,
24 | say: data.text
25 | };
26 |
27 | this.broadcast("msg", this.appendMessage(msg));
28 | };
29 | });
30 |
31 | // every 20 minutes
32 | // this.setSimulationInterval(() => this.sendBetaMessage(), 20 * 1000 * 60);
33 | }
34 |
35 | onAuth (client, options) {
36 | return typeof(verifyToken(options.token)._id) === "string";
37 | }
38 |
39 | onJoin (client: Client) {
40 | this.lastMessages.forEach(msg => client.send("msg", msg));
41 | }
42 |
43 | appendMessage(message) {
44 | if (!message.timestamp) {
45 | message.timestamp = Date.now();
46 | }
47 |
48 | this.lastMessages.push(message);
49 |
50 | // only 20 messages are allowed!
51 | if (this.lastMessages.length > 40) {
52 | this.lastMessages.shift();
53 | }
54 |
55 | ChatLog.create(message);
56 | return message;
57 | }
58 |
59 | // sendBetaMessage () {
60 | // this.broadcast({
61 | // name: "Mazmorra (beta)",
62 | // lvl: 1,
63 | // progress: 1,
64 | // text: "Please join Discord to report issues or suggest improvements. Link in the bottom right.",
65 | // timestamp: Date.now()
66 | // });
67 | // }
68 |
69 | async onLeave (client) {}
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "../server-compiled",
4 | "target": "es6",
5 | "module": "commonjs",
6 | "esModuleInterop": true,
7 | "experimentalDecorators": true,
8 | "allowJs": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/utils/Debug.ts:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | export const debugLog = debug('mazmorra:logs');
4 |
--------------------------------------------------------------------------------
/server/utils/GridUtils.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "../entities/Entity";
2 | import { Unit } from "../entities/Unit";
3 | import { MapSchema } from "@colyseus/schema";
4 |
5 | export class GridUtils {
6 | entities: MapSchema;
7 |
8 | constructor (entities) {
9 | this.entities = entities
10 | }
11 |
12 | getEntityAt (x, y, classReference: any = Entity, meetAttribute?: string) {
13 | // var entities = []
14 | // const values = this.entities.values();
15 |
16 | for (const entity of this.entities.values()) {
17 | if (
18 | entity.position.y == x && entity.position.x == y &&
19 | entity instanceof classReference &&
20 | (!meetAttribute || entity[meetAttribute])
21 | ) {
22 | return entity;
23 | }
24 | }
25 |
26 | return null
27 | }
28 |
29 | getAllEntitiesAt (x, y) {
30 | return Array.from(this.entities.values()).filter(entity => {
31 | return (entity.position.y == x && entity.position.x == y);
32 | });
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const SpritesheetPlugin = require('./webpack/SpritesheetPlugin')
5 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
6 | const AudioSprite = require("audiosprite-loader");
7 | const TerserPlugin = require('terser-webpack-plugin');
8 |
9 | // var stylusLoader = ExtractTextPlugin.extract("style-loader", "css-loader!stylus-loader");
10 | const stylusLoader = ExtractTextPlugin.extract({ fallback: "style-loader", use: ["css-loader", "stylus-loader"] })
11 | const mode = process.env.NODE_ENV || "development";
12 |
13 | console.log("PRODUCTION?", (mode === "production"));
14 |
15 | module.exports = {
16 | mode,
17 | entry: {
18 | bundle: './client/main.js',
19 | dungeon_viewer: './client/dungeon_viewer.js'
20 | },
21 |
22 | output: {
23 | path: path.join(__dirname, 'public'),
24 | // filename: 'bundle.js'
25 | },
26 |
27 | devtool: 'source-map',
28 |
29 | devServer: {
30 | host: '0.0.0.0',
31 | contentBase: './public',
32 | },
33 |
34 | module: {
35 | rules: [
36 | {
37 | test: /.*\.jsx?$/,
38 | exclude: /(node_modules|bower_components)/,
39 | loader: 'babel-loader',
40 | },
41 | { test: /\.styl$/, loader: stylusLoader },
42 | { test: /\.(woff|woff2|eot|ttf|svg|jpg|png)$/, loader: 'file-loader?limit=1024&name=[name].[ext]' },
43 | { test: /\.(wav|ogg|mp3|aif)$/, loader: AudioSprite.loader() }
44 | ],
45 |
46 | },
47 |
48 | plugins: [
49 | new AudioSprite.Plugin(),
50 |
51 | new webpack.ProvidePlugin({
52 | THREE: "three",
53 | Tweener: "tweener",
54 | App: [__dirname + "/client/core/App", "default"],
55 | ResourceManager: [__dirname + '/client/resource/manager', 'default'],
56 | config: __dirname + '/client/config'
57 | }),
58 |
59 | new ExtractTextPlugin("style.css"),
60 |
61 | new SpritesheetPlugin(__dirname + "/public/images/sprites/*.png", {
62 | format: 'json',
63 | path: "public/images",
64 | powerOfTwo: true,
65 | padding: 1
66 | })
67 | ],
68 |
69 | optimization: {
70 | minimize: (mode === "production"),
71 | minimizer: [
72 | new TerserPlugin({
73 | terserOptions: {
74 | compress: {},
75 | mangle: true,
76 | toplevel: true,
77 | output: {
78 | comments: false
79 | }
80 | }
81 | })
82 | ],
83 | },
84 |
85 | resolve: {
86 | extensions: ['.js', '.jsx', '.json']
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/webpack/SpritesheetPlugin.js:
--------------------------------------------------------------------------------
1 | var spritesheet = require('spritesheet-js');
2 |
3 | function SpritesheetPlugin (path, options) {
4 | this.path = path
5 | this.options = options
6 | }
7 |
8 | SpritesheetPlugin.prototype.apply = function (compiler) {
9 | var plugin = this
10 |
11 | console.log("generating spritesheet...")
12 | spritesheet(plugin.path, plugin.options, function (err) {
13 | if (err) throw err;
14 | console.log("spritesheet generated.")
15 | });
16 |
17 |
18 | // compiler.plugin('run', function (compilation, callback) {
19 | // // console.log("emit:", plugin.path, plugin.options);
20 | // console.log("generating spritesheet...", compilation)
21 | //
22 | // spritesheet(plugin.path, plugin.options, function (err) {
23 | // if (err) throw err;
24 | // console.log("spritesheet generated.")
25 | // callback()
26 | // });
27 | //
28 | // })
29 |
30 | // compiler.plugin('done', function() {
31 | // console.log('Hello World!');
32 | // });
33 |
34 | };
35 |
36 | module.exports = SpritesheetPlugin;
37 |
--------------------------------------------------------------------------------