Script dei mobile
Questa pagina fa parte di Scrivere script Lua. Le funzioni che uno script chiama sul proprio NPC sono nel
riferimento: npc e mobile.
Ogni hook che il server chiama, con i suoi argomenti e se viene eseguito dentro l’azione, è nell’unico elenco degli
hook degli script.
Un template di mobile indica il proprio script con script_id, il nome di una tabella Lua globale
definita da scripts/mobiles/<script_id>.lua. Il server carica ogni *.lua direttamente
in quella directory all’avvio, in ordine di nome, dopo init.lua; uno script che fallisce
il caricamento viene segnalato come qualsiasi errore di script e il server parte con gli altri.
[[mobile]]id = "cat"name = "a cat"body = 201script_id = "wander"La tabella può definire queste funzioni; ciascuna è opzionale:
| Funzione | Quando |
|---|---|
on_think(serial) |
A ogni ciclo di pensiero dell’NPC: ogni ultima.npcs.think_interval_ms (500 ms per impostazione predefinita) mentre un giocatore si trova nei settori 5×5 attorno a lui; vedi Tick degli NPC. Un pensiero è istantaneo, come in ModernUO: non deve chiamare wait (il server avvisa una volta per script), quindi mantieni la temporizzazione nello script, per esempio contando i pensieri. |
on_speech(serial, speaker, text, keywords, type) |
Quando un giocatore pronuncia text a portata d’orecchio (i comandi non vengono ascoltati): 15 caselle a voce normale o come emote, 1 casella per un sussurro, 18 per un urlo. speaker è il seriale del giocatore; keywords sono le parole chiave del parlato individuate dal client, un array di numeri qualunque sia la sua lingua, come SpeechKeywordType.Bank; type è il modo in cui è stato detto, un numero da confrontare con SpeechType.Regular, SpeechType.Emote, SpeechType.Whisper o SpeechType.Yell. Può chiamare wait. |
on_spawn(serial) |
Una volta, subito dopo la generazione dell’NPC (.spawn), nel mondo con i suoi oggetti e già mostrato, prima di qualsiasi altra funzione del suo script. Non quando gli NPC salvati vengono caricati all’avvio. Può chiamare wait. |
on_mobile_in_range(serial, other) |
Ogni volta che un altro mobile, giocatore o NPC, entra entro ultima.npcs.sense_range caselle (8 per impostazione predefinita, un quadrato lungo X e Y) tramite un passo o entrando nel mondo. Una volta per arrivo: si attiva di nuovo solo dopo che il mobile ha lasciato il raggio ed è tornato. In entrambe le direzioni: anche un NPC che cammina verso un mobile lo percepisce. Gli NPC caricati insieme all’avvio non si percepiscono finché uno non esce dal raggio e torna. other è il suo seriale; npc.name(other) restituisce nil per un giocatore. Può chiamare wait. |
on_mobile_killed(serial, killed, killer) |
Quando un mobile, giocatore o NPC, viene ucciso entro ultima.npcs.sense_range celle da questo NPC (8 per impostazione predefinita, un quadrato lungo X e Y), escluso chi è morto. serial è l’NPC che viene avvisato, killed il seriale di chi è morto e killer il seriale di chi lo ha ucciso, oppure nil quando non lo ha fatto nessuno. npc.name(killed) restituisce nil per un giocatore. Un NPC che sta morendo non viene avvisato. |
on_death(serial, corpse, killer) |
Quando l’NPC muore (Morte e resurrezione), dopo che il cadavere esiste ed è mostrato e prima che l’NPC lasci il mondo, quindi può ancora essere letto. corpse è il seriale del cadavere, nil quando non ne è stato creato uno; killer è il seriale di chi lo ha ucciso, nil quando non lo ha fatto nessuno. Se ucciso da uno script, viene eseguito al turno successivo del game loop, subito prima che l’NPC venga rimosso; non deve chiamare wait(). |
on_health_low(serial, hits, hits_max, attacker) |
Quando i punti ferita dell’NPC scendono sotto la percentuale low_health_at del suo template per un danno che non lo uccide. Una volta: scatta di nuovo solo dopo che i punti ferita sono risaliti a 15 punti percentuali sopra la soglia. attacker è il seriale di chi ha inflitto il danno, 0 quando nessuno (un veleno, un campo). Vedi Punti ferita e mana bassi. Viene eseguito al turno successivo del game loop e può chiamare wait(). Dopo la ripresa, verifica che l’NPC sia ancora nel mondo, per esempio con npc.name(serial) ~= nil, prima di agire su di esso. |
on_mana_low(serial, mana, mana_max, needed) |
Quando il mana di un NPC che lancia incantesimi scende sotto needed, il costo del suo incantesimo più economico. Una volta: scatta di nuovo solo dopo che il mana risale a needed più il costo di un altro incantesimo simile, oppure a metà del massimo, il minore dei due. Vedi Punti ferita e mana bassi. Viene eseguito al turno successivo del game loop e può chiamare wait(). Dopo la ripresa, verifica che l’NPC sia ancora nel mondo, per esempio con npc.name(serial) ~= nil, prima di agire su di esso. |
on_damage(serial, attacker, amount, kind) |
Subito prima che l’NPC perda punti ferita: un colpo, un tiro, un incantesimo, un tick di veleno, un campo. Restituisci un numero per sostituire il danno, false per nessuno. Viene eseguito subito, anche dentro uno script di incantesimo, e non deve chiamare wait(). Vedi Combattimenti e vita. |
can_die(serial, killer) |
Un colpo o un tick di veleno lascerebbe l’NPC senza punti ferita. Restituisci false per tenerlo in vita con 1 punto ferita. Subito; non deve chiamare wait(). Vedi Combattimenti e vita. |
on_heal(serial, healer, amount) |
Prima che una benda o un incantesimo di cura restituisca punti ferita all’NPC (mobile.heal). Restituisci un numero o false, come on_damage. |
on_attack(serial, target) |
L’NPC sta per colpire. Restituisci false per annullare questo colpo. |
on_hit(serial, target, damage), on_miss(serial, target) |
Il colpo dell’NPC ha ferito, o ha mancato. In coda. |
on_combat_start(serial, target), on_combat_end(serial, target, reason) |
L’NPC ha ottenuto un bersaglio dal nulla, oppure lo ha perso (killed, lost, expired, fled, stopped). In coda. |
on_resurrect(serial, healer) |
L’NPC è tornato dal suo cadavere. In coda. |
on_tame(serial, tamer), on_release(serial, owner) |
La creatura è diventata di un giocatore, oppure non è più di nessuno. In coda. |
on_enter_region(serial, region), on_leave_region(serial, region) |
L’NPC ha attraversato il confine di una regione con nome. In coda. |
on_drag_drop(serial, giver, item) |
Quando un giocatore rilascia un oggetto sull’NPC da 2 caselle o meno, sulla stessa mappa; lo staff da qualsiasi distanza, come in ModernUO. Da più lontano un giocatore legge That is too far away. e l’oggetto torna indietro. giver è il seriale del giocatore, item quello dell’oggetto. Restituisci true quando lo script ha preso l’oggetto, dopo averlo spostato o eliminato (item.delete, item.move_into, item.move_to): l’oggetto non è su alcun cursore mentre la funzione viene eseguita. Qualsiasi altra cosa, una funzione assente, un errore o un wait() restituiscono l’oggetto al punto da cui il giocatore lo ha sollevato, come ogni rilascio su un NPC senza questa funzione. Il can_drop dell’oggetto viene interrogato prima, e il suo on_drop non viene chiamato per un oggetto preso da un NPC. |
on_context_menu(serial, player) |
Quando quel giocatore chiede il menu contestuale dell’NPC. Restituisci le voci che l’NPC aggiunge, una tabella di { id, cliloc, range, enabled }, oppure nulla; risponde subito e non deve chiamare wait. |
on_context_menu_select(serial, player, id) |
Quando il giocatore ha scelto una di quelle voci: id è quello della voce. Chiamato solo per una voce che il giocatore ha visto e che è nella sua portata. Può chiamare wait. |
on_spawn, on_mobile_in_range, on_mobile_killed, on_health_low e on_mana_low, e le notifiche di Combattimenti e vita, vengono eseguiti subito dopo ciò che li ha causati, al turno
successivo del game loop: un passo fatto da npc.step dentro un handler in esecuzione non può
avviare subito un altro script. Nessuna funzione viene eseguita prima che gli script siano caricati
all’avvio, dopo che gli NPC salvati sono entrati nel mondo.
Gli script agiscono sul proprio NPC con il modulo npc, passando il suo seriale. Un seriale che
non è un NPC nel mondo, come un NPC rimosso o un giocatore, restituisce false o
nil, mai un errore: un handler che ha atteso può sopravvivere al proprio NPC e uno script non può
mai far parlare o muovere un giocatore.
Un NPC che prende ciò che gli viene dato e conserva solo l’oro:
collector = {}
function collector.on_drag_drop(serial, giver, given) if item.template(given) ~= "gold_coin" then npc.say(serial, "I take gold only.")
return false end
npc.say(serial, "Thank you.") item.delete(given)
return trueendIl file scripts/mobiles/wander.lua della distribuzione, copiato nella directory radice da mgctl:
wander = {}
local thinks = {}
function wander.on_think(serial) thinks[serial] = (thinks[serial] or 0) + 1
if thinks[serial] % 4 == 0 then npc.wander(serial) endend
function wander.on_speech(serial, speaker, text) if text:lower():find("hello", 1, true) then npc.look_at(serial, speaker) wait(1) npc.say(serial, "Well met, traveller.") endend
function wander.on_spawn(serial) npc.say(serial, "*stretches*")end
function wander.on_mobile_in_range(serial, other) if npc.name(other) == nil then npc.say(serial, "Who goes there?") endendNessun template nel repository lo usa: aggiungi script_id = "wander" a un template di mobile
per provarlo. Un NPC generato da una regione di spawn porta la propria area di origine nelle proprietà spawn.x1,
spawn.y1, spawn.x2 e spawn.y2, che npc.home restituisce come tabella: npc.wander compie passi solo
al suo interno e riporta l’NPC dentro quando è fuori. Passeggia come le creature di ModernUO, soprattutto
dritto davanti a sé, mentre lo script precedente sceglieva una nuova direzione a ogni passo.
Le tabelle local di uno script vivono in memoria: ripartono vuote dopo un riavvio o un
ricaricamento. Per ricordare qualcosa tra riavvii, conservalo nelle proprietà dell’NPC, anteponendo alla
chiave il nome dello script, come vega.lua conta i saluti che ascolta:
function vega.on_speech(serial, speaker, text) if text:lower():find("hello", 1, true) then npc.look_at(serial, speaker)
local times = (npc.get_prop(serial, "vega.greeted") or 0) + 1 npc.set_prop(serial, "vega.greeted", times) npc.say(serial, "Meow! That's " .. times .. " hellos.") endendUna modifica successiva all’ultimo salvataggio del mondo va persa se il server si arresta senza salvare.
Ricarica uno script con script reload mobiles/wander.lua. La sua tabella viene sostituita,
quindi gli NPC usano le nuove funzioni dal loro prossimo pensiero; lo stato conservato nelle tabelle
local del vecchio file riparte e le attese lasciate dai suoi handler vengono annullate,
perché le chiamate di uno script appartengono a mobiles/<script_id>.lua. Quando il server si arresta,
gli script non vengono più chiamati, prima che si arresti il motore degli script.
Lo script dei mostri, monster.lua, è descritto con gli altri
script distribuiti.
Punti ferita e mana bassi
Section titled “Punti ferita e mana bassi”Due hook avvisano uno script che il suo NPC sta per esaurirsi, così che possa chiamare aiuto, bere una pozione, ritirarsi o
cambiare tattica. Sono hook, non comportamento: il common/creature.lua distribuito mantiene la propria logica di flee_at e non
li usa.
brute = {}
function brute.on_health_low(serial, hits, hits_max, attacker) npc.say(serial, "Enough of this!")
if attacker ~= 0 then -- call for the others, drink, or turn away from `attacker` endend
function brute.on_mana_low(serial, mana, mana_max, needed) -- out of mana for even the cheapest spell: fall back to meleeendEntrambi gli eventi seguono le stesse regole:
- Scattano sul fronte. L’evento scatta una volta, quando il valore passa da sopra il suo livello basso a sotto. Non scatta di nuovo finché il valore resta basso, né mentre oscilla attorno alla soglia: viene riarmato solo quando il valore risale oltre un livello più alto (l’isteresi).
- Un NPC che è già basso quando viene visto la prima volta, come uno caricato da un mondo salvato, non scatta: scatta solo dopo essersi ripreso ed aver attraversato di nuovo la soglia. Lo stato non viene salvato.
- Li sentono solo gli NPC con un template e uno script, mai i giocatori. Vengono accodati come
on_death, al turno successivo del game loop, e non vengono mai eseguiti dentro il danno o l’incantesimo che li ha causati. Un evento dovuto dopo che l’NPC è morto o è stato rimosso, come un attraversamento seguito da un colpo mortale nello stesso passaggio, viene scartato. Uno script che non definisce la funzione non costa nulla. - Ogni fonte di cambiamento passa dallo stesso punto, quindi contano tutte: un colpo in mischia, una freccia, un incantesimo, un
tick di veleno, un campo, il mana che un incantesimo costa, Mana Drain e Mana Vampire,
mobile.set_statse il comando.set.
on_health_low scatta quando hits * 100 < hits_max * low_health_at, per un danno che non uccide (un colpo mortale è
on_death, non questo), e solo quando i punti ferita sono scesi. Il low_health_at del template è una percentuale da 0 a
100 (25 se non impostato, 0 per disattivarlo), ereditata tramite base_id. Viene riarmato quando i punti ferita raggiungono
low_health_at più 15 punti percentuali (100 al massimo) del massimo. Una modifica del massimo non scatta da
sola: lo fa il danno successivo sotto la soglia.
on_mana_low scatta quando il mana scende sotto needed, il mana dell’incantesimo più economico che l’NPC può lanciare; un NPC che
non lancia nulla, senza spell_circles nel suo template, senza Magery o senza mana, non lo sente mai (una creatura con Magery e
mana ma senza spell_circles non lancia nulla, quindi non le viene detto che ne ha poco). needed viene passato così che lo script non debba
calcolarlo. Viene riarmato al doppio di needed o a metà di mana_max, il minore dei due, e mai sotto
needed + 1 (un mana_max pari a needed riarma a needed). Scatta anche dopo uno svuotamento o qualunque altra perdita di
mana, non solo un lancio. Un animale risollevato dal suo cadavere torna senza mana: non è un evento, e sente
on_mana_low solo dopo che il suo mana si è rigenerato sopra il livello di riarmo ed è sceso di nuovo.
Combattimenti e vita
Section titled “Combattimenti e vita”Gli hook di un combattimento e della vita di una creatura sono elencati, con i loro argomenti esatti e le loro regole, negli hook degli script. Tre di essi cambiano ciò che accade, quindi vengono eseguiti subito, prima del cambiamento, e uno script che fallisce conta come privo di quella funzione:
golem = {}
-- A stone body takes half of the blows and none of the poison; a fire trap or a field is not softened.function golem.on_damage(serial, attacker, amount, kind) if kind == "poison" then return false elseif kind == "melee" or kind == "ranged" or kind == "spell" then return math.floor(amount / 2) endend
-- A boss that gets up once.function golem.can_die(serial, killer) if npc.get_prop(serial, "second_life") == nil then npc.set_prop(serial, "second_life", true) mobile.set_stats(serial, { hits = 40 })
return false endend
-- A swing that does not happen while the NPC is busy casting.function golem.on_attack(serial, target) return npc.get_prop(serial, "casting") == nilendkindèmelee,ranged,spell,poisonoother. Gli script lo indicano con l’ultimo argomento opzionale dicombat.harm(target, damage, attacker, kind):spellse omesso,otherper un campo, un’esplosione o una trappola. Il veleno e i colpi del server indicano il proprio.- Un danno di 0, dall’hook o dall’armatura, fa comunque partire l’animazione di colpo subìto: l’NPC reagisce a un colpo che non ha
fatto nulla, e non genera né
on_hitnéon_miss. can_dierifiuta solo un colpo o un tick di veleno letale, dopo cheon_damageha detto la sua: l’NPC resta con 1 punto ferita, reagisce come per qualsiasi colpo che non ha ucciso, può sentireon_health_low, e il prossimo colpo letale lo chiede di nuovo. Un NPC avvelenato tenuto a 1 punto ferita continua a subire i tick..killemobile.killnon lo chiedono. I giocatori non vengono mai interpellati.- Un handler che danneggia, cura o uccide il proprio NPC non viene richiamato per questo: la chiamata annidata passa come
se lo script non avesse l’hook. Dopo
on_damageil server guarda di nuovo: se l’handler ha curato il veleno, ucciso l’NPC o lo ha rimosso, il colpo o il tick finisce lì, ecan_dienon viene chiesto per un NPC che non ha più punti ferita. - Un NPC che muore, o viene ucciso con
.killomobile.kill, termina ogni combattimento contro di sé per gli NPC che lo combattevano conon_combat_ende il motivokilled, anche quando lascia il mondo subito. on_attackche restituiscefalsespende il ritardo del colpo e non fa altro, quindi viene chiesto di nuovo quando il prossimo colpo è dovuto; lo stesso vale per uno che ha concluso il combattimento dell’NPC o lo ha rivolto contro un altro bersaglio (combat.stop,combat.attack): quel colpo non viene sferrato. Cambiare bersaglio in un combattimento non genera néon_combat_startnéon_combat_end; un NPC che muore non generaon_combat_end, lo diceon_death.- Gli hook in coda vengono eseguiti dopo il cambiamento, a un turno successivo del game loop, quindi un handler vede il mondo com’è allora, e vengono scartati per un NPC che nel frattempo è morto o ha lasciato il mondo.
on_healviene chiesto damobile.heal(patient, amount, healer), che le bende e gli incantesimi di cura usano, e solo quando c’è qualcosa da curare: non per un NPC al massimo o una quantità di 0. Rigenerazione, cibo, pozioni emobile.set_statsnon sono cure.on_enter_regioneon_leave_regionportano il nome della regione, come lo dàworld.region; lasciarne una per un’altra genera prima l’uscita. Un NPC il cui script non definisce né l’uno né l’altro non costa nulla per i suoi passi.
Percorrere un tragitto
Section titled “Percorrere un tragitto”npc.walk_to permette a un NPC di raggiungere un luogo aggirando muri, acqua e dirupi, e passando dalle porte chiuse che apre: un umano o un mostro apre una porta non chiusa a chiave, un animale o una creatura marina no, a meno che il suo template di mobile non lo dica con opens_doors = true o false. Lo script lo chiama
a ogni tick e l’NPC compie un passo ogni volta:
guard = {}
function guard.on_think(serial) local state = npc.walk_to(serial, 1434, 1699)
if state == "arrived" then npc.say(serial, "All quiet at the bank.") endend| Risposta | Significato |
|---|---|
"moving" |
L’NPC ha compiuto un passo, oppure ha chiesto a una porta sulla sua strada di aprirsi: passa a una chiamata successiva, e dopo tre chiamate davanti a una porta che resta chiusa la risposta è "blocked" |
"arrived" |
Si trova entro range caselle dal luogo (predefinito 0), alla sua altezza; non viene controllato che non ci sia nulla tra loro |
"blocked" |
Il passo è stato rifiutato oppure l’NPC attende di cercare un’altra strada |
"no_path" |
L’ultima ricerca non ha raggiunto il luogo: nulla porta lì oppure porta solo nelle vicinanze |
nil |
Il seriale non è un NPC, range è negativo oppure z è fuori dall’intervallo da -128 a 127 |
Il percorso viene trovato con la ricerca dei percorsi del server e conservato per l’NPC, quindi la maggior parte delle chiamate compie solo il passo successivo. Una ricerca viene eseguita quando l’NPC non ha più passi oppure il luogo è cambiato, e solo:
- due secondi dopo l’ultima ricerca dell’NPC, come in ModernUO; dieci secondi quando quella ricerca non ha raggiunto lo stesso luogo da dove si trova l’NPC, perché quel tipo di ricerca è costoso;
- per dieci NPC al secondo nell’intero server; gli altri attendono il proprio turno.
Mentre non può cercare, un NPC prosegue lungo il percorso che ha oppure, se non ne ha, cammina dritto
verso il luogo, così uno che insegue qualcosa continua a muoversi. Si avvicina a un luogo irraggiungibile
fin dove porta un percorso. Per seguire qualcuno, passa la sua posizione a ogni
tick e un range di 1 per fermarsi accanto a lui:
local where = mobile.location(target)npc.walk_to(serial, where.x, where.y, where.z, 1, true)running cambia solo l’aspetto del passo: un NPC compie un passo per on_think, due al secondo.
Senza z il luogo è il terreno più alto della casella non sopra la testa dell’NPC, altrimenti il
più alto presente. Partenza e destinazione devono trovarsi entro ultima.world.pathfinding_range caselle (38);
più lontano restituisce "no_path". Una porta chiusa non a chiave viene aperta lungo la strada (l’on_npc_use della porta, fino a tre tentativi), se l’NPC può aprire le porte; una chiusa a chiave, o una che l’NPC non può aprire, blocca il passaggio: la
aggira, oppure si avvicina e poi risponde "no_path". Gli altri mobile non bloccano un percorso.
Venditori ed eventi del mondo
Section titled “Venditori ed eventi del mondo”Lo script di un venditore può rifiutare una riga di un acquisto o di una vendita con can_buy e can_sell, e sente la riga
completata con on_bought e on_sold; gli argomenti e le regole sono negli hook degli script, e la finestra del negozio in
Venditori. Uno script che vuole sapere quando un NPC viene creato, muore o viene domato, o
quando qualcosa viene creato con un mestiere o smembrato, ascolta con events.on mobile_created, mobile_deleted, mobile_died,
pet_tamed, craft_succeeded e corpse_carved: sono elencati con gli altri eventi del bus.
Creato con ❤️ da Tom
