Merge branch 'master' of github.com:highfidelity/hifi into tablet-ui-edit-js

This commit is contained in:
Seth Alves 2017-02-28 13:33:51 -08:00
commit c2085ebb6e
61 changed files with 4509 additions and 655 deletions

View file

@ -168,7 +168,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
QList<AvatarSharedPointer> avatarList;
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
int listItem = 0;
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
@ -176,7 +175,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
// but not have yet sent data that's linked to the node. Check for that case and don't
// consider those nodes.
if (otherNodeData) {
listItem++;
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
avatarList << otherAvatar;
avatarDataToNodes[otherAvatar] = otherNode;
@ -185,8 +183,8 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
ViewFrustum cameraView = nodeData->getViewFrustom();
std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
avatarList, cameraView,
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
[&](AvatarSharedPointer avatar)->uint64_t{
auto avatarNode = avatarDataToNodes[avatar];

Binary file not shown.

Binary file not shown.

View file

@ -3104,23 +3104,18 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() ||
getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_OVERLAY_ID) {
if (_mouseToOverlays) {
getOverlays().mouseMoveEvent(&mappedEvent);
} else {
getEntities()->mouseMoveEvent(&mappedEvent);
}
getOverlays().mouseMoveEvent(&mappedEvent);
getEntities()->mouseMoveEvent(&mappedEvent);
}
if (!_mouseToOverlays) {
_controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts
}
_controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts
// if one of our scripts have asked to capture this event, then stop processing it
if (_controllerScriptingInterface->isMouseCaptured()) {
return;
}
if (!_mouseToOverlays && _keyboardMouseDevice->isActive()) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->mouseMoveEvent(event);
}
}
@ -3128,7 +3123,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
void Application::mousePressEvent(QMouseEvent* event) {
// Inhibit the menu if the user is using alt-mouse dragging
_altPressed = false;
_mouseToOverlays = false;
auto offscreenUi = DependencyManager::get<OffscreenUi>();
// If we get a mouse press event it means it wasn't consumed by the offscreen UI,
@ -3145,23 +3139,20 @@ void Application::mousePressEvent(QMouseEvent* event) {
event->buttons(), event->modifiers());
if (!_aboutToQuit) {
if (getOverlays().mousePressEvent(&mappedEvent)) {
_mouseToOverlays = true;
} else if (!_controllerScriptingInterface->areEntityClicksCaptured()) {
getOverlays().mousePressEvent(&mappedEvent);
if (!_controllerScriptingInterface->areEntityClicksCaptured()) {
getEntities()->mousePressEvent(&mappedEvent);
}
}
if (!_mouseToOverlays) {
_controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts
}
_controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts
// if one of our scripts have asked to capture this event, then stop processing it
if (_controllerScriptingInterface->isMouseCaptured()) {
return;
}
if (!_mouseToOverlays && hasFocus()) {
if (hasFocus()) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->mousePressEvent(event);
}
@ -3195,23 +3186,18 @@ void Application::mouseReleaseEvent(QMouseEvent* event) {
event->buttons(), event->modifiers());
if (!_aboutToQuit) {
if (_mouseToOverlays) {
getOverlays().mouseReleaseEvent(&mappedEvent);
} else {
getEntities()->mouseReleaseEvent(&mappedEvent);
}
getOverlays().mouseReleaseEvent(&mappedEvent);
getEntities()->mouseReleaseEvent(&mappedEvent);
}
if (!_mouseToOverlays) {
_controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts
}
_controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts
// if one of our scripts have asked to capture this event, then stop processing it
if (_controllerScriptingInterface->isMouseCaptured()) {
return;
}
if (!_mouseToOverlays && hasFocus()) {
if (hasFocus()) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->mouseReleaseEvent(event);
}
@ -3457,7 +3443,7 @@ void Application::idle(float nsecsElapsed) {
#ifdef Q_OS_WIN
static std::once_flag once;
std::call_once(once, [] {
initCpuUsage();
initCpuUsage();
});
vec3 kernelUserAndSystem;

View file

@ -676,8 +676,6 @@ private:
void addAssetToWorldInfoDone(QString modelName);
void addAssetToWorldError(QString modelName, QString errorText);
bool _mouseToOverlays { false };
QQuickItem* _addAssetToWorldMessageBox{ nullptr };
QStringList _addAssetToWorldInfoKeys; // Model name
QStringList _addAssetToWorldInfoMessages; // Info message

View file

@ -351,7 +351,6 @@ void Avatar::simulate(float deltaTime, bool inView) {
_jointDataSimulationRate.increment();
_skeletonModel->simulate(deltaTime, true);
_skeletonModelSimulationRate.increment();
locationChanged(); // joints changed, so if there are any children, update them.
_hasNewJointData = false;
@ -367,8 +366,8 @@ void Avatar::simulate(float deltaTime, bool inView) {
} else {
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
_skeletonModel->simulate(deltaTime, false);
_skeletonModelSimulationRate.increment();
}
_skeletonModelSimulationRate.increment();
}
// update animation for display name fade in/out

View file

@ -157,15 +157,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
lock.unlock();
PerformanceTimer perfTimer("otherAvatars");
uint64_t startTime = usecTimestampNow();
auto avatarMap = getHashCopy();
QList<AvatarSharedPointer> avatarList = avatarMap.values();
ViewFrustum cameraView;
qApp->copyDisplayViewFrustum(cameraView);
std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
avatarList, cameraView,
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
[](AvatarSharedPointer avatar)->uint64_t{
return std::static_pointer_cast<Avatar>(avatar)->getLastRenderUpdateTime();
@ -194,10 +193,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
});
render::PendingChanges pendingChanges;
const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec
const uint64_t MAX_UPDATE_BUDGET = 2000; // usec
uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET;
uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET;
uint64_t startTime = usecTimestampNow();
const uint64_t UPDATE_BUDGET = 2000; // usec
uint64_t updateExpiry = startTime + UPDATE_BUDGET;
int numAvatarsUpdated = 0;
int numAVatarsNotUpdated = 0;
@ -223,7 +221,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY;
uint64_t now = usecTimestampNow();
if (now < renderExpiry) {
if (now < updateExpiry) {
// we're within budget
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
if (inView && avatar->hasNewJointData()) {
@ -232,21 +230,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
avatar->simulate(deltaTime, inView);
avatar->updateRenderItem(pendingChanges);
avatar->setLastRenderUpdateTime(startTime);
} else if (now < maxExpiry) {
// we've spent most of our time budget, but we still simulate() the avatar as it if were out of view
// --> some avatars may freeze until their priority trickles up
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
if (inView && avatar->hasNewJointData()) {
numAVatarsNotUpdated++;
}
avatar->simulate(deltaTime, false);
} else {
// we've spent ALL of our time budget --> bail on the rest of the avatar updates
// we've spent our full time budget --> bail on the rest of the avatar updates
// --> more avatars may freeze until their priority trickles up
// --> some scale or fade animations may glitch
// --> some avatar velocity measurements may be a little off
// HACK: no time simulate, but we will take the time to count how many were tragically missed
// no time simulate, but we take the time to count how many were tragically missed
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
if (!inView) {
break;

View file

@ -95,12 +95,6 @@ void CauterizedModel::createCollisionRenderItemSet() {
Model::createCollisionRenderItemSet();
}
// Called within Model::simulate call, below.
void CauterizedModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
Model::updateRig(deltaTime, parentTransform);
_needsUpdateClusterMatrices = true;
}
void CauterizedModel::updateClusterMatrices() {
PerformanceTimer perfTimer("CauterizedModel::updateClusterMatrices");

View file

@ -37,7 +37,6 @@ public:
void createVisibleRenderItemSet() override;
void createCollisionRenderItemSet() override;
virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override;
virtual void updateClusterMatrices() override;
void updateRenderItems() override;

View file

@ -260,6 +260,11 @@ QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) {
return AvatarData::toByteArrayStateful(dataDetail);
}
void MyAvatar::resetSensorsAndBody() {
qApp->getActiveDisplayPlugin()->resetSensors();
reset(true, false, true);
}
void MyAvatar::centerBody() {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "centerBody");
@ -2483,6 +2488,45 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
}
}
bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) {
auto hipsIndex = getJointIndex("Hips");
if (index != hipsIndex) {
qWarning() << "Pinning is only supported for the hips joint at the moment.";
return false;
}
setPosition(position);
setOrientation(orientation);
_rig->setMaxHipsOffsetLength(0.05f);
auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index);
if (it == _pinnedJoints.end()) {
_pinnedJoints.push_back(index);
}
return true;
}
bool MyAvatar::clearPinOnJoint(int index) {
auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index);
if (it != _pinnedJoints.end()) {
_pinnedJoints.erase(it);
auto hipsIndex = getJointIndex("Hips");
if (index == hipsIndex) {
_rig->setMaxHipsOffsetLength(FLT_MAX);
}
return true;
}
return false;
}
float MyAvatar::getIKErrorOnLastSolve() const {
return _rig->getIKErrorOnLastSolve();
}
// thread-safe
void MyAvatar::addHoldAction(AvatarActionHold* holdAction) {
std::lock_guard<std::mutex> guard(_holdActionsMutex);

View file

@ -99,6 +99,7 @@ public:
void reset(bool andRecenter = false, bool andReload = true, bool andHead = true);
Q_INVOKABLE void resetSensorsAndBody();
Q_INVOKABLE void centerBody(); // thread-safe
Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe
@ -216,6 +217,11 @@ public:
virtual void clearJointData(int index) override;
virtual void clearJointsData() override;
Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation);
Q_INVOKABLE bool clearPinOnJoint(int index);
Q_INVOKABLE float getIKErrorOnLastSolve() const;
Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString());
Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; }
Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; }
@ -527,6 +533,8 @@ private:
bool didTeleport();
bool getIsAway() const { return _isAway; }
void setAway(bool value);
std::vector<int> _pinnedJoints;
};
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);

View file

@ -166,7 +166,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
_rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
// evaluate AnimGraph animation and update jointStates.
CauterizedModel::updateRig(deltaTime, parentTransform);
Model::updateRig(deltaTime, parentTransform);
Rig::EyeParameters eyeParams;
eyeParams.worldHeadOrientation = headParams.worldHeadOrientation;
@ -179,7 +179,9 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
_rig->updateFromEyeParameters(eyeParams);
} else {
CauterizedModel::updateRig(deltaTime, parentTransform);
// no need to call Model::updateRig() because otherAvatars get their joint state
// copied directly from AvtarData::_jointData (there are no Rig animations to blend)
_needsUpdateClusterMatrices = true;
// This is a little more work than we really want.
//

View file

@ -345,23 +345,14 @@ OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) {
const float LARGE_NEGATIVE_FLOAT = -9999999;
glm::vec3 origin(pointCopy.x, pointCopy.y, LARGE_NEGATIVE_FLOAT);
glm::vec3 direction(0, 0, 1);
// BoxFace thisFace;
glm::vec3 thisSurfaceNormal;
// float distance;
unsigned int bestStackOrder = 0;
OverlayID bestOverlayID = UNKNOWN_OVERLAY_ID;
while (i.hasNext()) {
i.next();
OverlayID thisID = i.key();
if (i.value()->is3D()) {
// auto thisOverlay = std::dynamic_pointer_cast<Base3DOverlay>(i.value());
// if (thisOverlay && !thisOverlay->getIgnoreRayIntersection()) {
// if (thisOverlay->findRayIntersection(origin, direction, distance, thisFace, thisSurfaceNormal)) {
// return thisID;
// }
// }
} else {
if (!i.value()->is3D()) {
auto thisOverlay = std::dynamic_pointer_cast<Overlay2D>(i.value());
if (thisOverlay && thisOverlay->getVisible() && thisOverlay->isLoaded() &&
thisOverlay->getBoundingRect().contains(pointCopy.x, pointCopy.y, false)) {

View file

@ -189,6 +189,7 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const std::vector<I
}
}
}
_maxErrorOnLastSolve = maxError;
// finally set the relative rotation of each tip to agree with absolute target rotation
for (auto& target: targets) {
@ -268,13 +269,13 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
glm::quat deltaRotation;
if (targetType == IKTarget::Type::RotationAndPosition ||
targetType == IKTarget::Type::HipsRelativeRotationAndPosition) {
targetType == IKTarget::Type::HipsRelativeRotationAndPosition) {
// compute the swing that would get get tip closer
glm::vec3 targetLine = target.getTranslation() - jointPosition;
const float MIN_AXIS_LENGTH = 1.0e-4f;
RotationConstraint* constraint = getConstraint(pivotIndex);
if (constraint && constraint->isLowerSpine()) {
if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) {
// for these types of targets we only allow twist at the lower-spine
// (this prevents the hand targets from bending the spine too much and thereby driving the hips too far)
glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans();
@ -300,8 +301,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
const float MIN_ADJUSTMENT_ANGLE = 1.0e-4f;
if (angle > MIN_ADJUSTMENT_ANGLE) {
// reduce angle by a fraction (for stability)
const float FRACTION = 0.5f;
angle *= FRACTION;
const float STABILITY_FRACTION = 0.5f;
angle *= STABILITY_FRACTION;
deltaRotation = glm::angleAxis(angle, axis);
// The swing will re-orient the tip but there will tend to be be a non-zero delta between the tip's
@ -323,7 +324,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe
glm::vec3 axis = glm::normalize(deltaRotation * leverArm);
swingTwistDecomposition(missingRotation, axis, swingPart, twistPart);
float dotSign = copysignf(1.0f, twistPart.w);
deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, FRACTION)) * deltaRotation;
const float LIMIT_LEAK_FRACTION = 0.1f;
deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation;
}
}
}
@ -486,7 +488,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
// measure new _hipsOffset for next frame
// by looking for discrepancies between where a targeted endEffector is
// and where it wants to be (after IK solutions are done)
glm::vec3 newHipsOffset = Vectors::ZERO;
// use weighted average between HMD and other targets
float HMD_WEIGHT = 10.0f;
float OTHER_WEIGHT = 1.0f;
float totalWeight = 0.0f;
glm::vec3 additionalHipsOffset = Vectors::ZERO;
for (auto& target: targets) {
int targetIndex = target.getIndex();
if (targetIndex == _headIndex && _headIndex != -1) {
@ -497,32 +505,61 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual);
totalWeight += OTHER_WEIGHT;
} else if (target.getType() == IKTarget::Type::HmdHead) {
// we want to shift the hips to bring the head to its designated position
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
_hipsOffset += target.getTranslation() - actual;
// and ignore all other targets
newHipsOffset = _hipsOffset;
break;
glm::vec3 thisOffset = target.getTranslation() - actual;
glm::vec3 futureHipsOffset = _hipsOffset + thisOffset;
if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) {
// it is imperative to shift the hips and bring the head to its designated position
// so we slam newHipsOffset here and ignore all other targets
additionalHipsOffset = futureHipsOffset - _hipsOffset;
totalWeight = 0.0f;
break;
} else {
additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual);
totalWeight += HMD_WEIGHT;
}
}
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition);
totalWeight += OTHER_WEIGHT;
}
}
if (totalWeight > 1.0f) {
additionalHipsOffset /= totalWeight;
}
// Add downward pressure on the hips
additionalHipsOffset *= 0.95f;
additionalHipsOffset -= 1.0f;
// smooth transitions by relaxing _hipsOffset toward the new value
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f;
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
_hipsOffset += additionalHipsOffset * tau;
// clamp the hips offset
float hipsOffsetLength = glm::length(_hipsOffset);
if (hipsOffsetLength > _maxHipsOffsetLength) {
_hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength;
}
}
}
}
return _relativePoses;
}
void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) {
// manually adjust scale here
const float METERS_TO_CENTIMETERS = 100.0f;
_maxHipsOffsetLength = METERS_TO_CENTIMETERS * maxLength;
}
void AnimInverseKinematics::clearIKJointLimitHistory() {
for (auto& pair : _constraints) {
pair.second->clearHistory();
@ -740,7 +777,7 @@ void AnimInverseKinematics::initConstraints() {
stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST);
std::vector<float> minDots;
const float MAX_SPINE_SWING = PI / 14.0f;
const float MAX_SPINE_SWING = PI / 10.0f;
minDots.push_back(cosf(MAX_SPINE_SWING));
stConstraint->setSwingLimits(minDots);
if (0 == baseName.compare("Spine1", Qt::CaseSensitive)
@ -776,11 +813,11 @@ void AnimInverseKinematics::initConstraints() {
} else if (0 == baseName.compare("Head", Qt::CaseSensitive)) {
SwingTwistConstraint* stConstraint = new SwingTwistConstraint();
stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot());
const float MAX_HEAD_TWIST = PI / 9.0f;
const float MAX_HEAD_TWIST = PI / 6.0f;
stConstraint->setTwistLimits(-MAX_HEAD_TWIST, MAX_HEAD_TWIST);
std::vector<float> minDots;
const float MAX_HEAD_SWING = PI / 10.0f;
const float MAX_HEAD_SWING = PI / 6.0f;
minDots.push_back(cosf(MAX_HEAD_SWING));
stConstraint->setSwingLimits(minDots);

View file

@ -39,6 +39,10 @@ public:
void clearIKJointLimitHistory();
void setMaxHipsOffsetLength(float maxLength);
float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; }
protected:
void computeTargets(const AnimVariantMap& animVars, std::vector<IKTarget>& targets, const AnimPoseVec& underPoses);
void solveWithCyclicCoordinateDescent(const std::vector<IKTarget>& targets);
@ -83,6 +87,7 @@ protected:
// experimental data for moving hips during IK
glm::vec3 _hipsOffset { Vectors::ZERO };
float _maxHipsOffsetLength{ FLT_MAX };
int _headIndex { -1 };
int _hipsIndex { -1 };
int _hipsParentIndex { -1 };
@ -90,6 +95,8 @@ protected:
// _maxTargetIndex is tracked to help optimize the recalculation of absolute poses
// during the the cyclic coordinate descent algorithm
int _maxTargetIndex { 0 };
float _maxErrorOnLastSolve { FLT_MAX };
};
#endif // hifi_AnimInverseKinematics_h

View file

@ -319,6 +319,39 @@ void Rig::clearIKJointLimitHistory() {
}
}
void Rig::setMaxHipsOffsetLength(float maxLength) {
_maxHipsOffsetLength = maxLength;
if (_animNode) {
_animNode->traverse([&](AnimNode::Pointer node) {
auto ikNode = std::dynamic_pointer_cast<AnimInverseKinematics>(node);
if (ikNode) {
ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength);
}
return true;
});
}
}
float Rig::getMaxHipsOffsetLength() const {
return _maxHipsOffsetLength;
}
float Rig::getIKErrorOnLastSolve() const {
float result = 0.0f;
if (_animNode) {
_animNode->traverse([&](AnimNode::Pointer node) {
auto ikNode = std::dynamic_pointer_cast<AnimInverseKinematics>(node);
if (ikNode) {
result = ikNode->getMaxErrorOnLastSolve();
}
return true;
});
}
return result;
}
int Rig::getJointParentIndex(int childIndex) const {
if (_animSkeleton && isIndexValid(childIndex)) {
return _animSkeleton->getParentIndex(childIndex);
@ -1274,39 +1307,50 @@ void Rig::copyJointsIntoJointData(QVector<JointData>& jointDataVec) const {
void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
PerformanceTimer perfTimer("copyJoints");
PROFILE_RANGE(simulation_animation_detail, "copyJoints");
if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._relativePoses.size()) {
// make a vector of rotations in absolute-geometry-frame
const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
std::vector<glm::quat> rotations;
rotations.reserve(absoluteDefaultPoses.size());
const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
for (int i = 0; i < jointDataVec.size(); i++) {
const JointData& data = jointDataVec.at(i);
if (data.rotationSet) {
// JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
rotations.push_back(rigToGeometryRot * data.rotation);
} else {
rotations.push_back(absoluteDefaultPoses[i].rot());
}
}
if (!_animSkeleton) {
return;
}
if (jointDataVec.size() != (int)_internalPoseSet._relativePoses.size()) {
// animations haven't fully loaded yet.
_internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
}
// convert rotations from absolute to parent relative.
_animSkeleton->convertAbsoluteRotationsToRelative(rotations);
// store new relative poses
const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
for (int i = 0; i < jointDataVec.size(); i++) {
const JointData& data = jointDataVec.at(i);
_internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
_internalPoseSet._relativePoses[i].rot() = rotations[i];
if (data.translationSet) {
// JointData translations are in scaled relative-frame so we scale back to regular relative-frame
_internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
} else {
_internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
}
// make a vector of rotations in absolute-geometry-frame
const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
std::vector<glm::quat> rotations;
rotations.reserve(absoluteDefaultPoses.size());
const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
for (int i = 0; i < jointDataVec.size(); i++) {
const JointData& data = jointDataVec.at(i);
if (data.rotationSet) {
// JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
rotations.push_back(rigToGeometryRot * data.rotation);
} else {
rotations.push_back(absoluteDefaultPoses[i].rot());
}
}
// convert rotations from absolute to parent relative.
_animSkeleton->convertAbsoluteRotationsToRelative(rotations);
// store new relative poses
const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
for (int i = 0; i < jointDataVec.size(); i++) {
const JointData& data = jointDataVec.at(i);
_internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
_internalPoseSet._relativePoses[i].rot() = rotations[i];
if (data.translationSet) {
// JointData translations are in scaled relative-frame so we scale back to regular relative-frame
_internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
} else {
_internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
}
}
// build absolute poses and copy to externalPoseSet
buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
QWriteLocker writeLock(&_externalPoseSetLock);
_externalPoseSet = _internalPoseSet;
}
void Rig::computeAvatarBoundingCapsule(

View file

@ -104,6 +104,10 @@ public:
void clearJointAnimationPriority(int index);
void clearIKJointLimitHistory();
void setMaxHipsOffsetLength(float maxLength);
float getMaxHipsOffsetLength() const;
float getIKErrorOnLastSolve() const;
int getJointParentIndex(int childIndex) const;
@ -318,6 +322,8 @@ protected:
bool _enabledAnimations { true };
mutable uint32_t _jointNameWarningCount { 0 };
float _maxHipsOffsetLength { 1.0f };
float _maxErrorOnLastSolve { 0.0f };
private:
QMap<int, StateHandler> _stateHandlers;

View file

@ -2324,61 +2324,57 @@ float AvatarData::_avatarSortCoefficientSize { 0.5f };
float AvatarData::_avatarSortCoefficientCenter { 0.25 };
float AvatarData::_avatarSortCoefficientAge { 1.0f };
std::priority_queue<AvatarPriority> AvatarData::sortAvatars(
QList<AvatarSharedPointer> avatarList,
const ViewFrustum& cameraView,
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
std::function<float(AvatarSharedPointer)> getBoundingRadius,
std::function<bool(AvatarSharedPointer)> shouldIgnore) {
void AvatarData::sortAvatars(
QList<AvatarSharedPointer> avatarList,
const ViewFrustum& cameraView,
std::priority_queue<AvatarPriority>& sortedAvatarsOut,
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
std::function<float(AvatarSharedPointer)> getBoundingRadius,
std::function<bool(AvatarSharedPointer)> shouldIgnore) {
uint64_t startTime = usecTimestampNow();
PROFILE_RANGE(simulation, "sort");
uint64_t now = usecTimestampNow();
glm::vec3 frustumCenter = cameraView.getPosition();
const glm::vec3& forward = cameraView.getDirection();
for (int32_t i = 0; i < avatarList.size(); ++i) {
const auto& avatar = avatarList.at(i);
std::priority_queue<AvatarPriority> sortedAvatars;
{
PROFILE_RANGE(simulation, "sort");
for (int32_t i = 0; i < avatarList.size(); ++i) {
const auto& avatar = avatarList.at(i);
if (shouldIgnore(avatar)) {
continue;
}
// priority = weighted linear combination of:
// (a) apparentSize
// (b) proximity to center of view
// (c) time since last update
glm::vec3 avatarPosition = avatar->getPosition();
glm::vec3 offset = avatarPosition - frustumCenter;
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
// FIXME - AvatarData has something equivolent to this
float radius = getBoundingRadius(avatar);
const glm::vec3& forward = cameraView.getDirection();
float apparentSize = 2.0f * radius / distance;
float cosineAngle = glm::length(glm::dot(offset, forward) * forward) / distance;
float age = (float)(startTime - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
// NOTE: we are adding values of different units to get a single measure of "priority".
// Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
// These weights are pure magic tuning and should be hard coded in the relation below,
// but are currently exposed for anyone who would like to explore fine tuning:
float priority = _avatarSortCoefficientSize * apparentSize
+ _avatarSortCoefficientCenter * cosineAngle
+ _avatarSortCoefficientAge * age;
// decrement priority of avatars outside keyhole
if (distance > cameraView.getCenterRadius()) {
if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
priority += OUT_OF_VIEW_PENALTY;
}
}
sortedAvatars.push(AvatarPriority(avatar, priority));
if (shouldIgnore(avatar)) {
continue;
}
// priority = weighted linear combination of:
// (a) apparentSize
// (b) proximity to center of view
// (c) time since last update
glm::vec3 avatarPosition = avatar->getPosition();
glm::vec3 offset = avatarPosition - frustumCenter;
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
// FIXME - AvatarData has something equivolent to this
float radius = getBoundingRadius(avatar);
float apparentSize = 2.0f * radius / distance;
float cosineAngle = glm::dot(offset, forward) / distance;
float age = (float)(now - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
// NOTE: we are adding values of different units to get a single measure of "priority".
// Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
// These weights are pure magic tuning and should be hard coded in the relation below,
// but are currently exposed for anyone who would like to explore fine tuning:
float priority = _avatarSortCoefficientSize * apparentSize
+ _avatarSortCoefficientCenter * cosineAngle
+ _avatarSortCoefficientAge * age;
// decrement priority of avatars outside keyhole
if (distance > cameraView.getCenterRadius()) {
if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
priority += OUT_OF_VIEW_PENALTY;
}
}
sortedAvatarsOut.push(AvatarPriority(avatar, priority));
}
return sortedAvatars;
}
QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) {

View file

@ -597,9 +597,10 @@ public:
static const float OUT_OF_VIEW_PENALTY;
static std::priority_queue<AvatarPriority> sortAvatars(
static void sortAvatars(
QList<AvatarSharedPointer> avatarList,
const ViewFrustum& cameraView,
std::priority_queue<AvatarPriority>& sortedAvatarsOut,
std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
std::function<float(AvatarSharedPointer)> getBoundingRadius,
std::function<bool(AvatarSharedPointer)> shouldIgnore);

View file

@ -336,8 +336,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
const double pStrength = 2.0;
int width = image.width();
int height = image.height();
// THe end result image for normal map is RGBA32 even though the A is not used
QImage result(width, height, QImage::Format_RGBA8888);
QImage result(width, height, QImage::Format_RGB888);
for (int i = 0; i < width; i++) {
const int iNextClamped = clampPixelCoordinate(i + 1, width - 1);
@ -377,21 +376,19 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
glm::normalize(v);
// convert to rgb from the value obtained computing the filter
QRgb qRgbValue = qRgb(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z));
QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0);
result.setPixel(i, j, qRgbValue);
}
}
gpu::Texture* theTexture = nullptr;
if ((image.width() > 0) && (image.height() > 0)) {
gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA);
gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA);
gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)));
theTexture->setSource(srcImageName);
theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits());
generateMips(theTexture, image, formatMip, true);
}
return theTexture;

Binary file not shown.

View file

