Vic3 mod 制作教程 第五章 事件、决议、JE(上)(文本版)
终于度过了完全没有基础的阶段,现在来看我视频的观众多少都会有点基础。因此,终于可以正式做起mod了
因此,本章就从创建mod做起,建立起一系列的作弊事件,作弊决议和作弊JE
本章分为上下两章绝对不是因为时长问题,而是因为中间需要讲解本地化和界面代码,才能正常地进行下半章的解说,这一章就只需要建立最简单的事件,决议和JE
## 第一节 新建空白的mod和打开游戏之前的准备
首先,打开启动器,点击所有已安装mod,然后点击新建mod,键入名称,本教程就以“modding_tutorial”作为名称。然后在播放集里关闭其他的mod,点击modding_tutorial,然后点击设置,“以调试模式启动该游戏”。这里关闭其他mod的原因是为了排除其他mod对游戏修改的影响,如果你确信你启动的其他mod不会影响你的mod内容,你可以打开。
当然,如果就这么打开这游戏,游戏不会有任何变化,因为mod文件什么东西都没有。因此需要打开`文档/Paradox Interactive/Victoria 3/mod/modding_tutorial/`,然后就会发现,除了`.metadata`文件夹,什么也没有。因此需要添加文件。
mod的文件夹是是原版文件夹`game`的映射,游戏文件里的`jomini`,`binaries`是不能修改的,可以看到,`game`文件夹里面有很多的文件,点开`common`里面更加多,所以需要有的放矢,只添加自己需要的。本章节中,因为只是简单地制作事件、决议、JE,所以相对的,要找到它们对应的文件夹。事件的文件夹就是单独一个`events`,决议的文件夹在`common/decisions`,JE的文件夹在`common/journal_entries`,此外为了维护代码,还需要添加scripted_effect和scripted_trigger,以及script_value。
如果要修改游戏原来就有的事件、决议、JE,把原版对应的事件、决议、JE拖到mod里然后修改就好,但是这里是新建作弊事件,作弊决议和作弊JE,因此需要创建一些`utf-8 with bom`格式的txt文件,在vscode打开然后创建是相对简单的方法。
调试模式打开可以进行热更新,可以边改边看。但是其具有一定前提,其中一个前提是,不能新增或者删减mod文件,但是可以在已经有的mod的文件里添加,所以无论如何,建议先新建一个空白的文件。
新建完空白文档之后正式进入游戏。
## 第二节 新建一个事件
最为基本的event如下
event的触发机制经过了新版引擎的优化,不再拥有MTTH(Mean time to happen)机制,不熟悉MTTH机制的也不需要特意去了解,因为这是已经被废弃的功能。event的触发机制通过一种钩子上下文触发,这种上下文具有多种参数,基本位于on_action里,有时候也会在一些游戏概念里出现,一般都会被P社特意标明:“这是一个on_action钩子”。这些钩子上下文一般拥有三种参数,events,effects,random_events,如果不考虑随机触发,希望立即触发,那么直接在events上下文中填入用空格或者换行隔开的事件名称即可,如果考虑到随机触发,则需要在事件名称前添加等于号,等于号左边的参数填写权重,如果希望一定权重不发生任何事件,等于号右边就应当是0;关于钩子上下文,也就是on_action的更详细的说明会在第八章说明。用钩子触发的event会受到event中名为trigger的trigger上下文影响,而用控制台或者effect触发的event则不会。
总之可以在mod文件夹的event文件夹里的文件填入我列举的事件的文本代码,保存之后通过控制台event example_event.1来调用,也可以顺便在控制台看到控制台打印出的trigger
```perl
namespace = example_event # 不定义namespace则根本无法调用event
example_event.1 = {
###########参数区###########
type = country_event # 定义了ROOT是什么Scope类型
placement = root
hidden = no # 定义了是否是隐藏事件,如果是隐藏事件最好只有immediate
title = "example title"
desc = "example desc"
flavor = "example favor"
icon = "gfx/interface/icons/event_icons/event_skull.dds"
event_image = {
# 可以是bk2视频,也可以是dds图片
# video should be a bk2 (Bink Video) file
video = "gfx/event_pictures/middleeast_courtroom_upheaval.bk2"
# dds图片的情况如下
# texture = "gfx/texture.dds"
}
on_created_soundeffect = "event:/SFX/UI/Alerts/event_appear"
on_opened_soundeffect = "event:/SFX/Events/middleeast/courtroom_upheaval"
#######行动区########
immediate = { # Effect上下文,
set_variable = { name = get_var value = 1 }
# 设置不存在的变量的操作建议在immediate里
}
trigger = { # 是否会触发的trigger上下文,但是在trigger_event这个effect调用的时候会被忽略掉
c:FRA ?= this
}
########选项区#########
duration = 3
option = {
name = "Option 1"
change_infamy = -150
trigger = { # 定义这个option是否显示的trigger上下文,如果trigger为假则不会显示
var:get_var > 1
}
}
option = {
name = "Option 2"
highlighted_option = yes # 是否高亮
default_option = yes # 是否默认选项
add_era_researched = era_1
add_era_researched = era_2
add_era_researched = era_3
add_era_researched = era_4
add_era_researched = era_5
}
option = {
name = "Option 3"
highlighted_option = yes # 是否高亮
if = {
limit = { exists = currently_enacting_law.type }
activate_law = currently_enacting_law.type
}
set_variable = { name = get_var value = 2 }
}
}
```
因为暂时还不说到本地化,所以需要使用本地化的部分姑且用双引号括起来的文本。
事件里有很多参数,可以分为参数区,行动区,选项区。
参数区定义了一系列的参数,有type,placement,hidden这些参数。type参数定义了ROOT是什么Scope类型,可以是country_event, character_event, 或者state_event,placement定义了发生的位置,而hidden则定义其是否可见,如果不可见,不建议使用选项区的内容。
另外的风味化参数是title,desc,flavor,icon,event_image,on_created_soundeffect,on_opened_soundeffect,这些参数是可有可无的参数,但是添加进去会让事件有标题和描述和事件图片之类的。最需要说的就是event_image,它提供了两个参数,video和texture,一般来说是二者选择其一,video是bk2格式的视频,而texture是dds格式的图片,这些都是下半部分再说的东西,因此姑且先使用原版的。
还有一些风味化参数需要留到下半部分才方便描述。
然后是行动区,行动区有immediate和trigger两个参数。immediate是事件发生之后立即发生的事件,适合预先定义一些变量和scope。trigger则是在被on_action里的random_events和events这两个参数中调用时候是否执行的trigger上下文,在trigger_event里就会忽略掉。
最后是选项区,选项区包括duration和多个option。duration表示事件的存续时间,如果过了这段时间就会自动执行默认的选项。而option则有很多参数,有name,highlighted_option,default_option,trigger这几个参数,name是风味化参数,定义了选项的名称。highlighted_option就是指这是否可以高亮,如果是yes就高亮。default_option就是指这是否是默认选项,如果为yes就是默认选项,duration过去了之后就会选择这一个,因此最好是只指定一个option是default_option。trigger则是定义这个option是否显示的trigger上下文,如果trigger为假则不会显示。
介绍完之后就新建一个event,在immediate里设置一个get_var的variable,其值为1,整出三个option。第一个option设置了trigger,只有get_var大于1的时候显示,可以减少150恶名;第二个option设置为默认且高亮,可以研究所有科技,查阅文档即可得;第三个option设置get_var为2,然后如果正在通过一个法律则立即通过之。而这,就是一个基本的作弊事件了。
第二个选项的提示文本并不好看,所以可以把option2改成这样
```perl
option = {
name = "Option 2"
highlighted_option = yes # 是否高亮
default_option = yes # 是否默认选项
custom_tooltip = { # 这是一个effect
text = "研究所有科技"
subject = root # 执行对象
add_era_researched = era_1
add_era_researched = era_2
add_era_researched = era_3
add_era_researched = era_4
add_era_researched = era_5
}
}
```
作弊事件虽然可以通过研究所有科技和通过正在研究的法律,但是对于如何正在研究一个法律则立即研究之,则会更加的难,因为technology不像law和law_type那样完善,不客气的说,technology是残废的scope。因此达到这样的效果则会更加的艰难。
判断一个国家是否在研究某个科技和立即研究某个科技都是有的,但是并没有一个确定的对象显示某一个国家在研究什么科技,所以只能通过非常啰嗦的if,else,else_if的effect来执行,这会显得事件非常冗长,所以,更正确的做法是,使用scripted_effect定义一个非常长的effect,然后再在事件里调用。
对于科技,大概是这样的
```perl
if = { limit = { is_researching_technology = sericulture }
add_technology_researched = sericulture
}
if = { limit = { is_researching_technology = enclosure }
add_technology_researched = enclosure
}
```
要做出scripted_effect,只需要这样
```perl
finish_researching_techs = {
if = { limit = { is_researching_technology = sericulture }
add_technology_researched = sericulture
} else_if = { limit = { is_researching_technology = enclosure }
add_technology_researched = enclosure
}
}
```
在事件里调用就只需要这样写
```perl
finish_researching_techs = yes
```
这里直接给出遍历了所有科技之后的scripted_effect,创建新的选项
```perl
option = {
name = "Option 4"
finish_researching_techs = yes
}
```
此外,scripted_effect还具有含参数的用法
```perl
functional_scripted_effect = {
$PARAMETER$ = {
add_treasury = 10000000
}
}
```
调用的时候换成这样
```perl
functional_scripted_effect = { PARAMETER = c:GBR }
```
scripted_effect的参数是字面量,所以不需要一定要写成scope,但是使用scope可读性更强
```perl
functional_scripted_effect_2 = {
c:$PARAMETER$ = {
add_treasury = 10000000
}
}
functional_scripted_effect_2 = { PARAMETER = GBR }
```
## 第三节 决议的编辑
相比事件来说,决议的编辑要更为简单,当然,这并不代表这是一件好事,因为更简单往往意味着更少的功能。而好死不死,Vic3的决议是历代P社游戏里面表现力最差的:没有任何配图,朴素到了只有一个标题一个介绍一个不知所谓的绿色圆按钮(在1.3版本里则是不知所谓的蓝色圆按钮),而且被雪藏到了一个不好找的地方(日志 -> 决议)。可以说除了这玩意叫决议之外和钢4的决议简直是地花板和天花板的区别。
相比之下,钢4的决议就更为难入门,但是钢4的一个决议组就可以完成Vic3里事件+决议+JE的效果,非常超模。但是总之还是来看看Vic3的决议如何构成的。
VIc3的决议和JE都有官方文档,因此就官方文档进而介绍。一个决议有四个必要的元素:名称,is_shown的trigger上下文,possible的trigger上下文,when_taken的effect。如果可以,还可以加入 ai_chance,这是一个value
顾名思义,is_shown就是判断是否需要显示,possible就是判断是否需要执行,when_taken就是执行的效果,然后看起来就是这样的。
```perl
example_decision = {
is_shown = {
NOT = { has_variable = dummy_decision_taken }
country_rank = rank_value:great_power # only visible to great powers
}
possible = {
country_has_primary_culture = cu:french # can be taken only by countries where the french culture is primary
}
when_taken = {
annex = c:GBR
set_variable = {
name = dummy_decision_taken
value = yes
}
}
ai_chance = {
base = 20
modifier = {
trigger = { c:PRU ?= root }
add = 10
}
}
}
```
嗯,就是这样的,然后就没有然后了,这玩意上限就在这了。后续也不怎么样,下半章也不会太重点描述,甚至有更好的替代方案。
## 第四节 JE的编辑
JE,全称Journal Entry,或者叫日志,是V3做的一种类似任务树的概念。JE和前面二者的最大区别是,JE是可以在文本代码中作为Scope调用的,而事件和决议则不然。此外,很多JE的热刷新都要求跑一段时间才可刷新。
作为scope调用的je需要在国家scope作为输出,以je:作为event target,然后根据je名称得到输出
JE的完整结构如下
```perl
journal_entry_key = {
###############预定义描述###########
# root JE所属的国家
# scope:journal_entry 这个JE
# scope:target
###############定义区###############
icon = "gfx/interface/icons/event_icons/event_industry.dds"
timeout = 720
should_be_pinned_by_default = yes
progressbar = yes
progress_desc = "这是进度条"
can_deactivate = yes
inheritable = no
weight = 200
# how_tutorial = how_tutorial_lesson_key 教程相关JE
##############trigger上下文区#################
is_shown_when_inactive = { }
possible = { }
is_progressing = { }
complete = { }
fail = { }
invalid = { }
##############effect上下文区#################
immediate = { }
on_timeout = { }
on_complete = { }
on_fail = { }
on_invalid = { }
##########value区#############################
current_value = {
value = gdp
}
goal_add_value = {
value = gdp
multiply = 0.25
}
########钩子区#########
on_weekly_pulse = {
random_events = { }
}
# on_action which is triggered every first day of the month
on_monthly_pulse = {
events = { }
}
# on_action which is triggered every first day of the year
on_yearly_pulse = {
effect = { }
}
################GUI区################
# 下半章的内容
# status_desc = {
# first_valid = {
# triggered_desc = {
# desc = journal_entry_key_status_1
# trigger = {
# has_variable = mining_strike
# }
# }
# triggered_desc = {
# desc = journal_entry_key_status_2
# trigger = {
# has_variable = industrial_strike
# }
# }
# triggered_desc = {
# desc = journal_entry_key_status_2
# trigger = {
# has_variable = railway_strike
# }
# }
# }
# }
# scripted_button = scripted_button_key
}
```
可以看到我给JE分了七个区:预定义描述、定义区、trigger上下文区、effect上下文区、value区、钩子区和GUI区,GUI区是第七章的内容,暂不描述。
首先看预定义描述:root是激活了该JE的国家,指代这个JE的就是scope:journal_entry,scope:target就是添加je时使用的target
添加je使用的effect是`add_journal_entry = { type = <key> target = <scope> }`,参数里的target可以在JE里通过scope:target调用
在定义区中,icon定义了图标,timeout定义了超时时间,should_be_pinned_by_default定义了其是否可以默认钉选。
progressbar定义了其是否有根据Value区内容设置的进度条,progress_desc定义了进度条上的文字。
can_deactivate定义了其能否在激活后当possible的上下文为假时取消激活,inheritable定义了其能否被根据拥有je的国家生成的国家继承。
最后两个定义参数是weight和how_tutorial,weight是在界面中显示的顺序,how_tutorial是和教程相关的,比较无关,所以不说。
trigger上下文区有六个trigger上下文:is_shown_when_inactive,possible,is_progressing和complete,fail,invalid。
is_shown_when_inactive默认为假,当其为真时,即使没有添加到国家中,也会在面板中显示。
possible默认为真,当其和is_shown_when_inactive为真时,激活该JE,如果can_deactivate = yes,那且其为假,那么取消该JE的激活。
is_progressing是确定其状态的trigger,当其为真时,JE进入到一个称为is_progressing的状态,可以通过输入为journalentry的scope的is_progressing的trigger判断。
complete,fail,invalid分别是判断JE完成、失败和失效的trigger上下文,当其为真时,进入完成、失败或失效的状态,如果有对应的effect上下文则执行之。
effect上下文区有五个effect上下文:immediate、on_timeout和on_complete、on_fail、on_invalid
immediate是激活该JE之后立即执行的effect,和事件的immediate上下文一样,这适合用以定义一些variable和scope。
on_timeout是当超时执行内部effect的钩子。而on_complete、on_fail、on_invalid是对应complete,fail,invalid的钩子,当进入完成、失败或失效的状态时执行之。
value区定义了两个value,current_value和goal_add_value,其中,current_value实时更新,而goal_add_value则是在激活的时候就确定,不能改变。二者定义型态和scripted_value一致。
钩子区有三个钩子:on_weekly_pulse、on_monthly_pulse、on_yearly_pulse,分别是每周、每月、每年执行一次内容。
这些钩子有三个参数,第一个是称为effect的effect上下文,顾名思义,这里可以放入effect,第二个是称为event的上下文,按照空格或者换行隔开放入列举的event。
最后一个是random_events,里面可以填入等号左边为权重数字,右边为event名称的文本代码,当不想执行事件的时候,等号右边放入0。
## 第五节 编辑一个JE
这么多区域既没有必要按照上面为了方便阐述类型而排布的顺序而排列,也没有必要全部列举,实践中编辑一个JE只需要按照需求添加有必要的参数就可以
创建一个JE如下
```perl
journal_five_years_plan_looped = {
icon = "gfx/interface/icons/event_icons/event_industry.dds"
should_be_pinned_by_default = yes
timeout = 720
immediate = {
set_variable = { name = five_years_plan_loops value = 1 }
}
is_shown_when_inactive = { always = yes }
current_value = {
value = gdp
divide = var:five_years_plan_loops
}
goal_add_value = {
value = gdp
multiply = 0.25
}
progressbar = yes
progress_desc = "这是进度条"
on_weekly_pulse = {
effect = {
if = {
limit = { scope:journal_entry = { is_goal_complete = yes } }
change_variable = { name = five_years_plan_loops add = 0.25 }
}
}
}
weight = 100
}
```
当然跑gdp调试就太麻烦了,所以不如设置一个variable算了
```perl
journal_five_years_plan_looped_vared = {
icon = "gfx/interface/icons/event_icons/event_industry.dds"
should_be_pinned_by_default = yes
immediate = {
set_variable = { name = five_years_plan_vared_loops value = 1 }
set_variable = { name = emulated_gdp value = 10 }
}
is_shown_when_inactive = { always = yes }
current_value = {
value = var:emulated_gdp
divide = var:five_years_plan_vared_loops
}
goal_add_value = {
value = var:emulated_gdp
multiply = 0.25
}
progressbar = yes
progress_desc = "这是进度条"
on_weekly_pulse = {
effect = {
if = {
limit = { scope:journal_entry = { is_goal_complete = yes } }
change_variable = { name = five_years_plan_vared_loops add = 0.25 }
}
}
}
}
```
在script explorer修改emulated_gdp值,查看效果。
这样,就基本的学会了JE的编写。
## 课后
很显然,我这里的教程项目预留了一些坑,可以通过观察这些坑来获得更好的教学效果
1. 为什么example_event需要通过同时触发两个事件,然后在其中第一个事件里选择选项3,然后第二个事件里才会有选项1?
2. 很显然,当没有科技正在研究的时候,选项4的tooltip仍然会显示:完成某某科技的研究,应当如何改进,才能让没有研究科技的时候不显示这个tooltip?
这些就可以作为课后思考,会得到更好的收获。