Browse Source

Initial Commit

tags/v0.1.2
bigbadwaffle 7 years ago
commit
213ebc4b96
100 changed files with 3757 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +17
    -0
      LICENSE
  3. +0
    -0
     
  4. +81
    -0
      src/client/css/colors.less
  5. +1
    -0
      src/client/css/main.css
  6. +70
    -0
      src/client/css/main.less
  7. +1
    -0
      src/client/css/performance.css
  8. +21
    -0
      src/client/css/performance.less
  9. +33
    -0
      src/client/css/ui.less
  10. BIN
     
  11. BIN
     
  12. BIN
     
  13. BIN
     
  14. BIN
     
  15. BIN
     
  16. BIN
     
  17. BIN
     
  18. BIN
     
  19. BIN
     
  20. BIN
     
  21. BIN
     
  22. BIN
     
  23. BIN
     
  24. BIN
     
  25. BIN
     
  26. BIN
     
  27. BIN
     
  28. BIN
     
  29. BIN
     
  30. BIN
     
  31. BIN
     
  32. BIN
     
  33. BIN
     
  34. BIN
     
  35. BIN
     
  36. BIN
     
  37. BIN
     
  38. BIN
     
  39. BIN
     
  40. BIN
     
  41. BIN
     
  42. BIN
     
  43. BIN
     
  44. BIN
     
  45. BIN
     
  46. BIN
     
  47. BIN
     
  48. BIN
     
  49. BIN
     
  50. BIN
     
  51. BIN
     
  52. BIN
     
  53. BIN
     
  54. BIN
     
  55. BIN
     
  56. BIN
     
  57. BIN
     
  58. BIN
     
  59. BIN
     
  60. BIN
     
  61. BIN
     
  62. BIN
     
  63. BIN
     
  64. +16
    -0
      src/client/index.html
  65. +56
    -0
      src/client/js/app.js
  66. +303
    -0
      src/client/js/canvas.js
  67. +9
    -0
      src/client/js/components/aggro.js
  68. +76
    -0
      src/client/js/components/animation.js
  69. +97
    -0
      src/client/js/components/attackAnimation.js
  70. +66
    -0
      src/client/js/components/bumpAnimation.js
  71. +61
    -0
      src/client/js/components/chatter.js
  72. +64
    -0
      src/client/js/components/chest.js
  73. +47
    -0
      src/client/js/components/components.js
  74. +33
    -0
      src/client/js/components/dialogue.js
  75. +131
    -0
      src/client/js/components/effects.js
  76. +78
    -0
      src/client/js/components/explosion.js
  77. +84
    -0
      src/client/js/components/flash.js
  78. +33
    -0
      src/client/js/components/gatherer.js
  79. +64
    -0
      src/client/js/components/inventory.js
  80. +93
    -0
      src/client/js/components/keyboardMover.js
  81. +133
    -0
      src/client/js/components/light.js
  82. +147
    -0
      src/client/js/components/mouseMover.js
  83. +119
    -0
      src/client/js/components/moveAnimation.js
  84. +45
    -0
      src/client/js/components/particles.js
  85. +100
    -0
      src/client/js/components/pather.js
  86. +69
    -0
      src/client/js/components/player.js
  87. +121
    -0
      src/client/js/components/projectile.js
  88. +9
    -0
      src/client/js/components/prophecies.js
  89. +42
    -0
      src/client/js/components/quests.js
  90. +32
    -0
      src/client/js/components/reputation.js
  91. +33
    -0
      src/client/js/components/resourceNode.js
  92. +56
    -0
      src/client/js/components/serverActions.js
  93. +285
    -0
      src/client/js/components/spellbook.js
  94. +52
    -0
      src/client/js/components/stash.js
  95. +105
    -0
      src/client/js/components/stats.js
  96. +51
    -0
      src/client/js/components/trade.js
  97. +191
    -0
      src/client/js/input.js
  98. +85
    -0
      src/client/js/main.js
  99. +113
    -0
      src/client/js/misc/helpers.js
  100. +432
    -0
      src/client/js/misc/pathfinder.js

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
node_modules
storage.db

+ 17
- 0
LICENSE View File

@@ -0,0 +1,17 @@
*Can't sell the game (not even a modified version of it)
*Can't sell mods/content
*Can modify
*Can distribute
*Can't be held liable
*Must include original
*Must state changes
*Must disclose source
*Must include license
*Must include copyright
*Must include install instructions
*Can't distribute hacked clients that would allow you/someone else to gain control/any kind of benefit from other players (be it in game our out of game)
*If you make stuff available on or through the Game, you give Us the permission to use, change it, and grant others these permissions.
*Things you make available on or through the Game must not be offensive to people, or illegal.
*Anything you make available on our Game must be your own work.
*The game is provided 'as is', updates and upgrades not promised.
*You CAN make money on your own server but you can't charge people for playing

+ 0
- 0
View File


+ 81
- 0
src/client/css/colors.less View File

@@ -0,0 +1,81 @@
@white: rgb(242, 245, 245);
@black: #2d2136;

@brown: rgb(107, 79, 76);

@darkGray: rgb(55, 48, 65);
@gray: rgb(60, 63, 76);
@lightGray: rgb(146, 147, 152);

@yellow: #ffeb38;
@purple: #a24eff;
@orange: #ff6942;

@red: #d43346;
@blue: #3fa7dd;
@green: #80f643;

@greenB: #4ac441;

@blackA: #505360;
@blackB: #3c3f4c;
@blackC: #373041;
@blackD: #312136;

@brownC: #b15a30;
@brownD: #763b3b;

@redA: #ff4252;

@orangeB: #db5538;
@orangeC: #b34b3a;
@orangeD: #953f36;

@yellowB: #faac45;

@blueA: #48edff;
@blueB: #3fa7dd;
@blueC: #3a71ba;
@blueD: #42548d;

@tealB: #51fc9a;
@tealC: #44cb95;

@purpleA: #a24eff;
@purpleB: #7a3ad3;
@purpleC: #533399;
@purpleD: #393268;

@pinkA: #fc66f7;

@grayB: #c0c3cf;
@grayC: #929398;
@grayD: #69696e;

.q0 {
color: @white;
}

.q1 {
color: @blue;
}

.q2 {
color: @yellow;
}

.q3 {
color: @purple;
}

.q4 {
color: @orange;
}

.color-red {
color: @red;
}

.color-green {
color: @green;
}

+ 1
- 0
src/client/css/main.css View File

@@ -0,0 +1 @@
.q0{color:#f2f5f5}.q1{color:#3fa7dd}.q2{color:#ffeb38}.q3{color:#a24eff}.q4{color:#ff6942}.color-red{color:#d43346}.color-green{color:#80f643}body{margin:0;width:100vw;height:100vh;background-color:#2d2136;overflow:hidden}.ui-container{width:100%;height:100%;position:absolute;left:0;top:0;z-index:20;overflow:hidden}@font-face{font-family:bitty;src:url('../fonts/bitty.ttf')}*{box-sizing:border-box;font-family:bitty;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.canvasContainer{position:relative;float:left}.canvasContainer.visible{opacity:1;transition:1s}.canvasContainer canvas{left:0;top:0}.disabled{opacity:.4 !important;pointer-events:none !important}::-webkit-scrollbar{width:16px}::-webkit-scrollbar-track{background-color:#3c3f4c;-webkit-border-radius:0;border-radius:0}::-webkit-scrollbar-thumb{-webkit-border-radius:10px;border-radius:0;background:#929398}

+ 70
- 0
src/client/css/main.less View File

@@ -0,0 +1,70 @@
@import "colors.less";

body {
margin: 0px;
width: 100vw;
height: 100vh;
background-color: @black;
overflow: hidden;
}

.ui-container {
width: 100%;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
z-index: 20;
overflow: hidden;
}

@font-face
{
font-family: bitty;
src: url('../fonts/bitty.ttf');
}

* {
box-sizing: border-box;
font-family: bitty;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

.canvasContainer {
position: relative;
float: left;

&.visible {
opacity: 1;
transition: 1s;
}

canvas {
left: 0px;
top: 0px;
}
}

.disabled {
opacity: 0.4 !important;
pointer-events: none !important;
}

::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-track {
background-color: @gray;
-webkit-border-radius: 0px;
border-radius: 0px;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 0px;
background: @lightGray;
}

+ 1
- 0
src/client/css/performance.css View File

@@ -0,0 +1 @@
.q0{color:#f2f5f5}.q1{color:#3fa7dd}.q2{color:#ffeb38}.q3{color:#a24eff}.q4{color:#ff6942}.color-red{color:#d43346}.color-green{color:#80f643}body{margin:0;width:100vw;height:100vh;background-color:#eee}.ui-container{width:100%;height:100%;position:absolute;left:0;top:0}*{box-sizing:border-box;font-family:bitty}

+ 21
- 0
src/client/css/performance.less View File

@@ -0,0 +1,21 @@
@import "colors.less";

body {
margin: 0px;
width: 100vw;
height: 100vh;
background-color: #eee;
}

.ui-container {
width: 100%;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
}

* {
box-sizing: border-box;
font-family: bitty;
}

+ 33
- 0
src/client/css/ui.less View File

@@ -0,0 +1,33 @@
@import "colors.less";

[class^="ui"] {
.el {
height: 35px;
text-align: center;
padding: 0px 0px 0px 0px;
background-color: transparent;
}

.textbox {
border: none;
outline: none;
font-size: 16px;
}

div.textbox {
padding-top: 6px;
}

.button {
cursor: pointer;
padding-top: 10px;

&:hover {
background-color: lighten(@black, 10%);
}
}

.spacer-h {
width: 100%;
}
}

BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 16
- 0
src/client/index.html View File

@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>dev</title>
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="js/system/addons.js"></script>
<script src="plugins/require.js" data-main="js/app"></script>
<script>if (window.module) module = window.module;</script>
<link rel="stylesheet" href="css/main.css">
<link rel="icon" href="images/favicon.ico">
</head>
<body>
<div class="canvasContainer"></div>
<div class="ui-container"></div>
</body>
</html>

+ 56
- 0
src/client/js/app.js View File

@@ -0,0 +1,56 @@
require = requirejs;

require.config({
baseUrl: '',
waitSeconds: 120,
paths: {
'socket': 'plugins/socket',
'jquery': 'plugins/jquery.min',
'json': 'plugins/json',
'text': 'plugins/text',
'html': 'plugins/html',
'css': 'plugins/css',
'bin': 'plugins/bin',
'audio': 'plugins/audio',
'worker': 'plugins/worker',
'main': 'js/main',
'helpers': 'js/misc/helpers',
'particles': 'plugins/pixi.particles',
'pixi': 'plugins/pixi.min'
},
shim: {
'socket': {
exports: 'io'
},
'jquery': {
exports: '$'
},
'helpers': {
deps: [
'jquery'
]
},
'pixi': {
exports: 'PIXI'
},
'particles': {
deps: [
'pixi'
]
},
'main': {
deps: [
'helpers',
'js/input'
]
}
}
});

require([
'main'
], function(
main
) {
main.init();
});

+ 303
- 0
src/client/js/canvas.js View File

@@ -0,0 +1,303 @@
define([
'js/system/events',
'js/resources',
'js/rendering/tileOpacity',
'js/misc/physics',
'js/spriteBuilder'
], function(
events,
resources,
tileOpacity,
physics,
spriteBuilder
) {
return {
map: null,
mapSprite: null,

//TEST: Remove
visMap: null,

pos: {
x: 0,
y: 0
},
moveTo: null,
moveSpeed: 0,
moveSpeedMax: 1.5,
moveSpeedInc: 0.5,
moveSpeedFlatten: 16,

size: {
x: 0,
y: 0,
},
layers: {},

tileSize: {
w: 32,
h: 32
},

zoneId: null,

player: null,

init: function() {
events.on('onGetMap', this.onGetMap.bind(this));

var canvas = $('.canvasContainer canvas');

canvas.each(function(i, c) {
c = $(c);

if (!c.attr('layer'))
return;

this.layers[c.attr('layer')] = {
canvas: c,
ctx: c[0].getContext('2d')
};

c[0].width = this.size.x = $('body').width();
c[0].height = this.size.y = $('body').height();
c.css('z-index', i + 1);
}.bind(this));

this.layers.particles.ctx.globalCompositeOperation = 'lighter';

$('.canvasContainer')
.width(this.size.x)
.height(this.size.y);
},
onGetMap: function(msg) {
$('.canvasContainer')
.removeClass('visible');

setTimeout(function() {
$('.canvasContainer').addClass('visible');
}, 1000);
if (this.zoneId != null) {
events.emit('onRezone', this.zoneId);
}

this.zoneId = msg.zoneId;

$('.canvasContainer').addClass('visible');

this.map = msg.map;
this.visMap = msg.visMap;

physics.init(msg.collisionMap);

msg.clientObjects.forEach(function(c) {
c.zoneId = this.zoneId;
events.emit('onGetObject', c);
}, this);

this.mapSprite = spriteBuilder.buildSprite(
['tiles', 'walls', 'objects'], [this.map['tiles'], this.map['walls'], this.map['objects']], [0.55, 0.85, 0.85]
);
},
fadeOut: function() {
$('.canvasContainer')
.removeClass('visible');

setTimeout(function() {
$('.canvasContainer').addClass('visible');
}, 1000);
},
setPosition: function(pos, instant) {
if (instant) {
this.fadeOut();

this.moveTo = null;
this.pos = pos;
return;
}

this.moveTo = pos;
},
clear: function(filter) {
for (var l in this.layers) {
var ctx = this.layers[l].ctx;

ctx.save();

ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, this.size.x, this.size.y);

ctx.restore();
}
},
begin: function() {
if (this.moveTo) {
var deltaX = this.moveTo.x - this.pos.x;
var deltaY = this.moveTo.y - this.pos.y;

if ((deltaX != 0) || (deltaY != 0)) {
var moveSpeed = this.moveSpeed;
var distance = Math.max(Math.abs(deltaX), Math.abs(deltaY));
if (moveSpeed > distance)
moveSpeed = distance;

if (distance > this.moveSpeedFlatten) {
var maxSpeed = this.moveSpeedMax;
if (distance > 64) {
maxSpeed += (distance - 64) / 1000;
}
if (this.moveSpeed < maxSpeed)
this.moveSpeed += this.moveSpeedInc;
}

deltaX = (deltaX / distance) * moveSpeed;
deltaY = (deltaY / distance) * moveSpeed;

this.pos.x = this.pos.x + (deltaX);
this.pos.y = this.pos.y + (deltaY);
} else {
this.moveSpeed = 0;
this.moveTo = null;
}
}

for (var l in this.layers) {
var ctx = this.layers[l].ctx;

ctx.save();
ctx.translate(-~~this.pos.x, -~~this.pos.y);
}
},
end: function() {
for (var l in this.layers) {
var ctx = this.layers[l].ctx;

ctx.restore();
}
},

renderMap: function() {
if (!this.player)
return;

this.layers['tiles'].ctx.drawImage(
this.mapSprite,
this.pos.x,
this.pos.y,
this.size.x,
this.size.y,
this.pos.x,
this.pos.y,
this.size.x,
this.size.y
);
},
renderObject: function(obj) {
if (!this.player)
return;

var x = obj.x;
var y = obj.y;

var pX = this.player.x;
var pY = this.player.y;

var dx = Math.abs(x - pX);
var dy = Math.abs(y - pY);

var dxMax = (this.size.x / 64) + 4;
var dyMax = (this.size.y / 64) + 4;

if ((dx > dxMax) || (dy > dyMax))
return;

var sprite = resources.sprites[obj.sheetName].image;
var ctx = this.layers[obj.layerName || obj.sheetName].ctx;

var tileY = ~~(obj.cell / 8);
var tileX = obj.cell - (tileY * 8);

var offsetX = obj.offsetX || 0;
var offsetY = obj.offsetY || 0;

var alpha = 1;
if (obj.alpha != null)
alpha = obj.alpha;

ctx.globalAlpha = alpha;

var size = obj.size || 32;

if (obj.flipX) {
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(
sprite,
tileX * 32,
tileY * 32,
32,
32,
-(x * 32) - (~~(offsetX / 4) * 4),
(y * 32) + (~~(offsetY / 4) * 4),
-32,
32
);
ctx.restore();
}
else if (obj.flipY) {
ctx.save();
ctx.scale(1, -1);
ctx.drawImage(
sprite,
tileX * 32,
tileY * 32,
32,
32,
(x * 32) - (~~(offsetX / 4) * 4),
-(y * 32) + (~~(offsetY / 4) * 4),
32,
-32
);
ctx.restore();
} else
ctx.drawImage(sprite, tileX * size, tileY * size, size, size, (x * 32) + (~~(offsetX / 4) * 4), (y * 32) + (~~(offsetY / 4) * 4), size, size);
},
renderRect: function(layer, x, y, color) {
if (!this.player)
return;

var pX = this.player.x;
var pY = this.player.y;

var dx = Math.abs(x - pX);
var dy = Math.abs(y - pY);

var dxMax = (this.size.x / 64) + 4;
var dyMax = (this.size.y / 64) + 4;

if ((dx > dxMax) || (dy > dyMax))
return;

var ctx = this.layers[layer].ctx;
ctx.fillStyle = color;
ctx.fillRect(x * 32, y * 32, 32, 32);
},
renderText: function(layer, text, x, y) {
var ctx = this.layers[layer].ctx;
ctx.fillStyle = 'white';

ctx.fillText(text, x, y);
},
renderOutlineText: function(layer, text, x, y, centerX) {
var ctx = this.layers[layer].ctx;
ctx.lineWidth = 2;

if (centerX)
x -= (ctx.measureText(text).width / 2);

ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
}
};
});

+ 9
- 0
src/client/js/components/aggro.js View File

@@ -0,0 +1,9 @@
define([
], function(
) {
return {
type: 'aggro'
};
});

+ 76
- 0
src/client/js/components/animation.js View File

@@ -0,0 +1,76 @@
define([
'js/renderer'
], function(
renderer
) {
return {
type: 'animation',

frames: 4,
frameDelay: 4,

sheet: 'attacks',

row: null,
col: null,

loop: 1,
loopCounter: 0,

frame: 0,

frameDelayCd: 0,

oldTexture: null,

init: function(blueprint) {
if (!this.obj.sprite)
return true;
this.oldTexture = this.obj.sprite.texture;

this.frame = 0;
this.frameDelayCd = 0;

for (var p in this.template) {
this[p] = this.template[p];
}

this.frameDelayCd = this.frameDelay;

this.setSprite();
},

setSprite: function() {
renderer.setSprite({
sprite: this.obj.sprite,
cell: (this.row * 8) + this.col + this.frame,
sheetName: this.sheet
});
},

update: function() {
if (this.frameDelayCd > 0)
this.frameDelayCd--;
else {
this.frameDelayCd = this.frameDelay;
this.frame++;
if (this.frame == this.frames) {
this.loopCounter++;
if (this.loopCounter == this.loop) {
this.destroyed = true;
return;
}
else
this.frame = 0;
}
}

this.setSprite();
},

destroy: function() {
this.obj.sprite.texture = this.oldTexture;
}
};
});

+ 97
- 0
src/client/js/components/attackAnimation.js View File

@@ -0,0 +1,97 @@
define([
'js/rendering/effects',
'js/renderer'
], function(
effects,
renderer
) {
var scale = 40;
return {
type: 'attackAnimation',

frames: 4,
frameDelay: 4,
layer: 'attacks',
spriteSheet: 'attacks',

row: null,
col: null,

loop: 1,
loopCounter: 0,

frame: 0,

frameDelayCd: 0,

flipped: false,

sprite: null,

init: function(blueprint) {
effects.register(this);

this.flipped = (Math.random() < 0.5);

this.frameDelayCd = this.frameDelay;

var cell = (this.row * 8) + this.col + this.frame;

this.sprite = renderer.buildObject({
sheetName: this.spriteSheet,
cell: cell,
x: this.obj.x,
y: this.obj.y,
offsetX: this.obj.offsetX,
offsetY: this.obj.offsetY,
flipX: this.flipped
});
},

renderManual: function() {
if (this.frameDelayCd > 0)
this.frameDelayCd--;
else {
this.frameDelayCd = this.frameDelay;
this.frame++;
if (this.frame == this.frames) {
this.loopCounter++;
if (this.loopCounter == this.loop) {
if (this.destroyObject)
this.obj.destroyed = true;
else
this.destroyed = true;
return;
}
else
this.frame = 0;
}
}

this.sprite.x = this.obj.x * scale;
this.sprite.y = this.obj.y * scale;

var cell = (this.row * 8) + this.col + this.frame;

renderer.setSprite({
sheetName: this.spriteSheet,
cell: cell,
flipX: this.flipped,
sprite: this.sprite
});

if (this.flipped)
this.sprite.x += scale;
},

destroyManual: function() {
renderer.destroyObject({
layerName: this.spriteSheet,
sprite: this.sprite
});

effects.unregister(this);
}
};
});

+ 66
- 0
src/client/js/components/bumpAnimation.js View File

@@ -0,0 +1,66 @@
define([
'js/rendering/effects'
], function(
effects
) {
return {
type: 'bumpAnimation',

deltaX: 0,
deltaY: 0,

updateCd: 0,
updateCdMax: 1,

direction: 1,
speed: 2,

duration: 3,
durationCounter: 0,

init: function(blueprint) {
//Only allow one bumper at a time
if (this.obj.components.filter(function(c) { c.type == this.type }) > 1)
return true;
},

update: function() {
var deltaX = this.deltaX;
if (deltaX < 0)
this.obj.flipX = true;
else if (deltaX > 0)
this.obj.flipX = false;

if (this.updateCd > 0) {
this.updateCd--;
}
else {
this.obj.offsetX += (this.deltaX * this.direction * this.speed);
this.obj.offsetY += (this.deltaY * this.direction * this.speed);

this.updateCd = this.updateCdMax;
this.durationCounter++;

if (this.durationCounter == this.duration) {
this.durationCounter = 0;
this.direction *= -1;
if (this.direction == 1)
this.destroyed = true;
else
this.obj.dirty = true;
}
}

this.obj.setSpritePosition();
},

destroy: function() {
this.obj.offsetX = 0;
this.obj.offsetY = 0;

this.obj.setSpritePosition();

effects.unregister(this);
}
};
});

+ 61
- 0
src/client/js/components/chatter.js View File

@@ -0,0 +1,61 @@
define([
'js/renderer'
], function(
renderer
) {
var scale = 40;

return {
type: 'chatter',

cd: 0,
cdMax: 150,

init: function(blueprint) {

},

update: function() {
var chatSprite = this.obj.chatSprite;
if (!chatSprite)
return;

if (this.cd > 0) {
this.cd--;
}
else if (this.cd == 0) {
renderer.destroyObject({
sprite: chatSprite
});
this.obj.chatSprite = null;
}
},

extend: function(serverMsg) {
var msg = serverMsg.msg + '\n\'';

var obj = this.obj;

if (obj.chatSprite) {
renderer.destroyObject({
sprite: obj.chatSprite
});
}

var color = null;
if (msg[0] == '*')
color = 0xffeb38;

obj.chatSprite = renderer.buildText({
layerName: 'effects',
text: msg,
color: color,
x: (obj.x * scale) + (scale / 2),
y: (obj.y * scale) - (scale * 0.8)
});
obj.chatSprite.visible = true;

this.cd = this.cdMax;
}
};
});

+ 64
- 0
src/client/js/components/chest.js View File

@@ -0,0 +1,64 @@
define([

], function(

) {
var colors = [
'f2f5f5',
'3fa7dd',
'a24eff',
'ff6942'
];

var chances = [
0.02,
0.05,
0.1,
0.17
];

return {
type: 'chest',

ownerId: null,

init: function(blueprint) {
if (this.ownerId != -1) {
if (!window.player) {
this.hideSprite();
return;
}

if (this.ownerId != window.player.serverId) {
this.hideSprite();
return;
}
}

var color = colors[this.obj.cell - 50];

this.obj.addComponent('particles', {
chance: chances[this.obj.cell - 50],
blueprint: {
color: {
start: colors[this.obj.cell - 50]
},
alpha: {
start: 0.75,
end: 0.2
},
lifetime: {
start: 1,
end: 4
},
chance: chances[this.obj.cell - 50]
}
});
},

hideSprite: function() {
if (this.obj.sprite)
this.obj.sprite.visible = false;
}
};
});

+ 47
- 0
src/client/js/components/components.js View File

@@ -0,0 +1,47 @@
var components = [
'keyboardMover',
'mouseMover',
'player',
'pather',
'attackAnimation',
'moveAnimation',
'bumpAnimation',
'animation',
'light',
'projectile',
'particles',
'explosion',
'spellbook',
'inventory',
'stats',
'chest',
'effects',
'aggro',
'quests',
'resourceNode',
'gatherer',
'stash',
'flash',
'chatter',
'dialogue',
'trade',
'prophecies',
'reputation',
'serverActions'
].map(function(c) {
return 'js/components/' + c;
});

define(components, function() {
var templates = {};

[].forEach.call(arguments, function(t) {
templates[t.type] = t;
});

return {
getTemplate: function(type) {
return templates[type];
}
};
});

+ 33
- 0
src/client/js/components/dialogue.js View File

@@ -0,0 +1,33 @@
define([
'js/system/client',
'js/system/events'
], function(
client,
events
) {
return {
type: 'dialogue',

init: function() {

},

talk: function(target) {
client.request({
cpn: 'player',
method: 'performAction',
data: {
cpn: 'dialogue',
method: 'talk',
data: {
target: target.id
}
}
});
},

extend: function(blueprint) {
events.emit('onGetTalk', blueprint.state);
}
};
});

+ 131
- 0
src/client/js/components/effects.js View File

@@ -0,0 +1,131 @@
define([
'js/renderer'
], function(
renderer
) {
var scale = 40;

var auras = {
reflectDamage: 0,
stealth: 1,
holyVengeance: 8,
rare: 16
};

return {
type: 'effects',

alpha: 0,
alphaDir: 0.0025,

alphaMax: 0.6,
alphaMin: 0.35,

alphaCutoff: 0.4,

effects: [],

init: function(blueprint) {
var sprite = this.obj.sprite;

this.effects = this.effects
.filter(function(e) {
return (auras[e] != null);
}, this)
.map(function(e) {
return {
name: e,
sprite: renderer.buildObject({
layerName: 'effects',
sheetName: 'auras',
x: this.obj.x - 0.5,
y: this.obj.y - 0.5,
w: scale * 2,
h: scale * 2,
cell: auras[e]
})
}
}, this);
},
extend: function(blueprint) {
if (blueprint.addEffects) {
blueprint.addEffects = blueprint.addEffects
.filter(function(e) {
return (auras[e] != null);
})
.map(function(e) {
return {
name: e,
sprite: renderer.buildObject({
layerName: 'effects',
sheetName: 'auras',
x: this.obj.x - 0.5,
y: this.obj.y - 0.5,
w: scale * 2,
h: scale * 2,
cell: auras[e]
})
}
}, this);

this.effects.push.apply(this.effects, blueprint.addEffects || []);
}
if (blueprint.removeEffects) {
blueprint.removeEffects.forEach(function(r) {
var effect = this.effects.find(function(e) {
return (e.name == r);
});

if (!effect)
return;

renderer.destroyObject({
layerName: 'effects',
sprite: effect.sprite
});

this.effects.spliceFirstWhere(function(e) {
return (e.name == r);
});
}, this);
}
},

update: function() {
this.alpha += this.alphaDir;
if ((this.alphaDir > 0) && (this.alpha >= this.alphaMax)) {
this.alpha = this.alphaMax;
this.alphaDir *= -1;
} else if ((this.alphaDir < 0) && (this.alpha <= this.alphaMin)) {
this.alpha = this.alphaMin;
this.alphaDir *= -1;
}

var x = (this.obj.x - 0.5) * scale;
var y = (this.obj.y - 0.5) * scale;

var useAlpha = this.alpha;
if (useAlpha < this.alphaCutoff)
useAlpha = 0;
else {
useAlpha -= this.alphaCutoff;
useAlpha /= (this.alphaMax - this.alphaCutoff);
}

this.effects.forEach(function(e) {
e.sprite.alpha = useAlpha;
e.sprite.x = x;
e.sprite.y = y;
}, this);
},

destroy: function() {
this.effects.forEach(function(e) {
renderer.destroyObject({
layerName: 'effects',
sprite: e.sprite
});
});
}
};
});

+ 78
- 0
src/client/js/components/explosion.js View File

@@ -0,0 +1,78 @@
define([
'js/rendering/effects'
], function(
effects
) {
return {
type: 'explosion',

count: 10,

blueprint: null,
particles: null,

init: function(blueprint) {
this.blueprint = {
new: true,
blueprint: $.extend(true, {
color: {
start: ['fcfcfc', '929398'],
end: ['505360', '3c3f4c']
},
scale: {
start: {
min: 8,
max: 18
},
end: {
min: 4,
max: 12
}
},
speed: {
start: {
min: 4,
max: 24
},
end: {
min: 2,
max: 18
}
},
particlesPerWave: 14,
particleSpacing: 0,
lifetime: {
min: 1,
max: 3
},
randomColor: true,
randomScale: true,
randomSpeed: true,
frequency: 1
}, blueprint.blueprint, {
spawnType: 'burst',
emitterLifetime: -1,
chance: null,
scale: {
start: {
min: 6,
max: 16
},
end: {
min: 0,
max: 10
}
}
})
};
},

explode: function(blueprint) {
this.particles = this.obj.addComponent('particles', this.blueprint);

this.particles.emitter.update(0.2);
this.particles.emitter.emit = false;
}
};
});

+ 84
- 0
src/client/js/components/flash.js View File

@@ -0,0 +1,84 @@
define([
'js/renderer'
], function(
renderer
) {
return {
type: 'flash',

color: '#48edff',

filter: null,

lum: -1,
lumDir: 0.075,

state: 0,
maxState: 0,

frame: 1,

oldTexture: null,

init: function() {
//Destroy self
if (!this.obj.sprite)
return true;

console.log(this.animation);

this.oldTexture = this.obj.sprite.texture;

renderer.setSprite({
sprite: this.obj.sprite,
cell: 8 + this.frame,
sheetName: 'animChar'
});

this.maxState = ((Math.abs(this.lum) / this.lumDir) + (Math.abs(this.lum) / (this.lumDir))) / 5;
},

getColor: function() {
var hex = String(this.color).replace(/[^0-9a-f]/gi, '');
if (hex.length < 6)
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];

var rgb = '#';
var c = 0;
for (var i = 0; i < 3; i++) {
c = parseInt(hex.substr(i * 2, 2), 16);
c = Math.round(Math.min(Math.max(0, c + (c * this.lum)), 255)).toString(16);
rgb += ('00' + c).substr(c.length);
}

return rgb.replace('#', '0x');
},

update: function() {
this.state++;
if (this.state >= this.maxState) {
this.state = 0;
this.frame++;

if (this.frame <= 5) {
renderer.setSprite({
sprite: this.obj.sprite,
cell: 8 + this.frame,
sheetName: 'animChar'
});
}
}

this.lum += this.lumDir;

if ((this.lumDir > 0) && (this.lum >= 0))
this.lumDir = -Math.abs(this.lumDir);
else if ((this.lumDir <= 0) && (this.lum <= -1))
this.destroyed = true;
},

destroy: function() {
this.obj.sprite.texture = this.oldTexture;
}
};
});

+ 33
- 0
src/client/js/components/gatherer.js View File

@@ -0,0 +1,33 @@
define([
'js/system/client',
'js/system/events'
], function(
client,
events
) {
return {
type: 'gatherer',

init: function() {
this.obj.on('onKeyDown', this.onKeyDown.bind(this));
},

extend: function(msg) {
events.emit('onShowProgress', 'Gathering...', msg.progress);
},

onKeyDown: function(key) {
if (key != 'g')
return;

client.request({
cpn: 'player',
method: 'performAction',
data: {
cpn: 'gatherer',
method: 'gather'
}
});
}
};
});

+ 64
- 0
src/client/js/components/inventory.js View File

@@ -0,0 +1,64 @@
define([
'js/system/events'
], function(
events
) {
return {
type: 'inventory',

items: [],

init: function(blueprint) {
this.items.forEach(function(i) {
if ((i.stats) && (i.stats.hpMax)) {
i.stats.vit = i.stats.hpMax;
delete i.stats.hpMax;
}
});

events.emit('onGetItems', this.items);
},
extend: function(blueprint) {
if (blueprint.destroyItems)
events.emit('onDestroyItems', blueprint.destroyItems);
if (blueprint.getItems) {
var items = this.items;
var newItems = blueprint.getItems || [];
var nLen = newItems.length;

for (var i = 0; i < nLen; i++) {
var nItem = newItems[i];
var nId = nItem.id;

if ((nItem.stats) && (nItem.stats.hpMax)) {
nItem.stats.vit = nItem.stats.hpMax;
delete nItem.stats.hpMax;
}

var findItem = items.find(function(item) {
return (item.id == nId);
});
if (findItem) {
if (!nItem.eq)
delete findItem.eq;
if (nItem.stats)
delete findItem.stats;
if (!nItem.power)
delete findItem.power;
$.extend(true, findItem, nItem);

newItems.splice(i, 1);
i--;
nLen--;
} else
nItem.isNew = true;
}

this.items.push.apply(this.items, blueprint.getItems || []);

events.emit('onGetItems', this.items);
}
}
};
});

+ 93
- 0
src/client/js/components/keyboardMover.js View File

@@ -0,0 +1,93 @@
define([
'js/input',
'js/system/client',
'js/misc/physics'
], function(
input,
client,
physics
) {
return {
type: 'keyboardMover',

path: [],
moveCd: 0,
moveCdMax: 8,
direction: {
x: 0,
y: 0
},

update: function() {
if (input.isKeyDown('esc')) {
client.request({
cpn: 'player',
method: 'queueAction',
data: {
action: 'clearQueue',
priority: true
}
});
}

if (this.moveCd > 0) {
this.moveCd--;
return;
}

this.keyMove();
},

bump: function(dx, dy) {
if (this.obj.pather.path.length > 0)
return;
this.obj.addComponent('bumpAnimation', {
deltaX: dx,
deltaY: dy
});
},

keyMove: function() {
var delta = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};

if ((!delta.x) && (!delta.y))
return;

this.direction.x = delta.x;
this.direction.y = delta.y;

var newX = this.obj.pather.pathPos.x + delta.x;
var newY = this.obj.pather.pathPos.y + delta.y;

if (physics.isTileBlocking(~~newX, ~~newY)) {
this.bump(delta.x, delta.y)
return;
}

this.moveCd = this.moveCdMax;

this.addQueue(newX, newY);
},
addQueue: function(x, y) {
this.obj.dirty = true;

this.obj.pather.add(x, y);

this.obj.pather.pathPos.x = x;
this.obj.pather.pathPos.y = y;

client.request({
cpn: 'player',
method: 'move',
data: {
x: x,
y: y
}
});
}
};
});

