├── .gitignore ├── README.md ├── Technical_Details.md ├── bundle.js ├── bundle.js.map ├── data ├── audio │ └── ow_kazoo_theme.mp3 ├── fonts │ ├── JiangChengYuanTi-400W.ttf │ └── OFL.txt └── sectors │ ├── brittle_hollow.json │ ├── comet.json │ ├── dark_bramble.json │ ├── eye_of_the_universe.json │ ├── giants_deep.json │ ├── quantum_moon.json │ ├── rocky_twin.json │ ├── sandy_twin.json │ └── timber_hearth.json ├── index.html ├── package-lock.json ├── package.json ├── src ├── AnglerfishNode.ts ├── AudioManager.ts ├── Button.ts ├── DatabaseScreen.ts ├── Entity.ts ├── Enums.ts ├── EventScreen.ts ├── ExploreData.ts ├── ExploreScreen.ts ├── GameManager.ts ├── GameSave.ts ├── GlobalMessenger.ts ├── Locator.ts ├── Node.ts ├── NodeAction.ts ├── NodeConnection.ts ├── PlayerData.ts ├── QuantumNode.ts ├── Screen.ts ├── ScreenManager.ts ├── Sector.ts ├── SectorButtons.ts ├── SectorEditor.ts ├── SectorLibrary.ts ├── SectorScreen.ts ├── SectorTelescopeScreen.ts ├── SolarSystem.ts ├── SolarSystemScreen.ts ├── StatusFeed.ts ├── SupernovaScreen.ts ├── Telescope.ts ├── TimeLoop.ts ├── TitleScreen.ts ├── Vector2.ts ├── app.ts └── compat.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⌈Outer Wilds:文字冒险⌋ 汉化网页版 2 | 3 | [原项目](https://github.com/Hawkbat/OuterWildsTextAdventureWeb) 为 [Outer Wilds Text Adventure](https://www.mobiusdigitalgames.com/outer-wilds-text-adventure.html) 的网页移植版本,本项目为网页版提供了完整的简体中文本地化支持,访问 [crystfish.github.io/OWTAWebSC](https://crystfish.github.io/OWTAWebSC/) 在浏览器内体验文字版星际拓荒的魅力 4 | 5 | 当然你也可以在 [这里](https://github.com/CrystFish/OWTASC) 下载游玩已经汉化的 Java 原始程序 6 | 7 | ## 网页移植版的技术细节 8 | 9 | 详见 [技术细节](Technical_Details.md) 部分 10 | 11 | 中文文本由于词语之间没有空格而无法自动换行,因此本项目对文本显示部分进行了一定的修改(使用 Github Copilot 辅助) 12 | 13 | 注意:如果你在打开网页后没有背景音乐,可能是因为你的浏览器阻止了网页自动播放音频 14 | 15 | # 版权相关事项 16 | 17 | 所有资产和原始源代码均属于其原始版权所有者。本项目的作者不对上述资产或源代码(包括任何衍生材料)的版权提出任何要求 18 | 19 | 本项目仅用于教育目的 20 | 21 | 本项目的作者将遵守版权所有者的任何要求 22 | 23 | 汉化版本使用的字体为 江城圆体 ,本项目在遵守了相应许可证协议的前提下合理使用了上述字体 -------------------------------------------------------------------------------- /Technical_Details.md: -------------------------------------------------------------------------------- 1 | ## 技术细节 (Technical Details) 2 | (译者不是很了解 Java 与 TypeScript 的一些细节,译文仅供参考) 3 | 4 | 原始程序使用 Java 编写,而原程序的源代码与资产文件都包含在压缩包内。也就是说,用其他编程语言重写游戏并移植到其他平台是可行的 5 | 6 | 对于网页移植版而言,Java 源代码文件的后缀被改为 .ts(TypeScript)之后再一行行修改以符合新语言的语法。最大的问题在于: 7 | - 隐式的类字段访问(例如,Java 中是 `_myVar` 而 JavaScript 中是 `this._myVar`),需要手动检查每个变量引用,并在必要时添加 `this.`。虽然很耗时,但由于源代码中没有模棱两可的用法,因此相对容易处理 8 | - 方法重载,特别是构造函数重载,因为 JavaScript 的函数只能有一个实现。这些方法在使用 TypeScript 的类型联合和可选参数特性下被重写为等效的单一实现 9 | - C 语言风格的类型声明(例如 `String foo` 而非 TypeScript 的 `foo: string`),这个问题可以使用强力的正则表达式的查找与替换来解决 10 | 11 | 原始的 Java 版本使用了 [Processing](https://processing.org/) 引擎,这是一个轻量的“游戏引擎”,用于制作图形原型与互动式视觉演示。Processing 基金会也制作了该引擎的 JavaScript 版本,即 [p5.js](https://p5js.org/),很明显这不是用同一种编程语言编写的,但其 API 与 Java 版引擎相当类似。而这意味着,一旦代码修改完毕,大多数功能都能开箱即用、正常工作,无需编写额外的实现代码 12 | 13 | p5.js 中的一些方法和辅助类并不完美,这些部分被重新编写并放入了 `compat.ts` 中 -------------------------------------------------------------------------------- /data/audio/ow_kazoo_theme.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrystFish/OWTAWebSC/6741be9c6c1992b80a09ad083ec114a8b38df398/data/audio/ow_kazoo_theme.mp3 -------------------------------------------------------------------------------- /data/fonts/JiangChengYuanTi-400W.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrystFish/OWTAWebSC/6741be9c6c1992b80a09ad083ec114a8b38df398/data/fonts/JiangChengYuanTi-400W.ttf -------------------------------------------------------------------------------- /data/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) , (), 2 | with Reserved Font Name . 3 | Copyright (c) , (), 4 | with Reserved Font Name . 5 | Copyright (c) , (). 6 | 7 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 8 | This license is copied below, and is also available with a FAQ at: 9 | https://openfontlicense.org 10 | 11 | 12 | ----------------------------------------------------------- 13 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 14 | ----------------------------------------------------------- 15 | 16 | PREAMBLE 17 | The goals of the Open Font License (OFL) are to stimulate worldwide 18 | development of collaborative font projects, to support the font creation 19 | efforts of academic and linguistic communities, and to provide a free and 20 | open framework in which fonts may be shared and improved in partnership 21 | with others. 22 | 23 | The OFL allows the licensed fonts to be used, studied, modified and 24 | redistributed freely as long as they are not sold by themselves. The 25 | fonts, including any derivative works, can be bundled, embedded, 26 | redistributed and/or sold with any software provided that any reserved 27 | names are not used by derivative works. The fonts and derivatives, 28 | however, cannot be released under any other type of license. The 29 | requirement for fonts to remain under this license does not apply 30 | to any document created using the fonts or their derivatives. 31 | 32 | DEFINITIONS 33 | "Font Software" refers to the set of files released by the Copyright 34 | Holder(s) under this license and clearly marked as such. This may 35 | include source files, build scripts and documentation. 36 | 37 | "Reserved Font Name" refers to any names specified as such after the 38 | copyright statement(s). 39 | 40 | "Original Version" refers to the collection of Font Software components as 41 | distributed by the Copyright Holder(s). 42 | 43 | "Modified Version" refers to any derivative made by adding to, deleting, 44 | or substituting -- in part or in whole -- any of the components of the 45 | Original Version, by changing formats or by porting the Font Software to a 46 | new environment. 47 | 48 | "Author" refers to any designer, engineer, programmer, technical 49 | writer or other person who contributed to the Font Software. 50 | 51 | PERMISSION & CONDITIONS 52 | Permission is hereby granted, free of charge, to any person obtaining 53 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 54 | redistribute, and sell modified and unmodified copies of the Font 55 | Software, subject to the following conditions: 56 | 57 | 1) Neither the Font Software nor any of its individual components, 58 | in Original or Modified Versions, may be sold by itself. 59 | 60 | 2) Original or Modified Versions of the Font Software may be bundled, 61 | redistributed and/or sold with any software, provided that each copy 62 | contains the above copyright notice and this license. These can be 63 | included either as stand-alone text files, human-readable headers or 64 | in the appropriate machine-readable metadata fields within text or 65 | binary files as long as those fields can be easily viewed by the user. 66 | 67 | 3) No Modified Version of the Font Software may use the Reserved Font 68 | Name(s) unless explicit written permission is granted by the corresponding 69 | Copyright Holder. This restriction only applies to the primary font name as 70 | presented to the users. 71 | 72 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 73 | Software shall not be used to promote, endorse or advertise any 74 | Modified Version, except to acknowledge the contribution(s) of the 75 | Copyright Holder(s) and the Author(s) or with their explicit written 76 | permission. 77 | 78 | 5) The Font Software, modified or unmodified, in part or in whole, 79 | must be distributed entirely under this license, and must not be 80 | distributed under any other license. The requirement for fonts to 81 | remain under this license does not apply to any document created 82 | using the Font Software. 83 | 84 | TERMINATION 85 | This license becomes null and void if any of the above conditions are 86 | not met. 87 | 88 | DISCLAIMER 89 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 90 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 91 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 92 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 93 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 94 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 95 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 96 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 97 | OTHER DEALINGS IN THE FONT SOFTWARE. 98 | -------------------------------------------------------------------------------- /data/sectors/brittle_hollow.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "碎空星又破碎又空洞。地表不断受到其火山卫星的轰炸,每隔一段时间就会有一块地壳脱落并坠入到行星中心的黑洞中\n\n小心脚下"}, 3 | "Nodes": { 4 | "考古学家的营地": { 5 | "position": { 6 | "y": -205, 7 | "x": -1 8 | }, 9 | "explore": {"text": "你发现一位旅行者在篝火旁弹奏班卓琴\n\n这位旅行者告诉你南部附近的废墟里有一组壁画,描绘了挪麦人来到太阳系的旅程\n\n显然他们来到这里是为了寻找什么..."}, 10 | "description": "一片树木繁茂的空地,篝火熊熊燃烧", 11 | "signal": "TRAVELER", 12 | "campfire": true 13 | }, 14 | "悬崖古屋": { 15 | "position": { 16 | "y": -96, 17 | "x": -1 18 | }, 19 | "explore": { 20 | "text": "你发现了五幅古壁画\n\n壁画一描绘了一艘挪麦飞船接收到了来自宇宙之眼的信号(被描绘在你所在太阳系的旁边)\n\n壁画二描绘了挪麦飞船被困在黑荆星的场景\n\n壁画三描绘了飞船放出了三艘逃生舱\n\n壁画四描绘了一艘逃生舱被一条巨大的鮟鱇鱼追逐的场面\n\n壁画五描绘了一艘救生艇坠毁在碎空星,一艘坠毁在沙漏双星,还有一艘坠毁在黑荆星", 21 | "discover clue": "D_1" 22 | }, 23 | "description": "一座悬崖边的古屋" 24 | }, 25 | "冰川陆架": { 26 | "position": { 27 | "y": 128, 28 | "x": -128 29 | }, 30 | "explore": "你调查了这个类似捕梦网的结构,并注意到中心平坦的圆形平台上有细小沙粒", 31 | "description": "一个冰穴,内有结构类似捕梦网的外星建筑" 32 | }, 33 | "树木密布的陨石坑": { 34 | "position": { 35 | "y": -61, 36 | "x": -193 37 | }, 38 | "explore": "你发现了一个被废弃的营地和另一位旅行者的笔记。看来这位旅行者不久前离开去调查北极的遗迹了\n\n另一条笔记提到南部存在较新的废墟", 39 | "entry point": true, 40 | "description": "一个被森林覆盖的陨石坑,篝火冒着烟", 41 | "campfire": true 42 | }, 43 | "黑洞站": { 44 | "position": { 45 | "y": 66, 46 | "x": 1 47 | }, 48 | "explore": { 49 | "text": "你发现了一个分析行星中心黑洞的挪麦研究站。挪麦人利用这项研究开发了一种新的传送方法,涉及两个结构:发射塔和接收平台。这些传送装置仅在发射塔瞄准其对应的接收平台时激活\n\n挪麦人在灰烬双星上建造了一个由这些传送器组成的网络(每个行星一个),以推进在灰烬双星上建造某个巨型机器的进度", 50 | "discover clue": "TLD_2" 51 | }, 52 | "description": "一个危悬在黑洞之上的古研究站" 53 | }, 54 | "古塔": { 55 | "position": { 56 | "y": 128, 57 | "x": 125 58 | }, 59 | "explore": [ 60 | { 61 | "text": "你发现了有关量子卫星的古老记录。量子卫星有时会到达太阳系外的第五个位置。挪麦人发现了一种通过量子卫星抵达这个地方的方法,在那里他们见到了信号的源头", 62 | "discover clue": "QM_3" 63 | }, 64 | {"text": "你试图从塔底进入塔中,但入口的通道早已解体,而重力的牵引力太强..."} 65 | ], 66 | "entry point": true, 67 | "description": "一座横穿地壳的古塔", 68 | "signal": "QUANTUM" 69 | }, 70 | "古天文台": { 71 | "position": { 72 | "y": 216, 73 | "x": 0 74 | }, 75 | "explore": { 76 | "text": "你发现了一项似乎是与深巨星天气模式有关的研究。研究结果显示,深巨星上的大多数龙卷风都有强烈的上升气流,但有些龙卷风逆时针旋转并伴随着下行气流\n\n你找到一个大型探测器发射器的示意图,这个发射器建在环绕深巨星的轨道上\n\n在另一个角落,你找到了挪麦人在四个地点观测量子月亮的记录——沙漏双星、碎空星、木炉星和深巨星——并指出有时它会完全消失不见", 77 | "discover clue": "APL_2" 78 | }, 79 | "description": "一个塞满了古人科技的洞穴" 80 | }, 81 | "逃生舱坠毁点": { 82 | "position": { 83 | "y": -58, 84 | "x": 178 85 | }, 86 | "explore": "你发现了一个坠毁的逃生舱。根据日志描述,它是从黑荆星内部的某处发射的\n\n看起来挪麦人逃离了逃生舱,并前往了北极", 87 | "entry point": true, 88 | "description": "一个内含坠毁逃生舱的陨石坑", 89 | "signal": "BEACON" 90 | } 91 | }, 92 | "Connections": [ 93 | { 94 | "Node 2": "冰川陆架", 95 | "Node 1": "树木密布的陨石坑" 96 | }, 97 | { 98 | "Node 2": "古天文台", 99 | "Node 1": "冰川陆架" 100 | }, 101 | { 102 | "Node 2": "黑洞站", 103 | "Node 1": "冰川陆架" 104 | }, 105 | { 106 | "Node 2": "考古学家的营地", 107 | "Node 1": "树木密布的陨石坑" 108 | }, 109 | { 110 | "Node 2": "考古学家的营地", 111 | "Node 1": "逃生舱坠毁点" 112 | }, 113 | { 114 | "Node 2": "古塔", 115 | "Node 1": "逃生舱坠毁点" 116 | }, 117 | { 118 | "Node 2": "冰川陆架", 119 | "Node 1": "古塔" 120 | }, 121 | { 122 | "Node 2": "悬崖古屋", 123 | "Node 1": "考古学家的营地" 124 | } 125 | ] 126 | } -------------------------------------------------------------------------------- /data/sectors/comet.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "欢迎来到彗星"}, 3 | "Nodes": { 4 | "冰穴": { 5 | "position": { 6 | "y": 4, 7 | "x": 5 8 | }, 9 | "explore": {"text": "你探索了彗星内部中心的冰穴,发现了一种冻结在冰层中的奇怪发光物质。挪麦人的骸骨无处不在......大概是地表上那艘飞船的命运多舛的驾驶者们"}, 10 | "description": "冰穴" 11 | }, 12 | "冰面": { 13 | "position": { 14 | "y": 4, 15 | "x": -92 16 | }, 17 | "explore": {"text": "你在彗星冰冷的地表上发现了一艘小型挪麦飞船的残骸。日志表明其从沙漏双星发射,执行一项紧急任务"}, 18 | "entry point": true, 19 | "description": "荒芜的冰面" 20 | } 21 | }, 22 | "Connections": [{ 23 | "Node 2": "冰穴", 24 | "Node 1": "冰面" 25 | }] 26 | } -------------------------------------------------------------------------------- /data/sectors/dark_bramble.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "黑荆星是一团扭曲的笼罩在浓雾之中的藤蔓。冰与岩的碎片漂浮在其周围,这是它很久之前在宿主星球上肆意生长时的残余。随着太阳变得越来越大、越来越热,黑荆星在经历了漫长的休眠期后又恢复了生机\n\n很少有人敢于冒险进入黑荆星阴暗的深处,极少数人(具体来说是一个也没有)能够活着回来\n\n回头吧,你还有很多值得继续活下去的理由"}, 3 | "Nodes": { 4 | "坠毁的逃生舱": { 5 | "position": { 6 | "y": 72, 7 | "x": -125 8 | }, 9 | "explore": { 10 | "text": "你发现了一个几乎无法辨认的挪麦逃生舱残骸,它被藤蔓缠绕着。你还在附近发现了一个洞穴,坠毁后剩余的挪麦人就躲在那里\n\n幸存下来的挪麦人们发现,黑荆实际上是一种巨大的跨次元食肉植物,它们推测,如果能在其中一根荆棘上安装追踪装置,也许就能跟随它回到黑荆的根部(飞船坠毁的地方)", 11 | "discover clue": "D_3" 12 | }, 13 | "entry point": true, 14 | "description": "一艘坠毁的古逃生舱", 15 | "fog light": true, 16 | "signal": "BEACON", 17 | "probe description": "一束光穿过雾气", 18 | "gravity": false 19 | }, 20 | "Anglerfish": { 21 | "position": { 22 | "y": -37, 23 | "x": 343 24 | }, 25 | "anglerfish": true 26 | }, 27 | "Anglerfish7": { 28 | "position": { 29 | "y": -162, 30 | "x": 5 31 | }, 32 | "anglerfish": true 33 | }, 34 | "黑荆星外围": { 35 | "position": { 36 | "y": 4, 37 | "x": -335 38 | }, 39 | "explore": { 40 | "text": " ", 41 | "fire event": "explore bramble outskirts" 42 | }, 43 | "entry point": true, 44 | "description": "一簇开花的藤蔓", 45 | "ship access": true, 46 | "gravity": false 47 | }, 48 | "冰冻水母": { 49 | "position": { 50 | "y": 143, 51 | "x": 108 52 | }, 53 | "explore": { 54 | "text": "你在一块浮冰中发现了一只巨大的水母。它看起来没有带电,所以你直接进去调查\n\n你检查了水母,发现其体腔中空,大得足以容纳一个人。这就是旅行者所说的吗?", 55 | "discover clue": "APL_3" 56 | }, 57 | "description": "冰冻水母", 58 | "ship access": true, 59 | "probe description": "一只被冰封在巨大冰块中的水母", 60 | "gravity": false 61 | }, 62 | "Anglerfish2": { 63 | "position": { 64 | "y": 132, 65 | "x": 269 66 | }, 67 | "anglerfish": true 68 | }, 69 | "Anglerfish4": { 70 | "position": { 71 | "y": 178, 72 | "x": 27 73 | }, 74 | "anglerfish": true 75 | }, 76 | "Anglerfish3": { 77 | "position": { 78 | "y": -72, 79 | "x": -144 80 | }, 81 | "anglerfish": true 82 | }, 83 | "Anglerfish6": { 84 | "position": { 85 | "y": 22, 86 | "x": 188 87 | }, 88 | "anglerfish": true 89 | }, 90 | "古飞船": { 91 | "position": { 92 | "y": 47, 93 | "x": -24 94 | }, 95 | "explore": { 96 | "text": " ", 97 | "fire event": "explore ancient vessel" 98 | }, 99 | "entry point": false, 100 | "description": "古飞船遗址", 101 | "ship access": true, 102 | "gravity": false 103 | }, 104 | "Anglerfish5": { 105 | "position": { 106 | "y": -143, 107 | "x": 184 108 | }, 109 | "anglerfish": true 110 | }, 111 | "鮟鱇鱼骨架": { 112 | "position": { 113 | "y": -44, 114 | "x": 51 115 | }, 116 | "explore": { 117 | "text": "你在一条死去的鮟鱇鱼的骨架里遇到了一个灰头土脸的旅行者(鮟鱇鱼的诱饵还在发光,也许它能让其他鮟鱇鱼远离它)\n\n尽管他被困在这里已经有一段时间了,但他似乎并不急于返回文明世界\n\n作为不告诉任何人有关他行踪的回报,他告诉了你不远处的一个地点,那里有一个与抵达深巨星核心有关的秘密", 118 | "reveal paths": ["冰冻水母"] 119 | }, 120 | "entry point": true, 121 | "description": "一条鮟鱇鱼的骨架", 122 | "fog light": true, 123 | "signal": "TRAVELER", 124 | "probe description": "一束光穿过雾气", 125 | "gravity": false 126 | } 127 | }, 128 | "Connections": [{ 129 | "Node 2": "冰冻水母", 130 | "Node 1": "鮟鱇鱼骨架", 131 | "hidden": true 132 | }] 133 | } -------------------------------------------------------------------------------- /data/sectors/eye_of_the_universe.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "你跃迁到到了宇宙之眼"}, 3 | "Nodes": { 4 | "Ancient Vessel 2": { 5 | "name": "古飞船", 6 | "position": { 7 | "y": 200, 8 | "x": 0 9 | }, 10 | "explore": "飞船跃迁装置的能量正式耗尽了...看来这就是旅途的终点", 11 | "description": "一处古老的废墟", 12 | "gravity": false 13 | }, 14 | "宇宙之眼": { 15 | "position": { 16 | "y": 0, 17 | "x": 0 18 | }, 19 | "explore": {"text": "查看事件屏幕", "fire event" : "older than the universe"}, 20 | "description": "???", 21 | "gravity": false 22 | } 23 | }, 24 | "Connections": [ 25 | { 26 | "Node 2": "宇宙之眼", 27 | "Node 1": "Ancient Vessel 2" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /data/sectors/giants_deep.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "深巨星是一个汹涌的世界,这里有巨形龙卷风和广阔的海洋\n\n海面上散布的小岛经常会被经过的龙卷风卷入大气层再落回大海,溅起巨大的水花\n\n当你接近这颗行星时,你注意到在低轨道上有一个小型的挪麦空间站"}, 3 | "Nodes": { 4 | "深渊": { 5 | "position": { 6 | "y": 91, 7 | "x": -2 8 | }, 9 | "explore": [{ 10 | "text": "你在探索深渊时,发现了从挪麦探测器发射器上脱落的 “回响室”\n\n你在里面发现了一个看上去很庞大的档案库。随着探索的深入,你逐渐意识到这里的设计初衷是为了在时间流的断裂处保存信息数据\n\n在大厅的中央,一台投影仪显示着数以亿计的探测器累计起来的轨迹,这些探测器从太阳系的各个方向发射出去。似乎其中一个探测器成功找到了宇宙之眼!你一定要记下坐标...", 11 | "fire event": "learn signal coordinates" 12 | }], 13 | "entry point": false, 14 | "description": "海底深渊", 15 | "ship access": false, 16 | "probe description": "围绕着海洋最深处噼啪作响的能量屏障", 17 | "gravity": false 18 | }, 19 | "量子小岛": { 20 | "position": { 21 | "y": -154, 22 | "x": 124 23 | }, 24 | "explore": { 25 | "text": "你发现了一大堆奇怪的黑色石头,它们会趁你不注意的时候移动!\n\n挪麦人推测,这些岩石来自量子卫星,量子卫星有时也会出现在深巨星的轨道上。它们还发现了一种奇怪的现象,观察岩石的照片与直接观察岩石的效果是一样的", 26 | "discover clue": "QM_1" 27 | }, 28 | "entry point": true, 29 | "description": "一座堆满了黑色石头的小岛", 30 | "signal": "QUANTUM" 31 | }, 32 | "禅境小岛": { 33 | "position": { 34 | "y": 57, 35 | "x": -195 36 | }, 37 | "explore": "你遇到了一位旅行者,这位旅行者告诉你不久前有东西从天而降,沉入了大海\n\n这位旅行者还告诉你,其中一位旅行者曾多次试图到达海底。不幸的是,这位旅行者几年前飞向了黑荆星,从此杳无音信...", 38 | "entry point": true, 39 | "description": "一座被森林覆盖的小岛", 40 | "signal": "TRAVELER", 41 | "probe description": "小岛篝火缕缕炊烟", 42 | "campfire": true 43 | }, 44 | "挪麦空间站": { 45 | "position": { 46 | "y": -127, 47 | "x": -352 48 | }, 49 | "explore": { 50 | "text": "你在深巨星的低轨道上发现了一个(严重损坏的)挪麦空间站。仔细观察会发现它实际上是一个探测器发射器,不过探测器看起来已经发射出去了\n\n附近的一个电脑终端显示,空间站在发射探测器时受损,一个名为 “回响室” 的模块完全断裂,掉进了深巨星", 51 | "discover clue": "APL_1" 52 | }, 53 | "entry point": true, 54 | "description": "一个位于深巨星低轨道上的古空间站", 55 | "gravity": false 56 | }, 57 | "海洋中层": { 58 | "position": { 59 | "y": -61, 60 | "x": -3 61 | }, 62 | "explore": [ 63 | {"text": "你在水下飞来飞去,欣赏着窗前游过的巨型带电水母"}, 64 | { 65 | "text": "你弃船而去,游进一只水母的体内\n\n它向下游入深渊,带着你一路前行", 66 | "move to": "深渊", 67 | "require clue": "APL_3" 68 | } 69 | ], 70 | "entry point": false, 71 | "description": "海洋中层,巨型水母的家园", 72 | "ship access": true, 73 | "probe description": "巨型水母在海洋深处上下游动", 74 | "gravity": false 75 | }, 76 | "一座小岛": { 77 | "position": { 78 | "y": 136, 79 | "x": 148 80 | }, 81 | "explore": "你调查了这个类似捕梦网的结构,并注意到中心平坦的圆形平台上有细小沙粒", 82 | "description": "一座有着类似捕梦网结构的外星建筑的小岛" 83 | }, 84 | "天线小岛": { 85 | "position": { 86 | "y": 45, 87 | "x": 195 88 | }, 89 | "explore": { 90 | "text": "一根奇特的天线从岛中央伸出,直插云霄\n\n你发现了一条通往岛内的通道,在里面发现了一个奇怪的古代装置\n\n一个陶罐放在能量场中心的底座上。陶罐从底座上滚落,摔碎在地板上。一道亮光闪过,你看到破碎的碎片重新组合在了底座上!陶罐再次滚落到地上,周而复始...\n\n你能找到的挪麦人的记录都相当隐晦,但似乎它们似乎想建造一个更大的装置...前提是它们能产生运行它所需的大量能源", 91 | "discover clue": "TLD_1" 92 | }, 93 | "entry point": true, 94 | "description": "一座有一根伸向天空的奇怪天线的小岛" 95 | }, 96 | "风暴区": { 97 | "position": { 98 | "y": -148, 99 | "x": -130 100 | }, 101 | "explore": [ 102 | {"text": "你飞进了一个湍急的龙卷风中,巨大的龙卷风在海面上肆虐\n\n你不小心撞入了龙卷风当中,并被龙卷风强大的漩涡短暂抛向高层大气\n\n你坠回地面,发现了一个小型挪麦研究站。看起来研究人员正在监测龙卷风,并向碎空星的天文台报告它们的发现"}, 103 | { 104 | "text": "你发现了一个逆时针旋转的龙卷风,并飞了进去\n\n你的飞船被强大的下行漩涡猛烈地推向水下深处!", 105 | "require clue": "APL_2", 106 | "move to": "海洋中层" 107 | } 108 | ], 109 | "entry point": true, 110 | "description": "一团充满龙卷风的强劲云团", 111 | "ship access": true, 112 | "gravity": false 113 | } 114 | }, 115 | "Connections": [ 116 | { 117 | "Node 2": "天线小岛", 118 | "Node 1": "一座小岛" 119 | }, 120 | { 121 | "fail event": "dive attempt", 122 | "Node 2": "禅境小岛", 123 | "one-way": "true", 124 | "Node 1": "海洋中层" 125 | }, 126 | { 127 | "fail event": "dive attempt", 128 | "Node 2": "量子小岛", 129 | "one-way": "true", 130 | "Node 1": "海洋中层" 131 | }, 132 | { 133 | "fail event": "dive attempt", 134 | "Node 2": "天线小岛", 135 | "one-way": "true", 136 | "Node 1": "海洋中层" 137 | }, 138 | { 139 | "fail event": "dive attempt", 140 | "Node 2": "风暴区", 141 | "one-way": "true", 142 | "Node 1": "海洋中层" 143 | }, 144 | { 145 | "fail event": "barrier attempt", 146 | "Node 2": "海洋中层", 147 | "one-way": "true", 148 | "Node 1": "深渊" 149 | } 150 | ] 151 | } -------------------------------------------------------------------------------- /data/sectors/quantum_moon.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "欢迎来到量子卫星"}, 3 | "Nodes": { 4 | "量子沙漠": { 5 | "position": { 6 | "y": -36, 7 | "x": -2 8 | }, 9 | "quantum location" : 0, 10 | "explore": "卫星表面被沙子覆盖,上面长满了仙人掌,当你未注视它们时,它们的位置会改变", 11 | "entry point": true, 12 | "description": "一片量子沙漠" 13 | }, 14 | "量子森林": { 15 | "position": { 16 | "y": -36, 17 | "x": -2 18 | }, 19 | "quantum location" : 1, 20 | "explore": "你发现自己被树木环绕,当你未注视它们时,它们的位置会改变。奇怪的是,你还发现了村子里的一盏灯和一个路标。它们为什么会在这里?", 21 | "entry point": true, 22 | "description": "一片量子森林" 23 | }, 24 | "量子冰原": { 25 | "position": { 26 | "y": -36, 27 | "x": -2 28 | }, 29 | "quantum location" : 2, 30 | "explore": "卫星表面被冰川覆盖。几棵弯弯折折的树(和几盏古老的灯)在你未注视它们时会改变位置", 31 | "entry point": true, 32 | "description": "一片量子冰原" 33 | }, 34 | "量子湿地": { 35 | "position": { 36 | "y": -36, 37 | "x": -2 38 | }, 39 | "quantum location" : 3, 40 | "explore": "卫星表面被水覆盖。小而空灵的水母漂浮于其中,当你未注视它们时,它们的位置会改变", 41 | "entry point": true, 42 | "description": "一片量子水域" 43 | }, 44 | "量子平地": { 45 | "position": { 46 | "y": -36, 47 | "x": -2 48 | }, 49 | "quantum location" : 4, 50 | "explore": 51 | { 52 | "text" : "你走到了一片平坦的地面上,上面散布着成堆的量子岩石,当你未注视它们时,这些岩石会移动。令你大吃一惊的是,一位活生生的挪麦古人站在你面前\n\n古人告诉你,量子卫星实际上是信号来源的一部分。它指向天空,你抬头看到遥远的超凡的能量云,周围环绕着数百个量子卫星\n\n古人不知道它是什么,只知道它似乎是所有量子现象的根本原因。如果这是真的,那么云里有什么?如果它被一个有意识的实体观察到又会发生什么?" 53 | }, 54 | "entry point": true, 55 | "description": "一片布满了量子岩石的平地,高大身影在此矗立" 56 | }, 57 | "量子塔楼": { 58 | "position": { 59 | "y": 90, 60 | "x": -4 61 | }, 62 | "quantum location" : 0, 63 | "entanglement node" : true, 64 | "explore": 65 | { 66 | "text" : "你的目光盯着这座古老的塔楼(这样它就不会在你靠近它时改变位置)然后走进去。尽管里面非常黑暗,但似乎没有什么有趣的东西" 67 | }, 68 | "description": "一座古老的塔楼" 69 | }, 70 | "量子木屋": { 71 | "position": { 72 | "y": 90, 73 | "x": -4 74 | }, 75 | "quantum location" : 1, 76 | "entanglement node" : true, 77 | "explore": 78 | { 79 | "text" : "你的目光盯着这座小木屋(这样它就不会在你靠近它时改变位置)然后走进去。尽管里面非常黑暗,但似乎没有什么有趣的东西" 80 | }, 81 | "description": "一间来自村庄的小木屋" 82 | }, 83 | "量子悬屋": { 84 | "position": { 85 | "y": 90, 86 | "x": -4 87 | }, 88 | "quantum location" : 2, 89 | "entanglement node" : true, 90 | "explore": 91 | { 92 | "text" : "你的目光盯着这座悬崖边的古老房屋(这样它就不会在你靠近它时改变位置)然后走进去。尽管里面非常黑暗,但似乎没有什么有趣的东西" 93 | }, 94 | "description": "一座悬崖边的古老房屋" 95 | }, 96 | "量子研究站": { 97 | "position": { 98 | "y": 90, 99 | "x": -4 100 | }, 101 | "quantum location" : 3, 102 | "entanglement node" : true, 103 | "explore": 104 | { 105 | "text" : "你的目光盯着这个古研究站(这样它就不会在你靠近它时改变位置)然后走进去。尽管里面非常黑暗,但似乎没有什么有趣的东西" 106 | }, 107 | "description": "一座古研究站" 108 | }, 109 | "量子祭坛": { 110 | "position": { 111 | "y": 90, 112 | "x": -4 113 | }, 114 | "quantum location" : 4, 115 | "entanglement node" : true, 116 | "signal": "QUANTUM", 117 | "explore": 118 | { 119 | "text" : "挪麦人似乎为了纪念信号而建造了某种祭坛,里面一片漆黑" 120 | }, 121 | "description": "一座古祭坛" 122 | } 123 | }, 124 | "Connections": [{ 125 | "Node 2": "量子塔楼", 126 | "Node 1": "量子沙漠" 127 | },{ 128 | "Node 2": "量子木屋", 129 | "Node 1": "量子森林" 130 | },{ 131 | "Node 2": "量子悬屋", 132 | "Node 1": "量子冰原" 133 | },{ 134 | "Node 2": "量子研究站", 135 | "Node 1": "量子湿地" 136 | },{ 137 | "Node 2": "量子祭坛", 138 | "Node 1": "量子平地" 139 | }] 140 | } -------------------------------------------------------------------------------- /data/sectors/rocky_twin.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "余烬双星上纵横交错着条纹状岩石峡谷,仙人掌点缀其间,地表凹陷处隐约可见复杂的地下洞穴网络\n\n巨大的沙漏从沙漏双星的一颗延伸到另一颗。沙子似乎从灰烬双星的表面流出,流入余烬双星的峡谷和洞穴中"}, 3 | "Nodes": { 4 | "湖床洞穴": { 5 | "position": { 6 | "y": 77, 7 | "x": -1 8 | }, 9 | "explore": { 10 | "text": "你发现了一个挪麦研究前哨站的遗迹,包括研究人员的骸骨。令人不安的是,只要你停止注视这些骸骨,它们就会移动\n\n这种令人毛骨悚然的现象是由洞穴中心的一块黑色岩石碎片造成的(据挪麦研究人员描述,这块碎片来自 “游荡的卫星”)\n\n研究人员发现,量子碎片会让附近的物体也拥有量子性质,甚至对生物也有效果(前提是光线太暗,无法观察自己或周边环境)", 11 | "discover clue": "QM_2" 12 | }, 13 | "description": "一个封闭而黑暗的洞穴", 14 | "allow telescope": false, 15 | "signal": "QUANTUM", 16 | "entanglement node": true 17 | }, 18 | "坠毁的逃生舱": { 19 | "position": { 20 | "y": -181, 21 | "x": -95 22 | }, 23 | "explore": { 24 | "text": "你发现了一个坠毁的逃生舱。根据日志描述,它是从黑荆星内部的某处发射的\n\n挪麦人似乎立即从逃生舱躲到了地下,以躲避太阳的强烈热量", 25 | "reveal paths": ["地下城市"] 26 | }, 27 | "entry point": true, 28 | "description": "一个内有坠毁逃生舱的陨石坑", 29 | "reveal paths": ["地下城市"], 30 | "signal": "BEACON" 31 | }, 32 | "干涸的湖床": { 33 | "position": { 34 | "y": 199, 35 | "x": -3 36 | }, 37 | "explore": { 38 | "text": "你遇到一位正忙着用望远镜扫视天空的旅行者。这位旅行者告诉了你关于“量子卫星”的事,这是一个不盯着就会瞬移的天体。显然,挪麦人出于某些原因而对它非常感兴趣\n\n在你烤棉花糖的时候,这位旅行者解释说,你所处的巨大深坑曾经是一个充满水的湖泊,就像干涸的峡谷也曾是河流一样\n\n旅行者告诉你湖床底部有一个秘密洞穴的入口,并提到挪麦人可能在下面发现了一些奇怪的东西...", 39 | "reveal paths": ["湖床洞穴"] 40 | }, 41 | "entry point": true, 42 | "description": "干涸的湖床与一堆篝火", 43 | "signal": "TRAVELER", 44 | "campfire": true 45 | }, 46 | "狭小的洞穴": { 47 | "position": { 48 | "y": 68, 49 | "x": -101 50 | }, 51 | "explore": {"text": "起初,这个洞穴似乎并不起眼,但你发现,有时(如果你反复回头看),洞穴中央会出现一大块看起来很不自然的岩石"}, 52 | "description": "一个有点冷清的洞穴", 53 | "allow telescope": false, 54 | "signal": "QUANTUM", 55 | "entanglement node": true 56 | }, 57 | "地下城市": { 58 | "position": { 59 | "y": -87, 60 | "x": -5 61 | }, 62 | "explore": {"text": "在一个巨大的地下洞穴中,你发现了钟乳石与石笋边的许多悬崖小屋\n\n看来挪麦人在这里生活了很长时间"}, 63 | "description": "一座壮观的地下城市", 64 | "allow telescope": false 65 | }, 66 | "重力炮": { 67 | "position": { 68 | "y": -61, 69 | "x": 163 70 | }, 71 | "explore": {"text": "这个地方看起来像是挪麦人用来向其他星球发射飞船的某种大炮"}, 72 | "description": "一个内衬大金属环的圆筒形腔体", 73 | "allow telescope": false 74 | }, 75 | "北部峡谷": {"position": { 76 | "y": -79, 77 | "x": -184 78 | }}, 79 | "拱形峡谷": { 80 | "position": { 81 | "y": -182, 82 | "x": 82 83 | }, 84 | "explore": { 85 | "text": "你发现一座古老的挪麦观测台就坐落在峡谷的顶端\n\n挪麦人注意到,每当观测卫星时,它都会随机出现在不同的位置。它们推测,这个“游荡的卫星”可能与宇宙之眼有关,并决心前往宇宙之眼\n\n在峡谷底部,你发现了两个通往地下的洞穴入口!", 86 | "reveal paths": [ 87 | "地下城市", 88 | "重力炮" 89 | ] 90 | }, 91 | "entry point": true, 92 | "description": "一个具有拱形岩层的狭窄峡谷", 93 | "reveal paths": [ 94 | "地下城市", 95 | "重力炮" 96 | ] 97 | }, 98 | "化石洞穴": { 99 | "position": { 100 | "y": -35, 101 | "x": -100 102 | }, 103 | "explore": { 104 | "text": "在洞穴的远处,你发现了一块巨大的鮟鱇鱼化石嵌在洞壁上。它的诱饵还散发着微弱的光芒\n\n附近有一组听起来与儿童游戏相关的规则。一名玩家(鮟鱇鱼)会闭上眼睛,尝试抓住试图溜过鮟鱇鱼到达目标(逃生舱)的其他玩家", 105 | "discover clue": "D_2" 106 | }, 107 | "description": "一个巨大的洞穴,洞壁上镶嵌着一块巨大的化石", 108 | "allow telescope": false 109 | }, 110 | "地图室": { 111 | "position": { 112 | "y": 48, 113 | "x": 164 114 | }, 115 | "explore": { 116 | "text": "你在余烬双星上找到了挪麦人建造的巨型机器的图纸\n\n这台机器的设计利用了超新星的能量\n\n控制中心位于行星中心的一个中空空腔内,与地表完全隔绝", 117 | "discover clue": "TLD_3" 118 | }, 119 | "description": "一座高大圆柱形塔楼的底层", 120 | "allow telescope": false 121 | }, 122 | "南部峡谷": {"position": { 123 | "y": 98, 124 | "x": -184 125 | }}, 126 | "秘密避难所": { 127 | "position": { 128 | "y": 7, 129 | "x": -4 130 | }, 131 | "explore": {"text": "这里是一片坟墓。地上到处都是古老的骸骨。墙上还有一副壁画,展示着沙漏双星的上方有颗噩梦般的彗星"}, 132 | "description": "布满骸骨的狭小洞穴", 133 | "allow telescope": false 134 | } 135 | }, 136 | "Connections": [ 137 | { 138 | "Node 2": "湖床洞穴", 139 | "Node 1": "干涸的湖床", 140 | "one-way": true, 141 | "hidden": true 142 | }, 143 | { 144 | "Node 2": "干涸的湖床", 145 | "Node 1": "地图室", 146 | "one-way": true 147 | }, 148 | { 149 | "Node 2": "秘密避难所", 150 | "Node 1": "地下城市" 151 | }, 152 | { 153 | "Node 2": "化石洞穴", 154 | "Node 1": "地下城市" 155 | }, 156 | { 157 | "Node 2": "地图室", 158 | "Node 1": "重力炮" 159 | }, 160 | { 161 | "Node 2": "地下城市", 162 | "Node 1": "重力炮" 163 | }, 164 | { 165 | "Node 2": "地下城市", 166 | "Node 1": "坠毁的逃生舱", 167 | "hidden": true 168 | }, 169 | { 170 | "Node 2": "地下城市", 171 | "Node 1": "拱形峡谷", 172 | "hidden": true 173 | }, 174 | { 175 | "Node 2": "坠毁的逃生舱", 176 | "Node 1": "拱形峡谷" 177 | }, 178 | { 179 | "Node 2": "重力炮", 180 | "Node 1": "拱形峡谷", 181 | "hidden": true 182 | }, 183 | { 184 | "Node 2": "坠毁的逃生舱", 185 | "Node 1": "北部峡谷" 186 | }, 187 | { 188 | "Node 2": "干涸的湖床", 189 | "Node 1": "南部峡谷" 190 | }, 191 | { 192 | "Node 2": "南部峡谷", 193 | "Node 1": "北部峡谷" 194 | }, 195 | { 196 | "Node 2": "南部峡谷", 197 | "Node 1": "狭小的洞穴" 198 | } 199 | ] 200 | } -------------------------------------------------------------------------------- /data/sectors/sandy_twin.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector Arrival": {"text": "灰烬双星是一片毫无特色的沙漠,只有一些高大的古建筑\n\n在赤道上有四个大小不一的圆柱形塔楼\n\n两个巨大的天线状结构分别从南北两极伸出\n\n巨大的沙漏从沙漏双星的一颗延伸到另一颗。沙子似乎从灰烬双星的表面流出,流入余烬双星的峡谷和洞穴中"}, 3 | "Nodes": { 4 | "庞大的古塔": { 5 | "position": { 6 | "y": -1, 7 | "x": 211 8 | }, 9 | "explore": [ 10 | { 11 | "on turn": 0, 12 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,余烬双星就在头顶上", 13 | "turn cycle": 4 14 | }, 15 | { 16 | "on turn": 1, 17 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,木灶星就在头顶上", 18 | "turn cycle": 4 19 | }, 20 | { 21 | "on turn": 2, 22 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,碎空星就在头顶上", 23 | "turn cycle": 4 24 | }, 25 | { 26 | "on turn": 3, 27 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,深巨星就在头顶上\n\n突然,塔内充满光芒,周围的世界消失了!", 28 | "teleport to": "一座小岛", 29 | "turn cycle": 4 30 | } 31 | ], 32 | "description": "一座庞大的古塔" 33 | }, 34 | "破旧的古塔": { 35 | "position": { 36 | "y": -1, 37 | "x": -209 38 | }, 39 | "explore": [ 40 | { 41 | "on turn": 0, 42 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,余烬双星就在头顶上", 43 | "turn cycle": 4 44 | }, 45 | { 46 | "on turn": 1, 47 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,木灶星就在头顶上\n\n突然,塔内充满光芒,周围的世界消失了!", 48 | "teleport to": "古矿坑", 49 | "turn cycle": 4 50 | }, 51 | { 52 | "on turn": 2, 53 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,碎空星就在头顶上", 54 | "turn cycle": 4 55 | }, 56 | { 57 | "on turn": 3, 58 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,深巨星就在头顶上", 59 | "turn cycle": 4 60 | } 61 | ], 62 | "description": "一座千疮百孔的古塔" 63 | }, 64 | "残破的古塔": { 65 | "position": { 66 | "y": -194, 67 | "x": -2 68 | }, 69 | "explore": [ 70 | { 71 | "on turn": 0, 72 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,余烬双星就在头顶上", 73 | "turn cycle": 4 74 | }, 75 | { 76 | "on turn": 1, 77 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,木灶星就在头顶上", 78 | "turn cycle": 4 79 | }, 80 | { 81 | "on turn": 2, 82 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,碎空星就在头顶上\n\n突然,塔内充满光芒,周围的世界消失了!", 83 | "teleport to": "冰川陆架", 84 | "turn cycle": 4 85 | }, 86 | { 87 | "on turn": 3, 88 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,深巨星就在头顶上", 89 | "turn cycle": 4 90 | } 91 | ], 92 | "description": "一座断壁残垣的古塔" 93 | }, 94 | "时间循环中心": { 95 | "position": { 96 | "y": 80, 97 | "x": -3 98 | }, 99 | "explore": { 100 | "text": "查看时间循环中心事件屏幕", 101 | "fire event": "time loop central", 102 | "reveal paths": [ 103 | "成对的古塔" 104 | ] 105 | }, 106 | "entry point": false, 107 | "description": "灰烬双星中心的密室" 108 | }, 109 | "成对的古塔": { 110 | "position": { 111 | "y": 218, 112 | "x": -2 113 | }, 114 | "explore": [ 115 | { 116 | "on turn": 0, 117 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,余烬双星就在头顶上\n\n突然,塔内充满光芒,周围的世界消失了!", 118 | "teleport to": "时间循环中心", 119 | "turn cycle": 4 120 | }, 121 | { 122 | "on turn": 1, 123 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,木灶星就在头顶上", 124 | "turn cycle": 4 125 | }, 126 | { 127 | "on turn": 2, 128 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,碎空星就在头顶上", 129 | "turn cycle": 4 130 | }, 131 | { 132 | "on turn": 3, 133 | "text": "你走上了塔楼中央的圆形小平台\n塔楼没有天花板,当行星自转时,你可以看到天体在你头顶旋转\n\n此时此刻,深巨星就在头顶上", 134 | "turn cycle": 4 135 | } 136 | ], 137 | "description": "由一条短道连接起来的两座小古塔" 138 | }, 139 | "巨型天线": { 140 | "position": { 141 | "y": -1, 142 | "x": -2 143 | }, 144 | "explore": "一根巨大的天线从沙地中伸出,耸立在你的头顶\n你发现了一个曾经通往星球中心的舱门,但它现在已经被封死了", 145 | "entry point": true, 146 | "description": "巨型天线状结构" 147 | } 148 | }, 149 | "Connections": [ 150 | { 151 | "Node 2": "破旧的古塔", 152 | "Node 1": "成对的古塔" 153 | }, 154 | { 155 | "Node 2": "残破的古塔", 156 | "Node 1": "破旧的古塔" 157 | }, 158 | { 159 | "Node 2": "庞大的古塔", 160 | "Node 1": "残破的古塔" 161 | }, 162 | { 163 | "Node 2": "成对的古塔", 164 | "Node 1": "庞大的古塔" 165 | }, 166 | { 167 | "Node 2": "破旧的古塔", 168 | "Node 1": "巨型天线" 169 | }, 170 | { 171 | "Node 2": "庞大的古塔", 172 | "Node 1": "巨型天线" 173 | }, 174 | { 175 | "Node 2": "成对的古塔", 176 | "one-way": true, 177 | "Node 1": "时间循环中心", 178 | "hidden": true 179 | } 180 | ] 181 | } -------------------------------------------------------------------------------- /data/sectors/timber_hearth.json: -------------------------------------------------------------------------------- 1 | { 2 | "Connections": [ 3 | { 4 | "Node 2": "石窟", 5 | "Node 1": "古矿坑" 6 | }, 7 | { 8 | "Node 2": "天文台", 9 | "Node 1": "村庄" 10 | }, 11 | { 12 | "Node 2": "村庄", 13 | "hidden": true, 14 | "Node 1": "石窟", 15 | "description": "一条横穿星球中心的水下通道" 16 | } 17 | ], 18 | "Sector Arrival": {"text": "木灶星表面满是覆盖着树木的环形山,其中一个是你们这一物种的家园"}, 19 | "Nodes": { 20 | "卫星": { 21 | "explore": "你发现了挪麦天文台的遗址\n\n挪麦人尝试捕获特定的信号,但一无所获,信号的源头被称作宇宙之眼", 22 | "description": "有少量挪麦遗址的卫星", 23 | "position": { 24 | "x": 301, 25 | "y": -179 26 | }, 27 | "entry point": true 28 | }, 29 | "村庄": { 30 | "explore": "你与村民们交流了一番,他们祝你探险顺利", 31 | "description": "环形山内的小村庄", 32 | "position": { 33 | "x": 73, 34 | "y": 11 35 | }, 36 | "campfire": true, 37 | "entry point": true 38 | }, 39 | "石窟": { 40 | "explore": { 41 | "text": "你发现挪麦人在研究分析它们在这个石窟中发现的单细胞生物\n\n当你探向水池深处时,你发现了一条水下密道!", 42 | "reveal paths": ["村庄"] 43 | }, 44 | "allow telescope": false, 45 | "description": "一个石窟,内有深潭", 46 | "position": { 47 | "x": -80, 48 | "y": 11 49 | } 50 | }, 51 | "古矿坑": { 52 | "explore": "挪麦人在这个环形山开采原料。从挖掘量来看,他们一定是在建造什么庞然大物\n\n在环形山边缘还有一个类似捕梦网的结构,你注意到中心平坦的圆形平台上有细小的沙粒", 53 | "description": "挪麦人采矿遗址", 54 | "position": { 55 | "x": -176, 56 | "y": 11 57 | }, 58 | "entry point": true 59 | }, 60 | "天文台": { 61 | "explore": { 62 | "fire event": "learn launch codes", 63 | "text": "你参观了天文台博物馆,都是在以前的探险中带回的文物收藏\n\n博物馆有一场关于\u201c挪麦文明\u201d的大型展览,\u201c挪麦\u201d是一个神秘的外星种族,数千年前曾居住在你们的太阳系中。人们对其知之甚少,但在各个星球上仍能看到他们的遗迹\n\n你与馆长交谈并获取了发射密码。你的飞船 (橙色三角形) 已准备起飞!\n\n在你走出博物馆的路上,挪麦雕像的眼睛开始发光。你眼前出现了闪光,伴以一系列奇异的图像(包括太阳演变为超新星的图像)\n\n嗯...好奇怪" 64 | }, 65 | "description": "俯瞰村庄的小天文台", 66 | "position": { 67 | "x": 176, 68 | "y": -46 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Outer Wilds:文字冒险 7 | 8 | 9 | 10 | 49 | 50 | 51 |
52 | 53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outer-wilds-text-adventure", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "@types/p5": "^1.7.6", 6 | "chokidar-cli": "^2.1.0", 7 | "concurrently": "^6.5.1", 8 | "esbuild": "0.21.4", 9 | "lite-server": "^2.6.1", 10 | "typescript": "^5.4.5" 11 | }, 12 | "dependencies": { 13 | "p5": "^1.9.4" 14 | }, 15 | "scripts": { 16 | "dev": "concurrently \"npm run server\" \"npm run watch\"", 17 | "watch": "chokidar 'src/**/*' -c 'npm run bundle'", 18 | "server": "lite-server", 19 | "bundle": "esbuild src/app.ts --bundle --minify --sourcemap --outfile=bundle.js" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AnglerfishNode.ts: -------------------------------------------------------------------------------- 1 | import { OWNode } from "./Node"; 2 | import { messenger } from "./app"; 3 | import { JSONObject } from "./compat"; 4 | 5 | export class AnglerfishNode extends OWNode 6 | { 7 | constructor(nodeName: string, nodeJSONObj: JSONObject) 8 | { 9 | super(nodeName, nodeJSONObj); 10 | this.entryPoint = true; 11 | this.shipAccess = true; 12 | this.gravity = false; 13 | 14 | this._visible = true; 15 | } 16 | 17 | getKnownName(): string 18 | { 19 | if (this._visited) return "鮟鱇鱼"; 20 | else return "???"; 21 | } 22 | 23 | getDescription(): string 24 | { 25 | return "一条巨大而饥饿的鮟鱇鱼"; 26 | } 27 | 28 | getProbeDescription(): string 29 | { 30 | return "一束光穿过雾气"; 31 | } 32 | 33 | hasDescription(): boolean {return true;} 34 | 35 | isProbeable(): boolean {return true;} 36 | 37 | isExplorable(): boolean {return true;} // tricks graphics into rendering question mark 38 | 39 | visit(): void 40 | { 41 | this._visited = true; 42 | this.setVisible(true); 43 | 44 | messenger.sendMessage("death by anglerfish"); 45 | 46 | if (this._observer != null) 47 | { 48 | this._observer.onNodeVisited(this); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/AudioManager.ts: -------------------------------------------------------------------------------- 1 | import { minim } from "./app"; 2 | import { AudioPlayer, println } from "./compat"; 3 | 4 | export class SoundLibrary 5 | { 6 | static kazooTheme: AudioPlayer; 7 | 8 | static loadSounds(): void 9 | { 10 | println("Sounds loading..."); 11 | SoundLibrary.kazooTheme = minim.loadFile("data/audio/ow_kazoo_theme.mp3"); 12 | } 13 | } 14 | 15 | export class AudioManager 16 | { 17 | static currentSound: AudioPlayer; 18 | 19 | constructor() 20 | { 21 | SoundLibrary.loadSounds(); 22 | } 23 | 24 | static play(sound: AudioPlayer): void 25 | { 26 | AudioManager.currentSound = sound; 27 | AudioManager.currentSound.play(); 28 | } 29 | 30 | static pause(): void 31 | { 32 | if (AudioManager.currentSound != null) 33 | { 34 | AudioManager.currentSound.pause(); 35 | } 36 | else 37 | { 38 | println("Current sound is NULL!!!"); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Button.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "p5"; 2 | import { Entity } from "./Entity"; 3 | import { Vector2 } from "./Vector2"; 4 | import { smallFontData } from "./app"; 5 | 6 | export interface ButtonObserver 7 | { 8 | onButtonUp(button: Button): void; 9 | onButtonEnterHover(button: Button): void; 10 | onButtonExitHover(button: Button): void; 11 | } 12 | 13 | export class Button extends Entity 14 | { 15 | id: string; 16 | hoverState: boolean = false; 17 | visible: boolean = true; 18 | enabled: boolean = true; 19 | 20 | _bounds: Vector2; 21 | _observer: ButtonObserver; 22 | 23 | _buttonDown: boolean = false; 24 | _wasMousePressed: boolean = false; 25 | 26 | _buttonColor: Color = color(0, 0, 100); 27 | _hasDisabledPrompt: boolean = false; 28 | _disabledPrompt: string; 29 | 30 | constructor(buttonID: string, x: number = 0, y: number = 0, w: number = 150, h: number = 50) 31 | { 32 | super(new Vector2(x, y)); 33 | this.id = this._disabledPrompt = buttonID; 34 | this._bounds = new Vector2(w, h); 35 | } 36 | 37 | setColor(newColor: Color): void {this._buttonColor = newColor;} 38 | setDisabledPrompt(prompt: string): void 39 | { 40 | this._disabledPrompt = prompt; 41 | this._hasDisabledPrompt = true; 42 | } 43 | 44 | getWidth(): number 45 | { 46 | return this._bounds.x; 47 | } 48 | 49 | setObserver(observer: ButtonObserver): void 50 | { 51 | this._observer = observer; 52 | } 53 | 54 | update(): void 55 | { 56 | if (!this.enabled) 57 | { 58 | this._buttonDown = false; 59 | this.hoverState = false; 60 | return; 61 | } 62 | 63 | if (this.isPointInBounds(mouseX, mouseY)) 64 | { 65 | if (!this.hoverState) 66 | { 67 | this.hoverState = true; 68 | this.onButtonEnterHover(); 69 | this._observer.onButtonEnterHover(this); 70 | } 71 | 72 | if (!this._wasMousePressed && this.mousePressed()) 73 | { 74 | this._buttonDown = true; 75 | } 76 | // fire event on release 77 | if (this._buttonDown && !this.mousePressed()) 78 | { 79 | this._buttonDown = false; 80 | this.onButtonUp(); 81 | this._observer.onButtonUp(this); 82 | } 83 | } 84 | else 85 | { 86 | this._buttonDown = false; 87 | 88 | if (this.hoverState) 89 | { 90 | this.hoverState = false; 91 | this.onButtonExitHover(); 92 | this._observer.onButtonExitHover(this); 93 | } 94 | } 95 | 96 | this._wasMousePressed = this.mousePressed(); 97 | } 98 | 99 | onButtonExitHover(): void{} 100 | onButtonEnterHover(): void{} 101 | onButtonUp(): void{} 102 | 103 | mousePressed(): boolean 104 | { 105 | return mouseIsPressed;// && mouseButton == LEFT; 106 | } 107 | 108 | draw(): void 109 | { 110 | if (!this.visible) {return;} 111 | 112 | let alpha: number = 100; 113 | 114 | if (!this.enabled) alpha = 25; 115 | 116 | stroke(hue(this._buttonColor), saturation(this._buttonColor), brightness(this._buttonColor), alpha); 117 | fill(0, 0, 0); 118 | 119 | if (this.hoverState) 120 | { 121 | if (this._buttonDown) 122 | { 123 | stroke(0, 100, 100); 124 | } 125 | else 126 | { 127 | stroke(200, 100, 100); 128 | } 129 | } 130 | 131 | this.drawShape(); 132 | this.drawText(alpha); 133 | } 134 | 135 | drawShape(): void 136 | { 137 | rectMode(CENTER); 138 | rect(this.screenPosition.x, this.screenPosition.y, this._bounds.x, this._bounds.y); 139 | } 140 | 141 | drawText(alpha: number): void 142 | { 143 | fill(0, 0, 100, alpha); 144 | textSize(14); 145 | textFont(smallFontData); 146 | textAlign(CENTER, CENTER); 147 | 148 | if (this.enabled) 149 | { 150 | text(this.id, this.screenPosition.x, this.screenPosition.y); 151 | } 152 | else 153 | { 154 | text(this._disabledPrompt, this.screenPosition.x, this.screenPosition.y); 155 | } 156 | } 157 | 158 | isPointInBounds(x: number, y: number): boolean 159 | { 160 | if (x > this.screenPosition.x - this._bounds.x * 0.5 && x < this.screenPosition.x + this._bounds.x * 0.5) 161 | { 162 | if (y > this.screenPosition.y - this._bounds.y * 0.5 && y < this.screenPosition.y + this._bounds.y * 0.5) 163 | { 164 | return true; 165 | } 166 | } 167 | return false; 168 | } 169 | } -------------------------------------------------------------------------------- /src/DatabaseScreen.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "p5"; 2 | import { Button } from "./Button"; 3 | import { Entity } from "./Entity"; 4 | import { Curiosity } from "./Enums"; 5 | import { Clue } from "./PlayerData"; 6 | import { OWScreen } from "./Screen"; 7 | import { playerData, feed, gameManager, mediumFontData } from "./app"; 8 | 9 | export interface DatabaseObserver 10 | { 11 | onInvokeClue(clue: Clue): void; 12 | } 13 | 14 | export class DatabaseScreen extends OWScreen implements ClueButtonObserver 15 | { 16 | _clueRoot: Entity; 17 | _activeClue: Clue; 18 | _observer: DatabaseObserver; 19 | 20 | constructor() 21 | { 22 | super(); 23 | this.addButtonToToolbar(new Button("关闭数据库", 0, 0, 150, 50)); 24 | this._clueRoot = new Entity(100, 140); 25 | 26 | // create clue buttons using PlayerData's list of clues 27 | for (let i: number = 0; i < playerData.getClueCount(); i++) 28 | { 29 | const clueButton: ClueButton = new ClueButton(playerData.getClueAt(i), i * 40, this); 30 | this.addButton(clueButton); 31 | this._clueRoot.addChild(clueButton); 32 | } 33 | } 34 | 35 | setObserver(observer: DatabaseObserver): void 36 | { 37 | this._observer = observer; 38 | } 39 | 40 | onEnter(): void 41 | { 42 | } 43 | 44 | onExit(): void 45 | { 46 | this._observer = null; 47 | } 48 | 49 | onClueMouseOver(clue: Clue): void 50 | { 51 | this._activeClue = clue; 52 | } 53 | 54 | onClueSelected(clue: Clue): void 55 | { 56 | if (this._observer != null) 57 | { 58 | this._observer.onInvokeClue(clue); 59 | } 60 | else 61 | { 62 | feed.publish("那个现在还不能帮助到你", true); 63 | } 64 | } 65 | 66 | onButtonUp(button: Button): void 67 | { 68 | if (button.id == "关闭数据库") 69 | { 70 | gameManager.popScreen(); 71 | } 72 | } 73 | 74 | update(): void {} 75 | 76 | render(): void 77 | { 78 | fill(0, 0, 0); 79 | stroke(0, 0, 100); 80 | 81 | rectMode(CORNER); 82 | 83 | const x: number = width/2 - 100; 84 | const y: number = 200; 85 | const w: number = 500; 86 | const h: number = 300; 87 | 88 | rect(x, y, w, h); 89 | 90 | let _displayText: string = "选择一项条目"; 91 | 92 | if (this._activeClue != null) 93 | { 94 | _displayText = this._activeClue.description; 95 | } 96 | else if (playerData.getKnownClueCount() == 0) 97 | { 98 | _displayText = "暂无条目"; 99 | } 100 | 101 | textFont(mediumFontData); 102 | textSize(18); 103 | textAlign(LEFT, TOP); 104 | fill(0, 0, 100); 105 | 106 | // 自动换行处理 107 | const wrappedLines = this.wrapText(_displayText, w - 20); 108 | 109 | let currentY = y + 10; // 初始 y 坐标 110 | const lineHeight = 24; // 行高 111 | 112 | for (const line of wrappedLines) { 113 | if (line === "") { 114 | // 如果是空行,增加 y 坐标,保留空行 115 | currentY += lineHeight; 116 | } else { 117 | text(line, x + 10, currentY); // 绘制每一行文本 118 | currentY += lineHeight; // 增加 y 坐标,确保下一行不会重叠 119 | } 120 | } 121 | 122 | feed.render(); 123 | } 124 | 125 | /** 126 | * 自动换行函数:根据最大宽度将文本分割成多行,并保留空行 127 | * @param text 原始文本 128 | * @param maxWidth 最大宽度 129 | * @returns 分割后的多行文本数组 130 | */ 131 | wrapText(text: string, maxWidth: number): string[] { 132 | const lines: string[] = []; 133 | const paragraphs = text.split('\n'); // 按换行符分割段落 134 | 135 | for (const paragraph of paragraphs) { 136 | if (paragraph.trim() === "") { 137 | // 如果段落是空的,保留空行 138 | lines.push(""); 139 | continue; 140 | } 141 | 142 | let currentLine = ""; 143 | 144 | for (const char of paragraph) { 145 | const testLine = currentLine + char; 146 | if (textWidth(testLine) > maxWidth) { 147 | lines.push(currentLine); // 当前行已满,保存 148 | currentLine = char; // 开始新的一行 149 | } else { 150 | currentLine = testLine; // 继续添加字符 151 | } 152 | } 153 | 154 | if (currentLine) { 155 | lines.push(currentLine); // 保存最后一行 156 | } 157 | } 158 | 159 | return lines; 160 | } 161 | } 162 | 163 | export interface ClueButtonObserver 164 | { 165 | onClueSelected(clue: Clue): void; 166 | onClueMouseOver(clue: Clue): void; 167 | } 168 | 169 | export class ClueButton extends Button 170 | { 171 | _clue: Clue; 172 | _clueObserver: ClueButtonObserver; 173 | 174 | constructor(clue: Clue, y: number, observer: ClueButtonObserver) 175 | { 176 | super(clue.name, (textWidth(clue.name) + 10) * 0.5, y, textWidth(clue.name) + 10, 20); 177 | this._clue = clue; 178 | this._clueObserver = observer; 179 | } 180 | 181 | getClue(): Clue 182 | { 183 | return this._clue; 184 | } 185 | 186 | update(): void 187 | { 188 | this.enabled = this._clue.discovered; 189 | this.visible = this._clue.discovered; 190 | super.update(); 191 | } 192 | 193 | draw(): void 194 | { 195 | if (!this.visible) {return;} 196 | 197 | super.draw(); 198 | 199 | let symbolColor: Color; 200 | 201 | if (this._clue.curiosity == Curiosity.VESSEL) 202 | { 203 | symbolColor = color(100, 100, 100); 204 | } 205 | else if (this._clue.curiosity == Curiosity.ANCIENT_PROBE_LAUNCHER) 206 | { 207 | symbolColor = color(200, 100, 100); 208 | } 209 | else if (this._clue.curiosity == Curiosity.TIME_LOOP_DEVICE) 210 | { 211 | symbolColor = color(20, 100, 100); 212 | } 213 | else 214 | { 215 | symbolColor = color(300, 100, 100); 216 | } 217 | 218 | fill(symbolColor); 219 | noStroke(); 220 | ellipse(this.screenPosition.x - this._bounds.x * 0.5 - 20, this.screenPosition.y, 10, 10); 221 | 222 | noFill(); 223 | stroke(symbolColor); 224 | ellipse(this.screenPosition.x - this._bounds.x * 0.5 - 20, this.screenPosition.y, 15, 15); 225 | } 226 | 227 | onButtonEnterHover(): void 228 | { 229 | this._clueObserver.onClueMouseOver(this._clue); 230 | } 231 | 232 | onButtonUp(): void 233 | { 234 | this._clueObserver.onClueSelected(this._clue); 235 | } 236 | } -------------------------------------------------------------------------------- /src/Entity.ts: -------------------------------------------------------------------------------- 1 | import { OWNode } from "./Node"; 2 | import { Sector } from "./Sector"; 3 | import { Vector2 } from "./Vector2"; 4 | 5 | export class Entity 6 | { 7 | position: Vector2; 8 | screenPosition: Vector2; 9 | 10 | parent: Entity | null = null; 11 | _children: Entity[]; 12 | 13 | constructor() 14 | constructor(x: number, y: number) 15 | constructor(pos: Vector2) 16 | constructor(posOrX?: Vector2 | number, y?: number) 17 | { 18 | let pos: Vector2 = new Vector2() 19 | if (typeof posOrX === 'number' && typeof y === 'number') { 20 | pos = new Vector2(posOrX, y) 21 | } else if (posOrX instanceof Vector2) { 22 | pos = posOrX 23 | } 24 | this.position = new Vector2(); 25 | this.screenPosition = new Vector2(); 26 | this._children = []; 27 | 28 | this.setPosition(pos); 29 | } 30 | 31 | setPosition(newPos: Vector2): void 32 | setPosition(x: number, y: number): void 33 | setPosition(newPosOrX: Vector2 | number, y?: number): void 34 | { 35 | this.position.x = newPosOrX instanceof Vector2 ? newPosOrX.x : newPosOrX; 36 | this.position.y = newPosOrX instanceof Vector2 ? newPosOrX.y : y ?? 0; 37 | 38 | // update child screen positions 39 | if (this.parent != null) 40 | { 41 | this.updateScreenPosition(this.parent.screenPosition); 42 | } 43 | else 44 | { 45 | this.updateScreenPosition(new Vector2(0, 0)); 46 | } 47 | } 48 | 49 | updateScreenPosition(parentScreenPos: Vector2): void 50 | { 51 | this.screenPosition.assign(parentScreenPos.add(this.position)); 52 | 53 | for (let i: number = 0; i < this._children.length; i++) 54 | { 55 | this._children[i].updateScreenPosition(this.screenPosition); 56 | } 57 | } 58 | 59 | setScreenPosition(newScreenPos: Vector2): void 60 | { 61 | if (this.parent == null) 62 | { 63 | this.setPosition(newScreenPos); 64 | } 65 | else 66 | { 67 | this.setPosition(newScreenPos.sub(this.parent.screenPosition)); 68 | } 69 | } 70 | 71 | draw(): void 72 | { 73 | // stub to override 74 | } 75 | 76 | render(): void 77 | { 78 | this.draw(); 79 | } 80 | 81 | addChild(child: Entity): void 82 | { 83 | if (!this._children.includes(child)) 84 | { 85 | this._children.push(child); 86 | child.parent = this; 87 | child.updateScreenPosition(this.screenPosition); 88 | } 89 | } 90 | 91 | removeChild(child: Entity): void 92 | { 93 | if (this._children.includes(child)) { 94 | this._children.splice(this._children.indexOf(child), 1); 95 | } 96 | child.parent = null; 97 | } 98 | } 99 | 100 | export class Actor extends Entity 101 | { 102 | currentSector: Sector | null = null; 103 | lastSector: Sector | null = null; 104 | currentNode: OWNode | null = null; 105 | 106 | static SPEED: number = 10; 107 | 108 | _moveTowardsTarget: boolean = false; 109 | _targetScreenPos: Vector2; 110 | _distToTarget: number = 0; 111 | _offset: Vector2; 112 | 113 | constructor() 114 | { 115 | super(new Vector2(0, 0)); 116 | this._targetScreenPos = new Vector2(); 117 | this._offset = new Vector2(0, 0); 118 | } 119 | 120 | isDead(): boolean 121 | { 122 | return false; 123 | } 124 | 125 | update(): void 126 | { 127 | this._offset.y = 0; 128 | 129 | if (this.currentNode == null || !this.currentNode.gravity) 130 | { 131 | this._offset.y = Math.sin(millis() * 0.005) * 5.0; 132 | } 133 | 134 | if (this._moveTowardsTarget) 135 | { 136 | const d: Vector2 = this._targetScreenPos.sub(this.screenPosition); 137 | this._distToTarget = d.magnitude(); 138 | const v: number = Math.min(this._distToTarget, Actor.SPEED); 139 | this.setScreenPosition(this.screenPosition.add(d.normalize().mult(v))); 140 | } 141 | } 142 | 143 | draw(): void 144 | { 145 | fill(0, 0, 100); 146 | ellipse(this.screenPosition.x, this.screenPosition.y, 10, 10); 147 | } 148 | 149 | setNode(node: OWNode): void 150 | { 151 | this.currentNode = node; 152 | this._targetScreenPos.assign(node.screenPosition); 153 | this.setScreenPosition(node.screenPosition); 154 | } 155 | 156 | moveToScreenPosition(screenPos: Vector2): void 157 | { 158 | this._targetScreenPos.assign(screenPos); 159 | this._moveTowardsTarget = true; 160 | } 161 | 162 | moveToNode(node: OWNode): void 163 | { 164 | this.currentNode = node; 165 | this._targetScreenPos.assign(node.screenPosition); 166 | this._moveTowardsTarget = true; 167 | } 168 | } 169 | 170 | export class Player extends Actor 171 | { 172 | setNode(node: OWNode): void 173 | { 174 | super.setNode(node); 175 | node.visit(); 176 | } 177 | 178 | moveToNode(node: OWNode): void 179 | { 180 | super.moveToNode(node); 181 | node.visit(); 182 | } 183 | 184 | update(): void 185 | { 186 | super.update(); 187 | //println(this._targetScreenPos); 188 | } 189 | 190 | draw(): void 191 | { 192 | this.drawAt(this.screenPosition.x, this.screenPosition.y + this._offset.y, 1); 193 | } 194 | 195 | drawAt(xPos: number, yPos: number, s: number): void 196 | { 197 | stroke(30, 100, 100); 198 | fill(0, 0, 0); 199 | 200 | push(); 201 | translate(xPos, yPos); 202 | scale(s); 203 | ellipse(0, 0, 10, 20); 204 | pop(); 205 | } 206 | } 207 | 208 | export class Ship extends Actor 209 | { 210 | _player: Actor; 211 | 212 | constructor(player: Actor) 213 | { 214 | super(); 215 | this._player = player; 216 | } 217 | 218 | update(): void 219 | { 220 | super.update(); 221 | } 222 | 223 | draw(): void 224 | { 225 | this.drawAt(this.screenPosition.x, this.screenPosition.y + this._offset.y, 1, false); 226 | } 227 | 228 | drawAt(xPos: number, yPos: number, s: number, skipFill: boolean): void 229 | { 230 | stroke(30, 100, 100); 231 | fill(0, 0, 0); 232 | 233 | if (this._player.currentNode == this.currentNode && !skipFill) 234 | { 235 | fill(30,100,100); 236 | } 237 | 238 | push(); 239 | translate(xPos, yPos); 240 | scale(s); 241 | triangle(-20, 15, 20, 15, 0, -20); 242 | pop(); 243 | } 244 | } 245 | 246 | export class Probe extends Actor 247 | { 248 | isDead(): boolean 249 | { 250 | return this._distToTarget < 0.1; 251 | } 252 | 253 | update(): void 254 | { 255 | super.update(); 256 | } 257 | 258 | draw(): void 259 | { 260 | noStroke(); 261 | fill(30, 100, 100); 262 | 263 | push(); 264 | translate(this.screenPosition.x, this.screenPosition.y); 265 | ellipse(0, 0, 10, 10); 266 | pop(); 267 | } 268 | } -------------------------------------------------------------------------------- /src/Enums.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum SectorName {NONE, ROCKY_TWIN, SANDY_TWIN, TIMBER_HEARTH, BRITTLE_HOLLOW, GIANTS_DEEP, DARK_BRAMBLE, COMET, QUANTUM_MOON, EYE_OF_THE_UNIVERSE}; 3 | 4 | export enum Frequency {BEACON, QUANTUM, TRAVELER}; 5 | 6 | export enum Curiosity {VESSEL, TIME_LOOP_DEVICE, QUANTUM_MOON, ANCIENT_PROBE_LAUNCHER}; -------------------------------------------------------------------------------- /src/EventScreen.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "./Button"; 2 | import { DatabaseObserver } from "./DatabaseScreen"; 3 | import { Message } from "./GlobalMessenger"; 4 | import { Clue } from "./PlayerData"; 5 | import { OWScreen } from "./Screen"; 6 | import { EndScreen } from "./TitleScreen"; 7 | import { feed, timeLoop, gameManager, playerData, messenger, locator, mediumFontData } from "./app"; 8 | 9 | export abstract class EventScreen extends OWScreen implements DatabaseObserver 10 | { 11 | static BOX_WIDTH: number = 700; 12 | static BOX_HEIGHT: number = 400; 13 | 14 | _nextButton: Button; 15 | _databaseButton: Button; 16 | 17 | constructor() 18 | { 19 | super() 20 | this.overlay = true; // continue to render BG 21 | this.initButtons(); 22 | } 23 | 24 | initButtons(): void 25 | { 26 | this.addButtonToToolbar(this._nextButton = new Button("继续", 0, 0, 150, 50)); 27 | } 28 | 29 | addDatabaseButton(): void 30 | { 31 | this.addButtonToToolbar(this._databaseButton = new Button("查看数据库", 0, 0, 150, 50)); 32 | } 33 | 34 | addContinueButton(): void 35 | { 36 | this.addButtonToToolbar(this._nextButton = new Button("继续", 0, 0, 150, 50)); 37 | } 38 | 39 | update(): void{} 40 | 41 | renderBackground(): void {} 42 | 43 | render(): void { 44 | push(); 45 | translate(width / 2 - EventScreen.BOX_WIDTH / 2, height / 2 - EventScreen.BOX_HEIGHT / 2); 46 | 47 | stroke(0, 0, 100); 48 | fill(0, 0, 0); 49 | rectMode(CORNER); 50 | rect(0, 0, EventScreen.BOX_WIDTH, EventScreen.BOX_HEIGHT); 51 | 52 | fill(0, 0, 100); 53 | 54 | textFont(mediumFontData); 55 | textSize(18); 56 | textAlign(LEFT, TOP); 57 | 58 | // 获取需要显示的文本 59 | const displayText = this.getDisplayText(); 60 | const wrappedLines = this.wrapText(displayText, EventScreen.BOX_WIDTH - 20); // 自动换行处理 61 | 62 | let y = 10; // 初始 y 坐标 63 | const lineHeight = 24; // 固定行高,确保行间距足够 64 | 65 | for (const line of wrappedLines) { 66 | if (line === "") { 67 | // 如果是空行,直接增加 y 坐标,保留空行 68 | y += lineHeight; 69 | } else { 70 | text(line, 10, y); // 绘制每一行文本 71 | y += lineHeight; // 增加 y 坐标,确保下一行不会重叠 72 | } 73 | } 74 | 75 | pop(); 76 | 77 | feed.render(); 78 | timeLoop.renderTimer(); 79 | } 80 | 81 | /** 82 | * 自动换行函数:根据最大宽度将文本分割成多行,并保留空行 83 | * @param text 原始文本 84 | * @param maxWidth 最大宽度 85 | * @returns 分割后的多行文本数组 86 | */ 87 | wrapText(text: string, maxWidth: number): string[] { 88 | const lines: string[] = []; 89 | const paragraphs = text.split('\n'); // 按换行符分割段落 90 | 91 | for (const paragraph of paragraphs) { 92 | if (paragraph.trim() === "") { 93 | // 如果段落是空的,保留空行 94 | lines.push(""); 95 | continue; 96 | } 97 | 98 | let currentLine = ""; 99 | 100 | for (const char of paragraph) { 101 | const testLine = currentLine + char; 102 | if (textWidth(testLine) > maxWidth) { 103 | lines.push(currentLine); // 当前行已满,保存 104 | currentLine = char; // 开始新的一行 105 | } else { 106 | currentLine = testLine; // 继续添加字符 107 | } 108 | } 109 | 110 | if (currentLine) { 111 | lines.push(currentLine); // 保存最后一行 112 | } 113 | } 114 | 115 | return lines; 116 | } 117 | 118 | onButtonUp(button: Button): void 119 | { 120 | if (button == this._nextButton) 121 | { 122 | this.onContinue(); 123 | } 124 | else if (button == this._databaseButton) 125 | { 126 | gameManager.pushScreen(gameManager.databaseScreen); 127 | gameManager.databaseScreen.setObserver(this); 128 | } 129 | } 130 | 131 | onInvokeClue(clue: Clue): void {} 132 | 133 | abstract getDisplayText(): string; 134 | 135 | abstract onContinue(): void; 136 | 137 | onEnter(): void{} 138 | 139 | onExit(): void{} 140 | } 141 | 142 | export class DeathByAnglerfishScreen extends EventScreen 143 | { 144 | onEnter(): void 145 | { 146 | feed.clear(); 147 | feed.publish("你被鮟鱇鱼吃掉了", true); 148 | } 149 | 150 | getDisplayText(): string 151 | { 152 | return "你在飞向闪耀的光芒时,突然意识到这实际上是巨型鮟鱇鱼的诱饵!\n\n你试图扭头就跑,但为时已晚 —— 鮟鱇鱼一口就把你吞没了\n\n世界变得一片漆黑..."; 153 | } 154 | onContinue(): void 155 | { 156 | playerData.killPlayer(); 157 | } 158 | } 159 | 160 | export class DiveAttemptScreen extends EventScreen 161 | { 162 | onEnter(): void 163 | { 164 | feed.clear(); 165 | feed.publish("你尝试潜入水下", true); 166 | } 167 | 168 | getDisplayText(): string 169 | { 170 | return "你尝试潜入水下,但浅层强大的水流阻止你继续潜入几百米下的深水区"; 171 | } 172 | 173 | onContinue(): void 174 | { 175 | gameManager.popScreen(); 176 | } 177 | } 178 | 179 | export class TeleportScreen extends EventScreen 180 | { 181 | _text: string; 182 | _destination: string; 183 | 184 | constructor(teleportText: string, destination: string) 185 | { 186 | super() 187 | this._text = teleportText; 188 | this._destination = destination; 189 | } 190 | 191 | onExit(): void 192 | { 193 | feed.clear(); 194 | feed.publish("你已被传送至新地点", true); 195 | } 196 | 197 | getDisplayText(): string 198 | { 199 | return this._text; 200 | } 201 | 202 | onContinue(): void 203 | { 204 | gameManager.popScreen(); 205 | messenger.sendMessage(new Message("teleport to", this._destination)); 206 | } 207 | } 208 | 209 | export class MoveToScreen extends EventScreen 210 | { 211 | _text: string; 212 | _destination: string; 213 | 214 | constructor(moveText: string, destination: string) 215 | { 216 | super() 217 | this._text = moveText; 218 | this._destination = destination; 219 | } 220 | 221 | getDisplayText(): string 222 | { 223 | return this._text; 224 | } 225 | 226 | onContinue(): void 227 | { 228 | gameManager.popScreen(); 229 | messenger.sendMessage(new Message("move to", this._destination)); 230 | } 231 | } 232 | 233 | export class SectorArrivalScreen extends EventScreen 234 | { 235 | _arrivalText: string; 236 | _sectorName: string; 237 | 238 | constructor(arrivalText: string, sectorName: string) 239 | { 240 | super() 241 | this._arrivalText = arrivalText; 242 | this._sectorName = sectorName; 243 | } 244 | 245 | onEnter(): void 246 | { 247 | feed.clear(); 248 | feed.publish("你已抵达" + this._sectorName); 249 | } 250 | 251 | getDisplayText(): string 252 | { 253 | return this._arrivalText; 254 | } 255 | 256 | onContinue(): void 257 | { 258 | gameManager.popScreen(); 259 | } 260 | } 261 | 262 | export class QuantumArrivalScreen extends EventScreen 263 | { 264 | _screenIndex: number = 0; 265 | _photoTaken: boolean = false; 266 | 267 | initButtons(): void 268 | { 269 | this.addDatabaseButton(); 270 | this.addContinueButton(); 271 | } 272 | 273 | getDisplayText(): string 274 | { 275 | if (this._screenIndex == 0) 276 | { 277 | if (!this._photoTaken) 278 | { 279 | return "你接近了量子卫星, 一团奇怪的迷雾开始遮挡你的视线..."; 280 | } 281 | else 282 | { 283 | return "在它被逐渐逼近的迷雾完全遮挡之前,你使用小侦察兵拍摄了卫星的照片"; 284 | } 285 | } 286 | else if (this._screenIndex == 1) 287 | { 288 | if (!this._photoTaken) 289 | { 290 | return "迷雾完全吞没了你的飞船,然后突然消散,就像它出现时那样\n\n你环顾四周,量子卫星已经神秘消失了"; 291 | } 292 | else 293 | { 294 | return "你一头扎进雾中,确保自己盯着刚刚拍到的照片\n\n突然,巨大的形状从雾中浮现...你抵达了量子卫星的表面!"; 295 | } 296 | } 297 | return ""; 298 | } 299 | 300 | onInvokeClue(clue: Clue): void 301 | { 302 | if (clue.id === "QM_1") 303 | { 304 | gameManager.popScreen(); 305 | this._photoTaken = true; 306 | this._databaseButton.enabled = false; 307 | } 308 | else 309 | { 310 | feed.publish("那个现在还不能帮助到你", true); 311 | } 312 | } 313 | 314 | onContinue(): void 315 | { 316 | this._screenIndex++; 317 | 318 | this._databaseButton.enabled = false; 319 | 320 | if (this._screenIndex > 1) 321 | { 322 | if (!this._photoTaken) 323 | { 324 | gameManager.popScreen(); 325 | messenger.sendMessage("quantum moon vanished"); 326 | } 327 | else 328 | { 329 | gameManager.popScreen(); 330 | } 331 | } 332 | } 333 | } 334 | 335 | export class QuantumEntanglementScreen extends EventScreen 336 | { 337 | _displayText: string; 338 | 339 | constructor() 340 | { 341 | super() 342 | if (locator.player.currentSector.getName() === "Quantum Moon") 343 | { 344 | this._displayText = "你关闭了手电筒,世界变得一片漆黑\n\n当你重新打开手电筒时,四周的环境发生了变化..."; 345 | } 346 | else 347 | { 348 | this._displayText = "你爬上了量子碎片的顶部并关闭了手电筒。环境实在是太黑了,伸手不见五指\n\n当你重新打开手电筒时,你仍然站在量子碎片的顶部,但四周的环境发生了变化..."; 349 | } 350 | } 351 | 352 | onEnter(): void 353 | { 354 | feed.clear(); 355 | feed.publish("你与量子物体一块发生纠缠现象了"); 356 | } 357 | 358 | getDisplayText(): string 359 | { 360 | return this._displayText; 361 | } 362 | 363 | onContinue(): void 364 | { 365 | gameManager.popScreen(); 366 | } 367 | } 368 | 369 | export class FollowTheVineScreen extends EventScreen 370 | { 371 | _screenIndex: number = 0; 372 | _silentRunning: boolean = false; 373 | 374 | initButtons(): void 375 | { 376 | this.addDatabaseButton(); 377 | this.addContinueButton(); 378 | } 379 | 380 | getDisplayText(): string 381 | { 382 | if (this._screenIndex == 0) 383 | { 384 | return "你向其中一朵怪异的蓝色花朵发射小侦察兵,它很快就被吞噬了。你跟随小侦察兵的追踪信号,顺着错综复杂的藤蔓进入黑荆星的中心深处\n\n你如此专注于跟随小侦察兵的信号,以至于你没有注意到发光的诱饵,但已为时已晚。你直接飞进了鮟鱇鱼的巢穴里!"; 385 | } 386 | else if (this._screenIndex == 1) 387 | { 388 | if (!this._silentRunning) 389 | { 390 | return "你反转了飞船的推进器,但为时已晚。鮟鱇鱼飞速猛扑过来,吞噬了整个飞船"; 391 | } 392 | else 393 | { 394 | return "你突然想起了那个古老儿童游戏的规则,你关掉了引擎,悄悄地漂进了巢穴\n\n似乎没有鮟鱇鱼注意到你,你安全地到达了另一边。你继续跟随小侦察兵的信号前进,没过多久,你到达了一个纠缠于黑荆根部的古老遗迹"; 395 | } 396 | } 397 | return ""; 398 | } 399 | 400 | onButtonUp(button: Button): void 401 | { 402 | if (button == this._nextButton) 403 | { 404 | this.onContinue(); 405 | } 406 | else if (button == this._databaseButton) 407 | { 408 | gameManager.pushScreen(gameManager.databaseScreen); 409 | gameManager.databaseScreen.setObserver(this); 410 | } 411 | } 412 | 413 | onInvokeClue(clue: Clue): void 414 | { 415 | if (clue.id === "D_2") 416 | { 417 | gameManager.popScreen(); 418 | this._silentRunning = true; 419 | this._screenIndex++; 420 | this._databaseButton.enabled = false; 421 | } 422 | else 423 | { 424 | feed.publish("那个现在还不能帮助到你", true); 425 | } 426 | } 427 | 428 | onContinue(): void 429 | { 430 | this._screenIndex++; 431 | this._databaseButton.enabled = false; 432 | 433 | if (this._screenIndex > 1) 434 | { 435 | if (!this._silentRunning) 436 | { 437 | gameManager.popScreen(); 438 | playerData.killPlayer(); 439 | } 440 | else 441 | { 442 | gameManager.popScreen(); 443 | messenger.sendMessage(new Message("move to", "古飞船")); 444 | } 445 | } 446 | } 447 | } 448 | 449 | export class AncientVesselScreen extends EventScreen 450 | { 451 | _warpButton: Button; 452 | _displayText: string; 453 | 454 | constructor() 455 | { 456 | super(); 457 | this._displayText = "你探索了这片废墟,最终找到了这里。尽管飞船的生命维持系统已经失效,但一些计算机终端仍在使用某种辅助电源运行。你找到了挪麦人从宇宙之眼接收的原始信号的记录。根据它们的分析,无论信号来自什么物体,都比宇宙本身更为古老!\n\n你又四处探查了一番,发现这艘船的跃迁装置在几百年前就完成了充能"; 458 | } 459 | 460 | initButtons(): void 461 | { 462 | this.addButtonToToolbar(this._warpButton = new Button("使用跃迁装置", 0, 0, 150, 50)); 463 | super.initButtons(); 464 | } 465 | 466 | getDisplayText(): string 467 | { 468 | return this._displayText; 469 | } 470 | 471 | onButtonUp(button: Button): void 472 | { 473 | if (button == this._warpButton) 474 | { 475 | if (playerData.knowsSignalCoordinates()) 476 | { 477 | if (!timeLoop.getEnabled()) 478 | { 479 | gameManager.popScreen(); 480 | messenger.sendMessage(new Message("teleport to", "Ancient Vessel 2")); 481 | feed.clear(); 482 | feed.publish("古飞船跃迁到了宇宙之眼所在的坐标"); 483 | } 484 | else 485 | { 486 | this._displayText = "你输入了宇宙之眼的坐标并尝试使用跃迁装置,但由于存在“巨大的时间扭曲”,它拒绝启动'."; 487 | } 488 | } 489 | else 490 | { 491 | this._displayText = "你试图使用跃迁装置,但没有目的地坐标,装置显然无法启动"; 492 | } 493 | } 494 | else if (button == this._nextButton) 495 | { 496 | this.onContinue(); 497 | } 498 | } 499 | 500 | onContinue(): void 501 | { 502 | gameManager.popScreen(); 503 | } 504 | } 505 | 506 | export class TimeLoopCentralScreen extends EventScreen 507 | { 508 | _screenIndex: number = 0; 509 | _yes: Button; 510 | _no: Button; 511 | 512 | initButtons(): void 513 | { 514 | this.addContinueButton(); 515 | } 516 | 517 | getDisplayText(): string 518 | { 519 | if (this._screenIndex == 0) 520 | { 521 | return "你正位于灰烬双星中心的一个巨大球形舱内。你在外面看到的两根巨型天线延伸到了地表以下,并汇聚到了密室中心一个精心制作的线圈状的挪麦科技装置中。这一定就是时间循环的源头!\n\n你发现了一个信号发射器,它能自动通知深巨星轨道上的挪麦空间站在每次循环开始时发射一个探测器\n\n时间循环装置需要超新星的能量来改变时间的流逝。几千年前,挪麦人曾试人工激发超新星,但没有成功"; 522 | } 523 | 524 | return "你最终找到了通往密室中心的路,并找到了看起来像是时间循环机器的界面\n\n你想关闭时间循环吗?"; 525 | } 526 | 527 | onButtonUp(button: Button): void 528 | { 529 | super.onButtonUp(button); 530 | 531 | if (button == this._yes) 532 | { 533 | messenger.sendMessage("关闭时间循环"); 534 | gameManager.popScreen(); 535 | } 536 | else if (button == this._no) 537 | { 538 | gameManager.popScreen(); 539 | } 540 | } 541 | 542 | onContinue(): void 543 | { 544 | this._screenIndex++; 545 | this.removeButtonFromToolbar(this._nextButton); 546 | this.addButtonToToolbar(this._yes = new Button("是")); 547 | this.addButtonToToolbar(this._no = new Button("否")); 548 | } 549 | } 550 | 551 | export class EyeOfTheUniverseScreen extends EventScreen 552 | { 553 | _screenIndex: number = 0; 554 | 555 | getDisplayText(): string 556 | { 557 | if (this._screenIndex == 0) 558 | { 559 | return "当你靠近围绕着宇宙之眼的奇特能量云时,你看到最后几颗恒星在远处爆发。宇宙变成了一片漆黑的虚无\n\n当你到达云层中心时,云层逐渐消散,露出一个漂浮在空中的奇特球形物体。在它闪闪发光的表面上,你看到了数十亿个光点。当你靠近时,你发现这些光点像是恒星和星系团。每当你把目光从球体上移开,再移回来时,恒星和星系的结构就会发生变化\n\n你启动了喷气背包,进入了球体..."; 560 | } 561 | 562 | return "有那么一瞬间,你发现自己漂浮在星辰和银河的海洋中。突然,所有的恒星都坍缩成你正前方的一个光点。前几秒什么都没有发生,然后光点突然向外爆发出惊人的能量。冲击波将你猛烈撞向了空中\n\n你的生命维持系统正在崩溃,而你只能眼睁睁地看着能量与物质四向喷向太空\n\n过了一会,你的视野正在逐渐变黑"; 563 | } 564 | 565 | onContinue(): void 566 | { 567 | this._screenIndex++; 568 | 569 | if (this._screenIndex == 2) 570 | { 571 | gameManager._solarSystem.player.currentSector = null; 572 | gameManager.pushScreen(new EndScreen()); 573 | } 574 | } 575 | } 576 | 577 | export class BrambleOutskirtsScreen extends EventScreen 578 | { 579 | _screenIndex: number = 0; 580 | _yes: Button; 581 | _no: Button; 582 | 583 | initButtons(): void 584 | { 585 | this.addDatabaseButton(); 586 | this.addButtonToToolbar(this._yes = new Button("是")); 587 | this.addButtonToToolbar(this._no = new Button("否")); 588 | } 589 | 590 | getDisplayText(): string 591 | { 592 | if (this._screenIndex == 0) 593 | { 594 | return "你正在探索黑荆星的外围,藤蔓的顶端会开出巨大的异形白花(以及几朵蓝花)\n\n你注意到靠近每朵花中心的地方都有一个小开口...你想靠近仔细看看吗?"; 595 | } 596 | 597 | return "当你靠近时,花朵打开了,一股奇怪的力量开始把你推进去。你拼命想逃离,但没有用\n\n你被一朵巨大的花朵毫不留情地吞噬了。世界一片漆黑,你能听到自己被消化的声音..."; 598 | } 599 | 600 | onButtonUp(button: Button): void 601 | { 602 | super.onButtonUp(button); 603 | 604 | if (button == this._yes) 605 | { 606 | this._screenIndex++; 607 | this.removeButtonFromToolbar(this._yes); 608 | this.removeButtonFromToolbar(this._no); 609 | this.removeButtonFromToolbar(this._databaseButton); 610 | this.addContinueButton(); 611 | } 612 | else if (button == this._no) 613 | { 614 | gameManager.popScreen(); 615 | } 616 | } 617 | 618 | onInvokeClue(clue: Clue): void 619 | { 620 | if (clue.id === "D_3") 621 | { 622 | gameManager.popScreen(); 623 | messenger.sendMessage("follow the vine"); 624 | } 625 | else 626 | { 627 | feed.publish("那个现在还不能帮助到你", true); 628 | } 629 | } 630 | 631 | onContinue(): void 632 | { 633 | playerData.killPlayer(); 634 | } 635 | } -------------------------------------------------------------------------------- /src/ExploreData.ts: -------------------------------------------------------------------------------- 1 | import { MoveToScreen, TeleportScreen } from "./EventScreen"; 2 | import { OWNode } from "./Node"; 3 | import { timeLoop, gameManager, messenger, playerData, feed } from "./app"; 4 | import { JSONArray, JSONObject } from "./compat"; 5 | 6 | export class ExploreData 7 | { 8 | _node: OWNode; 9 | _exploreString: string; 10 | _exploreArray: JSONArray; 11 | 12 | _nodeObj: JSONObject; 13 | _activeExploreObj: JSONObject; 14 | 15 | _dirty: boolean; 16 | 17 | constructor(node: OWNode, nodeObj: JSONObject) 18 | { 19 | this._node = node; 20 | this._nodeObj = nodeObj; 21 | } 22 | 23 | parseJSON(): void 24 | { 25 | // parse as string 26 | this._exploreString = this._nodeObj.getString("explore", "Nothing to see here!"); 27 | this._exploreArray = new JSONArray(); 28 | 29 | // parse as explore object 30 | if (this._exploreString.charAt(0) == '{') 31 | { 32 | this._activeExploreObj = this._nodeObj.getJSONObject("explore"); 33 | } 34 | // parse as array of explore objects 35 | else if (this._exploreString.charAt(0) == '[') 36 | { 37 | this._exploreArray = this._nodeObj.getJSONArray("explore"); 38 | this._activeExploreObj = this._exploreArray.getJSONObject(0); 39 | } 40 | 41 | this._dirty = true; 42 | } 43 | 44 | updateActiveExploreData(): void 45 | { 46 | // check wait times 47 | for (let i: number = 0; i < this._exploreArray.size(); i++) 48 | { 49 | const exploreObj: JSONObject = this._exploreArray.getJSONObject(i); 50 | 51 | const turnCycle: number = exploreObj.getInt("turn cycle", 1); 52 | const turn: number = timeLoop.getActionPoints() % turnCycle; 53 | 54 | if (exploreObj.getInt("on turn", -1) == turn && exploreObj != this._activeExploreObj) 55 | { 56 | this._activeExploreObj = exploreObj; 57 | this._dirty = true; 58 | } 59 | } 60 | } 61 | 62 | canClueBeInvoked(clueID: string): boolean 63 | { 64 | if (clueID === "QM_2" && this._node.allowQuantumEntanglement()) 65 | { 66 | return true; 67 | } 68 | 69 | for (let i: number = 0; i < this._exploreArray.size(); i++) 70 | { 71 | const exploreObj: JSONObject = this._exploreArray.getJSONObject(i); 72 | 73 | if (exploreObj.getString("require clue", "") === clueID && exploreObj != this._activeExploreObj) 74 | { 75 | return true; 76 | } 77 | 78 | // NO LONGER IN USE 79 | if (exploreObj.hasKey("clue event") && exploreObj.getJSONObject("clue event").getString("clue id") === clueID) 80 | { 81 | return true; 82 | } 83 | } 84 | return false; 85 | } 86 | 87 | invokeClue(clueID: string): void 88 | { 89 | if (clueID === "QM_2" && this._node.allowQuantumEntanglement()) 90 | { 91 | gameManager.popScreen(); 92 | messenger.sendMessage("quantum entanglement"); 93 | } 94 | 95 | for (let i: number = 0; i < this._exploreArray.size(); i++) 96 | { 97 | const exploreObj: JSONObject = this._exploreArray.getJSONObject(i); 98 | 99 | // unlock explore screens 100 | if (exploreObj.getString("require clue", "") === clueID && exploreObj != this._activeExploreObj) 101 | { 102 | this._activeExploreObj = exploreObj; 103 | this._dirty = true; 104 | } 105 | 106 | // NO LONGER IN USE 107 | // fire clue events 108 | if (exploreObj.hasKey("clue event")) 109 | { 110 | const eventClueID: string = exploreObj.getJSONObject("clue event").getString("clue id"); 111 | 112 | if (eventClueID === clueID) 113 | { 114 | const eventID: string = exploreObj.getJSONObject("clue event").getString("event id"); 115 | messenger.sendMessage(eventID); 116 | } 117 | } 118 | } 119 | } 120 | 121 | explore(): void 122 | { 123 | this.updateActiveExploreData(); // sets dirty flag if explore data has changed 124 | 125 | if (this._dirty && this._activeExploreObj != null) 126 | { 127 | this.fireEvents(this._activeExploreObj); 128 | this.discoverClues(this._activeExploreObj); 129 | this.revealHiddenPaths(this._activeExploreObj); 130 | this._dirty = false; 131 | } 132 | } 133 | 134 | fireEvents(exploreObj: JSONObject): void 135 | { 136 | if (exploreObj.hasKey("fire event")) 137 | { 138 | messenger.sendMessage(exploreObj.getString("fire event")); 139 | } 140 | if (exploreObj.hasKey("move to")) 141 | { 142 | gameManager.swapScreen(new MoveToScreen(exploreObj.getString("text"), exploreObj.getString("move to"))); 143 | } 144 | if (exploreObj.hasKey("teleport to")) 145 | { 146 | gameManager.swapScreen(new TeleportScreen(exploreObj.getString("text"), exploreObj.getString("teleport to"))); 147 | } 148 | } 149 | 150 | discoverClues(exploreObj: JSONObject): void 151 | { 152 | if (exploreObj.hasKey("discover clue")) 153 | { 154 | playerData.discoverClue(exploreObj.getString("discover clue")); 155 | } 156 | } 157 | 158 | revealHiddenPaths(exploreObj: JSONObject): void 159 | { 160 | // reveal hidden paths 161 | if (exploreObj.hasKey("reveal paths")) 162 | { 163 | const pathArray: JSONArray = exploreObj.getJSONArray("reveal paths"); 164 | 165 | for (let i: number = 0; i < pathArray.size(); i++) 166 | { 167 | this._node.getConnection(pathArray.getString(i)).revealHidden(); 168 | } 169 | 170 | feed.publish("你发现了一条隐藏通道!", true); 171 | } 172 | } 173 | 174 | getExploreText(): string 175 | { 176 | if (this._activeExploreObj != null) 177 | { 178 | return this._activeExploreObj.getString("text", "no explore text"); 179 | } 180 | 181 | return this._exploreString; 182 | } 183 | } -------------------------------------------------------------------------------- /src/ExploreScreen.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "./Button"; 2 | import { DatabaseObserver } from "./DatabaseScreen"; 3 | import { ExploreData } from "./ExploreData"; 4 | import { OWNode } from "./Node"; 5 | import { Clue } from "./PlayerData"; 6 | import { OWScreen } from "./Screen"; 7 | import { feed, timeLoop, gameManager, locator, mediumFontData } from "./app"; 8 | 9 | export class ExploreScreen extends OWScreen implements DatabaseObserver 10 | { 11 | static BOX_WIDTH: number = 700; 12 | static BOX_HEIGHT: number = 400; 13 | _exploreData: ExploreData; 14 | 15 | _databaseButton: Button; 16 | _backButton: Button; 17 | _waitButton: Button; 18 | 19 | constructor(location: OWNode) 20 | { 21 | super(); 22 | this._exploreData = location.getExploreData(); 23 | this.overlay = true; // continue to render BG 24 | 25 | this.addButtonToToolbar(this._databaseButton = new Button("查看数据库", 0, 0, 150, 50)); 26 | this.addButtonToToolbar(this._waitButton = new Button("等待 [ 1分钟 ]", 0, 0, 150, 50)); 27 | this.addButtonToToolbar(this._backButton = new Button("继续", 0, 0, 150, 50)); 28 | 29 | this._exploreData.parseJSON(); 30 | } 31 | 32 | update(): void{} 33 | 34 | renderBackground(): void {} 35 | 36 | render(): void 37 | { 38 | push(); 39 | translate(width / 2 - ExploreScreen.BOX_WIDTH / 2, height / 2 - ExploreScreen.BOX_HEIGHT / 2); 40 | 41 | stroke(0, 0, 100); 42 | fill(0, 0, 0); 43 | rectMode(CORNER); 44 | rect(0, 0, ExploreScreen.BOX_WIDTH, ExploreScreen.BOX_HEIGHT); 45 | 46 | fill(0, 0, 100); 47 | 48 | textFont(mediumFontData); 49 | textSize(18); 50 | textAlign(LEFT, TOP); 51 | 52 | const exploreText = this._exploreData.getExploreText(); 53 | const wrappedLines = this.wrapText(exploreText, ExploreScreen.BOX_WIDTH - 20); // 自动换行处理 54 | 55 | let y = 10; // 初始 y 坐标 56 | const lineHeight = 24; // 固定行高,确保行间距足够 57 | 58 | for (const line of wrappedLines) { 59 | if (line === "") { 60 | // 如果是空行,直接增加 y 坐标,保留空行 61 | y += lineHeight; 62 | } else { 63 | text(line, 10, y); // 绘制每一行文本 64 | y += lineHeight; // 增加 y 坐标,确保下一行不会重叠 65 | } 66 | } 67 | 68 | pop(); 69 | 70 | feed.render(); 71 | timeLoop.renderTimer(); 72 | } 73 | 74 | /** 75 | * 自动换行函数:根据最大宽度将文本分割成多行,并保留空行 76 | * @param text 原始文本 77 | * @param maxWidth 最大宽度 78 | * @returns 分割后的多行文本数组 79 | */ 80 | wrapText(text: string, maxWidth: number): string[] { 81 | const lines: string[] = []; 82 | const paragraphs = text.split('\n'); // 按换行符分割段落 83 | 84 | for (const paragraph of paragraphs) { 85 | if (paragraph.trim() === "") { 86 | // 如果段落是空的,保留空行 87 | lines.push(""); 88 | continue; 89 | } 90 | 91 | let currentLine = ""; 92 | 93 | for (const char of paragraph) { 94 | const testLine = currentLine + char; 95 | if (textWidth(testLine) > maxWidth) { 96 | lines.push(currentLine); // 当前行已满,保存 97 | currentLine = char; // 开始新的一行 98 | } else { 99 | currentLine = testLine; // 继续添加字符 100 | } 101 | } 102 | 103 | if (currentLine) { 104 | lines.push(currentLine); // 保存最后一行 105 | } 106 | } 107 | 108 | return lines; 109 | } 110 | 111 | onEnter(): void {} 112 | 113 | onExit(): void {} 114 | 115 | onInvokeClue(clue: Clue): void 116 | { 117 | // try to invoke it on the node first 118 | if (this._exploreData.canClueBeInvoked(clue.id)) 119 | { 120 | // force-quit the database screen 121 | gameManager.popScreen(); 122 | this._exploreData.invokeClue(clue.id); 123 | this._exploreData.explore(); 124 | } 125 | // next try the whole sector 126 | else if (locator.player.currentSector != null && locator.player.currentSector.canClueBeInvoked(clue)) 127 | { 128 | gameManager.popScreen(); 129 | locator.player.currentSector.invokeClue(clue); 130 | } 131 | else 132 | { 133 | feed.publish("那个现在还不能帮助到你", true); 134 | } 135 | } 136 | 137 | onButtonUp(button: Button): void 138 | { 139 | if (button == this._databaseButton) 140 | { 141 | gameManager.pushScreen(gameManager.databaseScreen); 142 | gameManager.databaseScreen.setObserver(this); 143 | } 144 | else if (button == this._backButton) 145 | { 146 | gameManager.popScreen(); 147 | } 148 | else if (button == this._waitButton) 149 | { 150 | timeLoop.waitFor(1); 151 | this._exploreData.explore(); 152 | } 153 | } 154 | 155 | onButtonEnterHover(button: Button): void{} 156 | onButtonExitHover(button: Button): void{} 157 | } -------------------------------------------------------------------------------- /src/GameManager.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseScreen } from "./DatabaseScreen"; 2 | import { SectorName } from "./Enums"; 3 | import { DeathByAnglerfishScreen, DiveAttemptScreen, FollowTheVineScreen, AncientVesselScreen, TimeLoopCentralScreen, EyeOfTheUniverseScreen, BrambleOutskirtsScreen } from "./EventScreen"; 4 | import { GlobalObserver, Message } from "./GlobalMessenger"; 5 | import { Locator } from "./Locator"; 6 | import { ScreenManager } from "./ScreenManager"; 7 | import { Sector } from "./Sector"; 8 | import { SectorScreen } from "./SectorScreen"; 9 | import { SectorTelescopeScreen } from "./SectorTelescopeScreen"; 10 | import { SolarSystem } from "./SolarSystem"; 11 | import { SolarSystemMapScreen, SolarSystemTelescopeScreen } from "./SolarSystemScreen"; 12 | import { FlashbackScreen, GameOverScreen } from "./SupernovaScreen"; 13 | import { Telescope } from "./Telescope"; 14 | import { TitleScreen } from "./TitleScreen"; 15 | import { messenger, feed, timeLoop, playerData, locator, resetLocator } from "./app"; 16 | 17 | export class GameManager extends ScreenManager implements GlobalObserver 18 | { 19 | // game screens 20 | titleScreen: TitleScreen; 21 | databaseScreen: DatabaseScreen; 22 | solarSystemMapScreen: SolarSystemMapScreen; 23 | 24 | // game objects 25 | _solarSystem: SolarSystem; 26 | _telescope: Telescope; 27 | 28 | _flashbackTriggered: boolean = false; 29 | 30 | newGame(): void 31 | { 32 | this.setupSolarSystem(); 33 | 34 | this.titleScreen = new TitleScreen(); 35 | this.databaseScreen = new DatabaseScreen(); 36 | 37 | this.pushScreen(this.titleScreen); 38 | } 39 | 40 | resetTimeLoop(): void 41 | { 42 | this._flashbackTriggered = false; 43 | this._screenStack.length = 0; 44 | this.setupSolarSystem(); 45 | this.loadSector(SectorName.TIMBER_HEARTH); 46 | } 47 | 48 | setupSolarSystem(): void 49 | { 50 | messenger.removeAllObservers(); 51 | messenger.addObserver(this); 52 | 53 | feed.init(); 54 | timeLoop.init(); 55 | playerData.init(); 56 | 57 | this._telescope = new Telescope(); 58 | 59 | this._solarSystem = new SolarSystem(); 60 | this._solarSystem.timberHearth.addActor(this._solarSystem.player, "村庄"); 61 | 62 | this.solarSystemMapScreen = new SolarSystemMapScreen(this._solarSystem); 63 | 64 | if (playerData.knowsLaunchCodes()) 65 | { 66 | messenger.sendMessage("spawn ship"); 67 | } 68 | 69 | resetLocator(); 70 | } 71 | 72 | // runs after everything else updates 73 | lateUpdate(): void 74 | { 75 | // check if the sun explodes (this check has to be last to override all other screens) 76 | timeLoop.lateUpdate(); 77 | 78 | // check if the player died 79 | if (playerData.isPlayerDead() && !this._flashbackTriggered) 80 | { 81 | this._flashbackTriggered = true; 82 | 83 | if (timeLoop.getEnabled()) 84 | { 85 | this.swapScreen(new FlashbackScreen()); 86 | } 87 | else 88 | { 89 | this.swapScreen(new GameOverScreen()); 90 | } 91 | } 92 | } 93 | 94 | onReceiveGlobalMessage(message: Message): void 95 | { 96 | // TRIGGERED FROM SECTORSCREEN (NOT EXPLORE SCREEN) 97 | if (message.id === "death by anglerfish") 98 | { 99 | this.pushScreen(new DeathByAnglerfishScreen()); 100 | } 101 | else if (message.id === "dive attempt") 102 | { 103 | this.pushScreen(new DiveAttemptScreen()); 104 | } 105 | // TRIGGERED FROM EVENT SCREEN 106 | else if (message.id === "follow the vine") 107 | { 108 | this.swapScreen(new FollowTheVineScreen()); 109 | } 110 | // TRIGGERED FROM EXPLORE DATA 111 | else if (message.id === "explore ancient vessel") 112 | { 113 | this.swapScreen(new AncientVesselScreen()); 114 | } 115 | else if (message.id === "time loop central") 116 | { 117 | this.swapScreen(new TimeLoopCentralScreen()); 118 | } 119 | else if (message.id === "older than the universe") 120 | { 121 | this.swapScreen(new EyeOfTheUniverseScreen()); 122 | } 123 | else if (message.id === "explore bramble outskirts") 124 | { 125 | this.swapScreen(new BrambleOutskirtsScreen()); 126 | } 127 | } 128 | 129 | loadTelescopeView(): void 130 | { 131 | this.pushScreen(new SolarSystemTelescopeScreen(this._solarSystem, this._telescope)); 132 | 133 | // if (_solarSystem.player.currentSector != null) 134 | // { 135 | // loadSectorTelescopeView(_solarSystem.player.currentSector); 136 | // } 137 | } 138 | 139 | loadSectorTelescopeView(sector: Sector): void 140 | { 141 | this.pushScreen(new SectorTelescopeScreen(sector, this._telescope)); 142 | } 143 | 144 | loadSolarSystemMap(): void 145 | { 146 | this.swapScreen(this.solarSystemMapScreen); 147 | } 148 | 149 | loadSector(sectorName: SectorName): void 150 | loadSector(sector: Sector): void 151 | loadSector(sectorOrName: Sector | SectorName): void 152 | { 153 | const sector = sectorOrName instanceof Sector ? sectorOrName : this._solarSystem.getSectorByName(sectorOrName) 154 | this.swapScreen(new SectorScreen(sector, this._solarSystem.player, this._solarSystem.ship)); 155 | } 156 | } -------------------------------------------------------------------------------- /src/GameSave.ts: -------------------------------------------------------------------------------- 1 | import { PlayerData } from "./PlayerData"; 2 | 3 | export class GameSave { 4 | static PLAYER_DATA = "playerData"; 5 | 6 | static saveData(data: PlayerData) { 7 | localStorage.setItem(this.PLAYER_DATA, JSON.stringify(data)); 8 | } 9 | 10 | static loadData(): PlayerData { 11 | const playerData = new PlayerData(); 12 | if (!this.hasData()) { 13 | return playerData; 14 | } 15 | 16 | const playerDataSave = JSON.parse(localStorage.getItem(this.PLAYER_DATA)); 17 | Object.assign(playerData, playerDataSave); 18 | return playerData; 19 | } 20 | 21 | static clearData() { 22 | localStorage.removeItem(this.PLAYER_DATA); 23 | } 24 | 25 | static hasData(): boolean { 26 | return Boolean(localStorage.getItem(this.PLAYER_DATA)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GlobalMessenger.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface GlobalObserver 3 | { 4 | onReceiveGlobalMessage(message: Message): void; 5 | } 6 | 7 | export class GlobalMessenger 8 | { 9 | _observers: GlobalObserver[]; 10 | 11 | constructor() 12 | { 13 | this._observers = new Array(); 14 | } 15 | 16 | addObserver(observer: GlobalObserver): void 17 | { 18 | if (!this._observers.includes(observer)) 19 | { 20 | this._observers.push(observer); 21 | } 22 | //println("Observer Count:", this._observers.length); 23 | } 24 | 25 | removeObserver(observer: GlobalObserver): void 26 | { 27 | this._observers.splice(this._observers.indexOf(observer), 1); 28 | } 29 | 30 | removeAllObservers(): void 31 | { 32 | this._observers.length = 0; 33 | } 34 | 35 | sendMessage(messageID: string): void 36 | sendMessage(message: Message): void 37 | sendMessage(messageOrID: string | Message): void 38 | { 39 | const message = messageOrID instanceof Message ? messageOrID : new Message(messageOrID); 40 | for (let i: number = 0; i < this._observers.length; i++) 41 | { 42 | this._observers[i].onReceiveGlobalMessage(message); 43 | } 44 | } 45 | } 46 | 47 | export class Message 48 | { 49 | id: string; 50 | text: string; 51 | 52 | constructor(messageID: string, t?: string) 53 | { 54 | this.id = messageID; 55 | if (t !== undefined) this.text = t; 56 | } 57 | } -------------------------------------------------------------------------------- /src/Locator.ts: -------------------------------------------------------------------------------- 1 | import { Actor } from "./Entity"; 2 | import { QuantumMoon } from "./SectorLibrary"; 3 | import { gameManager } from "./app"; 4 | 5 | export class Locator 6 | { 7 | player: Actor; 8 | ship: Actor; 9 | 10 | _quantumSector: QuantumMoon; 11 | 12 | constructor() 13 | { 14 | this.player = gameManager._solarSystem.player; 15 | this.ship = gameManager._solarSystem.ship; 16 | this._quantumSector = gameManager._solarSystem.quantumMoon as QuantumMoon; 17 | } 18 | 19 | getQuantumMoonLocation(): number 20 | { 21 | return this._quantumSector.getQuantumLocation(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Node.ts: -------------------------------------------------------------------------------- 1 | import { ButtonObserver, Button } from "./Button"; 2 | import { Entity } from "./Entity"; 3 | import { ExploreData } from "./ExploreData"; 4 | import { GlobalObserver, Message } from "./GlobalMessenger"; 5 | import { NodeConnection } from "./NodeConnection"; 6 | import { Signal } from "./Telescope"; 7 | import { Vector2 } from "./Vector2"; 8 | import { messenger, EDIT_MODE, playerData, TEXT_SIZE } from "./app"; 9 | import { JSONObject, println } from "./compat"; 10 | 11 | export interface NodeButtonObserver 12 | { 13 | onNodeSelected(node: OWNode): void; 14 | onNodeGainFocus(node: OWNode): void; 15 | onNodeLoseFocus(node: OWNode): void; 16 | } 17 | 18 | export interface NodeObserver 19 | { 20 | onNodeVisited(node: OWNode): void; 21 | } 22 | 23 | export class OWNode extends Entity implements ButtonObserver, GlobalObserver 24 | { 25 | /** NODE DATA **/ 26 | _id: string = ""; 27 | _name: string = ""; 28 | 29 | entryPoint: boolean = false; 30 | shipAccess: boolean = false; 31 | allowTelescope: boolean = true; 32 | gravity: boolean = true; 33 | 34 | _signal: Signal | null = null; 35 | 36 | /** EXPLORE STATE **/ 37 | _visible: boolean = false; 38 | _visited: boolean = false; 39 | _explored: boolean = false; 40 | 41 | _inRange: boolean = false; 42 | 43 | _button: Button | null = null; 44 | _connections: Map; 45 | 46 | _observers: NodeButtonObserver[]; 47 | _observer: NodeObserver | null = null; 48 | 49 | _nodeJSONObj: JSONObject | null = null; 50 | _exploreData: ExploreData | null = null; 51 | 52 | constructor(x: number, y: number) 53 | constructor(id: string, nodeJSONObj: JSONObject) 54 | constructor(xOrID: number | string, yOrObj: number | JSONObject) 55 | { 56 | super(typeof xOrID === 'number' ? xOrID : 0, typeof yOrObj === 'number' ? yOrObj : 0) 57 | this._connections = new Map(); 58 | this._observers = new Array(); 59 | messenger.addObserver(this); 60 | if (typeof xOrID === 'string' && typeof yOrObj !== 'number') { 61 | this._id = xOrID; 62 | this._name = xOrID; 63 | 64 | this.loadJSON(yOrObj); 65 | 66 | if (this.entryPoint) 67 | { 68 | this._visible = true; 69 | } 70 | 71 | this._button = new Button(this._id, 0, 0, this.getSize() * 1.5, this.getSize() * 1.5); 72 | this._button.setObserver(this); 73 | this._button.visible = false; 74 | this.addChild(this._button); 75 | } 76 | } 77 | 78 | loadJSON(nodeJSONObj: JSONObject): void 79 | { 80 | this._nodeJSONObj = nodeJSONObj; 81 | 82 | this._name = this._nodeJSONObj.getString("name", this._id); 83 | 84 | if (nodeJSONObj.hasKey("explore")) 85 | { 86 | this._exploreData = new ExploreData(this, nodeJSONObj); 87 | } 88 | 89 | this.position.x = this._nodeJSONObj.getJSONObject("position").getFloat("x"); 90 | this.position.y = this._nodeJSONObj.getJSONObject("position").getFloat("y"); 91 | 92 | this._visible = this._nodeJSONObj.getBoolean("start visible", this._visible); 93 | 94 | if (EDIT_MODE) 95 | { 96 | this._visible = true; 97 | } 98 | 99 | this.entryPoint = this._nodeJSONObj.getBoolean("entry point", this.entryPoint); 100 | this.shipAccess = this.entryPoint || this._nodeJSONObj.getBoolean("ship access", this.shipAccess); 101 | this.allowTelescope = this._nodeJSONObj.getBoolean("allow telescope", this.allowTelescope); 102 | this.gravity = this._nodeJSONObj.getBoolean("gravity", this.gravity); 103 | 104 | if (this._nodeJSONObj.hasKey("signal")) 105 | { 106 | this._signal = new Signal(this._nodeJSONObj.getString("signal")); 107 | } 108 | } 109 | 110 | savePosition(): void 111 | { 112 | this._nodeJSONObj.getJSONObject("position").setFloat("x", this.position.x); 113 | this._nodeJSONObj.getJSONObject("position").setFloat("y", this.position.y); 114 | println(this._id + " position saved:", this.position); 115 | } 116 | 117 | onReceiveGlobalMessage(message: Message): void 118 | { 119 | 120 | } 121 | 122 | isExplorable(): boolean {return (this._nodeJSONObj != null && this._nodeJSONObj.hasKey("explore"));} 123 | 124 | getExploreData(): ExploreData {return this._exploreData;} 125 | 126 | getProbeDescription(): string 127 | { 128 | if (this._nodeJSONObj.hasKey("probe description")) 129 | { 130 | return this._nodeJSONObj.getString("probe description"); 131 | } 132 | return this.getDescription(); 133 | } 134 | 135 | getDescription(): string {return this._nodeJSONObj.getString("description", "a vast expanse of nothing");} 136 | 137 | hasDescription(): boolean {return (this._nodeJSONObj != null && this._nodeJSONObj.hasKey("description"));} 138 | 139 | isProbeable(): boolean {return (this._nodeJSONObj != null && this._nodeJSONObj.hasKey("description"));} 140 | 141 | isConnectedTo(node: OWNode): boolean {return this._connections.has(node);} 142 | 143 | inRange(): boolean 144 | { 145 | return this._inRange; 146 | } 147 | 148 | updateInRange(isPlayerInShip: boolean, playerNode: OWNode): void 149 | { 150 | this._inRange = false; 151 | 152 | if (playerNode == this) 153 | { 154 | this._inRange = true; 155 | } 156 | 157 | if (this.entryPoint && isPlayerInShip) 158 | { 159 | this._inRange = true; 160 | } 161 | 162 | if (playerNode != null && this.isConnectedTo(playerNode)) 163 | { 164 | this._inRange = true; 165 | } 166 | } 167 | 168 | getConnection(node: OWNode): NodeConnection 169 | getConnection(nodeID: string): NodeConnection 170 | getConnection(nodeOrID: OWNode | string): NodeConnection 171 | { 172 | if (nodeOrID instanceof OWNode) { 173 | return this._connections.get(nodeOrID); 174 | } 175 | for (const node of this._connections.keys()) 176 | { 177 | if (node.getID() === nodeOrID) 178 | { 179 | return this.getConnection(node); 180 | } 181 | } 182 | return null; 183 | } 184 | 185 | allowQuantumEntanglement(): boolean // note - "quantum state" only used for Quantum Moon right now 186 | { 187 | if (this._nodeJSONObj == null) return false; 188 | return this._nodeJSONObj.getBoolean("entanglement node", false); 189 | } 190 | 191 | getSignal(): Signal {return this._signal;} 192 | 193 | getID(): string {return this._id;} 194 | 195 | getActualName(): string {return this._name;} 196 | 197 | getKnownName(): string 198 | { 199 | if (this._visited) return this.getActualName(); 200 | else return "???"; 201 | } 202 | 203 | setVisible(visible: boolean): void {this._visible = visible;} 204 | 205 | visit(): void 206 | { 207 | this._visited = true; 208 | this.setVisible(true); 209 | 210 | if (this._nodeJSONObj != null && this._nodeJSONObj.hasKey("fire event")) 211 | { 212 | messenger.sendMessage(this._nodeJSONObj.getString("fire event")); 213 | } 214 | 215 | for (const connection of this._connections.values()) 216 | { 217 | connection.reveal(); 218 | } 219 | 220 | if (this._observer != null) 221 | { 222 | this._observer.onNodeVisited(this); 223 | } 224 | } 225 | 226 | explore(): void 227 | { 228 | this._explored = true; 229 | this._exploreData.explore(); 230 | 231 | if (this._signal != null) 232 | { 233 | playerData.learnFrequency(this._signal.frequency); 234 | } 235 | } 236 | 237 | update(): void 238 | { 239 | if (!this._visible) {return;} 240 | this._button.enabled = this.inRange() || EDIT_MODE; 241 | this._button.update(); 242 | } 243 | 244 | getAlpha(): number 245 | { 246 | if (!this.inRange()) 247 | { 248 | return 35; 249 | } 250 | return 100; 251 | } 252 | 253 | getSize(): number 254 | { 255 | if (this.entryPoint) 256 | { 257 | return 50; 258 | } 259 | else if (!this.isExplorable()) 260 | { 261 | return 25; 262 | } 263 | 264 | return 35; 265 | } 266 | 267 | draw(): void 268 | { 269 | if (!this._visible) {return;} 270 | 271 | if (this._button.hoverState) 272 | { 273 | stroke(200, 100, 100, this.getAlpha()); 274 | } 275 | else 276 | { 277 | stroke(0, 0, 100, this.getAlpha()); 278 | } 279 | 280 | push(); 281 | translate(this.screenPosition.x, this.screenPosition.y); 282 | 283 | if (!this.isExplorable()) 284 | { 285 | fill(0, 0, 0); 286 | ellipse(0, 0, this.getSize(), this.getSize()); 287 | pop(); 288 | return; 289 | } 290 | 291 | if (this.entryPoint) 292 | { 293 | fill(0, 0, 0); 294 | ellipse(0, 0, this.getSize(), this.getSize()); 295 | 296 | // halfWidth: number = getSize() * 0.5; 297 | // halfHeight: number = getSize() * 0.5; 298 | // offset: number = 7; 299 | 300 | // push(); 301 | // rotate(PI * 0.25); 302 | // line(-halfWidth, 0, -halfWidth + offset, 0); 303 | // line(halfWidth - offset, 0, halfWidth, 0); 304 | // line(0, -halfHeight, 0, -halfHeight + offset); 305 | // line(0, halfHeight - offset, 0, halfHeight); 306 | // pop(); 307 | } 308 | else 309 | { 310 | fill(0, 0, 0); 311 | rect(0, 0, this.getSize(), this.getSize()); 312 | } 313 | 314 | if (!this._explored) 315 | { 316 | fill(0, 0, 100, this.getAlpha()); 317 | textAlign(CENTER, CENTER); 318 | textSize(30); 319 | text('?', 0, 0); 320 | } 321 | 322 | pop(); 323 | } 324 | 325 | drawName(): void 326 | { 327 | if (!this._visible) {return;} 328 | if (!this.isExplorable()) {return;} 329 | 330 | // draw text backdrop 331 | const textPos: Vector2 = new Vector2(this.screenPosition.x, this.screenPosition.y - this.getSize() / 2 - 20); 332 | 333 | noStroke(); 334 | fill(0, 0, 0); 335 | textSize(TEXT_SIZE); 336 | rect(textPos.x, textPos.y, textWidth(this.getKnownName()), TEXT_SIZE + 4); 337 | 338 | fill(0, 0, 100, this.getAlpha()); 339 | 340 | textAlign(CENTER, CENTER); 341 | text(this.getKnownName(), textPos.x, textPos.y); 342 | } 343 | 344 | addConnection(connection: NodeConnection): void 345 | { 346 | if ([...this._connections.values()].includes(connection)) 347 | { 348 | println("These nodes are already connected!!!"); 349 | return; 350 | } 351 | 352 | if (connection.node1 != this) 353 | { 354 | this._connections.set(connection.node1, connection); 355 | } 356 | else 357 | { 358 | this._connections.set(connection.node2, connection); 359 | } 360 | } 361 | 362 | setNodeObserver(observer: NodeObserver): void 363 | { 364 | this._observer = observer; 365 | } 366 | 367 | addObserver(observer: NodeButtonObserver): void 368 | { 369 | this._observers.push(observer); 370 | } 371 | 372 | removeAllObservers(): void 373 | { 374 | this._observers.length = 0; 375 | } 376 | 377 | onButtonUp(button: Button): void 378 | { 379 | for (let i: number = 0; i < this._observers.length; i++) 380 | { 381 | this._observers[i].onNodeSelected(this); 382 | } 383 | } 384 | 385 | onButtonEnterHover(button: Button): void 386 | { 387 | for (let i: number = 0; i < this._observers.length; i++) 388 | { 389 | this._observers[i].onNodeGainFocus(this); 390 | } 391 | } 392 | 393 | onButtonExitHover(button: Button): void 394 | { 395 | for (let i: number = 0; i < this._observers.length; i++) 396 | { 397 | this._observers[i].onNodeLoseFocus(this); 398 | } 399 | } 400 | } -------------------------------------------------------------------------------- /src/NodeAction.ts: -------------------------------------------------------------------------------- 1 | import { Actor, Probe } from "./Entity"; 2 | import { ExploreScreen } from "./ExploreScreen"; 3 | import { OWNode } from "./Node"; 4 | import { NodeConnection } from "./NodeConnection"; 5 | import { feed, timeLoop, gameManager, messenger } from "./app"; 6 | 7 | export interface NodeActionObserver 8 | { 9 | onExploreNode(node: OWNode): void; 10 | onProbeNode(node: OWNode): void; 11 | onTravelAttempt(succeeded: boolean, node: OWNode, connection: NodeConnection): void; 12 | } 13 | 14 | export abstract class NodeAction 15 | { 16 | _prompt: string = ""; 17 | _mouseButton: string = LEFT; 18 | _observer: NodeActionObserver; 19 | 20 | setObserver(observer: NodeActionObserver): void 21 | { 22 | this._observer = observer; 23 | } 24 | 25 | getMouseButton(): string 26 | { 27 | return this._mouseButton; 28 | } 29 | 30 | setMouseButton(button: string): void 31 | { 32 | this._mouseButton = button; 33 | } 34 | 35 | abstract execute(): void; 36 | 37 | getCost(): number 38 | { 39 | return 0; 40 | } 41 | 42 | getPrompt(): string 43 | { 44 | return this._prompt; 45 | } 46 | 47 | setPrompt(description: string): void 48 | { 49 | if (this._mouseButton == LEFT) 50 | { 51 | this._prompt += "左键 - " + description; 52 | } 53 | else 54 | { 55 | this._prompt += "右键 - " + description; 56 | } 57 | 58 | this._prompt += " [ " + this.getCost() + " 分钟 ]"; 59 | } 60 | } 61 | 62 | export class ProbeAction extends NodeAction 63 | { 64 | _player: Actor; 65 | _location: OWNode; 66 | 67 | constructor(button: string, player: Actor, location: OWNode, observer: NodeActionObserver) 68 | { 69 | super(); 70 | this._player = player; 71 | this._location = location; 72 | this.setMouseButton(button); 73 | this.setObserver(observer); 74 | this.setPrompt("发射侦察兵"); 75 | } 76 | 77 | execute(): void 78 | { 79 | feed.publish("你看见了" + this._location.getProbeDescription()); 80 | 81 | const probe: Actor = new Probe(); 82 | this._player.currentSector.addActor(probe); 83 | probe.setScreenPosition(this._player.screenPosition); 84 | probe.moveToNode(this._location); 85 | 86 | this._observer.onProbeNode(this._location); 87 | } 88 | } 89 | 90 | export class ExploreAction extends NodeAction 91 | { 92 | _location: OWNode; 93 | 94 | constructor(button: string, location: OWNode, observer: NodeActionObserver) 95 | { 96 | super(); 97 | this._location = location; 98 | this.setMouseButton(button); 99 | this.setObserver(observer); 100 | this.setPrompt("探索"); 101 | } 102 | 103 | getCost(): number 104 | { 105 | return 1; 106 | } 107 | 108 | execute(): void 109 | { 110 | timeLoop.spendActionPoints(this.getCost()); 111 | 112 | // prevent the action from happening if the sun's going to explode 113 | if (timeLoop.getActionPoints() == 0) 114 | { 115 | return; 116 | } 117 | 118 | feed.clear(); 119 | feed.publish("你探索了" + this._location.getActualName()); 120 | 121 | this._observer.onExploreNode(this._location); 122 | gameManager.pushScreen(new ExploreScreen(this._location)); 123 | this._location.explore(); 124 | } 125 | } 126 | 127 | export class TravelAction extends NodeAction 128 | { 129 | _player: Actor; 130 | _ship: Actor; 131 | _destination: OWNode; 132 | 133 | constructor(button: string, player: Actor, ship: Actor | null, destination: OWNode, observer: NodeActionObserver) 134 | { 135 | super(); 136 | this._ship = ship; 137 | this._player = player; 138 | this._destination = destination; 139 | this.setMouseButton(button); 140 | this.setObserver(observer); 141 | this.setPrompt(); 142 | } 143 | 144 | setPrompt(prompt?: string): void 145 | { 146 | if (prompt !== undefined) return super.setPrompt(prompt); 147 | if (this._ship != null) 148 | { 149 | if (this._ship.currentNode == null && this._destination.gravity) 150 | { 151 | this.setPrompt("降落到此处"); 152 | return; 153 | } 154 | this.setPrompt("飞到此处"); 155 | } 156 | else if (this._destination.gravity) 157 | { 158 | this.setPrompt("移动到此处"); 159 | } 160 | else 161 | { 162 | this.setPrompt("飞到此处"); 163 | } 164 | } 165 | 166 | getCost(): number 167 | { 168 | return 1; 169 | } 170 | 171 | execute(): void 172 | { 173 | feed.clear(); 174 | 175 | if (this._player.currentNode != null) 176 | { 177 | const connection: NodeConnection = this._destination.getConnection(this._player.currentNode); 178 | 179 | if (connection != null) 180 | { 181 | if (!connection.traversibleFrom(this._player.currentNode)) 182 | { 183 | connection.fireFailEvent(); 184 | feed.publish(connection.getWrongWayText(), true); 185 | return; 186 | } 187 | 188 | connection.fireTraverseEvent(); 189 | connection.traverse(); 190 | 191 | if (connection.hasDescription()) 192 | { 193 | feed.publish("你穿过了" + connection.getDescription()); 194 | } 195 | } 196 | } 197 | 198 | // publish feed first in case we want to override it (e.g. death-by-anglerfish scenario) 199 | if (this._destination.hasDescription()) 200 | { 201 | feed.publish("你已抵达" + this._destination.getDescription()); 202 | } 203 | 204 | if (this._ship != null) 205 | { 206 | this._ship.moveToNode(this._destination); 207 | } 208 | 209 | messenger.sendMessage("reset reachability"); 210 | this._player.moveToNode(this._destination); 211 | this._observer.onTravelAttempt(true, this._destination, this._destination.getConnection(this._player.currentNode)); 212 | timeLoop.spendActionPoints(this.getCost()); 213 | } 214 | } -------------------------------------------------------------------------------- /src/NodeConnection.ts: -------------------------------------------------------------------------------- 1 | import { OWNode } from "./Node"; 2 | import { Vector2 } from "./Vector2"; 3 | import { EDIT_MODE, messenger } from "./app"; 4 | import { JSONObject } from "./compat"; 5 | 6 | export class NodeConnection 7 | { 8 | node1: OWNode; 9 | node2: OWNode; 10 | 11 | _description: string; 12 | _hasDescription: boolean = false; 13 | 14 | _adjacentToPlayer: boolean = false; 15 | 16 | _traversed: boolean = false; 17 | _visible: boolean = false; 18 | _gated: boolean = false; 19 | _oneWay: boolean = false; 20 | 21 | _hidden: boolean = false; 22 | 23 | _connectionObj: JSONObject; 24 | 25 | constructor(n1: OWNode, n2: OWNode, connectionObj: JSONObject) 26 | { 27 | this.node1 = n1; 28 | this.node2 = n2; 29 | 30 | this._connectionObj = connectionObj; 31 | 32 | this.node1.addConnection(this); 33 | this.node2.addConnection(this); 34 | 35 | this._oneWay = connectionObj.getBoolean("one-way", this._oneWay); 36 | this._hidden = connectionObj.getBoolean("hidden", this._hidden); 37 | this._gated = connectionObj.getBoolean("gated", this._gated); 38 | 39 | if (EDIT_MODE) 40 | { 41 | this._visible = true; 42 | } 43 | 44 | if (connectionObj.hasKey("description")) 45 | { 46 | this._hasDescription = true; 47 | this._description = connectionObj.getString("description"); 48 | } 49 | } 50 | 51 | updateAdjacentToPlayer(playerNode: OWNode): void 52 | { 53 | this._adjacentToPlayer = false; 54 | 55 | if (this.node1 == playerNode || this.node2 == playerNode) 56 | { 57 | this._adjacentToPlayer = true; 58 | } 59 | } 60 | 61 | hasDescription(): boolean 62 | { 63 | return this._hasDescription; 64 | } 65 | 66 | getDescription(): string 67 | { 68 | return this._description; 69 | } 70 | 71 | getWrongWayText(): string 72 | { 73 | return "看起来这条路只能从另一侧才能通行"; 74 | } 75 | 76 | getOtherNode(node: OWNode): OWNode 77 | { 78 | if (node == this.node1) 79 | { 80 | return this.node2; 81 | } 82 | 83 | return this.node1; 84 | } 85 | 86 | traversibleFrom(startingNode: OWNode): boolean 87 | { 88 | return (!this._gated && (!this._oneWay || startingNode == this.node1)); 89 | } 90 | 91 | fireTraverseEvent(): void 92 | { 93 | if (this._connectionObj.hasKey("traverse event")) 94 | { 95 | messenger.sendMessage(this._connectionObj.getString("traverse event")); 96 | } 97 | } 98 | 99 | fireFailEvent(): void 100 | { 101 | if (this._connectionObj.hasKey("fail event")) 102 | { 103 | messenger.sendMessage(this._connectionObj.getString("fail event")); 104 | } 105 | } 106 | 107 | traverse(): void 108 | { 109 | //_oneWay = false; 110 | this._traversed = true; 111 | } 112 | 113 | revealHidden(): void 114 | { 115 | this._hidden = false; 116 | this.reveal(); 117 | } 118 | 119 | setVisible(visible: boolean): void {this._visible = visible;} 120 | 121 | reveal(): void 122 | { 123 | if (this._hidden) 124 | { 125 | return; 126 | } 127 | 128 | this.node1.setVisible(true); 129 | this.node2.setVisible(true); 130 | this._visible = true; 131 | } 132 | 133 | getAlpha(): number 134 | { 135 | if (!this._adjacentToPlayer) 136 | { 137 | return 35; 138 | } 139 | return 100; 140 | } 141 | 142 | render(): void 143 | { 144 | if (!this._visible) {return;} 145 | 146 | const dir: Vector2 = (this.node2.screenPosition.sub(this.node1.screenPosition)); 147 | const dist: number = dir.magnitude(); 148 | dir.normalize(); 149 | 150 | // draw segmented line 151 | if (!this._traversed) 152 | { 153 | stroke(0, 0, 100, this.getAlpha()); 154 | //stroke(200, 100, 100); 155 | 156 | let l: number = 0; 157 | const segmentLength: number = 5; 158 | 159 | while(l < dist) 160 | { 161 | const startPos: Vector2 = this.node1.screenPosition.add(dir.mult(l)); 162 | const endPos: Vector2 = this.node1.screenPosition.add(dir.mult(l+segmentLength)); 163 | line(startPos.x, startPos.y, endPos.x, endPos.y); 164 | l += segmentLength * 3; 165 | } 166 | } 167 | // draw solid line 168 | else 169 | { 170 | stroke(0, 0, 100, this.getAlpha()); 171 | line(this.node1.screenPosition.x, this.node1.screenPosition.y, this.node2.screenPosition.x, this.node2.screenPosition.y); 172 | } 173 | 174 | if (!this._oneWay) {return;} 175 | 176 | // draw arrow 177 | const tip: Vector2 = this.node1.screenPosition.add(dir.mult(dist * 0.6)); 178 | const base: Vector2 = tip.sub(dir.mult(14)); 179 | 180 | const right: Vector2 = base.add(dir.rightNormal().scale(7)); 181 | const left: Vector2 = base.add(dir.leftNormal().scale(7)); 182 | 183 | fill(0, 0, 0); 184 | triangle(right.x, right.y, left.x, left.y, tip.x, tip.y); 185 | } 186 | } -------------------------------------------------------------------------------- /src/PlayerData.ts: -------------------------------------------------------------------------------- 1 | import { Frequency, Curiosity } from "./Enums"; 2 | import { GameSave } from "./GameSave"; 3 | import { GlobalObserver, Message } from "./GlobalMessenger"; 4 | import { START_WITH_LAUNCH_CODES, START_WITH_COORDINATES, START_WITH_SIGNALS, messenger, feed, locator, gameManager, START_WITH_CLUES, frequencyToString } from "./app"; 5 | 6 | export class PlayerData implements GlobalObserver 7 | { 8 | _knowsLaunchCodes: boolean; 9 | _knowsSignalCoordinates: boolean; 10 | 11 | _clueList: Clue[]; 12 | _knownFrequencies: Frequency[]; 13 | 14 | _knownClueCount: number = 0; 15 | 16 | // resets every loop 17 | _isDead: boolean = false; 18 | 19 | constructor() 20 | { 21 | this._knowsLaunchCodes = START_WITH_LAUNCH_CODES; 22 | this._knowsSignalCoordinates = START_WITH_COORDINATES; 23 | 24 | this._clueList = new Array(); 25 | this._knownFrequencies = new Array(); 26 | this._knownFrequencies.push(Frequency.TRAVELER); 27 | 28 | if (START_WITH_SIGNALS) 29 | { 30 | this._knownFrequencies.push(Frequency.BEACON); 31 | this._knownFrequencies.push(Frequency.QUANTUM); 32 | } 33 | 34 | this._clueList.push(new Clue(Curiosity.ANCIENT_PROBE_LAUNCHER, "APL_1", "沉底模块", "数据收集模块从挪麦探测器发射器上脱落,掉进了深巨星的中心。")); 35 | this._clueList.push(new Clue(Curiosity.ANCIENT_PROBE_LAUNCHER, "APL_2", "汹涌的龙卷风", "深巨星上的大多数龙卷风都有强烈的上升气流,但有些逆时针旋转的龙卷风有着下行气流")); 36 | this._clueList.push(new Clue(Curiosity.ANCIENT_PROBE_LAUNCHER, "APL_3", "水母", "深巨星水母的空腔恰好能够容下一个人")); 37 | 38 | this._clueList.push(new Clue(Curiosity.QUANTUM_MOON, "QM_3", "第五个位置", "量子卫星有时会拜访太阳系外的第五个位置")); 39 | this._clueList.push(new Clue(Curiosity.QUANTUM_MOON, "QM_1", "量子成像", "观察量子物体的照片与直接观察物体本身一样,能有效地防止物体移动")); 40 | this._clueList.push(new Clue(Curiosity.QUANTUM_MOON, "QM_2", "量子纠缠", "普通物体在靠近量子物体时会与之“纠缠”在一起,并开始表现出量子属性\n\n只要无法观察自己或周围环境,即使是生命体也会被纠缠")); 41 | 42 | this._clueList.push(new Clue(Curiosity.VESSEL, "D_1", "失落的飞船", "挪麦人来到这个太阳系是为了寻找它们称之为“宇宙之眼”的神秘的信号,它们乘坐的飞船在黑荆星的某处遇难")); 43 | this._clueList.push(new Clue(Curiosity.VESSEL, "D_2", "孩童的游戏", "挪麦人孩童们玩了一个游戏,重现了族人逃离黑荆星的情景。根据游戏规则,三名玩家(逃生舱)必须在不被发现的情况下偷偷溜过蒙着眼睛的玩家(鮟鱇鱼)")); 44 | this._clueList.push(new Clue(Curiosity.VESSEL, "D_3", "追踪装置", "挪麦飞船坠毁在黑荆的根部。挪麦人试图将追踪装置插入黑荆的一根藤蔓中,以重新定位根部,但它们无法穿透藤蔓坚硬的外表")); 45 | 46 | this._clueList.push(new Clue(Curiosity.TIME_LOOP_DEVICE, "TLD_1", "时间循环装置", "挪麦研究人员在深巨星制造出一个小型但功能正常的时间循环装置后,计划在灰烬双星上建造一个完整规模的装置(前提是能产生足够的能量为其提供动力)")); 47 | this._clueList.push(new Clue(Curiosity.TIME_LOOP_DEVICE, "TLD_2", "跃迁塔", "挪麦人建造了特殊的塔楼,可以将塔内的任何人传送到相应的接收平台。只有当你能透过塔顶看到目的地时,才会传送")); 48 | this._clueList.push(new Clue(Curiosity.TIME_LOOP_DEVICE, "TLD_3", "大工程", "挪麦人挖掘了沙漏双星,建造了一台能够利用超新星能量的巨大装置\n\n控制中心位于行星中心的一个中空空腔内,与地表完全隔绝")); 49 | } 50 | 51 | init(): void 52 | { 53 | messenger.addObserver(this); 54 | this._isDead = false; 55 | } 56 | 57 | onReceiveGlobalMessage(message: Message): void 58 | { 59 | if (message.id === "learn launch codes" && !this._knowsLaunchCodes) 60 | { 61 | this._knowsLaunchCodes = true; 62 | feed.publish("已获取发射密码", true); 63 | messenger.sendMessage("spawn ship"); 64 | } 65 | else if (message.id === "learn signal coordinates" && !this._knowsSignalCoordinates) 66 | { 67 | this._knowsSignalCoordinates = true; 68 | feed.publish("已获取信号坐标", true); 69 | } 70 | 71 | GameSave.saveData(this); 72 | } 73 | 74 | killPlayer(): void 75 | { 76 | this._isDead = true; 77 | 78 | GameSave.saveData(this); 79 | } 80 | 81 | isPlayerDead(): boolean 82 | { 83 | return this._isDead; 84 | } 85 | 86 | isPlayerAtEOTU(): boolean 87 | { 88 | return ((locator.player.currentSector == gameManager._solarSystem.quantumMoon && locator.getQuantumMoonLocation() == 4) || locator.player.currentSector == gameManager._solarSystem.eyeOfTheUniverse); 89 | } 90 | 91 | knowsFrequency(frequency: Frequency): boolean 92 | { 93 | return this._knownFrequencies.includes(frequency); 94 | } 95 | 96 | knowsSignalCoordinates(): boolean 97 | { 98 | return this._knowsSignalCoordinates; 99 | } 100 | 101 | learnFrequency(frequency: Frequency): void 102 | { 103 | if (!this.knowsFrequency(frequency)) 104 | { 105 | this._knownFrequencies.push(frequency); 106 | feed.publish("频率已识别: " + frequencyToString(frequency), true); 107 | } 108 | 109 | GameSave.saveData(this); 110 | } 111 | 112 | getFrequencyCount(): number 113 | { 114 | return this._knownFrequencies.length; 115 | } 116 | 117 | knowsLaunchCodes(): boolean 118 | { 119 | return this._knowsLaunchCodes; 120 | } 121 | 122 | getClueAt(i: number): Clue 123 | { 124 | return this._clueList[i]; 125 | } 126 | 127 | getClueCount(): number 128 | { 129 | return this._clueList.length; 130 | } 131 | 132 | getKnownClueCount(): number 133 | { 134 | return this._knownClueCount; 135 | } 136 | 137 | discoverClue(id: string): void 138 | { 139 | for (let i: number = 0; i < this._clueList.length; i++) 140 | { 141 | if (this._clueList[i].id === id && !this._clueList[i].discovered) 142 | { 143 | this._clueList[i].discovered = true; 144 | this._knownClueCount++; 145 | feed.publish("信息已添加至数据库", true); 146 | } 147 | } 148 | 149 | GameSave.saveData(this); 150 | } 151 | } 152 | 153 | export class Clue 154 | { 155 | id: string; 156 | name: string; 157 | description: string; 158 | discovered: boolean; 159 | invoked: boolean = false; 160 | curiosity: Curiosity; 161 | 162 | constructor(curiosity: Curiosity, id: string, name: string, description: string) 163 | { 164 | this.curiosity = curiosity; 165 | this.id = id; 166 | this.name = name; 167 | this.description = description; 168 | this.discovered = false || START_WITH_CLUES; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/QuantumNode.ts: -------------------------------------------------------------------------------- 1 | import { OWNode } from "./Node"; 2 | import { locator } from "./app"; 3 | import { JSONObject } from "./compat"; 4 | 5 | export class QuantumNode extends OWNode 6 | { 7 | constructor(name: string, nodeJSON: JSONObject) 8 | { 9 | super(name, nodeJSON); 10 | } 11 | 12 | updateQuantumStatus(quantumState: number): void 13 | { 14 | const visible: boolean = this._nodeJSONObj.getInt("quantum location") == quantumState; 15 | this.setVisible(visible); 16 | 17 | // hide connections 18 | if (!visible) 19 | { 20 | for (const connection of this._connections.values()) 21 | { 22 | connection.setVisible(visible); 23 | } 24 | } 25 | } 26 | 27 | allowQuantumEntanglement(): boolean 28 | { 29 | return this._nodeJSONObj.getInt("quantum location") == locator.getQuantumMoonLocation() && this._nodeJSONObj.getBoolean("entanglement node", false); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Screen.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "p5"; 2 | import { ButtonObserver, Button } from "./Button"; 3 | import { Entity } from "./Entity"; 4 | import { Vector2 } from "./Vector2"; 5 | import { playerData } from "./app"; 6 | 7 | export abstract class OWScreen implements ButtonObserver 8 | { 9 | active: boolean = false; 10 | overlay: boolean = false; 11 | 12 | _buttons: Button[]; 13 | _toolbarButtons: Button[]; 14 | _starPositions: Vector2[]; 15 | 16 | _toolbarRoot: Entity; 17 | 18 | constructor() 19 | { 20 | this._buttons = new Array