Replace cmer_horse with whinny mod @ v1.0...

Release: https://github.com/AntumMT/mod-whinny/releases/tag/v1.0
master
Jordan Irwin 2021-05-21 06:35:07 -07:00
parent 33d93de645
commit 71df3133fd
51 changed files with 44723 additions and 12082 deletions

View File

@ -94,7 +94,6 @@ The game includes the mods from the default [minetest_game](https://github.com/m
* [dungeon_master][mobs_monster] ([MIT][lic.dungeon_master] -- version: [00c890f Git][ver.dungeon_master] *2021-05-20*
* [folks][] ([GPL][lic.gpl3.0]) -- version: [0.2.0][ver.folks] *2021-02-23*
* [ghost][creatures] ([Zlib][lic.creatures] / [CC BY-SA][lic.ccbysa3.0]) -- version: [c09aacb Git][ver.ghost] *2021-05-19*
* [horse][] ([MIT][lic.horse]) -- version: [3240dde Git][ver.horse] *2021-05-19*
* [kitten][mobs_animal] ([MIT][lic.mobs_kitten]) -- version: [34fac03 Git][ver.mobs_kitten] *2021-05-20*
* [mese_monster][mobs_monster] ([MIT][lic.mese_monster]) -- version: [cd02839 Git][ver.mese_monster] *2021-05-20*
* [oerkki][creatures] ([Zlib][lic.creatures] / [CC BY-SA][lic.ccbysa3.0]) -- version: [70410fe Git][ver.oerkki] *2021-05-05*
@ -107,6 +106,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
* [stone_monster][mobs_monster] ([MIT][lic.stone_monster]) -- version: [fa52f6f Git][ver.stone_monster] *2021-05-20*
* [tree_monster][mobs_monster] ([MIT][lic.tree_monster]) -- version: [f2977ff Git][ver.tree_monster] *2021-05-20*
* velociraptor ([LGPL][lic.lgpl2.1] / [CC BY-SA][lic.ccbysa3.0]) -- forked from *mobs_farlands* (part of [farlands][] sub-game)
* [whinny][] ([MIT][lic.whinny]) -- version: [1.0][ver.whinny] *2021-05-21*
* [zombie][creatures] ([Zlib][lic.creatures] / [CC BY-SA][lic.ccbysa3.0]) -- version: [4edb18e Git][ver.zombie] *2021-05-05*
* player/action/
* [hbsprint][] ([LGPL][lic.lgpl2.1]) -- version: [f566d0f Git][ver.hbsprint] *2021-02-18* ([patched][patch.hbsprint])
@ -310,7 +310,6 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[home_gui]: http://cornernote.github.io/minetest-home_gui/
[home_workshop]: https://gitlab.com/VanessaE/home_workshop_modpack
[homedecor]: https://forum.minetest.net/viewtopic.php?t=2041
[horse]: https://github.com/AntumMT/mod-horse
[hot_air_balloons]: https://forum.minetest.net/viewtopic.php?t=22367
[hovercraft]: https://forum.minetest.net/viewtopic.php?t=6722
[hudbars]: https://forum.minetest.net/viewtopic.php?t=11153
@ -380,6 +379,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[wardrobe]: https://github.com/AntumMT/mod-wardrobe
[wardrobe.old]: https://forum.minetest.net/viewtopic.php?t=9680
[weather]: https://forum.minetest.net/viewtopic.php?t=5245
[whinny]: https://github.com/AntumMT/mod-whinny
[whitelist]: https://forum.minetest.net/viewtopic.php?t=8434
[windmill]: https://forum.minetest.net/viewtopic.php?id=7440
[workbench]: https://forum.minetest.net/viewtopic.php?t=14085
@ -418,7 +418,6 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[lic.fort_spikes]: mods/buildings/fort_spikes/LICENSE
[lic.headanim]: mods/player/visuals/headanim/LICENSE
[lic.home_gui]: mods/ui/home_gui/LICENSE
[lic.horse]: mods/mobiles/horse/LICENSE.txt
[lic.hidename]: mods/antum/hidename/LICENSE.txt
[lic.homedecor]: mods/modpacks/homedecor/LICENSE
[lic.hovercraft]: mods/transport/hovercraft/LICENSE.txt
@ -459,6 +458,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[lic.waffles]: mods/furniture/waffles/LICENSE.txt
[lic.walking_light]: mods/lighting/walking_light/README.md
[lic.wardrobe]: mods/player/visuals/wardrobe/LICENSE.txt
[lic.whinny]: mods/mobiles/whinny/LICENSE.txt
[lic.windmill]: mods/buildings/windmill/README.md
[lic.cc0]: doc/licenses/CC0.txt
@ -552,7 +552,6 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[ver.home_gui]: https://github.com/cornernote/minetest-home_gui/tree/a291a09
[ver.home_workshop]: https://gitlab.com/VanessaE/home_workshop_modpack/tree/03325e8
[ver.homedecor]: https://gitlab.com/VanessaE/homedecor_modpack/tree/f1dc68a
[ver.horse]: https://github.com/AntumMT/mod-horse/tree/3240dde
[ver.hot_air_balloons]: https://notabug.org/NetherEran/hot_air_balloons/src/39a3572ad1bf7fd800525d68b128981e1b2c37d1
[ver.hovercraft]: https://github.com/stujones11/hovercraft/tree/4d50e68
[ver.hudbars]: http://repo.or.cz/minetest_hudbars.git/tree/0684bac
@ -620,6 +619,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[ver.waffles]: https://github.com/GreenXenith/waffles/tree/15bcdce
[ver.walking_light]: https://github.com/petermaloney/walking_light/tree/766ef0f
[ver.wardrobe]: https://github.com/AntumMT/mod-wardrobe/releases/tag/v1.3
[ver.whinny]: https://github.com/AntumMT/mod-whinny/releases/tag/v1.0
[ver.whitelist]: https://github.com/AntumMT/mod-whitelist/tree/b813b19
[ver.windmill]: https://github.com/Sokomine/windmill/tree/47b029d
[ver.workbench]: https://github.com/minetest-mods/workbench/tree/bd14f59

View File

@ -2018,22 +2018,38 @@ coloredwood_enable_stairsplus = true
#sneeker.spawn_maxlight = 5
# *** horse ***
# *** whinny ***
## Entity lifespan.
# type: int
# default: 300
#horse.lifetime = 300
## If enabled, messages all players when spawned.
# type: bool
# default: false
#display_mob_spawn = false
## Spawn interval in seconds.
# type: int
# default: 600 (10 minutes)
#horse.spawn_interval = 600
## If disabled, whinny will attack players.
# type: bool
# default: true
#whinny.peaceful_only = true
## Chance of spawn at interval.
## Inverted chance that entity will spawn.
# type: int
# default: 9000
#horse.spawn_chance = 9000
# min: 0
# default: 50000
#whinny.spawn_chance = 50000
## Minimum height at which entity can spawn.
# type: int
# default: -500
#whinny.spawn_height_min = -500
## Maximum height at which entity can spawn.
# type: int
# default: 500
#whinny.spawn_height_max = 500
## Use mouse to control direction instead of a/d keys.
# type: bool
# default: true
#whinny.enable_mouse_ctrl = true

View File

@ -1,3 +0,0 @@
next
- fixed apple eating sound so only nearby players hear it

View File

@ -1,33 +0,0 @@
## Horse Mob for [Minetest](http://minetest.net/)
---
### Description:
A ridable horse forked from [KPGMobs][kpgmobs] originally by PilzAdam & KrupnovPavel. It has been ported to use the [Creatures MOB-Framework API][git.creatures].
---
### Licensing:
- Code & models: [MIT](LICENSE.txt) (models by Krupnov Pavel)
- Sounds: see [sources.txt](sounds/sources.txt)
---
### Dependencies:
- Required:
- [default][git.default]
- [creatures][git.creatures]
- Optional:
- none
---
### Links:
- [Original KPGMobs forum thread][kpgmobs]
- [Git repo](https://github.com/AntumMT/mod-horse)
[kpgmobs]: https://forum.minetest.net/viewtopic.php?t=8798
[git.creatures]: https://github.com/AntumMT/mod-creatures
[git.default]: https://github.com/minetest/minetest_game/tree/master/mods/default

View File

@ -1,5 +0,0 @@
TODO:
- fix walking so player can control when riding
- prevent owned horses from jumping fences to escape corrals
- don't let horses "swim" in air

View File

@ -1,391 +0,0 @@
cmer_horse = {}
cmer_horse.modname = core.get_current_modname()
cmer_horse.modpath = core.get_modpath(cmer_horse.modname)
dofile(cmer_horse.modpath .. "/settings.lua")
--HORSE go go goooo :)
local horse = {
physical = true,
collisionbox = {-0.4, -0.01, -0.4, 0.4, 1, 0.4},
visual = "mesh",
stepheight = 1.1,
visual_size = {x=1,y=1},
mesh = "mobs_horseh1.x",
textures = {"mobs_horseh1.png"},
driver = nil,
v = 0,
}
local function is_ground(pos)
local nn = minetest.get_node(pos).name
return minetest.get_item_group(nn, "crumbly") ~= 0
end
local function get_sign(i)
-- FIXME:
if i == nil then i = 0 end
if i == 0 then
return 0
else
return i/math.abs(i)
end
end
local function get_velocity(v, yaw, y)
local x = math.cos(yaw)*v
local z = math.sin(yaw)*v
return {x=x, y=y, z=z}
end
local function get_v(v)
return math.sqrt(v.x^2+v.z^2)
end
function horse:on_rightclick(clicker)
if not clicker or not clicker:is_player() then
return
end
local pname = clicker:get_player_name()
if not self.owner then
local wielded = clicker:get_wielded_item()
-- FIXME: should be done with left click/punch
if wielded and wielded:get_name() == "default:apple" then
wielded:set_count(wielded:get_count()-1)
clicker:set_wielded_item(wielded)
core.sound_play("creatures_apple_bite", {object=self.object})
if self._appetite == nil then
self._appetite = 1
else
self._appetite = self._appetite + 1
if self._appetite == 10 then
self.owner = pname
self._appetite = nil
core.chat_send_player(pname, "You now own this horse!")
return
end
end
core.chat_send_player(pname, "This horse is still hungry. Keep feeding it.")
return
else
-- can't ride wild horses
core.chat_send_player(pname, "This horse is too wild to ride. Try feeding it some apples.")
return
end
end
-- only owner can ride
if self.owner and pname ~= self.owner then
core.chat_send_player(pname, "This horse is owned by " .. self.owner)
return
end
if self.driver and clicker == self.driver then
self.driver = nil
clicker:set_detach()
elseif not self.driver then
self.driver = clicker
clicker:set_attach(self.object, "", {x=0,y=5,z=0}, {x=0,y=0,z=0})
self.object:set_yaw(clicker:get_look_horizontal())
end
end
function horse:on_activate(staticdata, dtime_s)
--self.object:set_armor_groups({immortal=1})
if staticdata then
self.v = tonumber(staticdata)
end
end
function horse:get_staticdata()
return tostring(self.v)
end
function horse:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage)
if puncher and puncher:is_player() then
local pname = puncher:get_player_name()
local owner = self.owner
-- don't allow owned horses to be killed or owned by other players
if owner and pname ~= owner then
core.chat_send_player(pname, "This horse is owned by " .. owner)
return true
end
local wielded = puncher:get_wielded_item()
if wielded then
local wname = wielded:get_name()
local idx = wname:find(":")
-- can be tamed with any item named "lasso"
if wname and idx and wname:sub(idx+1) == "lasso" then
if not owner then
core.chat_send_player(pname, "This horse is too wild to tame. Try feeding it some apples.")
return true
else
local pinv = puncher:get_inventory()
if not pinv:room_for_item("main", self.name .. "_spawn_egg") then
core.chat_send_player(pname, "You don't have enought room in your inventory.")
else
self.object:remove()
puncher:get_inventory():add_item("main", self.name .. "_spawn_egg")
end
return true
end
end
end
end
return false
end
function horse:on_step(dtime)
if not self.driver then return false end
-- FIXME: let owners control horse
self.object:set_velocity({x=0, y=0, z=0})
self.object:set_animation()
--[[
self.v = get_v(self.object:get_velocity())*get_sign(self.v)
if self.driver then
local ctrl = self.driver:get_player_control()
if ctrl.up then
self.v = self.v+2
end
if ctrl.down then
self.v = self.v-0.1
end
if ctrl.left then
self.object:set_yaw(self.object:get_yaw()+math.pi/120+dtime*math.pi/120)
end
if ctrl.right then
self.object:set_yaw(self.object:get_yaw()-math.pi/120-dtime*math.pi/120)
end
if ctrl.jump then
local p = self.object:get_pos()
p.y = p.y-0.5
if is_ground(p) then
local pos = self.object:get_pos()
pos.y = math.floor(pos.y)+4
self.object:set_pos(pos)
self.object:set_velocity(get_velocity(self.v, self.object:get_yaw(), 0))
end
end
end
local s = get_sign(self.v)
self.v = self.v - 0.02*s
if s ~= get_sign(self.v) then
self.object:set_velocity({x=0, y=0, z=0})
self.v = 0
return
end
if math.abs(self.v) > 4.5 then
self.v = 4.5*get_sign(self.v)
end
local p = self.object:get_pos()
p.y = p.y-0.5
if not is_ground(p) then
if minetest.registered_nodes[minetest.get_node(p).name].walkable then
self.v = 0
end
self.object:set_acceleration({x=0, y=-10, z=0})
self.object:set_velocity(get_velocity(self.v, self.object:get_yaw(), self.object:get_velocity().y))
else
p.y = p.y+1
if is_ground(p) then
self.object:set_acceleration({x=0, y=3, z=0})
local y = self.object:get_velocity().y
if y > 2 then
y = 2
end
if y < 0 then
self.object:set_acceleration({x=0, y=10, z=0})
end
self.object:set_velocity(get_velocity(self.v, self.object:get_yaw(), y))
else
self.object:set_acceleration({x=0, y=0, z=0})
if math.abs(self.object:get_velocity().y) < 1 then
local pos = self.object:get_pos()
pos.y = math.floor(pos.y)+0.5
self.object:set_pos(pos)
self.object:set_velocity(get_velocity(self.v, self.object:get_yaw(), 0))
else
self.object:set_velocity(get_velocity(self.v, self.object:get_yaw(), self.object:get_velocity().y))
end
end
end
]]
return true
end
--[[
local likes = {"default:apple"}
if core.global_exists("farming") then
table.insert(likes, "farming:wheat")
end
]]
local drops = {}
if core.global_exists("mobs") then
table.insert(drops, {"mobs:meat_raw", {min=2, max=3}, 1.0})
table.insert(drops, {"mobs:leather", 1, 1.0})
end
local sounds = {
neigh = {
name = "creatures_horse_neigh_01",
gain = 1.0,
},
snort1 = {
name = "creatures_horse_snort_01",
gain = 1.0,
},
snort2 = {
name = "creatures_horse_snort_02",
gain = 1.0,
},
distress = {
name = "creatures_horse_neigh_02",
gain = 1.0,
},
}
-- FIXME:
-- - mounted horse movement is incorrect
local base_def = {
ownable = true,
stats = {
hp = 16,
hostile = false,
lifetime = cmer_horse.lifetime,
can_jump = 1, -- FIXME: should only not be able to jump over certain nodes for coralling
can_panic = true,
has_kockback = true,
},
modes = {
idle = {
chance = 0.3,
},
walk = {chance=0.7, moving_speed=1,},
},
model = {
mesh = "mobs_horse.x",
collisionbox = {-0.4, -0.01, -0.4, 0.4, 1, 0.4},
rotation = -90.0,
animations = {
idle = {
start = 25,
stop = 75,
speed = 15,
},
walk = {start=75, stop=100, speed=15,},
},
},
sounds = {
on_damage = sounds.distress,
on_death = sounds.snort2,
random = {
idle = sounds.snort1,
follow = sounds.neigh,
}
},
drops = drops,
spawning = {
abm_nodes = {
spawn_on = {"default:dirt_with_grass"},
neighbors = {},
},
abm_interval = cmer_horse.spawn_interval,
abm_chance = cmer_horse.spawn_chance,
max_number = 1,
light = {min=8, max=20},
height_limit = {min=-50, max=31000},
},
on_rightclick = function(self, clicker)
return horse.on_rightclick(self, clicker)
end,
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
return horse.on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
end,
on_step = function(self, dtime)
return horse.on_step(self, dtime)
end,
on_activate = function(self, staticdata)
return horse.on_activate(self, staticdata)
end,
get_staticdata = function(self)
return horse.get_staticdata(self)
end,
}
local horses = {
{
name = "horse_brown",
description = "Brown Horse",
textures = {"mobs_horse.png"},
inventory_image = "mobs_horse_brown_inv.png",
},
{
name = "horse_white",
description = "White Horse",
textures = {"mobs_horsepeg.png"},
inventory_image = "mobs_horse_white_inv.png",
},
{
name = "horse_black",
description = "Black Horse",
textures = {"mobs_horseara.png"},
inventory_image = "mobs_horse_black_inv.png",
},
}
for _, horse in ipairs(horses) do
local def = table.copy(base_def)
def.name = "creatures:" .. horse.name
def.model.textures = horse.textures
if not core.global_exists("asm") then
def.spawning.spawn_egg = {}
def.spawning.spawn_egg.description = horse.description
def.spawning.spawn_egg.texture = horse.inventory_image
else
asm.addEgg({
name=horse.name,
title=horse.description .. " Spawn Egg",
inventory_image = horse.inventory_image,
spawn = def.name,
})
core.register_alias(def.name .. "_spawn_egg", "spawneggs:" .. horse.name)
end
cmer.register_mob(def)
end
if not core.global_exists("mobs") then
cmer.register_alias("mob_horse:horse", "creatures:horse_brown")
cmer.register_alias("mobs:horse", "creatures:horse_brown")
end
if core.settings:get_bool("log_mods", false) then
core.log("action", "horse loaded")
end

View File

@ -1,4 +0,0 @@
name = cmer_horse
description = A ridable horse for Revived Creatures mob engine based on kpgmobs.
depends = cmer, default
optional_depends = mobs, asm_spawneggs

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,24 +0,0 @@
--- Entity lifespan.
--
-- @setting horse.lifetime
-- @settype int
-- @default 300
cmer_horse.lifetime = tonumber(core.settings:get("horse.lifetime")) or 300
--- Spawn rate frequency.
--
--
-- @setting horse.spawn_interval
-- @settype int
-- @default 600 (10 minutes)
-- @see [ABM definition](http://minetest.gitlab.io/minetest/definition-tables.html#abm-activeblockmodifier-definition)
cmer_horse.spawn_interval = tonumber(core.settings:get("horse.spawn_interval")) or 10 * 60
--- Chance of spawn at interval.
--
-- @setting horse.spawn_chance
-- @settype int
-- @default 9000
-- @see [ABM definition](http://minetest.gitlab.io/minetest/definition-tables.html#abm-activeblockmodifier-definition)
cmer_horse.spawn_chance = tonumber(core.settings:get("horse.spawn_chance")) or 9000

View File

@ -1,18 +0,0 @@
# Entity lifespan.
#
# type: int
# default: 300
horse.lifetime (Lifespan) int 300 1
# Spawn interval in seconds.
#
# type: int
# default: 600 (10 minutes)
horse.spawn_interval (Spawn interval) int 600 1
# Chance of spawn at interval.
#
# type: int
# default: 9000
horse.spawn_chance (Spawn chance) int 9000 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

View File

@ -0,0 +1,14 @@
1.0
- forked from original whinny mod
- updated for 0.4.16/5.x Minetest
- renamed horses
- added settings for min/max spawn height & spawn chance
- changed setting "only_peaceful_whinny" to "whinny.peaceful_only"
- player faces same direction as horse after mounting
- added setting to use mouse/look direction to control left/right turning (enabled by default)
- horse can follow multiple items
- added sounds for when damaged
- added sounds for when in "walk" & "stand" states
- added galloping sounds while riding
- mobs:lasso can be used to pick up tamed horses

View File

@ -1,8 +1,7 @@
The MIT License (MIT)
Copyright © 2021 Jordan Irwin (AntumDeluge)
Copyright © 2014 Krupnov Pavel
Copyright © 2021 Jordan Irwin (AntumDeluge)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -0,0 +1,24 @@
## Horses for Minetest
### Description:
A continuation of the [whinny][forum] mod for [Minetest], that adds tameable & rideable horses.
![screenshot](screenshot.png)
### Licensing:
- Code: [MIT](LICENSE.txt)
- Models & textures by sparky: CC0
- Sounds: see [sounds/sources.txt](sounds/sources.txt)
### Links:
- [Forum][forum]
- [Git repo](https://github.com/AntumMT/mod-whinny)
- [Changelog](CHANGES.txt)
- [TODO](TODO.txt)
[Minetest]: http://minetest.net/
[forum]: https://forum.minetest.net/viewtopic.php?t=17170

View File

@ -0,0 +1,6 @@
TODO:
- fix drops
- add localization support
- fix player position on horse
- make turning gradual when using mouse control

904
mods/mobiles/whinny/api.lua Normal file
View File

@ -0,0 +1,904 @@
local yaw = {}
function whinny:register_mob(name, def)
core.register_entity(name, {
name = name,
hp_min = def.hp_min,
hp_max = def.hp_max,
appetite = def.appetite,
physical = true,
collisionbox = def.collisionbox,
visual = def.visual,
visual_size = def.visual_size,
mesh = def.mesh,
makes_footstep_sound = def.makes_footstep_sound,
view_range = def.view_range,
walk_velocity = def.walk_velocity,
run_velocity = def.run_velocity,
damage = def.damage,
light_damage = def.light_damage,
water_damage = def.water_damage,
lava_damage = def.lava_damage,
disable_fall_damage = def.disable_fall_damage,
drops = def.drops,
armor = def.armor,
drawtype = def.drawtype,
on_rightclick = def.on_rightclick,
type = def.type,
attack_type = def.attack_type,
arrow = def.arrow,
shoot_interval = def.shoot_interval,
sounds = def.sounds,
animation = def.animation,
follow = def.follow,
jump = def.jump or true,
exp_min = def.exp_min or 0,
exp_max = def.exp_max or 0,
walk_chance = def.walk_chance or 50,
attacks_monsters = def.attacks_monsters or false,
group_attack = def.group_attack or false,
step = def.step or 0,
fov = def.fov or 120,
passive = def.passive or false,
stimer = 0,
timer = 0,
env_damage_timer = 0, -- only if state = "attack"
attack = {player=nil, dist=nil},
state = "stand",
v_start = false,
old_y = nil,
lifetimer = 600,
tamed = false,
do_attack = function(self, player, dist)
if self.state ~= "attack" then
if self.sounds.war_cry then
if math.random(0,100) < 90 then
core.sound_play(self.sounds.war_cry,{ object = self.object })
end
end
self.state = "attack"
self.attack.player = player
self.attack.dist = dist
end
end,
set_velocity = function(self, v)
local yaw = self.object:get_yaw()
if yaw then
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
local x = math.sin(yaw) * -v
local y = self.object:get_velocity().y
local z = math.cos(yaw) * v
for _, coord in ipairs({x, y, z}) do
if core.is_nan(coord) then
coord = 0
end
end
self.object:set_velocity({x=x, y=y, z=z})
end
end,
get_velocity = function(self)
local v = self.object:get_velocity()
if v then
return (v.x^2 + v.z^2)^(0.5)
end
return 0.0
end,
in_fov = function(self,pos)
-- checks if POS is in self's FOV
local yaw = self.object:get_yaw()
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
local vx = math.sin(yaw)
local vz = math.cos(yaw)
local ds = math.sqrt(vx^2 + vz^2)
local ps = math.sqrt(pos.x^2 + pos.z^2)
local d = { x = vx / ds, z = vz / ds }
local p = { x = pos.x / ps, z = pos.z / ps }
local an = ( d.x * p.x ) + ( d.z * p.z )
local a = math.deg( math.acos( an ) )
if a > ( self.fov / 2 ) then
return false
else
return true
end
end,
set_animation = function(self, type)
if not self.animation then return end
if not self.animation.current then
self.animation.current = ""
end
if type == "stand" and self.animation.current ~= "stand" then
if self.animation.stand_start and self.animation.stand_end and
self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.stand_start,y=self.animation.stand_end},
self.animation.speed_normal,
0
)
self.animation.current = "stand"
end
elseif type == "walk" and self.animation.current ~= "walk" then
if self.animation.walk_start and self.animation.walk_end and
self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.walk_start,y=self.animation.walk_end},
self.animation.speed_normal,
0
)
self.animation.current = "walk"
end
elseif type == "run" and self.animation.current ~= "run" then
if self.animation.run_start and self.animation.run_end and
self.animation.speed_run then
self.object:set_animation(
{x=self.animation.run_start,y=self.animation.run_end},
self.animation.speed_run,
0
)
self.animation.current = "run"
end
elseif type == "punch" and self.animation.current ~= "punch" then
if self.animation.punch_start and self.animation.punch_end and
self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.punch_start,y=self.animation.punch_end},
self.animation.speed_normal,
0
)
self.animation.current = "punch"
end
end
end,
on_step = function(self, dtime)
if self.type == "monster" and whinny.peaceful_only then
self.object:remove()
end
self.lifetimer = self.lifetimer - dtime
-- RJK:
--if self.lifetimer <= 0 and not self.tamed and self.type ~= "npc" then
if self.lifetimer <= 0 and not self.tamed and self.name ~= "whinny:ent" then
local player_count = 0
for _,obj in ipairs(core.get_objects_inside_radius(self.object:get_pos(), 10)) do
if obj:is_player() then
player_count = player_count+1
end
end
if player_count == 0 and self.state ~= "attack" then
core.log("action","lifetimer expired, removed mob "..self.name)
self.object:remove()
return
end
end
if self.object:get_velocity().y > 0.1 then
local yaw = self.object:get_yaw()
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
local x = math.sin(yaw) * -2
local z = math.cos(yaw) * 2
if core.get_item_group(core.get_node(self.object:get_pos()).name, "water") ~= 0 then
self.object:set_acceleration({x = x, y = 1.5, z = z})
else
self.object:set_acceleration({x = x, y = -14.5, z = z})
end
else
if core.get_item_group(core.get_node(self.object:get_pos()).name, "water") ~= 0 then
self.object:set_acceleration({x = 0, y = 1.5, z = 0})
else
self.object:set_acceleration({x = 0, y = -14.5, z = 0})
end
end
if self.disable_fall_damage and self.object:get_velocity().y == 0 then
if not self.old_y then
self.old_y = self.object:get_pos().y
else
local d = self.old_y - self.object:get_pos().y
if d > 5 then
local damage = d-5
self.object:set_hp(self.object:get_hp()-damage)
if self.object:get_hp() == 0 then
self.object:remove()
end
end
self.old_y = self.object:get_pos().y
end
end
self.timer = self.timer+dtime
if self.state ~= "attack" then
if self.timer < 1 then return end
self.timer = 0
end
if self.sounds and self.sounds.random and math.random(1, 100) <= 1 then
local to_play = self.sounds.random[self.state]
if to_play then
core.sound_play(to_play.name, {object=self.object, to_play.gain})
end
end
local do_env_damage = function(self)
local pos = self.object:get_pos()
local n = core.get_node(pos)
if self.light_damage and self.light_damage ~= 0 and pos.y>0 and
core.get_node_light(pos) and core.get_node_light(pos) > 4 and
core.get_timeofday() > 0.2 and core.get_timeofday() < 0.8 then
self.object:set_hp(self.object:get_hp()-self.light_damage)
if self.object:get_hp() == 0 then
self.object:remove()
end
end
if self.water_damage and self.water_damage ~= 0 and
core.get_item_group(n.name, "water") ~= 0 then
self.object:set_hp(self.object:get_hp()-self.water_damage)
if self.object:get_hp() == 0 then
self.object:remove()
end
end
if self.lava_damage and self.lava_damage ~= 0 and
core.get_item_group(n.name, "lava") ~= 0 then
self.object:set_hp(self.object:get_hp()-self.lava_damage)
if self.object:get_hp() == 0 then
self.object:remove()
end
end
end -- FIXME: missplaced end???
self.env_damage_timer = self.env_damage_timer + dtime
if self.state == "attack" and self.env_damage_timer > 1 then
self.env_damage_timer = 0
do_env_damage(self)
elseif self.state ~= "attack" then
do_env_damage(self)
end
-- FIND SOMEONE TO ATTACK
if ( self.type == "monster" or self.type == "barbarian" ) and
whinny.enable_damage and self.state ~= "attack" then
local s = self.object:get_pos()
local inradius = core.get_objects_inside_radius(s,self.view_range)
local player = nil
local type = nil
for _,oir in ipairs(inradius) do
if oir:is_player() then
player = oir
type = "player"
else
local obj = oir:get_luaentity()
if obj then
player = obj.object
type = obj.type
end
end
if type == "player" or type == "npc" then
local s = self.object:get_pos()
local p = player:get_pos()
local sp = s
p.y = p.y + 1
sp.y = sp.y + 1 -- aim higher to make looking up hills more realistic
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
if dist < self.view_range and self.in_fov(self,p) then
if core.line_of_sight(sp,p,2) == true then
self.do_attack(self,player,dist)
break
end
end
end
end
end
-- NPC FIND A MONSTER TO ATTACK
if self.type == "npc" and self.attacks_monsters and self.state ~= "attack" then
local s = self.object:get_pos()
local inradius = core.get_objects_inside_radius(s,self.view_range)
for _, oir in pairs(inradius) do
local obj = oir:get_luaentity()
if obj then
if obj.type == "monster" or obj.type == "barbarian" then
-- attack monster
local p = obj.object:get_pos()
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
print("attack monster at "..core.pos_to_string(obj.object:get_pos()))
self.do_attack(self,obj.object,dist)
break
end
end
end
end
local follow = false
local follow_multiple = type(self.follow) == "table"
if follow_multiple then
follow = #self.follow > 0
else
follow = self.follow ~= ""
end
if follow and not self.following then
for _, player in pairs(core.get_connected_players()) do
local s = self.object:get_pos()
local p = player:get_pos()
if s and p then
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
if self.view_range and dist < self.view_range then
self.following = player
break
end
end
end
end
if self.following and self.following:is_player() then
local wielded = self.following:get_wielded_item():get_name()
local likes_wielded = false
if follow_multiple then
for _, i in ipairs(self.follow) do
if i == wielded then
likes_wielded = true
break
end
end
else
likes_wielded = wielded == self.follow
end
if not likes_wielded then
self.following = nil
else
local s = self.object:get_pos()
local p = self.following:get_pos()
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
if dist > self.view_range then
self.following = nil
self.v_start = false
else
local vec = {x=p.x-s.x, y=p.y-s.y, z=p.z-s.z}
local yaw = math.atan(vec.z/vec.x)+math.pi/2
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
if p.x > s.x then
yaw = yaw+math.pi
end
self.object:set_yaw(yaw)
if dist > 2 then
if not self.v_start then
self.v_start = true
self.set_velocity(self, self.walk_velocity)
else
if self.jump and self.get_velocity(self) <= 1.5 and self.object:get_velocity().y == 0 then
local v = self.object:get_velocity()
v.y = 6
self.object:set_velocity(v)
end
self.set_velocity(self, self.walk_velocity)
end
self:set_animation("walk")
else
self.v_start = false
self.set_velocity(self, 0)
self:set_animation("stand")
end
return
end
end
end
if self.state == "stand" then
-- randomly turn
if math.random(1, 4) == 1 then
-- if there is a player nearby look at them
local lp = nil
local s = self.object:get_pos()
if self.type == "npc" then
local o = core.get_objects_inside_radius(self.object:get_pos(), 3)
local yaw = 0
for _,o in ipairs(o) do
if o:is_player() then
lp = o:get_pos()
break
end
end
end
if lp ~= nil then
local vec = {x=lp.x-s.x, y=lp.y-s.y, z=lp.z-s.z}
yaw = math.atan(vec.z/vec.x)+math.pi/2
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
if lp.x > s.x then
yaw = yaw+math.pi
end
else
yaw = self.object:get_yaw()
if yaw then
yaw = yaw + ((math.random(0,360)-180)/180*math.pi)
end
end
self.object:set_yaw(yaw)
end
self.set_velocity(self, 0)
self.set_animation(self, "stand")
if math.random(1, 100) <= self.walk_chance then
self.set_velocity(self, self.walk_velocity)
self.state = "walk"
self.set_animation(self, "walk")
end
elseif self.state == "walk" then
if math.random(1, 100) <= 30 then
local yaw = self.object:get_yaw()
if yaw then
self.object:set_yaw(yaw + ((math.random(0,360)-180)/180*math.pi))
end
end
local vel = self.object:get_velocity()
if self.jump and self.get_velocity(self) <= 0.5 and vel and vel.y == 0 then
local v = self.object:get_velocity()
v.y = 6
self.object:set_velocity(v)
end
self:set_animation("walk")
self.set_velocity(self, self.walk_velocity)
if math.random(1, 100) <= 30 then
self.set_velocity(self, 0)
self.state = "stand"
self:set_animation("stand")
end
elseif self.state == "attack" and self.attack_type == "dogfight" then
if not self.attack.player or not self.attack.player:get_pos() then
print("stop attacking")
self.state = "stand"
self:set_animation("stand")
return
end
local s = self.object:get_pos()
local p = self.attack.player:get_pos()
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
if dist > self.view_range or self.attack.player:get_hp() <= 0 then
self.state = "stand"
self.v_start = false
self.set_velocity(self, 0)
self.attack = {player=nil, dist=nil}
self:set_animation("stand")
return
else
self.attack.dist = dist
end
local vec = {x=p.x-s.x, y=p.y-s.y, z=p.z-s.z}
local yaw = math.atan(vec.z/vec.x)+math.pi/2
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
if p.x > s.x then
yaw = yaw+math.pi
end
self.object:set_yaw(yaw)
if self.attack.dist > 2 then
if not self.v_start then
self.v_start = true
self.set_velocity(self, self.run_velocity)
else
if self.jump and self.get_velocity(self) <= 0.5 and self.object:get_velocity().y == 0 then
local v = self.object:get_velocity()
v.y = 6
self.object:set_velocity(v)
end
self.set_velocity(self, self.run_velocity)
end
self:set_animation("run")
else
self.set_velocity(self, 0)
self:set_animation("punch")
self.v_start = false
if self.timer > 1 then
self.timer = 0
local p2 = p
local s2 = s
p2.y = p2.y + 1.5
s2.y = s2.y + 1.5
if core.line_of_sight(p2,s2) == true then
if self.sounds and self.sounds.attack then
core.sound_play(self.sounds.attack, {object = self.object})
end
self.attack.player:punch(self.object, 1.0,
{
full_punch_interval = 1.0,
damage_groups = {fleshy=self.damage},
},
vec
)
if math.random(0,3) == 3 and self.attack.player:is_player() then
local snum = math.random(1,4)
core.sound_play("default_hurt"..tostring(snum),
{
object = self.attack.player,
}
)
end
if self.attack.player:get_hp() <= 0 then
self.state = "stand"
self:set_animation("stand")
end
end
end
end
elseif self.state == "attack" and self.attack_type == "shoot" then
if not self.attack.player or not self.attack.player:is_player() then
self.state = "stand"
self:set_animation("stand")
return
end
local s = self.object:get_pos()
local p = self.attack.player:get_pos()
p.y = p.y - .5
s.y = s.y + .5
local dist = ((p.x-s.x)^2 + (p.y-s.y)^2 + (p.z-s.z)^2)^0.5
if dist > self.view_range or self.attack.player:get_hp() <= 0 then
self.state = "stand"
self.v_start = false
self.set_velocity(self, 0)
if self.type ~= "npc" then
self.attack = {player=nil, dist=nil}
end
self:set_animation("stand")
return
else
self.attack.dist = dist
end
local vec = {x=p.x-s.x, y=p.y-s.y, z=p.z-s.z}
local yaw = math.atan(vec.z/vec.x)+math.pi/2
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
if p.x > s.x then
yaw = yaw+math.pi
end
self.object:set_yaw(yaw)
self.set_velocity(self, 0)
if self.timer > self.shoot_interval and math.random(1, 100) <= 60 then
self.timer = 0
self:set_animation("punch")
if self.sounds and self.sounds.attack then
core.sound_play(self.sounds.attack, {object = self.object})
end
local p = self.object:get_pos()
p.y = p.y + (self.collisionbox[2]+self.collisionbox[5])/2
local obj = core.add_entity(p, self.arrow)
local amount = (vec.x^2+vec.y^2+vec.z^2)^0.5
local v = obj:get_luaentity().velocity
vec.y = vec.y+1
vec.x = vec.x*v/amount
vec.y = vec.y*v/amount
vec.z = vec.z*v/amount
obj:set_velocity(vec)
end
end
end,
on_activate = function(self, staticdata, dtime_s)
-- reset HP
local pos = self.object:get_pos()
local distance_rating = ( ( get_distance({x=0,y=0,z=0},pos) ) / 20000 )
local newHP = self.hp_min + math.floor( self.hp_max * distance_rating )
self.object:set_hp( newHP )
self.object:set_armor_groups({fleshy=self.armor})
self.object:set_acceleration({x=0, y=-10, z=0})
self.state = "stand"
self.object:set_velocity({x=0, y=self.object:get_velocity().y, z=0})
self.object:set_yaw(math.random(1, 360)/180*math.pi)
if self.type == "monster" and whinny.peaceful_only then
self.object:remove()
end
if self.type ~= "npc" then
self.lifetimer = 600 - dtime_s
end
if staticdata then
local tmp = core.deserialize(staticdata)
if tmp and tmp.lifetimer then
self.lifetimer = tmp.lifetimer - dtime_s
end
if tmp and tmp.tamed then
self.tamed = tmp.tamed
end
--[[if tmp and tmp.textures then
self.object:set_properties(tmp.textures)
end]]
end
if self.lifetimer <= 0 and not self.tamed and self.type ~= "npc" then
self.object:remove()
end
end,
get_staticdata = function(self)
local tmp = {
lifetimer = self.lifetimer,
tamed = self.tamed,
textures = def.available_textures["texture_"..math.random(1,def.available_textures["total"])],
}
self.object:set_properties(tmp)
return core.serialize(tmp)
end,
on_punch = function(self, hitter)
local weapon = hitter:get_wielded_item()
if weapon:get_definition().tool_capabilities ~= nil then
local wear = ( weapon:get_definition().tool_capabilities.full_punch_interval / 75 ) * 9000
weapon:add_wear(wear)
hitter:set_wielded_item(weapon)
end
if weapon:get_definition().sounds ~= nil then
local s = math.random(0,#weapon:get_definition().sounds)
core.sound_play(weapon:get_definition().sounds[s],
{
object = hitter,
}
)
else
core.sound_play("player_damage",
{
object = hitter,
}
)
end
if self.sounds and self.sounds.on_damage then
core.sound_play(self.sounds.on_damage.name,
{object=self.object, self.sounds.on_damage.gain})
end
-- FIXME: drops not working because HP never <= 0
if self.object:get_hp() <= 0 then
if hitter and hitter:is_player() and hitter:get_inventory() then
for _, drop in ipairs(self.drops) do
if math.random(1, drop.chance) == 1 then
hitter:get_inventory():add_item("main",
ItemStack(drop.name.." "..math.random(drop.min, drop.max)))
end
end
-- FIXME: doesn't work
if self.sounds.on_death ~= nil then
core.sound_play(self.sounds.on_death.name,
{object=self.object, self.sounds.on_death.gain})
end
if core.get_modpath("skills") and core.get_modpath("experience") then
-- DROP experience
local distance_rating = ( ( get_distance({x=0,y=0,z=0},pos) ) / ( skills.get_player_level(hitter:get_player_name()).level * 1000 ) )
local emax = math.floor( self.exp_min + ( distance_rating * self.exp_max ) )
local expGained = math.random(self.exp_min, emax)
skills.add_exp(hitter:get_player_name(),expGained)
local expStack = experience.exp_to_items(expGained)
for _,stack in ipairs(expStack) do
default.drop_item(pos,stack)
end
end
end
end
if self.passive == false then
self.do_attack(self,hitter,1)
-- alert other NPCs to the attack
local inradius = core.get_objects_inside_radius(hitter:get_pos(),5)
for _, oir in pairs(inradius) do
local obj = oir:get_luaentity()
if obj then
if obj.group_attack == true and obj.state ~= "attack" then
obj.do_attack(obj,hitter,1)
end
end
end
end
end,
})
end
whinny.spawning_whinny = {}
function whinny:register_spawn(name, nodes, max_light, min_light, chance, active_object_count, min_height, max_height, spawn_func)
whinny.spawning_whinny[name] = true
-- RJK: Add this:
if name == "whinny:rohan_guard" or
name == "whinny:gondor_guard" or
name == "whinny:dunlending" or
name == "whinny:hobbit" or
name == "whinny:orc" or
name == "whinny:half_troll" or
name == "whinny:troll" or
name == "whinny:dwarf" or
name == "whinny:dead_men" then
chance = chance * 1
end
core.register_abm({
nodenames = nodes,
--neighbors = {"air"},
interval = 30,
chance = chance,
action = function(pos, node, _, active_object_count_wider)
if active_object_count_wider > active_object_count then return end
if not whinny.spawning_whinny[name] then return end
--[[ don't spawn inside of blocks
local p2 = pos
p2.y = p2.y + 1
local p3 = p2
p3.y = p3.y + 1
if core.registered_nodes[core.get_node(p2).name].walkable == false or core.registered_nodes[core.get_node(p3).name].walkable == false then
return
end]]
pos.y = pos.y+1
if not core.get_node_light(pos) then return end
if core.get_node_light(pos) > max_light then return end
if core.get_node_light(pos) < min_light then return end
if pos.y > max_height then return end
if pos.y < min_height then return end
local get_node_pos
local get_node_pos_name
local registered_node
get_node_pos = core.get_node(pos)
if get_node_pos == nil then return end
get_node_pos_name = get_node_pos.name
if get_node_pos_name == nil then return end
registered_node = core.registered_nodes [get_node_pos_name]
if registered_node == nil then return end
if registered_node.walkable == true or registered_node.walkable == nil then return end
pos.y = pos.y+1
get_node_pos = core.get_node(pos)
if get_node_pos == nil then return end
get_node_pos_name = get_node_pos.name
if get_node_pos_name == nil then return end
registered_node = core.registered_nodes [get_node_pos_name]
if registered_node == nil then return end
if registered_node.walkable == true or registered_node.walkable == nil then return end
if spawn_func and not spawn_func(pos, node) then return end
if whinny.display_spawn then
core.chat_send_all("[whinny] Add "..name.." at "..core.pos_to_string(pos))
end
local mob = core.add_entity(pos, name)
-- setup the hp, armor, drops, etc... for this specific mob
local distance_rating = ( ( get_distance({x=0,y=0,z=0},pos) ) / 15000 )
if mob then
mob = mob:get_luaentity()
local newHP = mob.hp_min + math.floor( mob.hp_max * distance_rating )
mob.object:set_hp( newHP )
end
end
})
end
function whinny:register_arrow(name, def)
core.register_entity(name, {
physical = false,
visual = def.visual,
visual_size = def.visual_size,
textures = def.textures,
velocity = def.velocity,
hit_player = def.hit_player,
hit_node = def.hit_node,
on_step = function(self, dtime)
local pos = self.object:get_pos()
if core.get_node(self.object:get_pos()).name ~= "air" then
self.hit_node(self, pos, node)
self.object:remove()
return
end
pos.y = pos.y-1
for _,player in pairs(core.get_objects_inside_radius(pos, 1)) do
if player:is_player() then
self.hit_player(self, player)
self.object:remove()
return
end
end
end
})
end
function get_distance(pos1,pos2)
if ( pos1 ~= nil and pos2 ~= nil ) then
return math.abs(math.floor(math.sqrt( (pos1.x - pos2.x)^2 + (pos1.z - pos2.z)^2 )))
else
return 0
end
end

View File

@ -0,0 +1,562 @@
local rot_compensate = 4.7
-- name, fill value
local fill_values = {
["default:apple"] = 2,
["farming:wheat"] = 1,
["farming:barley"] = 3,
["farming:oat"] = 5,
["farming:carrot"] = 4,
["farming:carrot_gold"] = 5,
["farming:cucumber"] = 2,
}
local horse_likes = {}
for iname, fill in pairs(fill_values) do
if core.registered_items[iname] then
table.insert(horse_likes, iname)
else
whinny.log("warning", "\"" .. iname
.. "\" is not a registered item, removing it from items that horse will follow")
end
end
local sounds = {
neigh = {
name = "whinny_horse_neigh_01",
gain = 1.0,
},
snort1 = {
name = "whinny_horse_snort_01",
gain = 1.0,
},
snort2 = {
name = "whinny_horse_snort_02",
gain = 1.0,
},
distress = {
name = "whinny_horse_neigh_02",
gain = 1.0,
},
}
-- galloping sounds
local function handle_is_playing(handle)
return handle > -1
end
local function is_ground(pos)
local nn = core.get_node(pos).name
return core.get_item_group(nn, "crumbly") ~= 0 or
core.get_item_group(nn, "cracky") ~= 0 or
core.get_item_group(nn, "choppy") ~= 0 or
core.get_item_group(nn, "snappy") ~= 0
end
local function get_sign(i)
if i == 0 then
return 0
else
return i/math.abs(i)
end
end
local function get_velocity(speed, yaw, y)
local x = math.cos(yaw)*speed
local z = math.sin(yaw)*speed
return {x=x, y=y, z=z}
end
local function get_speed(velocity)
return math.sqrt(velocity.x^2+velocity.z^2)
end
-- ===================================================================
local horse_drops = {}
if core.registered_items["mobs:meat_raw"] then
table.insert(horse_drops, {name="mobs:meat_raw", chance=1, min=2, max=3})
end
local function register_wildhorse(color)
whinny:register_mob("whinny:horse_" .. color, {
type = "animal",
hp_min = 10,
hp_max = 10,
appetite = 40,
collisionbox = {-.5, -0.01, -.5, .5, 1.4, .5},
available_textures = {
total = 1,
texture_1 = {"whinny_horse_" .. color .. "_mesh.png"},
},
visual = "mesh",
drops = horse_drops,
mesh = "horsemob.x",
makes_footstep_sound = true,
walk_velocity = 1,
armor = 100,
drawtype = "front",
water_damage = 1,
lava_damage = 5,
light_damage = 0,
sounds = {
on_damage = sounds.distress,
on_death = sounds.snort2,
random = {
stand = sounds.snort1,
walk = sounds.neigh,
}
},
animation = {
speed_normal = 20,
stand_start = 300,
stand_end = 460,
walk_start = 10,
walk_end = 60
},
follow = horse_likes,
view_range = 5,
on_rightclick = function(self, clicker)
local pname = clicker:get_player_name()
local item = clicker:get_wielded_item()
local item_name = item:get_name()
local wants = false
if type(horse_likes) == "table" then
for _, i in ipairs(horse_likes) do
if i == item_name then
wants = true
break
end
end
else
wants = item_name == horse_likes
end
if wants then
local fills = fill_values[item_name]
if not fills then fills = 1 end
self.appetite = self.appetite - fills
core.sound_play("whinny_apple_bite", {object=self.object})
if not whinny.creative then
item:take_item()
clicker:set_wielded_item(item)
end
if self.appetite <= 0 then
-- replace with tamed horse
core.add_entity(self.object:get_pos(),
"whinny:horse_" .. color .. "_tame",
core.serialize({owner=pname}))
self.object:remove()
core.chat_send_player(pname, "This horse is now tame!")
return
else
core.chat_send_player(pname, "This horse is still hungry. Keep feeding it.")
end
else
-- can't ride wild horses
core.chat_send_player(pname, "This horse is too wild to ride. Try feeding it.")
end
end,
jump = true,
step = 1,
passive = true,
})
end
-- ===================================================================
local function register_basehorse(name, craftitem, horse)
if craftitem ~= nil then
function craftitem.on_place(itemstack, placer, pointed_thing)
if pointed_thing.above then
core.add_entity(pointed_thing.above, name, core.serialize({owner=placer:get_player_name()}))
if not whinny.creative then
itemstack:take_item()
end
end
return itemstack
end
core.register_craftitem(name, craftitem)
end
function horse:set_animation(type)
if type == self.animation_current then return end
if type == self.animation.mode_stand then
if self.animation.stand_start and self.animation.stand_end and self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.stand_start,y=self.animation.stand_end},
self.animation.speed_normal * 0.6,
0
)
self.animation_current = self.animation.mode_stand
end
elseif type == self.animation.mode_walk then
if self.animation.walk_start and self.animation.walk_end and self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.walk_start,y=self.animation.walk_end},
self.animation.speed_normal * 3,
0
)
self.animation_current = self.animation.mode_walk
end
elseif type == self.animation.mode_gallop then
if self.animation.gallop_start and self.animation.gallop_end and self.animation.speed_normal then
self.object:set_animation(
{x=self.animation.gallop_start,y=self.animation.gallop_end},
self.animation.speed_normal * 2,
0
)
self.animation_current = self.animation.mode_gallop
end
end
end
-- sounds played while riding
horse.gallop_handle_1 = -1
horse.gallop_handle_2 = -1
function horse:stop_gallop()
if self.gallop_handle_1 > -1 then
core.sound_stop(self.gallop_handle_1)
self.gallop_handle_1 = -1
end
if self.gallop_handle_2 > -1 then
core.sound_stop(self.gallop_handle_2)
self.gallop_handle_2 = -1
end
return self.gallop_handle_1 < 0 and self.gallop_handle_2 < 0
end
function horse:start_gallop(stage)
local to_play = "whinny_gallop_0" .. tostring(stage)
if stage == 1 and self.gallop_handle_1 < 0 then
self.gallop_handle_1 = core.sound_play(to_play, {object=self.object, loop=true})
return self.gallop_handle_1 >= 0
elseif stage == 2 and self.gallop_handle_2 < 0 then
self.gallop_handle_2 = core.sound_play(to_play, {object=self.object, loop=true})
return self.gallop_handle_2 >= 0
end
end
function horse:on_step(dtime)
local p = self.object:get_pos()
p.y = p.y - 0.1
local on_ground = is_ground(p)
local inside_block = self.object:get_pos()
inside_block.y = inside_block.y + 1
self.speed = get_speed(self.object:get_velocity())*get_sign(self.speed)
if self.driver then
-- galloping sounds
if self.speed > 5 then
if handle_is_playing(self.gallop_handle_1) then self:stop_gallop() end
if not handle_is_playing(self.gallop_handle_2) then
if not self:start_gallop(2) then
whinny.log("warning", "Failed to start gallop stage 2 sound")
end
end
elseif self.speed > 1 then
if handle_is_playing(self.gallop_handle_2) then self:stop_gallop() end
if not handle_is_playing(self.gallop_handle_1) then
if not self:start_gallop(1) then
whinny.log("warning", "Failed to start gallop stage 1 sound")
end
end
else
if handle_is_playing(self.gallop_handle_1) or handle_is_playing(self.gallop_handle_2) then
if not self:stop_gallop() then
whinny.log("warning", "Failed to stop gallop sounds")
end
end
end
-- driver controls
local ctrl = self.driver:get_player_control()
-- rotation (the faster we go, the less we rotate)
if whinny.enable_mouse_ctrl then
-- FIXME: turning should be gradual
local driver_look = self.driver:get_look_horizontal()
if driver_look then
self.object:set_yaw(driver_look - rot_compensate)
end
else
if ctrl.left then
self.object:set_yaw(self.object:get_yaw()+2*(1.5-math.abs(self.speed/self.max_speed))*math.pi/90 +dtime*math.pi/90)
end
if ctrl.right then
self.object:set_yaw(self.object:get_yaw()-2*(1.5-math.abs(self.speed/self.max_speed))*math.pi/90 -dtime*math.pi/90)
end
end
-- jumping (only if on ground)
if ctrl.jump then
if on_ground then
local v = self.object:get_velocity()
local vel = 3
--v.y = (self.jump_speed or 3)
v.y = vel
self.object:set_velocity(v)
end
end
-- forwards/backwards
if ctrl.up then
self.speed = self.speed + self.forward_boost
elseif ctrl.down then
if self.speed >= -2.9 then
self.speed = self.speed - self.forward_boost
end
elseif self.speed < 3 then
--decay speed if not going fast
self.speed = self.speed * 0.85
end
else
if math.abs(self.speed) < 1 then
self.speed = 0
else
self.speed = self.speed * 0.90
end
if self.sounds and self.sounds.random and math.random(1, 100) <= 1 then
local to_play = self.sounds.random.stand
if to_play then
core.sound_play(to_play.name, {object=self.object, to_play.gain})
end
end
end
if math.abs(self.speed) < 1 then
self.object:set_velocity({x=0,y=0,z=0})
self:set_animation(self.animation.mode_stand)
elseif self.speed > 5 then
self:set_animation(self.animation.mode_gallop)
else
self:set_animation(self.animation.mode_walk)
end
-- make sure we don't go past the limit
if core.get_item_group(core.get_node(inside_block).name, "water") ~= 0 then
if math.abs(self.speed) > self.max_speed * .2 then
self.speed = self.max_speed*get_sign(self.speed)*.2
end
else
if math.abs(self.speed) > self.max_speed then
self.speed = self.max_speed*get_sign(self.speed)
end
end
local p = self.object:get_pos()
p.y = p.y+1
if not is_ground(p) then
if core.registered_nodes[core.get_node(p).name].walkable then
self.speed = 0
end
self.object:set_acceleration({x=0, y=-10, z=0})
self.object:set_velocity(get_velocity(self.speed, self.object:get_yaw(), self.object:get_velocity().y))
else
self.object:set_acceleration({x=0, y=0, z=0})
-- falling
if math.abs(self.object:get_velocity().y) < 1 then
local pos = self.object:get_pos()
pos.y = math.floor(pos.y)+0.5
self.object:set_pos(pos)
self.object:set_velocity(get_velocity(self.speed, self.object:get_yaw(), 0))
else
self.object:set_velocity(get_velocity(self.speed, self.object:get_yaw(), self.object:get_velocity().y))
end
end
if self.object:get_velocity().y > 0.1 then
local yaw = self.object:get_yaw()
if self.drawtype == "side" then
yaw = yaw+(math.pi/2)
end
local x = math.sin(yaw) * -2
local z = math.cos(yaw) * 2
if core.get_item_group(core.get_node(inside_block).name, "water") ~= 0 then
self.object:set_acceleration({x = x, y = .1, z = z})
else
self.object:set_acceleration({x = x, y = -10, z = z})
end
else
if core.get_item_group(core.get_node(inside_block).name, "water") ~= 0 then
self.object:set_acceleration({x = 0, y = .1, z = 0})
else
self.object:set_acceleration({x = 0, y = -10, z = 0})
end
end
end
function horse:on_rightclick(clicker)
if not clicker or not clicker:is_player() then return end
if self.driver and clicker == self.driver then
self.driver = nil
clicker:set_detach()
clicker:set_eye_offset({x=0, y=0, z=0}, {x=0, y=0, z=0})
-- stop galloping sounds
if handle_is_playing(self.gallop_handle_1) or handle_is_playing(self.gallop_handle_2) then
if not self:stop_gallop() then
whinny.log("warning", "Failed to stop gallop sounds")
end
end
elseif not self.driver then
local pname = clicker:get_player_name()
local wielded = clicker:get_wielded_item():get_name()
-- FIXME: use other items if "mobs:lasso" not available (or any item named "lasso")
if wielded == "mobs:lasso" then
if self.owner and self.owner ~= pname then
core.chat_send_player(pname, "You cannot take " .. self.owner .. "'s horse")
else
local inv = clicker:get_inventory()
local stack = ItemStack(self.name)
if not inv:room_for_item("main", stack) then
core.chat_send_player(pname, "You do not have room in your inventory")
else
inv:add_item("main", stack)
self.object:remove()
end
end
return true
end
if self.owner and self.owner ~= pname then
core.chat_send_player(pname, "You cannot ride " .. self.owner .. "'s horse")
return true
end
self.driver = clicker
clicker:set_attach(self.object, "", {x=0,y=18,z=0}, {x=0,y=90,z=0})
clicker:set_eye_offset({x=0, y=8, z=0}, {x=0, y=0, z=0})
-- face same direction as horse
clicker:set_look_horizontal(self.object:get_yaw() + rot_compensate) -- FIXME: no idea why I need to add compensation
end
end
function horse:on_activate(staticdata, dtime_s)
self.object:set_armor_groups({fleshy=100})
if staticdata then
local data = core.deserialize(staticdata)
if data then
self.speed = data.speed
self.owner = data.owner
end
end
end
function horse:get_staticdata()
local data = {
speed = self.speed,
owner = self.owner,
}
return core.serialize(data)
end
function horse:on_punch(puncher, time_from_last_punch, tool_capabilities, direction)
if puncher:is_player() then
local pname = puncher:get_player_name()
-- don't allow owned horses to be killed or owned by other players
if self.owner and pname ~= self.owner then
core.chat_send_player(pname, "Don't kill " .. self.owner .. "'s horse!!!")
return true
end
-- don't damage your own horse while mounted
-- FIXME: horse still flashes
if self.driver then return true end
end
core.sound_play("player_damage", {object=self.object,})
if self.sounds and self.sounds.on_damage then
core.sound_play(self.sounds.on_damage.name,
{object=self.object, self.sounds.on_damage.gain})
end
end
core.register_entity(name, horse)
end
local function register_tamehorse(color, description)
register_basehorse("whinny:horse_" .. color .. "_tame",
{
description = description,
inventory_image = "whinny_horse_" .. color .. "_inv.png",
},
{
physical = true,
collisionbox = {-.5, -0.01, -.5, .5, 1.4, .5},
visual = "mesh",
stepheight = 1.1,
visual_size = {x=1, y=1},
mesh = "horse.x",
textures = {"whinny_horse_" .. color .. "_mesh.png"},
sounds = {
on_damage = sounds.distress,
on_death = sounds.snort2,
random = {
stand = sounds.snort1,
walk = sounds.neigh,
}
},
animation = {
speed_normal = 20,
stand_start = 300,
stand_end = 460,
walk_start = 10,
walk_end = 59,
gallop_start = 70,
gallop_end = 119,
mode_stand = 1,
mode_walk = 2,
mode_gallop = 3,
},
animation_current = 0,
max_speed = 7,
forward_boost = .2,
jump_boost = 4,
speed = 0,
driver = nil,
}
)
end
local spawn_nodes = {
"default:dirt_with_grass",
"default:dirt_with_dry_grass",
}
for color, name in pairs({["brown"]="Brown Horse", ["white"]="White Horse", ["black"]="Black Horse",}) do
register_tamehorse(color, name)
register_wildhorse(color)
whinny:register_spawn("whinny:horse_" .. color, spawn_nodes, 20, 6,
whinny.spawn_chance, 1, whinny.spawn_height_min, whinny.spawn_height_max)
-- to simplify item handling
core.register_alias("whinny:horse_" .. color, "whinny:horse_" .. color .. "_tame")
end

