Funpix Kit
Inicio / Componentes / fpx · social

fpx · social

Grupos (tope 8, liderazgo heredable) y trade P2P con escrow determinista anti-scam (candados + doble confirmación).

capa 0 v0.1.0 sin dependencias

El ZIP incluye: addons/fpx/social + sus tests.

Qué es

PartyState: membresía de grupo pura (tope 8, orden de entrada estable como desempate, el líder hereda al más antiguo al irse, transferencia de liderazgo, motivos tipados). TradeSession: escrow P2P determinista anti-scam — cambiar tu oferta exige tu candado abierto y revienta candado+confirmación del otro (anti bait-and-switch); confirmar exige ambos candados; doble confirmación sella (committed); TTL por tick() inyectado (sin relojes). La materialización del canje es del consumidor (InventoryOps.trade_apply en inventory).

Instalación

  1. Descarga fpx-social-v0.1.0.zip (botón de arriba) y extráelo en la raíz de tu proyecto Godot — deja la estructura tal cual:
text
tu-juego/
├── project.godot
├── addons/
│   └── fpx/
│       └── social/
└── tests/          ← opcional: bórralo si no quieres los tests
  1. Abre el proyecto (o corre --import headless). Sin plugins ni autoloads que activar: las clases quedan disponibles en cualquier script.
  2. Opcional — verifica con la suite (necesita GdUnit4, ver la guía): -a tests/social.

Ejemplo de uso

Usa las fixtures incluidas en el componente para probar en segundos; en tu juego, sustitúyelas por tus datos.

social · ejemplo
# Grupo: tope 8, orden de entrada estable, motivos tipados (&"" = OK).
var party := PartyState.new(1)
party.add(2)
party.add(3)
var reason := party.add(2)      # &"already_member" — nunca duplica
party.remove(1)                 # se va el líder → hereda el más antiguo
assert(party.leader == 2)
party.transfer_leadership(3)

# Trade P2P con escrow determinista (anti bait-and-switch): cambiar tu oferta
# exige tu candado abierto y REVIENTA el candado y la confirmación del otro.
var trade := TradeSession.new(11, 22)   # Ana ↔ Bob
trade.set_gold(11, 50)
trade.set_item(22, &"pocion", 2)
trade.lock(11)
trade.lock(22)
trade.confirm(11)
trade.confirm(22)               # doble confirmación → sella (committed)

if trade.state == TradeSession.COMMITTED:
	# La materialización es del CONSUMIDOR (inventory · InventoryOps.trade_apply):
	var offer_a := {"gold": 50, "items": []}
	var offer_b := {"gold": 0, "items": [{"item": pocion, "count": 2}]}
	var applied := InventoryOps.trade_apply(inv_ana, 100, offer_a, inv_bob, 0, offer_b)
	# el oro lo mueve el llamador cuando applied["reason"] == OK

# TTL por tick() inyectado: sin relojes dentro del componente.
trade.tick(120.0)               # → cancelled por inactividad (IDLE_TIMEOUT_S)

# Cada lado ve la sesión desde su perspectiva (para pintar la UI):
var view := trade.view(11)      # {mine: {...}, theirs: {...}, state, ...}

Demo en vivo

El componente REAL corriendo en tu navegador — Godot 4.7 exportado a Web sobre las fixtures del kit. La primera carga baja el motor (~10 MB comprimido) y se comparte entre todas las demos.

abrir a pantalla completa ↗

API pública

ClaseQué da
PartyStatenew(leader_id) · add/remove(id) -> motivo · transfer_leadership · size/is_full/has_member · MAX_SIZE = 8
TradeSessionnew(a, b) · set_gold/set_item(who, …) · lock/unlock/confirm(who) · cancel(reason) · tick(s) · view(who) · state (negotiating/committed/cancelled)

Firmas completas (del código)

PartyStateextends RefCountedparty_state.gd

Membresía de un GRUPO (M5, plan MMORPG: arenas ≤8 y mazmorras por party). Núcleo puro (A5): solo ids y reglas — las invitaciones, el enrutado y los datos vivos (nombre/HP) son del servicio y del zone server. El orden de members es el de ENTRADA (estable, sirve de desempate para heredar el liderazgo); el líder puede no ser members[0] tras una transferencia.

const OK := &"" # mismo contrato que CharacterProgress.OK / CombatAction
const MAX_SIZE := 8
var leader := 0
var members: Array[int] = []
PartyState.new(leader_id: int)

Constructor.

func size() -> int
func has_member(char_id: int) -> bool
func is_full() -> bool
func add(char_id: int) -> StringName
func remove(char_id: int) -> StringName

Salida (voluntaria o kick). Si se va el LÍDER, hereda el más antiguo.

func transfer_leadership(to_id: int) -> StringName
TradeSessionextends RefCountedtrade_session.gd

Escrow P2P (M5, plan MMORPG §11 E4): máquina de estados DETERMINISTA de la negociación entre dos personajes. Núcleo puro (A5): sin señales, sin relojes (el TTL avanza con tick() del llamador) y sin tocar inventarios — el CANJE lo materializa el servicio con ProgressMutations.trade_apply cuando esta sesión llega a COMMITTED. Seguridades anti-scam (referencia Dofus/WoW): - Cambiar TU oferta exige tu candado ABIERTO, y cualquier cambio revienta el candado Y la confirmación DEL OTRO (nadie confirma contra una oferta vieja). - Confirmar exige AMBOS candados echados; el doble confirm sella (COMMITTED). - Cancelar puede cualquiera, en cualquier momento; la inactividad expira.

const OK := &"" # mismo contrato que CharacterProgress.OK / CombatAction
const NEGOTIATING := &"negotiating"
const COMMITTED := &"committed"
const CANCELLED := &"cancelled"
const IDLE_TIMEOUT_S := 120.0

Inactividad máxima antes de expirar la negociación (segundos de tick()).

var a_id := 0
var b_id := 0
var state: StringName = NEGOTIATING
var cancel_reason: StringName = &""

Motivo del cierre cuando state == CANCELLED (quién/por qué lo decide el caller).

TradeSession.new(first_id: int, second_id: int)

Constructor.

func has_side(who: int) -> bool
func other(who: int) -> int
func offer_of(who: int) -> Dictionary

Oferta de un lado (copia profunda: el llamador no puede mutar el escrow).

func is_locked(who: int) -> bool
func is_confirmed(who: int) -> bool
func is_empty() -> bool
func set_gold(who: int, amount: int) -> StringName

Fija el ORO ofrecido por un lado. Solo con la negociación viva y TU candado abierto; el cambio revienta candado+confirmación del otro (oferta nueva).

func set_item(who: int, item_id: StringName, count: int) -> StringName

Fija la cantidad ABSOLUTA de un ítem en la oferta (0 = quitarlo). El stock real lo valida el servicio contra el inventario (aquí solo la negociación).

func lock(who: int) -> StringName

Echa el candado: tu oferta queda congelada hasta que la cambies (unlock).

func unlock(who: int) -> StringName

Abre TU candado (para retocar la oferta); anula las confirmaciones.

func confirm(who: int) -> StringName

Confirmación final: exige AMBOS candados. El doble confirm sella la sesión (COMMITTED) — a partir de ahí solo el servicio materializa el canje.

func cancel(reason: StringName) -> void

Cierre unilateral (o del sistema: timeout/desconexión). Idempotente.

func tick(seconds: float) -> void

Avanza el reloj de INACTIVIDAD (lo alimenta el servicio; determinista).

func view(who: int) -> Dictionary

Vista para la UI/protocolo DESDE la perspectiva de un lado.

Qué configura el juego

Quién puede invitar/echar (servicio), qué hace al sellar (committed → InventoryOps.trade_apply + mover oro), el TTL.

Fixture mínima

testing/social_fixtures.gd — SocialFixtures.full_party() (8, líder 1), locked_trade() (Ana 50 oro ↔ Bob 2 pociones, ambos candados).

social_fixtures.gdcódigo completo
social_fixtures.gd
# res://addons/fpx/social/testing/social_fixtures.gd
class_name SocialFixtures
extends RefCounted
## Fixture mínima del componente `social`: un grupo lleno (8, líder 1) y una
## sesión de trade con ambas ofertas puestas y candadas — el estado justo antes
## del doble confirm. Ids de personaje estables: son contrato del fixture.

const LEADER_ID := 1
const ANA := 11
const BOB := 22


## Grupo a tope: líder 1 + miembros 2..8.
static func full_party() -> PartyState:
	var party := PartyState.new(LEADER_ID)
	for id: int in range(2, PartyState.MAX_SIZE + 1):
		party.add(id)
	return party


## Trade Ana↔Bob: Ana ofrece 50 de oro, Bob 2 pociones; ambos candados echados.
static func locked_trade() -> TradeSession:
	var s := TradeSession.new(ANA, BOB)
	s.set_gold(ANA, 50)
	s.set_item(BOB, &"pocion", 2)
	s.lock(ANA)
	s.lock(BOB)
	return s

Cómo se prueba

tests/social/ — party_state_test, trade_session_test (máquina de estados completa), social_fixtures_test.

Ejemplar oro (Stremus)

scripts/core/social/{party_state,trade_session}.gd (main@0be95b4); M5 escrow E4.

Deuda declarada

  • Los servicios de servidor del ejemplar (PartyService/TradeService, invitaciones, kick, timeouts) llegan con server_kernel (FK-8+).
Generado el 2026-08-31 · Funpix Kit — componentes extraídos de Stremus: El Tercer Despertar (ejemplar oro) · Funpixart