+ 133
- 0
src/client/js/components/light.js View File

@@ -0,0 +1,133 @@
define([
'js/canvas',
'js/rendering/effects',
'js/renderer'
], function(
canvas,
effects,
renderer
) {
var scale = 40;

return {
type: 'light',

lightCd: 0,
lightO: {},

emitters: {},

range: 3,

init: function(blueprint) {
this.blueprint = this.blueprint || {};

var x = this.obj.x;
var y = this.obj.y;

var range = this.range;
var halfRange = (range - 1) / 2;

for (var i = 0; i < range; i++) {
for (var j = 0; j < range; j++) {
var n = i + '|' + j;

var maxAlpha = (1 + ((halfRange * 2) - (Math.abs(halfRange - i) + Math.abs(halfRange - j)))) * 0.1;

this.emitters[n] = renderer.buildEmitter({
pos: {
x: ((x + i - halfRange) * scale) + (scale / 2),
y: ((y + j - halfRange) * scale) + (scale / 2)
},
scale: {
start: {
min: 24,
max: 32
},
end: {
min: 12,
max: 22
}
},
color: this.blueprint.color || {
start: ['ffeb38'],
end: ['ffeb38', 'ff6942', 'd43346']
},
alpha: {
start: maxAlpha,
end: 0
},
frequency: 0.9 + (~~(Math.random() * 10) / 10),
blendMode: 'screen',
lifetime: this.blueprint.lifetime || {
min: 1,
max: 4
},
speed: {
start: {
min: 0,
max: 4
},
end: {
min: 0,
max: 2
}
},
randomSpeed: true,
randomColor: true,
randomScale: true
});
}
}
},

update: function() {

},

render: function() {
return;
if (this.lightCd > 0) {
this.lightCd--;
} else {
this.lightCd = 5;
}

ctx = canvas.layers.particles.ctx;
var color = 'rgba(255, 255, 125, $O$)';

var x = this.obj.x;
var y = this.obj.y;

var range = this.range;
var halfRange = (range - 1) / 2;

for (var i = 0; i < range; i++) {
for (var j = 0; j < range; j++) {
var o = range - (Math.abs(halfRange - i) + Math.abs(halfRange - j));
o /= 6;

var n = i + '|' + j;

if (this.lightCd == 0) {
if (Math.random() < 0.5)
this.lightO[n] = (Math.random() * (o * o));
}

o = o * (0.4 + this.lightO[n]);
canvas.renderRect('effects', (x + i - halfRange), (y + j - halfRange), color.replace('$O$', o));
}
}
},

destroy: function() {
var keys = Object.keys(this.emitters);
for (var i = 0; i < keys.length; i++) {
var emitter = this.emitters[keys[i]];
delete this.emitters[keys[i]];

renderer.destroyEmitter(emitter);
}
}
};
});

+ 147
- 0
src/client/js/components/mouseMover.js View File

@@ -0,0 +1,147 @@
define([
'js/system/events',
'js/renderer',
'js/system/client',
'js/input',
'js/objects/objects'
], function(
events,
renderer,
client,
input,
objects
) {
var scale = 40;

return {
type: 'mouseMover',

hoverTile: {
x: 0,
y: 0
},

path: [],

pathColor: 'rgba(255, 255, 255, 0.5)',

mouseDown: false,
opacityCounter: 0,

sprite: null,

init: function() {
this.sprite = renderer.buildObject({
layerName: 'effects',
x: 0,
y: 0,
w: scale,
h: scale,
sheetName: 'ui',
cell: 7
});
},

clearPath: function() {
this.path.forEach(function(p) {
if (p.sprite) {
renderer.destroyObject({
sprite: p.sprite,
layerName: 'effects'
})
}
});

this.path = [];
},

showPath: function(e) {
if ((e.button != null) && (e.button != 0))
return;

var tileX = ~~(e.x / scale);
var tileY = ~~(e.y / scale);

if ((tileX == this.hoverTile.x) && (tileY == this.hoverTile.y))
return;

events.emit('onChangeHoverTile', tileX, tileY);

this.hoverTile.x = ~~(e.x / scale);
this.hoverTile.y = ~~(e.y / scale);

this.sprite.x = (this.hoverTile.x * scale);
this.sprite.y = (this.hoverTile.y * scale);

return;

if ((!e.down) && (!this.mouseDown))
return;

this.mouseDown = true;

var obj = this.obj;

this.clearPath();

//We floor the position in case we're charging (subpixel position)
var path = physics.getPath({
x: ~~this.obj.pather.pathPos.x,
y: ~~this.obj.pather.pathPos.y
}, {
x: this.hoverTile.x,
y: this.hoverTile.y
});

this.path = path.map(function(p) {
return {
x: p.x,
y: p.y,
sprite: renderer.buildRectangle({
layerName: 'effects',
x: (p.x * scale) + 4,
y: (p.y * scale) + 4,
w: 24,
h: 24,
color: '0x48edff',
alpha: 0.2
})
};
});
},
queuePath: function(e) {
this.mouseDown = false;

if ((this.path.length == 0) || (e.down))
return;

client.request({
cpn: 'player',
method: 'moveList',
data: this.path.map(function(p) {
return {
x: p.x,
y: p.y
}
})
});

this.obj.pather.setPath(this.path);
this.path = [];
},

update: function() {
this.opacityCounter++;
if (this.sprite)
this.sprite.alpha = 0.35 + Math.abs(Math.sin(this.opacityCounter / 20) * 0.35);
this.showPath(input.mouse);
},

destroy: function() {
renderer.destroyObject({
sprite: this.sprite,
layerName: 'effects'
});
}
};
});

+ 119
- 0
src/client/js/components/moveAnimation.js View File

@@ -0,0 +1,119 @@
define([

], function(

) {
return {
type: 'moveAnimation',

targetX: 0,
targetY: 0,

x: 0,
y: 0,

ttl: 50,
endTime: 0,

particles: null,

init: function(blueprint) {
this.particles = this.obj.addComponent('particles', {
blueprint: {
scale: {
start: {
min: 6,
max: 16
},
end: {
min: 0,
max: 10
}
},
opacity: {
start: 0.05,
end: 0
},
lifetime: {
min: 1,
max: 2
},
speed: {
start: {
min: 2,
max: 20
},
end: {
min: 0,
max: 8
}
},
color: {
start: 'fcfcfc',
end: 'c0c3cf'
},
randomScale: true,
randomSpeed: true,
chance: 0.4
}
});

this.endTime = +new Date + this.ttl;

var obj = this.obj;
this.x = obj.x;
this.y = obj.y;

if (this.targetX > this.x) {
this.obj.flipX = false;
}
else if (this.targetX < this.x)
this.obj.flipX = true;

this.obj.setSpritePosition();
},

update: function() {
var source = this.obj;
var target = this.target;

var dx = this.targetX - this.x;
var dy = this.targetY - this.y;

var ticksLeft = ~~((this.endTime - (+new Date)) / 16);

if (ticksLeft <= 0) {
this.obj.x = this.targetX;
this.obj.y = this.targetY;

this.obj.setSpritePosition();

this.destroyed = true;
this.particles.destroyed = true;

//Sometimes we just move to a point without exploding
if (target) {
target.addComponent('explosion', {
new: true,
blueprint: {
r: 242,
g: 245,
b: 245
}
}).explode();
}
} else {
dx /= ticksLeft;
dy /= ticksLeft;

this.x += dx;
this.y += dy;

this.obj.x = (~~((this.x * 32) / 8) * 8) / 32;
this.obj.y = (~~((this.y * 32) / 8) * 8) / 32;

this.obj.setSpritePosition();
}
}
};
});

+ 45
- 0
src/client/js/components/particles.js View File

@@ -0,0 +1,45 @@
define([
'js/renderer'
], function(
renderer
) {
var scale = 40;

return {
type: 'particles',
emitter: null,

init: function(blueprint) {
this.blueprint = this.blueprint || {};
this.blueprint.pos = {
x: (this.obj.x * scale) + (scale / 2),
y: (this.obj.y * scale) + (scale / 2)
};

this.emitter = renderer.buildEmitter(this.blueprint);
},

update: function() {
if (this.ttl != null) {
this.ttl--;
if (this.ttl <= 0) {
if (this.destroyObject)
this.obj.destroyed = true;
else
this.destroyed = true;
return;
}
}

if (!this.emitter.emit)
return;

this.emitter.spawnPos.x = (this.obj.x * scale) + (scale / 2);
this.emitter.spawnPos.y = (this.obj.y * scale) + (scale / 2);
},

destroy: function() {
renderer.destroyEmitter(this.emitter);
}
};
});

+ 100
- 0
src/client/js/components/pather.js View File

@@ -0,0 +1,100 @@
define([
'js/renderer',
'js/system/events'
], function(
renderer,
events
) {
var scale = 40;
var scaleMult = 5;

return {
type: 'pather',

path: [],

pathColor: 'rgba(255, 255, 255, 0.5)',

pathPos: {
x: 0,
y: 0
},

lastX: 0,
lastY: 0,

init: function() {
events.on('onDeath', this.onDeath.bind(this));
events.on('onClearQueue', this.onDeath.bind(this));

this.pathPos.x = this.obj.x;
this.pathPos.y = this.obj.y;
},

onDeath: function() {
this.path.forEach(function(p) {
renderer.destroyObject({
layerName: 'effects',
sprite: p.sprite
});
});

this.path = [];
this.pathPos.x = this.obj.x;
this.pathPos.y = this.obj.y;
},

add: function(x, y) {
this.path.push({
x: x,
y: y,
sprite: renderer.buildRectangle({
layerName: 'effects',
alpha: 0.2,
x: (x * scale) + scaleMult,
y: (y * scale) + scaleMult,
w: scale - (scaleMult * 2),
h: scale - (scaleMult * 2)
})
});
},

update: function() {
var x = this.obj.x;
var y = this.obj.y;

if (this.path.length == 0) {
this.pathPos.x = x;
this.pathPos.y = y;
}

if ((x == this.lastX) && (y == this.lastY))
return;

this.lastX = x;
this.lastY = y;

for (var i = 0; i < this.path.length; i++) {
var p = this.path[i];

if ((p.x == x) && (p.y == y)) {
for (var j = 0; j <= i; j++) {
renderer.destroyObject({
layerName: 'effects',
sprite: this.path[j].sprite
});
}
this.path.splice(0, i + 1);
return;
}
}
},

setPath: function(path) {
this.path = this.path.concat(path);

this.pathPos.x = path[path.length - 1].x;
this.pathPos.y = path[path.length - 1].y;
}
};
});

+ 69
- 0
src/client/js/components/player.js View File

@@ -0,0 +1,69 @@
define([
'js/renderer',
'js/system/events'
], function(
renderer,
events
) {
var scale = 40;

return {
type: 'player',

oldPos: {
x: 0,
y: 0
},

init: function() {
this.obj.addComponent('keyboardMover');
this.obj.addComponent('mouseMover');
this.obj.addComponent('serverActions');

this.obj.addComponent('pather');

events.emit('onGetPortrait', this.obj.class);
},

update: function() {
var obj = this.obj;
var oldPos = this.oldPos;

if ((oldPos.x == obj.x) && (oldPos.y == obj.y))
return;

var dx = obj.x - oldPos.x;
var dy = obj.y - oldPos.y;

var instant = false;
if ((dx > 5) || (dy > 5))
instant = true;
if (dx != 0)
dx = dx / Math.abs(dx);
if (dy != 0)
dy = dy / Math.abs(dy);

this.oldPos.x = this.obj.x;
this.oldPos.y = this.obj.y;

this.canvasFollow({
x: dx,
y: dy
}, instant);
},

canvasFollow: function(delta, instant) {
var obj = this.obj;
delta = delta || {
x: 0,
y: 0
};

renderer.setPosition({
x: (obj.x - (renderer.width / (scale * 2))) * scale,
y: (obj.y - (renderer.height / (scale * 2))) * scale
}, instant);
},
};
});

+ 121
- 0
src/client/js/components/projectile.js View File

@@ -0,0 +1,121 @@
define([
'js/rendering/effects',
'js/canvas'
], function(
effects,
canvas
) {
var scale = 40;

return {
type: 'projectile',

source: null,
target: null,

row: null,
col: null,

x: 0,
y: 0,

ttl: 50,
endTime: 0,

particles: null,

init: function(blueprint) {
if ((!this.source) || (!this.target)) {
this.obj.destroyed = true;
return;
}

this.endTime = +new Date + this.ttl;

var source = this.source;
this.x = source.x;
this.y = source.y;

if (blueprint.projectileOffset) {
if ((source.sprite) && (source.sprite.scale.x < 0))
blueprint.projectileOffset.x *= -1;

this.x += (blueprint.projectileOffset.x || 0);
this.y += (blueprint.projectileOffset.y || 0);
}

this.obj.x = this.x;
this.obj.y = this.y;

var particlesBlueprint = this.particles ? {
blueprint: this.particles
} : {
blueprint: {
color: {
start: ['7a3ad3', '3fa7dd'],
end: ['3fa7dd', '7a3ad3']
},
scale: {
start: {
min: 2,
max: 14
},
end: {
min: 0,
max: 8
}
},
lifetime: {
min: 1,
max: 3
},
alpha: {
start: 0.7,
end: 0
},
randomScale: true,
randomColor: true,
chance: 0.6
}
};

this.particles = this.obj.addComponent('particles', particlesBlueprint);
this.obj.addComponent('explosion', particlesBlueprint);

effects.register(this);
},

renderManual: function() {
var source = this.obj;
var target = this.target;

var dx = target.x - this.x;
var dy = target.y - this.y;

var ticksLeft = ~~((this.endTime - (+new Date)) / 16);

if (ticksLeft <= 0) {
this.obj.x = target.x;
this.obj.y = target.y;
this.particles.emitter.emit = false;
if (!this.noExplosion)
this.obj.explosion.explode();
this.obj.destroyed = true;
}
else {
dx /= ticksLeft;
dy /= ticksLeft;

this.x += dx;
this.y += dy;

this.obj.x = (~~((this.x * scale) / 4) * 4) / scale;
this.obj.y = (~~((this.y * scale) / 4) * 4) / scale;
}
},

destroy: function() {
effects.unregister(this);
}
};
});

+ 9
- 0
src/client/js/components/prophecies.js View File

@@ -0,0 +1,9 @@
define([
], function(
) {
return {
type: 'prophecies'
};
});

+ 42
- 0
src/client/js/components/quests.js View File

