├── .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