UNPKG

@nxg-org/mineflayer-physics-util

Version:

Provides functionality for more accurate entity and projectile tracking.

1,042 lines (1,041 loc) 76.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.BotcraftPhysics = void 0; const mineflayer_util_plugin_1 = require("@nxg-org/mineflayer-util-plugin"); const vec3_1 = require("vec3"); const physicsUtils_1 = require("../../util/physicsUtils"); const math = __importStar(require("../info/math")); const attributes = __importStar(require("../info/attributes")); const states_1 = require("../states"); function extractAttribute(ctx, genericName) { const data = ctx.data.attributesByName[genericName]; if (data == null) return null; if (ctx.supportFeature("attributesPrefixedByMinecraft")) { return `minecraft:${data.resource}`; } else { return data.resource; } } /** * Looking at this code, it's too specified towards players. * * I will eventually split this code into PlayerState and bot.entityState, where bot.entityState contains fewer controls. */ class BotcraftPhysics { constructor(mcData) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; this.data = mcData; const blocksByName = mcData.blocksByName; this.supportFeature = (0, physicsUtils_1.makeSupportFeature)(mcData); this.movementSpeedAttribute = extractAttribute(this, "movementSpeed"); this.movementEfficiencyAttribute = extractAttribute(this, "movementEfficiency"); this.jumpStrengthAttribute = extractAttribute(this, "jumpStrength"); this.waterMovementEfficiencyAttribute = extractAttribute(this, "waterMovementEfficiency"); this.stepHeightAttribute = extractAttribute(this, "stepHeight"); this.blockSlipperiness = {}; this.slimeBlockId = blocksByName.slime_block ? blocksByName.slime_block.id : blocksByName.slime.id; this.blockSlipperiness[this.slimeBlockId] = 0.8; this.blockSlipperiness[blocksByName.ice.id] = 0.98; this.blockSlipperiness[blocksByName.packed_ice.id] = 0.98; // 1.9+ if (blocksByName.frosted_ice) this.blockSlipperiness[blocksByName.frosted_ice.id] = 0.98; // 1.13+ if (blocksByName.blue_ice) this.blockSlipperiness[blocksByName.blue_ice.id] = 0.989; this.bedId = (_d = (_b = (_a = blocksByName.bed) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : (_c = blocksByName.white_bed) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : -1; this.soulsandId = blocksByName.soul_sand.id; this.scaffoldId = (_f = (_e = blocksByName.scaffolding) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : -1; // 1.14+ this.berryBushId = (_h = (_g = blocksByName.sweet_berry_bush) === null || _g === void 0 ? void 0 : _g.id) !== null && _h !== void 0 ? _h : -1; // 1.14+ this.powderSnowId = (_k = (_j = blocksByName.powder_snow) === null || _j === void 0 ? void 0 : _j.id) !== null && _k !== void 0 ? _k : -1; this.honeyblockId = blocksByName.honey_block ? blocksByName.honey_block.id : -1; // 1.15+ this.webId = blocksByName.cobweb ? blocksByName.cobweb.id : blocksByName.web.id; this.waterId = blocksByName.water.id; this.lavaId = blocksByName.lava.id; this.ladderId = blocksByName.ladder.id; this.vineId = blocksByName.vine.id; this.waterLike = new Set(); if (blocksByName.seagrass) this.waterLike.add(blocksByName.seagrass.id); // 1.13+ if (blocksByName.tall_seagrass) this.waterLike.add(blocksByName.tall_seagrass.id); // 1.13+ if (blocksByName.kelp) this.waterLike.add(blocksByName.kelp.id); // 1.13+ if (blocksByName.kelp_plant) this.waterLike.add(blocksByName.kelp_plant.id); // 1.13+ this.bubblecolumnId = blocksByName.bubble_column ? blocksByName.bubble_column.id : -1; // 1.13+ if (blocksByName.bubble_column) this.waterLike.add(this.bubblecolumnId); this.statusEffectNames = {}; // mmm, speed. this.enchantmentNames = {}; //mmm, double speed. let ind = 0; const tmp = (0, physicsUtils_1.getStatusEffectNamesForVersion)(this.supportFeature); for (const key in tmp) { this.statusEffectNames[ind] = tmp[key]; ind++; } Object.freeze(this.statusEffectNames); ind = 0; const tmp1 = (0, physicsUtils_1.getEnchantmentNamesForVersion)(this.supportFeature); for (const key in tmp1) { this.enchantmentNames[ind] = tmp1[key]; } Object.freeze(this.enchantmentNames); } getEntityBB(entity, pos) { const w = entity.getHalfWidth(); return new mineflayer_util_plugin_1.AABB(-w, 0, -w, w, entity.height, w).translate(pos.x, pos.y, pos.z); } setPositionToBB(entity, bb, pos) { const halfWidth = entity.getHalfWidth(); pos.x = bb.minX + halfWidth; pos.y = bb.minY; pos.z = bb.minZ + halfWidth; } getSurroundingBBs(queryBB, world, underlying = true) { const surroundingBBs = []; const cursor = new vec3_1.Vec3(0, 0, 0); for (cursor.y = Math.floor(queryBB.minY) - Number(underlying); cursor.y <= Math.floor(queryBB.maxY); ++cursor.y) { for (cursor.z = Math.floor(queryBB.minZ); cursor.z <= Math.floor(queryBB.maxZ); ++cursor.z) { for (cursor.x = Math.floor(queryBB.minX); cursor.x <= Math.floor(queryBB.maxX); ++cursor.x) { const block = world.getBlock(cursor); if (block != null) { const blockPos = block.position; for (const shape of block.shapes) { const blockBB = new mineflayer_util_plugin_1.AABB(shape[0], shape[1], shape[2], shape[3], shape[4], shape[5]); blockBB.translate(blockPos.x, blockPos.y, blockPos.z); surroundingBBs.push(blockBB); } } } } } return surroundingBBs; } getWaterInBBs(bb, world) { const bbs = []; const cursor = new vec3_1.Vec3(0, 0, 0); for (cursor.y = Math.floor(bb.minY); cursor.y <= Math.floor(bb.maxY); cursor.y++) { for (cursor.z = Math.floor(bb.minZ); cursor.z <= Math.floor(bb.maxZ); cursor.z++) { for (cursor.x = Math.floor(bb.minX); cursor.x <= Math.floor(bb.maxX); cursor.x++) { const block = world.getBlock(cursor); if (block && (block.type === this.waterId || this.waterLike.has(block.type) || block.getProperties().waterlogged)) { const waterLevel = cursor.y + 1 - this.getLiquidHeightPcent(block); if (Math.ceil(bb.maxY) >= waterLevel) { bbs.push(new mineflayer_util_plugin_1.AABB(cursor.x, cursor.y, cursor.z, cursor.x + 1, cursor.y + 1, cursor.z + 1)); } } } } } return bbs; } getEffectLevel(wantedEffect, effects) { const effectDescriptor = this.data.effectsByName[this.statusEffectNames[wantedEffect]]; if (!effectDescriptor) { return 0; } const effectInfo = effects[effectDescriptor.id]; if (!effectInfo) { return 0; } return effectInfo.amplifier + 1; } getEnchantmentLevel(wantedEnchantment, enchantments) { const enchantmentName = this.enchantmentNames[wantedEnchantment]; const enchantmentDescriptor = this.data.enchantmentsByName[enchantmentName]; if (!enchantmentDescriptor) { return 0; } for (const enchInfo of enchantments) { if (typeof enchInfo.id === "string") { if (enchInfo.id.includes(enchantmentName)) { return enchInfo.lvl; } } else if (enchInfo.id === enchantmentDescriptor.id) { return enchInfo.lvl; } } return 0; } verGreaterThan(ver) { return this.data.version[">"](ver); } verLessThan(ver) { return this.data.version["<"](ver); } worldIsFree(world, bb, ignoreLiquid) { const bbs = ignoreLiquid ? this.getSurroundingBBs(bb, world, false) : [...this.getSurroundingBBs(bb, world, false), ...this.getWaterInBBs(bb, world)]; // now we have to actually check for collisions. for (const blockBB of bbs) { if (blockBB.intersects(bb)) { const blockAt = world.getBlock(blockBB.minPoint()); // console.log('world not free due to block: ', blockAt.name, blockAt.position) return false; } } return true; } /** * 1:1 copy of the original physicsTick function from botcraft * https://github.com/adepierre/Botcraft/blob/6c572071b0237c27a85211a246ce10565ef4d80f/botcraft/src/Game/Physics/PhysicsManager.cpp#L277 * * * @param ctx * @param world */ physicsTick(ctx, world) { // Check for rocket boosting if currently in elytra flying mode const entity = ctx.state; // if (ctx.state.elytraFlying) { // // TODO: entity check for fireworks // // TODO: check if firework is attached to player // if (false) { // // player->speed += player->front_vector * 0.1 + (player->front_vector * 1.5 - player->speed) * 0.5; // } // } // for now, only check if this is a player. const playerFlag = ctx.entityType.type === "player"; this.fluidPhysics(ctx, world, true); this.fluidPhysics(ctx, world, false); // updateSwimming moved into AiStep. // separation into a new function // originally: https://github.com/adepierre/Botcraft/blob/6c572071b0237c27a85211a246ce10565ef4d80f/botcraft/src/Game/Physics/PhysicsManager.cpp#L325 if (playerFlag) { this.localPlayerAIStep(ctx, world); } } /** * Assume later than 1.13.2 before calling this function. * @param player * @param world */ updatePoses(ctx, world) { const player = ctx.state; const swimPose = (0, states_1.getCollider)(states_1.PlayerPoses.SWIMMING, player.pos).expand(-1e-7, -1e-7, -1e-7); if (this.worldIsFree(world, swimPose, false)) { // update poses let currentPose; // player->GetDataSharedFlagsIdImpl(EntitySharedFlagsId::FallFlying) if (player.fallFlying) { currentPose = states_1.PlayerPoses.FALL_FLYING; } // (player->GetSleepingPosIdImpl() // this is based on metadata which I currently do not have access to. else if (player.pose === states_1.PlayerPoses.SLEEPING) { currentPose = states_1.PlayerPoses.SLEEPING; } else if (this.isSwimmingAndNotFlying(ctx, world)) { currentPose = states_1.PlayerPoses.SWIMMING; } // player->GetDataLivingEntityFlagsImpl() & 0x04 // no clue. else if (false) { currentPose = states_1.PlayerPoses.SPIN_ATTACK; } else if (player.control.sneak && !player.flying) { currentPose = states_1.PlayerPoses.SNEAKING; } else { currentPose = states_1.PlayerPoses.STANDING; } const poseBB = (0, states_1.getCollider)(currentPose, player.pos).expand(-1e-7, -1e-7, -1e-7); if (player.gameMode === "spectator" || this.worldIsFree(world, poseBB, false)) { player.pose = currentPose; } else { const crouchBB = (0, states_1.getCollider)(states_1.PlayerPoses.SNEAKING, player.pos).expand(-1e-7, -1e-7, -1e-7); if (this.worldIsFree(world, crouchBB, false)) { player.pose = states_1.PlayerPoses.SNEAKING; } else { player.pose = states_1.PlayerPoses.SWIMMING; } } } } fluidPhysics(ctx, world, water) { const player = ctx.state; const aabb = (0, states_1.getCollider)(player.pose, player.pos).expand(-1e-3, -1e-3, -1e-3); // -0.001 if (water) { player.isInWater = false; player.isUnderWater = false; } else { player.isInLava = false; player.isUnderLava = false; } const minAABB = aabb.minPoint(); const maxAABB = aabb.maxPoint(); const eyeHeight = player.eyeHeight + player.pos.y; const waterCond = (block) => block.type === this.waterId || this.waterLike.has(block.type) || block.getProperties().waterlogged; const lavaCond = (block) => block.type === this.lavaId; const push = new vec3_1.Vec3(0, 0, 0); const blockPos = new vec3_1.Vec3(0, 0, 0); let fluidRelativeHeight = 0.0; let numPush = 0; for (blockPos.x = Math.floor(minAABB.x); blockPos.x <= Math.floor(maxAABB.x); ++blockPos.x) { for (blockPos.y = Math.floor(minAABB.y); blockPos.y <= Math.floor(maxAABB.y); ++blockPos.y) { for (blockPos.z = Math.floor(minAABB.z); blockPos.z <= Math.floor(maxAABB.z); ++blockPos.z) { const block = world.getBlock(blockPos); if (block == null) continue; const waterRes = waterCond(block); const lavaRes = lavaCond(block); if ((waterRes && !water) || (lavaRes && water) || (!waterRes && !lavaRes)) { continue; } let fluidHeight = 0.0; const blockAbv = world.getBlock(blockPos.offset(0, 1, 0)); if ((blockAbv != null && waterCond(blockAbv) && waterRes) || (lavaCond(blockAbv) && lavaRes)) { fluidHeight = 1.0; } else { fluidHeight = this.getLiquidHeightPcent(block); } if (fluidHeight + blockPos.y < minAABB.y) { continue; } if (water) { player.isInWater = true; if (fluidHeight + blockPos.y > eyeHeight) { player.isUnderWater = true; } } else { player.isInLava = true; if (fluidHeight + blockPos.y > eyeHeight) { player.isUnderLava = true; } } fluidRelativeHeight = Math.max(fluidHeight + blockPos.y - minAABB.y, fluidRelativeHeight); if (player.flying) continue; const currentPush = this.getFlow(block, world); if (fluidRelativeHeight < 0.4) { currentPush.scale(fluidRelativeHeight); } push.add(currentPush); numPush++; } } } if (push.norm() > 0.0) { if (numPush > 0) { push.scale(1.0 / numPush); } if (water) { push.scale(0.014); } else { const worldInUltraWarm = false; // TODO: implement this (bot world relevance) push.scale(worldInUltraWarm ? 0.007 : 0.0023333333333333335); } } const pushNorm = push.norm(); if (Math.abs(player.vel.x) < ctx.worldSettings.negligeableVelocity && Math.abs(player.vel.z) < ctx.worldSettings.negligeableVelocity && pushNorm < 0.0045000000000000005) { // normalize and scale push.normalize().scale(0.0045000000000000005); } player.vel.add(push); } getFlow(block, world) { const curlevel = this.getRenderedDepth(block); const flow = new vec3_1.Vec3(0, 0, 0); for (const [dx, dz] of [ [0, 1], [-1, 0], [0, -1], [1, 0], ]) { const adjBlock = world.getBlock(block.position.offset(dx, 0, dz)); const adjLevel = this.getRenderedDepth(adjBlock); if (adjLevel < 0) { if (adjBlock && adjBlock.boundingBox !== "empty") { const adjLevel = this.getRenderedDepth(world.getBlock(block.position.offset(dx, -1, dz))); if (adjLevel >= 0) { const f = adjLevel - (curlevel - 8); flow.x += dx * f; flow.z += dz * f; } } } else { const f = adjLevel - curlevel; flow.x += dx * f; flow.z += dz * f; } } if (block.metadata >= 8) { for (const [dx, dz] of [ [0, 1], [-1, 0], [0, -1], [1, 0], ]) { const adjBlock = world.getBlock(block.position.offset(dx, 0, dz)); const adjUpBlock = world.getBlock(block.position.offset(dx, 1, dz)); if ((adjBlock && adjBlock.boundingBox !== "empty") || (adjUpBlock && adjUpBlock.boundingBox !== "empty")) { flow.normalize().translate(0, -6, 0); } } } return flow.normalize(); } getLiquidHeightPcent(block) { return 1 - (this.getRenderedDepth(block) + 1) / 9; } getRenderedDepth(block) { if (!block) return -1; if (this.waterLike.has(block.type)) return 0; if (block.getProperties().waterlogged) return 0; if (block.type !== this.waterId) return -1; const meta = block.metadata; return meta >= 8 ? 0 : meta; } updateSwimming(player, world) { if (player.flying) { player.swimming = false; } else if (player.swimming) { player.swimming = player.sprinting && player.isInWater; } else { const block = world.getBlock(player.pos); player.swimming = player.sprinting && player.isUnderWater && block != null && !!(block.type === this.waterId || this.waterLike.has(block.type) || block.getProperties().waterlogged); } } isMovingSlowly(ctx, world) { const player = ctx.state; if (this.verGreaterThan("1.13.2")) { const visuallyCrawling = (player.pose === states_1.PlayerPoses.SWIMMING || (!player.fallFlying && player.pose === states_1.PlayerPoses.FALL_FLYING)) && !player.isInWater; return player.crouching || visuallyCrawling; } return player.crouching; } localPlayerAIStep(ctx, world) { const player = ctx.state; const tickStartedOnGround = player.onGround; const heading = (0, states_1.convInpToAxes)(player); player.heading = heading; // moved into AiStep since it's tied to player behavior. Strictly, is Player::updateSwimming. this.updateSwimming(player, world); this.inputsToCrouch(ctx, heading, world); this.inputsToSprint(ctx, heading, world); this.inputsToFly(ctx, heading, world); // If sneaking in water, add downward speed if (player.isInWater && player.control.sneak && !player.flying) { player.vel.y -= 0.03999999910593033; } if (player.flying) { player.vel.y += (-1 * player.control.sneak + player.control.jump) * player.flySpeed * 3.0; } // player::AiStep { player.flyJumpTriggerTime = Math.max(0, player.flyJumpTriggerTime - 1); } // livingEntity::AiStep { player.jumpTicks = Math.max(0, player.jumpTicks - 1); if (Math.abs(player.vel.x) < ctx.worldSettings.negligeableVelocity) { player.vel.x = 0; } if (Math.abs(player.vel.y) < ctx.worldSettings.negligeableVelocity) { player.vel.y = 0; } if (Math.abs(player.vel.z) < ctx.worldSettings.negligeableVelocity) { player.vel.z = 0; } this.inputsToJump(player, world, ctx.worldSettings); // TODO: properly implement heading handler. forward-axis is forward, left-axis = strafe. weird naming. heading.forward *= 0.98; heading.strafe *= 0.98; // player->inputs.forward_axis *= 0.98f; // player->inputs.left_axis *= 0.98f; // Compensate water downward speed depending on looking direction (?) if (this.isSwimmingAndNotFlying(ctx, world)) { const mSinPitch = player.pitch; let condition = mSinPitch < 0.0 || player.control.jump; if (!condition) { // check above block const bl1 = world.getBlock(new vec3_1.Vec3(player.pos.x, player.pos.y + 1.0 - 0.1, player.pos.z)); condition = bl1 != null && (this.waterId === bl1.type || this.waterLike.has(bl1.type)); } if (condition) { // console.log('changing vel by', (mSinPitch - player.vel.y) * (mSinPitch < -0.2 ? 0.085 : 0.06)); player.vel.y += (mSinPitch - player.vel.y) * (mSinPitch < -0.2 ? 0.085 : 0.06); } } // Refresh pose before movement so the first fall-flying tick uses the // correct collider instead of the standing bounding box. if (this.verGreaterThan("1.13.2")) { this.updatePoses(ctx, world); } const velY = player.vel.y; this.movePlayer(ctx, world); // TODO: should be in player-specific logic?? if (player.flying) { player.vel.y = 0.6 * velY; /* player->SetDataSharedFlagsIdImpl(EntitySharedFlagsId::FallFlying, false); */ player.fallFlying = false; } player.onClimbable = this.isInClimbable(player, world); } // !livingplayer::AiStep if (player.onGround && player.flying && player.gameMode !== "spectator") { player.flying = false; // onUpdateAbilities(); } // Gen: pretty sure mobs can't spawn or exist outside world borders. this should be fine in here. player.pos.x = math.clamp(-2.9999999e7, player.pos.x, 2.9999999e7); player.pos.z = math.clamp(-2.9999999e7, player.pos.z, 2.9999999e7); if (this.verGreaterThan("1.13.2")) { this.updatePoses(ctx, world); } // Preserve the current inputs for the next tick after all previous-state // consumers in this tick have already read the prior values. player.lastOnGround = player.onGround; player.prevHeading.forward = heading.forward; player.prevHeading.strafe = heading.strafe; player.prevControl.jump = player.control.jump; player.prevControl.sneak = player.control.sneak; } inputsToCrouch(ctx, heading, world) { var _a, _b, _c; const player = ctx.state; if (this.verGreaterThan("1.13.2")) { const sneakBb = (0, states_1.getCollider)(states_1.PlayerPoses.SNEAKING, player.pos); const standBb = (0, states_1.getCollider)(states_1.PlayerPoses.STANDING, player.pos); sneakBb.expand(-1e-7, -1e-7, -1e-7); standBb.expand(-1e-7, -1e-7, -1e-7); player.crouching = !this.isSwimmingAndNotFlying(ctx, world) && this.worldIsFree(world, sneakBb, false) && (player.prevControl.sneak || !this.worldIsFree(world, standBb, false)); } else { player.crouching = !this.isSwimmingAndNotFlying(ctx, world) && player.prevControl.sneak; } // Determine if moving slowly const isMovingSlowly = this.isMovingSlowly(ctx, world); // Handle post-1.21.3 sprinting conditions if (this.verGreaterThan("1.21.3")) { // TODO: just use stored blindness effect. const hasBlindness = this.getEffectLevel(physicsUtils_1.CheapEffects.BLINDNESS, player.effects) > 0; // Stop sprinting when crouching fix in 1.21.4+ if (hasBlindness || isMovingSlowly) { this.setSprinting(ctx, false); } } // Apply slow down to player inputs when moving slowly if (isMovingSlowly) { let sneakCoefficient; if (this.verLessThan("1.19")) { sneakCoefficient = 0.3; } else if (this.verLessThan("1.21")) { sneakCoefficient = 0.3 + player.swiftSneak * 0.15; sneakCoefficient = Math.min(Math.max(0.0, sneakCoefficient), 1.0); } else { sneakCoefficient = (_c = (_b = (_a = player.attributes) === null || _a === void 0 ? void 0 : _a.sneakingSpeed) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : 0.3; // TODO: this shouldn't be happening! } heading.forward *= sneakCoefficient; heading.strafe *= sneakCoefficient; } } isSwimmingAndNotFlying(ctx, world) { const entity = ctx.state; // return !player->flying && // player->game_mode != GameType::Spectator && // player->GetDataSharedFlagsIdImpl(EntitySharedFlagsId::Swimming); if (entity instanceof states_1.PlayerState) { return !entity.flying && entity.gameMode !== "spectator" && entity.swimming; } else { return false; // TODO: proper handling of non-player mobs. } } inputsToSprint(ctx, heading, world) { const player = ctx.state; // console.log('is sprinting', player.sprinting, 'can sprint', this.canStartSprinting(ctx, heading), player.sprinting) if (this.canStartSprinting(ctx, heading) && (player.control.sprint || (player.sprintTriggerTime > 0 && heading.forward >= (player.isInWater ? 1e-5 : 0.8)))) { this.setSprinting(ctx, true); } // console.log('should stop', this.shouldStopRunSprinting(ctx, heading), 'minor collision', player.isCollidedHorizontallyMinor) // Stop sprinting if necessary if (player.sprinting) { if (!player.control.sprint) { this.setSprinting(ctx, false); } if (this.isSwimming(ctx)) { if (this.shouldStopSwimSprinting(ctx, heading)) { this.setSprinting(ctx, false); } } else if (this.shouldStopRunSprinting(ctx, heading)) { this.setSprinting(ctx, false); } } } canStartSprinting(ctx, heading) { const player = ctx.state; return !player.sprinting && heading.forward >= (player.isInWater ? 1e-5 : 0.8) && this.hasEnoughFoodToSprint(ctx) && !player.isUsingItem && !player.blindness && // (!player.isPassenger || this.vehicleCanSprint(ctx)) && (!player.fallFlying || player.isUnderWater) && (!player.crouching || player.isUnderWater) && (!player.isInWater || player.isUnderWater); } hasEnoughFoodToSprint(ctx) { const player = ctx.state; return /* player.isPassenger ||*/ player.food > 6 || player.mayFly; } vehicleCanSprint(ctx) { return false; // const player = ctx.state as PlayerState; // return player.vehicle && // player.vehicle.canSprint && // player.vehicle.isLocalInstanceAuthoritative; } isSwimming(ctx) { const player = ctx.state; return player.isInWater && player.isUnderWater; } handleFallFlyingCollisions(player, previousHorizontalSpeed) { if (!player.isCollidedHorizontally) return; const currentHorizontalSpeed = Math.sqrt(player.vel.x * player.vel.x + player.vel.z * player.vel.z); const deltaSpeed = previousHorizontalSpeed - currentHorizontalSpeed; const collisionDamage = deltaSpeed * 10.0 - 3.0; if (collisionDamage > 0.0) { // Vanilla applies fly-into-wall damage and sound here. // BotcraftPhysics currently does not model those side effects. } } applyFireworkBoost(player) { if (player.fireworkRocketDuration <= 0) return; if (!player.fallFlying) { player.fireworkRocketDuration = 0; return; } const { lookDir } = (0, physicsUtils_1.getLookingVector)(player); player.vel.x += lookDir.x * 0.1 + (lookDir.x * 1.5 - player.vel.x) * 0.5; player.vel.y += lookDir.y * 0.1 + (lookDir.y * 1.5 - player.vel.y) * 0.5; player.vel.z += lookDir.z * 0.1 + (lookDir.z * 1.5 - player.vel.z) * 0.5; --player.fireworkRocketDuration; } shouldStopRunSprinting(ctx, heading) { const player = ctx.state; return player.blindness > 0 || // (player.isPassenger && !this.vehicleCanSprint(ctx)) || heading.forward < 1e-5 || !this.hasEnoughFoodToSprint(ctx) || (player.isCollidedHorizontally && !player.isCollidedHorizontallyMinor) || (player.isInWater && !player.isUnderWater); } shouldStopSwimSprinting(ctx, heading) { const player = ctx.state; return player.blindness > 0 || // (player.isPassenger && !this.vehicleCanSprint(ctx)) || !player.isInWater || (heading.forward <= 1e-5 && !player.onGround && !player.control.sneak) || !this.hasEnoughFoodToSprint(ctx); } /** * TODO: almost certainly unfinished. * @param player * @param value */ setSprinting(ctx, value) { const player = ctx.state; let attr = player.attributes[this.movementSpeedAttribute]; if (attr != null) attributes.deleteAttributeModifier(attr, ctx.worldSettings.sprintingUUID); attr = attributes.createAttributeValue(ctx.worldSettings.playerSpeed); if (value) { attributes.addAttributeModifier(attr, { uuid: ctx.worldSettings.sprintingUUID, amount: ctx.worldSettings.sprintSpeed, operation: 2, }); } // potential deviation from vanilla. // if (value != player.sprinting) { // player.sprintTriggerTime = ctx.worldSettings.sprintTimeTriggerCooldown; // } player.attributes[this.movementSpeedAttribute] = attr; player.sprinting = value; } inputsToFly(ctx, heading, world) { const player = ctx.state; let flyChanged = false; if (player.mayFly) { // Auto trigger if in spectator mode if (player.gameMode === "spectator") { player.flying = true; flyChanged = true; // onUpdateAbilities(); } else { // If double jump in creative, swap flying mode if (player.prevControl.jump && player.control.jump) { if (player.flyJumpTriggerTime === 0) { player.flyJumpTriggerTime = ctx.worldSettings.flyJumpTriggerCooldown; } else if (this.isSwimmingAndNotFlying(ctx, world)) { player.flying = !player.flying; flyChanged = true; // onUpdateAbilities(); player.flyJumpTriggerTime = 0; } } } } const hasLevitationEffect = player.levitation > 0; if (player.control.jump && !flyChanged && !player.prevControl.jump && !player.flying && !player.onClimbable && !player.onGround && !player.isInWater && /* !player->GetDataSharedFlagsIdImpl(EntitySharedFlagsId::FallFlying) && */ !player.fallFlying && !hasLevitationEffect) { const hasElytra = player.validElytraEquipped; if (hasElytra) { player.fallFlying = true; // https://github.com/adepierre/Botcraft/blob/6c572071b0237c27a85211a246ce10565ef4d80f/botcraft/src/Game/Physics/PhysicsManager.cpp#L792 // something about setting actions? } } } inputsToJump(entity, world, worldSettings) { // TODO: implement non-player entity jumping. var _a, _b; if (entity instanceof states_1.PlayerState) { const player = entity; if (!player.control.jump) { player.jumpTicks = 0; return; } if (player.flying) { return; } if (player.isInWater || player.isInLava) { player.vel.y += 0.03999999910593033; // magic number } else if (player.onGround && player.jumpTicks === 0) { const blockJumpFactor = this.getBlockJumpFactor(player, world, worldSettings); const jumpBoost = Math.fround(0.1 * player.jumpBoost); // in mineflayer, level 1 is 1, not 0. if (this.verLessThan("1.20.5")) { const jumpPower = Math.fround(Math.fround(0.42) * Math.fround(blockJumpFactor) + jumpBoost); player.vel.y = Math.max(jumpPower, player.vel.y); if (player.sprinting) { const yawRad = Math.PI - player.yaw; // should already be in yaw. MINEFLAYER SPECIFC CHANGE, MATH.PI - // potential inconsistency here. This may not be accurate. const offsetX = Math.fround(Math.sin(yawRad)) * 0.2; const offsetZ = Math.fround(Math.cos(yawRad)) * 0.2; player.vel.x -= offsetX; player.vel.z += offsetZ; } } else { const value = Math.fround((_b = (_a = entity.attributes[this.jumpStrengthAttribute]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : Math.fround(0.42)); const jumpPower = Math.fround(value * Math.fround(blockJumpFactor) + jumpBoost); if (jumpPower > 1e-5) { player.vel.y = Math.max(jumpPower, player.vel.y); if (player.sprinting) { const yawRad = Math.PI - player.yaw; // should already be in yaw. MINEFLAYER SPECIFC CHANGE, MATH.PI - player.vel.x -= Math.sin(yawRad) * 0.2; player.vel.z += Math.cos(yawRad) * 0.2; } } } player.jumpTicks = worldSettings.autojumpCooldown; } } } getBlockJumpFactor(entity, world, worldSettings) { const feetBlock = world.getBlock(new vec3_1.Vec3(entity.pos.x, entity.pos.y, entity.pos.z)); const feetJumpFactor = this.getKnownJumpFactor(feetBlock === null || feetBlock === void 0 ? void 0 : feetBlock.type, worldSettings); if (feetJumpFactor !== 1.0) return feetJumpFactor; const supportBlock = world.getBlock(this.getBlockBelowAffectingMovement(entity, world)); return this.getKnownJumpFactor(supportBlock === null || supportBlock === void 0 ? void 0 : supportBlock.type, worldSettings); } getKnownJumpFactor(blockType, worldSettings) { if (blockType == null) return 1.0; if (blockType === this.honeyblockId) return worldSettings.honeyblockJumpSpeed; return 1.0; } getOffGroundSpeed(player) { if (player.flying) { return player.sprinting ? player.flySpeed * 2.0 : player.flySpeed; } return player.sprinting ? Math.fround(0.025999999) : Math.fround(0.02); } getBlockBelowAffectingMovement(entity, world) { if (entity.supportingBlockPos != null) { return entity.supportingBlockPos; //.offset(0, -0.500001, 0); } else { return entity.pos.offset(0, -0.500001, 0); } } /** * Taken from original physics impl. * @param entity * @returns */ getMovementSpeedAttribute(entity) { const isSprinting = entity.state instanceof states_1.PlayerState && entity.state.sprinting; let attribute; // In 1.21+, this should map to 'minecraft:movement_speed' if (entity.state.attributes && entity.state.attributes[this.movementSpeedAttribute]) { attribute = entity.state.attributes[this.movementSpeedAttribute]; } else { // Create fallback if server hasn't sent it attribute = attributes.createAttributeValue(Math.fround(entity.worldSettings.playerSpeed)); } // --- SPRINTING --- attribute = attributes.deleteAttributeModifier(attribute, entity.worldSettings.sprintingUUID); if (isSprinting) { attribute = attributes.addAttributeModifier(attribute, { uuid: entity.worldSettings.sprintingUUID, // Math.fround(0.3) perfectly resolves to 0.30000001192092896 amount: Math.fround(0.3), operation: 2, }); } // --- SPEED EFFECT --- const SPEED_UUID = "91AEAA56-376B-4498-935B-2F7F68070635"; attribute = attributes.deleteAttributeModifier(attribute, SPEED_UUID); const speedLevel = this.getEffectLevel(physicsUtils_1.CheapEffects.SPEED, entity.state.effects); if (speedLevel > 0) { attribute = attributes.addAttributeModifier(attribute, { uuid: SPEED_UUID, // Truncate the 0.2 first, then multiply, then truncate again (how Java float math works) amount: Math.fround(Math.fround(0.2) * speedLevel), operation: 2, }); } // --- SLOWNESS EFFECT --- const SLOWNESS_UUID = "7107DE5E-7CE8-4030-940E-514C1F160890"; attribute = attributes.deleteAttributeModifier(attribute, SLOWNESS_UUID); const slownessLevel = this.getEffectLevel(physicsUtils_1.CheapEffects.SLOWNESS, entity.state.effects); if (slownessLevel > 0) { attribute = attributes.addAttributeModifier(attribute, { uuid: SLOWNESS_UUID, amount: Math.fround(Math.fround(-0.15) * slownessLevel), operation: 2, }); } // Calculate the final speed and cast the entire result to a 32-bit float const attributeSpeed = Math.fround(attributes.getAttributeValue(attribute)); return attributeSpeed; } /** * Assume EPhysicsCtx is wrapping a PlayerState. * @param ctx * @param world */ movePlayer(ctx, world) { var _a, _b, _c; const player = ctx.state; { // LivingEntity::travel const goingDown = player.vel.y <= 0.0; const hasSlowFalling = player.slowFalling > 0; // named drag in botcraft. That seems incorrect. Not sure yet. let gravity; if (this.verLessThan("1.20.5")) { gravity = goingDown && hasSlowFalling ? 0.01 : ctx.gravity; } else { gravity = goingDown && hasSlowFalling ? Math.min(0.01, ctx.gravity) : ctx.gravity; } if (player.isInWater && !player.flying) { const initY = player.pos.y; let waterSlowDown = player.sprinting ? ctx.sprintWaterInertia : ctx.waterInertia; let inputStrength = 0.02; let depthStriderMult; if (this.verLessThan("1.21")) { const depthStrider = player.depthStrider; depthStriderMult = Math.min(depthStrider, 3) / 3; } else { depthStriderMult = (_b = (_a = player.attributes[this.waterMovementEfficiencyAttribute]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : 0; // random value as default } if (!player.onGround) { waterSlowDown += (0.54600006 - waterSlowDown) * depthStriderMult; // magic number const movementSpeed = this.getMovementSpeedAttribute(ctx); // slight deviation, using utility method inputStrength += Math.fround(movementSpeed - inputStrength) * depthStriderMult; } if (this.verGreaterThan("1.12.2")) { if (player.dolphinsGrace > 0) { waterSlowDown = 0.96; // magic number } } this.applyInputs(inputStrength, player); this.applyMovement(ctx, world); if (player.isCollidedHorizontally && player.onClimbable) { player.vel.y = 0.2; } player.vel.x *= waterSlowDown; player.vel.y *= 0.800000011920929; // magic number, pretty sure this is wrong. player.vel.z *= waterSlowDown; if (!player.sprinting) { // this logic does not look entirely correct. I believe this is an attempt at version-agnostic water gravity. // originally, the neg. vel value here was hardcoded. // if (goingDown && // Math.abs(player.vel.y - 0.005) >= ectx.worldSettings.negligeableVelocity && // Math.abs(player.vel.y - gravity / 16) < ectx.worldSettings.negligeableVelocity) { // player.vel.y -= ectx.worldSettings.negligeableVelocity; // } else { // player.vel.y -= gravity / 16 // } // because of this, I will implement my own version. // if (goingDown) { player.vel.y -= ctx.waterGravity; // } } const bb = player.getBB().expand(-1e-7, -1e-7, -1e-7); bb.translate(0, 0.6000000238418579 - player.pos.y + initY, 0); bb.translateVec(player.vel); if (player.isCollidedHorizontally && this.worldIsFree(world, bb, false)) { player.vel.y = ctx.worldSettings.outOfLiquidImpulse; } } else if (player.isInLava && !player.flying) { const initY = player.pos.y; this.applyInputs(0.02, player); this.applyMovement(ctx, world); player.vel.scale(0.5); player.vel.y -= ctx.lavaGravity; const bb = player.getBB().expand(-1e-7, -1e-7, -1e-7); bb.translate(0, 0.6000000238418579 - player.pos.y + initY, 0); // Math.fround(0.60) bb.translateVec(player.vel); if (player.isCollidedHorizontally && this.worldIsFree(world, bb, false)) { player.vel.y = ctx.worldSettings.outOfLiquidImpulse; } } // elytra flying. else if (player.fallFlying) { const previousHorizontalSpeed = Math.sqrt(player.vel.x * player.vel.x + player.vel.z * player.vel.z); // slight deviation // sqrt(front_vector.x² + front_vector.z²) to follow vanilla code const { pitch, sinPitch, cosPitch, lookDir } = (0, physicsUtils_1.getLookingVector)(player); const cosPitchFromLength = Math.sqrt(lookDir.x * lookDir.x + lookDir.z * lookDir.z); const cosPitchSqr = cosPitch * cosPitch; const hVel = Math.sqrt(player.vel.x * player.vel.x + player.vel.z * player.vel.z); player.vel.y += gravity * (-1 + 0.75 * cosPitchSqr); if (player.vel.y < 0.0 && cosPitchFromLength > 0.0) { const deltaSpeed = -player.vel.y * 0.1 * cosPitchSqr; player.vel.x += (lookDir.x * deltaSpeed) / cosPitchFromLength; player.vel.z += (lookDir.z * deltaSpeed) / cosPitchFromLength; player.vel.y += deltaSpeed; } if (player.pitch > 0.0 && cosPitchFromLength > 0.0) { const deltaSpeed = hVel * lookDir.y * 0.04; player.vel.x += (-lookDir.x * deltaSpeed) / cosPitchFromLength; player.vel.z += (-lookDir.z * deltaSpeed) / cosPitchFromLength; player.vel.y += deltaSpeed * 3.2; // magic number } if (cosPitchFromLength > 0.0) { player.vel.x += ((lookDir.x / cosPitchFromLength) * hVel - player.vel.x) * 0.1; player.vel.z += ((lookDir.z / cosPitchFromLength) * hVel - player.vel.z) * 0.1; } player.vel.x *= Math.fround(0.99); // magic number, this DEFINITELY should be replaced by a drag value. player.vel.z *= Math.fround(0.99); // magic number, this DEFINITELY should be replaced by a drag value. player.vel.y *= Math.fround(0.98); // magic number, this DEFINITELY should be replaced by a drag value. this.applyMovement(ctx, world); this.handleFallFlyingCollisions(player, previousHorizontalSpeed); } else { const blockBelow = world.getBlock(this.getBlockBelowAffectingMovement(player, world)); // deviation. using our stores slipperiness values. const friction = blockBelow ? (_c = this.blockSlipperiness[blockBelow.type]) !== null && _c !== void 0 ? _c : ctx.worldSettings.defaultSlipperiness : ctx.worldSettings.defaultSlipperiness; // console.log(blockBelow.name, blockBelow.position, player.supportingBlockPos, friction) const inertia = player.lastOnGround ? friction * ctx.airborneInertia : ctx.airborneInertia; // deviation, adding additional logic for changing attribute values. const movementSpeedAttr = this.getMovementSpeedAttribute(ctx); let inputStrength; if (player.lastOnGround) { inputStrength = movementSpeedAttr * (0.21600002 / (friction * friction * friction)); } else { inputStrength = this.getOffGroundSpeed(player); } this.applyInputs(inputStrength, player); if (player.onClimbable) { // LivingEntity::handleOnClimbable player.vel.x = math.clamp(-0.15000000596046448, player.vel.x, 0.15000000596046448); // Math.fround(0.15) player.vel.z = math.clamp(-0.15000000596046448, player.vel.z, 0.15000000596046448); // Math.fround(0.15) player.vel.y = Math.max(-0.15000000596046448, player.vel.y); // Math.fround(0.15) const feetBlock = world.getBlock(new vec3_1.Vec3(player.pos.x, player.pos.y, player.pos.z)); if (feetBlock && (this.scaffoldId === feetBlock.type) !== player.control.sneak) { player.vel.y = 0.0; } } this.applyMovement(ctx, world); // if colliding and in climbable, go up. if ((player.isCollidedHorizontally || player.control.jump) && player.onClimbable) { // TODO: or in powder with leather boots. player.vel.y = 0.2; }