@@ -0,0 +1,42 @@
define([
'js/system/events'
], function(
events
) {
return {
type: 'quests',
quests: [],

init: function() {
this.quests.forEach(function(q) {
events.emit('onObtainQuest', q);
});
},

extend: function(blueprint) {
if (blueprint.updateQuests) {
blueprint.updateQuests.forEach(function(q) {
events.emit('onUpdateQuest', q);
var index = this.quests.firstIndex(function(qq) {
return (qq.id == q.id);
});
this.quests.splice(index, 1, q);
}, this);
}
if (blueprint.completeQuests) {
blueprint.completeQuests.forEach(function(q) {
events.emit('onCompleteQuest', q);
this.quests.spliceWhere(function(qq) {
return (qq.id == q);
});
}, this);
}
if (blueprint.obtainQuests) {
blueprint.obtainQuests.forEach(function(q) {
events.emit('onObtainQuest', q);
this.quests.push(q);
}, this);
}
}
};
});

+ 32
- 0
src/client/js/components/reputation.js View File

@@ -0,0 +1,32 @@
define([
'js/system/events'
], function(
events
) {
return {
type: 'reputation',

list: [],
factions: [],

init: function() {
events.emit('onGetReputations', this.list);
},

extend: function(blueprint) {
if (blueprint.modifyRep) {
blueprint.modifyRep.forEach(function(m) {
var exists = this.list.find(l => (l.id == m.id));
if (!exists)
this.list.push(m);
else
exists.rep = m.rep;
}, this);

delete blueprint.modifyRep;

events.emit('onGetReputations', this.list);
}
}
};
});

+ 33
- 0
src/client/js/components/resourceNode.js View File

@@ -0,0 +1,33 @@
define([
], function(
) {
return {
type: 'resourceNode',

init: function() {
this.obj.addComponent('particles', {
chance: 0.1,
blueprint: {
color: {
start: 'f2f5f5'
},
alpha: {
start: 0.75,
end: 0.2
},
scale: {
start: 6,
end: 2
},
lifetime: {
min: 1,
max: 3
},
chance: 0.025
}
});
}
};
});

+ 56
- 0
src/client/js/components/serverActions.js View File

@@ -0,0 +1,56 @@
define([
'js/system/events',
'js/system/client'
], function(
events,
client
) {
return {
type: 'serverActions',

actions: [],

init: function(blueprint) {
events.on('onKeyUp', this.onKeyUp.bind(this));
},

onKeyUp: function(key) {
this.actions.forEach(function(a) {
if (a.key != key)
return;

client.request({
cpn: 'player',
method: 'performAction',
data: a.action
});
}, this);
},

extend: function(blueprint) {
if (blueprint.addActions) {
blueprint.addActions.forEach(function(a) {
var exists = this.actions.some(function(ta) {
return ((ta.targetId == a.targetId) && (ta.cpn == a.cpn) && (ta.method == a.method));
});
if (exists)
return;

this.actions.push(a);
}, this);

delete blueprint.addActions;
}

if (blueprint.removeActions) {
blueprint.removeActions.forEach(function(a) {
this.actions.spliceWhere(function(ta) {
return ((ta.targetId == a.targetId) && (ta.cpn == a.cpn) && (ta.method == a.method));
});
}, this);

delete blueprint.removeActions;
}
}
};
});

+ 285
- 0
src/client/js/components/spellbook.js View File

@@ -0,0 +1,285 @@
define([
'js/system/client',
'js/renderer',
'js/system/events'
], function(
client,
renderer,
events
) {
var scale = 40;

var objects = null;
require(['js/objects/objects'], function(o) {
objects = o;
});

return {
type: 'spellbook',

hoverTarget: null,

target: null,
selected: null,
reticleState: 0,
reticleCd: 0,
reticleCdMax: 10,

renderRange: null,

reticleSprite: null,
tarpSprite: null,

shiftDown: false,

init: function(blueprint) {
this.targetSprite = renderer.buildObject({
sheetName: 'ui',
layerName: 'effects',
cell: this.reticleState,
});
this.targetSprite.visible = false;

this.reticleSprite = renderer.buildObject({
sheetName: 'ui',
layerName: 'effects',
cell: 8 + this.reticleState,
});
this.reticleSprite.visible = false;

events.emit('onGetSpells', this.spells);

this.reticleCd = this.reticleCdMax;
this.obj.on('onDeath', this.onDeath.bind(this));

this.obj.on('onMobHover', this.onMobHover.bind(this));
this.obj.on('mouseDown', this.onMouseDown.bind(this));

this.obj.on('onKeyDown', this.onKeyDown.bind(this));
this.obj.on('onKeyUp', this.onKeyUp.bind(this));
},

extend: function(blueprint) {
if (blueprint.removeSpells) {
blueprint.removeSpells.forEach(function(spellId) {
this.spells.spliceWhere(function(s) {
return (s.id == spellId);
});
}, this);

events.emit('onGetSpells', this.spells);
}

if (blueprint.getSpells) {
blueprint.getSpells.forEach(function(s) {
var foundIndex = this.spells.firstIndex(fs => fs.id == s.id);
if (foundIndex != -1) {
this.spells.splice(foundIndex, 1, s);
return;
}

this.spells.push(s);
}, this);

events.emit('onGetSpells', this.spells);
}
},

getSpell: function(number) {
var spellNumber = -1;

if (number == 1) {
spellNumber = 0;
} else if (number == 2)
spellNumber = 1;
else if (number == 3)
spellNumber = 2;

if (spellNumber == -1)
return;

var spell = this.spells[spellNumber];
if (!spell)
return null;

return spell;
},

onMobHover: function(target) {
this.hoverTarget = target;
},

onMouseDown: function(e, target) {
this.target = target || this.hoverTarget;

if (this.target) {
this.targetSprite.x = this.target.x * scale;
this.targetSprite.y = this.target.y * scale;

this.targetSprite.visible = true;
} else {
client.request({
cpn: 'player',
method: 'queueAction',
data: {
action: 'spell',
priority: true,
target: null
}
});

this.targetSprite.visible = false;
}

events.emit('onSetTarget', this.target, e);
},

tabTarget: function() {
this.onMouseDown(null, objects.getClosest(window.player.x, window.player.y, 10, this.target));
},

build: function(destroy) {
client.request({
cpn: 'player',
method: 'performAction',
data: {
instanceModule: 'customMap',
method: 'customize',
data: {
tile: 189,
direction: this.obj.keyboardMover.direction,
destroy: destroy
}
},
callback: renderer.onGetMapCustomization.bind(renderer)
});
},

onKeyDown: function(key) {
if (key == 'b') {
this.build();
return;
}
else if (key == 'n') {
this.build(true);
return;
}

if (key == 'shift') {
this.shiftDown = true;
return;
} else if (key == 'tab') {
this.tabTarget();
return;
}

var spell = this.getSpell(key);
if (!spell)
return;

var oldTarget = null;
if (this.shiftDown) {
oldTarget = this.target;
this.target = this.obj;
}

if ((!spell.targetGround) && (!this.target))
return;

var hoverTile = this.obj.mouseMover.hoverTile;
var target = spell.targetGround ? hoverTile : this.target.id;

if (this.shiftDown)
this.target = oldTarget;

client.request({
cpn: 'player',
method: 'queueAction',
data: {
action: 'spell',
priority: true,
spell: key - 1,
auto: spell.auto,
target: target,
self: this.shiftDown
}
});
},

onKeyUp: function(key) {
if (key == 'shift') {
this.shiftDown = false;
return;
}
},

onDeath: function() {
this.target = null;
this.targetSprite.visible = false;
},

update: function() {
if ((this.target) && (this.target.destroyed)) {
this.target = null;
this.targetSprite.visible = false;
}
if ((this.target) && (this.target.nonSelectable)) {
this.target = null;
this.targetSprite.visible = false;
}

if (this.reticleCd > 0)
this.reticleCd--;
else {
this.reticleCd = this.reticleCdMax;
this.reticleState++;
if (this.reticleState == 4)
this.reticleState = 0;
}

if (!this.target)
return;

renderer.setSprite({
sprite: this.targetSprite,
cell: this.reticleState,
sheetName: 'ui'
});

this.targetSprite.x = this.target.x * scale;
this.targetSprite.y = this.target.y * scale;
},

destroy: function() {
if (this.targetSprite) {
renderer.destroyObject({
layerName: 'effects',
sprite: this.targetSprite
});
}
},

render: function() {
if (this.reticleCd > 0)
this.reticleCd--;
else {
this.reticleCd = this.reticleCdMax;
this.reticleState++;
if (this.reticleState == 4)
this.reticleState = 0;
}

if (!this.target)
return;

renderer.setSprite({
sprite: this.targetSprite,
cell: this.reticleState,
sheetName: 'ui'
});

this.targetSprite.x = this.target.x * scale;
this.targetSprite.y = this.target.y * scale;
}
};
});

+ 52
- 0
src/client/js/components/stash.js View File

@@ -0,0 +1,52 @@
define([
'js/system/events'
], function(
events
) {
return {
type: 'stash',

active: false,

items: null,

init: function() {
events.emit('onGetStashItems', this.items);
},

extend: function(blueprint) {
if (blueprint.active != null)
this.active = blueprint.active;

if (blueprint.getItems) {
var items = this.items;
var newItems = blueprint.getItems || [];
var nLen = newItems.length;

for (var i = 0; i < nLen; i++) {
var nItem = newItems[i];
var nId = nItem.id;

var findItem = items.find(function(item) {
return (item.id == nId);
});
if (findItem) {
$.extend(true, findItem, nItem);

newItems.splice(i, 1);
i--;
nLen--;
}
}

this.items.push.apply(this.items, blueprint.getItems || []);

events.emit('onGetStashItems', this.items);
}

if (blueprint.destroyItems) {
events.emit('onDestroyStashItems', blueprint.destroyItems);
}
}
};
});

+ 105
- 0
src/client/js/components/stats.js View File

