How to climb a tree?

So, I want hearthlings to climb the palm trees when harvesting it for coconuts.

I then created a new action doing “harvest_renewable_resource_adjacent” with higher priority.
It worked fine. Harvest something and it will use the default action, harvest a palm tree and it will run my action.

But how do I move them up?
I tried changing it into a compound action that would run “goto_location” up and then another down, but it obviously fails as it can’t find a path into the air…
Another thing, even if the above worked, the climbing animation only looks good when the hearthling is “inside” the ladder, this means that I will need to move their position not only up, but also inside the tree trunk…

Any idea? I basically need to set a starting and ending coordinate that the hearthling moves through, even through air or solid objects.

1 Like

i think you will need a new animation - in this animation its climb the tree and get back down - like the harvest animation

I’m not sure how possible the following will be but:
ladder that looks like tree trunk in such a way that moving on it makes climbing look good.
Seperate entity that is the top of the plant and the fruit, that will spawn together with the “tree/ladder”?
Seperate your problems into two more manageable ones?

I can’t use a new animation because I would have to make multiple animations for different tree types and sizes. And it would be a huge animation, because I would not be able to loop it (If made small loopable parts, it would probably look bad at the loops and transitions)

I can’t add “invisible” ladders to it because that would multiply the entity counter in the map by 5.

Hmm yeah now I’m thinking “rummage at the bottom of the tree and don’t climb it”

Or rummage at the bottom, teleport into the top, rummage there, teleport back :confused:

What about adding/removing the invisible ladders only when they go to harvest it?

They would climb it but the animation will not look like they are grabbing the tree. There would be a 1 voxel gap.

How about one of those… How do you describe it, its like a giant belt that goes around you and the tree? They use it for climbing, that might tie the whole together?

Sure, how I would do that in code?
Isn’t the same thing? Moving from current coordinate to a coordinate above it?

I was looking into making climbable ivy a while ago and looked through a lot of the ladder code. As long as I remember/understood it right it’s not the ladder in itself that allows them to move vertically but that it adds a vertical path (don’t remember the exact name) to the nav-mesh in the same spot.
Maybe that’s what you meant with “invisible ladders” but in case you meant adding actual ladder entities and hiding the mesh then I don’t think you have to. Add a vertical path inside the palm tree and it should probably work. You might have to turn of the collision box for the tree when telling them to get the coconuts, so they can reach the vertical path.
Don’t know if they will do the climb animation automatically when moving vertically.

I was poking through entities.lua which is in radiant.smod and found a few functions that might help? I dunno.

function entities.teleport_items(items, origin, min_radius, max_radius)
   for _, e in pairs(items) do
      local location = radiant.terrain.find_placement_point(origin, min_radius, max_radius)
      radiant.terrain.place_entity(e, location)
   end
end

function entities.distance_between(object_a, object_b)
   local point_a, point_b

   assert(object_a and object_b)

   if radiant.util.is_a(object_a, Point3) then
      point_a = object_a
   elseif radiant.util.is_a(object_a, Entity) then
      local mob = object_a:get_component('mob')
      point_a = mob and mob:get_world_location()
   else
      error('unexpected type for arg1 "%s"', type(object_a))
   end

   if radiant.util.is_a(object_b, Point3) then
      point_b = object_b
   elseif radiant.util.is_a(object_b, Entity) then
      local rcs = object_b:get_component('region_collision_shape')
      if rcs then
         -- find the point on the region closets to point_a
         local region = rcs:get_region()
         if region then
            local world_space_region = entities.local_to_world(region:get(), object_b)
            point_b = world_space_region:get_closest_point(point_a)
         end
      end
      if not point_b then
         -- still no point?  use the world location
         local mob = object_b:get_component('mob')
         point_b = mob and mob:get_world_location()
      end
   end

   if not point_a or not point_b then
      -- either a or b was an entity that's not in the world.  doh!
      return radiant.math.INFINITY
   end
   return point_a:distance_to(point_b)
end

-- A function that calculates JUST the distance between the world grid locations of two entities
function entities.distance_between_entities(entity_a, entity_b)
   local point_a = entity_a:get_component('mob'):get_world_location()
   local point_b = entity_b:get_component('mob'):get_world_location()
   if not point_a or not point_b then
      -- either a or b was an entity that's not in the world.  doh!
      return radiant.math.INFINITY
   end

   return point_a:distance_to(point_b)
end

function entities.exists(entity)
   return entity and entity:is_valid()
end

function entities.move_to(entity, location)
   radiant.check.is_entity(entity)

   if type(location) == "table" then
      location = Point3(location.x, location.y, location.z)
   end
   entity:add_component('mob'):move_to(location)
end

function entities.move_to_absolute(entity, location)
   local parent_origin = entities.get_world_location(entities.get_parent(entity))
   entity:add_component('mob'):move_to(location - parent_origin)
end

function entities.move_to_grid_aligned(entity, location)
   radiant.check.is_entity(entity)

   if type(location) == "table" then
      location = Point3(location.x, location.y, location.z)
   end
   entity:add_component('mob'):move_to_grid_aligned(location)
end

function entities.turn_to(entity, degrees)
   if not entity or not entity:is_valid() then
      return
   end
   radiant.check.is_number(degrees)

   entity:add_component('mob'):turn_to(degrees)
end

function entities.turn_to_face(entity, target, opt_no_interpolate)
   local point = entities.get_facing_point(entity, target)

   local no_interpolate = not not opt_no_interpolate

   if no_interpolate then
      entity:get_component('mob'):set_skip_interpolation(true)
   end
   if point then
      entity:get_component('mob'):turn_to_face_point(point)
   end
end

-- Returns the (voxel, integer) grid location in front of the specified entity.
function entities.get_grid_in_front(entity)
   local mob = entity:get_component('mob')
   local facing = radiant.math.round(mob:get_facing() / 90) * 90
   local location = mob:get_world_grid_location()
   local offset = radiant.math.rotate_about_y_axis(-Point3.unit_z, facing):to_closest_int()
   return location + offset
end

function entities.get_facing_point(entity, target)
   if not entity or not entity:is_valid() then
      return nil
   end

   local point = nil
   if radiant.util.is_a(target, Entity) then
      -- if the target has a non-empty destination region, face the closest point in that region
      local dest_component = target:get_component('destination')
      local boxed_dest_region = dest_component and dest_component:get_region()
      local dest_region = boxed_dest_region and boxed_dest_region:get()
      if dest_region and not dest_region:empty() then
         local dest_region_world = entities.local_to_world(dest_region, target)
         local entity_location = entities.get_world_location(entity)
         point = dest_region_world:get_closest_point(entity_location)
      else
         point = entities.get_world_location(target)
      end

   elseif radiant.util.is_a(target, Point3) then
      point = target
   end
   return point
end

function entities.get_facing(entity)
   if not entity or not entity:is_valid() then
      return nil
   end

   local mob = entity:get_component('mob')
   local facing = mob and mob:get_facing()
   return facing
end


function entities.get_entity(id)
   local entity = radiant.get_entity(id)
   return entity
end

function entities.get_animation_table(entity)
   local name
   local render_info = entity:get_component('render_info')
   if render_info then
      name = render_info:get_animation_table()
   end
   return name
end

function entities.set_animation_table(entity, animation_table_name)
   local render_info = entity:get_component('render_info')
   if render_info and animation_table_name then
      render_info:set_animation_table(animation_table_name)
   end
end

function entities.get_attribute(entity, attribute_name, default)
   if entity and entity:is_valid() then
      local ac = entity:get_component('stonehearth:attributes')
      if ac then
         return ac:get_attribute(attribute_name, default)
      end
   end
   return default
end


function entities.is_adjacent_to(source, target)
   if not source or not target then
      return false
   end

   if radiant.util.is_a(source, Entity) and radiant.util.is_a(target, Entity) then
      local result = _radiant.sim.is_adjacent_to(source, target)
      return result
   end

   if radiant.util.is_a(source, Point3) and radiant.util.is_a(target, Point3) then
      local result = source:is_adjacent_to(target)
      return result
   end

   log:error('invalid parameters %s, %s', tostring(source), tostring(target))
   assert(false)
end

local function is_xz_adjacent(p1, p2)
   if not p1 or not p2 then
      return false
   end

   local q1 = Point2(p1.x, p1.z)
   local q2 = Point2(p2.x, p2.z)
   local result = q1:is_adjacent_to(q2)
   return result
end

function entities.location_within_reach(entity, target_location)
   if not entity or not entity:is_valid() then
      return false
   end

   if not target_location then
      return false
   end

   local mob = entity:get_component('mob')
   if not mob then
      return false
   end

   local entity_location = mob:get_world_grid_location()
   if not entity_location then
      return false
   end

   if not is_xz_adjacent(target_location, entity_location) then
      return false
   end

   local collision_type = mob:get_mob_collision_type()
   local max_reach_up, max_reach_down = _radiant.sim.get_entity_reach(collision_type)

   local delta = target_location - entity_location
   local result = radiant.math.in_bounds(delta.y, -max_reach_down, max_reach_up)
   return result
end

function entities.get_target_table(entity, table_name)
   if not entity or not entity:is_valid() then
      return nil
   end

   return entity:add_component('stonehearth:target_tables')
                    :get_target_table(table_name)
end

function entities.is_standing_on_ladder(entity)
   local entity_location = entities.get_world_grid_location(entity)
   local support_location = entity_location - Point3.unit_y

   -- quick rejection test
   if _physics:is_blocked(entity, support_location) then
      return false
   end

   local support_entities = radiant.terrain.get_entities_at_point(support_location)

   for id, entity in pairs(support_entities) do
      if entity:get_component('vertical_pathing_region') then
         return true, entity
      end
   end

   return false
end

Or “shake” the tree to make the fruit fall down into baskets?

1 Like

This sounds like the most feasible one to me

That is what I need.

They do, if they path straight up or down they switch the animation to climbing.

Those do not offer a smooth movement, only instant jumps from one position to another. I would need to do small increments every frame to simulate movement.

That is basically the current behavior. They hammer the tree base.


I really want to do it the proper way, no hacks or workaround. Cause it can be used later to other things like flying creatures or boats.
I will try a few more things

3 Likes

sorry bruno, i think you may already be on the top of the modder-foodchain in that respect :’) (lets be fair i live off code and tricks i can beg,borrow, steal or reverse engineer :stuck_out_tongue: )
maybe the devs have any ideas? @max99x

Can’t check the code now but if you find the lua about building ladders and search for “vertical” I think you should find it.

1 Like

I finally spend some more time on it and got it to work. Should be included in the mod in a few days, after I finish a few other things.

12 Likes

Nice. I was just thinking the other day that squirrels should really climb trees instead of just hopping along on the ground…

Looks awesome! Good job :smiley:

Edit: I wonder if anything in it could perhaps help or give a clue about the Highland vines
Guess I’ll wait for the update to look at it :3

It’s so cute!!! :jubilant: