mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-03 04:02:44 +00:00
Workign on the homebrew
This commit is contained in:
162
client/naturalCrit/combatManager/encounter/encounter.jsx
Normal file
162
client/naturalCrit/combatManager/encounter/encounter.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Store = require('naturalCrit/combat.store.js');
|
||||
|
||||
var MonsterCard = require('./monsterCard/monsterCard.jsx');
|
||||
|
||||
|
||||
|
||||
var attrMod = function(attr){
|
||||
return Math.floor(attr/2) - 5;
|
||||
}
|
||||
|
||||
var Encounter = React.createClass({
|
||||
mixins : [Store.mixin()],
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enemies: this.createEnemies(this.props)
|
||||
};
|
||||
},
|
||||
|
||||
onStoreChange : function(){
|
||||
var players = Store.getplayersText();
|
||||
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
desc : '',
|
||||
reward : '',
|
||||
enemies : [],
|
||||
players : '',
|
||||
unique : {},
|
||||
|
||||
monsterManual : {}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState({
|
||||
enemies : this.createEnemies(nextProps)
|
||||
})
|
||||
},
|
||||
|
||||
createEnemies : function(props){
|
||||
var self = this;
|
||||
return _.indexBy(_.map(props.enemies, function(type, index){
|
||||
return self.createEnemy(props, type, index)
|
||||
}), 'id')
|
||||
},
|
||||
|
||||
createEnemy : function(props, type, index){
|
||||
var stats = props.unique[type] || props.monsterManual[type];
|
||||
if(!stats) return;
|
||||
return _.extend({
|
||||
id : type + index,
|
||||
name : type,
|
||||
currentHP : stats.hp,
|
||||
initiative : _.random(1,20) + attrMod(stats.attr.dex)
|
||||
}, stats);
|
||||
},
|
||||
|
||||
|
||||
updateHP : function(enemyId, newHP){
|
||||
this.state.enemies[enemyId].currentHP = newHP;
|
||||
this.setState({
|
||||
enemies : this.state.enemies
|
||||
});
|
||||
},
|
||||
removeEnemy : function(enemyId){
|
||||
delete this.state.enemies[enemyId];
|
||||
this.setState({
|
||||
enemies : this.state.enemies
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getPlayerObjects : function(){
|
||||
return _.reduce(this.props.players.split('\n'), function(r, line){
|
||||
var parts = line.split(' ');
|
||||
if(parts.length != 2) return r;
|
||||
r.push({
|
||||
name : parts[0],
|
||||
initiative : parts[1] * 1,
|
||||
isPC : true
|
||||
})
|
||||
return r;
|
||||
},[])
|
||||
},
|
||||
|
||||
|
||||
renderEnemies : function(){
|
||||
var self = this;
|
||||
|
||||
var sortedEnemies = _.sortBy(_.union(_.values(this.state.enemies), this.getPlayerObjects()), function(e){
|
||||
if(e && e.initiative) return -e.initiative;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return _.map(sortedEnemies, function(enemy){
|
||||
if(enemy.isPC){
|
||||
return <PlayerCard {...enemy} key={enemy.name} />
|
||||
}
|
||||
|
||||
return <MonsterCard
|
||||
{...enemy}
|
||||
key={enemy.id}
|
||||
updateHP={self.updateHP.bind(self, enemy.id)}
|
||||
remove={self.removeEnemy.bind(self, enemy.id)}
|
||||
/>
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
|
||||
var reward;
|
||||
if(this.props.reward){
|
||||
reward = <div className='reward'>
|
||||
<i className='fa fa-trophy' /> Rewards: {this.props.reward}
|
||||
</div>
|
||||
}
|
||||
|
||||
return(
|
||||
<div className='mainEncounter'>
|
||||
<div className='info'>
|
||||
<h1>{this.props.name}</h1>
|
||||
<p>{this.props.desc}</p>
|
||||
{reward}
|
||||
</div>
|
||||
|
||||
<div className='cardContainer'>
|
||||
{this.renderEnemies()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Encounter;
|
||||
|
||||
|
||||
var PlayerCard = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
initiative : 0
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
return <div className='playerCard'>
|
||||
<span className='name'>{_.startCase(this.props.name)}</span>
|
||||
<span className='initiative'><i className='fa fa-hourglass-2'/>{this.props.initiative}</span>
|
||||
</div>
|
||||
},
|
||||
|
||||
})
|
||||
36
client/naturalCrit/combatManager/encounter/encounter.less
Normal file
36
client/naturalCrit/combatManager/encounter/encounter.less
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
.mainEncounter{
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
width : auto;
|
||||
|
||||
&>.info{
|
||||
|
||||
margin-left: 10px;
|
||||
padding-bottom : 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
h1{
|
||||
font-size: 2em;
|
||||
font-weight: 800;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
p{
|
||||
margin-left: 10px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
max-width: 600px;
|
||||
}
|
||||
.reward{
|
||||
font-size: 0.8em;
|
||||
font-weight: 800;
|
||||
margin-top: 5px;
|
||||
i{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var RollDice = require('naturalCrit/rollDice');
|
||||
|
||||
var AttackSlot = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
uses : null
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
lastRoll: {},
|
||||
usedCount : 0
|
||||
};
|
||||
},
|
||||
|
||||
rollDice : function(key, notation){
|
||||
var res = RollDice(notation);
|
||||
this.state.lastRoll[key] = res
|
||||
this.state.lastRoll[key + 'key'] = _.uniqueId(key);
|
||||
this.setState({
|
||||
lastRoll : this.state.lastRoll
|
||||
})
|
||||
},
|
||||
|
||||
renderUses : function(){
|
||||
var self = this;
|
||||
if(!this.props.uses) return null;
|
||||
|
||||
return _.times(this.props.uses, function(index){
|
||||
var atCount = index < self.state.usedCount;
|
||||
return <i
|
||||
key={index}
|
||||
className={cx('fa', {'fa-circle-o' : !atCount, 'fa-circle' : atCount})}
|
||||
onClick={self.updateCount.bind(self, atCount)}
|
||||
/>
|
||||
})
|
||||
},
|
||||
updateCount : function(used){
|
||||
this.setState({
|
||||
usedCount : this.state.usedCount + (used ? -1 : 1)
|
||||
});
|
||||
},
|
||||
|
||||
renderNotes : function(){
|
||||
var notes = _.omit(this.props, ['name', 'atk', 'dmg', 'uses', 'heal']);
|
||||
return _.map(notes, function(text, key){
|
||||
return <div key={key}>{key + ': ' + text}</div>
|
||||
});
|
||||
},
|
||||
|
||||
renderRolls : function(){
|
||||
var self = this;
|
||||
|
||||
return _.map(['atk', 'dmg', 'heal'], function(type){
|
||||
if(!self.props[type]) return null;
|
||||
return <div className={cx('roll', type)} key={type}>
|
||||
|
||||
<button onClick={self.rollDice.bind(self, type, self.props[type])}>
|
||||
<i className={cx('fa', {
|
||||
'fa-hand-grab-o' : type=='dmg',
|
||||
'fa-bullseye' : type=='atk',
|
||||
'fa-plus' : type=='heal'
|
||||
})} />
|
||||
{self.props[type]}
|
||||
</button>
|
||||
<span key={self.state.lastRoll[type+'key']}>{self.state.lastRoll[type]}</span>
|
||||
</div>
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='attackSlot'>
|
||||
<div className='info'>
|
||||
<div className='name'>{this.props.name}</div>
|
||||
<div className='uses'>
|
||||
{this.renderUses()}
|
||||
</div>
|
||||
<div className='notes'>
|
||||
{this.renderNotes()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='rolls'>
|
||||
{this.renderRolls()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AttackSlot;
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
.attackSlot{
|
||||
//border : 1px solid black;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom : 5px;
|
||||
font-size : 0.8em;
|
||||
.info, .rolls{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
}
|
||||
.info{
|
||||
width : 40%;
|
||||
.name{
|
||||
font-weight : 800;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.notes{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
.uses{
|
||||
cursor : pointer;
|
||||
//font-size: 0.8em;
|
||||
//margin-top: 3px;
|
||||
}
|
||||
}
|
||||
.rolls{
|
||||
.roll{
|
||||
margin-bottom : 2px;
|
||||
&>span{
|
||||
font-weight: 800;
|
||||
.fadeInLeft();
|
||||
}
|
||||
button{
|
||||
width : 70px;
|
||||
margin-right : 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
text-align : left;
|
||||
border : none;
|
||||
outline : 0;
|
||||
i{
|
||||
width : 15px;
|
||||
margin-right : 5px;
|
||||
border-right : 1px solid white;
|
||||
}
|
||||
&:hover{
|
||||
//text-align: right;
|
||||
}
|
||||
}
|
||||
&.atk{
|
||||
button{
|
||||
background-color : fade(@blue, 40%);
|
||||
i { border-color: @blue}
|
||||
}
|
||||
}
|
||||
&.dmg{
|
||||
button{
|
||||
background-color : fade(@red, 40%);
|
||||
i { border-color: @red}
|
||||
}
|
||||
}
|
||||
&.heal{
|
||||
button{
|
||||
background-color : fade(@green, 40%);
|
||||
i { border-color: @green}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var AttackSlot = require('./attackSlot/attackSlot.jsx');
|
||||
|
||||
var MonsterCard = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
hp : 1,
|
||||
currentHP : 1,
|
||||
ac: 1,
|
||||
move : 30,
|
||||
attr : {
|
||||
str : 8,
|
||||
con : 8,
|
||||
dex : 8,
|
||||
int : 8,
|
||||
wis : 8,
|
||||
cha : 8
|
||||
},
|
||||
attacks : {},
|
||||
spells : {},
|
||||
abilities : [],
|
||||
items : [],
|
||||
|
||||
updateHP : function(){},
|
||||
remove : function(){},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
status : 'normal',
|
||||
usedItems : [],
|
||||
lastRoll : { },
|
||||
mousePos : null,
|
||||
tempHP : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener('mousemove', this.handleMouseDrag);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
},
|
||||
|
||||
handleMouseDown : function(e){
|
||||
this.setState({
|
||||
mousePos : {
|
||||
x : e.pageX,
|
||||
y : e.pageY,
|
||||
}
|
||||
});
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
},
|
||||
handleMouseUp : function(e){
|
||||
if(!this.state.mousePos) return;
|
||||
|
||||
|
||||
this.props.updateHP(this.props.currentHP + this.state.tempHP);
|
||||
this.setState({
|
||||
mousePos : null,
|
||||
tempHP : 0
|
||||
});
|
||||
},
|
||||
|
||||
handleMouseDrag : function(e){
|
||||
if (!this.state.mousePos) return;
|
||||
var distance = Math.sqrt(Math.pow(e.pageX - this.state.mousePos.x, 2) + Math.pow(e.pageY - this.state.mousePos.y, 2));
|
||||
var mult = (e.pageY > this.state.mousePos.y ? -1 : 1)
|
||||
|
||||
this.setState({
|
||||
tempHP : Math.floor(distance * mult/25)
|
||||
})
|
||||
},
|
||||
|
||||
addUsed : function(item, shouldRemove){
|
||||
if(!shouldRemove) this.state.usedItems.push(item);
|
||||
if(shouldRemove) this.state.usedItems.splice(this.state.usedItems.indexOf(item), 1);
|
||||
|
||||
this.setState({
|
||||
usedItems : this.state.usedItems
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
renderHPBox : function(){
|
||||
var self = this;
|
||||
|
||||
var tempHP
|
||||
if(this.state.tempHP){
|
||||
var sign = (this.state.tempHP > 0 ? '+' : '');
|
||||
tempHP = <span className='tempHP'>{['(',sign,this.state.tempHP,')'].join('')}</span>
|
||||
}
|
||||
|
||||
return <div className='hpBox' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<div className='currentHP'>
|
||||
{tempHP} {this.props.currentHP}
|
||||
</div>
|
||||
{self.renderStats()}
|
||||
</div>
|
||||
},
|
||||
|
||||
renderStats : function(){
|
||||
var stats = {
|
||||
'fa fa-shield' : this.props.ac,
|
||||
//'fa fa-hourglass-2' : this.props.initiative,
|
||||
}
|
||||
return _.map(stats, function(val, icon){
|
||||
return <div className='stat' key={icon}> {val} <i className={icon} /></div>
|
||||
})
|
||||
},
|
||||
|
||||
renderAttacks : function(){
|
||||
var self = this;
|
||||
return _.map(this.props.attacks, function(attack, name){
|
||||
return <AttackSlot key={name} name={name} {...attack} />
|
||||
})
|
||||
},
|
||||
|
||||
renderSpells : function(){
|
||||
var self = this;
|
||||
return _.map(this.props.spells, function(spell, name){
|
||||
return <AttackSlot key={name} name={name} {...spell} />
|
||||
})
|
||||
},
|
||||
|
||||
renderAbilities : function(){
|
||||
return _.map(this.props.abilities, function(text, name){
|
||||
return <div className='ability' key={name}>
|
||||
<span className='name'>{name}</span>: {text}
|
||||
</div>
|
||||
});
|
||||
},
|
||||
|
||||
renderItems : function(){
|
||||
var self = this;
|
||||
var usedItems = this.state.usedItems.slice(0);
|
||||
return _.map(this.props.items, function(item, index){
|
||||
var used = _.contains(usedItems, item);
|
||||
if(used){
|
||||
usedItems.splice(usedItems.indexOf(item), 1);
|
||||
}
|
||||
return <span
|
||||
key={index}
|
||||
className={cx({'used' : used})}
|
||||
onClick={self.addUsed.bind(self, item, used)}>
|
||||
{item}
|
||||
</span>
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
var condition = ''
|
||||
if(this.props.currentHP + this.state.tempHP > this.props.hp) condition='overhealed';
|
||||
if(this.props.currentHP + this.state.tempHP <= this.props.hp * 0.5) condition='hurt';
|
||||
if(this.props.currentHP + this.state.tempHP <= this.props.hp * 0.2) condition='last_legs';
|
||||
if(this.props.currentHP + this.state.tempHP <= 0) condition='dead';
|
||||
|
||||
|
||||
return(
|
||||
<div className={cx('monsterCard', condition)}>
|
||||
<div className='healthbar' style={{width : (this.props.currentHP + this.state.tempHP)/this.props.hp*100 + '%'}} />
|
||||
<div className='overhealbar' style={{width : (this.props.currentHP + this.state.tempHP - this.props.hp)/this.props.hp*100 + '%'}} />
|
||||
|
||||
|
||||
{this.renderHPBox()}
|
||||
<div className='info'>
|
||||
<span className='name'>{this.props.name}</span>
|
||||
</div>
|
||||
|
||||
<div className='attackContainer'>
|
||||
{this.renderAttacks()}
|
||||
</div>
|
||||
<div className='spellContainer'>
|
||||
{this.renderSpells()}
|
||||
</div>
|
||||
|
||||
<div className='abilitiesContainer'>
|
||||
{this.renderAbilities()}
|
||||
</div>
|
||||
<div className='itemContainer'>
|
||||
<i className='fa fa-flask' />
|
||||
{this.renderItems()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MonsterCard;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
{this.props.initiative}
|
||||
|
||||
<i className='fa fa-times' onClick={this.props.remove} />
|
||||
*/
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
@marginSize : 10px;
|
||||
.playerCard{
|
||||
display : inline-block;
|
||||
box-sizing : border-box;
|
||||
margin : @marginSize;
|
||||
padding : 10px;
|
||||
background-color : white;
|
||||
border : 1px solid #bbb;
|
||||
.name{
|
||||
margin-right : 20px;
|
||||
}
|
||||
.initiative{
|
||||
font-size : 0.8em;
|
||||
i{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
}
|
||||
&:nth-child(5n + 1){ background-color: fade(@blue, 25%); }
|
||||
&:nth-child(5n + 2){ background-color: fade(@purple, 25%); }
|
||||
&:nth-child(5n + 3){ background-color: fade(@steel, 25%); }
|
||||
&:nth-child(5n + 4){ background-color: fade(@green, 25%); }
|
||||
&:nth-child(5n + 5){ background-color: fade(@orange, 25%); }
|
||||
}
|
||||
.monsterCard{
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
box-sizing : border-box;
|
||||
width : 220px;
|
||||
margin : @marginSize;
|
||||
padding : 10px;
|
||||
background-color : white;
|
||||
border : 1px solid #bbb;
|
||||
.healthbar{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
left : 0px;
|
||||
z-index : 50;
|
||||
height : 3px;
|
||||
max-width : 100%;
|
||||
background-color : @green;
|
||||
}
|
||||
.overhealbar{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
left : 0px;
|
||||
z-index : 100;
|
||||
height : 3px;
|
||||
max-width : 100%;
|
||||
background-color : @blueLight;
|
||||
}
|
||||
&.hurt{
|
||||
.healthbar{
|
||||
background-color : orange;
|
||||
}
|
||||
}
|
||||
&.last_legs{
|
||||
background-color : lighten(@red, 49%);
|
||||
.healthbar{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
&.dead{
|
||||
opacity : 0.3;
|
||||
}
|
||||
&>.info{
|
||||
margin-bottom : 10px;
|
||||
.name{
|
||||
margin-right : 10px;
|
||||
font-size : 1.5em;
|
||||
}
|
||||
.stat{
|
||||
margin-right : 5px;
|
||||
font-size : 0.7em;
|
||||
i{
|
||||
font-size : 0.7em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hpBox{
|
||||
.noselect();
|
||||
position : absolute;
|
||||
top : 5px;
|
||||
right : 5px;
|
||||
cursor : pointer;
|
||||
text-align : right;
|
||||
.currentHP{
|
||||
font-size : 2em;
|
||||
font-weight : 800;
|
||||
line-height : 0.8em;
|
||||
.tempHP{
|
||||
vertical-align : top;
|
||||
font-size : 0.4em;
|
||||
line-height : 0.8em;
|
||||
}
|
||||
}
|
||||
.stat{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
.hpText{
|
||||
font-size : 0.6em;
|
||||
font-weight : 800;
|
||||
}
|
||||
}
|
||||
.abilitiesContainer{
|
||||
margin-top : 10px;
|
||||
.ability{
|
||||
font-size: 0.7em;
|
||||
.name{
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
.itemContainer{
|
||||
margin-top : 10px;
|
||||
i{
|
||||
font-size : 0.7em;
|
||||
}
|
||||
span{
|
||||
margin-right : 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.7em;
|
||||
&.used{
|
||||
text-decoration : line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user