0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-08 01:02:51 +00:00

Initial commit

This commit is contained in:
Scott Tolksdorf
2015-11-14 16:17:26 -05:00
parent 85d363a508
commit 3dee96ad38
16 changed files with 1263 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var AttackSlot = React.createClass({
getDefaultProps: function() {
return {
name : '',
uses : null
};
},
getInitialState: function() {
return {
lastRoll: {},
usedCount : 0
};
},
rollDice : function(key, notation){
var additive = 0;
var dice = _.reduce([/\+(.*)/, /\-(.*)/], function(r, regexp){
var res = r.match(regexp);
if(res){
additive = res[0]*1;
r = r.replace(res[0], '')
}
return r;
}, notation)
var numDice = dice.split('d')[0];
var die = dice.split('d')[1];
var diceRoll = _.times(numDice, function(){
return _.random(1, die);
});
var res = _.sum(diceRoll) + additive;
if(numDice == 1 && die == 20){
if(diceRoll[0] == 1) res = 'Fail!';
if(diceRoll[0] == 20) res = 'Crit!';
}
this.state.lastRoll[key] = res
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 key + ': ' + text
}).join(', ');
},
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-medkit' : type=='heal'
})} />
{self.props[type]}
</button>
<span>{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;

View File

@@ -0,0 +1,67 @@
.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;
}
.notes{
font-size : 0.8em;
}
.uses{
cursor : pointer;
}
}
.rolls{
.roll{
margin-bottom : 2px;
span{
font-weight: 800;
}
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}
}
}
}
}
}

View File

@@ -0,0 +1,183 @@
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 {
//currentHP: this.props.hp,
status : 'normal',
usedThings : [],
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)
})
},
renderHPBox : function(){
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>
<div className='maxHP'>{this.props.hp}</div>
</div>
},
renderStats : function(){
},
rollDice : function(key, notation){
var additive = 0;
var dice = _.reduce([/\+(.*)/, /\-(.*)/], function(r, regexp){
var res = r.match(regexp);
if(res){
additive = res[0]*1;
r = r.replace(res[0], '')
}
return r;
}, notation)
var numDice = dice.split('d')[0];
var die = dice.split('d')[1];
var diceRoll = _.times(numDice, function(){
return _.random(1, die);
});
var res = _.sum(diceRoll) + additive;
if(numDice == 1 && die == 20){
if(diceRoll[0] == 1) res = 'Fail!';
if(diceRoll[0] == 20) res = 'Crit!';
}
this.state.lastRoll[key] = res
this.setState({
lastRoll : this.state.lastRoll
})
},
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} />
})
},
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='name'>{this.props.name}</div>
<div className='attackContainer'>
{this.renderAttacks()}
</div>
<div className='spellContainer'>
{this.renderSpells()}
</div>
{this.props.initiative}
<i className='fa fa-times' onClick={this.props.remove} />
</div>
);
}
});
module.exports = MonsterCard;

View File

@@ -0,0 +1,77 @@
.noselect(){
-webkit-touch-callout : none;
-webkit-user-select : none;
-khtml-user-select : none;
-moz-user-select : none;
-ms-user-select : none;
user-select : none;
}
.monsterCard{
position : relative;
display : inline-block;
vertical-align : top;
box-sizing : border-box;
width : 250px;
margin : 30px;
padding : 10px;
background-color : white;
border : 1px solid #bbb;
.healthbar{
position : absolute;
top : 0px;
left : 0px;
height : 3px;
max-width : 100%;
background-color : @green;
z-index : 50;
}
.overhealbar{
position : absolute;
top : 0px;
left : 0px;
height : 3px;
max-width : 100%;
background-color : @blueLight;
z-index : 100;
}
&.hurt{
.healthbar{
background-color : orange;
}
}
&.last_legs{
background-color: lighten(@red, 49%);
.healthbar{
background-color : red;
}
}
&.dead{
opacity: 0.3;
}
.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;
}
}
.maxHP{
font-size : 0.8em;
}
.hpText{
font-size : 0.6em;
font-weight : 800;
}
}
}

View File

@@ -0,0 +1,155 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var MonsterCard = require('./monsterCard/monsterCard.jsx');
var encounter = {
name : 'The Big Bad',
enemies : ['goblin', 'goblin'],
reserve : ['goblin'],
}
var MonsterManual = {
'goblin' : {
"hp" : 40,
"mov": 30,
"attr" : {
"str" : 8,
"con" : 8,
"dex" : 8,
"int" : 8,
"wis" : 8,
"cha" : 8
},
"attacks" : {
"dagger" : {
"atk" : "1d20-5",
"dmg" : "1d4+5",
"type" : "pierce",
"notes" : "Super cool"
},
"bow" : {
"atk" : "1d20+2",
"dmg" : "6d6",
"rng" : "30"
}
},
"spells" : {
"fireball": {
"dmg" : "6d6",
"uses" : 4
},
"healing_bolt" : {
"heal" : "2d8+4",
"uses" : 6
}
},
"abilities" : ["pack tactics"],
"items" : []
}
}
var attrMod = function(attr){
return Math.floor(attr/2) - 5;
}
var NaturalCrit = React.createClass({
getInitialState: function() {
var self = this;
return {
enemies: _.indexBy(_.map(encounter.enemies, function(type, index){
return self.createEnemy(type, index)
}), 'id')
};
},
createEnemy : function(type, index){
var stats = MonsterManual[type]
return _.extend({
id : type + index,
name : type,
currentHP : stats.hp,
initiative : _.random(1,20) + attrMod(stats.attr.dex)
}, stats);
},
addPC : function(name, initiative){
this.state.enemies[name] = {
name : name,
id : name,
initiative : initiative,
isPC : true
};
this.setState({
enemies : this.state.enemies
})
},
addRandomPC : function(){
this.addPC(
_.sample(['zatch', 'jasper', 'el toro', 'tulik']) + _.random(1,1000),
_.random(1,25)
)
},
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
});
},
render : function(){
var self = this;
console.log();
var sortedEnemies = _.sortBy(this.state.enemies, function(e){
return -e.initiative;
});
var cards = _.map(sortedEnemies, function(enemy){
return <MonsterCard
{...enemy}
key={enemy.id}
updateHP={self.updateHP.bind(self, enemy.id)}
remove={self.removeEnemy.bind(self, enemy.id)} />
})
return(
<div className='naturalCrit'>
<button className='rollInitiative' onClick={this.addRandomPC}> rollInitiative</button>
Project Ready!
{cards}
</div>
);
}
});
module.exports = NaturalCrit;

View File

@@ -0,0 +1,19 @@
@import 'naturalCrit/reset.less';
//@import 'naturalCrit/elements.less';
@import 'naturalCrit/animations.less';
@import 'naturalCrit/colors.less';
body{
background-color : #eee;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0;
padding : 0;
}
.naturalCrit{
color : #333;
background-color: #eee;
}

18
client/template.dot Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<script>global=window</script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link rel="icon" href="/assets/NaturalCrit/favicon.ico" type="image/x-icon" />
{{=vitreum.css}}
{{=vitreum.globals}}
<title>NaturalCrit</title>
</head>
<body>
<div id="reactContainer">{{=vitreum.component}}</div>
</body>
{{=vitreum.libs}}
{{=vitreum.js}}
{{=vitreum.reactRender}}
</html>