View File

@ -0,0 +1,30 @@
whinny = {}
whinny.modname = core.get_current_modname()
whinny.modpath = core.get_modpath(whinny.modname)
whinny.log = function(lvl, msg)
if not msg then
msg = lvl
lvl = nil
end
msg = "[" .. whinny.modname .. "] " .. msg
if not lvl then
core.log(msg)
else
core.log(lvl, msg)
end
end
local scripts = {
"settings",
"api",
"horse",
}
for _, script in ipairs(scripts) do
dofile(whinny.modpath .. "/" .. script .. ".lua")
end

View File

@ -0,0 +1,4 @@
name = whinny
version = 1.0
depends = default
optional_depends = mobs, farming

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

View File

@ -0,0 +1,24 @@
-- common settings
whinny.creative = core.settings:get_bool("creative_mode", false)
whinny.enable_damage = core.settings:get_bool("enable_damage", true)
-- mobs_redo settings
whinny.display_spawn = core.settings:get_bool("display_mob_spawn", false) -- deprecated?
-- unique settings
whinny.peaceful_only = core.settings:get_bool("whinny.peaceful_only", true)
whinny.spawn_chance = tonumber(core.settings:get("whinny.spawn_chance") or 50000)
whinny.spawn_height_min = tonumber(core.settings:get("whinny.spawn_height_min") or -500)
whinny.spawn_height_max = tonumber(core.settings:get("whinny.spawn_height_max") or 500)
whinny.enable_mouse_ctrl = core.settings:get_bool("whinny.enable_mouse_ctrl", true)

View File

@ -0,0 +1,18 @@
# If enabled, messages all players when spawned.
display_mob_spawn (Display spawn) bool false
# If disabled, whinny will attack players.
whinny.peaceful_only (Only peaceful) bool true
# Inverted chance that entity will spawn.
whinny.spawn_chance (Spawn chance) int 50000 0
# Minimum height at which entity can spawn.
whinny.spawn_height_min (Spawn min height) int -500
# Maximum height at which entity can spawn.
whinny.spawn_height_max (Spawn max height) int 500
# Use mouse to control direction instead of a/d keys.
whinny.enable_mouse_ctrl (Turn with mouse) bool true

View File

@ -1,25 +1,35 @@
- creatures_apple_bite:
- whinny_apple_bite:
- author: sonicmariobrotha
- license: CC0
- link: https://freesound.org/people/sonicmariobrotha/sounds/333825/
- creatures_horse_neigh_01:
- whinny_gallop_01:
- author: Alan McKinney (alanmcki)
- license: CC BY 3.0
- link: https://freesound.org/people/alanmcki/sounds/403026/
- whinny_gallop_02:
- author: Alan McKinney (alanmcki)
- license: CC BY 3.0
- link: https://freesound.org/people/alanmcki/sounds/403025/
- whinny_horse_neigh_01:
- author: GoodListener
- license: CC BY 3.0
- link: https://freesound.org/people/GoodListener/sounds/322443/
- creatures_horse_neigh_02:
- whinny_horse_neigh_02:
- author: foxen10
- license: CC0
- link: https://freesound.org/people/foxen10/sounds/149024/
- creatures_horse_snort_01:
- whinny_horse_snort_01:
- author: madklown
- license: CC0
- link: https://freesound.org/people/madklown/sounds/184503/
- creatures_horse_snort_02:
- whinny_horse_snort_02:
- author: 0_ciz
- license: CC0
- link: https://freesound.org/people/o_ciz/sounds/475480/

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -139,27 +139,6 @@ ghost.spawn_interval (Spawn interval) int 40 1
ghost.spawn_chance (Spawn chance) int 7300 1
[**horse]
# Entity lifespan.
#
# type: int
# default: 300
horse.lifetime (Lifespan) int 300 1
# Spawn interval in seconds.
#
# type: int
# default: 600 (10 minutes)
horse.spawn_interval (Spawn interval) int 600 1
# Chance of spawn at interval.
#
# type: int
# default: 9000
horse.spawn_chance (Spawn chance) int 9000 1
[**kitten]
# Spawn interval in seconds.
@ -298,6 +277,27 @@ mobs.velociraptor_max_height (Velociraptor max spawn height) int 31000
mobs.velociraptor_spawn_time (Velociraptor spawn time) enum any day,night,any
[**whinny]
# If enabled, messages all players when spawned.
display_mob_spawn (Display spawn) bool false
# If disabled, whinny will attack players.
whinny.peaceful_only (Only peaceful) bool true
# Inverted chance that entity will spawn.
whinny.spawn_chance (Spawn chance) int 50000 0
# Minimum height at which entity can spawn.
whinny.spawn_height_min (Spawn min height) int -500
# Maximum height at which entity can spawn.
whinny.spawn_height_max (Spawn max height) int 500
# Use mouse to control direction instead of a/d keys.
whinny.enable_mouse_ctrl (Turn with mouse) bool true
[*airtanks]
# Number of uses for steel air tanks.