@ -17,12 +17,20 @@ var NUMBER_OF_STEPS = 6;
var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx");
var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx");
var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx");
var TARGET_MODEL_DIMENSIONS = {
x: 1.15,
y: 0.5,
z: 1.15
};
var COLORS_TELEPORT_SEAT = {
red: 255,
green: 0,
blue: 170
}
var COLORS_TELEPORT_CAN_TELEPORT = {
red: 97,
green: 247,
@ -35,29 +43,30 @@ var COLORS_TELEPORT_CANNOT_TELEPORT = {
blue: 141
};
var COLORS_TELEPORT_TOO_CLOSE = {
var COLORS_TELEPORT_CANCEL = {
red: 255,
green: 184,
blue: 73
};
var TELEPORT_CANCEL_RANGE = 1;
var USE_COOL_IN = true;
var COOL_IN_DURATION = 500;
const handInfo = {
right: {
controllerInput: Controller.Standard.RightHand
},
left: {
controllerInput: Controller.Standard.LeftHand
}
};
function ThumbPad(hand) {
this.hand = hand;
var _thisPad = this;
this.buttonPress = function(value) {
_thisPad.buttonValue = value;
if (value === 0) {
if (activationTimeout !== null) {
Script.clearTimeout(activationTimeout);
activationTimeout = null;
}
}
};
}
@ -67,7 +76,6 @@ function Trigger(hand) {
this.buttonPress = function(value) {
_this.buttonValue = value;
};
this.down = function() {
@ -78,347 +86,224 @@ function Trigger(hand) {
var coolInTimeout = null;
var TELEPORTER_STATES = {
IDLE: 'idle',
COOL_IN: 'cool_in',
TARGETTING_INVALID: 'targetting_invalid',
}
var TARGET = {
NONE: 'none', // Not currently targetting anything
INVISIBLE: 'invisible', // The current target is an invvsible surface
INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.)
SURFACE: 'surface', // The current target is a valid surface
SEAT: 'seat', // The current target is a seat
}
function Teleporter() {
var _this = this;
this.intersection = null;
this.rightOverlayLine = null;
this.leftOverlayLine = null;
this.targetOverlay = null;
this.cancelOverlay = null;
this.updateConnected = null;
this.smoothArrivalInterval = null;
this.teleportHand = null;
this.tooClose = false;
this.inCoolIn = false;
this.active = false;
this.state = TELEPORTER_STATES.IDLE;
this.currentTarget = TARGET.INVALID;
this.initialize = function() {
this.createMappings();
this.overlayLines = {
left: null,
right: null,
};
this.updateConnected = null;
this.activeHand = null;
this.createMappings = function() {
teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName);
this.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random();
this.teleportMappingInternal = Controller.newMapping(this.telporterMappingInternalName);
Controller.enableMapping(teleporter.telporterMappingInternalName);
// Setup overlays
this.cancelOverlay = Overlays.addOverlay("model", {
url: TOO_CLOSE_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
visible: false
});
this.targetOverlay = Overlays.addOverlay("model", {
url: TARGET_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
visible: false
});
this.seatOverlay = Overlays.addOverlay("model", {
url: SEAT_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
visible: false
});
this.enableMappings = function() {
Controller.enableMapping(this.telporterMappingInternalName);
};
this.disableMappings = function() {
Controller.disableMapping(teleporter.telporterMappingInternalName);
};
this.enterTeleportMode = function(hand) {
this.cleanup = function() {
this.disableMappings();
Overlays.deleteOverlay(this.targetOverlay);
this.targetOverlay = null;
Overlays.deleteOverlay(this.cancelOverlay);
this.cancelOverlay = null;
Overlays.deleteOverlay(this.seatOverlay);
this.seatOverlay = null;
this.deleteOverlayBeams();
if (this.updateConnected === true) {
Script.update.disconnect(this, this.update);
}
};
this.enterTeleportMode = function(hand) {
if (inTeleportMode === true) {
return;
}
if (isDisabled === 'both') {
if (isDisabled === 'both' || isDisabled === hand) {
return;
}
inTeleportMode = true;
this.inCoolIn = true;
if (coolInTimeout !== null) {
Script.clearTimeout(coolInTimeout);
}
this.state = TELEPORTER_STATES.COOL_IN;
coolInTimeout = Script.setTimeout(function() {
_this.inCoolIn = false;
if (_this.state === TELEPORTER_STATES.COOL_IN) {
_this.state = TELEPORTER_STATES.TARGETTING;
}
}, COOL_IN_DURATION)
if (this.smoothArrivalInterval !== null) {
Script.clearInterval(this.smoothArrivalInterval);
}
if (activationTimeout !== null) {
Script.clearInterval(activationTimeout);
}
this.teleportHand = hand;
this.initialize();
Script.update.connect(this.update);
this.activeHand = hand;
this.enableMappings();
Script.update.connect(this, this.update);
this.updateConnected = true;
};
this.createTargetOverlay = function(visible) {
if (visible == undefined) {
visible = true;
}
if (_this.targetOverlay !== null) {
return;
}
var targetOverlayProps = {
url: TARGET_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
visible: visible
};
_this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps);
};
this.createCancelOverlay = function(visible) {
if (visible == undefined) {
visible = true;
}
if (_this.cancelOverlay !== null) {
return;
}
var cancelOverlayProps = {
url: TOO_CLOSE_MODEL_URL,
dimensions: TARGET_MODEL_DIMENSIONS,
visible: visible
};
_this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps);
};
this.deleteCancelOverlay = function() {
if (this.cancelOverlay === null) {
return;
}
Overlays.deleteOverlay(this.cancelOverlay);
this.cancelOverlay = null;
}
this.hideCancelOverlay = function() {
if (this.cancelOverlay === null) {
return;
}
this.intersection = null;
Overlays.editOverlay(this.cancelOverlay, { visible: false });
}
this.showCancelOverlay = function() {
if (this.cancelOverlay === null) {
return this.createCancelOverlay();
}
Overlays.editOverlay(this.cancelOverlay, { visible: true });
}
this.deleteTargetOverlay = function() {
if (this.targetOverlay === null) {
return;
}
Overlays.deleteOverlay(this.targetOverlay);
this.intersection = null;
this.targetOverlay = null;
}
this.hideTargetOverlay = function() {
if (this.targetOverlay === null) {
return;
}
this.intersection = null;
Overlays.editOverlay(this.targetOverlay, { visible: false });
}
this.showTargetOverlay = function() {
if (this.targetOverlay === null) {
return this.createTargetOverlay();
}
Overlays.editOverlay(this.targetOverlay, { visible: true });
}
this.turnOffOverlayBeams = function() {
this.rightOverlayOff();
this.leftOverlayOff();
}
this.exitTeleportMode = function(value) {
if (activationTimeout !== null) {
Script.clearTimeout(activationTimeout);
activationTimeout = null;
}
if (this.updateConnected === true) {
Script.update.disconnect(this.update);
Script.update.disconnect(this, this.update);
}
this.disableMappings();
this.turnOffOverlayBeams();
this.deleteOverlayBeams();
this.hideTargetOverlay();
this.hideCancelOverlay();
this.updateConnected = null;
this.inCoolIn = false;
this.state = TELEPORTER_STATES.IDLE;
inTeleportMode = false;
};
this.update = function() {
if (isDisabled === 'both') {
return;
}
if (teleporter.teleportHand === 'left') {
if (isDisabled === 'left') {
return;
this.deleteOverlayBeams = function() {
for (key in this.overlayLines) {
if (this.overlayLines[key] !== null) {
Overlays.deleteOverlay(this.overlayLines[key]);
this.overlayLines[key] = null;
}
teleporter.leftRay();
if ((leftPad.buttonValue === 0) && inTeleportMode === true) {
if (_this.inCoolIn === true) {
_this.exitTeleportMode();
_this.hideTargetOverlay();
_this.hideCancelOverlay();
} else {
_this.teleport();
}
return;
}
} else {
if (isDisabled === 'right') {
return;
}
teleporter.rightRay();
if ((rightPad.buttonValue === 0) && inTeleportMode === true) {
if (_this.inCoolIn === true) {
_this.exitTeleportMode();
_this.hideTargetOverlay();
_this.hideCancelOverlay();
} else {
_this.teleport();
}
return;
}
}
};
this.rightRay = function() {
var pose = Controller.getPoseValue(Controller.Standard.RightHand);
var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {
x: 1,
y: 0,
z: 0
}));
var rightPickRay = {
origin: rightPosition,
direction: Quat.getUp(rightRotation),
};
this.rightPickRay = rightPickRay;
var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 50));
var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity], true, true);
if (rightIntersection.intersects) {
if (this.tooClose === true) {
this.hideTargetOverlay();
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
if (this.cancelOverlay !== null) {
this.updateCancelOverlay(rightIntersection);
} else {
this.createCancelOverlay();
}
} else {
if (this.inCoolIn === true) {
this.hideTargetOverlay();
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
if (this.cancelOverlay !== null) {
this.updateCancelOverlay(rightIntersection);
} else {
this.createCancelOverlay();
}
} else {
this.hideCancelOverlay();
this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
if (this.targetOverlay !== null) {
this.updateTargetOverlay(rightIntersection);
} else {
this.createTargetOverlay();
}
}
}
} else {
this.hideTargetOverlay();
this.rightLineOn(rightPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT);
}
}
this.update = function() {
if (_this.state === TELEPORTER_STATES.IDLE) {
return;
}
this.leftRay = function() {
var pose = Controller.getPoseValue(Controller.Standard.LeftHand);
var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
// Get current hand pose information so that we can get the direction of the teleport beam
var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput);
var handPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition();
var handRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) :
Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {
x: 1,
y: 0,
z: 0
}));
var leftPickRay = {
origin: leftPosition,
direction: Quat.getUp(leftRotation),
var pickRay = {
origin: handPosition,
direction: Quat.getUp(handRotation),
};
this.leftPickRay = leftPickRay;
// We do up to 2 ray picks to find a teleport location.
// There are 2 types of teleport locations we are interested in:
// 1. A visible floor. This can be any entity surface that points within some degree of "up"
// 2. A seat. The seat can be visible or invisible.
//
// * In the first pass we pick against visible and invisible entities so that we can find invisible seats.
// We might hit an invisible entity that is not a seat, so we need to do a second pass.
// * In the second pass we pick against visible entities only.
//
var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true);
var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 50));
var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity], true, true);
if (leftIntersection.intersects) {
if (this.tooClose === true) {
this.hideTargetOverlay();
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
if (this.cancelOverlay !== null) {
this.updateCancelOverlay(leftIntersection);
} else {
this.createCancelOverlay();
}
} else {
if (this.inCoolIn === true) {
this.hideTargetOverlay();
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE);
if (this.cancelOverlay !== null) {
this.updateCancelOverlay(leftIntersection);
} else {
this.createCancelOverlay();
}
} else {
this.hideCancelOverlay();
this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
if (this.targetOverlay !== null) {
this.updateTargetOverlay(leftIntersection);
} else {
this.createTargetOverlay();
}
}
}
} else {
var teleportLocationType = getTeleportTargetType(intersection);
if (teleportLocationType === TARGET.INVISIBLE) {
intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true);
teleportLocationType = getTeleportTargetType(intersection);
}
if (teleportLocationType === TARGET.NONE) {
this.hideTargetOverlay();
this.leftLineOn(leftPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT);
this.hideCancelOverlay();
this.hideSeatOverlay();
var farPosition = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, 50));
this.updateLineOverlay(_this.activeHand, pickRay.origin, farPosition, COLORS_TELEPORT_CANNOT_TELEPORT);
} else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) {
this.hideTargetOverlay();
this.hideSeatOverlay();
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL);
this.updateDestinationOverlay(this.cancelOverlay, intersection);
} else if (teleportLocationType === TARGET.SURFACE) {
if (this.state === TELEPORTER_STATES.COOL_IN) {
this.hideTargetOverlay();
this.hideSeatOverlay();
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL);
this.updateDestinationOverlay(this.cancelOverlay, intersection);
} else {
this.hideCancelOverlay();
this.hideSeatOverlay();
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CAN_TELEPORT);
this.updateDestinationOverlay(this.targetOverlay, intersection);
}
} else if (teleportLocationType === TARGET.SEAT) {
this.hideCancelOverlay();
this.hideTargetOverlay();
this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_SEAT);
this.updateDestinationOverlay(this.seatOverlay, intersection);
}
if (((_this.activeHand == 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) {
this.exitTeleportMode();
this.hideCancelOverlay();
this.hideTargetOverlay();
this.hideSeatOverlay();
if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) {
// Do nothing
} else if (teleportLocationType === TARGET.SEAT) {
Entities.callEntityMethod(intersection.entityID, 'sit');
} else if (teleportLocationType === TARGET.SURFACE) {
var offset = getAvatarFootOffset();
intersection.intersection.y += offset;
MyAvatar.position = intersection.intersection;
HMD.centerUI();
}
}
};
this.rightLineOn = function(closePoint, farPoint, color) {
if (this.rightOverlayLine === null) {
this.updateLineOverlay = function(hand, closePoint, farPoint, color) {
if (this.overlayLines[hand] === null) {
var lineProperties = {
start: closePoint,
end: farPoint,
@ -431,10 +316,10 @@ function Teleporter() {
glow: 1.0
};
this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties);
this.overlayLines[hand] = Overlays.addOverlay("line3d", lineProperties);
} else {
var success = Overlays.editOverlay(this.rightOverlayLine, {
var success = Overlays.editOverlay(this.overlayLines[hand], {
start: closePoint,
end: farPoint,
color: color
@ -442,47 +327,19 @@ function Teleporter() {
}
};
this.leftLineOn = function(closePoint, farPoint, color) {
if (this.leftOverlayLine === null) {
var lineProperties = {
ignoreRayIntersection: true,
start: closePoint,
end: farPoint,
color: color,
visible: true,
alpha: 1,
solid: true,
glow: 1.0,
drawInFront: true
};
this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties);
} else {
var success = Overlays.editOverlay(this.leftOverlayLine, {
start: closePoint,
end: farPoint,
color: color
});
}
};
this.rightOverlayOff = function() {
if (this.rightOverlayLine !== null) {
Overlays.deleteOverlay(this.rightOverlayLine);
this.rightOverlayLine = null;
}
this.hideCancelOverlay = function() {
Overlays.editOverlay(this.cancelOverlay, { visible: false });
};
this.leftOverlayOff = function() {
if (this.leftOverlayLine !== null) {
Overlays.deleteOverlay(this.leftOverlayLine);
this.leftOverlayLine = null;
}
this.hideTargetOverlay = function() {
Overlays.editOverlay(this.targetOverlay, { visible: false });
};
this.updateTargetOverlay = function(intersection) {
_this.intersection = intersection;
this.hideSeatOverlay = function() {
Overlays.editOverlay(this.seatOverlay, { visible: false });
};
this.updateDestinationOverlay = function(overlayID, intersection) {
var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP);
var euler = Quat.safeEulerAngles(rotation);
var position = {
@ -491,115 +348,15 @@ function Teleporter() {
z: intersection.intersection.z
};
this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal);
var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0);
Overlays.editOverlay(this.targetOverlay, {
Overlays.editOverlay(overlayID, {
visible: true,
position: position,
rotation: towardUs
});
};
this.updateCancelOverlay = function(intersection) {
_this.intersection = intersection;
var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP);
var euler = Quat.safeEulerAngles(rotation);
var position = {
x: intersection.intersection.x,
y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2,
z: intersection.intersection.z
};
this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal);
var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0);
Overlays.editOverlay(this.cancelOverlay, {
visible: true,
position: position,
rotation: towardUs
});
};
this.triggerHaptics = function() {
var hand = this.teleportHand === 'left' ? 0 : 1;
var haptic = Controller.triggerShortHapticPulse(0.2, hand);
};
this.teleport = function(value) {
if (value === undefined) {
this.exitTeleportMode();
}
if (this.intersection !== null) {
if (this.tooClose === true) {
this.exitTeleportMode();
this.hideCancelOverlay();
return;
}
var offset = getAvatarFootOffset();
this.intersection.intersection.y += offset;
this.exitTeleportMode();
// Disable smooth arrival, possibly temporarily
//this.smoothArrival();
MyAvatar.position = _this.intersection.intersection;
_this.hideTargetOverlay();
_this.hideCancelOverlay();
HMD.centerUI();
}
};
this.findMidpoint = function(start, end) {
var xy = Vec3.sum(start, end);
var midpoint = Vec3.multiply(0.5, xy);
return midpoint
};
this.getArrivalPoints = function(startPoint, endPoint) {
var arrivalPoints = [];
var i;
var lastPoint;
for (i = 0; i < NUMBER_OF_STEPS; i++) {
if (i === 0) {
lastPoint = startPoint;
}
var newPoint = _this.findMidpoint(lastPoint, endPoint);
lastPoint = newPoint;
arrivalPoints.push(newPoint);
}
arrivalPoints.push(endPoint);
return arrivalPoints;
};
this.smoothArrival = function() {
_this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection);
_this.smoothArrivalInterval = Script.setInterval(function() {
if (_this.arrivalPoints.length === 0) {
Script.clearInterval(_this.smoothArrivalInterval);
HMD.centerUI();
return;
}
var landingPoint = _this.arrivalPoints.shift();
MyAvatar.position = landingPoint;
if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) {
_this.hideTargetOverlay();
_this.hideCancelOverlay();
}
}, SMOOTH_ARRIVAL_SPACING);
}
this.createTargetOverlay(false);
this.createCancelOverlay(false);
}
//related to repositioning the avatar after you teleport
@ -611,20 +368,16 @@ function getAvatarFootOffset() {
var jointName = d.joint;
if (jointName === "RightUpLeg") {
upperLeg = d.translation.y;
}
if (jointName === "RightLeg") {
} else if (jointName === "RightLeg") {
lowerLeg = d.translation.y;
}
if (jointName === "RightFoot") {
} else if (jointName === "RightFoot") {
foot = d.translation.y;
}
if (jointName === "RightToeBase") {
} else if (jointName === "RightToeBase") {
toe = d.translation.y;
}
if (jointName === "RightToe_End") {
} else if (jointName === "RightToe_End") {
toeTop = d.translation.y;
}
})
});
var offset = upperLeg + lowerLeg + foot + toe + toeTop;
offset = offset / 100;
@ -655,7 +408,6 @@ var rightTrigger = new Trigger('right');
var mappingName, teleportMapping;
var activationTimeout = null;
var TELEPORT_DELAY = 0;
function isMoving() {
@ -668,17 +420,44 @@ function isMoving() {
}
};
function parseJSON(json) {
try {
return JSON.parse(json);
} catch (e) {
return undefined;
}
}
// When determininig whether you can teleport to a location, the normal of the
// point that is being intersected with is looked at. If this normal is more
// than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then
// you can't teleport there.
var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
function isValidTeleportLocation(position, surfaceNormal) {
const MAX_ANGLE_FROM_UP_TO_TELEPORT = 70;
function getTeleportTargetType(intersection) {
if (!intersection.intersects) {
return TARGET.NONE;
}
var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']);
var data = parseJSON(props.userData);
if (data !== undefined && data.seat !== undefined) {
return TARGET.SEAT;
}
if (!props.visible) {
return TARGET.INVISIBLE;
}
var surfaceNormal = intersection.surfaceNormal;
var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z);
var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI);
return angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE;
if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) ||
Vec3.distance(MyAvatar.position, intersection.intersection) <= TELEPORT_CANCEL_RANGE) {
return TARGET.INVALID;
} else {
return TARGET.SURFACE;
}
};
function registerMappings() {
@ -695,20 +474,13 @@ function registerMappings() {
if (isDisabled === 'left' || isDisabled === 'both') {
return;
}
if (activationTimeout !== null) {
return
}
if (leftTrigger.down()) {
return;
}
if (isMoving() === true) {
return;
}
activationTimeout = Script.setTimeout(function() {
Script.clearTimeout(activationTimeout);
activationTimeout = null;
teleporter.enterTeleportMode('left')
}, TELEPORT_DELAY)
teleporter.enterTeleportMode('left')
return;
});
teleportMapping.from(Controller.Standard.RightPrimaryThumb)
@ -716,9 +488,6 @@ function registerMappings() {
if (isDisabled === 'right' || isDisabled === 'both') {
return;
}
if (activationTimeout !== null) {
return
}
if (rightTrigger.down()) {
return;
}
@ -726,11 +495,7 @@ function registerMappings() {
return;
}
activationTimeout = Script.setTimeout(function() {
teleporter.enterTeleportMode('right')
Script.clearTimeout(activationTimeout);
activationTimeout = null;
}, TELEPORT_DELAY)
teleporter.enterTeleportMode('right')
return;
});
};
@ -741,18 +506,11 @@ var teleporter = new Teleporter();
Controller.enableMapping(mappingName);
Script.scriptEnding.connect(cleanup);
function cleanup() {
teleportMapping.disable();
teleporter.disableMappings();
teleporter.deleteTargetOverlay();
teleporter.deleteCancelOverlay();
teleporter.turnOffOverlayBeams();
if (teleporter.updateConnected !== null) {
Script.update.disconnect(teleporter.update);
}
teleporter.cleanup();
}
Script.scriptEnding.connect(cleanup);
var isDisabled = false;
var handleHandMessages = function(channel, message, sender) {

View file

@ -167,12 +167,12 @@ WebTablet = function (url, width, dpi, hand, clientOnly) {
isAA: HMD.active
});
var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.035;
var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.009;
this.homeButtonID = Overlays.addOverlay("sphere", {
name: "homeButton",
localPosition: {x: 0.0, y: -HOME_BUTTON_Y_OFFSET, z: -0.01},
localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0},
localRotation: Quat.angleAxis(0, Y_AXIS),
dimensions: { x: 0.04, y: 0.04, z: 0.02},
dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor},
alpha: 0.0,
visible: true,
drawInFront: false,

View file

@ -26,9 +26,15 @@
print("show tablet-ui");
var DEFAULT_WIDTH = 0.4375;
var DEFAULT_HMD_TABLET_SCALE = 100;
var HMD_TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE;
UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (HMD_TABLET_SCALE / 100), null, activeHand, true);
var DEFAULT_TABLET_SCALE = 100;
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode;
var TABLET_SCALE = DEFAULT_TABLET_SCALE;
if (toolbarMode) {
TABLET_SCALE = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE;
} else {
TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE;
}
UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (TABLET_SCALE / 100), null, activeHand, true);
UIWebTablet.register();
HMD.tabletID = UIWebTablet.tabletEntityID;
HMD.homeButtonID = UIWebTablet.homeButtonID;

View file

@ -0,0 +1,257 @@
(function() {
Script.include("/~/system/libraries/utils.js");
var SETTING_KEY = "com.highfidelity.avatar.isSitting";
var ROLE = "fly";
var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx";
var ANIMATION_FPS = 30;
var ANIMATION_FIRST_FRAME = 1;
var ANIMATION_LAST_FRAME = 10;
var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT'];
var RELEASE_TIME = 500; // ms
var RELEASE_DISTANCE = 0.2; // meters
var MAX_IK_ERROR = 20;
var DESKTOP_UI_CHECK_INTERVAL = 250;
var DESKTOP_MAX_DISTANCE = 5;
var SIT_DELAY = 25
this.entityID = null;
this.timers = {};
this.animStateHandlerID = null;
this.preload = function(entityID) {
this.entityID = entityID;
}
this.unload = function() {
if (MyAvatar.sessionUUID === this.getSeatUser()) {
this.sitUp(this.entityID);
}
if (this.interval) {
Script.clearInterval(this.interval);
this.interval = null;
}
this.cleanupOverlay();
}
this.setSeatUser = function(user) {
var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData;
userData = JSON.parse(userData);
if (user) {
userData.seat.user = user;
} else {
delete userData.seat.user;
}
Entities.editEntity(this.entityID, {
userData: JSON.stringify(userData)
});
}
this.getSeatUser = function() {
var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]);
var userData = JSON.parse(properties.userData);
if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) {
var avatar = AvatarList.getAvatar(userData.seat.user);
if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) {
return null;
}
}
return userData.seat.user;
}
this.checkSeatForAvatar = function() {
var seatUser = this.getSeatUser();
var avatarIdentifiers = AvatarList.getAvatarIdentifiers();
for (var i in avatarIdentifiers) {
var avatar = AvatarList.getAvatar(avatarIdentifiers[i]);
if (avatar && avatar.sessionUUID === seatUser) {
return true;
}
}
return false;
}
this.sitDown = function() {
if (this.checkSeatForAvatar()) {
print("Someone is already sitting in that chair.");
return;
}
this.setSeatUser(MyAvatar.sessionUUID);
var previousValue = Settings.getValue(SETTING_KEY);
Settings.setValue(SETTING_KEY, this.entityID);
if (previousValue === "") {
MyAvatar.characterControllerEnabled = false;
MyAvatar.hmdLeanRecenterEnabled = false;
MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME);
MyAvatar.resetSensorsAndBody();
}
var that = this;
Script.setTimeout(function() {
var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]);
var index = MyAvatar.getJointIndex("Hips");
MyAvatar.pinJoint(index, properties.position, properties.rotation);
that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) {
return { headType: 0 };
}, ["headType"]);
Script.update.connect(that, that.update);
Controller.keyPressEvent.connect(that, that.keyPressed);
Controller.keyReleaseEvent.connect(that, that.keyReleased);
for (var i in RELEASE_KEYS) {
Controller.captureKeyEvents({ text: RELEASE_KEYS[i] });
}
}, SIT_DELAY);
}
this.sitUp = function() {
this.setSeatUser(null);
if (Settings.getValue(SETTING_KEY) === this.entityID) {
MyAvatar.restoreRoleAnimation(ROLE);
MyAvatar.characterControllerEnabled = true;
MyAvatar.hmdLeanRecenterEnabled = true;
var index = MyAvatar.getJointIndex("Hips");
MyAvatar.clearPinOnJoint(index);
MyAvatar.resetSensorsAndBody();
Script.setTimeout(function() {
MyAvatar.bodyPitch = 0.0;
MyAvatar.bodyRoll = 0.0;
}, SIT_DELAY);
Settings.setValue(SETTING_KEY, "");
}
MyAvatar.removeAnimationStateHandler(this.animStateHandlerID);
Script.update.disconnect(this, this.update);
Controller.keyPressEvent.disconnect(this, this.keyPressed);
Controller.keyReleaseEvent.disconnect(this, this.keyReleased);
for (var i in RELEASE_KEYS) {
Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] });
}
}
this.sit = function () {
this.sitDown();
}
this.createOverlay = function() {
var text = "Click to sit";
var textMargin = 0.05;
var lineHeight = 0.15;
this.overlay = Overlays.addOverlay("text3d", {
position: { x: 0.0, y: 0.0, z: 0.0},
dimensions: { x: 0.1, y: 0.1 },
backgroundColor: { red: 0, green: 0, blue: 0 },
color: { red: 255, green: 255, blue: 255 },
topMargin: textMargin,
leftMargin: textMargin,
bottomMargin: textMargin,
rightMargin: textMargin,
text: text,
lineHeight: lineHeight,
alpha: 0.9,
backgroundAlpha: 0.9,
ignoreRayIntersection: true,
visible: true,
isFacingAvatar: true
});
var textSize = Overlays.textSize(this.overlay, text);
var overlayDimensions = {
x: textSize.width + 2 * textMargin,
y: textSize.height + 2 * textMargin
}
var properties = Entities.getEntityProperties(this.entityID, ["position", "registrationPoint", "dimensions"]);
var yOffset = (1.0 - properties.registrationPoint.y) * properties.dimensions.y + (overlayDimensions.y / 2.0);
var overlayPosition = Vec3.sum(properties.position, { x: 0, y: yOffset, z: 0 });
Overlays.editOverlay(this.overlay, {
position: overlayPosition,
dimensions: overlayDimensions
});
}
this.cleanupOverlay = function() {
if (this.overlay !== null) {
Overlays.deleteOverlay(this.overlay);
this.overlay = null;
}
}
this.update = function(dt) {
if (MyAvatar.sessionUUID === this.getSeatUser()) {
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
var avatarDistance = Vec3.distance(MyAvatar.position, properties.position);
var ikError = MyAvatar.getIKErrorOnLastSolve();
if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) {
print("IK error: " + ikError + ", distance from chair: " + avatarDistance);
this.sitUp(this.entityID);
}
}
}
this.keyPressed = function(event) {
if (isInEditMode()) {
return;
}
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
var that = this;
this.timers[event.text] = Script.setTimeout(function() {
that.sitUp();
}, RELEASE_TIME);
}
}
this.keyReleased = function(event) {
if (RELEASE_KEYS.indexOf(event.text) !== -1) {
if (this.timers[event.text]) {
Script.clearTimeout(this.timers[event.text]);
delete this.timers[event.text];
}
}
}
this.canSitDesktop = function() {
var properties = Entities.getEntityProperties(this.entityID, ["position"]);
var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position);
return distanceFromSeat < DESKTOP_MAX_DISTANCE && !this.checkSeatForAvatar();
}
this.hoverEnterEntity = function(event) {
if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) {
return;
}
var that = this;
this.interval = Script.setInterval(function() {
if (that.overlay === null) {
if (that.canSitDesktop()) {
that.createOverlay();
}
} else if (!that.canSitDesktop()) {
that.cleanupOverlay();
}
}, DESKTOP_UI_CHECK_INTERVAL);
}
this.hoverLeaveEntity = function(event) {
if (this.interval) {
Script.clearInterval(this.interval);
this.interval = null;
}
this.cleanupOverlay();
}
this.clickDownOnEntity = function () {
if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) {
return;
}
if (this.canSitDesktop()) {
this.sitDown();
}
}
});

View file

@ -0,0 +1,111 @@
# Shortbow
Shortbow is a wave-based archery game.
## Notes
There are several design decisions that are based on certain characteristics of the High Fidelity platform,
and in particular, [server entity scripts](https://wiki.highfidelity.com/wiki/Creating_Entity_Server_Scripts),
which are touched on below.
It is recommended that you understand the basics of client entity scripts and server entity scripts (and their
differences) if you plan on digging into the shortbow code.
* Client entity scripts end in `ClientEntity.js` and server entity scripts end in `ServerEntity.js`.
* Server entity scripts are not guaranteed to have access to *other* entities, and should not rely on it.
* You should not rely on using `Entities.getEntityProperties` to access the properties of entities
other than the entity that the server entity script is running on. This also applies to other
functions like `Entities.findEntities`. This means that something like the `ShortGameManager` (described below)
will not know the entity IDs of entities like the start button or scoreboard text entities, so it
has to find ways to work around that limitation.
* You can, however, use `Entities.editEntity` to edit other entities.
* *NOTE*: It is likely that this will change in the future, and server entity scripts will be able to
query the existence of other entities in some way. This will obviate the need for some of the workarounds
used in shortbow.
* The Entity Script Server (where server entity scripts) does not run a physics simulation
* Server entity scripts do not generate collision events like would be used with
`Script.addEventHandler(entityID, "collisionWithEntity", collideHandler)`.
* High Fidelity distributes its physics simulation to clients, so collisions need to be handled
there. In the case of enemies in shortbow, for instance, the collisions are handled in the
client entity script `enemeyClientEntity.js`.
* If no client is present to run the physics simulation, entities will not physically interact with other
entities.
* But, entities will still apply their basic physical properties.
## Shortbow Game Manager
This section describes both `shortbowServerEntity.js` and `shortbowGameManager.js`. The `shortbowServerEntity.js` script
exists on the main arena model entity, and is the parent of all of the other parts of the game, other than the
enemies and bows that are spawned during gameplay.
The `shortbowServerEntity.js` script is a server entity script that runs the shortbow game. The actual logic for
the game is stored inside `shortbowGameManager.js`, in the `ShortbowGameManager` prototype.
## Enemy Scripts
These scripts exist on each enemy that is spawned.
### enemyClientEntity.js
This script handles collision events on the client. There are two collisions events that it is interested in:
1. Collisions with arrows
1. Arrow entities have "projectile" in their name
1. Any other entity with "projectile" in its name could be used to destroy the enemy
1. Collisions with the gate that the enemies roll towards
1. The gate entity has "GateCollider" in its name
### enemyServerEntity.js
This script acts as a fail-safe to work around some of the limitations that are mentioned under [Notes](#notes).
In particular, if no client is running the physics simulation of an enemy entity, it may fall through the floor
of the arena or never have a collision event generated against the gate. A server entity script also
cannot access the properties of another entity, so it can't know if the entity has moved far away from
the arena.
To handle this, this script is used to periodically send heartbeats to the [ShortbowGameManager](#shortbow-game-manager)
that runs the game. The heartbeats include the position of the enemy. If the script that received the
heartbeats hasn't heard from `enemyServerEntity.js` in too long, or the entity has moved too far away, it
will remove it from it's local list of enemies that still need to be destroyed. This ensure that the game
doesn't get into a "hung" state where it's waiting for an enemy that will never escape through the gate or be
destroyed.
## Start Button
These scripts exist on the start button.
### startGameButtonClientEntity.js
This script sends a message to the [ShortbowGameManager](#shortbow-game-manager) to request that the game be started.
### startGameButtonServerEntity.js
When the shortbow game starts the start button is hidden, and when the shortbow game ends it is shown again.
As described in the [Notes](#notes), server entity scripts cannot access other entities, including their IDs.
Because of this, the [ShortbowGameManager](#shortbow-game-manager) has no way of knowing the id of the start button,
and thus being able to use `Entities.editEntity` to set its `visible` property to `true` or `false`. One way around
this, and is what is used here, is to use `Messages` on a common channel to send messages to a server entity script
on the start button to request that it be shown or hidden.
## Display (Scoreboard)
This script exists on each text entity that scoreboard is composed of. The scoreboard area is composed of a text entity for each of the following: Score, High Score, Wave, Lives.
### displayServerEntity.js
The same problem that exists for [startGameButtonServerEntity.js](#startgamebuttonserverentityjs) exists for
the text entities on the scoreboard. This script works around the problem in the same way, but instead of
receiving a message that tells it whether to hide or show itself, it receives a message that contains the
text to update the text entity with. For intance, the "lives" display entity will receive a message each
time a life is lost with the current number of lives.
## How is the "common channel" determined that is used in some of the client and server entity scripts?
Imagine that there are two instances of shortbow next to each. If the channel being used is always the same,
and not differentiated in some way between the two instances, a "start game" message sent from [startGameButtonClientEntity.js](#startgamebuttoncliententityjs)
on one game will be received and handled by both games, causing both of them to start. We need a way to create
a channel that is unique to the scripts that are running for a particular instance of shortbow.
All of the entities in shortbow, besides the enemy entities, are children of the main arena entity that
runs the game. All of the scripts on these entities can access their parentID, so they can use
a prefix plus the parentID to generate a unique channel for that instance of shortbow.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

View file

@ -0,0 +1,671 @@
//
// Created by Seth Alves on 2016-9-7
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* global MyAvatar, Vec3, Controller, Quat */
var GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing";
setGrabCommunications = function setFarGrabCommunications(on) {
Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : "");
}
getGrabCommunications = function getFarGrabCommunications() {
return !!Settings.getValue(GRAB_COMMUNICATIONS_SETTING, "");
}
// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378
var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; // x = upward, y = forward, z = lateral
getGrabPointSphereOffset = function(handController) {
if (handController === Controller.Standard.RightHand) {
return GRAB_POINT_SPHERE_OFFSET;
}
return {
x: GRAB_POINT_SPHERE_OFFSET.x * -1,
y: GRAB_POINT_SPHERE_OFFSET.y,
z: GRAB_POINT_SPHERE_OFFSET.z
};
};
// controllerWorldLocation is where the controller would be, in-world, with an added offset
getControllerWorldLocation = function (handController, doOffset) {
var orientation;
var position;
var pose = Controller.getPoseValue(handController);
var valid = pose.valid;
var controllerJointIndex;
if (pose.valid) {
if (handController === Controller.Standard.RightHand) {
controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND");
} else {
controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
}
orientation = Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(controllerJointIndex));
position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(controllerJointIndex)));
// add to the real position so the grab-point is out in front of the hand, a bit
if (doOffset) {
var offset = getGrabPointSphereOffset(handController);
position = Vec3.sum(position, Vec3.multiplyQbyV(orientation, offset));
}
} else if (!HMD.isHandControllerAvailable()) {
// NOTE: keep this offset in sync with scripts/system/controllers/handControllerPointer.js:493
var VERTICAL_HEAD_LASER_OFFSET = 0.1;
position = Vec3.sum(Camera.position, Vec3.multiplyQbyV(Camera.orientation, {x: 0, y: VERTICAL_HEAD_LASER_OFFSET, z: 0}));
orientation = Quat.multiply(Camera.orientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 }));
valid = true;
}
return {position: position,
translation: position,
orientation: orientation,
rotation: orientation,
valid: valid};
};
//
//
//
//
//
//
// bow.js
//
// This script attaches to a bow that you can pick up with a hand controller.
// Created by James B. Pollack @imgntn on 10/19/2015
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/*global Script, Controller, SoundCache, Entities, getEntityCustomData, setEntityCustomData, MyAvatar, Vec3, Quat, Messages */
function getControllerLocation(controllerHand) {
var standardControllerValue =
(controllerHand === "right") ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
return getControllerWorldLocation(standardControllerValue, true);
};
(function() {
Script.include("/~/system/libraries/utils.js");
const NULL_UUID = "{00000000-0000-0000-0000-000000000000}";
const NOTCH_ARROW_SOUND_URL = Script.resolvePath('notch.wav');
const SHOOT_ARROW_SOUND_URL = Script.resolvePath('String_release2.L.wav');
const STRING_PULL_SOUND_URL = Script.resolvePath('Bow_draw.1.L.wav');
const ARROW_HIT_SOUND_URL = Script.resolvePath('Arrow_impact1.L.wav');
const ARROW_MODEL_URL = Script.resolvePath('arrow.fbx');
const ARROW_DIMENSIONS = {
x: 0.20,
y: 0.19,
z: 0.93
};
const MIN_ARROW_SPEED = 3.0;
const MAX_ARROW_SPEED = 30.0;
const ARROW_TIP_OFFSET = 0.47;
const ARROW_GRAVITY = {
x: 0,
y: -9.8,
z: 0
};
const ARROW_LIFETIME = 15; // seconds
const ARROW_PARTICLE_LIFESPAN = 2; // seconds
const TOP_NOTCH_OFFSET = 0.6;
const BOTTOM_NOTCH_OFFSET = 0.6;
const LINE_DIMENSIONS = {
x: 5.0,
y: 5.0,
z: 5.0
};
const DRAW_STRING_THRESHOLD = 0.80;
const DRAW_STRING_PULL_DELTA_HAPTIC_PULSE = 0.09;
const DRAW_STRING_MAX_DRAW = 0.7;
const MIN_ARROW_DISTANCE_FROM_BOW_REST = 0.2;
const MAX_ARROW_DISTANCE_FROM_BOW_REST = ARROW_DIMENSIONS.z - 0.2;
const MIN_HAND_DISTANCE_FROM_BOW_TO_KNOCK_ARROW = 0.25;
const MIN_ARROW_DISTANCE_FROM_BOW_REST_TO_SHOOT = 0.30;
const NOTCH_OFFSET_FORWARD = 0.08;
const NOTCH_OFFSET_UP = 0.035;
const SHOT_SCALE = {
min1: 0.0,
max1: 0.6,
min2: 1.0,
max2: 15.0
};
const USE_DEBOUNCE = false;
const TRIGGER_CONTROLS = [
Controller.Standard.LT,
Controller.Standard.RT,
];
function interval() {
var lastTime = new Date().getTime();
return function getInterval() {
var newTime = new Date().getTime();
var delta = newTime - lastTime;
lastTime = newTime;
return delta;
};
}
var checkInterval = interval();
var _this;
function Bow() {
_this = this;
return;
}
const STRING_NAME = 'Hifi-Bow-String';
const ARROW_NAME = 'Hifi-Arrow-projectile';
const STATE_IDLE = 0;
const STATE_ARROW_GRABBED = 1;
Bow.prototype = {
topString: null,
aiming: false,
arrowTipPosition: null,
preNotchString: null,
stringID: null,
arrow: null,
stringData: {
currentColor: {
red: 255,
green: 255,
blue: 255
}
},
state: STATE_IDLE,
sinceLastUpdate: 0,
preload: function(entityID) {
this.entityID = entityID;
this.stringPullSound = SoundCache.getSound(STRING_PULL_SOUND_URL);
this.shootArrowSound = SoundCache.getSound(SHOOT_ARROW_SOUND_URL);
this.arrowHitSound = SoundCache.getSound(ARROW_HIT_SOUND_URL);
this.arrowNotchSound = SoundCache.getSound(NOTCH_ARROW_SOUND_URL);
var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData;
print(userData);
this.userData = JSON.parse(userData);
this.stringID = null;
},
unload: function() {
Messages.sendLocalMessage('Hifi-Hand-Disabler', "none");
Entities.deleteEntity(this.arrow);
},
startEquip: function(entityID, args) {
this.hand = args[0];
this.bowHand = args[0];
this.stringHand = this.bowHand === "right" ? "left" : "right";
Entities.editEntity(_this.entityID, {
collidesWith: "",
});
var data = getEntityCustomData('grabbableKey', this.entityID, {});
data.grabbable = false;
setEntityCustomData('grabbableKey', this.entityID, data);
this.initString();
var self = this;
this.updateIntervalID = Script.setInterval(function() { self.update(); }, 11);
},
getStringHandPosition: function() {
return getControllerLocation(this.stringHand).position;
},
releaseEquip: function(entityID, args) {
Script.clearInterval(this.updateIntervalID);
this.updateIntervalID = null;
Messages.sendLocalMessage('Hifi-Hand-Disabler', "none");
var data = getEntityCustomData('grabbableKey', this.entityID, {});
data.grabbable = true;
setEntityCustomData('grabbableKey', this.entityID, data);
Entities.deleteEntity(this.arrow);
this.resetStringToIdlePosition();
this.destroyArrow();
Entities.editEntity(_this.entityID, {
collidesWith: "static,dynamic,kinematic,otherAvatar,myAvatar"
});
},
update: function(entityID) {
var self = this;
self.deltaTime = checkInterval();
//debounce during debugging -- maybe we're updating too fast?
if (USE_DEBOUNCE === true) {
self.sinceLastUpdate = self.sinceLastUpdate + self.deltaTime;
if (self.sinceLastUpdate > 60) {
self.sinceLastUpdate = 0;
} else {
return;
}
}
//invert the hands because our string will be held with the opposite hand of the first one we pick up the bow with
this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[(this.hand === 'left') ? 1 : 0]);
this.bowProperties = Entities.getEntityProperties(this.entityID, ['position', 'rotation']);
var notchPosition = this.getNotchPosition(this.bowProperties);
var stringHandPosition = this.getStringHandPosition();
var handToNotch = Vec3.subtract(notchPosition, stringHandPosition);
var pullBackDistance = Vec3.length(handToNotch);
if (this.state === STATE_IDLE) {
this.pullBackDistance = 0;
this.resetStringToIdlePosition();
if (this.triggerValue >= DRAW_STRING_THRESHOLD && pullBackDistance < MIN_HAND_DISTANCE_FROM_BOW_TO_KNOCK_ARROW && !this.backHandBusy) {
//the first time aiming the arrow
var handToDisable = (this.hand === 'right' ? 'left' : 'right');
this.state = STATE_ARROW_GRABBED;
}
}
if (this.state === STATE_ARROW_GRABBED) {
if (!this.arrow) {
var handToDisable = (this.hand === 'right' ? 'left' : 'right');
Messages.sendLocalMessage('Hifi-Hand-Disabler', handToDisable);
this.playArrowNotchSound();
this.arrow = this.createArrow();
this.playStringPullSound();
}
if (this.triggerValue < DRAW_STRING_THRESHOLD) {
if (pullBackDistance >= (MIN_ARROW_DISTANCE_FROM_BOW_REST_TO_SHOOT)) {
// The arrow has been pulled far enough back that we can release it
Messages.sendLocalMessage('Hifi-Hand-Disabler', "none");
this.updateArrowPositionInNotch(true, true);
this.arrow = null;
this.state = STATE_IDLE;
this.resetStringToIdlePosition();
} else {
// The arrow has not been pulled far enough back so we just remove the arrow
Messages.sendLocalMessage('Hifi-Hand-Disabler', "none");
Entities.deleteEntity(this.arrow);
this.arrow = null;
this.state = STATE_IDLE;
this.resetStringToIdlePosition();
}
} else {
this.updateArrowPositionInNotch(false, true);
this.updateString();
}
}
},
destroyArrow: function() {
var children = Entities.getChildrenIDs(this.entityID);
children.forEach(function(childID) {
var childName = Entities.getEntityProperties(childID, ["name"]).name;
if (childName == ARROW_NAME) {
Entities.deleteEntity(childID);
// Continue iterating through children in case we've ended up in
// a bad state where there are multiple arrows.
}
});
},
createArrow: function() {
this.playArrowNotchSound();
var arrow = Entities.addEntity({
name: ARROW_NAME,
type: 'Model',
modelURL: ARROW_MODEL_URL,
shapeType: 'simple-compound',
dimensions: ARROW_DIMENSIONS,
position: this.bowProperties.position,
parentID: this.entityID,
dynamic: false,
collisionless: true,
collisionSoundURL: ARROW_HIT_SOUND_URL,
damping: 0.01,
userData: JSON.stringify({
grabbableKey: {
grabbable: false
},
creatorSessionUUID: MyAvatar.sessionUUID
})
});
var makeArrowStick = function(entityA, entityB, collision) {
Entities.editEntity(entityA, {
localAngularVelocity: {
x: 0,
y: 0,
z: 0
},
localVelocity: {
x: 0,
y: 0,
z: 0
},
gravity: {
x: 0,
y: 0,
z: 0
},
parentID: entityB,
dynamic: false,
collisionless: true,
collidesWith: ""
});
Script.removeEventHandler(arrow, "collisionWithEntity", makeArrowStick);
};
Script.addEventHandler(arrow, "collisionWithEntity", makeArrowStick);
return arrow;
},
initString: function() {
// Check for existence of string
var children = Entities.getChildrenIDs(this.entityID);
children.forEach(function(childID) {
var childName = Entities.getEntityProperties(childID, ["name"]).name;
if (childName == STRING_NAME) {
this.stringID = childID;
}
});
// If thie string wasn't found, create it
if (this.stringID === null) {
this.stringID = Entities.addEntity({
collisionless: true,
dimensions: { "x": 5, "y": 5, "z": 5 },
ignoreForCollisions: 1,
linePoints: [ { "x": 0, "y": 0, "z": 0 }, { "x": 0, "y": -1.2, "z": 0 } ],
lineWidth: 5,
color: { red: 153, green: 102, blue: 51 },
name: STRING_NAME,
parentID: this.entityID,
localPosition: { "x": 0, "y": 0.6, "z": 0.1 },
localRotation: { "w": 1, "x": 0, "y": 0, "z": 0 },
type: 'Line',
userData: JSON.stringify({
grabbableKey: {
grabbable: false
}
})
});
}
this.resetStringToIdlePosition();
},
// This resets the string to a straight line
resetStringToIdlePosition: function() {
Entities.editEntity(this.stringID, {
linePoints: [ { "x": 0, "y": 0, "z": 0 }, { "x": 0, "y": -1.2, "z": 0 } ],
lineWidth: 5,
localPosition: { "x": 0, "y": 0.6, "z": 0.1 },
localRotation: { "w": 1, "x": 0, "y": 0, "z": 0 },
});
},
updateString: function() {
var upVector = Quat.getUp(this.bowProperties.rotation);
var upOffset = Vec3.multiply(upVector, TOP_NOTCH_OFFSET);
var downVector = Vec3.multiply(-1, Quat.getUp(this.bowProperties.rotation));
var downOffset = Vec3.multiply(downVector, BOTTOM_NOTCH_OFFSET);
var backOffset = Vec3.multiply(-0.1, Quat.getFront(this.bowProperties.rotation));
var topStringPosition = Vec3.sum(this.bowProperties.position, upOffset);
this.topStringPosition = Vec3.sum(topStringPosition, backOffset);
var bottomStringPosition = Vec3.sum(this.bowProperties.position, downOffset);
this.bottomStringPosition = Vec3.sum(bottomStringPosition, backOffset);
var stringProps = Entities.getEntityProperties(this.stringID, ['position', 'rotation']);
var handPositionLocal = Vec3.subtract(this.arrowRearPosition, stringProps.position);
handPositionLocal = Vec3.multiplyQbyV(Quat.inverse(stringProps.rotation), handPositionLocal);
var linePoints = [
{ x: 0, y: 0, z: 0 },
handPositionLocal,
{ x: 0, y: -1.2, z: 0 },
];
Entities.editEntity(this.stringID, {
linePoints: linePoints,
});
},
getNotchPosition: function(bowProperties) {
var frontVector = Quat.getFront(bowProperties.rotation);
var notchVectorForward = Vec3.multiply(frontVector, NOTCH_OFFSET_FORWARD);
var upVector = Quat.getUp(bowProperties.rotation);
var notchVectorUp = Vec3.multiply(upVector, NOTCH_OFFSET_UP);
var notchPosition = Vec3.sum(bowProperties.position, notchVectorForward);
notchPosition = Vec3.sum(notchPosition, notchVectorUp);
return notchPosition;
},
updateArrowPositionInNotch: function(shouldReleaseArrow, doHapticPulses) {
//set the notch that the arrow should go through
var notchPosition = this.getNotchPosition(this.bowProperties);
//set the arrow rotation to be between the notch and other hand
var stringHandPosition = this.getStringHandPosition();
var handToNotch = Vec3.subtract(notchPosition, stringHandPosition);
var arrowRotation = Quat.rotationBetween(Vec3.FRONT, handToNotch);
var backHand = this.hand === 'left' ? 1 : 0;
var pullBackDistance = Vec3.length(handToNotch);
// pulse as arrow is drawn
if (doHapticPulses &&
Math.abs(pullBackDistance - this.pullBackDistance) > DRAW_STRING_PULL_DELTA_HAPTIC_PULSE) {
Controller.triggerHapticPulse(1, 20, backHand);
this.pullBackDistance = pullBackDistance;
}
if (pullBackDistance > DRAW_STRING_MAX_DRAW) {
pullBackDistance = DRAW_STRING_MAX_DRAW;
}
var handToNotchDistance = Vec3.length(handToNotch);
var stringToNotchDistance = Math.max(MIN_ARROW_DISTANCE_FROM_BOW_REST, Math.min(MAX_ARROW_DISTANCE_FROM_BOW_REST, handToNotchDistance));
var halfArrowVec = Vec3.multiply(Vec3.normalize(handToNotch), ARROW_DIMENSIONS.z / 2.0);
var offset = Vec3.subtract(notchPosition, Vec3.multiply(Vec3.normalize(handToNotch), stringToNotchDistance - ARROW_DIMENSIONS.z / 2.0));
var arrowPosition = offset;
// Set arrow rear position
var frontVector = Quat.getFront(arrowRotation);
var frontOffset = Vec3.multiply(frontVector, -ARROW_TIP_OFFSET);
var arrorRearPosition = Vec3.sum(arrowPosition, frontOffset);
this.arrowRearPosition = arrorRearPosition;
//if we're not shooting, we're updating the arrow's orientation
if (shouldReleaseArrow !== true) {
Entities.editEntity(this.arrow, {
position: arrowPosition,
rotation: arrowRotation
});
} else {
//shoot the arrow
var arrowAge = Entities.getEntityProperties(this.arrow, ["age"]).age;
//scale the shot strength by the distance you've pulled the arrow back and set its release velocity to be
// in the direction of the v
var arrowForce = this.scaleArrowShotStrength(stringToNotchDistance);
var handToNotchNorm = Vec3.normalize(handToNotch);
var releaseVelocity = Vec3.multiply(handToNotchNorm, arrowForce);
//make the arrow physical, give it gravity, a lifetime, and set our velocity
var arrowProperties = {
dynamic: true,
collisionless: false,
collidesWith: "static,dynamic,otherAvatar", // workaround: not with kinematic --> no collision with bow
velocity: releaseVelocity,
parentID: NULL_UUID,
gravity: ARROW_GRAVITY,
lifetime: arrowAge + ARROW_LIFETIME,
};
// add a particle effect to make the arrow easier to see as it flies
var arrowParticleProperties = {
accelerationSpread: { x: 0, y: 0, z: 0 },
alpha: 1,
alphaFinish: 0,
alphaSpread: 0,
alphaStart: 0.3,
azimuthFinish: 3.1,
azimuthStart: -3.14159,
color: { red: 255, green: 255, blue: 255 },
colorFinish: { red: 255, green: 255, blue: 255 },
colorSpread: { red: 0, green: 0, blue: 0 },
colorStart: { red: 255, green: 255, blue: 255 },
emitAcceleration: { x: 0, y: 0, z: 0 },
emitDimensions: { x: 0, y: 0, z: 0 },
emitOrientation: { x: -0.7, y: 0.0, z: 0.0, w: 0.7 },
emitRate: 0.01,
emitSpeed: 0,
emitterShouldTrail: 0,
isEmitting: 1,
lifespan: ARROW_PARTICLE_LIFESPAN,
lifetime: ARROW_PARTICLE_LIFESPAN + 1,
maxParticles: 1000,
name: 'arrow-particles',
parentID: this.arrow,
particleRadius: 0.132,
polarFinish: 0,
polarStart: 0,
radiusFinish: 0.35,
radiusSpread: 0,
radiusStart: 0.132,
speedSpread: 0,
textures: Script.resolvePath('arrow-sparkle.png'),
type: 'ParticleEffect'
};
Entities.addEntity(arrowParticleProperties);
// actually shoot the arrow
Entities.editEntity(this.arrow, arrowProperties);
// play the sound of a shooting arrow
this.playShootArrowSound();
Entities.addAction("travel-oriented", this.arrow, {
forward: { x: 0, y: 0, z: -1 },
angularTimeScale: 0.1,
tag: "arrow from hifi-bow",
ttl: ARROW_LIFETIME
});
}
},
scaleArrowShotStrength: function(value) {
var percentage = (value - MIN_ARROW_DISTANCE_FROM_BOW_REST)
/ (MAX_ARROW_DISTANCE_FROM_BOW_REST - MIN_ARROW_DISTANCE_FROM_BOW_REST);
return MIN_ARROW_SPEED + (percentage * (MAX_ARROW_SPEED - MIN_ARROW_SPEED)) ;
},
playStringPullSound: function() {
var audioProperties = {
volume: 0.10,
position: this.bowProperties.position
};
this.stringPullInjector = Audio.playSound(this.stringPullSound, audioProperties);
},
playShootArrowSound: function(sound) {
var audioProperties = {
volume: 0.15,
position: this.bowProperties.position
};
Audio.playSound(this.shootArrowSound, audioProperties);
},
playArrowNotchSound: function() {
var audioProperties = {
volume: 0.15,
position: this.bowProperties.position
};
Audio.playSound(this.arrowNotchSound, audioProperties);
},
changeStringPullSoundVolume: function(pullBackDistance) {
var audioProperties = {
volume: this.scaleSoundVolume(pullBackDistance),
position: this.bowProperties.position
};
this.stringPullInjector.options = audioProperties;
},
scaleSoundVolume: function(value) {
var min1 = SHOT_SCALE.min1;
var max1 = SHOT_SCALE.max1;
var min2 = 0;
var max2 = 0.2;
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
},
handleMessages: function(channel, message, sender) {
if (sender !== MyAvatar.sessionUUID) {
return;
}
if (channel !== 'Hifi-Object-Manipulation') {
return;
}
try {
var data = JSON.parse(message);
var action = data.action;
var hand = data.joint;
var isBackHand = ((_this.hand == "left" && hand == "RightHand") ||
(_this.hand == "right" && hand == "LeftHand"));
if ((action == "equip" || action == "grab") && isBackHand) {
_this.backHandBusy = true;
}
if (action == "release" && isBackHand) {
_this.backHandBusy = false;
}
} catch (e) {
print("WARNING: bow.js -- error parsing Hifi-Object-Manipulation message: " + message);
}
}
};
var bow = new Bow();
Messages.subscribe('Hifi-Object-Manipulation');
Messages.messageReceived.connect(bow.handleMessages);
return bow;
});

View file

@ -0,0 +1,44 @@
{
"Entities": [
{
"clientOnly": 0,
"collisionsWillMove": 1,
"compoundShapeURL": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow_collision_hull.obj",
"created": "2017-02-14T18:54:38Z",
"dimensions": {
"x": 0.039999999105930328,
"y": 1.2999999523162842,
"z": 0.20000000298023224
},
"dynamic": 1,
"gravity": {
"x": 0,
"y": -9.8000001907348633,
"z": 0
},
"id": "{73954924-2e18-4787-91a7-092c2afb6242}",
"lastEdited": 1487098438422164,
"lastEditedBy": "{d2da5e17-9125-414d-ac4e-cd7fba6c22f8}",
"modelURL": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow-deadly.fbx",
"name": "WG.Hifi-Bow",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"queryAACube": {
"scale": 1.3159027099609375,
"x": -0.65795135498046875,
"y": -0.65795135498046875,
"z": -0.65795135498046875
},
"rotation": {
"w": 0.9717707633972168,
"x": 0.15437555313110352,
"y": -0.10472267866134644,
"z": -0.14421302080154419
},
"script": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow.js",
"shapeType": "compound",
"type": "Model",
"userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}"
}
],
"Version": 67
}

View file

@ -0,0 +1,32 @@
{
"Entities": [ {
"collisionsWillMove": 1,
"compoundShapeURL": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow_collision_hull.obj",
"created": "2016-09-01T23:57:55Z",
"dimensions": {
"x": 0.039999999105930328,
"y": 1.2999999523162842,
"z": 0.20000000298023224
},
"dynamic": 1,
"gravity": {
"x": 0,
"y": -1,
"z": 0
},
"modelURL": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow-deadly.fbx",
"name": "Hifi-Bow",
"rotation": {
"w": 0.9718012809753418,
"x": 0.15440607070922852,
"y": -0.10469216108322144,
"z": -0.14418250322341919
},
"script": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow.js",
"shapeType": "compound",
"type": "Model",
"userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}"
}
],
"Version": 57
}

View file

@ -0,0 +1,21 @@
v -0.016461 -0.431491 -0.033447
v -0.007624 0.437384 -0.046243
v 0.011984 -0.424659 -0.03691
v 0.015514 0.425913 -0.028648
v -0.010788 -0.421429 0.093711
v 0.007135 -0.423115 0.098735
v -0.010208 0.425558 0.096005
v 0.006734 0.43913 0.088902
f 1 2 3
f 3 2 4
f 5 6 7
f 7 6 8
f 1 5 2
f 2 5 7
f 3 4 6
f 6 4 8
f 1 3 5
f 5 3 6
f 2 7 4
f 4 7 8

Binary file not shown.

View file

@ -0,0 +1,67 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var leftHandPosition = {
"x": 0,
"y": 0.0559,
"z": 0.0159
};
var leftHandRotation = Quat.fromPitchYawRollDegrees(90, -90, 0);
var rightHandPosition = Vec3.multiplyVbyV(leftHandPosition, { x: -1, y: 0, z: 0 });
var rightHandRotation = Quat.fromPitchYawRollDegrees(90, 90, 0);
var userData = {
"grabbableKey": {
"grabbable": true
},
"wearable": {
"joints": {
"LeftHand": [
leftHandPosition,
leftHandRotation
],
"RightHand": [
rightHandPosition,
rightHandRotation
]
}
}
};
var id = Entities.addEntity({
"position": MyAvatar.position,
"collisionsWillMove": 1,
"compoundShapeURL": Script.resolvePath("bow_collision_hull.obj"),
"created": "2016-09-01T23:57:55Z",
"dimensions": {
"x": 0.039999999105930328,
"y": 1.2999999523162842,
"z": 0.20000000298023224
},
"dynamic": 1,
"gravity": {
"x": 0,
"y": -9.8,
"z": 0
},
"modelURL": Script.resolvePath("bow-deadly.fbx"),
"name": "Hifi-Bow",
"rotation": {
"w": 0.9718012809753418,
"x": 0.15440607070922852,
"y": -0.10469216108322144,
"z": -0.14418250322341919
},
"script": Script.resolvePath("bow.js"),
"shapeType": "compound",
"type": "Model",
"userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}",
"lifetime": 600
});
print("Created bow:", id);

View file

@ -0,0 +1,61 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals utils */
(function() {
Script.include('utils.js');
function Enemy() {
}
Enemy.prototype = {
preload: function(entityID) {
this.entityID = entityID;
// To avoid sending extraneous messages and checking entities that we've already
// seen, we keep track of which entities we've collided with previously.
this.entityIDsThatHaveCollidedWithMe = [];
Script.addEventHandler(entityID, "collisionWithEntity", this.onCollide.bind(this));
var userData = Entities.getEntityProperties(this.entityID, 'userData').userData;
var data = utils.parseJSON(userData);
if (data !== undefined && data.gameChannel !== undefined) {
this.gameChannel = data.gameChannel;
} else {
print("enemyEntity.js | ERROR: userData does not contain a game channel and/or team number");
}
},
onCollide: function(entityA, entityB, collision) {
if (this.entityIDsThatHaveCollidedWithMe.indexOf(entityB) > -1) {
return;
}
this.entityIDsThatHaveCollidedWithMe.push(entityB);
var colliderName = Entities.getEntityProperties(entityB, 'name').name;
if (colliderName.indexOf("projectile") > -1) {
Messages.sendMessage(this.gameChannel, JSON.stringify({
type: "enemy-killed",
entityID: this.entityID,
position: Entities.getEntityProperties(this.entityID, 'position').position
}));
Entities.deleteEntity(this.entityID);
} else if (colliderName.indexOf("GateCollider") > -1) {
Messages.sendMessage(this.gameChannel, JSON.stringify({
type: "enemy-escaped",
entityID: this.entityID,
position: Entities.getEntityProperties(this.entityID, 'position').position
}));
Entities.deleteEntity(this.entityID);
}
}
};
return new Enemy();
});

View file

@ -0,0 +1,41 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals utils */
(function() {
Script.include('utils.js');
function Enemy() {
}
Enemy.prototype = {
preload: function(entityID) {
this.entityID = entityID;
var userData = Entities.getEntityProperties(this.entityID, 'userData').userData;
var data = utils.parseJSON(userData);
if (data !== undefined && data.gameChannel !== undefined) {
this.gameChannel = data.gameChannel;
} else {
print("enemyServerEntity.js | ERROR: userData does not contain a game channel and/or team number");
}
var self = this;
this.heartbeatTimerID = Script.setInterval(function() {
Messages.sendMessage(self.gameChannel, JSON.stringify({
type: "enemy-heartbeat",
entityID: self.entityID,
position: Entities.getEntityProperties(self.entityID, 'position').position
}));
}, 1000);
},
unload: function() {
Script.clearInterval(this.heartbeatTimerID);
}
};
return new Enemy();
});

View file

@ -0,0 +1,877 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals SHORTBOW_ENTITIES:true */
// This is a copy of the data in shortbow.json, which is an export of the shortbow
// scene.
//
// Because .json can't be Script.include'd directly, the contents are copied over
// to here and exposed as a global variable.
//
SHORTBOW_ENTITIES =
{
"Entities": [
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{02f39515-cab4-41d5-b315-5fb41613f844}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750446,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -5.1684012413024902,
"y": 0.54034698009490967,
"z": -11.257695198059082
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": -1.3604279756546021,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 2,
"y": 0.69999998807907104,
"z": 0.0099999997764825821
},
"id": "{3eae601e-3c6e-49ab-8f40-dedee32f7573}",
"lastEdited": 1487894036038423,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayScore",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 1.5265679359436035,
"z": -9.5913219451904297
},
"queryAACube": {
"scale": 2.118985652923584,
"x": -5.1109838485717773,
"y": -1803.69189453125,
"z": -26.774648666381836
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"score\"}"
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.71920669078826904,
"y": 3.3160061836242676,
"z": 2.2217941284179688
},
"id": "{04288f77-64df-4323-ac38-9c1960a393a5}",
"lastEdited": 1487893058314990,
"lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx",
"name": "SB.StartButton",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -9.8358345031738281,
"y": 0.45674961805343628,
"z": -13.044205665588379
},
"queryAACube": {
"scale": 4.0558013916015625,
"x": -7.844393253326416,
"y": -1805.730224609375,
"z": -31.195960998535156
},
"rotation": {
"w": 1,
"x": 1.52587890625e-05,
"y": 1.52587890625e-05,
"z": 1.52587890625e-05
},
"script": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js",
"shapeType": "static-mesh",
"type": "Model",
"userData": "{\"grabbableKey\":{\"wantsTrigger\":true}}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 2,
"y": 0.69999998807907104,
"z": 0.0099999997764825821
},
"id": "{1196096f-bcc9-4b19-970d-605113474c1b}",
"lastEdited": 1487894037900323,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayHighScore",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 0.26189804077148438,
"z": -9.5913219451904297
},
"queryAACube": {
"scale": 2.118985652923584,
"x": -5.11102294921875,
"y": -1804.95654296875,
"z": -26.77461051940918
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"highscore\"}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 1.4120937585830688,
"y": 0.71569448709487915,
"z": 0.0099999997764825821
},
"id": "{293c294d-1df5-461e-82a3-66abee852d44}",
"lastEdited": 1487894033695485,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayWave",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 1.5265679359436035,
"z": -7.2889409065246582
},
"queryAACube": {
"scale": 1.5831384658813477,
"x": -4.8431310653686523,
"y": -1803.4239501953125,
"z": -24.204343795776367
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"wave\"}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 1.4120937585830688,
"y": 0.71569448709487915,
"z": 0.0099999997764825821
},
"id": "{379afa7b-c668-4c4e-b290-e5c37fb3440c}",
"lastEdited": 1487893055310428,
"lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}",
"lineHeight": 0.5,
"name": "SB.DisplayLives",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 0.26189804077148438,
"z": -7.2889409065246582
},
"queryAACube": {
"scale": 1.5831384658813477,
"x": -4.8431692123413086,
"y": -1804.6885986328125,
"z": -24.204303741455078
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"lives\"}"
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{760e81a1-a804-4f5e-9769-393d021fc8fe}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234633,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -1.89238440990448,
"y": -5.3368110656738281,
"z": 11.512755393981934
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.9147146940231323,
"y": -1809.7066650390625,
"z": -4.8219971656799316
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{0a76d0ac-6353-467b-8edc-56417d5a987c}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440235124,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 3.6569130420684814,
"y": -5.3365960121154785,
"z": 10.01292610168457
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 7.4640579223632812,
"y": -1809.7066650390625,
"z": -6.3216567039489746
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{f8549c8a-e646-4feb-bbaf-70e7d5be755a}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440235339,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 8.8902750015258789,
"y": -5.3364419937133789,
"z": 10.195274353027344
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 12.697414398193359,
"y": -1809.7066650390625,
"z": -6.1391491889953613
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{f3aea4ae-4445-4a2d-8d61-e9fd72f04008}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751269,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -2.5027251243591309,
"y": 0.54042834043502808,
"z": -11.257777214050293
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.3052481412887573,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{cc1ac907-124b-4372-8c4c-82d175546725}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751135,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 2.7972855567932129,
"y": 0.54059004783630371,
"z": -11.257938385009766
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 6.6052589416503906,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{e25ce690-e267-4c51-80a0-f63a72474b8a}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751527,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.17114110291004181,
"y": 0.54050993919372559,
"z": -11.257858276367188
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 3.979114294052124,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{91ee2285-38f8-4795-b6bc-7abc8dcde07c}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750806,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 5.4656705856323242,
"y": 0.54067152738571167,
"z": -11.258020401000977
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 9.2736434936523438,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{d81e5fae-8a8d-4186-bbd2-0c3ae737b0f2}",
"ignoreForCollisions": 1,
"lastEdited": 1487892552671000,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 9.6099967956542969,
"y": 0.64012420177459717,
"z": -9.9802846908569336
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 13.417934417724609,
"y": -1803.730712890625,
"z": -26.314868927001953
},
"rotation": {
"w": 0.22495110332965851,
"x": -2.9734959753113799e-05,
"y": 0.97437006235122681,
"z": 2.9735869247815572e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{7056e21e-bce6-4c4b-bbca-36bea1dce303}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750993,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 8.1799373626708984,
"y": 0.54075431823730469,
"z": -11.258102416992188
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 11.987911224365234,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{ed073620-e304-4b8e-b12a-5371b595bbf6}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234415,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -2.3618791103363037,
"y": -2.0691573619842529,
"z": 11.254574775695801
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.4453276395797729,
"y": -1806.43896484375,
"z": -5.0802912712097168
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{32ed7820-c386-4da1-b676-7e63762861a3}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234854,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.64757472276687622,
"y": -2.5217375755310059,
"z": 10.08248233795166
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 4.454803466796875,
"y": -1806.8917236328125,
"z": -6.2522788047790527
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 26.619264602661133,
"y": 14.24090576171875,
"z": 39.351066589355469
},
"id": "{d4c8f577-944d-4d50-ac85-e56387c0ef0a}",
"lastEdited": 1487892440231278,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx",
"name": "SB.Platform",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.097909502685070038,
"y": -1.0163799524307251,
"z": 2.0321114063262939
},
"queryAACube": {
"scale": 49.597328186035156,
"x": -20.681917190551758,
"y": -1829.9739990234375,
"z": -38.890060424804688
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shapeType": "static-mesh",
"type": "Model"
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 23.341892242431641,
"y": 12.223045349121094,
"z": 32.012016296386719
},
"friction": 1,
"id": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"lastEdited": 1487892440231832,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx",
"name": "SB.Scoreboard",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"queryAACube": {
"scale": 41.461017608642578,
"x": -20.730508804321289,
"y": -20.730508804321289,
"z": -20.730508804321289
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"serverScripts": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js",
"shapeType": "static-mesh",
"type": "Model"
},
{
"clientOnly": 0,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 15.710711479187012,
"y": 4.7783288955688477,
"z": 1.6129581928253174
},
"id": "{84cdff6e-a68d-4bbf-8660-2d6a8c2f1fd0}",
"lastEdited": 1487892440231522,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.GateCollider",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.31728419661521912,
"y": -4.3002614974975586,
"z": -12.531644821166992
},
"queryAACube": {
"scale": 16.50031852722168,
"x": -3.913693904876709,
"y": -1816.709716796875,
"z": -36.905204772949219
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
}
],
"Version": 68
};
// Add LocalPosition to entity data if parent properties are available
var entities = SHORTBOW_ENTITIES.Entities;
var entitiesByID = {};
var i, entity;
for (i = 0; i < entities.length; ++i) {
entity = entities[i];
entitiesByID[entity.id] = entity;
}
for (i = 0; i < entities.length; ++i) {
entity = entities[i];
if (entity.parentID !== undefined) {
var parent = entitiesByID[entity.parentID];
if (parent !== undefined) {
entity.localPosition = Vec3.subtract(entity.position, parent.position);
delete entity.position;
}
}
}

View file

@ -0,0 +1,840 @@
{
"Entities": [
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{02f39515-cab4-41d5-b315-5fb41613f844}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750446,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -5.1684012413024902,
"y": 0.54034698009490967,
"z": -11.257695198059082
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": -1.3604279756546021,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 2,
"y": 0.69999998807907104,
"z": 0.0099999997764825821
},
"id": "{3eae601e-3c6e-49ab-8f40-dedee32f7573}",
"lastEdited": 1487894036038423,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayScore",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 1.5265679359436035,
"z": -9.5913219451904297
},
"queryAACube": {
"scale": 2.118985652923584,
"x": -5.1109838485717773,
"y": -1803.69189453125,
"z": -26.774648666381836
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"score\"}"
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.71920669078826904,
"y": 3.3160061836242676,
"z": 2.2217941284179688
},
"id": "{04288f77-64df-4323-ac38-9c1960a393a5}",
"lastEdited": 1487893058314990,
"lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx",
"name": "SB.StartButton",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -9.8358345031738281,
"y": 0.45674961805343628,
"z": -13.044205665588379
},
"queryAACube": {
"scale": 4.0558013916015625,
"x": -7.844393253326416,
"y": -1805.730224609375,
"z": -31.195960998535156
},
"rotation": {
"w": 1,
"x": 1.52587890625e-05,
"y": 1.52587890625e-05,
"z": 1.52587890625e-05
},
"script": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js",
"shapeType": "static-mesh",
"type": "Model",
"userData": "{\"grabbableKey\":{\"wantsTrigger\":true}}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 2,
"y": 0.69999998807907104,
"z": 0.0099999997764825821
},
"id": "{1196096f-bcc9-4b19-970d-605113474c1b}",
"lastEdited": 1487894037900323,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayHighScore",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 0.26189804077148438,
"z": -9.5913219451904297
},
"queryAACube": {
"scale": 2.118985652923584,
"x": -5.11102294921875,
"y": -1804.95654296875,
"z": -26.77461051940918
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"highscore\"}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 1.4120937585830688,
"y": 0.71569448709487915,
"z": 0.0099999997764825821
},
"id": "{293c294d-1df5-461e-82a3-66abee852d44}",
"lastEdited": 1487894033695485,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"lineHeight": 0.5,
"name": "SB.DisplayWave",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 1.5265679359436035,
"z": -7.2889409065246582
},
"queryAACube": {
"scale": 1.5831384658813477,
"x": -4.8431310653686523,
"y": -1803.4239501953125,
"z": -24.204343795776367
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"wave\"}"
},
{
"backgroundColor": {
"blue": 65,
"green": 78,
"red": 82
},
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 1.4120937585830688,
"y": 0.71569448709487915,
"z": 0.0099999997764825821
},
"id": "{379afa7b-c668-4c4e-b290-e5c37fb3440c}",
"lastEdited": 1487893055310428,
"lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}",
"lineHeight": 0.5,
"name": "SB.DisplayLives",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -8.0707607269287109,
"y": 0.26189804077148438,
"z": -7.2889409065246582
},
"queryAACube": {
"scale": 1.5831384658813477,
"x": -4.8431692123413086,
"y": -1804.6885986328125,
"z": -24.204303741455078
},
"rotation": {
"w": 0.70708787441253662,
"x": -1.52587890625e-05,
"y": 0.70708787441253662,
"z": -1.52587890625e-05
},
"text": "0",
"type": "Text",
"userData": "{\"displayType\":\"lives\"}"
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{760e81a1-a804-4f5e-9769-393d021fc8fe}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234633,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -1.89238440990448,
"y": -5.3368110656738281,
"z": 11.512755393981934
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.9147146940231323,
"y": -1809.7066650390625,
"z": -4.8219971656799316
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{0a76d0ac-6353-467b-8edc-56417d5a987c}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440235124,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 3.6569130420684814,
"y": -5.3365960121154785,
"z": 10.01292610168457
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 7.4640579223632812,
"y": -1809.7066650390625,
"z": -6.3216567039489746
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{f8549c8a-e646-4feb-bbaf-70e7d5be755a}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440235339,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 8.8902750015258789,
"y": -5.3364419937133789,
"z": 10.195274353027344
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 12.697414398193359,
"y": -1809.7066650390625,
"z": -6.1391491889953613
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{f3aea4ae-4445-4a2d-8d61-e9fd72f04008}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751269,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -2.5027251243591309,
"y": 0.54042834043502808,
"z": -11.257777214050293
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.3052481412887573,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{cc1ac907-124b-4372-8c4c-82d175546725}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751135,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 2.7972855567932129,
"y": 0.54059004783630371,
"z": -11.257938385009766
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 6.6052589416503906,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{e25ce690-e267-4c51-80a0-f63a72474b8a}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708751527,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.17114110291004181,
"y": 0.54050993919372559,
"z": -11.257858276367188
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 3.979114294052124,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{91ee2285-38f8-4795-b6bc-7abc8dcde07c}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750806,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 5.4656705856323242,
"y": 0.54067152738571167,
"z": -11.258020401000977
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 9.2736434936523438,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{d81e5fae-8a8d-4186-bbd2-0c3ae737b0f2}",
"ignoreForCollisions": 1,
"lastEdited": 1487892552671000,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 9.6099967956542969,
"y": 0.64012420177459717,
"z": -9.9802846908569336
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 13.417934417724609,
"y": -1803.730712890625,
"z": -26.314868927001953
},
"rotation": {
"w": 0.22495110332965851,
"x": -2.9734959753113799e-05,
"y": 0.97437006235122681,
"z": 2.9735869247815572e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{7056e21e-bce6-4c4b-bbca-36bea1dce303}",
"ignoreForCollisions": 1,
"lastEdited": 1487892708750993,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.BowSpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 8.1799373626708984,
"y": 0.54075431823730469,
"z": -11.258102416992188
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 11.987911224365234,
"y": -1803.830078125,
"z": -27.592727661132812
},
"rotation": {
"w": 0.17366324365139008,
"x": 4.9033405957743526e-07,
"y": -0.98480510711669922,
"z": -2.9563087082351558e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{ed073620-e304-4b8e-b12a-5371b595bbf6}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234415,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": -2.3618791103363037,
"y": -2.0691573619842529,
"z": 11.254574775695801
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 1.4453276395797729,
"y": -1806.43896484375,
"z": -5.0802912712097168
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"collidesWith": "",
"collisionMask": 0,
"collisionless": 1,
"color": {
"blue": 171,
"green": 50,
"red": 62
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 0.24400754272937775,
"y": 0.24400754272937775,
"z": 0.24400754272937775
},
"id": "{32ed7820-c386-4da1-b676-7e63762861a3}",
"ignoreForCollisions": 1,
"lastEdited": 1487892440234854,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.EnemySpawn",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.64757472276687622,
"y": -2.5217375755310059,
"z": 10.08248233795166
},
"queryAACube": {
"scale": 0.42263346910476685,
"x": 4.454803466796875,
"y": -1806.8917236328125,
"z": -6.2522788047790527
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 26.619264602661133,
"y": 14.24090576171875,
"z": 39.351066589355469
},
"id": "{d4c8f577-944d-4d50-ac85-e56387c0ef0a}",
"lastEdited": 1487892440231278,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx",
"name": "SB.Platform",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.097909502685070038,
"y": -1.0163799524307251,
"z": 2.0321114063262939
},
"queryAACube": {
"scale": 49.597328186035156,
"x": -20.681917190551758,
"y": -1829.9739990234375,
"z": -38.890060424804688
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shapeType": "static-mesh",
"type": "Model"
},
{
"clientOnly": 0,
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 23.341892242431641,
"y": 12.223045349121094,
"z": 32.012016296386719
},
"friction": 1,
"id": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"lastEdited": 1487892440231832,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx",
"name": "SB.Scoreboard",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"queryAACube": {
"scale": 41.461017608642578,
"x": -20.730508804321289,
"y": -20.730508804321289,
"z": -20.730508804321289
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"serverScripts": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js",
"shapeType": "static-mesh",
"type": "Model"
},
{
"clientOnly": 0,
"color": {
"blue": 0,
"green": 0,
"red": 255
},
"created": "2017-02-23T23:28:32Z",
"dimensions": {
"x": 15.710711479187012,
"y": 4.7783288955688477,
"z": 1.6129581928253174
},
"id": "{84cdff6e-a68d-4bbf-8660-2d6a8c2f1fd0}",
"lastEdited": 1487892440231522,
"lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}",
"name": "SB.GateCollider",
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}",
"position": {
"x": 0.31728419661521912,
"y": -4.3002614974975586,
"z": -12.531644821166992
},
"queryAACube": {
"scale": 16.50031852722168,
"x": -3.913693904876709,
"y": -1816.709716796875,
"z": -36.905204772949219
},
"rotation": {
"w": 1,
"x": -1.52587890625e-05,
"y": -1.52587890625e-05,
"z": -1.52587890625e-05
},
"shape": "Cube",
"type": "Box",
"visible": 0
}
],
"Version": 68
}

View file

@ -0,0 +1,621 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals ShortbowGameManager:true, utils */
Script.include('utils.js');
// +--------+ +-----------+ +-----------------+
// | | | |<-----+ |
// | IDLE +----->| PLAYING | | BETWEEN_WAVES |
// | | | +----->| |
// +--------+ +-----+-----+ +-----------------+
// ^ |
// | v
// | +-------------+
// | | |
// +---------+ GAME_OVER |
// | |
// +-------------+
var GAME_STATES = {
IDLE: 0,
PLAYING: 1,
BETWEEN_WAVES: 2,
GAME_OVER: 3
};
// Load the sounds that we will be using in the game so they are ready to be
// used when we need them.
var BEGIN_BUILDING_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOn.wav"));
var GAME_OVER_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOver.wav"));
var WAVE_COMPLETE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/waveComplete.wav"));
var EXPLOSION_SOUND = SoundCache.getSound(Script.resolvePath("sounds/explosion.wav"));
var TARGET_HIT_SOUND = SoundCache.getSound(Script.resolvePath("sounds/targetHit.wav"));
var ESCAPE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/escape.wav"));
const STARTING_NUMBER_OF_LIVES = 6;
const ENEMIES_PER_WAVE_MULTIPLIER = 2;
const POINTS_PER_KILL = 100;
const ENEMY_SPEED = 3.0;
// Encode a set of key-value pairs into a param string. Does NOT do any URL escaping.
function encodeURLParams(params) {
var paramPairs = [];
for (var key in params) {
paramPairs.push(key + "=" + params[key]);
}
return paramPairs.join("&");
}
function sendAndUpdateHighScore(entityID, score, wave, numPlayers, onResposeReceived) {
const URL = 'https://script.google.com/macros/s/AKfycbwbjCm9mGd1d5BzfAHmVT_XKmWyUYRkjCEqDOKm1368oM8nqWni/exec';
print("Sending high score");
const paramString = encodeURLParams({
entityID: entityID,
score: score,
wave: wave,
numPlayers: numPlayers
});
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
print("ready state: ", request.readyState, request.status, request.readyState === request.DONE, request.response);
if (request.readyState === request.DONE && request.status === 200) {
print("Got response for high score: ", request.response);
var response = JSON.parse(request.responseText);
if (response.highScore !== undefined) {
onResposeReceived(response.highScore);
}
}
};
request.open('GET', URL + "?" + paramString);
request.timeout = 10000;
request.send();
}
function findChildrenWithName(parentID, name) {
var childrenIDs = Entities.getChildrenIDs(parentID);
var matchingIDs = [];
for (var i = 0; i < childrenIDs.length; ++i) {
var id = childrenIDs[i];
var childName = Entities.getEntityProperties(id, 'name').name;
if (childName === name) {
matchingIDs.push(id);
}
}
return matchingIDs;
}
function getPropertiesForEntities(entityIDs, desiredProperties) {
var properties = [];
for (var i = 0; i < entityIDs.length; ++i) {
properties.push(Entities.getEntityProperties(entityIDs[i], desiredProperties));
}
return properties;
}
var baseEnemyProperties = {
"name": "SB.Enemy",
"damping": 0,
"linearDamping": 0,
"angularDamping": 0,
"acceleration": {
"x": 0,
"y": -9,
"z": 0
},
"angularVelocity": {
"x": -0.058330666273832321,
"y": -0.77943277359008789,
"z": -2.1163818836212158
},
"clientOnly": 0,
"collisionsWillMove": 1,
"dimensions": {
"x": 0.63503998517990112,
"y": 0.63503998517990112,
"z": 0.63503998517990112
},
"dynamic": 1,
"gravity": {
"x": 0,
"y": -15,
"z": 0
},
"lifetime": 30,
"id": "{ed8f7339-8bbd-4750-968e-c3ceb9d64721}",
"modelURL": Script.resolvePath("models/Amber.fbx"),
"owningAvatarID": "{00000000-0000-0000-0000-000000000000}",
"queryAACube": {
"scale": 1.0999215841293335,
"x": -0.54996079206466675,
"y": -0.54996079206466675,
"z": -0.54996079206466675
},
"shapeType": "sphere",
"type": "Model",
"script": Script.resolvePath('enemyClientEntity.js'),
"serverScripts": Script.resolvePath('enemyServerEntity.js')
};
function searchForChildren(parentID, names, callback, timeoutMs) {
// Map from name to entity ID for the children that have been found
var foundEntities = {};
for (var i = 0; i < names.length; ++i) {
foundEntities[names[i]] = null;
}
const CHECK_EVERY_MS = 500;
const maxChecks = Math.ceil(timeoutMs / CHECK_EVERY_MS);
var check = 0;
var intervalID = Script.setInterval(function() {
check++;
var childrenIDs = Entities.getChildrenIDs(parentID);
print("\tNumber of children:", childrenIDs.length);
for (var i = 0; i < childrenIDs.length; ++i) {
print("\t\t" + i + ".", Entities.getEntityProperties(childrenIDs[i]).name);
var id = childrenIDs[i];
var name = Entities.getEntityProperties(id, 'name').name;
var idx = names.indexOf(name);
if (idx > -1) {
foundEntities[name] = id;
print(name, id);
names.splice(idx, 1);
}
}
if (names.length === 0 || check >= maxChecks) {
Script.clearInterval(intervalID);
callback(foundEntities);
}
}, CHECK_EVERY_MS);
}
ShortbowGameManager = function(rootEntityID, bowPositions, spawnPositions) {
print("Starting game manager");
var self = this;
this.gameState = GAME_STATES.IDLE;
this.rootEntityID = rootEntityID;
this.bowPositions = bowPositions;
this.rootPosition = null;
this.spawnPositions = spawnPositions;
this.loadedChildren = false;
const START_BUTTON_NAME = 'SB.StartButton';
const WAVE_DISPLAY_NAME = 'SB.DisplayWave';
const SCORE_DISPLAY_NAME = 'SB.DisplayScore';
const LIVES_DISPLAY_NAME = 'SB.DisplayLives';
const HIGH_SCORE_DISPLAY_NAME = 'SB.DisplayHighScore';
const SEARCH_FOR_CHILDREN_TIMEOUT = 5000;
searchForChildren(rootEntityID, [
START_BUTTON_NAME,
WAVE_DISPLAY_NAME,
SCORE_DISPLAY_NAME,
LIVES_DISPLAY_NAME,
HIGH_SCORE_DISPLAY_NAME
], function(children) {
self.loadedChildren = true;
self.startButtonID = children[START_BUTTON_NAME];
self.waveDisplayID = children[WAVE_DISPLAY_NAME];
self.scoreDisplayID = children[SCORE_DISPLAY_NAME];
self.livesDisplayID = children[LIVES_DISPLAY_NAME];
self.highScoreDisplayID = children[HIGH_SCORE_DISPLAY_NAME];
sendAndUpdateHighScore(self.rootEntityID, self.score, self.waveNumber, 1, self.setHighScore.bind(self));
self.reset();
}, SEARCH_FOR_CHILDREN_TIMEOUT);
// Gameplay state
this.waveNumber = 0;
this.livesLeft = STARTING_NUMBER_OF_LIVES;
this.score = 0;
this.nextWaveTimer = null;
this.spawnEnemyTimers = [];
this.remainingEnemies = [];
this.bowIDs = [];
this.startButtonChannelName = 'button-' + this.rootEntityID;
// Entity client and server scripts will send messages to this channel
this.commChannelName = "shortbow-" + this.rootEntityID;
Messages.subscribe(this.commChannelName);
Messages.messageReceived.connect(this, this.onReceivedMessage);
print("Listening on: ", this.commChannelName);
Messages.sendMessage(this.commChannelName, 'hi');
};
ShortbowGameManager.prototype = {
reset: function() {
Entities.editEntity(this.startButtonID, { visible: true });
},
cleanup: function() {
Messages.unsubscribe(this.commChannelName);
Messages.messageReceived.disconnect(this, this.onReceivedMessage);
if (this.checkEnemiesTimer) {
Script.clearInterval(this.checkEnemiesTimer);
this.checkEnemiesTimer = null;
}
for (var i = this.bowIDs.length - 1; i >= 0; i--) {
Entities.deleteEntity(this.bowIDs[i]);
}
this.bowIDs = [];
for (i = 0; i < this.remainingEnemies.length; i++) {
Entities.deleteEntity(this.remainingEnemies[i].id);
}
this.remainingEnemies = [];
this.gameState = GAME_STATES.IDLE;
},
startGame: function() {
if (this.gameState !== GAME_STATES.IDLE) {
print("shortbowGameManagerManager.js | Error, trying to start game when not in idle state");
return;
}
if (this.loadedChildren === false) {
print('shortbowGameManager.js | Children have not loaded, not allowing game to start');
return;
}
print("Game started!!");
this.rootPosition = Entities.getEntityProperties(this.rootEntityID, 'position').position;
Entities.editEntity(this.startButtonID, { visible: false });
// Spawn bows
var bowSpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.BowSpawn');
var bowSpawnProperties = getPropertiesForEntities(bowSpawnEntityIDs, ['position', 'rotation']);
for (var i = 0; i < bowSpawnProperties.length; ++i) {
const props = bowSpawnProperties[i];
Vec3.print("Creating bow: " + i, props.position);
this.bowIDs.push(Entities.addEntity({
"position": props.position,
"rotation": props.rotation,
"collisionsWillMove": 1,
"compoundShapeURL": Script.resolvePath("bow/bow_collision_hull.obj"),
"created": "2016-09-01T23:57:55Z",
"dimensions": {
"x": 0.039999999105930328,
"y": 1.2999999523162842,
"z": 0.20000000298023224
},
"dynamic": 1,
"gravity": {
"x": 0,
"y": -9.8,
"z": 0
},
"modelURL": Script.resolvePath("bow/bow-deadly.fbx"),
"name": "WG.Hifi-Bow",
"script": Script.resolvePath("bow/bow.js"),
"shapeType": "compound",
"type": "Model",
"userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}"
}));
}
// Initialize game state
this.waveNumber = 0;
this.setScore(0);
this.setLivesLeft(STARTING_NUMBER_OF_LIVES);
this.nextWaveTimer = Script.setTimeout(this.startNextWave.bind(this), 100);
this.spawnEnemyTimers = [];
this.checkEnemiesTimer = null;
this.remainingEnemies = [];
// SpawnQueue is a list of enemies left to spawn. Each entry looks like:
//
// { spawnAt: 1000, position: { x: 0, y: 0, z: 0 } }
//
// where spawnAt is the number of millseconds after the start of the wave
// to spawn the enemy. The list is sorted by spawnAt, ascending.
this.spawnQueue = [];
this.gameState = GAME_STATES.BETWEEN_WAVES;
Audio.playSound(BEGIN_BUILDING_SOUND, {
volume: 1.0,
position: this.rootPosition
});
},
startNextWave: function() {
if (this.gameState !== GAME_STATES.BETWEEN_WAVES) {
return;
}
print("Starting next wave");
this.gameState = GAME_STATES.PLAYING;
this.waveNumber++;
this.remainingEnemies= [];
this.spawnQueue = [];
this.spawnStartTime = Date.now();
Entities.editEntity(this.waveDisplayID, { text: this.waveNumber});
var numberOfEnemiesLeftToSpawn = this.waveNumber * ENEMIES_PER_WAVE_MULTIPLIER;
var delayBetweenSpawns = 2000 / Math.max(1, Math.log(this.waveNumber));
var currentDelay = 2000;
print("Number of enemies:", numberOfEnemiesLeftToSpawn);
this.checkEnemiesTimer = Script.setInterval(this.checkEnemies.bind(this), 100);
var enemySpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.EnemySpawn');
var enemySpawnProperties = getPropertiesForEntities(enemySpawnEntityIDs, ['position', 'rotation']);
for (var i = 0; i < numberOfEnemiesLeftToSpawn; ++i) {
print("Adding enemy");
var idx = Math.floor(Math.random() * enemySpawnProperties.length);
var props = enemySpawnProperties[idx];
this.spawnQueue.push({
spawnAt: currentDelay,
position: props.position,
rotation: props.rotation,
velocity: Vec3.multiply(ENEMY_SPEED, Quat.getFront(props.rotation))
});
currentDelay += delayBetweenSpawns;
}
print("Starting wave", this.waveNumber);
},
checkWaveComplete: function() {
if (this.gameState !== GAME_STATES.PLAYING) {
return;
}
if (this.spawnQueue.length === 0 && this.remainingEnemies.length === 0) {
this.gameState = GAME_STATES.BETWEEN_WAVES;
Script.setTimeout(this.startNextWave.bind(this), 5000);
Script.clearInterval(this.checkEnemiesTimer);
this.checkEnemiesTimer = null;
// Play after 1.5s to let other sounds finish playing
var self = this;
Script.setTimeout(function() {
Audio.playSound(WAVE_COMPLETE_SOUND, {
volume: 1.0,
position: self.rootPosition
});
}, 1500);
}
},
setHighScore: function(highScore) {
print("Setting high score to:", this.highScoreDisplayID, highScore);
Entities.editEntity(this.highScoreDisplayID, { text: highScore });
},
setLivesLeft: function(lives) {
lives = Math.max(0, lives);
this.livesLeft = lives;
Entities.editEntity(this.livesDisplayID, { text: this.livesLeft });
},
setScore: function(score) {
this.score = score;
Entities.editEntity(this.scoreDisplayID, { text: this.score });
},
checkEnemies: function() {
if (this.gameState !== GAME_STATES.PLAYING) {
return;
}
// Check the spawn queueu to see if there are any enemies that need to
// be spawned
var waveElapsedTime = Date.now() - this.spawnStartTime;
while (this.spawnQueue.length > 0 && waveElapsedTime > this.spawnQueue[0].spawnAt) {
baseEnemyProperties.position = this.spawnQueue[0].position;
baseEnemyProperties.rotation = this.spawnQueue[0].rotation;
baseEnemyProperties.velocity= this.spawnQueue[0].velocity;
baseEnemyProperties.userData = JSON.stringify({
gameChannel: this.commChannelName,
grabbableKey: {
grabbable: false
}
});
var entityID = Entities.addEntity(baseEnemyProperties);
this.remainingEnemies.push({
id: entityID,
lastKnownPosition: baseEnemyProperties.position,
lastHeartbeat: Date.now()
});
this.spawnQueue.splice(0, 1);
Script.setTimeout(function() {
const JUMP_SPEED = 5.0;
var velocity = Entities.getEntityProperties(entityID, 'velocity').velocity;
velocity.y += JUMP_SPEED;
Entities.editEntity(entityID, { velocity: velocity });
}, 500 + Math.random() * 4000);
}
// Check the list of remaining enemies to see if any are too far away
// or haven't been heard from in awhile - if so, delete them.
var enemiesEscaped = false;
const MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS = 5000;
const MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY = 200;
for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
var enemy = this.remainingEnemies[i];
var timeSinceLastHeartbeat = Date.now() - enemy.lastHeartbeat;
var distance = Vec3.distance(enemy.lastKnownPosition, this.rootPosition);
if (timeSinceLastHeartbeat > MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS
|| distance > MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY) {
print("EXPIRING: ", enemy.id);
Entities.deleteEntity(enemy.id);
this.remainingEnemies.splice(i, 1);
Audio.playSound(TARGET_HIT_SOUND, {
volume: 1.0,
position: this.rootPosition
});
this.setScore(this.score + POINTS_PER_KILL);
enemiesEscaped = true;
}
}
if (enemiesEscaped) {
this.checkWaveComplete();
}
},
endGame: function() {
if (this.gameState !== GAME_STATES.PLAYING) {
return;
}
var self = this;
Script.setTimeout(function() {
Audio.playSound(GAME_OVER_SOUND, {
volume: 1.0,
position: self.rootPosition
});
}, 1500);
this.gameState = GAME_STATES.GAME_OVER;
print("GAME OVER");
// Update high score
sendAndUpdateHighScore(this.rootEntityID, this.score, this.waveNumber, 1, this.setHighScore.bind(this));
// Cleanup
Script.clearTimeout(this.nextWaveTimer);
this.nextWaveTimer = null;
var i;
for (i = 0; i < this.spawnEnemyTimers.length; ++i) {
Script.clearTimeout(this.spawnEnemyTimers[i]);
}
this.spawnEnemyTimers = [];
Script.clearInterval(this.checkEnemiesTimer);
this.checkEnemiesTimer = null;
for (i = this.bowIDs.length - 1; i >= 0; i--) {
var id = this.bowIDs[i];
print("Checking bow: ", id);
var userData = utils.parseJSON(Entities.getEntityProperties(id, 'userData').userData);
var bowIsHeld = userData.grabKey !== undefined && userData.grabKey !== undefined && userData.grabKey.refCount > 0;
print("Held: ", bowIsHeld);
if (!bowIsHeld) {
Entities.deleteEntity(id);
this.bowIDs.splice(i, 1);
}
}
for (i = 0; i < this.remainingEnemies.length; i++) {
Entities.deleteEntity(this.remainingEnemies[i].id);
}
this.remainingEnemies = [];
// Wait a short time before showing the start button so that any current sounds
// can finish playing.
const WAIT_TO_REENABLE_GAME_TIMEOUT_MS = 3000;
Script.setTimeout(function() {
Entities.editEntity(this.startButtonID, { visible: true });
this.gameState = GAME_STATES.IDLE;
}.bind(this), WAIT_TO_REENABLE_GAME_TIMEOUT_MS);
},
onReceivedMessage: function(channel, messageJSON, senderID) {
if (channel === this.commChannelName) {
var message = utils.parseJSON(messageJSON);
if (message === undefined) {
print("shortbowGameManager.js | Received non-json message:", JSON.stringify(messageJSON));
return;
}
switch (message.type) {
case 'start-game':
this.startGame();
break;
case 'enemy-killed':
this.onEnemyKilled(message.entityID, message.position);
break;
case 'enemy-escaped':
this.onEnemyEscaped(message.entityID);
break;
case 'enemy-heartbeat':
this.onEnemyHeartbeat(message.entityID, message.position);
break;
default:
print("shortbowGameManager.js | Ignoring unknown message type: ", message.type);
break;
}
}
},
onEnemyKilled: function(entityID, position) {
if (this.gameState !== GAME_STATES.PLAYING) {
return;
}
for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
var enemy = this.remainingEnemies[i];
if (enemy.id === entityID) {
this.remainingEnemies.splice(i, 1);
Audio.playSound(TARGET_HIT_SOUND, {
volume: 1.0,
position: this.rootPosition
});
// Update score
this.setScore(this.score + POINTS_PER_KILL);
print("SCORE: ", this.score);
this.checkWaveComplete();
break;
}
}
},
onEnemyEscaped: function(entityID, position) {
if (this.gameState !== GAME_STATES.PLAYING) {
return;
}
var enemiesEscaped = false;
for (var i = this.remainingEnemies.length - 1; i >= 0; --i) {
var enemy = this.remainingEnemies[i];
if (enemy.id === entityID) {
Entities.deleteEntity(enemy.id);
this.remainingEnemies.splice(i, 1);
this.setLivesLeft(this.livesLeft - 1);
Audio.playSound(ESCAPE_SOUND, {
volume: 1.0,
position: this.rootPosition
});
enemiesEscaped = true;
}
}
if (this.livesLeft <= 0) {
this.endGame();
} else if (enemiesEscaped) {
this.checkWaveComplete();
}
},
onEnemyHeartbeat: function(entityID, position) {
for (var i = 0; i < this.remainingEnemies.length; i++) {
var enemy = this.remainingEnemies[i];
if (enemy.id === entityID) {
enemy.lastHeartbeat = Date.now();
enemy.lastKnownPosition = position;
break;
}
}
}
};

View file

@ -0,0 +1,42 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals TEMPLATES:true, SHORTBOW_ENTITIES, ShortbowGameManager */
(function() {
Script.include('utils.js');
Script.include('shortbow.js');
Script.include('shortbowGameManager.js');
TEMPLATES = SHORTBOW_ENTITIES.Entities;
this.entityID = null;
var gameManager = null;
this.preload = function(entityID) {
this.entityID = entityID;
var bowPositions = [];
var spawnPositions = [];
for (var i = 0; i < TEMPLATES.length; ++i) {
var template = TEMPLATES[i];
if (template.name === "SB.BowSpawn") {
bowPositions.push(template.localPosition);
} else if (template.name === "SB.EnemySpawn") {
spawnPositions.push(template.localPosition);
}
}
gameManager = new ShortbowGameManager(this.entityID, bowPositions, spawnPositions);
};
this.unload = function() {
if (gameManager) {
gameManager.cleanup();
gameManager = null;
}
};
});

View file

@ -0,0 +1,210 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals utils, SHORTBOW_ENTITIES, TEMPLATES:true */
Script.include('utils.js');
Script.include('shortbow.js');
Script.include('shortbowGameManager.js');
TEMPLATES = SHORTBOW_ENTITIES.Entities;
// Merge two objects into a new object. If a key name appears in both a and b,
// the value in a will be used.
//
// @param {object} a
// @param {object} b
// @returns {object} The new object
function mergeObjects(a, b) {
var obj = {};
var key;
for (key in b) {
obj[key] = b[key];
}
for (key in a) {
obj[key] = a[key];
}
return obj;
}
// Spawn an entity from a template.
//
// The overrides can be used to override or add properties in the template. For instance,
// it's common to override the `position` property so that you can set the position
// of the entity to be spawned.
//
// @param {string} templateName The name of the template to spawn
// @param {object} overrides An object containing properties that will override
// any properties set in the template.
function spawnTemplate(templateName, overrides) {
var template = getTemplate(templateName);
if (template === null) {
print("ERROR, unknown template name:", templateName);
return null;
}
print("Spawning: ", templateName);
var properties = mergeObjects(overrides, template);
return Entities.addEntity(properties);
}
function spawnTemplates(templateName, overrides) {
var templates = getTemplates(templateName);
if (template.length === 0) {
print("ERROR, unknown template name:", templateName);
return [];
}
var spawnedEntities = [];
for (var i = 0; i < templates.length; ++i) {
print("Spawning: ", templateName);
var properties = mergeObjects(overrides, templates[i]);
spawnedEntities.push(Entities.addEntity(properties));
}
return spawnedEntities;
}
// TEMPLATES contains a dictionary of different named entity templates. An entity
// template is just a list of properties.
//
// @param name Name of the template to get
// @return {object} The matching template, or null if not found
function getTemplate(name) {
for (var i = 0; i < TEMPLATES.length; ++i) {
if (TEMPLATES[i].name === name) {
return TEMPLATES[i];
}
}
return null;
}
function getTemplates(name) {
var templates = [];
for (var i = 0; i < TEMPLATES.length; ++i) {
if (TEMPLATES[i].name === name) {
templates.push(TEMPLATES[i]);
}
}
return templates;
}
// Cleanup Shortbow template data
for (var i = 0; i < TEMPLATES.length; ++i) {
var template = TEMPLATES[i];
// Fixup model url
if (template.type === "Model") {
var urlParts = template.modelURL.split("/");
var filename = urlParts[urlParts.length - 1];
var newURL = Script.resolvePath("models/" + filename);
print("Updated url", template.modelURL, "to", newURL);
template.modelURL = newURL;
}
}
var entityIDs = [];
var scoreboardID = null;
var buttonID = null;
var waveDisplayID = null;
var scoreDisplayID = null;
var highScoreDisplayID = null;
var livesDisplayID = null;
var platformID = null;
function createLocalGame() {
var rootPosition = utils.findSurfaceBelowPosition(MyAvatar.position);
rootPosition.y += 6.11;
scoreboardID = spawnTemplate("SB.Scoreboard", {
position: rootPosition
});
entityIDs.push(scoreboardID);
// Create start button
buttonID = spawnTemplate("SB.StartButton", {
parentID: scoreboardID,
script: Script.resolvePath("startGameButtonClientEntity.js"),
userData: JSON.stringify({
grabbableKey: {
wantsTrigger: true
}
})
});
entityIDs.push(buttonID);
waveDisplayID = spawnTemplate("SB.DisplayWave", {
parentID: scoreboardID,
userData: JSON.stringify({
displayType: "wave"
})
});
entityIDs.push(waveDisplayID);
scoreDisplayID = spawnTemplate("SB.DisplayScore", {
parentID: scoreboardID,
userData: JSON.stringify({
displayType: "score"
})
});
entityIDs.push(scoreDisplayID);
livesDisplayID = spawnTemplate("SB.DisplayLives", {
parentID: scoreboardID,
userData: JSON.stringify({
displayType: "lives"
})
});
entityIDs.push(livesDisplayID);
highScoreDisplayID = spawnTemplate("SB.DisplayHighScore", {
parentID: scoreboardID,
userData: JSON.stringify({
displayType: "highscore"
})
});
entityIDs.push(highScoreDisplayID);
platformID = spawnTemplate("SB.Platform", {
parentID: scoreboardID
});
entityIDs.push(platformID);
spawnTemplate("SB.GateCollider", {
parentID: scoreboardID,
visible: false
});
entityIDs.push(platformID);
Entities.editEntity(scoreboardID, {
serverScripts: Script.resolvePath('shortbowServerEntity.js')
});
spawnTemplates("SB.BowSpawn", {
parentID: scoreboardID,
visible: false
});
spawnTemplates("SB.EnemySpawn", {
parentID: scoreboardID,
visible: false
});
var bowPositions = [];
var spawnPositions = [];
for (var i = 0; i < TEMPLATES.length; ++i) {
var template = TEMPLATES[i];
if (template.name === "SB.BowSpawn") {
bowPositions.push(Vec3.sum(rootPosition, template.localPosition));
Vec3.print("Pushing bow position", Vec3.sum(rootPosition, template.localPosition));
} else if (template.name === "SB.EnemySpawn") {
spawnPositions.push(Vec3.sum(rootPosition, template.localPosition));
Vec3.print("Pushing spawnposition", Vec3.sum(rootPosition, template.localPosition));
}
}
}
createLocalGame();
Script.stop();

View file

@ -0,0 +1,41 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals utils */
(function() {
Script.include('utils.js');
function StartButton() {
}
StartButton.prototype = {
preload: function(entityID) {
this.entityID = entityID;
this.commChannel = "shortbow-" + Entities.getEntityProperties(entityID, 'parentID').parentID;
Script.addEventHandler(entityID, "collisionWithEntity", this.onCollide.bind(this));
},
signalAC: function() {
Messages.sendMessage(this.commChannel, JSON.stringify({
type: 'start-game'
}));
},
onCollide: function(entityA, entityB, collision) {
var colliderName = Entities.getEntityProperties(entityB, 'name').name;
if (colliderName.indexOf("projectile") > -1) {
this.signalAC();
}
}
};
StartButton.prototype.startNearTrigger = StartButton.prototype.signalAC;
StartButton.prototype.startFarTrigger = StartButton.prototype.signalAC;
StartButton.prototype.clickDownOnEntity = StartButton.prototype.signalAC;
return new StartButton();
});

View file

@ -0,0 +1,57 @@
//
// Created by Ryan Huffman on 1/10/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals utils:true */
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
NOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof NOP
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
// Function.prototype doesn't have a prototype property
NOP.prototype = this.prototype;
}
fBound.prototype = new NOP();
return fBound;
};
}
utils = {
parseJSON: function(json) {
try {
return JSON.parse(json);
} catch (e) {
return undefined;
}
},
findSurfaceBelowPosition: function(pos) {
var result = Entities.findRayIntersection({
origin: pos,
direction: { x: 0.0, y: -1.0, z: 0.0 }
}, true);
if (result.intersects) {
return result.intersection;
}
return pos;
}
};