最终整理版

This commit is contained in:
2026-06-03 17:04:06 +08:00
commit 959055ce90
1240 changed files with 80570 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
extends NpcScript
# Requirements
var tributeItemID : int = DB.GetCellHash("Apple")
const tributeItemCount : int = 5
const playerLevelRequirement : int = 1
#
func OnStart():
if IsTriggering():
Chat("Focus on your mission!")
else:
Mes("Ah, another challenger! This cave is a proving ground, where the waves of corrupted creatures test your endurance. Think you're ready for what's inside?")
QuestionStart()
func QuestionStart():
Choice("I'm ready. Bring on the waves.", StartFight)
Choice("I need to go. This feels wrong.", Farewell)
Choice("What's this place? It feels strange.", ExplainCave)
Choice("Waves of monsters? Why are they coming?", ExplainWaves)
func ExplainCave():
Mes("Ah, this cave? It's... Alive, in a way. You feel it too, don't you?")
Mes("It breathes Kaore, the corrupted Mana. Everything here was once... More alive. But now it's just hungry, like me.")
QuestionsCave()
func QuestionsCave():
Choice("No more questions.", QuestionStart)
Choice("Who are you? You seem off.", ExplainNpc)
Choice("What exactly happened to you?", ExplainNpcCorruption)
Choice("Why is the cave like this?", ExplainCaveOrigin)
func ExplainWaves():
Mes("Waves... Yes, waves of those lost to Kaore. They are drawn here, hungry for more of the decaying Mana.")
Mes("You will fight them, over and over. But beware... With each wave, the cave itself grows more restless. You might not make it to the end.")
QuestionsWaves()
func QuestionsWaves():
Choice("No more questions.", QuestionStart)
Choice("What happens if I fail?", ExplainFailure)
Choice("Is there no way to cleanse the corruption?", ExplainCorruption)
Choice("Why does Kaore make things this way?", ExplainKaore)
func ExplainNpc():
Mes("Me? Oh, just a fellow survivor... Or maybe a guide? I can't tell anymore.")
Mes("I've spent too long near the Kaore. It's in my blood, in my thoughts... But it's okay. I think?")
Mes("But never mind me. You're here to fight, aren't you?")
QuestionsCave()
func ExplainNpcCorruption():
Mes("Oh, it started small... Whispers in the back of my mind, a strange craving for... Something.")
Mes("But now? I don't remember what I was before. Maybe it's better this way.")
Mes("You don't want this. But here we are.")
QuestionsCave()
func ExplainCaveOrigin():
Mes("This cave was once a Mana wellspring, a place of life and power.")
Mes("But after the Aethyra War, the Mana decayed into Kaore. Now its nothing more than a trap for those who wander too close.")
Mes("But don't worry. You'll either win... Or join the rest of us.")
QuestionsCave()
func ExplainFailure():
Mes("Even if you fall in battle, you need not fear. The Zielite Amulet you carry is more powerful than you may know.")
Mes("When you perish, your Zielite Amulet will pull your soul to the nearest Soul Menhir. Its the only reason I allow people like you to risk their lives here.")
QuestionsWaves()
func ExplainCorruption():
Mes("Ah, the corruption... Kaore, it's what Mana becomes when its left to decay.")
Mes("Mana flows through all living things. When it's pure, it nurtures life. But here... It's been stagnant for far too long.")
Mes("Once the Mana Trees was lost, the balance was broken. Now Kaore festers in places like this.")
Mes("It seeps into everything: creatures, the land, even people. And once you're touched by it, there's no going back.")
QuestionsWaves()
func ExplainKaore():
Mes("Kaore... It's the decayed Mana. It festers where life was once vibrant. Here, in this cave, it thrives.")
Mes("It changes things... Makes them hostile, makes them... Desperate.")
Mes("You'll feel it too if you stay long enough.")
QuestionsWaves()
func StartFight():
var alivePlayerCount : int = AlivePlayerCount()
if IsTriggering():
if alivePlayerCount == 0:
CallGlobal("OnCancel")
else:
Mes("The fight has already begun. You can't start another one now.")
elif alivePlayerCount > 0:
if not HasItem(tributeItemID, tributeItemCount):
Mes("You'll need to bring me 5 apples as a tribute before we begin.")
elif own.stat.level < playerLevelRequirement:
Mes("You're not strong enough for this fight. Come back when you've reached level %d." % playerLevelRequirement)
else:
RemoveItem(tributeItemID, tributeItemCount)
Chat("Ah, you're ready for this! The fight begins in 10 seconds, brace yourself!")
Action(Trigger)
+133
View File
@@ -0,0 +1,133 @@
extends NpcScript
# Mission parameters
var rewardExp : int = 1000
var rewardGP : int = 10000
# Wave parameters
var monstersPool : Array[EntityData] = []
const checkDelay : float = 2.0
const waveDelay : float = 300.0
const maxWave : int = 10
const spawnCenter : Vector2i = Vector2i(54 * 32, 67 * 32)
const spawnRadius : Vector2i = Vector2i(200, 200)
const startFightDelay : float = 10.0
const warmUpTickDelay : float = 1.0
# Local variables
var waveTimer : Timer = null
var waveCount : int = 0
var originalPlayerCount : int = 0
var waveMaxMonsters : int = 0
var playerList : Array[PlayerAgent] = []
#
func OnStart():
for mobID in DB.EntitiesDB:
var mob : EntityData = DB.EntitiesDB[mobID]
if !!(mob._behaviour & AICommons.Behaviour.AGGRESSIVE) and !(mob._behaviour & AICommons.Behaviour.IMMOBILE) and !mob._isBoss:
monstersPool.append(mob)
func OnCancel():
ClearTimer(waveTimer)
ClearPlayerList()
Reset()
if IsTriggering():
Trigger()
ClearTracker()
KillMonsters()
func OnTrigger():
AddTimer(npc, startFightDelay, StartFight)
TickWarmUp(0)
func TickWarmUp(tick : int):
if not IsTriggering():
return
DisplayTracker("Warm Up", tick, int(startFightDelay), "s")
if tick < int(startFightDelay):
AddTimer(npc, warmUpTickDelay, TickWarmUp.bind(tick + 1), "WarmUpTimer")
#
func RemoveFromPlayerList(player : PlayerAgent):
playerList.erase(player)
func ClearPlayerList():
for player in playerList:
Callback.ClearOneShot(player.tree_exiting)
Callback.ClearOneShot(player.agent_killed)
playerList.clear()
func StartFight():
Reset()
var ownInstance : WorldInstance = WorldAgent.GetInstanceFromAgent(own)
if ownInstance:
for player : PlayerAgent in ownInstance.players:
if ActorCommons.IsAlive(player):
playerList.append(player)
Callback.OneShotCallback(player.tree_exiting, RemoveFromPlayerList.bind(player))
Callback.OneShotCallback(player.agent_killed, RemoveFromPlayerList.bind(player))
originalPlayerCount = playerList.size()
waveTimer = AddTimer(npc, waveDelay, TimeoutWave)
NextWave()
func NextWave():
waveCount += 1
if waveCount > maxWave:
Reward()
else:
Notification("Wave %d." % waveCount)
SpawnMonsters()
waveMaxMonsters = AliveMonsterCount()
DisplayTracker("Monsters", 0, waveMaxMonsters)
waveTimer.start(waveDelay)
AddTimer(npc, checkDelay, CheckWave, "CheckTimer")
func Reset():
waveTimer = null
waveCount = 0
originalPlayerCount = 0
waveMaxMonsters = 0
func Reward():
Notification("Congrats, you won")
ClearTracker()
for player : PlayerAgent in playerList:
NpcCommons.AddExp(player, rewardExp)
NpcCommons.AddGP(player, rewardGP)
ClearPlayerList()
Reset()
if IsTriggering():
Trigger()
func SpawnMonsters():
var isStuck : bool = false
var remainingPoints : int = (5 * waveCount) + ceili(originalPlayerCount * 0.7)
monstersPool.shuffle()
while(remainingPoints > 0 and not isStuck):
isStuck = true
for mob in monstersPool:
var mobPoint : int = mob._stats["Level"] if "Level" in mob._stats else 1
if mobPoint > 0 and mobPoint <= remainingPoints:
var spawnAmount : int = randi_range(1, int(remainingPoints / floor(mobPoint)))
remainingPoints -= mobPoint * spawnAmount
Spawn(mob._id, spawnAmount, spawnCenter, spawnRadius)
func CheckWave():
if not IsTriggering():
return
if AlivePlayerCount() == 0:
OnCancel()
else:
var mobCount : int = AliveMonsterCount()
if mobCount > 0:
waveMaxMonsters = max(waveMaxMonsters, mobCount)
DisplayTracker("Monsters", waveMaxMonsters - mobCount, waveMaxMonsters)
AddTimer(npc, checkDelay, CheckWave, "CheckTimer")
else:
NextWave()
func TimeoutWave():
Notification("You were not able to clear the corruption in time.")
OnCancel()
+36
View File
@@ -0,0 +1,36 @@
extends NpcScript
#
func OnStart():
Mes("Would you like to change your hair style or color today?")
Choice("I want to try another style", OnHairstyle)
Choice("A new color", OnHaircolor)
Choice("None", OnQuit)
func OnHairstyle():
var hairstyles : Array[HairstyleData] = DB.HairstylesDB.values()
var count : int = hairstyles.size() -1
var randIdx : int = randi_range(0, count)
var newStyleIdx : int = hairstyles[randIdx]._id
if newStyleIdx == own.stat.haircolor:
randIdx = (randIdx + 1) % (count + 1)
newStyleIdx = hairstyles[randIdx]._id
own.stat.SetHairstyle(newStyleIdx)
Choice("Another style", OnHairstyle)
Choice("Perfect!", OnQuit)
func OnHaircolor():
var haircolors : Array = DB.PalettesDB[DB.Palette.HAIR].values()
var count : int = haircolors.size() - 1
var randIdx : int = randi_range(0, count)
var newColorIdx : int = haircolors[randIdx]._id
if newColorIdx == own.stat.haircolor:
randIdx = (randIdx + 1) % (count + 1)
newColorIdx = haircolors[randIdx]._id
own.stat.SetHaircolor(newColorIdx)
Choice("Another color", OnHaircolor)
Choice("Perfect!", OnQuit)
+11
View File
@@ -0,0 +1,11 @@
extends NpcScript
const animationSpeed : float = 30.0
func OnTrigger():
AddTimer(npc, animationSpeed, CloseChest)
#
func CloseChest():
if IsTriggering():
Trigger()
+6
View File
@@ -0,0 +1,6 @@
extends NpcScript
#
func OnStart():
Greeting()
AddTimer(own, 1.0, Callable())
+6
View File
@@ -0,0 +1,6 @@
extends NpcScript
#
func OnAreaEnter(player : PlayerAgent):
if player and not player.ownScript:
own.Interact(player)
+25
View File
@@ -0,0 +1,25 @@
extends NpcScript
class_name WarpGlobal
#
var warpName : String = ""
#
func OnAreaEnter(player : PlayerAgent):
if player and not player.ownScript:
if npc.spawnInfo.auto_warp:
OnWarpConfirm(player)
else:
npc.Interact(player)
func OnWarpConfirm(player : PlayerAgent):
NpcCommons.Warp(player, npc.spawnInfo.destination_map, npc.spawnInfo.destination_pos, npc.spawnInfo.direction)
func GetWarpField(_player : PlayerAgent) -> String:
return warpName
#
func OnStart():
var mapData : FileData = DB.MapsDB.get(npc.spawnInfo.destination_map, null)
if mapData:
warpName = mapData._name
+49
View File
@@ -0,0 +1,49 @@
extends NpcScript
#
const FILL_TIME : float = 10.0
const FILL_TICKS : int = 20
const FILL_TICK_TIME : float = FILL_TIME / FILL_TICKS
const MOVE_TOLERANCE_SQUARED : float = 8.0 * 8.0
var BOTTLE_ID : int = "Bottle".hash()
var WATER_BOTTLE_ID : int = "Water Bottle".hash()
#
func OnStart():
if not HasItem(BOTTLE_ID):
Mes("You would need a bottle to draw water from this well.")
elif not HasItemsSpace([[BOTTLE_ID, -1], [WATER_BOTTLE_ID, 1]]):
Mes("You don't have any available space on your inventory.")
else:
Mes("Hold still while your bottle is filling...")
Action(StartFill)
func StartFill():
OnFillTick(own.position, 0)
func OnFillTick(startPos : Vector2, tick : int):
if own.position.distance_squared_to(startPos) > MOVE_TOLERANCE_SQUARED:
ClearTracker()
Notification("You moved too far from the well!")
elif not HasItem(BOTTLE_ID):
ClearTracker()
elif tick >= FILL_TICKS:
CompleteFill()
else:
DisplayTracker("Filling...", tick, FILL_TICKS, "%")
AddTimer(own, FILL_TICK_TIME, OnFillTick.bind(startPos, tick + 1), own.nick)
func CompleteFill():
NpcCommons.ClearTracker(own)
if NpcCommons.RemoveItem(own, BOTTLE_ID):
NpcCommons.AddItem(own, WATER_BOTTLE_ID)
PromptNext()
func PromptNext():
if HasItem(BOTTLE_ID):
Mes("Your bottle is now filled with fresh water. You have another empty bottle.")
Choice("Fill another bottle", StartFill)
Choice("Leave")
else:
Mes("Your bottle is now filled with fresh water.")
@@ -0,0 +1,134 @@
extends NpcScript
#
var waitingPlayer : PlayerAgent = null
var games : Dictionary = {}
#
func JoinPvP(player : PlayerAgent) -> int:
if waitingPlayer and is_instance_valid(waitingPlayer) and waitingPlayer != player:
var game : Dictionary = _CreateGame(waitingPlayer, player)
games[waitingPlayer.get_rid().get_id()] = game
games[player.get_rid().get_id()] = game
NpcCommons.PushNotification(waitingPlayer, "An opponent has arrived! Talk to %s." % npc.nick)
waitingPlayer = null
return 1
waitingPlayer = player
return 0
func GetGame(player : PlayerAgent) -> Dictionary:
return games.get(player.get_rid().get_id(), {})
func GetHand(player : PlayerAgent) -> Array:
var game : Dictionary = GetGame(player)
if game.is_empty():
return []
if game.get("p1") == player:
return game.get("p1Hand", [])
return game.get("p2Hand", [])
func DrawCard(player : PlayerAgent) -> int:
var game : Dictionary = GetGame(player)
if game.is_empty():
return -1
var gameDeck : Array = game.get("deck", [])
if gameDeck.is_empty():
return -1
return gameDeck.pop_back()
func FinishHand(player : PlayerAgent, value : int, busted : bool):
var game : Dictionary = GetGame(player)
if game.is_empty():
return
if game.get("p1") == player:
game["p1Done"] = true
game["p1Value"] = value
game["p1Busted"] = busted
else:
game["p2Done"] = true
game["p2Value"] = value
game["p2Busted"] = busted
if game.get("p1Done", false) and game.get("p2Done", false):
_ResolveGame(game)
func LeavePvP(player : PlayerAgent):
if not player or not is_instance_valid(player):
return
if waitingPlayer == player:
waitingPlayer = null
var rid : int = player.get_rid().get_id()
if games.has(rid):
var game : Dictionary = games[rid]
if not game.has("result"):
var isP1 : bool = game.get("p1") == player
var opponent = game.get("p2") if isP1 else game.get("p1")
if isP1:
game["p1Done"] = true
game["p1Value"] = 0
game["p1Busted"] = true
else:
game["p2Done"] = true
game["p2Value"] = 0
game["p2Busted"] = true
_ResolveGame(game)
if opponent and is_instance_valid(opponent):
NpcCommons.PushNotification(opponent, "Your opponent left. You win!")
_ClearGameCallbacks(game)
games.erase(rid)
func ForfeitPvP(player : PlayerAgent):
LeavePvP(player)
func CleanupGame(player : PlayerAgent):
var game : Dictionary = GetGame(player)
if not game.is_empty():
_ClearGameCallbacks(game)
games.erase(player.get_rid().get_id())
#
func _CreateGame(p1 : PlayerAgent, p2 : PlayerAgent) -> Dictionary:
var gameDeck : Array = []
gameDeck.resize(52)
for i : int in range(52):
gameDeck[i] = i
gameDeck.shuffle()
Callback.OneShotCallback(p1.tree_exiting, _PlayerLeft.bind(p1))
Callback.OneShotCallback(p2.tree_exiting, _PlayerLeft.bind(p2))
return {
"p1": p1, "p2": p2,
"deck": gameDeck,
"p1Hand": [gameDeck.pop_back(), gameDeck.pop_back()],
"p2Hand": [gameDeck.pop_back(), gameDeck.pop_back()],
"p1Value": 0, "p2Value": 0,
"p1Done": false, "p2Done": false,
"p1Busted": false, "p2Busted": false,
}
func _ClearGameCallbacks(game : Dictionary):
var p1 = game.get("p1")
var p2 = game.get("p2")
if p1 and is_instance_valid(p1):
Callback.ClearOneShot(p1.tree_exiting)
if p2 and is_instance_valid(p2):
Callback.ClearOneShot(p2.tree_exiting)
func _ResolveGame(game : Dictionary):
var p1v : int = game.get("p1Value", 0)
var p2v : int = game.get("p2Value", 0)
var p1b : bool = game.get("p1Busted", false)
var p2b : bool = game.get("p2Busted", false)
if p1b and p2b:
game["result"] = "draw"
elif p1b:
game["result"] = "p2"
elif p2b:
game["result"] = "p1"
elif p1v > p2v:
game["result"] = "p1"
elif p2v > p1v:
game["result"] = "p2"
else:
game["result"] = "draw"
func _PlayerLeft(player : PlayerAgent):
LeavePvP(player)
@@ -0,0 +1,359 @@
extends NpcScript
class_name TicTacToeGlobal
#
static var BOARD_POSITION : Vector2i = Vector2i(120, 80) * 32 + Vector2i(16, 32) # Tile 120,80 with an offset of 16,32 pixels
static var CELL_ID : int = "TicTacToeCell".hash()
const CELL_COUNT : int = 9
const WINNING_LINES : Array[Array] = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
]
const IDLE_TIMEOUT : float = 30.0
const NPC_DELAY_MIN : float = 0.2
const NPC_DELAY_MAX : float = 1.0
#
enum State
{
NONE = 0,
X,
O
}
var boardStates : Array[State] = [
State.NONE, State.NONE, State.NONE,
State.NONE, State.NONE, State.NONE,
State.NONE, State.NONE, State.NONE
]
var boardNpcs : Array[NpcAgent] = [
null, null, null,
null, null, null,
null, null, null
]
var playerX : PlayerAgent = null
var playerO : PlayerAgent = null
var idleTimer : Timer = null
var startStep : State = State.NONE
var currentTurn : State = State.NONE
# Board handling
func SpawnCells():
var map : WorldMap = WorldAgent.GetMapFromAgent(npc)
if not map:
return
var inst : WorldInstance = WorldAgent.GetInstanceFromAgent(npc)
if not inst:
return
var spawn : SpawnObject = SpawnObject.new()
spawn.id = CELL_ID
spawn.type = "Npc"
spawn.state = ActorCommons.State.IDLE
spawn.is_persistant = false
spawn.map = map
for cellIdx in CELL_COUNT:
spawn.spawn_position = BOARD_POSITION + Vector2i((cellIdx % 3) * 32, int(cellIdx / 3.0) * 32)
var spawnedAgent : BaseAgent = WorldAgent.CreateAgent(spawn, inst.id)
if spawnedAgent and spawnedAgent is NpcAgent:
boardNpcs[cellIdx] = spawnedAgent
boardNpcs[cellIdx].interacted.connect(CellSelected.bind(boardNpcs[cellIdx]))
func CellSelected(playerAgent : BaseAgent, cellAgent : NpcAgent):
if playerAgent is not PlayerAgent:
return
if cellAgent:
var cellIdx : int = boardNpcs.find(cellAgent)
if cellIdx >= 0:
MakeMove(playerAgent, cellIdx)
func DespawnCells():
for cellIdx in CELL_COUNT:
if boardNpcs[cellIdx]:
if is_instance_valid(boardNpcs[cellIdx]):
WorldAgent.RemoveAgent.call_deferred(boardNpcs[cellIdx])
boardNpcs[cellIdx] = null
func DespawnUnusedCells():
for cellIdx in CELL_COUNT:
if boardStates[cellIdx] == State.NONE and boardNpcs[cellIdx] and is_instance_valid(boardNpcs[cellIdx]):
WorldAgent.RemoveAgent.call_deferred(boardNpcs[cellIdx])
boardNpcs[cellIdx] = null
# PvP matchmaking
func StartPvP(player : PlayerAgent) -> State:
if not player or not is_instance_valid(player):
return State.NONE
if startStep != State.O:
if not playerX or not is_instance_valid(playerX) or WorldAgent.GetInstanceFromAgent(playerX) != WorldAgent.GetInstanceFromAgent(player):
playerX = player
Callback.AddCallback(playerX.tree_exiting, LeaveQueue, [playerX], ConnectFlags.CONNECT_ONE_SHOT)
startStep = State.X
return startStep
elif not playerO or not is_instance_valid(playerO) or WorldAgent.GetInstanceFromAgent(playerO) != WorldAgent.GetInstanceFromAgent(player):
playerO = player
startStep = State.O
StartGame()
return startStep
return State.NONE
func LeavePvP(player : PlayerAgent):
if not player or not is_instance_valid(player):
if playerX and is_instance_valid(playerX):
NpcCommons.PushNotification(playerX, "Your opponent left. You win!")
elif playerO and is_instance_valid(playerO):
NpcCommons.PushNotification(playerO, "Your opponent left. You win!")
elif startStep == State.O and (player == playerX or player == playerO):
var opponent : PlayerAgent = playerO if player == playerX else playerX
if opponent and is_instance_valid(opponent):
NpcCommons.PushNotification(opponent, "Your opponent left. You win!")
EndGame()
func LeaveQueue(player : PlayerAgent):
if player and is_instance_valid(player) and player == playerX:
EndGame()
# Game management
func StartGame():
if currentTurn != State.NONE:
return
startStep = State.O
currentTurn = State.X
DespawnCells()
SpawnCells()
StartIdleTimer()
if playerX:
Callback.RemoveCallback(playerX.tree_exiting, LeaveQueue)
Callback.AddCallback(playerX.tree_exiting, LeavePvP, [playerX], ConnectFlags.CONNECT_ONE_SHOT)
NpcCommons.PushNotification(playerX, "Tic Tac Toe started! You play X!")
if playerO:
Callback.AddCallback(playerO.tree_exiting, LeavePvP, [playerO], ConnectFlags.CONNECT_ONE_SHOT)
NpcCommons.PushNotification(playerO, "Tic Tac Toe started! You play O!")
func StartPvE(player : PlayerAgent) -> bool:
if startStep == State.NONE:
playerX = player
playerO = null
StartGame()
return startStep == State.O
return false
# Move handling
func MakeMove(player : PlayerAgent, cellIndex : int) -> bool:
if currentTurn == State.NONE or cellIndex < 0 or cellIndex >= CELL_COUNT:
return false
var mark : State = boardStates[cellIndex]
if mark != State.NONE:
return false
if player == playerX and currentTurn == State.X:
mark = State.X
elif player == playerO and currentTurn == State.O:
mark = State.O
else:
return false
boardStates[cellIndex] = mark
UpdateCellVisual(cellIndex)
currentTurn = State.X if currentTurn == State.O else State.O
OnMovePlayed()
StartIdleTimer()
return true
func OnMovePlayed():
if startStep != State.O:
return
var result : State = CheckWin()
if result != State.NONE or CheckDraw():
AnnounceResult(result)
elif not playerO:
NotifyTurn()
var delay : float = randf_range(NPC_DELAY_MIN, NPC_DELAY_MAX)
Callback.SelfDestructTimer(npc, delay, PlayNPCMove)
else:
NotifyTurn()
func PlayNPCMove():
if currentTurn != State.O or playerO:
return
var npcMove : int = GetNPCMove()
if npcMove >= 0:
MakeMove(null, npcMove)
func NotifyTurn():
if currentTurn == State.NONE:
return
if playerO:
var activePlayer : PlayerAgent = playerX if currentTurn == State.X else playerO
var waitingPlayer : PlayerAgent = playerO if currentTurn == State.X else playerX
if activePlayer and is_instance_valid(activePlayer):
NpcCommons.PushNotification(activePlayer, "Your turn!")
if waitingPlayer and is_instance_valid(waitingPlayer):
NpcCommons.PushNotification(waitingPlayer, "Waiting for opponent...")
else:
if currentTurn == State.X:
if playerX and is_instance_valid(playerX):
NpcCommons.PushNotification(playerX, "Your turn!")
else:
if playerX and is_instance_valid(playerX):
NpcCommons.PushNotification(playerX, "Waiting for opponent...")
func UpdateCellVisual(cellIndex : int):
if cellIndex < 0 or cellIndex >= CELL_COUNT:
return
var cellNpc : NpcAgent = boardNpcs[cellIndex]
if not cellNpc or not is_instance_valid(cellNpc):
return
match boardStates[cellIndex]:
State.NONE:
cellNpc.state = ActorCommons.State.IDLE
State.X:
cellNpc.state = ActorCommons.State.TRIGGER
if cellNpc.interacted.is_connected(CellSelected):
cellNpc.interacted.disconnect(CellSelected)
State.O:
cellNpc.state = ActorCommons.State.SIT
if cellNpc.interacted.is_connected(CellSelected):
cellNpc.interacted.disconnect(CellSelected)
cellNpc.set_physics_process(true)
cellNpc.requireFullUpdate = true
# Idle timer
func StartIdleTimer():
StopIdleTimer()
idleTimer = Callback.SelfDestructTimer(npc, IDLE_TIMEOUT, IdleTimeout)
func StopIdleTimer():
if idleTimer and is_instance_valid(idleTimer) and not idleTimer.is_queued_for_deletion():
idleTimer.stop()
idleTimer.queue_free()
idleTimer = null
func IdleTimeout():
idleTimer = null
if currentTurn == State.NONE:
return
var loser : State = currentTurn
var winner : State = State.X if loser == State.O else State.O
var loserPlayer : PlayerAgent = playerX if loser == State.X else playerO
if loserPlayer and is_instance_valid(loserPlayer):
NpcCommons.PushNotification(loserPlayer, "You took too long! You lose.")
AnnounceResult(winner)
# Game end
func EndGame():
StopIdleTimer()
ClearPlayerCallbacks()
DespawnUnusedCells()
ResetBoard()
func ClearPlayerCallbacks():
if playerX and is_instance_valid(playerX):
Callback.RemoveCallback(playerX.tree_exiting, LeavePvP)
if playerO and is_instance_valid(playerO):
Callback.RemoveCallback(playerO.tree_exiting, LeavePvP)
func ResetBoard():
playerX = null
playerO = null
startStep = State.NONE
currentTurn = State.NONE
for cellIdx in CELL_COUNT:
boardStates[cellIdx] = State.NONE
# Win detection
func CheckWin() -> State:
for line in WINNING_LINES:
var state : State = boardStates[line[0]]
if state != State.NONE and state == boardStates[line[1]] and state == boardStates[line[2]]:
return state
return State.NONE
func CheckDraw() -> bool:
for cell in boardStates:
if cell == State.NONE:
return false
return true
func AnnounceResult(result : State):
var msg : String = ""
match result:
State.NONE:
msg = "It's a draw!"
State.X:
msg = "%s wins!" % (playerX.nick if playerX else "X")
State.O:
msg = "%s wins!" % (playerO.nick if playerO else npc.nick)
if playerX and is_instance_valid(playerX):
NpcCommons.PushNotification(playerX, msg)
if playerO and is_instance_valid(playerO):
NpcCommons.PushNotification(playerO, msg)
EndGame()
# NPC AI
func GetNPCMove() -> int:
var winMove : int = FindWinningMove(State.O)
if winMove >= 0:
return winMove
var blockMove : int = FindWinningMove(State.X)
if blockMove >= 0:
return blockMove
if boardStates[4] == State.NONE:
return 4
var corners : Array[int] = [0, 2, 6, 8]
corners.shuffle()
for corner in corners:
if boardStates[corner] == State.NONE:
return corner
var edges : Array[int] = [1, 3, 5, 7]
edges.shuffle()
for edge in edges:
if boardStates[edge] == State.NONE:
return edge
return -1
func FindWinningMove(state : State) -> int:
for line : Array in WINNING_LINES:
var totalPointInLine : int = 0
var firstEmptyIdx : int = -1
for idx : int in line:
if boardStates[idx] == state:
totalPointInLine += 1
elif boardStates[idx] == State.NONE:
firstEmptyIdx = idx
# Winning move found if 2 out of 3 point in a line are already matching the given state and if an empty index is available
if totalPointInLine == 2 and firstEmptyIdx >= 0:
return firstEmptyIdx
return -1
+68
View File
@@ -0,0 +1,68 @@
extends NpcScript
#
func OnStart():
match GetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING):
ProgressCommons.SPLATYNA_OFFERING.INACTIVE: Inactive()
ProgressCommons.SPLATYNA_OFFERING.STARTED: OnRecap()
_: OnFinish()
func Inactive():
Mes("Praise Splatyna, the mighty slime goddess!")
Mes("You, traveler! Do you come to offer gold to our great lady?")
InfoChoice()
func InfoChoice(previousText : int = -1):
var questState : int = GetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING)
if previousText != 0:
if questState == ProgressCommons.SPLATYNA_OFFERING.INACTIVE:
Choice("Sure, I'll bring her the offering.", OnAcceptQuest)
else:
Choice("What should I do?", OnRecap)
if previousText != 1:
Choice("This is nonsense, Im leaving.", OnDecline)
if previousText != 2:
Choice("Who is Splatyna?", OnAskAboutSplatyna)
if previousText != 3:
Choice("What is this place?", OnAskAboutPlace)
func OnRecap():
Mes("You still havent offered the gold to Splatyna?")
Mes("Go into her cave, find her followers, and give her the gold! The three loyal ones hold the keys, remember that!")
InfoChoice(0)
func OnFinish():
Mes("Wait... I heard something. A scream!")
Mes("Did something happen to Splatyna?! What have you done?")
Choice("Nothing! Everythings fine...", OnDeny)
Choice("Shes... Gone.", OnAdmit)
func OnDeny():
Mes("Good, good! As long as Splatyna still watches over us.")
func OnAdmit():
Mes("Gone?! No, no! Youre lying! Splatyna cant die!")
Mes("Just... Go, before I lose my mind!")
func OnAskAboutSplatyna():
Mes("Oh, Splatyna... She's not like the other slimes, no no. She's powerful! She's been blessed by Kaore!")
Mes("She doesn't need food or water like the rest of us. Kaore keeps her alive! Shes stronger and wiser than us poor souls!")
Mes("And the gold! Yes, she loves it. She says it keeps her safe, makes her strong! We give her gold, and she protects us, keeps the decay away!")
Mes("But don't anger her, no! She can make you crazy with just a look! Her magic... It's powerful, twisted by Kaore. She's... perfect.")
InfoChoice(2)
func OnAskAboutPlace():
Mes("This is Splatyna's sacred cave! Only those who respect her can enter, yes, yes!")
Mes("The slimes here, they're not normal. They're loyal to Splatyna, her closest followers. They guard her treasures, her power!")
Mes("Three of them are special, real loyal ones. They have the keys to her chamber, but they won't just hand them over, no no! You'll have to earn them!")
Mes("But don't worry. If you bring enough gold, maybe Splatyna will let you through...")
InfoChoice(3)
func OnAcceptQuest():
Mes("Very well, take this gold and offer it to Splatyna. She will judge your worth.")
SetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING, ProgressCommons.SPLATYNA_OFFERING.STARTED)
Farewell()
func OnDecline():
Chat("Blasphemy! You dare refuse Splatynas offering?!")
+35
View File
@@ -0,0 +1,35 @@
extends NpcScript
# Reward items
var appleID : int = DB.GetCellHash("Apple")
# Required items
var dorianKeyID : int = DB.GetCellHash("Dorian's Key")
var gabrielKeyID : int = DB.GetCellHash("Gabriel's Key")
var marvinKeyID : int = DB.GetCellHash("Marvin's Key")
#
func OnStart():
match GetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING):
ProgressCommons.SPLATYNA_OFFERING.INACTIVE: Inactive()
ProgressCommons.SPLATYNA_OFFERING.STARTED: TryOpen()
_: Empty()
func Inactive():
Chat("This chest seems to be sealed.")
func TryOpen():
if IsMonsterAlive("Splatyna"):
Chat("A dark presence is still around.")
else:
# Chest is not open, try to open it
if not IsTriggering():
Trigger()
# Chest is opened, you can withdraw your reward
if HasSpace(1):
SetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING, ProgressCommons.SPLATYNA_OFFERING.REWARDS_WITHDREW)
AddItem(appleID, 5)
func Empty():
Chat("This chest is empty.")
+38
View File
@@ -0,0 +1,38 @@
extends NpcScript
# Required items
var dorianKeyID : int = DB.GetCellHash("Dorian's Key")
var gabrielKeyID : int = DB.GetCellHash("Gabriel's Key")
var marvinKeyID : int = DB.GetCellHash("Marvin's Key")
var mapID : int = "Splatyna's Chamber".hash()
const mapPosition : Vector2 = Vector2(1500, 2190)
#
func OnStart():
match GetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING):
ProgressCommons.SPLATYNA_OFFERING.INACTIVE: Inactive()
_: AskChoice()
func AskChoice():
if HasItem(dorianKeyID) and HasItem(gabrielKeyID) and HasItem(marvinKeyID):
Mes("You notice three different key locks, what would you like to do?")
Choice("Open them", TryOpen)
Choice("Leave")
else:
Chat("You need three different keys to unlock this passage.")
func TryOpen():
# Check and remove items to open the chest
if GetQuest(ProgressCommons.Quest.SPLATYNA_OFFERING) != ProgressCommons.SPLATYNA_OFFERING.INACTIVE:
if HasItem(dorianKeyID) and HasItem(gabrielKeyID) and HasItem(marvinKeyID):
RemoveItem(dorianKeyID)
RemoveItem(gabrielKeyID)
RemoveItem(marvinKeyID)
Warp(mapID, mapPosition)
Close()
else:
Chat("You need three different keys to unlock this passage.")
func Inactive():
Chat("Three locks are blocking this passage.")
+26
View File
@@ -0,0 +1,26 @@
extends NpcScript
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
const bitIndex : int = 0
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
var state : int = GetQuest(questID)
if state >= ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED:
return
Mes("墙上刻着第一行字:我从图利姆沙偷走的不是金子,而是一条能让我活下去的路。")
Mes("字迹旁画着一只蛇,蛇头朝北。")
var newState : int = state | (1 << bitIndex)
if newState != state:
SetQuest(questID, newState)
if newState == ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND:
OnAllCluesFound()
func OnAllCluesFound():
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED)
AddItem(thiefsKeyID, 1)
Mes("五行刻字在你脑海中拼成完整的路线。石缝弹开,你找到一把盗贼钥匙。")
+26
View File
@@ -0,0 +1,26 @@
extends NpcScript
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
const bitIndex : int = 1
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
var state : int = GetQuest(questID)
if state >= ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED:
return
Mes("墙上刻着第二行字:沙会吞掉脚印,水会记住脚步。")
Mes("字迹下方有几道短线,像是在提醒你数清岔路。")
var newState : int = state | (1 << bitIndex)
if newState != state:
SetQuest(questID, newState)
if newState == ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND:
OnAllCluesFound()
func OnAllCluesFound():
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED)
AddItem(thiefsKeyID, 1)
Mes("五行刻字在你脑海中拼成完整的路线。石缝弹开,你找到一把盗贼钥匙。")
+26
View File
@@ -0,0 +1,26 @@
extends NpcScript
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
const bitIndex : int = 2
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
var state : int = GetQuest(questID)
if state >= ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED:
return
Mes("墙上刻着第三行字:若听见鳞片摩擦石头,就别拔刀;停下,等它先走。")
Mes("刻字边缘被蛇鳞磨得发亮,像是很多年都没有真正安静过。")
var newState : int = state | (1 << bitIndex)
if newState != state:
SetQuest(questID, newState)
if newState == ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND:
OnAllCluesFound()
func OnAllCluesFound():
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED)
AddItem(thiefsKeyID, 1)
Mes("五行刻字在你脑海中拼成完整的路线。石缝弹开,你找到一把盗贼钥匙。")
+26
View File
@@ -0,0 +1,26 @@
extends NpcScript
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
const bitIndex : int = 3
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
var state : int = GetQuest(questID)
if state >= ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED:
return
Mes("墙上刻着第四行字:我把宝箱留给能读懂害怕的人。贪心的人只会听见锁声。")
Mes("这里的沙子很新,似乎有人不久前才翻动过。")
var newState : int = state | (1 << bitIndex)
if newState != state:
SetQuest(questID, newState)
if newState == ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND:
OnAllCluesFound()
func OnAllCluesFound():
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED)
AddItem(thiefsKeyID, 1)
Mes("五行刻字在你脑海中拼成完整的路线。石缝弹开,你找到一把盗贼钥匙。")
+26
View File
@@ -0,0 +1,26 @@
extends NpcScript
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
const bitIndex : int = 4
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
var state : int = GetQuest(questID)
if state >= ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED:
return
Mes("墙上刻着最后一行字:北、东、静候、回头,再向没有风的地方走。")
Mes("最后一个字下面压着一块松动的石片。")
var newState : int = state | (1 << bitIndex)
if newState != state:
SetQuest(questID, newState)
if newState == ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND:
OnAllCluesFound()
func OnAllCluesFound():
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED)
AddItem(thiefsKeyID, 1)
Mes("五行刻字在你脑海中拼成完整的路线。石缝弹开,你找到一把盗贼钥匙。")
+64
View File
@@ -0,0 +1,64 @@
extends NpcScript
#
func OnStart():
match GetQuest(WaterPondGlobal.QUEST_ID):
ProgressCommons.SNAKE_PIT_BITING_THIRST.INACTIVE:
OnInactive()
ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED:
OnCheckProgress()
ProgressCommons.SNAKE_PIT_BITING_THIRST.REWARDS_WITHDREW:
OnComplete()
_:
OnInProgress()
# Quest states
func OnInactive():
Mes("我的水全洒了。没有水,我回不到图利姆沙。")
Mes("几天前我以为自己能征服这片沙地,就进了这座洞。里面有一处很干净的地下水池,以前旅人会来这里补水。")
Mes("后来蛇越来越多。它们毒性不强,但咬得很凶,我每次装好水,走不到一半就被咬到手抖,把水全洒掉。")
Mes("你能替我试一次吗?只要把水罐装满带回来,我就能回城。")
Mes("洞里还有些旧刻字,像是某个盗贼留下的路线。你如果看见,最好记下来,蛇坑里没有无用的线索。")
Choice("我去试试。", OnAccept)
Choice("今天不行。", OnDecline)
func OnCheckProgress():
var rid : int = own.get_rid().get_id()
if WaterPondGlobal.biteCounters.get(rid, 0) == 0:
OnInProgress()
else:
OnDeliverWater()
func OnInProgress():
Mes("清水池在洞穴深处,不是每一处水都能喝,脏水只会害你白跑。")
Mes("找到清水池后别乱动,等水罐装满,再尽快带回来。")
Mes("路上被蛇咬会漏水。漏光了就回水池重新装,别拿空罐子回来安慰我。")
func OnDeliverWater():
WaterPondGlobal.StopJugTransport(own)
ClearTracker()
Mes("你回来了!水还在,太好了。")
Mes("拿着这些,这是我剩下的东西。比不上你救我一命,但至少能让你少受点苦。")
SetQuest(WaterPondGlobal.QUEST_ID, ProgressCommons.SNAKE_PIT_BITING_THIRST.REWARDS_WITHDREW)
AddItem(DB.GetCellHash("Cactus Drink"), 10)
AddExp(50)
AddGP(1000)
AddKarma(1)
Mes("我现在就回图利姆沙。这里太危险了。")
Mes("以后我会乖乖用城里的井水。蛇坑里的水,就留给蛇和胆子太大的人吧。")
func OnComplete():
Mes("谢谢你帮我取回那罐水。现在我可以慢慢走回城,不用每一步都想着自己会不会倒下。")
Mes("沙漠很美,但我已经受够了它的待客方式。")
func OnDecline():
Mes("我明白。要是你改变主意,我还会在这里,希望不会太久。")
# Transitions
func OnAccept():
SetQuest(WaterPondGlobal.QUEST_ID, ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED)
Mes("拿着这个水罐。清水池在洞穴深处。")
Mes("我只记得一开始往北,之后转向东边。再后面也许是向南,也许是追着没有风的地方走。")
Mes("说实话,我记得最清楚的是蛇咬上来的声音。")
Mes("如果你在墙上看到旧盗贼的刻字,就照着记。那些字也许比我的脑袋可靠。")
Mes("最好带上可靠的宠物伙伴。蛇很多,单靠胆量装不满水。")
@@ -0,0 +1,40 @@
extends NpcScript
# Quest ID
const questID : int = ProgressCommons.Quest.SNAKE_PIT_THIEF
# Reward items
var scimitarID : int = DB.GetCellHash("Scimitar")
# Required items
var thiefsKeyID : int = DB.GetCellHash("Thief's Key")
#
func OnStart():
match GetQuest(questID):
ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED: OnTryOpen()
ProgressCommons.SNAKE_PIT_THIEF.REWARDS_WITHDREW: OnEmpty()
_: OnLocked()
func OnTryOpen():
if not HasItem(thiefsKeyID):
OnLocked()
return
if not IsTriggering():
Trigger()
Mes("五条刻字指向的锁孔终于转动。箱盖掀开时,一股干冷的旧沙味涌了出来。")
SetQuest(questID, ProgressCommons.SNAKE_PIT_THIEF.REWARDS_WITHDREW)
RemoveItem(thiefsKeyID, 1)
AddGP(200)
AddItem(scimitarID, 1)
AddExp(50)
AddKarma(2)
func OnEmpty():
Chat("盗贼宝箱已经空了,只剩几道被蛇鳞刮出的细痕。")
func OnLocked():
Chat("箱锁上刻着蛇形纹路。没有盗贼钥匙,打不开它。")
@@ -0,0 +1,13 @@
extends NpcScript
#
func OnStart():
match GetQuest(WaterPondGlobal.QUEST_ID):
ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED:
OnFill()
func OnFill():
var rid : int = own.get_rid().get_id()
if not WaterPondGlobal.biteCounters.has(rid) and npc.get_node_or_null(own.nick) == null:
(npc.ownScript as WaterPondGlobal).OnFillTick(own, own.position, 0)
Mes("你把水罐浸入清水池。先别动,等水慢慢装满。")
@@ -0,0 +1,6 @@
extends NpcScript
func OnStart():
match GetQuest(WaterPondGlobal.QUEST_ID):
ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED:
Mes("这池水混着泥和蛇蜕,绝不是毛罗说的清水池。")
@@ -0,0 +1,67 @@
extends NpcScript
class_name WaterPondGlobal
#
const QUEST_ID : int = ProgressCommons.Quest.SNAKE_PIT_BITING_THIRST
const MAX_BITES : int = 5
const FILL_TIME : float = 5.0
const FILL_TICKS : int = 10
const FILL_TICK_TIME : float = FILL_TIME / FILL_TICKS
const MOVE_TOLERANCE_SQUARED : float = 8.0 * 8.0
# Per-player bite counters [PlayerRID, BiteCount]
static var biteCounters : Dictionary[int, int] = {}
# Signal handling
static func StartJugTransport(player : PlayerAgent):
var rid : int = player.get_rid().get_id()
biteCounters[rid] = MAX_BITES
if not player.agent_damaged.is_connected(OnBite):
player.agent_damaged.connect(OnBite)
static func StopJugTransport(player : PlayerAgent):
var rid : int = player.get_rid().get_id()
biteCounters.erase(rid)
if player.agent_damaged.is_connected(OnBite):
player.agent_damaged.disconnect(OnBite)
# Biting handling
static func OnBite(player : PlayerAgent, value : int):
if value == 0 or player.progress.GetQuest(QUEST_ID) != ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED:
return
var rid : int = player.get_rid().get_id()
if not biteCounters.has(rid):
return
var remaining : int = biteCounters[rid] - 1
biteCounters[rid] = remaining
NpcCommons.PushTracker(player, "水罐完整度", remaining, MAX_BITES, "%")
if remaining <= 0:
Spill(player)
static func Spill(player : PlayerAgent):
StopJugTransport(player)
NpcCommons.SetQuest(player, QUEST_ID, ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED)
NpcCommons.ClearTracker(player)
NpcCommons.PushNotification(player, "你被蛇咬得太多,水罐漏空了。回清水池重新装水。")
# Jug filling handling
func OnFillTick(player : PlayerAgent, startPos : Vector2, tick : int):
if player.position.distance_squared_to(startPos) > MOVE_TOLERANCE_SQUARED:
NpcCommons.ClearTracker(player)
NpcCommons.PushNotification(player, "你动得太早,水还没装满。")
else:
if tick >= FILL_TICKS:
CompleteFill(player)
else:
NpcCommons.PushTracker(player, "正在装水", tick, FILL_TICKS, "%")
ScheduleTick(player, startPos, tick)
func ScheduleTick(player : PlayerAgent, startPos : Vector2, tick : int):
AddTimer(own, FILL_TICK_TIME, OnFillTick.bind(player, startPos, tick + 1), player.nick)
func CompleteFill(player : PlayerAgent):
StartJugTransport(player)
NpcCommons.PushTracker(player, "水罐完整度", MAX_BITES, MAX_BITES, "%")
NpcCommons.PushNotification(player, "水罐装满了,尽快带回毛罗。")
+39
View File
@@ -0,0 +1,39 @@
extends NpcScript
# Quest ID
const questID : int = ProgressCommons.Quest.SANDSTORM_MINE_ABANDONED_TREASURE
# Required items
var chestMineKeyID : int = DB.GetCellHash("Chest Mine Key")
# Reward items
var shortSwordID : int = DB.GetCellHash("Short Sword")
#
func OnStart():
match GetQuest(questID):
ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.KEY_FOUND: OnTryOpen()
ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.REWARDS_WITHDREW: OnEmpty()
_: OnLocked()
func OnTryOpen():
if not HasItem(chestMineKeyID):
OnLocked()
return
if not IsTriggering():
Trigger()
if HasSpace(1):
Mes("钥匙插进锁孔后,锈住的机关咔哒一声松开。箱子里还躺着一把矿工留下的短剑。")
RemoveItem(chestMineKeyID, 1)
SetQuest(questID, ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.REWARDS_WITHDREW)
AddItem(shortSwordID, 1)
else:
Mes("你找到了能打开箱子的钥匙,但背包已经装不下新的武器。")
func OnEmpty():
Chat("箱子已经空了,只剩下矿砂和木屑。")
func OnLocked():
Chat("矿工留下的旧箱子锁着,需要对应的矿洞钥匙。")
+13
View File
@@ -0,0 +1,13 @@
extends NpcScript
#
func OnStart():
Mes("你就是艾基努派来的侦察员?看起来还站得稳,这已经比我预想的好。")
Mes("别误会,艾基努不是想害你。他只是知道矿洞里需要一个会随机应变的人。")
Mes("从这里往东南走,旧井旁边就是沙漠风暴矿洞入口。")
Mes("如果你一路走到谷地外面,说明方向错了;真正的入口不会离风声太远。")
Mes("看守内森应该在入口站岗。他话多,但眼睛不瞎,能帮你确认矿洞情况。")
Mes("我们的几个人已经先进去了。你进去后先确认路线,再判断哪些宠物和怪物能避开,哪些必须处理。")
Mes("祝你好运。还有,别为了证明胆量去碰看起来会发光的东西。")
@@ -0,0 +1,9 @@
extends NpcScript
#
const mapPos : Vector2i = Vector2i(1760, 2560)
var mapName : int = "Desert Deep Level".hash()
#
func OnAreaEnter(player : PlayerAgent):
NpcCommons.WarpInstance(player, mapName, mapPos)
+20
View File
@@ -0,0 +1,20 @@
extends NpcScript
#
func OnStart():
Mes("从那条西侧山口继续走,就会离开 图利姆沙谷地,进入 祖尼石台地。")
Mes("那里路标少、岩影多,第一次去的人常常把回头路也弄丢。你确定方向吗?")
Choice("我在找 沙漠风暴矿洞。", OnMines)
Choice("我想回 图利姆沙。", OnTulimshar)
func OnMines():
LookAtNpc("To Mines")
Mes("沿这条路走,只会离 沙漠风暴矿洞 越来越远。")
ResetCamera()
Mes("先往回走,再朝谷地南侧找矿道。看到旧木梁和矿车轨迹,就说明方向对了。")
func OnTulimshar():
LookAtNpc("To Tulimshar")
Mes("沿那条路往北,能回到 图利姆沙 的城墙下。")
ResetCamera()
Mes("只要沙尘没有太厚,城墙从这里几乎就能看见。看到旗帜,就继续朝旗帜走。")
@@ -0,0 +1,33 @@
extends NpcScript
#
const QUEST_ID : int = ProgressCommons.Quest.DESERT_DEEP_XAKELBAEL
#
func OnAreaEnter(player : PlayerAgent):
var inst : WorldInstance = WorldAgent.GetInstanceFromAgent(npc)
if not inst or inst.id != player.get_rid().get_id():
return
var questState : int = player.progress.GetQuest(QUEST_ID)
if questState != ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED:
SpawnXakelbael(inst)
npc.RemoveTrigger()
func SpawnXakelbael(inst : WorldInstance):
var spawn : SpawnObject = SpawnObject.new()
spawn.map = inst.map
spawn.type = "Npc"
spawn.nick = "Xakelbael"
spawn.id = spawn.nick.hash()
spawn.spawn_position = Vector2i(1331, 1478)
spawn.spawn_offset = Vector2i(5, 5)
spawn.direction = ActorCommons.Direction.UP
spawn.state = ActorCommons.State.IDLE
spawn.trigger_radius = 120.0
spawn.own_script = "tonori/sandstorm/XakelbaelGlobal.gd"
spawn.player_script = "tonori/sandstorm/Xakelbael.gd"
spawn.is_persistant = false
spawn.respawn_delay = 0.0
WorldAgent.CreateAgent(spawn, inst.id)
@@ -0,0 +1,16 @@
extends NpcScript
# Quest ID
const questID : int = ProgressCommons.Quest.SANDSTORM_MINE_ABANDONED_TREASURE
# Reward items
var chestMineKeyID : int = DB.GetCellHash("Chest Mine Key")
#
func OnStart():
match GetQuest(questID):
ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.INACTIVE:
if HasSpace(1):
Mes("碎石下面露出一把生锈钥匙,柄上还刻着旧矿工的编号。")
SetQuest(questID, ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.KEY_FOUND)
AddItem(chestMineKeyID, 1)
+95
View File
@@ -0,0 +1,95 @@
extends NpcScript
const QUEST_ID = ProgressCommons.Quest.SANDSTORM_NATHAN_WATER
#
func OnStart():
Mes("你好,我是看守内森。平时我在港口站岗,今天被派来守这个快把人烤熟的矿洞入口。")
Mes("如果我说话有点乱,那是太阳先动的手。")
var questState : ProgressCommons.SANDSTORM_NATHAN_WATER = GetQuest(QUEST_ID) as ProgressCommons.SANDSTORM_NATHAN_WATER
Choice("这里就是沙漠风暴矿洞入口?", OnEntrance)
match questState:
ProgressCommons.SANDSTORM_NATHAN_WATER.STARTED:
Choice("关于那瓶水。", OnWaitingWater)
ProgressCommons.SANDSTORM_NATHAN_WATER.REWARDS_WITHDREW:
Choice("现在感觉好点了吗?", OnComplete)
Choice("我先去别处看看。", Farewell)
func Farewell():
Chat("小心脚下,沙子会把旧轨道盖得像没存在过一样。")
# Default dialogue flow
func OnEntrance():
Mes("没错。旧矿道就在这口井旁边,风从里面吹出来时会带着铁锈味。")
Mes("以前矿工每天从这里进出。后来沙暴越来越重,沙蝎和别的东西也把下层当成了巢。")
Mes("艾基努的人已经进去了,他们要确认矿道能不能重新开放。说实话,我更希望答案是不能。")
Choice("我就是来加入侦察的。", OnJoinThem)
func OnJoinThem():
Mes("真的?你看起来不像矿工。没有镐,也没有一脸认命的表情。")
Choice("我不是来采矿的,只负责侦察。", OnJustScouting)
func OnJustScouting():
Mes("明白了。那就进去吧,里面的人应该正等你。")
Mes("先别急着深入。入口层确认安全后,再考虑往废弃矿层和更深处走。")
Mes("如果你听见像石头在呼吸的声音,别逞强,先退回来。")
Choice("谢谢,内森。", OnNiceDay)
if GetQuest(QUEST_ID) == ProgressCommons.SANDSTORM_NATHAN_WATER.INACTIVE:
Choice("有什么能帮你的吗?", OnJobEasier)
func OnNiceDay():
Chat("等我能回港口看海,那才叫好日子。")
# Water quest
func OnJobEasier():
Mes("既然你问了,我确实想要一瓶水。")
Mes("不是喝的,我还有一点能喝的。")
Mes("我只是想把水倒在脸上,让自己别像挂在太阳底下的肉干。")
Mes("这请求听起来很蠢,但在这里站半天后,你会理解的。")
Choice("我理解,我会帮你找一瓶。", AcceptQuest)
Choice("抱歉,我现在有更紧急的事。", RefuseQuest)
func RefuseQuest():
Mes("没事,我懂。忘了我提过这事吧。")
Mes("大概就是因为我太会抱怨,才被派到这里来。")
Action(Farewell)
func AcceptQuest():
Mes("我就知道你会懂。水是生命,浇脸上的水也是。")
Narrate("内森低头看了看脚边被风埋住一半的靴子。")
Mes("我会在这里等你。反正我想走也走不了。")
SetQuest(QUEST_ID, ProgressCommons.SANDSTORM_NATHAN_WATER.STARTED)
if HasItem(DB.GetCellHash("Water Bottle")):
Choice("我身上正好有一瓶。", OnDeliverWater)
Choice("我去找找。", Farewell)
func OnWaitingWater():
Mes("是你啊。抱歉,风沙一大,我看谁都像会走路的土堆。")
Mes("找到水瓶了吗?")
if HasItem(DB.GetCellHash("Water Bottle")):
Choice("找到了。", OnDeliverWater)
Choice("还没有,我继续找。", Farewell)
func OnDeliverWater():
var waterBottleHash : int = DB.GetCellHash("Water Bottle")
if HasItem(waterBottleHash):
RemoveItem(waterBottleHash)
SetQuest(QUEST_ID, ProgressCommons.SANDSTORM_NATHAN_WATER.REWARDS_WITHDREW)
Mes("谢谢你,朋友。终于能喘口气了。")
Narrate("内森把你带来的水迅速倒在脸上和脖子上。")
Narrate("水顺着盔甲缝隙流进沙地,很快就被热风带走。")
AddExp(50)
AddGP(100)
Action(OnComplete)
else:
Action(Farewell)
func OnComplete():
Mes("这正是我需要的。你不知道这有多救命。")
Mes("现在我只需要再来一瓶。")
Chat("开玩笑的。大部分是。")
+32
View File
@@ -0,0 +1,32 @@
extends NpcScript
#
func OnStart():
Mes("等等,旅人。风向几分钟就变一次,别急着把自己交给沙子。")
Mes("如果你要去矿洞,让旧井一直在左手边。还有,别追沙尘里那些会动的影子。")
OnMainChoice()
func OnMainChoice():
Choice("矿洞附近发生了什么?", OnMines)
Choice("你见过异常的宠物吗?", OnPets)
Choice("出发前有什么建议?", OnAdvice)
Choice("我该继续赶路了。", Farewell)
func OnMines():
Mes("女王 想重新开放矿洞,可沙漠已经有好几年时间把那里变成自己的巢。")
Mes("沙蝎藏在暖石缝里,蛇会贴着薄沙游动,更深处还有一些不像普通动物的东西。")
Mes("艾基努的侦察队先进去,是因为总得有人在矿工送命前搞清楚里面到底有多糟。")
OnMainChoice()
func OnPets():
Mes("见过不少。风平时啾啾鸟会沿路盘旋,沙虫聚在仙人掌根旁,入夜后矿洞入口基本归沙蝎。")
Mes("想捕捉的话,先削弱它,再准备备用捕捉护符。沙漠不会等你慢慢翻包。")
OnMainChoice()
func OnAdvice():
Mes("水、护目镜、一只你信得过的宠物。顺序别弄错。")
Mes("如果风变红,找岩石躲。如果沙声突然安静,说明附近有东西在听。")
OnMainChoice()
func Farewell():
Chat("狂风压下来时,把身子放低。")
+48
View File
@@ -0,0 +1,48 @@
extends NpcScript
#
func OnStart():
Mes("沿这条路往东走,就会离开 图利姆沙谷地,进入 玛纳伊尔海岸 的方向。")
Mes("你是在找路,还是在确认自己没有被风带偏?")
DisplayChoices()
func DisplayChoices(hideChoice : int = -1):
if hideChoice != 1:
Choice("我在找 沙漠风暴矿洞。", OnMines)
if hideChoice != 2:
Choice("这片谷地有什么要注意?", OnValley)
if hideChoice != 3:
Choice("玛纳伊尔海岸是什么地方?", OnManayir)
if hideChoice != 4:
Choice("谢谢,我知道了。", Farewell)
func OnMines():
LookAtNpc("To Mines")
Mes("从这里直接往南,就能到 沙漠风暴矿洞。")
ResetCamera()
Mes("矿洞荒废很久了,不过我刚才看见一队 图利姆沙 人往那边去。")
Mes("他们不像普通商队,更像是带着目的来的。你如果也要下矿,最好先确认补给和宠物状态。")
DisplayChoices()
func OnValley():
Mes("图利姆沙谷地 是 托诺里 沙漠里少见的低谷,城就靠这里躲开最狠的热风。")
Mes("城墙把谷地、港口和居民区圈在一起,也把大部分野兽和沙暴挡在外面。")
Mes("但相对安全不等于安全。南边旧住区有失控的 卡奥雷,西边石台地更容易迷路。")
DisplayChoices()
func OnManayir():
Mes("那里靠海,是 托诺里 少数气候温和的地方。风里有盐味,沙子也不像谷地里这么烫。")
Mes("海滩外伸出一片半岛,我们称它为 纳瓦。")
Mes("玛纳伊尔教团 就在那里研究 玛纳、星象和一些普通人听完会头痛的东西。")
Choice("玛纳伊尔教团是什么?", OnManayirOrder)
DisplayChoices(3)
func OnManayirOrder():
Mes("那是一支古老的法师组织,研究 玛纳 的方式比商人算账还认真。")
Mes("旧故事里说,他们曾在远古时代治理过 图利姆沙。现在他们更愿意待在塔里,偶尔才和城里交易。")
Mes("我的族人尊 玛纳 为 圣灵。我们和 玛纳伊尔 交换知识,也互相保持距离。智慧有时比刀更锋利。")
DisplayChoices()
func Farewell():
Chat("顺着路标走,别让风替你决定方向。")
@@ -0,0 +1,21 @@
extends NpcScript
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.DESERT_DEEP_XAKELBAEL)
if questState == ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED:
Mes("你比我记住的更强。")
Mes("但深层矿道不会因为一次胜利就恢复安静。")
Mes("我们还会再见。卡奥雷 从不只留下一个影子。")
else:
Mes("谁踏进了这截被遗忘的根脉?")
Mes("矿工带走铁,王冠带走命令,而你带着宠物和一身不知天高地厚的勇气。")
Mes("这里的 玛纳树 早已听不见祈祷,只剩 卡奥雷 在石头里回响。")
Mes("如果你要替图利姆沙打开这条路,就先证明你能活着穿过我的影子。")
Action(npc.ownScript.StartFight)
func OnQuit():
super.OnQuit()
var questState : int = GetQuest(ProgressCommons.Quest.DESERT_DEEP_XAKELBAEL)
if questState == ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED:
npc.ownScript.RunAway.call_deferred()
@@ -0,0 +1,84 @@
extends NpcScript
#
const QUEST_ID : int = ProgressCommons.Quest.DESERT_DEEP_XAKELBAEL
const exitDelay : float = 8.0
const exitPosition : Vector2 = Vector2(1760, 2533)
#
var player : PlayerAgent = null
var playerModifier : StatModifier = null
#
func OnStart():
Callback.PlugCallback(npc.ready, DisableAIBehaviour)
func DisableAIBehaviour():
npc.aiBehaviour = AICommons.Behaviour.NONE
AI.Stop(npc)
func OnAreaEnter(_player : PlayerAgent):
if IsVisible() and _player and not _player.ownScript:
var questState : int = _player.progress.GetQuest(QUEST_ID)
if questState != ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED:
player = _player
Callback.OneShotCallback(player.tree_exiting, OnPlayerLeft)
npc.Interact(player)
func OnPlayerLeft():
RemovePlayerModifier()
if player:
NpcCommons.SetQuest(player, QUEST_ID, ProgressCommons.DESERT_DEEP_XAKELBAEL.INACTIVE)
player = null
# Player modifier
func AddPlayerModifier():
if not playerModifier and player:
playerModifier = AddModifier(CellCommons.Modifier.DodgeRate, 10000, player)
func RemovePlayerModifier():
if playerModifier and player:
RemoveModifier(playerModifier, player)
playerModifier = null
# Actions
func StartFight():
NpcCommons.SetQuest(player, QUEST_ID, ProgressCommons.DESERT_DEEP_XAKELBAEL.FIGHTING)
SetVisible(false)
AddPlayerModifier()
var mobs : Array[MonsterAgent] = Spawn(npc.data._id, 1, npc.position, Vector2i.DOWN)
if not mobs.is_empty():
var monsterAgent : MonsterAgent = mobs[0]
monsterAgent.agent_killed.connect(OnMonsterKilled)
AI.Refresh(monsterAgent)
func OnMonsterKilled(mob : BaseAgent):
RemovePlayerModifier()
npc.position = mob.position
RemoveAgent(mob)
SetState(ActorCommons.State.DEATH)
SetVisible(true)
NpcCommons.SetQuest(player, QUEST_ID, ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED)
if player and ActorCommons.IsAlive(player) and not player.ownScript:
player.AddScript(npc)
if player.ownScript:
player.ownScript.ApplyStep()
func RunAway():
if player:
Callback.ClearOneShot(player.tree_exiting)
player = null
SetState(ActorCommons.State.IDLE)
AddModifier(CellCommons.Modifier.WalkSpeed, 200)
AI.Reset(npc)
npc.WalkToward(exitPosition)
AddTimer(npc, exitDelay, WorldAgent.RemoveAgent.bind(npc))
@@ -0,0 +1,37 @@
extends NpcScript
#
func OnStart():
Mes("被派到内墙值守,算是守卫里难得的轻松差事。")
Mes("外墙那边要盯野兽和沙暴,我这里主要负责把好奇的人拦在宫门外。")
Mes("红女王 正在照料她的花园。除非你真有要紧事,否则别打扰她。")
OnMainChoice()
# Main choice loop
func OnMainChoice():
Choice("这座大建筑是什么?", OnCastle)
Choice("西边通向哪里?", OnWest)
Choice("南边是什么地方?", OnSouth)
Choice("我先走了。", Farewell)
# Answers
func OnCastle():
Mes("那是 红女王 的城堡,也是 图利姆沙 的中枢。")
Mes("如果你有真正重要的消息,可以申请觐见;如果只是想看热闹,我建议你转身去市场。")
Mes("城堡里还有大陆第二大的藏书馆,书量几乎能追上 玛纳伊尔 塔。法师们听到这句通常会假装没听见。")
OnMainChoice()
func OnWest():
Mes("西边是 祖尼石台地 和更荒的石台地。路不直,风也不讲道理。")
Mes("那一带有蛇、盗贼留下的旧营地,也有比我们更懂沙漠的部族。别把陌生土地当成空地图。")
Mes("真要过去,就贴着路标走,水袋装满,别随便碰看起来像祭坛或陷阱的东西。两者通常都不欢迎客人。")
OnMainChoice()
func OnSouth():
Mes("南边能看到旧仙人掌田和废弃住区。那里以前有人住,现在只剩风声和倒下的围栏。")
Mes("沙暴一年比一年近,最后把大家逼回城墙后面。")
Mes("卡奥雷 最近又不安分,守卫队不想再失去一片街区。你如果去南边,至少带一只可靠的宠物。")
OnMainChoice()
func Farewell():
Chat("别在宫门前逗留太久。")
+25
View File
@@ -0,0 +1,25 @@
extends NpcScript
#
func OnStart():
match randi_range(0, 5):
0: SeenClay()
1: GoingToSea()
2: Circles()
3: Chat("那块石头像不像一只睡着的小宠物?我觉得很像。")
4: Chat("小心躲起来的海盗!他们最喜欢抢没有写名字的藏宝图。")
5: Chat("别擦掉地上的粉笔线,我还没画完港口呢!")
func SeenClay():
Mes("你看到我的粉笔了吗?")
Mes("我猜是守卫拿走了。他们站岗太无聊,肯定想偷偷画自己的城堡。")
func GoingToSea():
Mes("总有一天我要坐船出海,去看看 图利姆沙 以外的世界。")
Mes("我听说北边有一座城市,沙子又白又冷,踩上去会像面粉一样响。")
func Circles():
Mes("你会画很圆很圆的圆吗?我会!爸爸教我的。")
Mes("薇画不圆,她画出来的圆像吃饱了的方块。")
Mes("她不喜欢我笑,所以玩井字棋时总要执叉号。")
Mes("我现在叫她狂野的叉。她说这个名字听起来像冒险队队长。")
@@ -0,0 +1,7 @@
extends NpcScript
#
func OnStart():
match GetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND):
ProgressCommons.GRAIN_IN_THE_SAND.STARTED:
Chat("一个封好的货桶。蜡封颜色不对,不是里斯基姆要找的那批。")
+50
View File
@@ -0,0 +1,50 @@
extends NpcScript
#
const QUEST_ID : int = ProgressCommons.Quest.TULIMSHAR_OLD_FRIENDSHIP
var sealedLettersID : int = DB.GetCellHash("Sealed Letters")
#
func OnStart():
var questState : int = GetQuest(QUEST_ID)
match questState:
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.ENVELOPES_FOUND:
ReceiveLetters()
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.LETTERS_DELIVERED, \
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.REWARDS_WITHDREW:
FreeRoaming()
_:
TulimsharWestWallLightTrigerGlobal.CallGuard(own)
func ReceiveLetters():
if HasItem(sealedLettersID):
Mes("你是谁?")
Choice("弗罗斯特让我来的。他让我把这些给你。", GiveLetters)
else:
Mes("你看起来有话要说。可你手里什么都没有。")
Mes("有东西要给我时再回来。我还有墙要巡。")
func GiveLetters():
Mes("什么。他让你带这些来?")
Narrate("议员博恩斯翻看那些信,手指在封口处停了很久。")
Mes("我认得这笔迹。")
Mes("很旧了。那时 女王 还把我们分派在不同岗位,我们甚至还没守同一段墙。")
Mes("弗罗斯特处理人。我处理石头。分工就是这样。")
Mes("有一阵子,这办法很好。")
Mes("后来 女王 不断加压。更多命令,更多临时要求,而且永远不能只把事情做好。")
Mes("弗罗斯特开始给一切立规程。我说,我们已经够难了,不需要再多一层压力。")
Mes("他说我把他关在外面,总是不告诉任何人就做决定。")
Mes("也许他说得对。语言不是我的工具,从来不是。")
Mes("我只想把东西建好,然后让别人停止争论我为什么这么建。")
Mes("最后我叫他走。他走了。故事到此为止。")
Mes("...")
Mes("显然并没有。否则我不会还在这里,守着一段没人真的想攻破的墙。")
Mes("除了你。不过既然弗罗斯特让你来,那另当别论。")
RemoveItem(sealedLettersID)
SetQuest(QUEST_ID, ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.LETTERS_DELIVERED)
Mes("把这个信封带回去给他。他会知道里面是什么。")
Mes("再告诉他,城墙还在。就这句。他会明白。")
func FreeRoaming():
Mes("又是你。走廊现在对你开放,我说话算数。")
Mes("我旁边的图书架后有条通道,可以通到城墙外。知道的人不多,别让它变成市场传闻。")
+137
View File
@@ -0,0 +1,137 @@
extends NpcScript
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.TUTORIAL)
if questState < ProgressCommons.TUTORIAL.ELANORE_DONE:
OnSendToKaelForRecovery()
elif questState < ProgressCommons.TUTORIAL.KAEL_DONE:
OnSendToKael()
elif questState < ProgressCommons.TUTORIAL.EKINU_DONE:
OnKaelReport()
else:
OnComplete()
#
func OnSendToKaelForRecovery():
Mes("你已经醒了?")
LookAtNpc("Kael")
Mes("先去找凯尔。他负责确认你能不能上路,也会给你说明为什么必须选择一只初始宠物。")
ResetCamera()
func OnSendToKael():
Mes("你看见那些沙虫了吗?")
Mes("恶心,但不能忽视。听老人说,它们以前没这么大,也没这么多。")
Mes("仙人掌农户受影响最大。沙虫啃坏作物,仙人掌怪又被 卡奥雷 侵蚀,再拖下去城里的药材和食物都会出问题。")
LookAtNpc("Kael")
Mes("看守凯尔在东边的仙人掌田附近。")
ResetCamera()
Mes("如果你想证明自己不是巡逻队捡回来的麻烦,就去帮他清理田地。")
# Kael's report
func OnKaelReport():
Mes("凯尔那边处理完了?")
Choice("处理完了。凯尔说我能帮你们的远征。", OnExpedition)
func OnExpedition():
Mes("嗯,他判断得没错。")
Mes("我们确实还缺一个能应付突发战斗的人。城墙不能抽走太多守卫,你这种外来帮手反而合适。")
Mes("你要往南走,去沙漠风暴矿洞。名字不是吓唬人的,那片谷地风向混乱,沙会把路和怪物一起藏起来。")
Choice("为什么要去那里?", OnWhyExpedition)
Choice("我具体要做什么?", OnJobExplanation)
func OnWhyExpedition():
Mes("那片矿洞以前盛产铁矿,整片区域又算红女王的私产,所以王宫一直惦记着它。")
Mes("后来沙暴加重,矿工不是被风吞掉,就是被矿道里的怪物袭击,矿洞才被迫废弃。")
Mes("现在王宫缺钱,女王又下令不惜代价重开矿洞。")
Mes("她当然不会亲自去确认下面有什么危险,受苦的永远是被派去干活的人。")
Choice("我具体要做什么?", OnJobExplanation)
func OnJobExplanation():
Mes("你的任务是侦察矿洞路线,确认旧矿道里有没有足以杀死矿工的大威胁。")
Mes("能避开的就记录,必须清理的就处理。别把每场战斗都当成证明自己的机会。")
if GetQuest(ProgressCommons.Quest.TUTORIAL) < ProgressCommons.TUTORIAL.EKINU_DONE:
var shortSwordID : int = DB.GetCellHash("Short Sword")
var desertGogglesID : int = DB.GetCellHash("Desert Goggles")
Mes("出城前不能让你空手去。")
Mes("拿着短剑和沙漠护目镜。风沙会抢走视线,怪物会抢走犹豫的时间。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.EKINU_DONE)
AddItem(shortSwordID)
AddItem(desertGogglesID)
AddExp(50)
Mes("这些足够让你自保,但不保证你能赢下所有战斗。")
Mes("遇到危险就跑。死在沙地里没有荣耀,只会让别人多挖一个坑。")
TeachSkill(DB.GetCellHash("Run"))
DisplayActions(["gp_run"])
Narrate("按住奔跑键可以冲刺,用来赶路或脱离危险。")
Narrate("冲刺会消耗耐力,别在真正需要逃命前把耐力耗空。")
Mes("差不多准备好了。")
LookAtNpc("Gilgames")
Mes("不过出城前先找吉尔伽美什说几句。他守着城门,见过太多新人把简单错误变成遗言。")
ResetCamera()
Mes("其他守卫已经先出发了。到城外找看守道森,他会告诉你矿洞入口怎么走。")
if GetQuest(ProgressCommons.Quest.MINE_EXPLORATION) < ProgressCommons.MINE_EXPLORATION.STARTED:
SetQuest(ProgressCommons.Quest.MINE_EXPLORATION, ProgressCommons.MINE_EXPLORATION.STARTED)
OnMainChoice()
# Main choice loop
func OnMainChoice():
if GetQuest(ProgressCommons.Quest.MINE_EXPLORATION) == ProgressCommons.MINE_EXPLORATION.STARTED:
Choice("再说一遍我的任务。", OnJobExplanation)
Choice("红女王是什么样的人?", OnRedQueen)
Choice("遇到危险怎么办?", OnStrangerDanger)
Choice("明白了,我出发。", Farewell)
# Attack and flee
func OnStrangerDanger():
Mes("能先手就先手,不能赢就撤。")
Mes("你的宠物不是用来替你送死的,受伤太重就换路线、补给、或者直接回城。")
DisplayActions(["gp_target"])
DisplayActions(["gp_interact"])
DisplayActions(["gp_run"])
Narrate("按住奔跑键可以冲刺,也可以在危险时拉开距离。")
Narrate("冲刺会消耗耐力,需要时再用。")
OnMainChoice()
# 红女王 chain
func OnRedQueen():
Mes("红女王,正式点叫卡罗琳娜一世,是这座城名义上的统治者。")
Mes("她还自称 托诺里 女王,好像城墙外的沙子也会听她命令。")
Mes("她说自己继承了古代 白金王朝 的血统。实际情况是,她父亲原本只是个非常聪明的仙人掌农户。")
Choice("听起来你很讨厌她。", OnDislikeQueen)
Choice("她父亲是农户?", OnRedQueenFather)
func OnRedQueenFather():
Mes("是。他让临终的老国王相信自己是私生子。")
Mes("老国王没有别的孩子,又确实喜欢他,就把继承权给了他。公平地说,她父亲很聪明,也比现在的女王更懂治理。")
Mes("可他死后,女儿只剩下王冠和故事。没人真心喜欢她,所以她更需要血统传说来撑住面子。")
Choice("听起来你很讨厌她。", OnDislikeQueen)
func OnDislikeQueen():
Mes("讨厌这个词太轻了。")
Mes("她冷漠、自私,只关心王宫花园和自己的脸面。")
Mes("别到处说是我讲的。巡逻队不怕风沙,但没人想因为几句真话被王宫找麻烦。")
Mes("她关心那些花,胜过关心被派去矿洞送命的人。")
Choice("那我们最好先做正事。", OnMainChoice)
# Expedition started
func Farewell():
if randi() % 2:
Chat("保持警觉。")
else:
Chat("完整地回来。")
func OnComplete():
var questState : int = GetQuest(ProgressCommons.Quest.MINE_EXPLORATION)
if questState == ProgressCommons.MINE_EXPLORATION.STARTED:
Mes("准备出发时,去城外找看守道森。")
else:
Mes("你很幸运,在哥布林发现你之前先被我们捡到。看到你恢复得不错,我也放心些。")
OnMainChoice()
+164
View File
@@ -0,0 +1,164 @@
extends NpcScript
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.TUTORIAL)
match questState:
ProgressCommons.TUTORIAL.INACTIVE:
OnFirstMeeting()
ProgressCommons.TUTORIAL.INTRO_ITEMS_GIVEN:
OnSendToKaelForCheck()
ProgressCommons.TUTORIAL.POTION_GIVEN:
OnSendToKaelForCheck()
ProgressCommons.TUTORIAL.CLOTHES_GIVEN:
OnSendToKaelForCheck()
ProgressCommons.TUTORIAL.UI_EXPLAINED:
OnSendToKaelForCheck()
_:
Mes("你回来了。需要药剂,还是想问些城里的事?")
OnMainChoice()
# First meeting
func OnFirstMeeting():
var waterBottleID : int = DB.GetCellHash("Water Bottle")
var cactusSourCandyID : int = DB.GetCellHash("Cactus Sour Candy")
Mes("醒了?欢迎来到图利姆沙。")
Mes("你差一点就倒在城墙看不见的地方了。巡逻队在沙地边缘发现你时,你已经晒到说不出话。")
Mes("我是埃拉诺。先喝点水,再吃这块仙人掌酸糖。别急着站太久,你的身体还没完全缓过来。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.INTRO_ITEMS_GIVEN)
AddItem(waterBottleID)
AddItem(cactusSourCandyID)
OnFeelingChoice()
func OnFeelingChoice():
Choice("我好多了,谢谢。", OnFeelingBetter)
Choice("还是有点虚弱。", OnFeelingWeak)
func OnFeelingWeak():
var cactusDrinkID : int = DB.GetCellHash("Cactus Drink")
Mes("那再拿一杯仙人掌饮料。剩下的就只能靠时间了,亲爱的。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.POTION_GIVEN)
AddItem(cactusDrinkID)
OnFeelingBetter()
func OnFeelingBetter():
Mes("能回答问题就好。关于你为什么会独自出现在沙漠里,凯尔会继续问。")
Mes("他负责巡逻队的初步确认,也负责给刚恢复的人安排第一只同行宠物。")
OnSendToKaelForCheck()
func OnGiveStarterClothes():
var cottonShirtID : int = DB.GetCellHash("Cotton Shirt")
var linenShortsID : int = DB.GetCellHash("Shorts")
Mes("不管你从哪里来,能站起来就是好事。图利姆沙需要愿意帮忙的人。")
Mes("最近沙漠越来越不安稳。东边山里有一群迷信 卡奥雷 的狂热者,袭击旅人,甚至曾经差点冲破城墙。")
Mes("先把这身破布换掉吧。城里人会看衣服判断你是不是刚从沙地里被捡回来。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.CLOTHES_GIVEN)
AddItem(cottonShirtID, 1, "Used")
AddItem(linenShortsID, 1, "Used")
OnExplainUI()
func OnExplainUI():
Mes("放你到城里乱跑之前,我先讲几件保命的事。")
HighlightUI(UICommons.UITarget.STATINDICATOR)
Narrate("这些是你的关键状态。探索和战斗时要随时留意。")
HighlightUI(UICommons.UITarget.HEALTHBAR)
Narrate("生命值代表你还能承受多少伤害。")
HighlightUI(UICommons.UITarget.MANABAR)
Narrate("玛纳 支撑技能释放。玛纳 不足时,许多能力无法使用。")
HighlightUI(UICommons.UITarget.STAMINABAR)
Narrate("耐力影响奔跑、攻击和体力动作。耗尽后会明显变慢。")
HighlightUI(UICommons.UITarget.MENUINDICATOR)
Narrate("菜单可以查看背包、技能、任务、设置等信息。")
HighlightUI(UICommons.UITarget.ACTION_BAR)
Narrate("快捷栏可以放常用技能和物品。战斗时每一秒都很重要。")
HighlightUI(UICommons.UITarget.INVENTORY)
Narrate("背包窗口可以管理物品、装备、药剂和收集品。")
HighlightUI(UICommons.UITarget.NONE)
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.UI_EXPLAINED)
Action(OnMainChoice)
# Main choice loop
func HasAllIngredients() -> bool:
return HasItem(DB.GetCellHash("Maggot Slime"), 6) and HasItem(DB.GetCellHash("Water Bottle")) and HasItem(DB.GetCellHash("Cactus Drink"))
func OnMainChoice():
var sideQuestState : int = GetQuest(ProgressCommons.Quest.ELANORE_POTION)
if sideQuestState == ProgressCommons.ELANORE_POTION.STARTED and HasAllIngredients():
Choice("我带来了你要的材料。", OnPotionQuestTurnIn)
elif sideQuestState == ProgressCommons.ELANORE_POTION.STARTED:
Choice("药剂材料还差什么?", OnPotionQuestReminder)
else:
Choice("有什么我能帮忙的吗?", OnHelpWithPotions)
Choice("卡奥雷 是什么?", OnExplainKaore)
Choice("你是谁?", OnExplainSelf)
if GetQuest(ProgressCommons.Quest.TUTORIAL) >= ProgressCommons.TUTORIAL.ELANORE_DONE:
Choice("谢谢,我先走了。", Farewell)
# Help with potions
func OnHelpWithPotions():
Mes("当然。我经常给守卫和居民调制治疗药剂,伤口、脱水和轻度 卡奥雷 侵蚀都能先稳住。")
Mes("药剂需要材料。你刚恢复,先做点不太危险的收集工作,也能熟悉城外生态。")
Mes("我需要 6 份沙虫黏液、1 瓶水和 1 杯仙人掌饮料。带回来后,我会给你一瓶仙人掌药剂。")
SetQuest(ProgressCommons.Quest.ELANORE_POTION, ProgressCommons.ELANORE_POTION.STARTED)
if GetQuest(ProgressCommons.Quest.TUTORIAL) < ProgressCommons.TUTORIAL.ELANORE_DONE:
OnSendToKael()
else:
OnMainChoice()
func OnPotionQuestReminder():
Mes("还需要 6 份沙虫黏液、1 瓶水和 1 杯仙人掌饮料。收齐后再来找我。")
OnMainChoice()
func OnPotionQuestTurnIn():
Mes("谢谢你。这些材料够我马上调一批新的治疗药剂。")
Mes("在城门边帮人,比困在高塔或王宫里讨论规矩有用得多。图利姆沙的领导者真该多看看这里。")
RemoveItem(DB.GetCellHash("Maggot Slime"), 6)
RemoveItem(DB.GetCellHash("Water Bottle"))
RemoveItem(DB.GetCellHash("Cactus Drink"))
SetQuest(ProgressCommons.Quest.ELANORE_POTION, ProgressCommons.ELANORE_POTION.INACTIVE)
AddItem(DB.GetCellHash("Cactus Potion"))
OnMainChoice()
# What is 卡奥雷
func OnExplainKaore():
Mes("玛纳 是连接生命的能量。卡奥雷 则像腐败的 玛纳,它不滋养生命,只会扭曲、侵蚀,让生物变得狂躁甚至半死不活。")
LookAtNpc("Nina")
Mes("如果你想知道更多,去问我的学徒 妮娜。她守着城里的 灵魂石碑,对 玛纳 和 卡奥雷 的解释比我更系统。")
ResetCamera()
OnMainChoice()
# Who are you
func OnExplainSelf():
Mes("我是埃拉诺,图利姆沙和周边土地的 卡维。外人通常把我们称作德鲁伊。")
Mes("我所属的 卡乌马图阿 传承守护着很古老的知识。不过现在你不用背这些名词。先活下来,再慢慢了解这个世界。")
OnMainChoice()
func Farewell():
if randi() % 2:
Chat("出城前检查水和药。")
else:
Chat("别一个人硬闯沙暴。")
# Tutorial conclusion
func OnSendToKael():
Mes("我不能把你留在这里一整天。你需要在城里站稳脚跟。")
Mes("看守凯尔正负责城墙内侧的巡逻和仙人掌田。如果你想找点正经事做,先去见他。")
LookAtNpc("Kael")
Mes("他就在东北边,靠近几棵棕榈树。")
Mes("沿着这面墙走到拐角,再往上走就能看见他。")
ResetCamera()
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.ELANORE_DONE)
DisplayActions(["gp_interact", "gp_target"])
Narrate("靠近 角色 后使用互动键可以交谈,也可以先用目标键选中对象。")
func OnSendToKaelForCheck():
Mes("先去找凯尔。")
Mes("你的身体能不能撑住外面的风、你醒来前发生了什么、还有初始宠物的选择,都由他来说明。")
LookAtNpc("Kael")
Mes("他就在东北边,靠近几棵棕榈树。沿着这面墙走到拐角,再往上走就能看见他。")
ResetCamera()
DisplayActions(["gp_interact", "gp_target"])
Narrate("靠近凯尔后使用互动键交谈。")
@@ -0,0 +1,7 @@
extends NpcScript
#
func OnAreaEnter(player : PlayerAgent):
if player and not player.ownScript:
if player.progress.GetQuest(ProgressCommons.Quest.TUTORIAL) == ProgressCommons.TUTORIAL.INACTIVE:
own.Interact(player)
@@ -0,0 +1,12 @@
extends NpcScript
#
func OnStart():
match GetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND):
ProgressCommons.GRAIN_IN_THE_SAND.STARTED:
OnSearch()
func OnSearch():
SetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND, ProgressCommons.GRAIN_IN_THE_SAND.SEARCHED_CRATES)
Mes("深蓝色蜡封上压着 阿尔蒂斯 的纹章。就是这一桶。")
Mes("里面是几袋磨得很细的面粉,和里斯基姆描述的一样。")
+107
View File
@@ -0,0 +1,107 @@
extends NpcScript
#
const QUEST_ID : int = ProgressCommons.Quest.TULIMSHAR_OLD_FRIENDSHIP
var sealedLettersID : int = DB.GetCellHash("Sealed Letters")
var heavyEnvelopeID : int = DB.GetCellHash("Heavy Envelope")
#
func OnStart():
var questState : int = GetQuest(QUEST_ID)
match questState:
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.INACTIVE:
QuestInactive()
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.STARTED:
QuestStarted()
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.ENVELOPES_FOUND:
QuestEnvelopesFound()
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.LETTERS_DELIVERED:
QuestRewards()
ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.REWARDS_WITHDREW:
QuestCompleted()
func QuestInactive():
Mes("小心脚下,新的仙人掌刚冒头,刺比看起来更快。")
Mes("抱歉,我不太习惯有人来这张长椅边聊天。离开宫里以后,访客就少多了。")
Mes("我以前是宫廷顾问。几年前辞了职,那地方每个人肩上都压着太多东西。")
Mes("现在这里只有我、土和仙人掌。至少种出来的东西能帮城里人熬过旱季。")
Mes("你有没有反复想过同一个决定?如果当时换一种说法、晚一点开口,事情会不会完全不同?")
Choice("你在想什么?", Lore)
Choice("我该走了。", Dismiss)
func QuestStarted():
Mes("还在找吗?西墙走廊不欢迎没有许可的人。")
Choice("再说说博恩斯。", Lore)
Choice("信在哪里?", Directions)
Choice("我会处理。", Dismiss)
func QuestEnvelopesFound():
Mes("你找到了。好,好。")
Mes("把封好的信交给议员博恩斯。我刚才看见你离开后,他立刻进了走廊。")
Mes("他的直觉一直很准。也可能是你太努力不被发现,反而弄出了动静。总之,他会在那里面。")
func QuestRewards():
if HasItem(heavyEnvelopeID):
Mes("你回来了。他说了什么。")
Mes("这个信封,很重。")
Mes("...")
Mes("金币。他把金币放在这里,还写了我的名字。")
Mes("女王 第一次把我们派到同一段城墙时,我们各自留了一份钱给对方。万一哪天事情糟到必须离开,至少还有路费。")
Mes("我以为我走之后,他早就把这份用掉了。如果他真用了,我也不会怪他。")
Mes("我配不上这封信。它该给一个比我更懂得做朋友的人。")
Mes("拿着吧。你一个下午做成的事,比我们这些年做得都多。")
RemoveItem(heavyEnvelopeID)
AddGP(1000)
SetQuest(QUEST_ID, ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.REWARDS_WITHDREW)
else:
Mes("你找到博恩斯了吗?他在西墙走廊里。")
func QuestCompleted():
Mes("今年的仙人掌长势不错。看起来,只要根还在,很多东西都能重新开始。")
Mes("我一直在想,我应该去那条走廊。不是今天,但我已经决定会去。")
Mes("他一直留着那些信。这说明有些门并没有真的关死,对吧?")
func Lore():
Mes("我曾经有个同僚,议员博恩斯。很聪明的人,什么都能修,什么都能造。")
Mes("只是他话少。比起解释一堵墙为什么该重修,他宁愿直接把墙拆了再砌好。")
Mes("而我负责和人打交道:排班、调解争执、让每个人知道下一步该做什么。")
Mes("我们一起撑住这些城墙很多年。在 女王 手下,这不算小事。")
Choice("后来发生了什么?", Conflict)
Choice("我该走了。", Dismiss)
func Conflict():
Mes("女王 的命令越来越多,而且永远要按她的方式做。")
Mes("我开始制定规程。什么事都写流程,什么错误都加一条规则。我以为只要把一切整理清楚,压力就不会压垮大家。")
Mes("博恩斯不这么看。他说我只是把更多压力堆到已经快撑不住的人身上。")
Mes("可他也会直接动手、直接决定,不告诉我,也不告诉别人。我总是在事后才知道。")
Mes("我们都想守住同一段墙。只是走着走着,我们不再讨论怎么守,而是在争谁的办法才算正确。")
Mes("现在回头看,我犯过太多错,没资格说自己全对。但那时候,我们谁也不肯退一步。")
Mes("最后他让我离开。我真的离开了。我不为这件事骄傲,只是当时觉得自己碍了所有人的路。")
Mes("那已经是很多年前了,可我还是常常想起。")
if GetQuest(QUEST_ID) == ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.INACTIVE:
Choice("我能帮上什么吗?", Quest)
Choice("听起来很遗憾。", Dismiss)
func Quest():
Mes("其实,也许有。")
Mes("就在我们左边的西墙走廊尽头,穿过议员博恩斯的巡逻区,有个小房间,里面有一排书架。")
Mes("书架上有一个信封。那是我们当年被 女王 分派到不同岗位时写给彼此的信,在一切变坏之前。")
Mes("我不知道它能不能改变什么。但也许他重新读到那些字,会想起我们并不总是这样。")
Mes("走廊有守卫巡逻。贴着阴影走,别站进亮处。被发现的话,守卫会把你带出去。")
Choice("我去取。", Accept)
Choice("现在不行。", Decline)
func Directions():
Mes("就在左边西墙走廊的尽头,图书架上方。")
Mes("记住,别进亮处。守卫接到的是严格命令:没有许可的人一律带离。")
func Accept():
SetQuest(QUEST_ID, ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.STARTED)
Mes("谢谢你,真的。")
Directions()
func Decline():
Chat("我明白。这对陌生人来说,确实要求太多了。")
func Dismiss():
Chat("好吧。仙人掌不会自己照顾自己。")
@@ -0,0 +1,58 @@
extends NpcScript
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.TUTORIAL)
if questState >= ProgressCommons.TUTORIAL.EKINU_DONE:
Mes("你就是艾基努临时收下的新人?很好,至少你还知道先问守门人。")
Mes("出城之后,沙子不会听你解释,怪物也不会等你准备好。想问什么,现在问。")
OnTutorialResume()
else:
Mes("离城门远点。还没得到巡逻队许可的人,不要堵在这里。")
# Common questions
func OnTutorialResume():
Choice("我该怎么变强?", OnStat)
Choice("如果迷路了怎么办?", OnMinimap)
Choice("背包和快捷栏怎么用?", OnShortcut)
Choice("城外最该小心什么?", OnWarning)
Choice("没问题了。", Farewell)
func Farewell():
Chat("活着回来。别让我多写一份报告。")
# Explanations
func OnStat():
Mes("完成委托、战斗、探索和帮助别人都会让你成长。别只盯着一条路,托诺里 的麻烦从来不排队。")
HighlightUI(UICommons.UITarget.STAT)
Narrate("升级后可以获得属性点。按照你的战斗方式分配,再确认选择。")
Narrate("力量提高物理伤害、移动速度和负重。")
Narrate("敏捷影响攻击节奏、攻击距离和闪避能力。")
Narrate("体质提高生命值、恢复能力和整体防御。")
Narrate("耐力让你跑得更久、恢复更快,也更容易连续行动。")
Narrate("专注影响 玛纳,并让技能更稳定地发挥效果。")
HighlightUI(UICommons.UITarget.STATINDICATOR)
Narrate("出城前看一眼生命、玛纳 和耐力。带着空条冲进荒野,不叫勇敢,叫给巡逻队添工作。")
HighlightUI(UICommons.UITarget.NONE)
Mes("你会需要每一点优势。沙漠里的敌人不一定强,但它们很擅长等你犯错。")
OnTutorialResume()
func OnMinimap():
Mes("地图是你的第二双眼睛。看不清路时先看地图,别硬凭记忆穿沙暴。")
HighlightUI(UICommons.UITarget.MINIMAP)
Narrate("迷路时可以在地图上确认目标位置,再朝目标移动。")
HighlightUI(UICommons.UITarget.NONE)
OnTutorialResume()
func OnShortcut():
Mes("常用的药、捕捉道具和技能都放到顺手的位置。等沙蝎扑到脸上再翻包,就太晚了。")
HighlightUI(UICommons.UITarget.ACTION_BAR)
Narrate("可以把常用物品、技能或动作放进快捷栏,战斗和探索时更快使用。")
HighlightUI(UICommons.UITarget.NONE)
OnTutorialResume()
func OnWarning():
Mes("第一,小心风。风沙会遮住路,也会遮住正在靠近你的东西。")
Mes("第二,别把宠物当成工具。它们会救你的命,但前提是你也照顾它们。")
Mes("第三,别相信王宫说的每一句话。巡逻队守的是城,不是红女王的面子。")
OnTutorialResume()
+232
View File
@@ -0,0 +1,232 @@
extends NpcScript
# Behaviour
const ThresholdHandValue : int = 17
# Card utilities
static var RANKS : PackedStringArray = ["王牌", "2", "3", "4", "5", "6", "7", "8", "9", "10", "侍从", "王后", "国王"]
static var SUITS : PackedStringArray = [
"黑桃",
"红心",
"方片",
"梅花"
]
# Game state
var deck : Array = []
var playerHand : Array = []
var dealerHand : Array = []
# Helper
static func _CardName(card : int) -> String:
return "%s%s" % [SUITS[floori(card / 13.0)], RANKS[card % 13]]
static func _CardValue(card : int) -> int:
var rank : int = card % 13
if rank == 0: return 11
if rank >= 10: return 10
return rank + 1
static func _HandValue(hand : Array) -> int:
var value : int = 0
var aces : int = 0
for card : int in hand:
value += _CardValue(card)
if card % 13 == 0:
aces += 1
while value > 21 and aces > 0:
value -= 10
aces -= 1
return value
static func _HandStr(hand : Array) -> String:
var parts : PackedStringArray = []
for card : int in hand:
parts.append(_CardName(card))
return ", ".join(parts) + " (%d)" % _HandValue(hand)
static func _NewDeck() -> Array:
var d : Array = []
d.resize(52)
for i : int in range(52):
d[i] = i
d.shuffle()
return d
# General flow
func OnStart():
Mes("欢迎,欢迎!你是来找人,还是来找一局好牌?")
Mes("听过 21 点 吗?别被名字吓到,规矩简单,胆量和分寸才是关键。")
Mes("坐下吧。图利姆沙 的风会吹乱很多东西,但吹不乱一副洗好的牌。")
DisplayChoices()
func DisplayChoices():
Choice("挑战 海兰德", StartDealer)
Choice("帮我找个对手", StartPvP)
Choice("讲讲规则", ShowRules)
Choice("今天先不玩", Decline)
func ShowRules():
Mes("规则很简单:尽量接近 21 点,但不能超过。")
Mes("数字牌按牌面算,花牌都算 10。王牌可以算 1,也可以算 11,看哪种对你更有利。")
Mes("比我点数高且不爆牌,你就赢。超过 21,就是爆牌。")
Mes("这游戏最难的不是加法,是知道什么时候该停手。许多商人一辈子都没学会。")
DisplayChoices()
func Decline():
Chat("想玩牌的时候来找我。好牌不会等人太久。")
func OnQuit():
if npc and npc.ownScript:
npc.ownScript.call("LeavePvP", own)
super.OnQuit()
# Player vs Dealer Mode
func StartDealer():
deck = _NewDeck()
playerHand = [deck.pop_back(), deck.pop_back()]
dealerHand = [deck.pop_back(), deck.pop_back()]
_DealerTurn()
func _DealerTurn():
if _HandValue(playerHand) == 21:
_DealerReveal()
else:
Mes("你的手牌:%s" % _HandStr(playerHand))
Mes("我明面上的牌:%s" % _CardName(dealerHand[0]))
Choice("要牌", _DealerHit)
Choice("停牌", _DealerReveal)
func _DealerHit():
playerHand.append(deck.pop_back())
var value : int = _HandValue(playerHand)
if value > 21:
Mes("你的手牌:%s" % _HandStr(playerHand))
Mes("哎呀,爆牌了!贪心常常只差一张牌,我见过太多次。")
Choice("再来一局", StartDealer)
Choice("离开", Decline)
elif value == 21:
_DealerReveal()
else:
_DealerTurn()
func _DealerReveal():
while _HandValue(dealerHand) < ThresholdHandValue:
dealerHand.append(deck.pop_back())
var pv : int = _HandValue(playerHand)
var dv : int = _HandValue(dealerHand)
Mes("你的手牌:%s" % _HandStr(playerHand))
Mes("我的手牌:%s" % _HandStr(dealerHand))
if dv > 21:
Mes("哈!老手也有伸手太长的时候。你赢得漂亮。")
elif pv > dv:
Mes("嗯,不错。你知道什么时候该停。")
elif dv > pv:
Mes("经验又赢了一次。别难过,多数人第一次都输给我。")
else:
Mes("%d 点平局!胆子不错,我承认这一点。" % pv)
Choice("再来一局", StartDealer)
Choice("离开", Decline)
# Player vs Player Mode
func StartPvP():
var result : int = npc.ownScript.call("JoinPvP", own)
if result == 0:
Mes("好,让我们看看还有谁有胆量坐上这张桌子。稍等。")
Choice("查看", _PvPCheck)
Choice("取消", _PvPCancel)
else:
_PvPBegin()
func _PvPCheck():
var game : Dictionary = npc.ownScript.call("GetGame", own)
if game.is_empty():
Mes("耐心点。好牌手要先学会等待。")
Choice("查看", _PvPCheck)
Choice("取消", _PvPCancel)
elif game.has("result"):
_PvPShowResult(game)
else:
_PvPBegin()
func _PvPCancel():
npc.ownScript.call("LeavePvP", own)
Chat("没有牌局?可惜。也许风向一变,你又想玩了。")
func _PvPBegin():
playerHand = npc.ownScript.call("GetHand", own)
Mes("哈!对手来了。让我看看你们谁更懂停手。")
_PvPTurn()
func _PvPTurn():
Mes("你的手牌:%s" % _HandStr(playerHand))
if _HandValue(playerHand) == 21:
Mes("21 点!")
_PvPStand()
else:
Choice("要牌", _PvPHit)
Choice("停牌", _PvPStand)
func _PvPHit():
var card : int = npc.ownScript.call("DrawCard", own)
if card < 0:
_PvPStand()
return
playerHand.append(card)
var value : int = _HandValue(playerHand)
if value > 21:
Mes("你的手牌:%s" % _HandStr(playerHand))
Mes("爆牌!刚才心里那点犹豫,其实已经给过你提醒了。")
npc.ownScript.call("FinishHand", own, value, true)
_PvPWait()
elif value == 21:
Mes("你的手牌:%s" % _HandStr(playerHand))
_PvPStand()
else:
_PvPTurn()
func _PvPStand():
npc.ownScript.call("FinishHand", own, _HandValue(playerHand), false)
_PvPWait()
func _PvPWait():
var game : Dictionary = npc.ownScript.call("GetGame", own)
if game.has("result"):
_PvPShowResult(game)
else:
Mes("你这边结束了。现在等对手做决定。")
Choice("查看", _PvPWaitCheck)
Choice("认输", _PvPForfeit)
func _PvPWaitCheck():
var game : Dictionary = npc.ownScript.call("GetGame", own)
if game.has("result"):
_PvPShowResult(game)
else:
Mes("还在思考。谨慎的人值得尊重,除非他只是忘了轮到自己。")
Choice("查看", _PvPWaitCheck)
Choice("认输", _PvPForfeit)
func _PvPShowResult(game : Dictionary):
var isP1 : bool = game.get("p1") == own
var myVal : int = game.get("p1Value") if isP1 else game.get("p2Value")
var oppBust : bool = game.get("p2Busted") if isP1 else game.get("p1Busted")
var oppHand : Array = game.get("p2Hand") if isP1 else game.get("p1Hand")
var myBust : bool = game.get("p1Busted") if isP1 else game.get("p2Busted")
Mes("你的点数:%d%s" % [myVal, "(爆牌)" if myBust else ""])
Mes("对手手牌:%s%s" % [_HandStr(oppHand), "(爆牌)" if oppBust else ""])
var result : String = game.get("result", "")
if result == "draw":
Mes("平局!势均力敌,这在我的桌上不常见。")
elif (result == "p1" and isP1) or (result == "p2" and not isP1):
Mes("胜利归你!今天牌也站在你这边。")
else:
Mes("可惜。牌有自己的脾气,但它总会再绕回来。")
npc.ownScript.call("CleanupGame", own)
Choice("再来一局", StartPvP)
Choice("离开", Decline)
func _PvPForfeit():
npc.ownScript.call("ForfeitPvP", own)
Mes("中途离桌?放在我年轻时,这至少要请全桌喝一轮。")
Choice("再来一局", StartPvP)
Choice("离开", Decline)
+160
View File
@@ -0,0 +1,160 @@
extends NpcScript
const PEYOTE_REQUIRED : int = 5
const MAGGOT_REQUIRED : int = 5
const FIELD_POSITION : Vector2 = Vector2(2912, 1312)
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.TUTORIAL)
if questState < ProgressCommons.TUTORIAL.ELANORE_DONE:
OnInitialRecovery()
elif questState == ProgressCommons.TUTORIAL.ELANORE_DONE:
OnFirstMeeting()
elif questState == ProgressCommons.TUTORIAL.KAEL_MET:
OnCheckProgress()
elif questState == ProgressCommons.TUTORIAL.KAEL_DONE:
OnSendToEkinu()
elif questState == ProgressCommons.TUTORIAL.EKINU_DONE:
OnComplete()
# Opening recovery and starter choice
func OnInitialRecovery():
var waterBottleID : int = DB.GetCellHash("Water Bottle")
var cactusSourCandyID : int = DB.GetCellHash("Cactus Sour Candy")
Mes("站稳。先让我看一下你的脸色。")
Mes("巡逻队在城外沙地边缘发现你时,你已经脱水到说不出话。埃拉诺把你从最危险的状态里拉了回来,现在轮到我确认你还能不能上路。")
Mes("抬手,握拳,深呼吸。很好。你还虚,但不是那种一阵风就能吹倒的虚。")
Mes("先拿着水和仙人掌酸糖。图利姆沙欢迎活人,不欢迎逞强的人。")
AddItem(waterBottleID)
AddItem(cactusSourCandyID)
OnOpeningBackstory()
func OnOpeningBackstory():
Mes("你是被沙漠送到城门口的陌生人。身上没有能说明身份的东西,脚印却从风暴方向断断续续延过来。")
Mes("最近 托诺里 不安稳。卡奥雷 的波动让野外宠物变得暴躁,东边还有一群把 卡奥雷 当信仰的疯子在袭击旅人。")
Mes("所以我不会让你空手出城,也不会让你在城里闲逛到下一场麻烦砸过来。")
OnStarterPetIntro()
func OnStarterPetIntro():
Mes("先选一只初始宠物。不是装饰,也不是玩伴。它会替你挡住第一口毒牙,陪你学会怎么在这里活下去。")
Mes("三只都经过巡逻队训练,性格和战斗方式不一样。")
Mes("露莉娅属水,耐久和恢复节奏稳定,适合稳扎稳打。")
Mes("啾啾鸟属风,动作快,适合先手和灵活周旋。")
Mes("绒绒兽是普通系,脾气稳,招式直接,适合不想把战斗想得太复杂的人。")
Mes("想清楚就选。没有绝对正确的答案,只有你愿意一起走下去的伙伴。")
# Initial encounter
func OnFirstMeeting():
Mes("选好伙伴了?好。能照顾宠物的人,至少比只会照顾自己的人可靠一点。")
Mes("现在说正事。我这边正缺人手。")
Mes("沙虫最近把仙人掌田啃得一塌糊涂。它们个头大,嘴却灵活,能绕开刺直接钻进茎里。")
Mes("一旦钻进去,整株仙人掌就会从里面被吃空。农户看到那种景象,脸色比沙暴天还难看。")
Choice("这些沙虫为什么这么大?", OnKaoreExplanation)
Choice("听起来很麻烦。", OnWildFauna)
Choice("我可以帮忙清理。", OnWildFauna)
func OnKaoreExplanation():
Mes("据说它们很久以前没这么夸张。可那是 卡奥雷时代 之前的事,已经没人亲眼记得。")
Mes("卡奥雷 让很多生物变得异常:有的变大,有的暴躁,有的像死了又没完全死。")
Mes("沙虫算是我们每天都能看见的麻烦版本。更糟的东西通常藏在城外。")
if GetQuest(ProgressCommons.Quest.TUTORIAL) >= ProgressCommons.TUTORIAL.KAEL_DONE:
MainChoices()
else:
Mes("闲话到此为止。我要你进田里把麻烦清掉。")
Choice("好,我开始。", OnFieldCleanUp)
Choice("城外到底发生了什么?", OnDesertExplanation)
func OnWildFauna():
Mes("不管原因是什么,它们都得被清理。")
Mes("城外的 卡奥雷 波动已经影响到城内田地,沙虫不是唯一的问题。")
Mes("有些仙人掌怪也被侵蚀了,会跳来跳去,往旁边作物上甩水,像在故意嘲笑农户。")
Choice("我去清理它们。", OnFieldCleanUp)
Choice("城外到底发生了什么?", OnDesertExplanation)
func OnDesertExplanation():
Mes("卡奥雷 正在影响各种生物。")
Mes("它会把活物扭成更糟的样子:更凶、更饿、更难控制。")
Mes("被侵蚀得太深时,它们甚至会像死物一样行动。那不是普通疾病,是一种很难洗掉的诅咒。")
if GetQuest(ProgressCommons.Quest.TUTORIAL) >= ProgressCommons.TUTORIAL.KAEL_DONE:
MainChoices()
else:
Mes("西边塔里的 玛纳伊尔 法师已经发出警告:托诺里 多处 卡奥雷 正在异常聚集。")
Choice("明白了,先处理田地。", OnFieldCleanUp)
Choice("玛纳伊尔 是谁?", OnManayir)
func OnManayir():
Mes("玛纳伊尔 是研究 玛纳 的古老组织,就在城西那座塔里。")
Mes("他们会监测 玛纳 和 卡奥雷 的流向,像预报天气一样发布警告。")
Mes("二十七年前,也是他们宣布 卡奥雷时代 结束。可结束不代表消失,只是没以前那么压得人喘不过气。")
Choice("先处理田地。", OnFieldCleanUp)
Choice("卡奥雷时代 是什么?", OnAgeOfKaore)
func OnAgeOfKaore():
Mes("我没亲眼经历过,但父辈都记得。那几百年里,卡奥雷 比 玛纳 更常见,变异生物和不死怪物到处都是。")
Mes("我们现在抱怨沙虫和仙人掌怪,其实已经比那时候幸运多了。")
Choice("先处理眼前的麻烦。", OnFieldCleanUp)
# Task assignment
func OnFieldCleanUp():
OnFightTutorial()
Mes("先从北边田里的仙人掌怪开始。")
LookAtPosition(FIELD_POSITION)
Mes("它们是被 卡奥雷 侵蚀的仙人掌,对人不算致命,但已经没法种植。")
Mes("它们会乱跳、甩水、惊扰宠物,还会把旁边作物弄坏。")
ResetCamera()
Mes("清理 5 只仙人掌怪和 5 只沙虫,今天的田地就能暂时安全。")
HighlightUI(UICommons.UITarget.PROGRESS)
Narrate("任务进度会记录你正在进行的委托和战斗目标。")
HighlightUI(UICommons.UITarget.NONE)
Narrate("图鉴和任务界面会帮助你确认已击败或遇见的宠物与怪物。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.KAEL_MET)
# Progress check
func OnCheckProgress():
var peyoteKills : int = GetBestiary("Peyote".hash())
var maggotKills : int = GetBestiary("Maggot".hash())
if peyoteKills >= PEYOTE_REQUIRED and maggotKills >= MAGGOT_REQUIRED:
OnTaskComplete()
elif peyoteKills < PEYOTE_REQUIRED:
Mes("还没完成。田里还需要再清理 %d 只仙人掌怪。" % (PEYOTE_REQUIRED - peyoteKills))
else:
Mes("仙人掌怪处理得不错,但还差 %d 只沙虫。" % (MAGGOT_REQUIRED - maggotKills))
# Task completion
func OnTaskComplete():
Mes("干得漂亮。")
Mes("你处理得比我预想的快,也比很多正式巡逻员稳。")
Mes("去向看守艾基努汇报吧。告诉他,我认可你今天的表现。")
Mes("我们很快要组织一支小队去沙漠侦察,你可能正适合帮忙。")
SetQuest(ProgressCommons.Quest.TUTORIAL, ProgressCommons.TUTORIAL.KAEL_DONE)
AddExp(50)
#
func OnSendToEkinu():
Mes("准备好后去找看守艾基努。")
Mes("他会告诉你下一步怎么做。")
MainChoices()
#
func OnComplete():
Mes("很高兴又见到你。你看,沙虫还是不肯给我放假。")
MainChoices()
#
func MainChoices():
Choice("这些沙虫为什么这么异常?", OnKaoreExplanation)
Choice("我该怎么战斗?", OnFightTutorial)
Choice("城外到底发生了什么?", OnDesertExplanation)
Choice("我先走了。", Farewell)
func Farewell():
Chat("出城后别只看前面,背后也可能有东西靠近。")
func OnFightTutorial():
Mes("这不是让你亲手去砍仙人掌怪。让你的宠物上。")
Mes("靠近野外宠物会进入宠物对战。进入战斗后,用战斗指令释放技能,用宠物指令切换队伍成员。")
Mes("把对手削弱后,可以用捕捉符尝试收服。别一上来就丢,满体力的野外宠物通常不会乖乖进符。")
DisplayActions(["gp_interact", "gp_target"])
Narrate("接触野外宠物会进入对战。战斗菜单中可以使用技能、捕捉符、切换宠物或逃跑。")
@@ -0,0 +1,21 @@
extends NpcScript
#
const QUEST_ID : int = ProgressCommons.Quest.TULIMSHAR_OLD_FRIENDSHIP
var sealedLettersID : int = DB.GetCellHash("Sealed Letters")
var heavyEnvelopeID : int = DB.GetCellHash("Heavy Envelope")
#
func OnStart():
var questState : int = GetQuest(QUEST_ID)
if questState != ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.STARTED:
return
if HasItemsSpace([[sealedLettersID, 1], [heavyEnvelopeID, 1]]):
Mes("你在积灰的书本之间找到了两个旧信封。")
Mes("一个封得很仔细,另一个意外地沉。")
SetQuest(QUEST_ID, ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.ENVELOPES_FOUND)
AddItem(sealedLettersID)
AddItem(heavyEnvelopeID)
else:
Mes("你看见了信封,但背包太满,已经装不下它们。")
@@ -0,0 +1,95 @@
extends NpcScript
class_name TulimsharWestWallLightTrigerGlobal
#
const QUEST_ID : int = ProgressCommons.Quest.TULIMSHAR_OLD_FRIENDSHIP
const GUARD_SPEED_BOOST : int = 200
const APPROACH_DISTANCE : float = 48.0
#
static var activeCatches : Dictionary[int, NpcAgent] = {}
#
func OnAreaEnter(player : PlayerAgent):
CallGuard(player)
static func CallGuard(player : PlayerAgent):
if not ActorCommons.IsAlive(player):
return
var playerRID : int = player.get_rid().get_id()
if activeCatches.has(playerRID):
return
var questState : int = player.progress.GetQuest(QUEST_ID)
if questState >= ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.LETTERS_DELIVERED:
return
if player.SetState(ActorCommons.State.TRIGGER):
var inst : WorldInstance = WorldAgent.GetInstanceFromAgent(player)
if inst:
var guard : BaseAgent = SpawnGuard(inst)
if guard:
activeCatches[playerRID] = guard
NpcCommons.PushNotification(player, "被守卫发现了!")
Callback.AddCallback(player.tree_exiting, Cleanup, [player, guard, playerRID], ConnectFlags.CONNECT_ONE_SHOT)
Callback.OneShotCallback(guard.ready, OnGuardReady.bind(player, guard, playerRID))
static func SpawnGuard(inst : WorldInstance) -> BaseAgent:
var spawn : SpawnObject = SpawnObject.new()
spawn.map = inst.map
spawn.type = "Npc"
spawn.nick = "图利姆沙 Guard" if randi() % 2 == 0 else "图利姆沙 Sbire"
spawn.id = spawn.nick.hash()
spawn.spawn_position = Vector2i(2688, 1472) # tile (84, 46)
spawn.spawn_offset = Vector2i.DOWN
spawn.player_script = "tonori/tulimshar/PatrolGuardCaught.gd"
return WorldAgent.CreateAgent(spawn, inst.id)
static func OnGuardReady(player : PlayerAgent, guard : NpcAgent, playerRID : int):
if not guard or not ActorCommons.IsAlive(player):
Cleanup(player, guard, playerRID)
return
var guardRID : int = guard.get_rid().get_id()
Launcher.World.BulkPreload(guard, guardRID, player.peerID)
player.CheckVisibility(guard)
# Wait one full update cycle for the SetData to be called
StartGuardNavigation.call_deferred(player, guard, playerRID)
static func StartGuardNavigation(player : PlayerAgent, guard : NpcAgent, playerRID : int):
if not guard or not ActorCommons.IsAlive(player):
Cleanup(player, guard, playerRID)
return
NpcCommons.AddModifier(guard, CellCommons.Modifier.WalkSpeed, GUARD_SPEED_BOOST)
AI.Stop(guard)
# Check if in correct distance to start the guard interaction
if (guard.position - player.position).length() < APPROACH_DISTANCE / 2.0:
OnGuardArrived(player, guard, playerRID)
# Stop right before the player position but still within the trigger position
else:
var stopPos : Vector2 = player.position + (guard.position - player.position).normalized() * APPROACH_DISTANCE / 2.0
guard.WalkToward(stopPos)
Callback.OneShotCallback(guard.agent.navigation_finished, OnGuardArrived.bind(player, guard, playerRID))
static func OnGuardArrived(player : PlayerAgent, guard : NpcAgent, playerRID : int):
if not guard or not ActorCommons.IsAlive(player):
Cleanup(player, guard, playerRID)
return
guard.Interact(player)
static func Cleanup(player : PlayerAgent, guard : NpcAgent, playerRID : int):
activeCatches.erase(playerRID)
if player and is_instance_valid(player):
if player.ownScript:
NpcCommons.ToggleContext(player, false)
player.ClearScript()
if player.state == ActorCommons.State.TRIGGER:
player.SetState(ActorCommons.State.TRIGGER)
if guard and is_instance_valid(guard):
WorldAgent.RemoveAgent.call_deferred(guard)
+48
View File
@@ -0,0 +1,48 @@
extends NpcScript
#
func OnStart():
if own.stat and own.stat.level < 5:
Mes("新面孔?欢迎来到图利姆沙。")
Mes("我是玛茜,平时帮外来者认路,也帮本地人把话说清楚。")
Mes("这里风沙大,脾气也容易被吹硬。记住一件事:在城里别随便羞辱别人,我们靠互相照应活下去。")
else:
Mes("想找路、打听城里的事,或者只是想听几句闲话,都可以问我。")
OnMainChoice()
# Main choice loop
func OnMainChoice():
Choice("给我讲讲这座城。", OnCityOverview)
Choice("住在这里是什么感觉?", OnLiveHere)
Choice("最近城里有什么传闻?", OnRumors)
Choice("我该去哪里补给?", OnSupplies)
Choice("我先走了。", Farewell)
# Answers
func OnCityOverview():
Mes("图利姆沙夹在东西两侧的山丘之间,像一只缩在石壳里的沙漠甲虫。")
Mes("城墙挡住了南边的沙暴,也挡住了许多被 卡奥雷 逼疯的野兽。")
Mes("城门附近是巡逻和救治伤员的地方;中央区有市场、面包铺和小游戏;再往北就是港口,外来的货物大多从那里上岸。")
Mes("如果你刚醒来没多久,先熟悉城门、市场和港口这三处就够了。")
OnMainChoice()
func OnLiveHere():
Mes("这里的人嘴上总说自己习惯了沙漠,其实谁都知道,一个人是扛不过 托诺里 的。")
Mes("水井坏了会有人去修,商船晚到会有人分面包,巡逻队缺人时也总有人硬着头皮顶上。")
Mes("当然,王宫里的人不一定这么想。他们更喜欢命令和税单。")
OnMainChoice()
func OnRumors():
Mes("最近传得最多的是两件事。第一,红女王又想重开沙漠风暴矿洞。")
Mes("第二,城外的宠物和怪物越来越不安分,沙虫啃坏了仙人掌田,沙蝎也开始靠近旧矿道。")
Mes("如果你能收服几只可靠的伙伴,巡逻队会更愿意相信你不是来添乱的。")
OnMainChoice()
func OnSupplies():
Mes("找吃的去里斯基姆的面包铺,找水先看井和商贩,想买药就去找埃拉诺。")
Mes("如果要出城,别只带一只宠物。沙地里最糟糕的不是迷路,是你以为自己还能再撑一场战斗。")
OnMainChoice()
# Farewell
func Farewell():
Chat("风向变得太快时,就先回城墙边歇一会儿。")
+135
View File
@@ -0,0 +1,135 @@
extends NpcScript
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.NINA_HUNGRY)
if questState == ProgressCommons.NINA_HUNGRY.INACTIVE:
OnIntro()
else:
Mes("又见面了。灵魂石碑 今天很安静,这是好事。")
OnPlayerChoice()
# Intro
func OnIntro():
Mes("你好。是埃拉诺让你来的?欢迎。")
Mes("你面前的是图利姆沙古老的 灵魂石碑。我负责守护它,也用它的力量保护城里的人。")
Mes("当然,是在王宫允许的范围内。这里很多事都要加上这句话。")
OnPlayerChoice()
func OnPlayerChoice():
if GetQuest(ProgressCommons.Quest.NINA_HUNGRY) == ProgressCommons.NINA_HUNGRY.STARTED and HasItem(DB.GetCellHash("Croissant")):
Choice("我给你带来了可颂。", OnCroissantTurnIn)
else:
Choice("有人在阻止你使用 石碑?", OnExplainOpposition)
Choice("讲讲 玛纳 和 卡奥雷。", OnExplainMana)
Choice("灵魂石碑 是什么?", OnExplainMenhir)
Choice("我先走了。", Farewell)
# Opposition and faith
func OnExplainOpposition():
Mes("不是直接阻止。卡维,也就是外人口中的德鲁伊,一直被允许留在图利姆沙。没有我们,这座城很难熬过干旱和 卡奥雷 侵袭。")
Mes("但王国官方信奉 萨维安教义。他们认为 玛纳 和 卡奥雷 一样危险,都应该尽量远离。")
Mes("有趣的是,公开反对 玛纳 的人,家人受伤或田地缺雨时,还是会来找我们。")
Mes("我只是希望大家能更诚实地面对古老传统。萨维安 的恐惧已经让世界吃过太多苦,现在还催生了崇拜 卡奥雷 的 瓦鲁尼亚人。")
Choice("卡维 怎么看待 玛纳?", OnKahwePosition)
Choice("我想问别的事。", OnPlayerChoice)
Choice("谢谢你,我先走了。", Farewell)
func OnKahwePosition():
Mes("卡维 传承的是 玛纳 与生命和谐共处的知识。我们想恢复 汉图 被毁后失去的平衡。")
Choice("汉图 是什么?", OnExplainHantu)
Choice("我想问别的事。", OnPlayerChoice)
Choice("谢谢你,我先走了。", Farewell)
func OnExplainHantu():
Mes("汉图 也叫 玛纳树,曾经像世界生命力的心脏,能自然引导 玛纳。")
Mes("古代有许多 汉图,它们稳定 玛纳,防止 玛纳 腐败成 卡奥雷。")
Mes("后来战争和 萨维安教义 的恐惧摧毁了它们。统治者试图创造一个没有 玛纳 的世界。")
Mes("结果你已经看见了:玛纳 没有消失,只是失去平衡,留下了 卡奥雷。")
Choice("现在局势怎样?", OnCurrentSituation)
Choice("我想问别的事。", OnPlayerChoice)
Choice("谢谢你,我先走了。", Farewell)
func OnCurrentSituation():
Mes("看你问谁。红女王会说普通人离魔法越远越好。")
Mes("可她自己在王宫里照样使用魔法。她真正的意思是:力量最好只掌握在少数人手里。")
Mes("我相信魔法是世界的一部分。水也危险,但没人会因为会溺水就封掉所有水井。")
Choice("红女王是什么人?", OnRedQueen)
Choice("有什么我能帮忙的吗?", OnAskForHelp)
func OnRedQueen():
Mes("她统治图利姆沙,也自称统治整个 托诺里。可城墙之外,听她命令的人并不多。")
Mes("祖尼 部族从未承认这个王国。她越控制不了外面,就越想控制城里的人。")
Mes("祖尼 仍保留古老魔法,这让他们很难被征服。红女王不想图利姆沙居民也拥有那种独立性。")
Choice("讲讲 祖尼。", OnZuni)
Choice("有什么我能帮忙的吗?", OnAskForHelp)
func OnZuni():
Mes("祖尼 在 托诺里 生活了很久,久到他们的故事里还记得这片土地不是沙漠时的样子。")
Mes("他们和图利姆沙一直有贸易,也大多友好。前提是我们别把士兵派得太远。")
Mes("一旦王国越界,他们就会反击。我尊重这一点:他们知道边界在哪里,也愿意守住家园。")
Choice("听起来我该更谨慎些。", OnZuniDismissal)
Choice("我想见见他们。", OnZuniMarket)
func OnZuniDismissal():
Mes("谨慎总比傲慢好。")
OnZuniMarket()
func OnZuniMarket():
Mes("北边市场里常有 祖尼 商人。只要你尊重他们,买不买东西都能聊上几句。")
Choice("有什么我能帮忙的吗?", OnAskForHelp)
# 玛纳 and 卡奥雷
func OnExplainMana():
Mes("玛纳 常被称作生命力,是活物之间流动的能量。")
Mes("正确引导 玛纳,可以施展魔法、治疗土地、维持生命。")
Mes("卡奥雷 则是 玛纳 与源头断裂后腐败留下的东西。它不滋养生命,只会侵蚀、扭曲,让生物变得敌对。")
Mes("这座 灵魂石碑 用 泽莱石 制成。泽莱石 能储存和释放 玛纳,所以它才能保护城门附近。")
OnPlayerChoice()
# 灵魂石碑 and 泽莱石
func OnExplainMenhir():
Mes("灵魂石碑 是用巨大 泽莱石 雕成的古代石碑。卡维 会借它引导并扩散 玛纳。")
Mes("它启动时能形成保护性的气场,驱散被 卡奥雷 侵蚀的生物,也能稳定周围土地。")
Mes("在 卡维 协助下,它还能治疗重伤者。某些时候,甚至能把濒死者的灵魂拉回安全处。")
Choice("泽莱石 是什么?", OnExplainZielite)
Choice("我想问别的事。", OnPlayerChoice)
Choice("谢谢你,我先走了。", Farewell)
func OnExplainZielite():
Mes("泽莱石 是一种天然亲近 玛纳 的稀有矿物,能吸收、储存并释放 玛纳。")
Mes("过去它并不少见,但 萨维安教义 掀起反 玛纳 清洗后,很多矿脉和工艺都被毁了。")
Mes("现在 泽莱石 多藏在护符、遗迹和少数仍然站立的 灵魂石碑 里。")
OnPlayerChoice()
# Hungry quest
func OnAskForHelp():
var questState : int = GetQuest(ProgressCommons.Quest.NINA_HUNGRY)
if questState == ProgressCommons.NINA_HUNGRY.STARTED:
Mes("暂时没有。只是,如果你真的路过面包铺,我还在惦记那份点心。")
OnPlayerChoice()
elif questState == ProgressCommons.NINA_HUNGRY.REWARDS_WITHDREW:
Mes("暂时没有,谢谢你还记得问。那份可颂让我今天好多了。")
OnPlayerChoice()
else:
Mes("暂时没有。")
Mes("不过我有点饿。不是要你帮我买吃的!我只是从早上开始就没离开过 石碑。")
Choice("我去市场时帮你看看点心。", OnStartHungryQuest)
Choice("那你记得休息。", Farewell)
func OnStartHungryQuest():
Mes("你真好。我平时不会开口,但现在如果有一份可颂,确实会像小小的奇迹。")
Mes("市场的面包铺通常能买到。我要是离开太久,石碑 这边没人照看。")
SetQuest(ProgressCommons.Quest.NINA_HUNGRY, ProgressCommons.NINA_HUNGRY.STARTED)
func OnCroissantTurnIn():
Mes("天啊。")
Mes("真的是可颂!我没想到你会记得。")
RemoveItem(DB.GetCellHash("Croissant"))
SetQuest(ProgressCommons.Quest.NINA_HUNGRY, ProgressCommons.NINA_HUNGRY.REWARDS_WITHDREW)
AddItem(DB.GetCellHash("Cactus Potion"), 10)
AddGP(100)
Mes("请收下这些仙人掌药剂,还有一点钱,至少补上你花掉的费用。")
func Farewell():
Chat("愿 灵魂石碑 保佑你一路平安。")
@@ -0,0 +1,18 @@
extends NpcScript
#
const ENTRANCE_POS : Vector2 = Vector2(1536, 1600) # tile (48, 50)
#
func OnStart():
Mes("站住。")
Mes("议员博恩斯的命令:没有许可,任何人不得进入这些走廊。")
Mes("我不管你为什么来。转身,出去。")
Action(Escort)
func Escort():
if own.state == ActorCommons.State.TRIGGER:
own.SetState(ActorCommons.State.TRIGGER)
var entranceMapID : int = "图利姆沙 Center".hash()
Action(NpcCommons.Warp.bind(own, entranceMapID, ENTRANCE_POS))
+111
View File
@@ -0,0 +1,111 @@
extends NpcScript
# Reward items
var croissantID : int = DB.GetCellHash("Croissant")
var cactusSourCandyID : int = DB.GetCellHash("Cactus Sour Candy")
#
func OnStart():
var questState : int = GetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND)
match questState:
ProgressCommons.GRAIN_IN_THE_SAND.INACTIVE:
OnInactive()
ProgressCommons.GRAIN_IN_THE_SAND.SEARCHED_CRATES:
OnReward()
ProgressCommons.GRAIN_IN_THE_SAND.REWARDS_WITHDREW:
OnComplete()
_:
OnKeepLooking()
# Quest states
func OnInactive():
Mes("欢迎,旅人。你来得正巧,也来得不太巧:架子上的面包刚刚卖空。")
Mes("图利姆沙 没有像样的农田,城外除了沙、风和仙人掌,几乎什么都长不稳。")
Mes("我的面粉都从 阿尔蒂斯 坐船运来。靠它做出的 沙暴面包,已经养活这座城很多年了。")
Mes("今天清晨有一批货到了码头,可这天气来回跑一趟,回来就没力气揉面了。")
Mes("阿尔蒂斯 面包坊的货桶上有深蓝色蜡封。你能去码头帮我把里面的面粉袋带回来吗?")
QuestChoice()
func QuestChoice(previousChoice : int = -1):
Choice("我去码头看看。", OnAccept)
if previousChoice != 1:
Choice("沙暴面包 是什么?", OnAskBread)
if previousChoice != 2:
Choice("沙漠里真的种不出粮食吗?", OnAskDesert)
Choice("晚点再说。", OnDecline)
func OnKeepLooking():
Mes("码头那边有线索吗?记住,找带深蓝色蜡封的货桶。")
Mes("我会把炉火留着,面粉一到就能开工。")
func OnComplete():
Mes("就算热得像站在炉膛旁边,刚出炉的面包也没人舍得错过。")
CompleteChoice()
func CompleteChoice(previousChoice : int = -1):
if previousChoice != 0:
Choice("给我讲讲 阿尔蒂斯。", OnAskArtis)
if previousChoice != 1:
Choice("图利姆沙 靠什么活下来?", OnAskCity)
if previousChoice != 2:
Choice("沙暴面包 是什么?", OnAskBread)
if previousChoice != 3:
Choice("沙漠里真的种不出粮食吗?", OnAskDesert)
if previousChoice != -1:
Choice("你忙吧。", OnFarewell)
# Optional dialogue
func OnAskBread():
Mes("我的招牌配方:阿尔蒂斯 的细面粉、仙人掌汁代替清水,再加一撮晒干的沙盐。")
Mes("这种面包外壳结实,穿过沙暴也不容易碎,第二天早上还带着香气,所以才叫 沙暴面包。")
if IsQuestCompleted(ProgressCommons.Quest.GRAIN_IN_THE_SAND):
CompleteChoice(2)
else:
QuestChoice(1)
func OnAskDesert():
Mes("能长的主要是仙人掌。它们很硬气,我们也很会利用:糖果、饮料、药膏,什么都能做。")
Mes("可粮食不一样。托诺里 从来不是产麦子的地方,没有商船,城里的炉火会先灭,人的肚子也会跟着空。")
if IsQuestCompleted(ProgressCommons.Quest.GRAIN_IN_THE_SAND):
CompleteChoice(3)
else:
QuestChoice(2)
func OnAskArtis():
Mes("阿尔蒂斯 是海对岸 奥罗拉 海岸上的大港,农田肥沃,工匠也多。")
Mes("我妹妹就住在那里。她烤的小饼干特别香,我这里用的面粉也来自她的面包坊。")
Mes("图利姆沙 市场上一半的货都靠 阿尔蒂斯 商船补给。少了它们,我们能吃的就只剩仙人掌和耐心了。")
CompleteChoice(0)
func OnAskCity():
Mes("贸易。图利姆沙 最擅长的不是种地,而是让来自三片大陆的货物在这里停脚。")
Mes("港口、城墙、商队、守卫,还有像我这样盯着下一船面粉的人,拼在一起才撑起这座沙漠城市。")
Mes("这也说明了一件事:码头上每一个迟到的货桶,都会让城里某个炉子安静下来。")
CompleteChoice(1)
func OnFarewell():
Mes("有空再来。只要炉火还亮,就会有一块新面包等着你。")
# Transitions to next states
func OnAccept():
SetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND, ProgressCommons.GRAIN_IN_THE_SAND.STARTED)
Mes("太感谢了,朋友。")
Mes("找深蓝色蜡封。看到 阿尔蒂斯 的印记,就说明货没错。")
func OnDecline():
Mes("没关系。炉火会等一会儿,只是别让它等到天黑。")
func OnReward():
Mes("你找到了!太好了。")
Mes("有了这些面粉,日落前 图利姆沙 就能闻到 沙暴面包 的香味。")
SetQuest(ProgressCommons.Quest.GRAIN_IN_THE_SAND, ProgressCommons.GRAIN_IN_THE_SAND.REWARDS_WITHDREW)
AddItem(cactusSourCandyID, 5)
AddItem(croissantID, 5)
AddKarma(1)
AddExp(20)
Mes("这些给你:可颂 和 仙人掌酸糖。一个是家传手艺,一个是我在沙暴天里琢磨出来的小发明。")
@@ -0,0 +1,41 @@
extends NpcScript
#
func OnStart():
Mes("需要指路?别在这片城墙下乱转,图利姆沙的路看起来直,走起来很会骗人。")
OnMainChoice()
# Main choice loop
func OnMainChoice():
Choice("港口在哪里?", OnPort)
Choice("东边海岸有什么?", OnEast)
Choice("沙漠风暴矿洞怎么走?", OnMines)
Choice("城里哪里比较安全?", OnSafe)
Choice("我先走了。", Farewell)
# Answers
func OnPort():
Mes("往北走。你会先闻到海味,再听见水手吵架,那就是港口。")
Mes("托诺里 自己产不了多少谷物,外地来的布料、面粉、药材也多靠那里。")
Mes("水手工作时别挡路。他们骂人比沙暴还快,而且通常骂得很准。")
OnMainChoice()
func OnEast():
Mes("东边是一长段海滩。喜欢沙子的话,那里多到能让你重新讨厌它。")
Mes("沿路继续走能靠近 玛纳伊尔 的塔。那些研究 玛纳 的人很少下城,但他们看见的东西比我们多。")
Mes("再往东北有座灯塔,天晴时从那里能望见整片海。迷路时认灯塔,比认自己的影子可靠。")
OnMainChoice()
func OnMines():
Mes("要去矿洞,就先出城往南,穿过沙漠风暴谷地。")
Mes("旧井旁边有入口,内森常被派在那里站岗。如果你看见一个快被晒干的守卫,多半就是他。")
Mes("别低估那条路。风会遮住怪物,怪物也会利用风。")
OnMainChoice()
func OnSafe():
Mes("城门附近有埃拉诺,灵魂石碑 那边有妮娜,市场人多,通常也安全。")
Mes("真正别乱闯的是城堡走廊。巡逻队对陌生人没耐心,尤其是灯光照得到的地方。")
OnMainChoice()
func Farewell():
Chat("看路,也看风。风有时候比人诚实。")
@@ -0,0 +1,36 @@
extends NpcScript
class_name SoulMenhirGlobal
#
const REGEN_HEALTH_BONUS : int = 3
const REGEN_MANA_BONUS : int = 3
const REGEN_STAMINA_BONUS : int = 3
var playerModifiers : Dictionary[int, Array] = {}
#
func OnAreaEnter(player : PlayerAgent):
if not player or not ActorCommons.IsAlive(player):
return
var playerRID : int = player.get_rid().get_id()
if playerModifiers.has(playerRID):
return
var mods : Array[StatModifier] = []
mods.append(AddModifier(CellCommons.Modifier.RegenHealth, REGEN_HEALTH_BONUS, player))
mods.append(AddModifier(CellCommons.Modifier.RegenMana, REGEN_MANA_BONUS, player))
mods.append(AddModifier(CellCommons.Modifier.RegenStamina, REGEN_STAMINA_BONUS, player))
playerModifiers[playerRID] = mods
func OnAreaExit(player : PlayerAgent):
if not player:
return
var playerRID : int = player.get_rid().get_id()
if not playerModifiers.has(playerRID):
return
for mod in playerModifiers[playerRID]:
RemoveModifier(mod, player)
playerModifiers.erase(playerRID)
@@ -0,0 +1,9 @@
extends WarpGlobal
#
func OnAreaEnter(player : PlayerAgent):
if player and player.progress:
if player.progress.GetQuest(ProgressCommons.Quest.TUTORIAL) < ProgressCommons.CompletedProgress:
Network.PushNotification("出城前先去找埃拉诺。", player.peerID)
return
super.OnAreaEnter(player)
+88
View File
@@ -0,0 +1,88 @@
extends NpcScript
#
func OnStart():
var globalScript : TicTacToeGlobal = (npc.ownScript as TicTacToeGlobal)
match globalScript.startStep:
TicTacToeGlobal.State.NONE:
if steps.is_empty():
match randi_range(0, 2):
0: Mes("要玩井字棋吗?棋盘是我自己画的!")
1:
Mes("我画不出圆圆的圈,所以我用小方块代替。安迪总笑我,但方块也可以很厉害。")
Mes("要玩井字棋吗?")
2:
Mes("前几天我看到一伙海盗和守卫玩井字棋。守卫输了,但他们说那叫战略撤退。")
Mes("你也要玩吗?")
else:
Mes("要玩井字棋吗?")
Choice("和你玩", StartPvE)
Choice("找别人玩", StartPvP)
Choice("怎么玩?", ShowRules)
Choice("晚点再玩", Decline)
TicTacToeGlobal.State.X:
if globalScript.playerX != own:
Mes("嘿,已经有人在等对手了!你可以去挑战他们!")
Choice("我来和他们玩!", StartPvP)
Choice("怎么玩?", ShowRules)
Choice("晚点再玩", Decline)
else:
Mes("还没人来。你要继续等吗?")
Choice("再等一会儿", WaitPvP)
Choice("怎么玩?", ShowRules)
Choice("我得走了", CancelPvP)
TicTacToeGlobal.State.O:
var isPlaying : bool = globalScript.playerX == own or globalScript.playerO == own
if isPlaying:
Mes("嘿,不许偷看提示!快回去下你的棋!")
else:
Mes("嘘,他们正在下棋!你可以看,但不能乱指。")
Choice("好啦好啦", OnQuit)
Choice("怎么玩?", ShowRules)
if isPlaying:
Choice("我得走了", CancelPvP)
func ShowRules():
Mes("你选一个格子,放上自己的记号,然后换另一个人。")
Mes("横着、竖着、斜着,只要三个连成一条线就赢。")
Mes("如果九个格子都填满还没人连成线,就是平局。平局也不错,只是没有胜利时那么好跳起来。")
OnStart()
func Decline():
Chat("好吧,改变主意就来找我,棋盘不会跑掉。")
# Player vs 角色
func StartPvE():
var globalScript : TicTacToeGlobal = (npc.ownScript as TicTacToeGlobal)
if globalScript.startStep == TicTacToeGlobal.State.NONE and globalScript.StartPvE(own):
DisplayActions(["gp_target", "gp_untarget"])
Mes("你执叉号!选一个格子,快快快!")
else:
Mes("等一下,已经有人在玩了!要排队。")
# Player vs Player
func StartPvP():
var globalScript : TicTacToeGlobal = (npc.ownScript as TicTacToeGlobal)
match globalScript.StartPvP(own):
TicTacToeGlobal.State.X:
Mes("现在等另一个人来挑战你。")
return
TicTacToeGlobal.State.O:
DisplayActions(["gp_target", "gp_untarget"])
Mes("太好了,你们都到齐了!好好玩!")
return
TicTacToeGlobal.State.NONE:
Mes("不行不行,棋盘现在被占用了!")
func WaitPvP():
if (npc.ownScript as TicTacToeGlobal).playerX == own:
Chat("一定很快就有人来,大概吧!")
func CancelPvP():
var globalScript : TicTacToeGlobal = (npc.ownScript as TicTacToeGlobal)
if globalScript.playerX == own:
globalScript.LeaveQueue(own)
Chat("啊,这么快就不等了?好吧。")
func OnQuit():
Chat("嘘,别打扰他们下棋。")