// ---- 01_class_core.js ----
// guardian_system source chunk
// Original lines: 1004-2205

function getOrCreateObjective(name) {
  try {
    return world.scoreboard.getObjective(name) ?? world.scoreboard.addObjective(name, name);
  } catch (e) {
    try { return world.scoreboard.getObjective(name); } catch (e2) { return undefined; }
  }
}

function cfgGet(key, def = 0) {
  const obj = getOrCreateObjective(OBJ_CONFIG);
  if (!obj) return def;

  // Caminho rápido: a maioria das versões aceita participant string direto.
  try {
    const v = obj.getScore(String(key));
    if (v !== undefined && v !== null) return v;
  } catch (e) {}

  // Fallback (mais caro): procurar participant por displayName.
  try {
    const p = obj.getParticipants().find((pp) => pp.displayName === key);
    if (!p) return def;
    const v2 = obj.getScore(p);
    return (v2 ?? def);
  } catch (e) { return def; }
}


function cfgSet(key, value) {
  const obj = getOrCreateObjective(OBJ_CONFIG);
  if (!obj) return;
  try {
    // setScore aceita string como participant fake em várias versões
    obj.setScore(String(key), Number(value) | 0);
  } catch (e) {
    try {
      // fallback: achar/criar participant
      const existing = obj.getParticipants().find((pp) => pp.displayName === key);
      if (existing) obj.setScore(existing, Number(value) | 0);
    } catch (e2) {}
  }
}

function mobGriefBloqueado() {
  return cfgGet(CFG_MOBGRIEF, 1) === 1;
}

function resolveEffectType(effectId) {
  // Ajuda: converte "minecraft:speed" -> "speed" e mantém também o id original.
  const id = String(effectId ?? "").toLowerCase();
  if (id.startsWith("minecraft:")) return id.slice("minecraft:".length);
  return id;
}

// "Infinito" na prática: duração bem longa + reaplicação periódica.
const EFEITO_DUR_TICKS = 1000000; // ~13h53m (ticks)

// Tags de controle (pra não spammar chat)
function getEffectCandidates(effectId) {
  const raw = String(effectId ?? "");
  const lower = raw.toLowerCase();
  const noNs = resolveEffectType(lower);
  // candidatos comuns para addEffect/removeEffect em diferentes builds
  const cands = [];
  if (raw) cands.push(raw);
  if (lower && lower !== raw) cands.push(lower);
  if (noNs) cands.push(noNs);
  if (noNs && !noNs.startsWith("minecraft:")) cands.push(`minecraft:${noNs}`);
  // remove duplicados mantendo ordem
  return [...new Set(cands)];
}

async function applyNpcEffect(player, effectId, amp = 0, showParticles = true) {
  // 1) Tenta API nativa (não depende de cheats)
  if (player && typeof player.addEffect === "function") {
    for (const cand of getEffectCandidates(effectId)) {
      try {
        player.addEffect(cand, EFEITO_DUR_TICKS, { amplifier: amp, showParticles: !!showParticles });
        return true;
      } catch (e) {}
    }
  }

  // 2) Sem fallback por comando (mantém compatível com mundos sem cheats)
  return false;
}

async function clearNpcEffect(player, effectId) {
  // 1) Tenta API nativa
  if (player && typeof player.removeEffect === "function") {
    for (const cand of getEffectCandidates(effectId)) {
      try {
        player.removeEffect(cand);
        return true;
      } catch (e) {}
    }
  }

  // 2) Sem fallback por comando (mantém compatível com mundos sem cheats)
  return false;
}
// ====== Diálogo (UI) ======
// Observação: isso é "diálogo por escolhas" (botões). Bedrock não suporta chat livre nativo aqui.
// Legacy manual PT/EN dialog tables removed.
// Visible NPC dialog now resolves through translation keys in the resource pack.

const DIALOG_LAYOUT = {
  manual: {
    start: "n0",
    nodes: {
      n0: { buttons: [{ next: "n1" }, { next: "n2" }, { next: "n3" }, { next: "n4" }, { action: "open_menu" }, { action: "end" }] },
      n1: { buttons: [{ next: "n2" }, { next: "n0" }, { action: "open_menu" }] },
      n2: { buttons: [{ next: "n3" }, { next: "n0" }, { action: "open_menu" }] },
      n3: { buttons: [{ next: "n4" }, { next: "n0" }, { action: "open_menu" }] },
      n4: { buttons: [{ next: "n0" }, { action: "open_menu" }, { action: "end" }] },
    },
  },
};

function _dialogTranslate(dialogId, nodeId, field) {
  return _rmTranslate(`lwj.guardian.dialog.${dialogId}.${nodeId}.${field}`);
}

function getDialog(player, dialogId) {
  const layout = DIALOG_LAYOUT?.[dialogId];
  if (!layout) return null;
  const nodes = {};
  for (const [nodeId, node] of Object.entries(layout.nodes ?? {})) {
    nodes[nodeId] = {
      title: _dialogTranslate(dialogId, nodeId, 'title'),
      body: _dialogTranslate(dialogId, nodeId, 'body'),
      buttons: (node.buttons ?? []).map((button, index) => ({
        ...button,
        text: _dialogTranslate(dialogId, nodeId, `button${index}`),
      })),
    };
  }
  return { start: layout.start, nodes };
}

function efeitoTag(effectId, amp = 0) {
  const enc = String(effectId).replace(/:/g, "__");
  return `${TAG_EFF_PREFIX}${enc}__a${amp}`;
}

function efeitoDisabledTag(effectId, amp = 0) {
  const enc = String(effectId).replace(/:/g, "__");
  return `${TAG_EFF_DISABLED_PREFIX}${enc}__a${amp}`;
}

function parseEfeitoTag(tag) {
  if (!tag || !tag.startsWith(TAG_EFF_PREFIX)) return null;
  const raw = tag.slice(TAG_EFF_PREFIX.length);
  const i = raw.lastIndexOf("__a");
  if (i <= 0) return null;
  const idEnc = raw.slice(0, i);
  const ampStr = raw.slice(i + 3);
  const effectId = idEnc.replace(/__/g, ":");
  const amp = Number(ampStr);
  return { effectId, amp: Number.isFinite(amp) ? amp : 0 };
}

function getEfeitosSalvos(player) {
  try {
    return player.getTags().filter((t) => t.startsWith(TAG_EFF_PREFIX));
  } catch {
    return [];
  }
}

function isSavedEffectEnabled(player, effectId, amp = 0) {
  try {
    return !player.hasTag(efeitoDisabledTag(effectId, amp));
  } catch {
    return true;
  }
}

function setSavedEffectEnabled(player, effectId, amp = 0, enabled = true) {
  const tag = efeitoDisabledTag(effectId, amp);
  try {
    if (enabled) player.removeTag(tag);
    else player.addTag(tag);
  } catch (e) {}
}

function efeitosAtivos(player) {
  try { return !player?.hasTag?.(TAG_EFEITO_OFF); } catch (e) { return true; }
}

function particulasAtivas(player) {
  try {
    if (!player?.hasTag?.(TAG_PARTICLES_PREF_SET)) return false;
    return !player?.hasTag?.(TAG_EFEITO_NO_PARTICLES);
  } catch (e) {
    return false;
  }
}


function setEfeitosAtivos(player, ativo) {
  try {
    if (ativo) player?.removeTag?.(TAG_EFEITO_OFF);
    else player?.addTag?.(TAG_EFEITO_OFF);
  } catch (e) {}
}

function setParticulasAtivas(player, ativo) {
  try {
    player?.addTag?.(TAG_PARTICLES_PREF_SET);
    if (ativo) player?.removeTag?.(TAG_EFEITO_NO_PARTICLES);
    else player?.addTag?.(TAG_EFEITO_NO_PARTICLES);
  } catch (e) {}
}

async function removerEfeitosSalvos(player) {
  const tags = getEfeitosSalvos(player);
  if (!tags.length) return;
  for (const t of tags) {
    const parsed = parseEfeitoTag(t);
    if (!parsed) continue;
    try { await clearNpcEffect(player, parsed.effectId); } catch (e) {}
  }
}

async function aplicarEfeitosSalvos(player) {
  const tags = getEfeitosSalvos(player);
  if (!tags.length) return;

  const enable = efeitosAtivos(player);
  const showParticles = particulasAtivas(player);

  for (const t of tags) {
    const parsed = parseEfeitoTag(t);
    if (!parsed) continue;

    try {
      const effectEnabled = isSavedEffectEnabled(player, parsed.effectId, parsed.amp ?? 0);
      if (enable && effectEnabled) {
        await applyNpcEffect(player, parsed.effectId, parsed.amp ?? 0, showParticles);
      } else {
        await clearNpcEffect(player, parsed.effectId);
      }
    } catch (e) {
      // não quebra o add-on se algum efeito não existir nessa versão
    }
  }
}


// ===== Controle de tempo do dia (sem precisar digitar comandos no chat) =====
// Vanilla: 24000 ticks em 20 minutos => 1200 ticks/min.
// 25 minutos: queremos 24000 em 25 min => 960 ticks/min (1.25x mais lento).
// Truque: não precisamos mexer no gamerule. A gente "puxa" o tempo um pouquinho pra trás,
// num intervalo fixo, pra que o avanço final fique mais lento.

