// Grand Line: Devil Fruits - SERVER script v1
// COPY THIS TO: .minecraft/kubejs/server_scripts/grandline_server.js
//
// Handles: Sea Prism Altar rituals, fruit eating + the second-fruit
// consequence, the sea-water curse, Ambition (Will) progression, gear
// timers/exhaustion, petrify/pull/stun entity effects, homie lifetime,
// phoenix Undying Flame, rubber fall immunity.

console.log('[Grand Line] Server script v1 loading...')

// ---------------------------------------------------------------------------
// CONFIG - edit and /reload
// ---------------------------------------------------------------------------
global.glConfig = {
  secondFruitMode: 'canon',   // 'canon' = the sea devils tear you apart (death), 'funny' = violent indigestion
  autoGrantAmbition: true,    // everyone gets the Ambition power on join
  seaCurse: true,             // fruit users are cursed while swimming
  willPerKill: 1,             // Will gained per kill
  willBossBonus: 2,           // extra Will for big targets (max HP >= 50)
  gearTimeoutG4: 7200,        // ticks before Gear 4 force-reverts (6min)
  gearTimeoutG5: 7200,        // ticks before Gear 5 force-reverts (6min)
  exhaustTicks: 500,          // exhaustion length after G4/G5 (25s)
  chibiTicks: 300,            // chibi-size length after Gear 3 (15s)
  undyingCooldown: 6000       // phoenix Undying Flame cooldown (5min)
}

// fruit item -> power + identity tag
var GL_FRUITS = {
  'grandline:gomu_gomu_fruit': { power: 'grandline:gomu',    name: 'Gomu Gomu no Mi' },
  'grandline:ope_ope_fruit':   { power: 'grandline:ope',     name: 'Ope Ope no Mi' },
  'grandline:tori_tori_fruit': { power: 'grandline:phoenix', name: 'Tori Tori no Mi: Model Phoenix' },
  'grandline:mera_mera_fruit': { power: 'grandline:mera',    name: 'Mera Mera no Mi' },
  'grandline:hie_hie_fruit':   { power: 'grandline:hie',     name: 'Hie Hie no Mi' },
  'grandline:ito_ito_fruit':   { power: 'grandline:ito',     name: 'Ito Ito no Mi' },
  'grandline:mero_mero_fruit': { power: 'grandline:mero',    name: 'Mero Mero no Mi' },
  'grandline:jiki_jiki_fruit': { power: 'grandline:jiki',    name: 'Jiki Jiki no Mi' },
  'grandline:soru_soru_fruit': { power: 'grandline:soru',    name: 'Soru Soru no Mi' },
  'grandline:yami_yami_fruit': { power: 'grandline:yami',    name: 'Yami Yami no Mi' },
  'grandline:pika_pika_fruit': { power: 'grandline:pika',    name: 'Pika Pika no Mi' },
  'grandline:uo_uo_fruit':     { power: 'grandline:seiryu',  name: 'Uo Uo no Mi: Model Seiryu' },
  'grandline:gura_gura_fruit': { power: 'grandline:gura',    name: 'Gura Gura no Mi' },
  'grandline:sara_sara_fruit': { power: 'grandline:sara',    name: 'Sara Sara no Mi: Model Axolotl' },
  'grandline:mochi_mochi_fruit': { power: 'grandline:mochi', name: 'Mochi Mochi no Mi' }
}
var GL_FRUIT_POWERS = [
  'grandline:gomu', 'grandline:gomu_g2', 'grandline:gomu_g3', 'grandline:gomu_g4', 'grandline:gomu_g5',
  'grandline:ope', 'grandline:phoenix', 'grandline:mera', 'grandline:hie', 'grandline:ito',
  'grandline:mero', 'grandline:jiki', 'grandline:soru', 'grandline:yami', 'grandline:pika',
  'grandline:seiryu', 'grandline:gura', 'grandline:sara', 'grandline:mochi',
  'grandline:sara_form', 'grandline:seiryu_form', 'grandline:phoenix_form'
]
// Sea Prism Altar rituals: hold a Devil's Core, sneak-click to cycle, click to forge.
// Every ritual consumes 1 Devil's Core + the listed ingredients.
var GL_ALTAR = [
  { fruit: 'grandline:gomu_gomu_fruit', name: 'Gomu Gomu no Mi',              tier: 'Late',   ing: [['minecraft:slime_ball',32],['minecraft:honeycomb',16],['minecraft:gold_block',3],['minecraft:diamond',2]] },
  { fruit: 'grandline:hie_hie_fruit',   name: 'Hie Hie no Mi',                tier: 'Early',  ing: [['minecraft:packed_ice',16],['minecraft:snow_block',8],['minecraft:blue_ice',4]] },
  { fruit: 'grandline:ito_ito_fruit',   name: 'Ito Ito no Mi',                tier: 'Early',  ing: [['minecraft:string',32],['minecraft:white_wool',16],['minecraft:spider_eye',4]] },
  { fruit: 'grandline:mero_mero_fruit', name: 'Mero Mero no Mi',              tier: 'Early',  ing: [['minecraft:pink_petals',16],['minecraft:amethyst_shard',8],['minecraft:honey_bottle',4]] },
  { fruit: 'grandline:mera_mera_fruit', name: 'Mera Mera no Mi',              tier: 'Mid',    ing: [['minecraft:blaze_powder',16],['minecraft:fire_charge',8],['minecraft:magma_block',4]] },
  { fruit: 'grandline:jiki_jiki_fruit', name: 'Jiki Jiki no Mi',              tier: 'Mid',    ing: [['minecraft:iron_block',8],['minecraft:redstone_block',8],['minecraft:compass',1]] },
  { fruit: 'grandline:soru_soru_fruit', name: 'Soru Soru no Mi',              tier: 'Late',   ing: [['minecraft:soul_sand',16],['minecraft:bone',24],['minecraft:ghast_tear',6],['minecraft:golden_apple',1]] },
  { fruit: 'grandline:ope_ope_fruit',   name: 'Ope Ope no Mi',                tier: 'Mid',    ing: [['minecraft:prismarine_crystals',16],['minecraft:golden_carrot',4],['minecraft:glistering_melon_slice',2]] },
  { fruit: 'grandline:pika_pika_fruit', name: 'Pika Pika no Mi',              tier: 'Late',   ing: [['minecraft:glowstone',16],['minecraft:gold_block',4],['minecraft:diamond',8]] },
  { fruit: 'grandline:yami_yami_fruit', name: 'Yami Yami no Mi',              tier: 'Mythic', ing: [['minecraft:obsidian',24],['minecraft:ink_sac',24],['minecraft:wither_rose',6],['minecraft:crying_obsidian',4]] },
  { fruit: 'grandline:tori_tori_fruit', name: 'Tori Tori no Mi (Phoenix)',    tier: 'Late',   ing: [['minecraft:blaze_rod',12],['minecraft:feather',24],['minecraft:lapis_lazuli',16],['minecraft:golden_apple',2]] },
  { fruit: 'grandline:gura_gura_fruit', name: 'Gura Gura no Mi',              tier: 'Mythic', ing: [['minecraft:tnt',32],['minecraft:anvil',3],['minecraft:obsidian',16]] },
  { fruit: 'grandline:uo_uo_fruit',     name: 'Uo Uo no Mi: Model Seiryu',    tier: 'Mythic', ing: [['minecraft:dragon_breath',12],['minecraft:prismarine_shard',24],['minecraft:blaze_rod',8],['minecraft:heart_of_the_sea',1]] },
  { fruit: 'grandline:sara_sara_fruit', name: 'Sara Sara no Mi: Model Axolotl', tier: 'Late',  ing: [['minecraft:slime_ball',24],['minecraft:pink_dye',8],['minecraft:fermented_spider_eye',4],['minecraft:tropical_fish',4]] },
  { fruit: 'grandline:mochi_mochi_fruit', name: 'Mochi Mochi no Mi',            tier: 'Late',   ing: [['minecraft:sugar',32],['minecraft:wheat',24],['minecraft:cake',1],['minecraft:diamond',4]] },
  { fruit: 'grandline:erase_fruit',     name: 'Erase Fruit (removes powers)', tier: 'Special', ing: [['minecraft:milk_bucket',1],['minecraft:bone_meal',16],['minecraft:ghast_tear',2]] }
]
// which fruit-identity tag maps to which (base) power - used to remember and
// restore your Devil Fruit power if anything ever strips it (e.g. death bugs)
var GL_TAG2POWER = {
  gl_df_gomu: 'grandline:gomu', gl_df_ope: 'grandline:ope', gl_df_tori: 'grandline:phoenix',
  gl_df_mera: 'grandline:mera', gl_df_hie: 'grandline:hie', gl_df_ito: 'grandline:ito',
  gl_df_mero: 'grandline:mero', gl_df_jiki: 'grandline:jiki', gl_df_soru: 'grandline:soru',
  gl_df_yami: 'grandline:yami', gl_df_pika: 'grandline:pika', gl_df_uo: 'grandline:seiryu',
  gl_df_gura: 'grandline:gura', gl_df_sara: 'grandline:sara', gl_df_mochi: 'grandline:mochi'
}
// transient tags stamped by power JSONs every tick; the script consumes then clears them
var GL_TRANSIENT = [
  'gl_df_user','gl_df_gomu','gl_df_ope','gl_df_tori','gl_df_mera','gl_df_hie','gl_df_ito',
  'gl_df_mero','gl_df_jiki','gl_df_soru','gl_df_yami','gl_df_pika','gl_df_uo','gl_df_gura','gl_df_sara',
  'gl_df_mochi','gl_rubber','gl_in_g2','gl_in_g3','gl_in_g4','gl_in_g5','gl_loki_on'
]

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
function glHasTag(e, t) { try { return e.tags.contains(t) } catch (err) { return false } }
function glClearTag(e, t) {
  try { e.removeTag(t) } catch (err) { try { e.tags.remove(t) } catch (err2) {} }
}
function glCount(p, id) {
  try { return p.inventory.count(id) } catch (err) {}
  try {
    var total = 0
    for (var i = 0; i < p.inventory.size; i++) {
      var st = p.inventory.getStackInSlot(i)
      if (st && ('' + st.id) === id) total += st.count
    }
    return total
  } catch (err) { return 0 }
}
function glItemName(id) {
  var s = ('' + id).split(':')[1] || ('' + id)
  return s.replace(/_/g, ' ')
}

// ---------------------------------------------------------------------------
// scoreboards
// ---------------------------------------------------------------------------
ServerEvents.loaded(event => {
  var s = event.server
  s.runCommandSilent('scoreboard objectives add gl_kills dummy "Grand Kills"')
  s.runCommandSilent('scoreboard objectives add gl_will dummy "Will"')
  console.log('[Grand Line] scoreboards ready')
})

// ---------------------------------------------------------------------------
// kills -> Grand Kills + Will
// ---------------------------------------------------------------------------
EntityEvents.death(event => {
  try {
    var src = event.source
    if (!src) return
    var killer = src.player
    if (!killer || !killer.isPlayer()) return
    // NOTE: count kills for slaying OTHER PLAYERS too (PvP) - only skip if the
    // dead entity IS the killer (suicide). Kills only count once the killer has
    // the Ambition/Will power (gl_will_user, stamped per-tick by that power).
    if (event.entity === killer) return
    if (!glHasTag(killer, 'gl_will_user')) return
    // per-player only: @s scoreboard + persistentData source of truth. NEVER @a.
    var glKpd = killer.persistentData
    var glK = glKpd.getInt('gl_kills_pd') + 1
    var glW = glKpd.getInt('gl_will_pd') + global.glConfig.willPerKill
    glKpd.putInt('gl_kills_pd', glK)
    glKpd.putInt('gl_will_pd', glW)
    killer.runCommandSilent('scoreboard players set @s gl_kills ' + glK)
    killer.runCommandSilent('scoreboard players add @s gl_will ' + global.glConfig.willPerKill)
    try {
      if (event.entity.maxHealth >= 50) {
        glKpd.putInt('gl_will_pd', glKpd.getInt('gl_will_pd') + global.glConfig.willBossBonus)
        killer.runCommandSilent('scoreboard players add @s gl_will ' + global.glConfig.willBossBonus)
        killer.tell(Text.gray('A mighty foe falls... your Will surges.'))
      }
    } catch (err) {}
  } catch (err) {}
})

// ---------------------------------------------------------------------------
// hurt hooks: rubber fall immunity + phoenix Undying Flame
// ---------------------------------------------------------------------------
EntityEvents.hurt(event => {
  try {
    var e = event.entity
    if (!e.isPlayer()) return
    var type = '' + event.source.type
    if (type === 'fall' && glHasTag(e, 'gl_rubber')) {
      event.cancel()
      return
    }
    if (glHasTag(e, 'gl_phoenix_form') && event.damage >= e.health) {
      if (e.persistentData.getInt('gl_undying_cd') <= 0) {
        event.cancel()
        e.health = 8
        e.persistentData.putInt('gl_undying_cd', global.glConfig.undyingCooldown)
        e.runCommandSilent('effect give @s minecraft:regeneration 10 2 true')
        e.runCommandSilent('effect give @s minecraft:absorption 20 1 true')
        e.runCommandSilent('execute at @s run particle minecraft:soul_fire_flame ~ ~1 ~ 1.5 1.5 1.5 0.15 120')
        e.runCommandSilent('execute at @s run particle minecraft:flash ~ ~1 ~ 0 0 0 0 1')
        e.runCommandSilent('playsound minecraft:item.totem.use master @a ~ ~ ~ 1 1.4')
        e.runCommandSilent('playsound minecraft:entity.blaze.ambient master @a ~ ~ ~ 1 0.6')
        e.tell(Text.aqua('The Undying Flame refuses to var you fall. (5 min cooldown)'))
      }
    }
  } catch (err) {}
})

// ---------------------------------------------------------------------------
// respawn: schedule the power-restore check (see tick loop)
// ---------------------------------------------------------------------------
PlayerEvents.respawned(event => {
  try {
    var rp = event.player
    rp.persistentData.putInt('gl_regrant', 60)
    // death exits any form: swap form powers back to base + reset the scale
    var GL_FORM2BASE = { 'grandline:sara_form':'grandline:sara', 'grandline:seiryu_form':'grandline:seiryu', 'grandline:phoenix_form':'grandline:phoenix' }
    for (var fpw in GL_FORM2BASE) {
      rp.runCommandSilent('execute if entity @s run superpower remove ' + fpw + ' @s')
    }
    rp.runCommandSilent('scale set pehkui:base 1 @s')
    for (var ft of ['gl_scaled','gl_sara_form','gl_dragon_form','gl_phoenix_form','gl_loki_form','gl_loki_base']) {
      rp.runCommandSilent('tag @s remove ' + ft)
    }
  } catch (err) {}
})

// ---------------------------------------------------------------------------
// eating a Devil Fruit
// ---------------------------------------------------------------------------
ItemEvents.rightClicked(event => {
  if (!event.server) return
  var p = event.player
  var id = '' + event.item.id
  var hand = ('' + event.hand).toLowerCase()
  if (hand.indexOf('main') < 0) return

  // Only fruit items past this point - and eating requires SNEAK so a fruit
  // can be held, admired, stockpiled or swapped without accidentally eating it.
  var isFruit = (id === 'grandline:erase_fruit') || (GL_FRUITS[id] != null)
  if (!isFruit) return
  if (!p.isCrouching()) {
    p.tell(Text.gray('Sneak + right-click to eat this Devil Fruit. (Hold it normally to keep it.)'))
    return
  }

  // ---- THE ERASE FRUIT: strips every Devil Fruit power, no questions asked ----
  if (id === 'grandline:erase_fruit') {
    p.runCommandSilent('clear @s grandline:erase_fruit 1')
    for (var i = 0; i < GL_FRUIT_POWERS.length; i++) {
      p.runCommandSilent('superpower remove ' + GL_FRUIT_POWERS[i] + ' @s')
    }
    for (var t in GL_TAG2POWER) { p.runCommandSilent('tag @s remove ' + t) }
    for (var t of ['gl_df_user','gl_mera_form','gl_phoenix_form','gl_dragon_form','gl_sara_form','gl_kikoku_drawn','gl_room_on','gl_in_g2','gl_in_g3','gl_in_g4','gl_in_g5','gl_scaled','gl_chibi','gl_rubber']) {
      p.runCommandSilent('tag @s remove ' + t)
    }
    p.runCommandSilent('scale set pehkui:base 1 @s')
    p.persistentData.putString('gl_power', '')
    p.persistentData.putInt('gl_df_seen', 0)
    p.runCommandSilent('execute at @s run particle minecraft:poof ~ ~1 ~ 0.8 1 0.8 0.08 60')
    p.runCommandSilent('execute at @s run particle minecraft:end_rod ~ ~1 ~ 0.8 1 0.8 0.05 30')
    p.runCommandSilent('playsound minecraft:item.bucket.empty master @a ~ ~ ~ 1 0.7')
    p.runCommandSilent('playsound minecraft:block.beacon.deactivate master @a ~ ~ ~ 1 1.5')
    p.tell(Text.gray('The Erase Fruit tastes like... nothing. The sea releases its claim on you.'))
    p.tell(Text.darkGray('All Devil Fruit powers are gone. You may eat a new fruit.'))
    return
  }

  var fruit = GL_FRUITS[id]
  if (!fruit) return

  if (glHasTag(p, 'gl_df_user') || p.persistentData.getInt('gl_df_seen') > 0) {
    // ============ THE SECOND FRUIT ============
    p.runCommandSilent('clear @s ' + id + ' 1')
    if (global.glConfig.secondFruitMode === 'funny') {
      p.runCommandSilent('effect give @s minecraft:levitation 10 4 true')
      p.runCommandSilent('effect give @s minecraft:nausea 30 1 true')
      p.runCommandSilent('effect give @s minecraft:glowing 30 0 true')
      p.runCommandSilent('execute at @s run particle minecraft:explosion ~ ~1 ~ 0.5 0.8 0.5 0 10')
      p.runCommandSilent('playsound minecraft:entity.generic.explode master @a ~ ~ ~ 1 1.6')
      p.runCommandSilent('playsound minecraft:block.glass.break master @a ~ ~ ~ 1 0.5')
      p.tell(Text.lightPurple('Your stomach VIOLENTLY disagrees with a second fruit. Up you go.'))
    } else {
      p.runCommandSilent('execute at @s run particle minecraft:explosion_emitter ~ ~1 ~ 1 1 1 0 3')
      p.runCommandSilent('execute at @s run particle minecraft:squid_ink ~ ~1 ~ 1 1.5 1 0.2 80')
      p.runCommandSilent('playsound minecraft:entity.wither.death master @a ~ ~ ~ 1 0.4')
      p.runCommandSilent('playsound minecraft:entity.elder_guardian.curse master @a ~ ~ ~ 1 0.6')
      for (var i = 0; i < GL_FRUIT_POWERS.length; i++) {
        p.runCommandSilent('superpower remove ' + GL_FRUIT_POWERS[i] + ' @s')
      }
      p.runCommandSilent('tellraw @a {"text":"' + p.username + ' ate a second Devil Fruit. The sea devils tore their body apart.","color":"dark_red","italic":true}')
      p.persistentData.putInt('gl_df_seen', 0)
      p.persistentData.putString('gl_power', '')
      p.runCommandSilent('kill @s')
    }
    return
  }

  // first fruit: grant the power
  p.runCommandSilent('clear @s ' + id + ' 1')
  p.persistentData.putInt('gl_df_seen', 10)
  p.persistentData.putString('gl_power', fruit.power)
  p.runCommandSilent('superpower add ' + fruit.power + ' @s')
  if (fruit.power === 'grandline:ope') {
    p.give('grandline:kikoku')
    p.tell(Text.darkPurple('A cursed blade materializes... Kikoku belongs to you now.'))
  }
  if (fruit.power === 'grandline:yami') {
    p.give('grandline:flintlock')
    p.tell(Text.of('Darkness clings to you - and a captain\'s flintlock finds your hand. Zehahaha!').darkGray())
  }
  if (fruit.power === 'grandline:seiryu') {
    p.give('grandline:kanabo')
    p.tell(Text.of('The Azure Dragon stirs - and a colossal spiked kanabo lands in your grip.').aqua())
  }
  if (fruit.power === 'grandline:mochi') {
    p.give('grandline:mogura')
    p.tell(Text.of('Your body turns to mochi - and the trident Mogura settles into your hand.').lightPurple())
  }
  p.runCommandSilent('effect give @s minecraft:hunger 10 1 true')
  p.runCommandSilent('execute at @s run particle minecraft:portal ~ ~1 ~ 0.8 1 0.8 0.5 60')
  p.runCommandSilent('execute at @s run particle minecraft:witch ~ ~1.5 ~ 0.5 0.5 0.5 0.05 20')
  p.runCommandSilent('playsound minecraft:entity.lightning_bolt.thunder master @a ~ ~ ~ 0.7 0.6')
  p.runCommandSilent('playsound minecraft:entity.player.burp master @a ~ ~ ~ 1 0.7')
  p.tell(Text.of('It tastes like despair boiled in seawater... and then power floods in.').gold())
  p.tell(Text.of('The ' + fruit.name + ' has chosen you. The sea will hate you forever.').gray().italic())
})

// ---------------------------------------------------------------------------
// Sea Prism Altar
// ---------------------------------------------------------------------------
BlockEvents.rightClicked(event => {
  try {
    if (!event.server) return
    if (('' + event.block.id) !== 'grandline:sea_prism_altar') return
    var hand = ('' + event.hand).toLowerCase()
    if (hand.indexOf('main') < 0) return
    var p = event.player
    // NOTE: no event.cancel() here - in KubeJS it THROWS (EventExit) and a
    // surrounding try/catch would swallow the rest of the handler

    var idx = p.persistentData.getInt('gl_attune')
    if (idx < 0 || idx >= GL_ALTAR.length) idx = 0

    if (p.isCrouching()) {
      idx = (idx + 1) % GL_ALTAR.length
      p.persistentData.putInt('gl_attune', idx)
    }
    var rec = GL_ALTAR[idx]
    var holdingCore = ('' + p.mainHandItem.id) === 'grandline:devils_core'

    // recipe status readout
    p.tell(Text.of('== Sea Prism Altar == attuned: ').gray().append(Text.of(rec.name).aqua().bold()).append(Text.of('  [' + rec.tier + ']').darkGray()))
    var allOk = glCount(p, 'grandline:devils_core') >= 1
    var line = (allOk ? Text.green(' [OK] ') : Text.red(' [--] ')).append(Text.of("1x devil's core").white())
    p.tell(line)
    for (var i = 0; i < rec.ing.length; i++) {
      var need = rec.ing[i]
      var have = glCount(p, need[0])
      var ok = have >= need[1]
      if (!ok) allOk = false
      p.tell((ok ? Text.green(' [OK] ') : Text.red(' [--] ')).append(Text.of(need[1] + 'x ' + glItemName(need[0]) + '  (' + have + ')').white()))
    }

    if (p.isCrouching()) {
      p.tell(Text.darkGray('(sneak-click cycles the attuned fruit; click while holding a Devil\'s Core to forge)'))
      p.runCommandSilent('playsound minecraft:block.amethyst_block.chime master @a ~ ~ ~ 1 1.2')
      return
    }
    if (!holdingCore) {
      p.tell(Text.darkGray('(hold a Devil\'s Core in your main hand and click to forge the attuned fruit)'))
      return
    }
    if (!allOk) {
      p.runCommandSilent('playsound minecraft:block.beacon.deactivate master @a ~ ~ ~ 1 0.7')
      p.tell(Text.red('The altar stays silent. Gather everything on the list first.'))
      return
    }
    // consume + forge
    p.runCommandSilent('clear @s grandline:devils_core 1')
    for (var i = 0; i < rec.ing.length; i++) {
      p.runCommandSilent('clear @s ' + rec.ing[i][0] + ' ' + rec.ing[i][1])
    }
    var pos = event.block.pos
    var server = event.server
    server.runCommandSilent('execute positioned ' + pos.x + ' ' + (pos.y + 1) + ' ' + pos.z + ' run particle minecraft:flash ~0.5 ~0.5 ~0.5 0 0 0 0 2')
    server.runCommandSilent('execute positioned ' + pos.x + ' ' + (pos.y + 1) + ' ' + pos.z + ' run particle minecraft:dust 0.25 0.79 0.75 2.0 ~0.5 ~1 ~0.5 1 1.5 1 0 80')
    server.runCommandSilent('execute positioned ' + pos.x + ' ' + (pos.y + 1) + ' ' + pos.z + ' run particle minecraft:bubble_pop ~0.5 ~1 ~0.5 1 1 1 0.1 60')
    server.runCommandSilent('execute positioned ' + pos.x + ' ' + (pos.y + 1) + ' ' + pos.z + ' run particle minecraft:witch ~0.5 ~1.5 ~0.5 0.6 0.8 0.6 0.05 40')
    p.runCommandSilent('playsound minecraft:entity.lightning_bolt.thunder master @a ~ ~ ~ 1 0.8')
    p.runCommandSilent('playsound minecraft:block.beacon.power_select master @a ~ ~ ~ 1 0.6')
    p.runCommandSilent('playsound minecraft:block.conduit.activate master @a ~ ~ ~ 1 0.8')
    p.give(rec.fruit)
    p.tell(Text.of('The sea trembles... the ').gold().append(Text.of(rec.name).aqua().bold()).append(Text.of(' takes form!').gold()))
  } catch (err) {
    console.log('[Grand Line] altar error: ' + err)
  }
})

// ---------------------------------------------------------------------------
// main tick loop
// ---------------------------------------------------------------------------
ServerEvents.tick(event => {
  var server = event.server
  if (server.tickCount % 2 !== 0) return   // run every 2 ticks

  var players = server.players
  for (var p of players) {
    try {
      var pd = p.persistentData

      // one-time Ambition grant
      if (global.glConfig.autoGrantAmbition && !pd.getBoolean('gl_ambition')) {
        p.runCommandSilent('superpower add grandline:ambition @s')
        pd.putBoolean('gl_ambition', true)
        p.tell(Text.gold('Something stirs within you... (Ambition unlocked - check your Powers menu)'))
      }

      // pd countdowns
      for (var key of ['gl_undying_cd', 'gl_exhaust_left', 'gl_chibi_left', 'gl_vortex_left']) {
        var v = pd.getInt(key)
        if (v > 0) pd.putInt(key, v - 2)
      }

      // Phoenix creative-style FLIGHT while transformed (double-tap space to fly).
      // Granted via vanilla mayfly so it works everywhere; revoked on revert/death.
      try {
        var glProt = false
        try { glProt = p.isCreative() || p.isSpectator() } catch (e) {}
        if (!glProt) {
          var glAb = p.getAbilities()
          if (glHasTag(p, 'gl_phoenix_form')) {
            if (!glAb.mayfly) { glAb.mayfly = true; p.onUpdateAbilities() }
            pd.putBoolean('gl_phoenix_fly', true)
          } else if (pd.getBoolean('gl_phoenix_fly')) {
            glAb.mayfly = false; glAb.flying = false; p.onUpdateAbilities()
            pd.putBoolean('gl_phoenix_fly', false)
          }
        }
      } catch (glErr) {}

      // exhaustion tag lifecycle
      if (glHasTag(p, 'gl_exhausted')) {
        if (pd.getInt('gl_exhaust_left') <= 0 && !pd.getBoolean('gl_exhaust_run')) {
          pd.putInt('gl_exhaust_left', global.glConfig.exhaustTicks)
          pd.putBoolean('gl_exhaust_run', true)
        } else if (pd.getBoolean('gl_exhaust_run') && pd.getInt('gl_exhaust_left') <= 0) {
          p.runCommandSilent('tag @s remove gl_exhausted')
          pd.putBoolean('gl_exhaust_run', false)
          p.tell(Text.gray('You catch your breath. The Gears answer again.'))
        }
      } else {
        pd.putBoolean('gl_exhaust_run', false)
      }

      // chibi (post Gear-3) lifecycle
      if (glHasTag(p, 'gl_chibi')) {
        if (!pd.getBoolean('gl_chibi_run')) {
          pd.putInt('gl_chibi_left', global.glConfig.chibiTicks)
          pd.putBoolean('gl_chibi_run', true)
        } else if (pd.getInt('gl_chibi_left') <= 0) {
          p.runCommandSilent('scale set pehkui:base 1 @s')
          p.runCommandSilent('tag @s remove gl_chibi')
          pd.putBoolean('gl_chibi_run', false)
          p.tell(Text.lightPurple('Pop! You reinflate to normal size.'))
        }
      } else {
        pd.putBoolean('gl_chibi_run', false)
      }

      // gear overdrive timers
      if (glHasTag(p, 'gl_in_g4')) {
        var t = pd.getInt('gl_g4_t') + 2
        pd.putInt('gl_g4_t', t)
        if (t > global.glConfig.gearTimeoutG4) {
          pd.putInt('gl_g4_t', 0)
          p.runCommandSilent('superpower remove grandline:gomu_g4 @s')
          p.runCommandSilent('superpower add grandline:gomu @s')
          p.runCommandSilent('scale set pehkui:base 1 @s')
          p.runCommandSilent('tag @s remove gl_scaled')
          p.runCommandSilent('tag @s add gl_exhausted')
          p.runCommandSilent('effect give @s minecraft:slowness 20 2 true')
          p.runCommandSilent('effect give @s minecraft:weakness 20 2 true')
          p.tell(Text.gray('Gear Fourth gives out on its own... exhaustion takes you.'))
        }
      } else { pd.putInt('gl_g4_t', 0) }
      if (glHasTag(p, 'gl_in_g5')) {
        var t = pd.getInt('gl_g5_t') + 2
        pd.putInt('gl_g5_t', t)
        if (server.tickCount % 30 === 0) {
          p.runCommandSilent('playsound minecraft:entity.warden.heartbeat master @a[distance=..24] ~ ~ ~ 0.7 1.7')
        }
        if (t > global.glConfig.gearTimeoutG5) {
          pd.putInt('gl_g5_t', 0)
          p.runCommandSilent('superpower remove grandline:gomu_g5 @s')
          p.runCommandSilent('superpower add grandline:gomu @s')
          p.runCommandSilent('scale set pehkui:base 1 @s')
          p.runCommandSilent('tag @s remove gl_scaled')
          p.runCommandSilent('tag @s add gl_exhausted')
          p.runCommandSilent('effect give @s minecraft:slowness 30 3 true')
          p.runCommandSilent('effect give @s minecraft:weakness 30 3 true')
          p.tell(Text.gray('The drums of liberation fade... your body demands rest.'))
        }
      } else { pd.putInt('gl_g5_t', 0) }

      // scale sanity REMOVED: it was the only thing that ever shrank players
      // mid-form and it kept racing the form tags. Forms reset their own
      // scale in last_tick; gears reset in the timer above; erase resets too.

      // Loki passive giant size: while the Regalia power stamps gl_loki_on
      // (transient), keep the base 1.6x size; when it stops for 100 straight
      // ticks (gear removed), shrink back.
      if (glHasTag(p, 'gl_loki_on')) {
        pd.putInt('gl_loki_off', 0)
        if (!glHasTag(p, 'gl_loki_base')) {
          p.runCommandSilent('tag @s add gl_loki_base')
          if (!glHasTag(p, 'gl_loki_form')) p.runCommandSilent('scale set pehkui:base 1.6 @s')
        }
      } else if (glHasTag(p, 'gl_loki_base')) {
        var glLokiOff = pd.getInt('gl_loki_off') + 2
        pd.putInt('gl_loki_off', glLokiOff)
        if (glLokiOff > 100) {
          pd.putInt('gl_loki_off', 0)
          p.runCommandSilent('tag @s remove gl_loki_base')
          if (!glHasTag(p, 'gl_loki_form')) p.runCommandSilent('scale set pehkui:base 1 @s')
        }
      }

      // fruit-ownership memory (covers tag-cleanup gaps for the eat check)
      // + remember WHICH fruit, so we can restore it after death if needed
      if (glHasTag(p, 'gl_df_user')) {
        pd.putInt('gl_df_seen', 10)
        for (var t in GL_TAG2POWER) {
          if (glHasTag(p, t)) { pd.putString('gl_power', GL_TAG2POWER[t]); break }
        }
      } else {
        var seen = pd.getInt('gl_df_seen')
        if (seen > 0) pd.putInt('gl_df_seen', seen - 2)
      }

      // death insurance: shortly after respawn, if the remembered fruit power
      // is not ticking its markers anymore, grant it back
      var regrant = pd.getInt('gl_regrant')
      if (regrant > 0) {
        pd.putInt('gl_regrant', regrant - 2)
        if (regrant <= 4) {
          var owned = pd.getString('gl_power')
          if (owned && owned.length > 0 && pd.getInt('gl_df_seen') <= 0 && !glHasTag(p, 'gl_df_user')) {
            p.runCommandSilent('superpower add ' + owned + ' @s')
            p.tell(Text.gray('The sea could not take it from you - your Devil Fruit power endures.'))
          }
          pd.putInt('gl_regrant', 0)
        }
      }

      // soulbound Kikoku: an Ope user who lost the blade gets it back
      if (server.tickCount % 200 === 0 && glHasTag(p, 'gl_df_ope')) {
        if ((glCount(p, 'grandline:kikoku') + glCount(p, 'grandline:kikoku_drawn')) <= 0) {
          p.give('grandline:kikoku')
          p.tell(Text.darkPurple('Kikoku finds its way back to its master.'))
        }
      }
      // soulbound flintlock: a Yami user who lost the gun gets it back
      if (server.tickCount % 200 === 0 && glHasTag(p, 'gl_df_yami')) {
        if (glCount(p, 'grandline:flintlock') <= 0) {
          p.give('grandline:flintlock')
          p.tell(Text.darkGray('Your captain\'s flintlock returns to your grip.'))
        }
      }
      // soulbound kanabo: a Seiryu (Kaido) user who lost the club gets it back
      if (server.tickCount % 200 === 0 && glHasTag(p, 'gl_df_uo')) {
        if (glCount(p, 'grandline:kanabo') <= 0) {
          p.give('grandline:kanabo')
          p.tell(Text.aqua('Your kanabo thunders back into your hand.'))
        }
      }
      // soulbound Mogura: a Mochi (Katakuri) user who lost the trident gets it back
      if (server.tickCount % 200 === 0 && glHasTag(p, 'gl_df_mochi')) {
        if (glCount(p, 'grandline:mogura') <= 0) {
          p.give('grandline:mogura')
          p.tell(Text.lightPurple('Mogura reforms from mochi in your grip.'))
        }
      }

      // sea-water curse
      if (global.glConfig.seaCurse && server.tickCount % 10 === 0 && glHasTag(p, 'gl_df_user')) {
        var wet = false
        try { wet = p.isInWater() } catch (err) { try { wet = p.inWater } catch (err2) {} }
        if (wet) {
          p.runCommandSilent('effect give @s minecraft:weakness 3 1 true')
          p.runCommandSilent('effect give @s minecraft:slowness 3 2 true')
          p.runCommandSilent('effect give @s minecraft:mining_fatigue 3 1 true')
          p.runCommandSilent('execute at @s run particle minecraft:bubble ~ ~1 ~ 0.4 0.6 0.4 0.02 6')
          if (server.tickCount % 60 === 0) {
            p.runCommandSilent('title @s actionbar {"text":"The sea drains your strength...","color":"dark_aqua","italic":true}')
          }
          if (glHasTag(p, 'gl_mera_form')) {
            p.runCommandSilent('effect give @s minecraft:weakness 5 3 true')
            p.runCommandSilent('execute at @s run particle minecraft:cloud ~ ~1 ~ 0.5 0.5 0.5 0.05 10')
            p.runCommandSilent('playsound minecraft:block.fire.extinguish master @a ~ ~ ~ 0.6 1')
          }
        }
      }

      // shambles (Law): swap with nearest living non-player within 10
      if (glHasTag(p, 'gl_do_shambles')) {
        p.runCommandSilent('tag @s remove gl_do_shambles')
        var box = p.boundingBox.inflate(10, 6, 10)
        var best = null, bestD = 1e9
        for (var e of p.level.getEntitiesWithin(box)) {
          try {
            if (!e.isLiving() || e.isPlayer()) continue
            var d = (e.x - p.x) * (e.x - p.x) + (e.y - p.y) * (e.y - p.y) + (e.z - p.z) * (e.z - p.z)
            if (d < bestD) { bestD = d; best = e }
          } catch (err) {}
        }
        if (best) {
          var px = p.x, py = p.y, pz = p.z
          var ex = best.x, ey = best.y, ez = best.z
          best.teleportTo(px, py, pz)
          p.teleportTo(ex, ey, ez)
          p.runCommandSilent('execute at @s run particle minecraft:end_rod ~ ~1 ~ 0.4 0.8 0.4 0.05 25')
          p.runCommandSilent('execute positioned ' + px + ' ' + py + ' ' + pz + ' run particle minecraft:end_rod ~ ~1 ~ 0.4 0.8 0.4 0.05 25')
          p.runCommandSilent('playsound minecraft:entity.enderman.teleport master @a ~ ~ ~ 1 1.6')
        } else {
          p.tell(Text.red('No target inside the ROOM to swap with.'))
        }
      }

      // yami vortex start
      if (glHasTag(p, 'gl_vortex_start')) {
        p.runCommandSilent('tag @s remove gl_vortex_start')
        pd.putInt('gl_vortex_left', 60)
      }

      // ---------------- entity effects around this player ----------------
      var box = p.boundingBox.inflate(16, 8, 16)
      var vortexOn = pd.getInt('gl_vortex_left') > 0
      for (var e of p.level.getEntitiesWithin(box)) {
        try {
          if (e.isPlayer() || !e.isLiving()) continue
          var epd = e.persistentData

          if (glHasTag(e, 'gl_puppet')) {
            glClearTag(e, 'gl_puppet')
            epd.putInt('gl_pull_t', 30)
            e.mergeNbt({ NoAI: true })
            epd.putInt('gl_noai_until', server.tickCount + 30)
          }
          if (glHasTag(e, 'gl_mag_pull')) {
            glClearTag(e, 'gl_mag_pull')
            epd.putInt('gl_pull_t', 24)
          }
          if (glHasTag(e, 'gl_petrified')) {
            glClearTag(e, 'gl_petrified')
            e.mergeNbt({ NoAI: true })
            epd.putInt('gl_noai_until', server.tickCount + 120)
            server.runCommandSilent('execute positioned ' + e.x + ' ' + e.y + ' ' + e.z + ' run particle minecraft:dust 0.6 0.6 0.6 1.6 ~ ~1 ~ 0.4 0.8 0.4 0 20')
            server.runCommandSilent('execute positioned ' + e.x + ' ' + e.y + ' ' + e.z + ' run playsound minecraft:block.stone.place master @a ~ ~ ~ 1 0.6')
          }
          if (glHasTag(e, 'gl_petrified_brief')) {
            glClearTag(e, 'gl_petrified_brief')
            e.mergeNbt({ NoAI: true })
            epd.putInt('gl_noai_until', server.tickCount + 40)
          }
          if (glHasTag(e, 'gl_kings_stun')) {
            glClearTag(e, 'gl_kings_stun')
            if (e.maxHealth <= 20) {
              e.mergeNbt({ NoAI: true })
              epd.putInt('gl_noai_until', server.tickCount + 160)
              server.runCommandSilent('execute positioned ' + e.x + ' ' + e.y + ' ' + e.z + ' run particle minecraft:crit ~ ~1.5 ~ 0.3 0.3 0.3 0.1 10')
            }
          }
          if (glHasTag(e, 'gl_homie')) {
            var ht = epd.getInt('gl_homie_t')
            if (ht === 0) epd.putInt('gl_homie_t', 1200)
            else if (ht <= 2) {
              server.runCommandSilent('execute positioned ' + e.x + ' ' + e.y + ' ' + e.z + ' run particle minecraft:sculk_soul ~ ~1 ~ 0.5 0.8 0.5 0.08 40')
              try { e.discard() } catch (err) { e.kill() }
              continue
            } else epd.putInt('gl_homie_t', ht - 2)
          }

          // pull toward player (parasite / magnet / vortex)
          var pull = epd.getInt('gl_pull_t')
          if (pull > 0 || vortexOn) {
            if (pull > 0) epd.putInt('gl_pull_t', pull - 2)
            var dx = p.x - e.x, dy = (p.y + 1) - e.y, dz = p.z - e.z
            var dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
            if (dist > 1.5 && dist < 16) {
              var f = vortexOn ? 0.45 : 0.35
              e.setMotion(dx / dist * f, Math.min(dy / dist * f + 0.05, 0.4), dz / dist * f)
            }
          }

          // NoAI restore - ABSOLUTE expiry (server.tickCount based) so a stunned
          // mob always frees itself once the time is up, even if the caster
          // walked away and the old per-tick countdown had frozen. Fixes the
          // "emperor stun never goes away" report.
          var noaiUntil = epd.getInt('gl_noai_until')
          if (noaiUntil > 0 && server.tickCount >= noaiUntil) {
            e.mergeNbt({ NoAI: false })
            epd.putInt('gl_noai_until', 0)
          }
        } catch (err) {}
      }
      if (vortexOn && pd.getInt('gl_vortex_left') <= 2) {
        p.runCommandSilent('execute at @s run particle minecraft:squid_ink ~ ~1 ~ 2 1.5 2 0.15 60')
        p.runCommandSilent('playsound minecraft:entity.generic.explode master @a ~ ~ ~ 0.8 0.6')
      }

      // transient tag cleanup (powers re-stamp every tick)
      for (var t of GL_TRANSIENT) {
        if (glHasTag(p, t)) p.runCommandSilent('tag @s remove ' + t)
      }
    } catch (err) {}
  }
})

console.log('[Grand Line] Server script v1 loaded')
