duel.lua 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. local vector_round = vector.round
  2. local vector_distance = vector.distance
  3. local math_random = math.random
  4. local function debug_print(msg)
  5. --minetest.chat_send_all(msg)
  6. end
  7. armor.dueling_players = armor.dueling_players or {}
  8. local dueling_players = armor.dueling_players
  9. local ACTIVE_DUEL_PUNCH = nil
  10. -- Query whether player is currently in a duel.
  11. function armor.player_in_duel(pname)
  12. if dueling_players[pname] then
  13. return true
  14. end
  15. end
  16. -- For best duels, these numbers should be the same.
  17. -- This is just a max size for an arena. You can make smaller, just don't mark
  18. -- the whole area as part of the arena. Minimum size is 1 city block.
  19. local PUBLIC_BED_DISTANCE = 256
  20. local OPPONENT_DISTANCE = 256
  21. local DUEL_MAX_RADIUS = 256
  22. armor.DUEL_MAX_RADIUS = DUEL_MAX_RADIUS
  23. local ENV_TIME_AFTER_PUNCH = 15 -- Time from last punch env damage is accounted.
  24. local SPAWN_SAFE_ZONE = 5
  25. local RESPAWN_TIME = 10
  26. local SHOUT_COLOR = core.get_color_escape_sequence("#ff2a00")
  27. local DUEL_MELEE_STRINGS = {
  28. "<loser> lost a duel.",
  29. "<loser> got owned.",
  30. "<winner> defeated <loser> in a duel.",
  31. "<loser> lost to <winner>.",
  32. "<loser> was beat by <winner> in combat!",
  33. "<winner> crushed <loser> in combat!",
  34. "<winner> crushed <loser> in a duel.",
  35. "<winner> totally owned <loser>.",
  36. "<winner> won a duel with <loser>.",
  37. "<winner> dealt out a whopping drubbing.",
  38. "<loser> got <l_himself> a severe drubbing.",
  39. "<winner> beat <loser> in honorable combat!",
  40. "<winner> bested <loser>.",
  41. "<loser> got a royal walloping from <winner>.",
  42. "<loser> got trashed in a duel by <winner>.",
  43. "<winner> trashed <loser> in a duel.",
  44. "<loser> got some major hurt from <winner>.",
  45. "<winner> gave out a royal swatting to <loser>.",
  46. "<loser> was killed by <winner>'s <w_weapon>.",
  47. }
  48. local DUEL_ARROW_STRINGS = {
  49. "<loser> didn't get out of the way of <winner>'s flying projectile.",
  50. "<loser> never saw it coming.",
  51. "<loser> faced down <winner>'s artillery and lost.",
  52. "<winner> used <loser> for ranged target practice.",
  53. "<loser> became a pincushion.",
  54. "<loser> HEARD <winner>'s incoming artillery. Didn't avoid it.",
  55. "<winner> is having fun with that <w_weapon>.",
  56. "<winner> has pulled out the big guns.",
  57. "<winner> is busy sniping with <w_his> <w_weapon>. Watch out!",
  58. "<winner> didn't have to get close to <loser> to make that kill.",
  59. }
  60. local DUEL_STOMP_STRINGS = {
  61. "<winner> stomped on <loser>'s head.",
  62. "<loser> was flattened.",
  63. "<loser> was flattened by <winner>.",
  64. "<loser> got a taste of jackboot.",
  65. "<winner> used <loser> to cushion <w_his> fall.",
  66. "<loser> got <l_himself> some pancaking by <winner>'s boots of steel.",
  67. "<winner> smashed <loser> into the earth.",
  68. "<winner> crushed <loser> from above.",
  69. "<winner> stomped <loser> into the ground.",
  70. "<winner> used <loser> to break <w_his> fall.",
  71. "<winner> used <loser> like a trampoline!",
  72. }
  73. local DUEL_SUICIDE_STRINGS = {
  74. "<loser> killed <l_himself>.",
  75. "<loser> got a taste of <l_his> own medicine.",
  76. "<loser> got on the wrong end of <l_his> own weapon.",
  77. "<loser> suicided.",
  78. "<loser> ended <l_himself>.",
  79. "<loser> won a fight with <l_himself>.",
  80. "<loser> died like a noob: harm self-inflicted.",
  81. "<loser> self-terminated.",
  82. "<loser> died: incompetence.",
  83. "<loser> perished: weapon misuse.",
  84. "<loser> died: couldn't take what <l_he> dished out.",
  85. "<loser> killed <l_himself> with <l_his> own <l_weapon>.",
  86. "<loser> used <l_his> <l_weapon> on <l_himself>. Idiot!",
  87. "<loser> did something stupid.",
  88. "<loser> won the Darwin award.",
  89. "<loser> held <l_his> <l_weapon> by the wrong end.",
  90. "<loser> hit <l_himself>: terrible aim.",
  91. }
  92. local function announce_begin(pname)
  93. local msg = "# Server: <" .. rename.gpn(pname) .. "> has agreed to participate in a duel!"
  94. minetest.chat_send_all(SHOUT_COLOR .. msg)
  95. chat_logging.log_server_message(msg)
  96. end
  97. local function announce_end(pname, reason)
  98. local reasonstr = "no reason"
  99. if reason == "jail" then
  100. reasonstr = "sent to jail"
  101. elseif reason == "death" then
  102. reasonstr = "perished ... unfortunately"
  103. elseif reason == "bounds" then
  104. reasonstr = "out of bounds"
  105. elseif reason == "far" then
  106. reasonstr = "too far away"
  107. elseif reason == "left" then
  108. reasonstr = "disjunction"
  109. end
  110. local sex = skins.get_gender_strings(pname)
  111. local msg = "# Server: <" .. rename.gpn(pname) .. "> has ended " .. sex.his .. " participation in a duel: " .. reasonstr .. "."
  112. minetest.chat_send_all(SHOUT_COLOR .. msg)
  113. chat_logging.log_server_message(msg)
  114. end
  115. -- Lets outside code query if this player is currently respawning (implies they're in a duel).
  116. function armor.is_duelist_respawning(pname)
  117. local data = dueling_players[pname]
  118. if not data then
  119. return
  120. end
  121. -- If the timer exists, they're respawning.
  122. if data.respawn_countdown then
  123. return true
  124. end
  125. end
  126. local function set_visible(player, visible)
  127. if visible then
  128. pova.remove_modifier(player, "nametag", "duelist:respawn_invis")
  129. pova.remove_modifier(player, "properties", "duelist:respawn_invis")
  130. else
  131. gauges.remove_hp_bar_for_player(player:get_player_name())
  132. -- Make them invisible.
  133. pova.set_modifier(player, "nametag",
  134. {color={a=0, r=0, g=0, b=0}, text=""}, "duelist:respawn_invis",
  135. {priority=1000})
  136. pova.set_modifier(player, "properties", {
  137. visual_size = {x=0, y=0},
  138. makes_footstep_sound = false,
  139. -- Cannot be zero-size because otherwise player would fall through cracks.
  140. --collisionbox = {0},
  141. --selectionbox = {0},
  142. collide_with_objects = false,
  143. is_visible = false,
  144. pointable = false,
  145. show_on_minimap = false,
  146. }, "duelist:respawn_invis", {priority=1000})
  147. end
  148. end
  149. function armor.dueling_hud_update(player, duel_data)
  150. player:hud_change(duel_data.hud[2], "text",
  151. "Participants: " .. #(armor.get_likely_opponents(player, duel_data.start_pos)))
  152. if duel_data.respawn_countdown then
  153. local text = "Respawn in: " .. duel_data.respawn_countdown
  154. player:hud_change(duel_data.hud[6], "text", text)
  155. elseif duel_data.out_of_bounds and duel_data.out_of_bounds > 0 then
  156. local text = "Return to the combat zone! (" .. (30 - duel_data.out_of_bounds) .. ")"
  157. player:hud_change(duel_data.hud[6], "text", text)
  158. else
  159. -- Hide this element.
  160. player:hud_change(duel_data.hud[6], "text", "")
  161. end
  162. end
  163. -- Check whether player is in bounds to duel, and end duel if necessary.
  164. function armor.check_bounds(pname)
  165. if dueling_players[pname] then
  166. local pref = minetest.get_player_by_name(pname)
  167. -- Player left the game unexpectedly.
  168. if not pref then
  169. dueling_players[pname] = nil
  170. announce_end(pname, "left")
  171. return
  172. end
  173. local player_pos = vector_round(pref:get_pos())
  174. local data = dueling_players[pname]
  175. local in_arena = (city_block:in_pvp_arena(player_pos) and
  176. minetest.test_protection(player_pos, ""))
  177. -- Respawn countdown timer.
  178. if data.respawn_countdown then
  179. if data.respawn_countdown > 0 then
  180. --pref:set_pos(data.respawn_pos)
  181. data.respawn_countdown = data.respawn_countdown - 1
  182. else
  183. data.respawn_countdown = nil
  184. set_visible(pref, true)
  185. minetest.chat_send_player(pname, "# Server: You respawned.")
  186. end
  187. end
  188. -- Disable respawn protection once player has moved out of respawn area.
  189. if data.no_respawn_protection == nil then
  190. if not armor.in_pvp_respawn_area(player_pos, data.start_pos) then
  191. debug_print('disabling respawn protection: ' .. pname .. ': player moved out of spawn')
  192. data.no_respawn_protection = true
  193. end
  194. end
  195. -- Arena distance checks.
  196. if vector_distance(data.start_pos, player_pos) > DUEL_MAX_RADIUS or not in_arena then
  197. if vector_distance(data.start_pos, player_pos) < (DUEL_MAX_RADIUS + 100) then
  198. -- Player is slightly out of bounds. Warn them to return.
  199. if data.out_of_bounds >= 30 then
  200. -- Player has been out of bounds for 30 seconds.
  201. armor.end_duel(pref, "bounds")
  202. return
  203. end
  204. data.out_of_bounds = data.out_of_bounds + 1
  205. else
  206. -- Player has completely left the duel area (teleport?) End duel immediately.
  207. armor.end_duel(pref, "far")
  208. return
  209. end
  210. elseif vector_distance(data.start_pos, player_pos) <= DUEL_MAX_RADIUS and in_arena then
  211. data.out_of_bounds = 0
  212. end
  213. -- HUD update.
  214. armor.dueling_hud_update(pref, data)
  215. -- Check again.
  216. minetest.after(1, function() armor.check_bounds(pname) end)
  217. end
  218. end
  219. -- Call this when a player begins to duel.
  220. function armor.add_dueling_player(player, duel_pos)
  221. local pname = player:get_player_name()
  222. if dueling_players[pname] then
  223. return
  224. end
  225. local yoff = 18
  226. local hud1 = player:hud_add({
  227. type = "text",
  228. position = {x=1.00, y=0.30},
  229. alignment = {x=-1, y=1},
  230. text = "PvP: Dueling!",
  231. number = 0xFFFFFF,
  232. size = {x=1, y=1},
  233. offset = {x=-16, y=yoff*1},
  234. })
  235. local hud2 = player:hud_add({
  236. type = "text",
  237. position = {x=1.00, y=0.30},
  238. alignment = {x=-1, y=1},
  239. text = "Participants: " .. #(armor.get_likely_opponents(player, duel_pos)),
  240. number = 0xFFFFFF,
  241. size = {x=1, y=1},
  242. offset = {x=-16, y=yoff*2},
  243. })
  244. local hud3 = player:hud_add({
  245. type = "text",
  246. position = {x=1.00, y=0.30},
  247. alignment = {x=-1, y=1},
  248. text = "Spawnpoints: " .. #(armor.get_public_spawns(duel_pos)),
  249. number = 0xFFFFFF,
  250. size = {x=1, y=1},
  251. offset = {x=-16, y=yoff*3},
  252. })
  253. local cb = city_block.get_block(duel_pos)
  254. local arena_name = ""
  255. if cb.area_name and cb.area_name ~= "" then
  256. arena_name = cb.area_name
  257. end
  258. local hud4 = player:hud_add({
  259. type = "text",
  260. position = {x=1.00, y=0.30},
  261. alignment = {x=-1, y=1},
  262. text = "Arena: " .. arena_name,
  263. number = 0xFFFFFF,
  264. size = {x=1, y=1},
  265. offset = {x=-16, y=yoff*0},
  266. })
  267. local hud5 = player:hud_add({
  268. type = "waypoint",
  269. name = "Arena Marker",
  270. number = 0xFFFFFF,
  271. world_pos = duel_pos,
  272. })
  273. local beds = {}
  274. local spawns = armor.get_public_spawns(duel_pos)
  275. for k = 1, #spawns do
  276. local id = player:hud_add({
  277. type = "waypoint",
  278. name = "Respawn Point",
  279. number = 0xFFFFFF,
  280. world_pos = spawns[k],
  281. precision = 0,
  282. })
  283. beds[#beds + 1] = id
  284. end
  285. -- The "Respawn" HUD counter. Shown only when dead and busy respawning.
  286. local hud6 = player:hud_add({
  287. type = "text",
  288. position = {x=0.50, y=0.40},
  289. alignment = {x=0, y=0},
  290. text = "",
  291. number = 0xFFFFFF,
  292. size = {x=3, y=1},
  293. offset = {x=0, y=0},
  294. })
  295. dueling_players[pname] = {
  296. start_time = os.time(),
  297. start_pos = duel_pos,
  298. out_of_bounds = 0,
  299. hud = {hud1, hud2, hud3, hud4, hud5, hud6, beds},
  300. }
  301. announce_begin(pname)
  302. chat_core.alert_player_sound(pname)
  303. minetest.after(1, function() armor.check_bounds(pname) end)
  304. return true
  305. end
  306. -- End current duel if one in progress.
  307. function armor.end_duel(player, reason)
  308. local pname = player:get_player_name()
  309. if dueling_players[pname] then
  310. local data = dueling_players[pname]
  311. if data.hud then
  312. for k = 1, #data.hud do
  313. if type(data.hud[k]) == "table" then
  314. for j = 1, #data.hud[k] do
  315. player:hud_remove(data.hud[k][j])
  316. end
  317. else
  318. player:hud_remove(data.hud[k])
  319. end
  320. end
  321. end
  322. data.hud = nil
  323. dueling_players[pname] = nil
  324. set_visible(player, true)
  325. announce_end(pname, reason)
  326. chat_core.alert_player_sound(pname)
  327. end
  328. end
  329. -- Get nearby players, not admins, not self.
  330. function armor.get_likely_opponents(player, pos)
  331. local targets = {}
  332. local pname = player:get_player_name()
  333. local players = minetest.get_connected_players()
  334. for k = 1, #players do
  335. local pref = players[k]
  336. if not gdac.player_is_admin(pref) then
  337. if pname ~= pref:get_player_name() then
  338. if vector_distance(pos, pref:get_pos()) < OPPONENT_DISTANCE then
  339. -- The player needs to have signaled their intent to duel.
  340. if dueling_players[pref:get_player_name()] then
  341. targets[#targets + 1] = pref
  342. end
  343. end
  344. end
  345. end
  346. end
  347. -- Sort opponents, nearest players first.
  348. table.sort(targets,
  349. function(a, b)
  350. local d1 = vector_distance(a:get_pos(), pos)
  351. local d2 = vector_distance(b:get_pos(), pos)
  352. return d1 < d2
  353. end)
  354. return targets
  355. end
  356. -- Get nearby public spawns IN a PvP zone.
  357. function armor.get_public_spawns(pos)
  358. local targets = {}
  359. local spawns = beds.nearest_public_spawns(pos, 5, PUBLIC_BED_DISTANCE)
  360. for k = 1, #spawns do
  361. -- Only include public spawns which are in a PvP arena.
  362. if city_block:in_pvp_arena(spawns[k]) then
  363. targets[#targets + 1] = spawns[k]
  364. end
  365. end
  366. -- Already sorted by distance.
  367. return targets
  368. end
  369. local function print_message(victim, punch_info)
  370. local pname = victim:get_player_name()
  371. local kname = punch_info.hitter
  372. local spamkey = "duel:" .. pname .. ":" .. kname
  373. if pname == punch_info.victim and kname == punch_info.hitter then
  374. if not spam.test_key(spamkey) then
  375. local msg
  376. if pname == kname then
  377. msg = DUEL_SUICIDE_STRINGS[math_random(1, #DUEL_SUICIDE_STRINGS)]
  378. elseif punch_info.stomp then
  379. msg = DUEL_STOMP_STRINGS[math_random(1, #DUEL_STOMP_STRINGS)]
  380. elseif punch_info.arrow then
  381. msg = DUEL_ARROW_STRINGS[math_random(1, #DUEL_ARROW_STRINGS)]
  382. else
  383. msg = DUEL_MELEE_STRINGS[math_random(1, #DUEL_MELEE_STRINGS)]
  384. end
  385. -- I can hear the snowflakes screaming "sexist" rn LOL.
  386. -- This is like holy water on a vampire!
  387. local psex = skins.get_gender_strings(pname)
  388. local ksex = skins.get_gender_strings(kname)
  389. msg = msg:gsub("<loser>", "<" .. rename.gpn(pname) .. ">")
  390. msg = msg:gsub("<winner>", "<" .. rename.gpn(kname) .. ">")
  391. msg = string.gsub(msg, "<w_himself>", ksex.himself)
  392. msg = string.gsub(msg, "<w_his>", ksex.his)
  393. msg = string.gsub(msg, "<w_him>", ksex.him)
  394. msg = string.gsub(msg, "<w_he>", ksex.he)
  395. msg = string.gsub(msg, "<l_himself>", psex.himself)
  396. msg = string.gsub(msg, "<l_his>", psex.his)
  397. msg = string.gsub(msg, "<l_him>", psex.him)
  398. msg = string.gsub(msg, "<l_he>", psex.he)
  399. -- Weapon name, or default description.
  400. local function weapon_string(msg, key, name)
  401. if string.find(msg, key) then
  402. local pref = minetest.get_player_by_name(name)
  403. if pref then
  404. local wield = pref:get_wielded_item()
  405. local def = minetest.registered_items[wield:get_name()]
  406. local meta = wield:get_meta()
  407. local description = meta:get_string("description")
  408. if description ~= "" then
  409. msg = string.gsub(msg, key, "'" .. utility.get_short_desc(description):trim() .. "'")
  410. elseif def and def.description then
  411. local str = utility.get_short_desc(def.description)
  412. if str == "" then
  413. str = "Potato Fist"
  414. end
  415. msg = string.gsub(msg, key, str)
  416. end
  417. else
  418. msg = string.gsub(msg, key, "Vanishing Act")
  419. end
  420. end
  421. return msg
  422. end
  423. msg = weapon_string(msg, "<w_weapon>", kname)
  424. msg = weapon_string(msg, "<l_weapon>", pname)
  425. minetest.chat_send_all("# Server: " .. msg)
  426. spam.mark_key(spamkey, 10)
  427. end
  428. end
  429. end
  430. local function heal_player(pname)
  431. local pref = minetest.get_player_by_name(pname)
  432. if not pref then
  433. return
  434. end
  435. local hp_max = pova.get_active_modifier(pref, "properties").hp_max
  436. pref:set_hp(hp_max, {reason="heal_command"})
  437. sprint.set_stamina(pref, SPRINT_STAMINA)
  438. bones.nohack.on_respawnplayer(pref)
  439. end
  440. local function lock_player_at_spawn(player, respawn_pos)
  441. local pname = player:get_player_name()
  442. -- Do this soon.
  443. -- This is a hacky way to prevent the player from being allowed to move.
  444. local function donext()
  445. local player = minetest.get_player_by_name(pname)
  446. if not player then
  447. return
  448. end
  449. local obj = minetest.add_entity(respawn_pos, "3d_armor:pvpduel_respawn")
  450. if not obj then
  451. return
  452. end
  453. local ent = obj:get_luaentity()
  454. if ent then
  455. ent.player_name = pname
  456. player:set_attach(obj)
  457. end
  458. end
  459. -- If we don't delay a little, other observing clients won't pick up
  460. -- that the player moved, and they'll just appear to be standing in the middle
  461. -- of the field until the respawn countdown finishes, at which point other
  462. -- clients will see them zip to the bed location.
  463. minetest.after(0.5, donext)
  464. end
  465. local function respawn_victim(player, respawn_pos)
  466. -- Move the player also.
  467. player:set_pos(respawn_pos)
  468. local pname = player:get_player_name()
  469. -- Re-engage respawn protection.
  470. -- This will disable if they hit anybody.
  471. -- It will also turn off if they leave spawn.
  472. local duel_info = dueling_players[pname]
  473. duel_info.no_respawn_protection = nil
  474. duel_info.respawn_pos = respawn_pos
  475. duel_info.respawn_countdown = RESPAWN_TIME
  476. -- This is to 1) hide a glitch where sometimes other client's don't notice
  477. -- that the player was teleported back to a respawn (this might have something
  478. -- to do with them not knowing about the player getting attached to the
  479. -- respawn entity), and 2) to prevent other players from knowing their respawn
  480. -- location right away.
  481. set_visible(player, false)
  482. -- Note: player is allowed to interact with themselves and nearby objects
  483. -- during respawn countdown. Keep this as a feature, NOT a bug! It allows them
  484. -- to do something while they wait a few seconds, such as switch their
  485. -- weapons/armor to prepare for the next round.
  486. preload_tp.execute({
  487. player_name = pname,
  488. target_position = respawn_pos,
  489. emerge_radius = 32,
  490. post_teleport_callback = function()
  491. ambiance.sound_play("respawn", respawn_pos, 0.5, 10)
  492. minetest.after(1, heal_player, pname)
  493. local pref = minetest.get_player_by_name(pname)
  494. if pref then
  495. lock_player_at_spawn(pref, respawn_pos)
  496. end
  497. end,
  498. force_teleport = true,
  499. send_blocks = false,
  500. particle_effects = false,
  501. })
  502. end
  503. -- Query whether a position is within the safe zone of any respawn point in a
  504. -- radius from a dueling arena position.
  505. function armor.in_pvp_respawn_area(pos, arena_pos)
  506. local spawns = armor.get_public_spawns(arena_pos)
  507. for k = 1, #spawns do
  508. if vector_distance(pos, spawns[k]) < SPAWN_SAFE_ZONE then
  509. return true
  510. end
  511. end
  512. end
  513. local function spawn_bones(pos, pname, hname)
  514. pos = vector_round(pos)
  515. local pref = minetest.get_player_by_name(pname)
  516. if not pref then
  517. return
  518. end
  519. local data = dueling_players[pname]
  520. if not data then
  521. return
  522. end
  523. -- Prevent placing bones near any of the public spawns.
  524. if armor.in_pvp_respawn_area(pos, data.start_pos) then
  525. return
  526. end
  527. pos = armor.find_ground_by_raycast(pos, pref)
  528. if minetest.get_node(pos).name == "air" then
  529. minetest.set_node(pos, {name="bones:bones_type2", param2=math_random(0, 3)})
  530. local meta = minetest.get_meta(pos)
  531. meta:set_string("infotext",
  532. "Duel: <" .. rename.gpn(pname) .. ">'s bones.\n" ..
  533. "Slain by <" .. rename.gpn(hname) .. ">.")
  534. meta:set_int("protection_cancel", 1)
  535. meta:mark_as_private("protection_cancel")
  536. end
  537. end
  538. local function spawn_bones_after(pos, pname, hname)
  539. minetest.after(0, spawn_bones, pos, pname, hname)
  540. end
  541. -- Cityblock punch handler uses this to check if a player should receive any
  542. -- damage at all. This is somewhat like jails, where brawling is not allowed.
  543. -- Return 'true' to disable damage from this punch.
  544. function armor.have_dueling_respawn_protection(player, hitter)
  545. local pname = player:get_player_name()
  546. local hname = hitter:get_player_name()
  547. local player_pos = vector_round(player:get_pos())
  548. if dueling_players[pname] and dueling_players[hname] then
  549. local duel_info = dueling_players[pname]
  550. -- If the hitter's respawn countdown is in progress, they cannot damage anyone!
  551. -- (Technically, during this period they shouldn't be able to interact.)
  552. if dueling_players[hname].respawn_countdown then
  553. return true
  554. end
  555. -- Hitter is punching, disable their respawn protection.
  556. dueling_players[hname].no_respawn_protection = true
  557. debug_print('respawn protection canceled for: ' .. hname)
  558. -- Shortcut if respawn protection is already disabled for this player.
  559. if duel_info.no_respawn_protection then
  560. debug_print('no respawn protection: ' .. pname)
  561. return
  562. end
  563. -- If respawn protection isn't explicitly turned off, and they're in a spawn
  564. -- area, then prevent damage from this punch.
  565. if armor.in_pvp_respawn_area(player_pos, duel_info.start_pos) then
  566. local key = "duel:spawnprotection:" .. pname
  567. if not spam.test_key(key) then
  568. minetest.chat_send_player(pname, "# Server: You were hit, but it reflected off your respawn protection.")
  569. spam.mark_key(key, 2)
  570. end
  571. return true
  572. end
  573. end
  574. end
  575. -- Called from the armor HP-change code only if player would die.
  576. -- Don't put logic here that needs to run for every punch, use the punch handler
  577. -- for that.
  578. function armor.handle_pvp_arena_death(hp_change, player)
  579. local pname = player:get_player_name()
  580. local player_pos = vector_round(player:get_pos())
  581. debug_print('pos: ' .. minetest.pos_to_string(player_pos) .. ': ' .. pname)
  582. -- Player must have signaled their intent to duel.
  583. if dueling_players[pname] then
  584. debug_print('dead player is dueling: ' .. pname)
  585. local duel_info = dueling_players[pname]
  586. local recently_in_arena = (duel_info.out_of_bounds > 0)
  587. -- PvP arena must be marked and protected.
  588. if recently_in_arena or city_block:in_pvp_arena(player_pos) then
  589. debug_print('in pvp arena: ' .. pname)
  590. if recently_in_arena or minetest.test_protection(player_pos, "") then
  591. debug_print('is_protected: ' .. pname)
  592. local opponents = armor.get_likely_opponents(player, duel_info.start_pos)
  593. local spawns = armor.get_public_spawns(duel_info.start_pos)
  594. debug_print('opponents: ' .. #opponents .. ': ' .. pname)
  595. debug_print('spawns: ' .. #spawns .. ': ' .. pname)
  596. -- Get notified punch info (from cityblock punch handler callback).
  597. -- Set global punch info to nil so we don't mistakenly use stale data later.
  598. local punch_info = ACTIVE_DUEL_PUNCH
  599. ACTIVE_DUEL_PUNCH = nil
  600. -- If punch info is nil at this point, the damage was caused by
  601. -- something other than a player. Could be env, could be mob, could even
  602. -- be a stray :set_hp() somewhere in the code. Use the last known punch
  603. -- table, if it is not too old.
  604. --
  605. -- This is also a feature: if a player manages to cause env damage to
  606. -- another player as a result of a normal punch, this will attribute the
  607. -- env damage to the hitter if the time separation is short enough.
  608. --
  609. -- This will work no matter how many times non-player-punch damage is
  610. -- applied, as long as the damage occurs shortly after the initial punch.
  611. if not punch_info then
  612. if os.time() <= (duel_info.last_punch_time + ENV_TIME_AFTER_PUNCH) then
  613. punch_info = duel_info.last_punch_table
  614. end
  615. end
  616. debug_print('punch info: ' .. dump(punch_info) .. ': ' .. pname)
  617. -- There must be nearby opponents and nearby spawns.
  618. -- No opponents == no duel, no spawns == not valid arena.
  619. if #opponents > 0 and #spawns > 0 and punch_info then
  620. -- The hitter must also be in the duel.
  621. -- PvP arenas are not meant to be used as defense against bastards!
  622. -- Duels are between friends, or frenemies.
  623. local hitter_is_dueling = false
  624. if punch_info.hitter and dueling_players[punch_info.hitter] then
  625. hitter_is_dueling = true
  626. end
  627. if hitter_is_dueling then
  628. -- If player has only 1 HP, they were already "dead" as far as we're concerned.
  629. -- However, it may transpire that a player gets to 1 hp naturally.
  630. -- So the only way to know if we should respawn the player is this:
  631. -- do they have a respawn countdown currently in progress? If yes,
  632. -- then they were already "killed" and we should skip this.
  633. if not duel_info.respawn_countdown then
  634. debug_print('handling duel death: ' .. pname)
  635. -- Get them off of whatever.
  636. default.detach_player_if_attached(player)
  637. -- Death sound needs to play before we respawn the player.
  638. coresounds.play_death_sound(player, pname)
  639. -- We MUST wait until next server step to spawn bones, because
  640. -- bones cancel protection, which would confuse the arena code and
  641. -- cause the player to die a real death! This can happen if two or
  642. -- more players die at the exact same time in the same spot
  643. -- (e.g., murder-suicide with a TNT arrow).
  644. spawn_bones_after(player_pos, pname, punch_info.hitter)
  645. -- Send taunt.
  646. print_message(player, punch_info)
  647. -- Send victim to a respawn point.
  648. respawn_victim(player, spawns[math_random(1, #spawns)])
  649. end
  650. debug_print('preventing real death: ' .. pname)
  651. -- Prevent real death, and all its consequences.
  652. -- Player will be fully healed after they teleport to a public spawn.
  653. -- Note: if player HP is 1, this should return 0 (no hp change allowed).
  654. if player:get_hp() <= 1 then
  655. return 0
  656. end
  657. return -(player:get_hp() - 1)
  658. end
  659. end
  660. else
  661. debug_print('NOT PROTECTED: ' .. pname)
  662. end
  663. end
  664. -- Player is dueling, but arena checks didn't pass.
  665. end
  666. -- Otherwise, do not interfere with normal damage to player.
  667. return hp_change
  668. end
  669. -- Used to query if this location is a valid combat arena.
  670. function armor.is_valid_arena(pos)
  671. pos = vector_round(pos)
  672. if city_block:in_pvp_arena(pos) then
  673. if minetest.test_protection(pos, "") then
  674. if #(armor.get_public_spawns(pos)) >= 2 then
  675. return true
  676. end
  677. end
  678. end
  679. end
  680. -- Called by the cityblock punch handler.
  681. function armor.notify_duel_punch(victim_name, hitter_name, stomp_flag, ranged_flag)
  682. ACTIVE_DUEL_PUNCH = {
  683. victim = victim_name,
  684. hitter = hitter_name,
  685. stomp = stomp_flag,
  686. arrow = ranged_flag,
  687. }
  688. -- Also set this table on the victim's dueling info.
  689. local data = dueling_players[victim_name]
  690. if data then
  691. data.last_punch_table = ACTIVE_DUEL_PUNCH
  692. data.last_punch_time = os.time()
  693. end
  694. end
  695. function armor.clear_duel_punch()
  696. ACTIVE_DUEL_PUNCH = nil
  697. end
  698. if not armor.duel_registered then
  699. armor.duel_registered = true
  700. local entity_def = {
  701. visual = "wielditem",
  702. visual_size = {x=0, y=0},
  703. collisionbox = {0, 0, 0, 0, 0, 0},
  704. physical = false,
  705. textures = {"air"},
  706. is_visible = false,
  707. static_save = false,
  708. --[[
  709. on_activate = function(self, staticdata, dtime_s)
  710. if self._play_immediate then
  711. self._ptime = 0
  712. self._ctime = 0
  713. end
  714. end,
  715. on_punch = function(self, puncher, time_from_last_punch, tool_caps, dir)
  716. end,
  717. on_death = function(self, killer)
  718. end,
  719. on_rightclick = function(self, clicker)
  720. end,
  721. get_staticdata = function(self)
  722. return ""
  723. end,
  724. --]]
  725. on_punch = function(self, puncher, time_from_last_punch, tool_caps, dir)
  726. end,
  727. on_blast = function()
  728. return false, false, {}
  729. end,
  730. detach_player = function(self)
  731. if self.player_name then
  732. local pref = minetest.get_player_by_name(self.player_name)
  733. if pref then
  734. self.player_name = nil
  735. pref:set_detach()
  736. self.object:remove()
  737. end
  738. end
  739. end,
  740. on_step = function(self, dtime)
  741. if self.player_name then
  742. local data = dueling_players[self.player_name]
  743. if not data then
  744. self.object:remove()
  745. return
  746. end
  747. -- Will be nil when the countdown has ended.
  748. if not data.respawn_countdown then
  749. local pref = minetest.get_player_by_name(self.player_name)
  750. if pref then
  751. self.player_name = nil
  752. pref:set_detach()
  753. self.object:remove()
  754. end
  755. else
  756. -- Keep attaching.
  757. local pref = minetest.get_player_by_name(self.player_name)
  758. if pref then
  759. pref:set_attach(self.object)
  760. end
  761. end
  762. end
  763. end,
  764. }
  765. minetest.register_entity("3d_armor:pvpduel_respawn", entity_def)
  766. end