let _dayLenModeCached = 1;
let _dayLenModeLastRefresh = -999999;

function _refreshDayLenMode() {
  // padrão: 25 minutos
  const v = cfgGet(CFG_DAYLEN, 1);
  _dayLenModeCached = (v === 0 || v === 1) ? v : 1;
}

function getDayLenMode() {
  // Esse método é chamado num loop de 1 tick: evitar leitura de scoreboard todo tick.
  let now = 0;
  try { now = system.currentTick ?? 0; } catch (e) { now = 0; }

  if ((now - _dayLenModeLastRefresh) >= 20) { // ~1s
    _dayLenModeLastRefresh = now;
    _refreshDayLenMode();
  }
  return _dayLenModeCached;
}

function setDayLenMode(mode) {
  cfgSet(CFG_DAYLEN, mode);

  // Atualiza cache imediatamente (UI responde na hora).
  _dayLenModeCached = (mode === 0 || mode === 1) ? mode : 1;
  try { _dayLenModeLastRefresh = system.currentTick ?? 0; } catch (e) { _dayLenModeLastRefresh = 0; }
}

// ===== Restrições de classe =====
// O modo Aprendiz assume o fluxo único de progressão.
// As limitações do caminho ficam sempre ativas e acompanham a classe atual.
function classRestrictionsEnabled() {
  return true;
}


let _dayLenTickCounter = 0;

/**
 * Controle de duração do dia sem comandos.
 * - Mode 0: normal (20 min, sem ajuste)
 * - Mode 1: 25 min (0.8x velocidade)  => a cada 5 ticks, volta 1
 */
function ajustarTempoParaModoTick() {
  const mode = getDayLenMode();
  if (mode === 0) return;

  // Mode 1: 25 min (0.8x velocidade)
  _dayLenTickCounter = (_dayLenTickCounter + 1) % 60;

  // Em 5 ticks, normal avançaria +5; queremos +4 => voltar 1
  if ((_dayLenTickCounter % 5) !== 0) return;

  const back = 1;

  try {
    const abs = (world.getDay() * 24000) + world.getTimeOfDay();
    const target = Math.max(0, abs - back);
    world.setAbsoluteTime(target);
  } catch (e) {
    // Se algo der errado (modo restrito), não ajusta.
  }
}



// ====== Configurações do item: travar após definir uma vez ======
function cfgItemLocked(player) {
  try { return !!player?.hasTag?.(TAG_CFG_LOCKED); } catch (e) { return false; }
}
function lockCfgItem(player) {
  ensurePlayerConfigDataVersion(player);
  try { player?.addTag?.(TAG_CFG_LOCKED); } catch (e) {}
}
function worldSettingsLocked() {
  return cfgGet(CFG_SETTINGS_LOCKED, 0) === 1;
}
function lockWorldSettings() {
  cfgSet(CFG_SETTINGS_LOCKED, 1);
}
function _dayLenLabelKey(mode) {
  return (mode === 0) ? "daylen_normal" : "daylen_25";
}
function buildConfigSummaryLegacyRaw(player) {
  const mode = getDayLenMode();
  return _rmJoin(
    "§l§6", _rmTranslate("lwj.guardian.legacy_settings.summary.personal"), "§r\n",
    _rmTranslate("lwj.ui.settings.summary.effect"), ": ", _stateLabelRaw(efeitosAtivos(player)), "\n",
    _rmTranslate("lwj.ui.settings.summary.particles"), ": ", _stateLabelRaw(particulasAtivas(player)), "\n\n",
    "§l§6", _rmTranslate("lwj.guardian.legacy_settings.summary.world"), "§r\n",
    _rmTranslate("lwj.ui.settings.summary.explosions"), ": ", _stateLabelRaw(mobGriefBloqueado()), "\n",
    _rmTranslate("lwj.ui.settings.summary.day_night"), ": ", _dayLenLabelRaw(mode)
  );
}
function showConfigLockedSummary(player, onDone) {
  const summary = buildConfigSummaryLegacyRaw(player);
  const f = new ActionFormData();
  f.title(_rmTranslate("lwj.guardian.legacy_settings.locked_title"));
  f.body(_rmJoin(
    _rmTranslate("lwj.guardian.legacy_settings.locked_body_intro"), "\n\n",
    summary
  ));
  f.button(_rmTranslate("lwj.ui.common.ok"));

  const canReset = isCreative(player);
  if (canReset) f.button(_rmTranslate("lwj.guardian.legacy_settings.reset_btn"));

  return _showFormSafe(f, player).then((res) => {
    if (res?.canceled) return onDone?.();
    if (canReset && res?.selection === 1) return confirmResetSettings(player, onDone);
    return onDone?.();
  });
}