@@ -0,0 +1,105 @@
define([
'js/system/events',
'js/renderer'
], function(
events,
renderer
) {
var scale = 40;

return {
type: 'stats',

values: null,

init: function(blueprint) {
if (this.obj.self)
events.emit('onGetStats', this.values);

var serverId = this.obj.serverId;
if (serverId != null)
events.emit('onGetPartyStats', serverId, this.values);

var obj = this.obj;

var yOffset = -12;
if (obj.isChampion)
yOffset = -18;

this.hpSprite = renderer.buildRectangle({
layerName: 'effects',
x: 0,
y: 0,
w: 0,
h: 0,
color: 0x802343
});

renderer.buildRectangle({
x: 0,
y: 0,
w: 0,
h: 0,
parent: this.hpSprite,
color: 0xd43346
});

this.updateHpSprite();
},

updateHpSprite: function() {
var obj = this.obj;

var yOffset = -12;
if (obj.isChampion)
yOffset = -18;

var x = obj.x * scale;
var y = (obj.y * scale) + yOffset;

renderer.moveRectangle({
sprite: this.hpSprite,
x: x + 4,
y: y,
w: (scale - 8),
h: 5
});

renderer.moveRectangle({
sprite: this.hpSprite.children[0],
x: x + 4,
y: y,
w: (this.values.hp / this.values.hpMax) * (scale - 8),
h: 5
});

this.hpSprite.visible = (this.values.hp < this.values.hpMax);
},

extend: function(blueprint) {
var bValues = blueprint.values || {};

var values = this.values;

for (var b in bValues) {
values[b] = bValues[b];
}

if (this.obj.self)
events.emit('onGetStats', this.values);

var serverId = this.obj.serverId;
if (serverId != null)
events.emit('onGetPartyStats', serverId, this.values);

this.updateHpSprite();
},

destroy: function() {
renderer.destroyObject({
sprite: this.hpSprite,
layerName: 'effects'
});
}
};
});

+ 51
- 0
src/client/js/components/trade.js View File

@@ -0,0 +1,51 @@
define([
'js/system/events'
], function(
events
) {
return {
type: 'trade',

itemList: null,
action: 'buy',

init: function(blueprint) {

},

extend: function(blueprint) {
var redraw = false;

if (blueprint.buyList) {
this.itemList = blueprint.buyList;
redraw = true;
this.action = 'buy';
if (blueprint.buyList.buyback)
this.action = 'buyback';
delete blueprint.buyList;
}
else if (blueprint.sellList) {
this.itemList = blueprint.sellList;
redraw = true;
this.action = 'sell';
delete blueprint.sellList;
}

if (blueprint.removeItems) {
this.itemList.items.spliceWhere(function(b) {
return (blueprint.removeItems.indexOf(b.id) > -1);
});
redraw = true;
delete blueprint.removeItems;
}

for (var p in blueprint) {
this[p] = blueprint[p];
}

if (redraw)
events.emit('onGetTradeList', this.itemList, this.action);
}
};
});

+ 191
- 0
src/client/js/input.js View File

@@ -0,0 +1,191 @@
define([
'js/system/events',
'js/renderer'
], function(
events,
renderer
) {
return {
axes: {
horizontal: {
negative: ['left', 'a', 'q', 'z'],
positive: ['right', 'd', 'e', 'c']
},
vertical: {
negative: ['up', 'w', 'q', 'e'],
positive: ['down', 's', 'x', 'z', 'c']
}
},

mappings: {
'8': 'backspace',
'9': 'tab',
'13': 'enter',
'16': 'shift',
'27': 'esc',
'37': 'left',
'38': 'up',
'39': 'right',
'40': 'down',
'46': 'del'
},

mouse: {
button: null,
x: 0,
y: 0
},
mouseRaw: null,

keys: {},

enabled: true,

init: function() {
$(window).on('keydown', this.events.keyboard.keyDown.bind(this));
$(window).on('keyup', this.events.keyboard.keyUp.bind(this));
events.on('onSceneMove', this.events.mouse.mouseMove.bind(this));

$('.ui-container')
.on('mousedown', this.events.mouse.mouseDown.bind(this))
.on('mouseup', this.events.mouse.mouseUp.bind(this))
.on('mousemove', this.events.mouse.mouseMove.bind(this));
},

resetKeys: function() {
this.keys = {};
},

getMapping: function(charCode) {
if (charCode >= 97)
return (charCode - 96).toString();

return (
this.mappings[charCode] ||
String.fromCharCode(charCode).toLowerCase()
);

},

isKeyDown: function(key, noConsume) {
var down = this.keys[key];
if (down != null) {
if (noConsume)
return true;
else {
this.keys[key] = 2;
return (down == 1);
}
} else
return false;
},
getAxis: function(name) {
var axis = this.axes[name];
if (!axis)
return 0;

var result = 0;

for (var i = 0; i < axis.negative.length; i++) {
if (this.keys[axis.negative[i]]) {
result--;
break;
}
}

for (var i = 0; i < axis.positive.length; i++) {
if (this.keys[axis.positive[i]]) {
result++;
break;
}
}

return result;
},

events: {
keyboard: {
keyDown: function(e) {
if (!this.enabled)
return;

if (e.target != document.body)
return true;
if ((e.keyCode == 9) || (e.keyCode == 8) || (e.keyCode == 122))
e.preventDefault();

var key = this.getMapping(e.which);

if (this.keys[key] != null)
this.keys[key] = 2;
else {
this.keys[key] = 1;
events.emit('onKeyDown', key);
}

if (key == 'backspace')
return false;
else if (e.key == 'F11')
events.emit('onToggleFullscreen');

},
keyUp: function(e) {
if (!this.enabled)
return;

if (e.target != document.body)
return;

var key = this.getMapping(e.which);

delete this.keys[key];

events.emit('onKeyUp', key);
}
},
mouse: {
mouseDown: function(e) {
var el = $(e.target);
if ((!el.hasClass('ui-container')) || (el.hasClass('blocking')))
return;

var button = e.button;
this.mouse.button = button;
this.mouse.down = true;
this.mouse.event = e;

events.emit('mouseDown', this.mouse);
},
mouseUp: function(e) {
var el = $(e.target);
if ((!el.hasClass('ui-container')) || (el.hasClass('blocking')))
return;

var button = e.button;
this.mouse.button = null;
this.mouse.down = false;

events.emit('mouseUp', this.mouse);
},
mouseMove: function(e) {
if (e)
this.mouseRaw = e;
else
e = this.mouseRaw;

if (!e)
return;

var el = $(e.target);
if ((!el.hasClass('ui-container')) || (el.hasClass('blocking')))
return;

this.mouse.x = e.offsetX + (renderer.pos.x);
this.mouse.y = e.offsetY + (renderer.pos.y);

events.emit('mouseMove', this.mouse);
}
}
}
};
});

+ 85
- 0
src/client/js/main.js View File

@@ -0,0 +1,85 @@
define([
'js/system/client',
'ui/factory',
'js/renderer',
'js/objects/objects',
'js/rendering/effects',
'js/rendering/numbers',
'js/input',
'js/system/events',
'js/resources',
'ui/templates/inventory/inventory',
'ui/templates/equipment/equipment',
'ui/templates/stash/stash',
'ui/templates/hud/hud',
'ui/templates/online/online',
'ui/templates/quests/quests',
'ui/templates/dialogue/dialogue',
'ui/templates/smithing/smithing',
'ui/templates/overlay/overlay',
'ui/templates/tooltips/tooltips',
'ui/templates/reputation/reputation',
'ui/templates/death/death'
], function(
client,
uiFactory,
renderer,
objects,
effects,
numbers,
input,
events,
resources
) {
return {
hasFocus: true,

init: function() {
events.on('onResourcesLoaded', this.start.bind(this));
},
start: function() {
window.onfocus = this.onFocus.bind(this, true);
window.onblur = this.onFocus.bind(this, false);
$(window).on('contextmenu', function(e) {
e.preventDefault();
return false;
});

objects.init();
client.init();
renderer.init();
input.init();

numbers.init();

uiFactory.init();
uiFactory.build('login', 'body');

this.update();
this.render();
},

onFocus: function(hasFocus) {
//Hack: Later we might want to make it not render when out of focus
this.hasFocus = true;

if (!hasFocus)
input.resetKeys();
},

render: function() {
numbers.render();

renderer.render();

requestAnimationFrame(this.render.bind(this));
},
update: function() {
objects.update();
renderer.update();
uiFactory.update();

setTimeout(this.update.bind(this), 16);
}
};
});

+ 113
- 0
src/client/js/misc/helpers.js View File

@@ -0,0 +1,113 @@
Array.prototype.firstIndex = function(callback, thisArg) {
var T = thisArg;
var O = Object(this);
var len = O.length >>> 0;

var k = 0;

while (k < len) {
var kValue;

if (k in O) {
kValue = O[k];

if (callback.call(T, kValue, k, O))
return k;
}
k++;
}

return -1;
};

Array.prototype.spliceWhere = function(callback, thisArg) {
var T = thisArg;
var O = Object(this);
var len = O.length >>> 0;

var k = 0;

while (k < len) {
var kValue;

if (k in O) {
kValue = O[k];

if (callback.call(T, kValue, k, O)) {
O.splice(k, 1);
k--;
}
}
k++;
}
};

Array.prototype.spliceFirstWhere = function(callback, thisArg) {
var T = thisArg;
var O = Object(this);
var len = O.length >>> 0;

var k = 0;

while (k < len) {
var kValue;

if (k in O) {
kValue = O[k];

if (callback.call(T, kValue, k, O)) {
O.splice(k, 1);
return;
}
}
k++;
}
};

window._ = {
create: function() {
var result = {};

[].slice.call(arguments).forEach(function(a) {
$.extend(true, result, a);
});

return result;
},
get2dArray: function(w, h, def) {
def = def || 0;

var result = [];
for (var i = 0; i < w; i++) {
var inner = [];
for (var j = 0; j < h; j++) {
if (def == 'array')
inner.push([]);
else
inner.push(def);
}

result.push(inner);
}

return result;
},
randWeighted: function(weights) {
var sample = [];
weights.forEach(function(w, i) {
for (var j = 0; j < w; j++) {
sample.push(i);
}
});

return sample[~~(Math.random() * sample.length)];
}
};

define([
], function(
) {
return window._;
});

+ 432
- 0
src/client/js/misc/pathfinder.js View File

