Skip to content

# [Feature] Open PvP system — retail 2014 rules (PvP situations, expert modes, skull system, per-viewer fields/walls, frag share) #810

Description

@Jattartajjar

[Feature] Open PvP system — retail 2014 rules (PvP situations, expert modes, skull system, per-viewer fields/walls, frag share)

Summary

This implements the complete Open PvP ruleset as used on retail Tibia since the Summer 2014 PvP revamp, following the specification published at https://tibiaduality.com/pvp-openpvp. The core concept is the PvP situation — a mutual, time-limited combat state between two players that everything else keys off: field damage, magic wall blocking, situation boxes and the expert PvP modes.

Everything below is implemented, built and verified in-game on protocol 15.25 (world type pvp).


1. General characteristics

Rule Status Implementation
Players can walk through each other Player::canWalkthrough/Ex — unconditional pass-through for players on open PvP (the PZ-only "double step" behaviour remains for other world types)
Floor fields only damage the caster unless in a PvP situation Gate at the top of MagicField::onStepInField — a player-made field applies no damage condition to bystanders
Players cannot pass their own Magic Walls but can pass others' Tile::queryAdd + Combat::isOwnedFieldBystander — a wall blocks its owner and everyone in a PvP situation with him; the wall stays up for those it blocks
Magic Wall / Wild Growth cast outside a PvP situation = PvE wall Flagged at cast time (pveWall custom attribute) — blocks only monsters; every player including the caster walks through
The Wild Growth rune can be cut only by its caster Owner check in the machete action (onUseMachete); ownerless map/quest growths remain cuttable by everyone
Non-first-in-stack players cannot use area spells/runes or initiate PvP Player::isFirstInStack — gates aggressive area casts in Spell::playerSpellCheck (covers runes via playerRuneSpellCheck) and PvP initiation in Combat::canDoCombat; defending inside an existing situation stays allowed
Kill participation tracked as percentages ("unfair fight") With 5 or fewer attackers everyone receives 100% of the frag; with 6+ each participant receives 5 / N of it (see §4)
Three frag bars (24 h / 7 days / 30 days) Already present upstream (sendUnjustifiedPoints, 0xB7) — now fed with weighted sums

The PvP situation core

  • Player::pvpSituationsguid → expiry map; every aggressive act between two players creates/refreshes a mutual entry (60 s, pzLocked duration).
  • isInPvpSituationWith() / hasActivePvpSituation() drive fields, walls, boxes and expert-mode checks.
  • Runtime-only state; expires passively, no persistence needed.

Per-viewer field & wall rendering

The client blocks movement onto an "unpassable" item locally and never even sends the walk packet — so server-side pass-through alone is not enough. Retail solves this by showing a different item to players the wall does not concern, and so do we:

  • ProtocolGame::AddItem swaps the wire id per viewer: bystanders receive the walkable/harmless variants (ITEM_MAGICWALL_SAFE 10181, ITEM_WILDGROWTH_SAFE 10182, ITEM_FIREFIELD_NOPVP 2131, ITEM_POISONFIELD_NOPVP 2134, ITEM_ENERGYFIELD_NOPVP 2135), while the owner and players in a PvP situation with him see the dangerous/solid originals.
  • The server still arbitrates every step in Tile::queryAdd, so a spoofed client gains nothing.
  • Bonus fix: the MW/WG rune scripts created their items via Game.createItem() without an owner (unlike combat-created fields) — item:setOwner(creature) added; ownership also powers the wild-growth cutting rule.

2. Expert PvP modes (dove / white hand / yellow hand / red fist)

Protocol (reverse-engineered on client 15.25)

  • The "E" button was permanently greyed out because the login packet hardcoded two flags to 0x00: can change pvp framing option and expert mode button enabled. Both are now sent as 0x01 on open PvP worlds.
  • The client appends the selected mode to the fight-modes packet — 0xA0 tail = [pvpMode:u8][junk:u8] (0 dove, 1 white hand, 2 yellow hand, 3 red fist; the second byte is uninitialized garbage and is ignored). parseFightModes now reads it, sendFightModes (0xA7) echoes the real mode back.

Enforcement (Combat::checkExpertPvpMode)

Mode May attack
🕊️ Dove Self-defence only — players who attacked you or are in a PvP situation with you. Area spells never splash innocents.
🤚 White hand Dove + players who attacked a member of your party/guild (hasAttackedAllyOf)
Yellow hand White hand + any skulled player (getSkullClient != NONE)
Red fist Anyone except party/guild members
  • Enforced both at target selection (canTargetCreature, replacing the legacy secure-mode check on open PvP) and per combat target (canDoCombat) — dove/white players cannot damage innocents even with area splash.
  • Bypasses: staff characters, guild wars, PvP zones and event teams.

3. Skull system