function _resetConfigItemPlayerLegacy(player) {
  ensurePlayerConfigDataVersion(player);
  // Reseta apenas o que o item "Configurações" controla (preferências do efeito/partículas)
  try { player?.removeTag?.(TAG_CFG_LOCKED); } catch (e) {}
  try { player?.removeTag?.(TAG_EFEITO_OFF); } catch (e) {}
  try { player?.removeTag?.(TAG_EFEITO_NO_PARTICLES); } catch (e) {}
  try { player?.removeTag?.(TAG_PARTICLES_PREF_SET); } catch (e) {}

  // Defaults do addon: partículas DESLIGADAS e efeito ATIVADO.
  try { player?.addTag?.(TAG_EFEITO_NO_PARTICLES); } catch (e) {}
  try { player?.addTag?.(TAG_PARTICLES_PREF_SET); } catch (e) {}

  // Reaplica/remover conforme as tags padrão
  try {
    if (efeitosAtivos(player)) void aplicarEfeitosSalvos(player);
    else void removerEfeitosSalvos(player);
  } catch (e) {}
}

function _resetConfigWorldLegacy() {
  // Destrava configurações globais e volta aos defaults
  try {
    cfgSet(CFG_SETTINGS_LOCKED, 0);
    cfgSet(CFG_MOBGRIEF, 1);
    setDayLenMode(1);
  } catch (e) {}
}


function confirmResetWorldOnly(player, onDone) {
  // Somente Criativo chama isto (botão não aparece fora do Criativo).
  const m = new MessageFormData();
  m.title(_gUi("world_reset.title"));
  m.body(_gUi("world_reset.body"));
  m.button1(_gUi("world_reset.confirm"));
  m.button2(_gUi("world_reset.cancel"));

  return _showFormSafe(m, player).then((res) => {
    if (res?.canceled) return onDone?.();
    if (res?.selection === 1) return onDone?.();

    _resetConfigWorld();

    try { __LWJ_MUTE_MESSAGE__(player, _gUi("world_reset.done")); } catch (e) {}
    return abrirPainelMundo(player, onDone);
  }).catch(() => onDone?.());
}

function confirmResetSettings(player, onDone) {
  // Somente Criativo chama isto (botão não aparece fora do Criativo).
  const m = new MessageFormData();
  m.title(_rmTranslate("lwj.guardian.legacy_settings.reset_title"));
  m.body(_rmTranslate("lwj.guardian.legacy_settings.reset_body"));
  m.button1(_rmTranslate("lwj.guardian.legacy_settings.reset_yes"));
  m.button2(_rmTranslate("lwj.guardian.legacy_settings.reset_no"));

  return _showFormSafe(m, player).then((res) => {
    if (res?.canceled) return onDone?.();
    if (res?.selection === 1) return onDone?.();

    _resetConfigItemPlayer(player);
    _resetConfigWorld();

    // Atualiza o nome do NPC para o padrão (legado) logo após reset
    try { const npc = findGuardianNpcAny(); if (npc) npc.nameTag = npcNameFor(player); } catch (e) {}

    try { __LWJ_MUTE_MESSAGE__(player, _rmTranslate("lwj.guardian.legacy_settings.reset_done")); } catch (e) {}

    // Abre novamente a configuração
    return abrirConfiguracoes(player, onDone);
  });
}


function abrirConfigItemLegacy(player) {
  abrirConfiguracoes(player);
}

// Tela única de configurações (sem "segunda tela" de botões)
function abrirConfiguracoesLegacy(player, onDone) {
  // Se já foi configurado, só mostra um resumo (somente leitura)
  if (cfgItemLocked(player)) return showConfigLockedSummary(player, onDone);

  const wLocked = worldSettingsLocked();

  // Aviso: configuração única
  const warn = new MessageFormData();
  warn.title(_rmTranslate("lwj.guardian.legacy_settings.once_title"));
  warn.body(_rmTranslate(wLocked ? "lwj.guardian.legacy_settings.once_body_world_locked" : "lwj.guardian.legacy_settings.once_body"));
  warn.button1(_rmTranslate("lwj.guardian.legacy_settings.once_yes"));
  warn.button2(_rmTranslate("lwj.guardian.legacy_settings.once_no"));

  return _showFormSafe(warn, player).then((wres) => {
    if (wres.canceled) return onDone?.();
    // selection: 0 = botão1, 1 = botão2
    if (wres.selection === 1) return onDone?.();
    return _abrirConfiguracoesModal(player, wLocked, onDone);
  });
}

function _abrirConfiguracoesModal(player, wLocked, onDone) {
  const effOn = efeitosAtivos(player);
  const particlesOn = particulasAtivas(player);

  const form = new ModalFormData();
  form.title(_rmTranslate("lwj.guardian.legacy_settings.title"));

  // Preferências do player
  form.toggle(_rmTranslate("lwj.ui.settings.personal.reward_effect"), { defaultValue: effOn });
  form.toggle(_rmTranslate("lwj.ui.settings.personal.effect_particles"), { defaultValue: particlesOn });

  // Configurações globais do mundo: só aparecem se ainda não foram travadas
  let dayOptions = null;
  let allowedModes = null;
  let defIdx = 0;

  if (!wLocked) {
    const mobgrief = mobGriefBloqueado();
    form.toggle(_rmTranslate("lwj.ui.settings.world.explosion_toggle"), { defaultValue: mobgrief });

    const dayMode = cfgGet(CFG_DAYLEN, 1);
    dayOptions = [_rmTranslate("lwj.ui.settings.world.day_normal"), _rmTranslate("lwj.ui.settings.world.day_25")];
    allowedModes = [0, 1];
    defIdx = Math.max(0, allowedModes.indexOf((dayMode === 0 || dayMode === 1) ? dayMode : 1));

    form.dropdown(
      _rmTranslate("lwj.ui.settings.world.day_length"),
      dayOptions,
      { defaultValueIndex: defIdx }
    );
  }

  return _showFormSafe(form, player).then((res) => {
    if (res.canceled) return onDone?.();

    const v = res.formValues ?? [];

    // v[0..] = preferências do player
    const novoEff = v[0] === true;
    const novoParticles = v[1] === true;

    setEfeitosAtivos(player, novoEff);
    setParticulasAtivas(player, novoParticles);

    if (novoEff) void aplicarEfeitosSalvos(player);
    else void removerEfeitosSalvos(player);

    // Config global (mundo) só pode ser definida uma vez
    if (!wLocked) {
      const novoMob = v[2] === true;
      const novoDayIndex = (typeof v[3] === "number") ? (v[3] | 0) : defIdx;
      const novoDayMode = allowedModes?.[novoDayIndex] ?? 1;

      cfgSet(CFG_MOBGRIEF, novoMob ? 1 : 0);
      setDayLenMode((novoDayMode === 0 || novoDayMode === 1) ? novoDayMode : 1);

      lockWorldSettings();
    }

    // Trava as configurações do item para este jogador
    lockCfgItem(player);

    __LWJ_MUTE_MESSAGE__(player, _rmTranslate("lwj.guardian.legacy_settings.locked_msg"));
    onDone?.();
  }).catch((e) => {
    onDone?.();
    throw e;
  });
}

function mostrarDialogo(player, dialogId = "manual", nodeId = null) {
  const dialog = getDialog(player, dialogId);
  if (!dialog) return openNpcUI(player);

  const currentNodeId = nodeId ?? dialog.start;
  const node = dialog.nodes?.[currentNodeId];
  if (!node) return openNpcUI(player);

  const form = new ActionFormData().title(node.title).body(node.body);
  for (const b of node.buttons) form.button(b.text);

  _showFormSafe(form, player).then((res) => {
    if (res.canceled) return;

    const chosen = node.buttons[res.selection];
    if (!chosen) return;

    if (chosen.action === "open_menu") return openNpcUI(player);
    if (chosen.action === "end") return;

    if (chosen.action === "settings") {
      return mostrarDialogo(player, dialogId, currentNodeId);
    }

    if (chosen.next) return mostrarDialogo(player, dialogId, chosen.next);
  });
}

function iniciarConversaNpc(player, npc) {
  // Ao interagir, abrimos o menu direto.
  return openNpcUI(player, npc);
}


const CLASSES = CG_CLASS_LIST;

const TAG_MANUAL_PREFIX = CG.prefixes.manual; // marca para não duplicar o livro automaticamente

const MANUAL_PAGE_KEYS = {
  [CG.classes.warrior]: [
    "lwj.manual.warrior.page1",
    "lwj.manual.warrior.page2",
    "lwj.manual.warrior.page3",
    "lwj.manual.warrior.page4",
  ],
  [CG.classes.archer]: [
    "lwj.manual.archer.page1",
    "lwj.manual.archer.page2",
    "lwj.manual.archer.page3",
  ],
  [CG.classes.mage]: [
    "lwj.manual.mage.page1",
    "lwj.manual.mage.page2",
    "lwj.manual.mage.page3",
  ],
};