@@ -0,0 +1,432 @@
// javascript-astar 0.4.1
// http://github.com/bgrins/javascript-astar
// Freely distributable under the MIT License.
// Implements the astar search algorithm in javascript using a Binary Heap.
// Includes Binary Heap (with modifications) from Marijn Haverbeke.
// http://eloquentjavascript.net/appendix2.html
(function(definition) {
/* global module, define */
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = definition();
} else if (typeof define === 'function' && define.amd) {
define([], definition);
} else {
var exports = definition();
window.astar = exports.astar;
window.Graph = exports.Graph;
}
})(function() {

function pathTo(node) {
var curr = node;
var path = [];
while (curr.parent) {
path.unshift(curr);
curr = curr.parent;
}
return path;
}

function getHeap() {
return new BinaryHeap(function(node) {
return node.f;
});
}

var astar = {
/**
* Perform an A* Search on a graph given a start and end node.
* @param {Graph} graph
* @param {GridNode} start
* @param {GridNode} end
* @param {Object} [options]
* @param {bool} [options.closest] Specifies whether to return the
path to the closest node if the target is unreachable.
* @param {Function} [options.heuristic] Heuristic function (see
* astar.heuristics).
*/
search: function(graph, start, end, options) {
start = graph.grid[start.x][start.y] || start;
end = graph.grid[end.x][end.y] || end;

graph.cleanDirty();
options = options || {};
var heuristic = options.heuristic || astar.heuristics.manhattan;
var closest = options.closest || false;
var distance = options.distance;

if (distance)
heuristic = astar.heuristics.manhattanDistance;

var openHeap = getHeap();
var closestNode = start; // set the start node to be the closest if required

start.h = heuristic(start, end, distance);
graph.markDirty(start);

openHeap.push(start);

while (openHeap.size() > 0) {

// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
var currentNode = openHeap.pop();

var onWall = !currentNode.isWall || currentNode.isWall();

if (!onWall) {
if (distance) {
if (currentNode.h == distance)
return pathTo(currentNode);
}
else {
// End case -- result has been found, return the traced path.
if (currentNode === end) {
return pathTo(currentNode);
}
}
}

// Normal case -- move currentNode from open to closed, process each of its neighbors.
currentNode.closed = true;

// Find all neighbors for the current node.
var neighbors = graph.neighbors(currentNode);

for (var i = 0, il = neighbors.length; i < il; ++i) {
var neighbor = neighbors[i];

if (neighbor.closed || neighbor.isWall()) {
// Not a valid node to process, skip to next neighbor.
continue;
}

// The g score is the shortest distance from start to current node.
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
var gScore = currentNode.g + neighbor.getCost(currentNode);
var beenVisited = neighbor.visited;

if (!beenVisited || gScore < neighbor.g) {

// Found an optimal (so far) path to this node. Take score for node to see how good it is.
neighbor.visited = true;
neighbor.parent = currentNode;
neighbor.h = neighbor.h || heuristic(neighbor, end, distance);
neighbor.g = gScore;
neighbor.f = neighbor.g + neighbor.h;
graph.markDirty(neighbor);
if (closest) {
// If the neighbour is closer than the current closestNode or if it's equally close but has
// a cheaper path than the current closest node then it becomes the closest node
if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {
closestNode = neighbor;
}
}

if (!beenVisited) {
// Pushing to heap will put it in proper place based on the 'f' value.
openHeap.push(neighbor);
} else {
// Already seen the node, but since it has been rescored we need to reorder it in the heap
openHeap.rescoreElement(neighbor);
}
}
}
}

if (closest) {
return pathTo(closestNode);
}

// No result was found - empty array signifies failure to find path.
return [];
},
// See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
heuristics: {
manhattan: function(pos0, pos1) {
var d1 = Math.abs(pos1.x - pos0.x);
var d2 = Math.abs(pos1.y - pos0.y);
return Math.max(d1, d2);
},
manhattanDistance: function(pos0, pos1, distance) {
var d1 = Math.abs(pos1.x - pos0.x);
var d2 = Math.abs(pos1.y - pos0.y);
return Math.abs(distance - Math.max(d1, d2)) + 1;
},
diagonal: function(pos0, pos1) {
var D = 1;
var D2 = Math.sqrt(2);
var d1 = Math.abs(pos1.x - pos0.x);
var d2 = Math.abs(pos1.y - pos0.y);
return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
}
},
cleanNode: function(node) {
if (!node)
return;
node.f = 0;
node.g = 0;
node.h = 0;
node.visited = false;
node.closed = false;
node.parent = null;
}
};

/**
* A graph memory structure
* @param {Array} gridIn 2D array of input weights
* @param {Object} [options]
* @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
*/
function Graph(gridIn, options) {
options = options || {};
this.nodes = [];
this.diagonal = !!options.diagonal;
this.grid = [];

for (var x = 0; x < gridIn.length; x++) {
this.grid[x] = [];

for (var y = 0, row = gridIn[x]; y < row.length; y++) {
if (!row[y]) {
node = new GridNode(x, y, row[y] ? 0 : 1);
this.grid[x][y] = node;
this.nodes.push(node);
}
}
}
this.init();
}

Graph.prototype.init = function() {
this.dirtyNodes = [];
for (var i = 0; i < this.nodes.length; i++) {
astar.cleanNode(this.nodes[i]);
}
};

Graph.prototype.cleanDirty = function() {
for (var i = 0; i < this.dirtyNodes.length; i++) {
astar.cleanNode(this.dirtyNodes[i]);
}
this.dirtyNodes = [];
};

Graph.prototype.markDirty = function(node) {
this.dirtyNodes.push(node);
};

Graph.prototype.neighbors = function(node) {
var ret = [];
var x = node.x;
var y = node.y;
var grid = this.grid;

// West
if (grid[x - 1] && grid[x - 1][y]) {
ret.push(grid[x - 1][y]);
}

// East
if (grid[x + 1] && grid[x + 1][y]) {
ret.push(grid[x + 1][y]);
}

// South
if (grid[x] && grid[x][y - 1]) {
ret.push(grid[x][y - 1]);
}

// North
if (grid[x] && grid[x][y + 1]) {
ret.push(grid[x][y + 1]);
}

if (this.diagonal) {
// Southwest
if (grid[x - 1] && grid[x - 1][y - 1]) {
ret.push(grid[x - 1][y - 1]);
}

// Southeast
if (grid[x + 1] && grid[x + 1][y - 1]) {
ret.push(grid[x + 1][y - 1]);
}

// Northwest
if (grid[x - 1] && grid[x - 1][y + 1]) {
ret.push(grid[x - 1][y + 1]);
}

// Northeast
if (grid[x + 1] && grid[x + 1][y + 1]) {
ret.push(grid[x + 1][y + 1]);
}
}

return ret;
};

Graph.prototype.toString = function() {
var graphString = [];
var nodes = this.grid;
for (var x = 0; x < nodes.length; x++) {
var rowDebug = [];
var row = nodes[x];
for (var y = 0; y < row.length; y++) {
rowDebug.push(row[y].weight);
}
graphString.push(rowDebug.join(" "));
}
return graphString.join("\n");
};

function GridNode(x, y, weight) {
this.x = x;
this.y = y;
this.weight = weight;
}

GridNode.prototype.toString = function() {
return "[" + this.x + " " + this.y + "]";
};

GridNode.prototype.getCost = function(fromNeighbor) {
// Take diagonal weight into consideration.
if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) {
return this.weight * 1.41421;
}
return this.weight;
};

GridNode.prototype.isWall = function() {
return this.weight === 0;
};

function BinaryHeap(scoreFunction) {
this.content = [];
this.scoreFunction = scoreFunction;
}

BinaryHeap.prototype = {
push: function(element) {
// Add the new element to the end of the array.
this.content.push(element);

// Allow it to sink down.
this.sinkDown(this.content.length - 1);
},
pop: function() {
// Store the first element so we can return it later.
var result = this.content[0];
// Get the element at the end of the array.
var end = this.content.pop();
// If there are any elements left, put the end element at the
// start, and let it bubble up.
if (this.content.length > 0) {
this.content[0] = end;
this.bubbleUp(0);
}
return result;
},
remove: function(node) {
var i = this.content.indexOf(node);

// When it is found, the process seen in 'pop' is repeated
// to fill up the hole.
var end = this.content.pop();

if (i !== this.content.length - 1) {
this.content[i] = end;

if (this.scoreFunction(end) < this.scoreFunction(node)) {
this.sinkDown(i);
} else {
this.bubbleUp(i);
}
}
},
size: function() {
return this.content.length;
},
rescoreElement: function(node) {
this.sinkDown(this.content.indexOf(node));
},
sinkDown: function(n) {
// Fetch the element that has to be sunk.
var element = this.content[n];

// When at 0, an element can not sink any further.
while (n > 0) {

// Compute the parent element's index, and fetch it.
var parentN = ((n + 1) >> 1) - 1;
var parent = this.content[parentN];
// Swap the elements if the parent is greater.
if (this.scoreFunction(element) < this.scoreFunction(parent)) {
this.content[parentN] = element;
this.content[n] = parent;
// Update 'n' to continue at the new position.
n = parentN;
}
// Found a parent that is less, no need to sink any further.
else {
break;
}
}
},
bubbleUp: function(n) {
// Look up the target element and its score.
var length = this.content.length;
var element = this.content[n];
var elemScore = this.scoreFunction(element);

while (true) {
// Compute the indices of the child elements.
var child2N = (n + 1) << 1;
var child1N = child2N - 1;
// This is used to store the new position of the element, if any.
var swap = null;
var child1Score;
// If the first child exists (is inside the array)...
if (child1N < length) {
// Look it up and compute its score.
var child1 = this.content[child1N];
child1Score = this.scoreFunction(child1);

// If the score is less than our element's, we need to swap.
if (child1Score < elemScore) {
swap = child1N;
}
}

// Do the same checks for the other child.
if (child2N < length) {
var child2 = this.content[child2N];
var child2Score = this.scoreFunction(child2);
if (child2Score < (swap === null ? elemScore : child1Score)) {
swap = child2N;
}
}

// If the element needs to be moved, swap it, and continue.
if (swap !== null) {
this.content[n] = this.content[swap];
this.content[swap] = element;
n = swap;
}
// Otherwise, we are done.
else {
break;
}
}
}
};

return {
astar: astar,
Graph: Graph,
GridNode: GridNode
};

});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save