This commit is contained in:
ntr 2019-05-06 00:10:13 +10:00
parent f04a7101ee
commit 3fb35288d8
15 changed files with 203 additions and 175 deletions

View File

@ -1,3 +1,8 @@
# vbox_info ->
combos [strike, [R R Attack]]
specs [spec [bonus amount, [r g b]]
# Playthrough # Playthrough
cryps join game cryps join game

View File

@ -116,6 +116,21 @@ button.left:hover, button.left:focus {
box-shadow: inset 0.5em 0 0 0 whitesmoke; box-shadow: inset 0.5em 0 0 0 whitesmoke;
} }
button.action {
animation: action 1s infinite ease-in-out alternate;
}
@keyframes action {
0% {
box-shadow: inset 0 0 0 0 whitesmoke;
}
100% {
box-shadow: inset 0.5em 0 0 0 whitesmoke;
}
}
svg { svg {
flex: 1; flex: 1;
fill: none; fill: none;
@ -427,6 +442,12 @@ header {
margin: 0; margin: 0;
} }
table .highlight {
background: whitesmoke;
color: black;
font-weight: bold;
}
button[disabled] { button[disabled] {
color: #333; color: #333;
border-color: #333; border-color: #333;
@ -489,7 +510,6 @@ table td {
padding: 0.2em; padding: 0.2em;
text-align: center; text-align: center;
height: 40px; height: 40px;
cursor: pointer;
text-transform: uppercase; text-transform: uppercase;
} }
@ -498,6 +518,7 @@ table td {
transition-duration: 0.5s; transition-duration: 0.5s;
transition-delay: 0; transition-delay: 0;
transition-timing-function: ease; transition-timing-function: ease;
cursor: pointer;
} }
.vbox-table table td:active { .vbox-table table td:active {

View File

@ -28,7 +28,8 @@
"phaser": "^3.15.1", "phaser": "^3.15.1",
"preact": "^8.3.1", "preact": "^8.3.1",
"preact-redux": "^2.0.3", "preact-redux": "^2.0.3",
"redux": "^4.0.0" "redux": "^4.0.0",
"redux-diff-logger": "0.0.9"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.3", "babel-core": "^6.26.3",

View File

@ -4,6 +4,9 @@ export const setAccount = value => ({ type: SET_ACCOUNT, value });
export const SET_CRYPS = 'SET_CRYPS'; export const SET_CRYPS = 'SET_CRYPS';
export const setCryps = value => ({ type: SET_CRYPS, value }); export const setCryps = value => ({ type: SET_CRYPS, value });
export const SET_VBOX_INFO = 'SET_VBOX_INFO';
export const setVboxInfo = value => ({ type: SET_VBOX_INFO, value });
export const SET_INSTANCES = 'SET_INSTANCES'; export const SET_INSTANCES = 'SET_INSTANCES';
export const setInstances = value => ({ type: SET_INSTANCES, value }); export const setInstances = value => ({ type: SET_INSTANCES, value });

View File

@ -2,7 +2,7 @@ const preact = require('preact');
const range = require('lodash/range'); const range = require('lodash/range');
const { ITEMS: { SKILLS, COLOURS, SPECS: SPEC_CONSTANT } } = require('./../constants'); const { ITEMS: { SKILLS, COLOURS, SPECS: SPEC_CONSTANT } } = require('./../constants');
const { COLOUR_ICONS, STATS, SPECS } = require('../utils'); const { COLOUR_ICONS, STATS, SPECS, convertVar, crypAvatar } = require('../utils');
function Info(args) { function Info(args) {
const { const {
@ -12,10 +12,16 @@ function Info(args) {
instance, instance,
player, player,
setInfo, setInfo,
vboxInfo,
} = args; } = args;
function infoVar([type, value]) { function CrypVar() {
let red = 0; let blue = 0; let green = 0; const [type, value] = info;
if (!type) return false;
let red = 0;
let blue = 0;
let green = 0;
player.cryps.forEach(cryp => { player.cryps.forEach(cryp => {
red += cryp.colours.red; red += cryp.colours.red;
blue += cryp.colours.blue; blue += cryp.colours.blue;
@ -100,7 +106,12 @@ function Info(args) {
} }
} }
function infoCrypElement(cryp) { function CrypInfo() {
if (!activeCryp) return false;
const cryp = player.cryps.find(c => c.id === activeCryp.id);
if (!cryp) return false;
// onClick={() => setInfo('skill', { skill: s, cryp })} // onClick={() => setInfo('skill', { skill: s, cryp })}
const skills = range(0, 3).map(i => { const skills = range(0, 3).map(i => {
const skill = cryp.skills[i]; const skill = cryp.skills[i];
@ -139,7 +150,6 @@ function Info(args) {
return ( return (
<div className="info-cryp"> <div className="info-cryp">
<h2>{cryp.name}</h2>
<div className="stats"> <div className="stats">
{stats} {stats}
</div> </div>
@ -158,10 +168,10 @@ function Info(args) {
return instance.rounds[instance.rounds.length - 1].find(r => r.player_ids.includes(id)); return instance.rounds[instance.rounds.length - 1].find(r => r.player_ids.includes(id));
} }
function playerText(player) { function playerText(p) {
const round = playerRound(player.id); const round = playerRound(p.id);
if (!round) { if (!round) {
return player.ready return p.ready
? 'ready' ? 'ready'
: ''; : '';
} }
@ -169,12 +179,14 @@ function Info(args) {
if (round.finished) return 'finished'; if (round.finished) return 'finished';
if (round.game_id) return 'in game'; if (round.game_id) return 'in game';
return player.ready return p.ready
? 'ready' ? 'ready'
: ''; : '';
} }
function scoreBoard() { function ScoreBoard() {
if (activeCryp || info[0]) return null;
const players = instance.players.map((p, i) => { const players = instance.players.map((p, i) => {
const pText = playerText(p); const pText = playerText(p);
return ( return (
@ -196,29 +208,33 @@ function Info(args) {
); );
} }
const scoreBoardEl = activeCryp || info[0] function Combos() {
? null if (!info[0]) return false;
: scoreBoard(); if (activeCryp) return false;
return (
const infoCryp = activeCryp <table>
? infoCrypElement(player.cryps.find(c => c.id === activeCryp.id)) <tbody>
: null; {vboxInfo.combos.filter(c => c.units.includes(info[1])).map((c, i) =>
<tr key={i} >
const otherInfo = info[0] <td className="highlight" >{convertVar(c.var)}</td>
? infoVar(info) {c.units.map(u => <td>{convertVar(u)}</td>)}
: null; </tr>
)}
</tbody>
</table>
);
}
// const beginningHdr = instance.phase === 'Lobby' // const beginningHdr = instance.phase === 'Lobby'
// ? <h2>game beginning...</h2> // ? <h2>game beginning...</h2>
// : null; // : null;
const instanceInfoClass = `instance-info ${!info[0] ? '' : 'hidden'}`;
return ( return (
<div className={instanceInfoClass} > <div className="instance-info" >
{scoreBoardEl} <ScoreBoard />
{infoCryp} <CrypInfo />
{otherInfo} <Combos />
<CrypVar />
</div> </div>
); );
} }

View File

@ -10,6 +10,7 @@ const addState = connect(
info, info,
ws, ws,
instance, instance,
vboxInfo,
player, player,
} = state; } = state;
@ -23,6 +24,7 @@ const addState = connect(
sendUnequip, sendUnequip,
instance, instance,
player, player,
vboxInfo,
}; };
}, },

View File

@ -69,7 +69,8 @@ function Cryp(props) {
return setActiveCryp(cryp); return setActiveCryp(cryp);
} }
return <button key={i} className="right" onClick={skillClick} >{s}</button>; const classes = `right ${skill ? '' : 'action'}`;
return <button key={i} className={classes} onClick={skillClick} >{s}</button>;
}); });
// needed for ondrop to fire // needed for ondrop to fire

View File

@ -3,19 +3,7 @@ const range = require('lodash/range');
const shapes = require('./shapes'); const shapes = require('./shapes');
function convertVar(v) { const { convertVar } = require('./../utils');
if (['Red', 'Green', 'Blue'].includes(v)) {
return (
shapes.vboxColour(v.toLowerCase())
);
}
return v || <span>&nbsp;</span>;
// uncomment for double borders in vbox;
// if (v) {
// return <div>{v}</div>;
// }
// return;
}
function Vbox(args) { function Vbox(args) {
const { const {
@ -131,10 +119,7 @@ function Vbox(args) {
function boundClick(e, i) { function boundClick(e, i) {
if (reclaiming && vbox.bound[i]) sendVboxReclaim(i); if (reclaiming && vbox.bound[i]) sendVboxReclaim(i);
else if (vbox.bound[i]) { else if (vbox.bound[i]) {
const insert = ['Red', 'Green', 'Blue'].includes(vbox.bound[i]) const insert = combiner.findIndex(j => j === null);
? combiner.findIndex(j => j === null)
: 2;
if (insert === -1) return setCombiner([i, null, null]); if (insert === -1) return setCombiner([i, null, null]);
combiner[insert] = i; combiner[insert] = i;
boundTimer = null; boundTimer = null;

View File

@ -18,24 +18,24 @@ function registerEvents(store) {
}); });
// cryp animations // // cryp animations
function crypAnimations() { // function crypAnimations() {
const cryps = document.querySelectorAll('img'); // const cryps = document.querySelectorAll('img');
if (!cryps.length) return window.requestAnimationFrame(crypAnimations); // if (!cryps.length) return window.requestAnimationFrame(crypAnimations);
return anime({ // return anime({
targets: 'img', // targets: 'img',
translateX: () => anime.random(-20, 20), // translateX: () => anime.random(-20, 20),
translateY: () => anime.random(0, -40), // translateY: () => anime.random(0, -40),
rotate: () => anime.random(-15, 15), // rotate: () => anime.random(-15, 15),
duration: () => anime.random(5000, 6000), // duration: () => anime.random(5000, 6000),
delay: () => anime.random(0, 1000), // delay: () => anime.random(0, 1000),
direction: 'alternate', // direction: 'alternate',
easing: 'linear', // easing: 'linear',
loop: true, // loop: true,
}); // });
} // }
setInterval(crypAnimations, 5000); // setInterval(crypAnimations, 5000);
crypAnimations(); // crypAnimations();
function setPing(ping) { function setPing(ping) {
store.dispatch(actions.setPing(ping)); store.dispatch(actions.setPing(ping));
@ -134,7 +134,6 @@ function registerEvents(store) {
const player = v.players.find(p => p.id === account.id); const player = v.players.find(p => p.id === account.id);
if (player) store.dispatch(actions.setPlayer(player)); if (player) store.dispatch(actions.setPlayer(player));
if (v) ws.startInstanceStateTimeout(v.id); if (v) ws.startInstanceStateTimeout(v.id);
return store.dispatch(actions.setInstance(v)); return store.dispatch(actions.setInstance(v));
} }
@ -150,6 +149,10 @@ function registerEvents(store) {
console.log('EVENT ->', 'crypStatusUpdate', { id, skill, target }); console.log('EVENT ->', 'crypStatusUpdate', { id, skill, target });
} }
function setVboxInfo(v) {
return store.dispatch(actions.setVboxInfo(v));
}
// events.on('SET_PLAYER', setInstance); // events.on('SET_PLAYER', setInstance);
// events.on('SEND_SKILL', function skillActive(gameId, crypId, targetCrypId, skill) { // events.on('SEND_SKILL', function skillActive(gameId, crypId, targetCrypId, skill) {
@ -189,94 +192,8 @@ function registerEvents(store) {
}); });
} }
// function loginPrompt() {
// const USER_INPUT = '<input className="input" type="email" placeholder="username" />';
// const PASSWORD_INPUT = '<input className="input" type="password" placeholder="password" />';
// const LOGIN_BUTTON = '<button type="submit">Login</button>';
// const REGISTER_BUTTON = '<button type="submit">Register</button>';
// const DEMO_BUTTON = '<button type="submit">Demo</button>';
// const ws = registry.get('ws');
// function submitLogin(instance, thisToast, button, e, inputs) {
// const USERNAME = inputs[0].value;
// const PASSWORD = inputs[1].value;
// ws.sendAccountLogin(USERNAME, PASSWORD);
// }
// function submitRegister(instance, thisToast, button, e, inputs) {
// const USERNAME = inputs[0].value;
// const PASSWORD = inputs[1].value;
// ws.sendAccountCreate(USERNAME, PASSWORD);
// }
// function submitDemo() {
// ws.sendAccountDemo();
// }
// const existing = document.querySelector('#login'); // Selector of your toast
// if (existing) toast.hide({}, existing, 'reconnect');
// toast.question({
// id: 'login',
// theme: 'dark',
// color: 'black',
// timeout: false,
// // overlay: true,
// drag: false,
// close: false,
// title: 'LOGIN',
// position: 'center',
// inputs: [
// [USER_INPUT, 'change', () => true, true], // true to focus
// [PASSWORD_INPUT, 'change', () => true],
// ],
// buttons: [
// [LOGIN_BUTTON, submitLogin], // true to focus
// [REGISTER_BUTTON, submitRegister], // true to focus
// [DEMO_BUTTON, submitDemo], // true to focus
// ],
// });
// console.log('ACCOUNT', function closeLoginCb() {
// const prompt = document.querySelector('#login'); // Selector of your toast
// if (prompt) toast.hide({ transitionOut: 'fadeOut' }, prompt, 'EVENT ->');
// });
// }
// events.on('CRYP_SPAWN', function spawnPrompt() {
// const NAME_INPUT = '<input className="input" type="email" placeholder="name" />';
// const SPAWN_BUTTON = '<button type="submit">SPAWN</button>';
// const ws = registry.get('ws');
// function submitSpawn(instance, thisToast, button, e, inputs) {
// const NAME = inputs[0].value;
// ws.sendCrypSpawn(NAME);
// instance.hide({ transitionOut: 'fadeOut' }, thisToast, 'button');
// }
// toast.question({
// theme: 'dark',
// color: 'black',
// timeout: false,
// // overlay: true,
// drag: false,
// close: true,
// title: 'SPAWN CRYP',
// position: 'center',
// inputs: [
// [NAME_INPUT, 'change', null, true], // true to focus
// ],
// buttons: [
// [SPAWN_BUTTON, submitSpawn], // true to focus
// ],
// });
// });
return { return {
errorPrompt, errorPrompt,
// loginPrompt,
clearCombiner, clearCombiner,
setAccount, setAccount,
setActiveSkill, setActiveSkill,
@ -294,6 +211,7 @@ function registerEvents(store) {
setZone, setZone,
setPing, setPing,
setScores, setScores,
setVboxInfo,
}; };
} }

View File

@ -1,8 +1,9 @@
const preact = require('preact'); const preact = require('preact');
const jdenticon = require('jdenticon'); const jdenticon = require('jdenticon');
const logger = require('redux-diff-logger');
const { Provider } = require('preact-redux'); const { Provider } = require('preact-redux');
const { createStore, combineReducers } = require('redux'); const { createStore, combineReducers, applyMiddleware } = require('redux');
const reducers = require('./reducers'); const reducers = require('./reducers');
const actions = require('./actions'); const actions = require('./actions');
@ -15,7 +16,8 @@ const Header = require('./components/header.container');
const Body = require('./components/body.component'); const Body = require('./components/body.component');
// Redux Store // Redux Store
const store = createStore( const createStoreWithMiddleware = applyMiddleware(logger)(createStore);
const store = createStoreWithMiddleware(
combineReducers({ combineReducers({
account: reducers.accountReducer, account: reducers.accountReducer,
activeSkill: reducers.activeSkillReducer, activeSkill: reducers.activeSkillReducer,
@ -27,6 +29,7 @@ const store = createStore(
resolution: reducers.resolutionReducer, resolution: reducers.resolutionReducer,
showLog: reducers.showLogReducer, showLog: reducers.showLogReducer,
info: reducers.infoReducer, info: reducers.infoReducer,
vboxInfo: reducers.vboxInfoReducer,
instance: reducers.instanceReducer, instance: reducers.instanceReducer,
player: reducers.playerReducer, player: reducers.playerReducer,
ping: reducers.pingReducer, ping: reducers.pingReducer,
@ -37,9 +40,10 @@ const store = createStore(
}) })
); );
document.fonts.load('16pt "Jura"').then(() => { document.fonts.load('16pt "Jura"').then(() => {
const events = registerEvents(store); const events = registerEvents(store);
store.subscribe(() => console.log(store.getState())); // store.subscribe(() => console.log(store.getState()));
setupKeys(store); setupKeys(store);
const ws = createSocket(events); const ws = createSocket(events);

View File

@ -20,6 +20,16 @@ function pingReducer(state = defaultPing, action) {
} }
} }
const defaultVboxInfo = { combos: [], vars: [] };
function vboxInfoReducer(state = defaultVboxInfo, action) {
switch (action.type) {
case actions.SET_VBOX_INFO:
return action.value;
default:
return state;
}
}
const defaultActiveSkill = null; const defaultActiveSkill = null;
function activeSkillReducer(state = defaultActiveSkill, action) { function activeSkillReducer(state = defaultActiveSkill, action) {
switch (action.type) { switch (action.type) {
@ -188,4 +198,5 @@ module.exports = {
wsReducer, wsReducer,
infoReducer, infoReducer,
pingReducer, pingReducer,
vboxInfoReducer,
}; };

View File

@ -64,14 +64,6 @@ function createSocket(events) {
send({ method: 'cryp_spawn', params: { name } }); send({ method: 'cryp_spawn', params: { name } });
} }
function sendCrypLearn(id, skill) {
send({ method: 'cryp_learn', params: { id, skill } });
}
function sendCrypForget(id, skill) {
send({ method: 'cryp_forget', params: { id, skill } });
}
function sendGameState(id) { function sendGameState(id) {
send({ method: 'game_state', params: { id } }); send({ method: 'game_state', params: { id } });
} }
@ -119,6 +111,10 @@ function createSocket(events) {
send({ method: 'player_vbox_reclaim', params: { instance_id: instanceId, index } }); send({ method: 'player_vbox_reclaim', params: { instance_id: instanceId, index } });
} }
function sendVboxInfo() {
send({ method: 'vbox_info', params: {} });
}
function sendGameSkill(gameId, crypId, targetCrypId, skill) { function sendGameSkill(gameId, crypId, targetCrypId, skill) {
send({ send({
method: 'game_skill', method: 'game_skill',
@ -178,6 +174,7 @@ function createSocket(events) {
function accountInstanceList(res) { function accountInstanceList(res) {
const [struct, playerList] = res; const [struct, playerList] = res;
sendVboxInfo();
events.setInstanceList(playerList); events.setInstanceList(playerList);
} }
@ -228,9 +225,9 @@ function createSocket(events) {
clearTimeout(instanceStateTimeout); clearTimeout(instanceStateTimeout);
} }
function instanceScores(response) { function vboxInfo(response) {
const [structName, scores] = response; const [structName, info] = response;
events.setScores(scores); events.setVboxInfo(info);
} }
// ------------- // -------------
@ -241,18 +238,16 @@ function createSocket(events) {
// this object wraps the reply types to a function // this object wraps the reply types to a function
const handlers = { const handlers = {
cryp_spawn: crypSpawn, cryp_spawn: crypSpawn,
cryp_forget: () => true,
cryp_learn: () => true,
game_state: gameState, game_state: gameState,
account_login: accountLogin, account_login: accountLogin,
account_create: accountLogin, account_create: accountLogin,
account_cryps: accountCryps, account_cryps: accountCryps,
account_instances: accountInstanceList, account_instances: accountInstanceList,
instance_scores: instanceScores,
zone_create: res => console.log(res), zone_create: res => console.log(res),
zone_state: zoneState, zone_state: zoneState,
zone_close: res => console.log(res), zone_close: res => console.log(res),
instance_state: instanceState, instance_state: instanceState,
vbox_info: vboxInfo,
}; };
function logout() { function logout() {
@ -346,8 +341,6 @@ function createSocket(events) {
sendGameSkill, sendGameSkill,
sendGameTarget, sendGameTarget,
sendCrypSpawn, sendCrypSpawn,
sendCrypLearn,
sendCrypForget,
sendSpecForget, sendSpecForget,
sendZoneCreate, sendZoneCreate,
sendZoneJoin, sendZoneJoin,
@ -364,6 +357,7 @@ function createSocket(events) {
sendVboxCombine, sendVboxCombine,
sendVboxDiscard, sendVboxDiscard,
sendVboxUnequip, sendVboxUnequip,
sendVboxInfo,
startInstanceStateTimeout, startInstanceStateTimeout,
startGameStateTimeout, startGameStateTimeout,
connect, connect,

View File

@ -344,8 +344,23 @@ function getCombatText(cryp, resolution) {
return ''; return '';
} }
function convertVar(v) {
if (['Red', 'Green', 'Blue'].includes(v)) {
return (
shapes.vboxColour(v.toLowerCase())
);
}
return v || <span>&nbsp;</span>;
// uncomment for double borders in vbox;
// if (v) {
// return <div>{v}</div>;
// }
// return;
}
module.exports = { module.exports = {
stringSort, stringSort,
convertVar,
numSort, numSort,
genAvatar, genAvatar,
crypAvatar, crypAvatar,

View File

@ -21,9 +21,9 @@ use account::{Account, account_create, account_login, account_from_token, accoun
use skill::{Skill}; use skill::{Skill};
// use zone::{Zone, zone_create, zone_join, zone_close}; // use zone::{Zone, zone_create, zone_join, zone_close};
use spec::{Spec}; use spec::{Spec};
use player::{Score, player_mm_cryps_set, Player}; use player::{Score, player_mm_cryps_set};
use instance::{Instance, instance_state, instance_new, instance_ready, instance_join}; use instance::{Instance, instance_state, instance_new, instance_ready, instance_join};
use vbox::{Var, vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip}; use vbox::{Var, VboxInfo, vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip, vbox_info};
pub struct Rpc; pub struct Rpc;
@ -86,6 +86,8 @@ impl Rpc {
"player_vbox_reclaim" => Rpc::player_vbox_reclaim(data, &mut tx, account.unwrap(), client), "player_vbox_reclaim" => Rpc::player_vbox_reclaim(data, &mut tx, account.unwrap(), client),
"player_vbox_unequip" => Rpc::player_vbox_unequip(data, &mut tx, account.unwrap(), client), "player_vbox_unequip" => Rpc::player_vbox_unequip(data, &mut tx, account.unwrap(), client),
"vbox_info" => Ok(RpcResponse { method: "vbox_info".to_string(), params: RpcResult::VboxInfo(vbox_info()) }),
_ => Err(format_err!("unknown method - {:?}", v.method)), _ => Err(format_err!("unknown method - {:?}", v.method)),
}; };
@ -392,6 +394,7 @@ pub enum RpcResult {
Account(Account), Account(Account),
CrypList(Vec<Cryp>), CrypList(Vec<Cryp>),
GameState(Game), GameState(Game),
VboxInfo(VboxInfo),
InstanceScores(Vec<(String, Score)>), InstanceScores(Vec<(String, Score)>),
// ZoneState(Zone), // ZoneState(Zone),
// ZoneClose(()), // ZoneClose(()),

View File

@ -344,7 +344,8 @@ impl From<Spec> for Var {
} }
struct Combo { #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Combo {
var: Var, var: Var,
units: Vec<Var>, units: Vec<Var>,
} }
@ -417,6 +418,50 @@ fn get_combos() -> Vec<Combo> {
return combinations; return combinations;
} }
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct VarInfo {
pub v: Var,
pub spec: bool,
pub skill: bool,
}
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct VboxInfo {
pub combos: Vec<Combo>,
pub vars: Vec<VarInfo>,
}
pub fn vbox_info() -> VboxInfo {
let combos = get_combos();
let mut vars = combos
.into_iter()
.flat_map(|mut c| {
c.units.push(c.var);
c.units
})
.collect::<Vec<Var>>();
vars.sort_unstable();
vars.dedup();
let vars = vars
.into_iter()
.map(|v| VarInfo {
v,
spec: v.into_spec().is_some(),
skill: v.into_skill().is_some(),
})
.collect::<Vec<VarInfo>>();
let combos = get_combos();
return VboxInfo {
combos,
vars,
};
}
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Vbox { pub struct Vbox {
pub bits: u16, pub bits: u16,
@ -632,4 +677,8 @@ mod tests {
assert_eq!(count.red, 2); assert_eq!(count.red, 2);
} }
#[test]
fn vbox_info_test() {
println!("{:#?}", vbox_info());
}
} }