function makeTranslatedPage(parts) {
  return JSON.stringify({
    rawtext: parts.map((part) => {
      if (typeof part === "string") return { translate: part };
      return part;
    }),
  });
}

function _getManualData(player, classeId) {
  const pageKeys = MANUAL_PAGE_KEYS?.[classeId];
  if (!Array.isArray(pageKeys) || pageKeys.length <= 0) return null;
  return { pageKeys };
}

function _makeManualBook(player, classeId) {
  const data = _getManualData(player, classeId);
  if (!data) return null;

  const book = new ItemStack(LWJ_IDS.item.writtenBook, 1);

  try {
    const comp = book.getComponent("minecraft:book");
    if (comp?.setContents) {
      const pages = (data.pageKeys ?? []).slice(0, 50).map((key) => makeTranslatedPage([key]));
      comp.setContents(pages);
    }
  } catch (e) {}

  try { book.setLore?.([`§0lwj_manual:${classeId}`]); } catch (e) {}

  return book;
}

function giveManualBook(player, classeId, force = false, silent = false) {
  try {
    const tag = cgManualTag(classeId);
    if (!force && player.hasTag?.(tag)) return;

    const book = _makeManualBook(player, classeId);
    if (!book) return;

    // Tenta adicionar ao inventário; se não couber, dropa.
    const c = getContainer(player);
    let dropped = false;
    if (c) {
      try {
        const leftover = c.addItem(book);
        if (leftover) {
          dropped = true;
          try { player.dimension.spawnItem(book, player.location); } catch (e) {}
        }
      } catch (e) {
        dropped = true;
        try { player.dimension.spawnItem(book, player.location); } catch (e2) {}
      }
    } else {
      dropped = true;
      try { player.dimension.spawnItem(book, player.location); } catch (e) {}
    }

    try { player.addTag?.(tag); } catch (e) {}

    if (!silent) {
      try {
        __LWJ_MUTE_MESSAGE__(player, _rmTranslate(dropped ? "lwj.guardian.manual.given_drop" : "lwj.guardian.manual.given"));
      } catch (e) {}
    }
  } catch (e) {}
}

function _stripFmtManual(s) {
  try { return String(s ?? "").replace(/§./g, ""); } catch { return ""; }
}

function hasManualBook(player, classeId) {
  try {
    const inv = player?.getComponent?.("minecraft:inventory")?.container;
    if (!inv) return false;
    const needle = String(classeId ?? "").toLowerCase();
    for (let i = 0; i < inv.size; i++) {
      const it = inv.getItem(i);
      if (!it) continue;
      const tid = String(it.typeId ?? "");
      if (tid !== "minecraft:writable_book" && tid !== "minecraft:written_book") continue;

      try {
        const lore = typeof it.getLore === "function" ? (it.getLore() ?? []) : [];
        for (const line of lore) {
          const raw = _stripFmtManual(line).trim().toLowerCase();
          if (!raw.startsWith('lwj_manual:')) continue;
          if (!needle || raw.endsWith(needle)) return true;
        }
      } catch (e) {}

      const nm = _stripFmtManual(it.nameTag ?? "").trim().toLowerCase();
      if (!nm) continue;
      if (!nm.startsWith("manual:")) continue;
      if (needle && nm.includes(needle)) return true;
    }
  } catch (e) {}
  return false;
}

const _manualEnsureLastTry = new Map(); // playerId -> tick
const _classInfoCache = new Map(); // playerId -> { tick, value }
const _effectApplyBusy = new Set();

function _playerKeySafe(player) {
  try { return player?.id ?? player?.name ?? String(player); } catch (e) { return String(player); }
}

function _cleanupGuardianCaches() {
  try {
    const active = new Set();
    for (const p of world.getPlayers()) active.add(_playerKeySafe(p));
    for (const k of _manualEnsureLastTry.keys()) {
      if (!active.has(k)) _manualEnsureLastTry.delete(k);
    }
    try { rjCleanupPlayerClassCache(world.getPlayers?.() ?? []); } catch (e) {}
    for (const k of _effectApplyBusy) {
      if (!active.has(k)) _effectApplyBusy.delete(k);
    }
  } catch (e) {}
}

async function applySavedEffectsSafely(player) {
  const pid = _playerKeySafe(player);
  if (_effectApplyBusy.has(pid)) return;
  _effectApplyBusy.add(pid);
  try {
    await aplicarEfeitosSalvos(player);
  } finally {
    _effectApplyBusy.delete(pid);
  }
}

function ensureManualBook(player) {
  try {
    if (!player || !player.isValid?.()) return;
  } catch (e) {}
  try {
    if (!player.hasTag?.(TAG_ESCOLHIDA)) return;
  } catch (e) { return; }

  const classe = getClasseInfo(player);
  if (!classe) return;

  // Se já tem algum manual (mesmo que antigo), não duplica.
  if (hasManualBook(player, classe.id)) return;

  const now = (() => { try { return system.currentTick | 0; } catch { return 0; } })();
  const last = _manualEnsureLastTry.get(player.id) ?? -999999;
  if (now - last < 600) return; // ~30s
  _manualEnsureLastTry.set(player.id, now);

  // Reentrega silenciosamente (evita depender do NPC/diálogo)
  try { giveManualBook(player, classe.id, true, true); } catch (e) {}
}



function classeNome(player, classe) {
  if (!classe) return "";
  const key = guardianClassTranslateKey(classe?.id);
  if (key) {
    const raw = _rmTranslate(key);
    try {
      const first = raw?.rawtext?.[0];
      if (typeof first?.translate === "string") return String(first.translate);
      if (typeof first?.text === "string") return first.text;
    } catch (e) {}
  }
  return String(classe?.fallbackName ?? classe?.id ?? "");
}

// ====== Efeitos de recompensa por classe ======
// Estes efeitos ficam salvos como "efeitos do NPC" (tags classe_eff_*),
// então o jogador pode ativar/desativar no Configurador sem precisar de cheats.
const LEGACY_REMOVED_CLASS_EFFECTS = [
  { id: "minecraft:absorption", amp: 0 },
  { id: "minecraft:slow_falling", amp: 0 },
];

const CLASS_REWARD_EFFECTS = {
  guerreiro: [
    { id: "minecraft:strength", amp: 0, translateKey: "lwj.effect.strength" },
    { id: "minecraft:resistance", amp: 0, translateKey: "lwj.effect.resistance" },
  ],
  arqueiro: [
    { id: "minecraft:speed", amp: 0, translateKey: "lwj.effect.speed" },
    { id: "minecraft:jump_boost", amp: 0, translateKey: "lwj.effect.jump_boost" },
  ],
  mago: [
    { id: "minecraft:night_vision", amp: 0, translateKey: "lwj.effect.night_vision" },
    { id: "minecraft:fire_resistance", amp: 0, translateKey: "lwj.effect.fire_resistance" },
    { id: "minecraft:haste", amp: 0, translateKey: "lwj.effect.haste" },
  ],
};

function getClassRewardEffectsForPlayer(player) {
  const info = getClasseInfo(player);
  if (!info?.id) return [];
  return CLASS_REWARD_EFFECTS?.[info.id] ?? [];
}

function classRewardEffectNameKey(effectId) {
  switch (String(effectId ?? "")) {
    case "minecraft:strength": return "lwj.potion.effect.strength";
    case "minecraft:resistance": return "lwj.potion.effect.resistance";
    case "minecraft:speed": return "lwj.potion.effect.speed";
    case "minecraft:jump_boost": return "lwj.potion.effect.jump_boost";
    case "minecraft:night_vision": return "lwj.potion.effect.night_vision";
    case "minecraft:fire_resistance": return "lwj.potion.effect.fire_resistance";
    case "minecraft:haste": return "lwj.potion.effect.haste";
    default: return null;
  }
}

function classRewardEffectNameRaw(eff) {
  const key = classRewardEffectNameKey(eff?.id);
  return key ? _rmTranslate(key) : (eff?.translateKey ? _rmTranslate(eff.translateKey) : _rmText(String(eff?.nome ?? eff?.id ?? "?")));
}

function buildClassEffectToggleSummaryRaw(player) {
  const list = getClassRewardEffectsForPlayer(player);
  if (!list.length) return _rmTranslate("lwj.ui.settings.personal.class_effects_empty");

  const raw = { rawtext: [] };
  _rmPush(raw, _rmTranslate("lwj.ui.settings.personal.class_effects_intro"), "\n\n");

  for (const eff of list) {
    const unlocked = !!player?.hasTag?.(efeitoTag(eff.id, eff.amp ?? 0));
    const enabled = unlocked && isSavedEffectEnabled(player, eff.id, eff.amp ?? 0);
    const stateKey = !unlocked
      ? "lwj.ui.settings.personal.effect_state_locked"
      : (enabled ? "lwj.ui.settings.personal.effect_state_enabled" : "lwj.ui.settings.personal.effect_state_disabled");
    _rmPush(raw, "• ", classRewardEffectNameRaw(eff), ": ", _rmTranslate(stateKey), "\n");
  }

  if (raw.rawtext.length > 0) {
    const last = raw.rawtext[raw.rawtext.length - 1];
    if (last?.text === "\n") raw.rawtext.pop();
  }
  return raw;
}

function buildClassEffectToggleSummary(player) {
  const raw = buildClassEffectToggleSummaryRaw(player);
  return String(raw?.rawtext?.map((part) => part?.text ?? "").join("") ?? "");
}

async function toggleClassRewardEffectForPlayer(player, eff) {
  if (!player || !eff?.id) return null;
  const amp = eff.amp ?? 0;
  if (!player.hasTag?.(efeitoTag(eff.id, amp))) return null;

  const currentlyEnabled = isSavedEffectEnabled(player, eff.id, amp);
  const nextEnabled = !currentlyEnabled;
  setSavedEffectEnabled(player, eff.id, amp, nextEnabled);

  try {
    if (efeitosAtivos(player) && nextEnabled) {
      await applyNpcEffect(player, eff.id, amp, particulasAtivas(player));
    } else {
      await clearNpcEffect(player, eff.id);
    }
  } catch (e) {}

  return nextEnabled;
}

async function grantClasseRewardEffects(player, classeId) {
  const list = CLASS_REWARD_EFFECTS?.[classeId] ?? [];
  if (!list.length) return;

  for (const eff of list) {
    // salvar como permanente
    try {
      const t = efeitoTag(eff.id, eff.amp ?? 0);
      if (!player.hasTag(t)) player.addTag(t);
    } catch (e) {}

    // aplicar agora (respeita preferências)
    try {
      if (efeitosAtivos(player)) {
        await applyNpcEffect(player, eff.id, eff.amp ?? 0, particulasAtivas(player));
      }
    } catch (e) {}
  }

  // feedback (uma vez no desbloqueio)
  try {
    const msg = { rawtext: [{ text: "§b" }, _rmTranslate("lwj.ui.settings.personal.effects_unlocked_prefix")] };
    list.forEach((eff, index) => {
      if (index > 0) _rmPush(msg, ", ");
      _rmPush(msg, classRewardEffectNameRaw(eff));
    });
    __LWJ_MUTE_MESSAGE__(player, msg);
  } catch (e) {}
}


// Migração/sincronização: quando eu trocar quais efeitos cada classe recebe,
// isso atualiza os "efeitos permanentes" já desbloqueados sem precisar resetar a classe.
function sincronizarEfeitosClasse(player) {
  const info = getClasseInfo(player);
  if (!info) return;
  const classeId = info.id;

  const desiredList = CLASS_REWARD_EFFECTS?.[classeId] ?? [];
  const desiredTags = new Set(desiredList.map((e) => efeitoTag(e.id, e.amp ?? 0)));

  // Todas as tags de recompensa possíveis (inclui efeitos removidos em rebalanceamentos)
  const all = [];
  try {
    for (const k of Object.keys(CLASS_REWARD_EFFECTS ?? {})) {
      for (const e of (CLASS_REWARD_EFFECTS?.[k] ?? [])) {
        all.push({ tag: efeitoTag(e.id, e.amp ?? 0), disabledTag: efeitoDisabledTag(e.id, e.amp ?? 0), effectId: e.id, amp: e.amp ?? 0 });
      }
    }
    for (const e of (LEGACY_REMOVED_CLASS_EFFECTS ?? [])) {
      all.push({ tag: efeitoTag(e.id, e.amp ?? 0), disabledTag: efeitoDisabledTag(e.id, e.amp ?? 0), effectId: e.id, amp: e.amp ?? 0 });
    }
  } catch (e) {}

  for (const it of all) {
    const tag = it.tag;
    const disabledTag = it.disabledTag;
    const effectId = it.effectId;

    try {
      if (desiredTags.has(tag)) {
        // (v0.0.7) Não adiciona automaticamente: desbloqueio é feito por troca no Suporte de Poções.
      } else {
        if (player.hasTag(tag)) {
          try { player.removeTag(tag); } catch (e) {}
          // limpa imediatamente (sem depender da expiração do efeito)
          try { void clearNpcEffect(player, effectId); } catch (e) {}
        }
        if (player.hasTag(disabledTag)) {
          try { player.removeTag(disabledTag); } catch (e) {}
        }
      }
    } catch (e) {}
  }
}


function ensureObjective(id, display) {
  const sb = world.scoreboard;
  let obj = sb.getObjective(id);
  if (!obj) obj = sb.addObjective(id, display);
  return obj;
}