Skull Rule (per spec) Status
White Attacking an unskulled player; 15 min after a kill, ~1 min after mere aggression; auto PZ-lock ✅ upstream, verified
🟡 Yellow Obtained when attacking a skulled player; visible only to the attacked player; duration like white fixed — upstream only handled retaliation against unskulled aggressors; attacking a player with a real skull never registered, so the victim never saw the yellow skull. Now: justified (no white skull for the attacker), addAttacked + skull refresh, PZ-lock applied
🔴 Red 3 frags/24 h, 5/7 days or 10/30 days → 30 days duration ✅ thresholds upstream; duration fixed 1 → 30 days; full drop + blessing loss upstream
Black 2× red thresholds (6/10/20) → 45 days; cannot attack unskulled; takes 100 % damage; respawns with 40 hp / 0 mana ✅ upstream; duration fixed 3 → 45 days
🟠 Orange Marks unjustified killers to their victims for 7 days or until vengeance fixed — the check only ran when the killer had no real skull, but an unjustified killer carries a white skull for 15 min, so the victim never saw orange. Now orange overrides white in the victim's view (red/black keep priority). Vengeance clearing (kill.unavenged) upstream
🕐 Day-window bug "Day" frag counter used a 4-hour window ✅ fixed → 24 h
🕐 Relog bug Kills were only loaded from DB within 24 h (FRAG_TIME), silently wiping the 30-day bar and the 7-day orange skull on relog ✅ fixed → 30-day load window

4. Frag share ("unfair fight")

  • Kill.weight (1.0 = full frag). On an unjustified kill the victim's damageMap is scanned for distinct player participants (through summon masters, within the in-fight window, victim excluded); with more than 5 participants each killer's entry is stored with weight 5 / N.
  • Red/black-skull thresholds and the three frag bars compare weighted sums, so the bars fill by percentages exactly like retail.
  • Persistence: new player_kills.weight column (smallint, percent, default 100) — migration 65 + schema.sql; player:getKills()/setKills() expose the weight as an optional 4th tuple field.

5. PvP situation boxes (yellow / orange / brown)

  • The creature description contains a persistent mark byte that upstream hardcoded to 0xFF (MARK_UNMARKED). It is now computed per viewer by Player::getPvpSituationMarkFor():
    • 🟨 yellow — you and everyone in a PvP situation with you,
    • 🟧 orange — a player fighting one of your party/guild mates,
    • 🟫 brown — participants of fights that don't involve you.
  • Refresh: sendUpdateCreature (0x6B) is emitted only on state changes (fight starts, adversary joins, situations expire — the last one polled at 1 Hz). Late-arriving spectators get the correct colour automatically with the regular creature description. No periodic resend, no flicker.
  • ⚠️ Do not use the 0x93 border mark for this — that channel is the client's short attack flash animation and blinks.

Config

worldType = "pvp"
dayKillsToRedSkull = 3
weekKillsToRedSkull = 5
monthKillsToRedSkull = 10
redSkullDuration = 30     -- was 1
blackSkullDuration = 45   -- was 3
orangeSkullDuration = 7
whiteSkullTime = 15 * 60 * 1000
walkThroughPlayers = true

Database

  • Migration data/migrations/65.luaALTER TABLE player_kills ADD COLUMN weight smallint(5) UNSIGNED NOT NULL DEFAULT 100 (+ schema.sql for fresh installs). Runs automatically at boot.

Dev tooling included

  • /pvpmark <markType>[,<weaponType>] (god talkaction) — sends raw 0x93 creature marks for client-side protocol experiments.
  • Lua: player:sendCreatureSquare(creature, markType[, weaponType]), player:hasActivePvpSituation().

In-game test checklist (all verified ✔)

  • Walkthrough: players pass through each other freely outside PZ
  • Dove blocks attacking innocents; red fist allows and yields a white skull + PZ-lock
  • Yellow hand may attack skulled players only
  • Field cast without a situation deals no damage to bystanders and shows the harmless sprite; the same field damages your adversary and shows the dangerous sprite
  • Own magic wall blocks you; a stranger's wall is transparent and walkable; after aggression it turns solid for you; PvE walls (cast out of combat) block only monsters
  • Wild growth cut only by its caster
  • Bottom-of-stack player cannot cast exori/GFB or initiate PvP; the top player can
  • White skull: ~1 min after aggression, 15 min after a kill; frag bars rise (33 % / 20 % / 10 % after one kill)
  • Yellow skull visible to the attacked skulled player only
  • Orange skull visible to the victim immediately after an unjustified kill; cleared by vengeance
  • Situation boxes render persistently (no flicker) and clear ~60 s after the fight

Known limitations

  • A field/wall already on the ground does not visually "re-arm" the moment a situation starts mid-lifetime — the sprite updates on the next tile refresh (fields live ≤ 20 s; server-side rules always apply regardless).
  • PvP situations are runtime-only and end on logout (retail-like).
  • Box palette indices (210/198/114) are configurable constants in player.cpp should the hues need tuning.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions