three+gsap实现带声效逼真三维场景万圣节施法者捉鬼游戏代码

代码语言:html

所属分类:游戏

代码描述:three+gsap实现带声效逼真三维场景万圣节施法者捉鬼游戏代码,有三个施法的符号,每个符号攻击力不同,恢复时间不同,鼠标点击拖动绘制符号即可施法,要保证水晶不被小鬼吸走全部能量。

代码标签: three gsap 逼真 三维 场景 万圣节 施法者 捉鬼 游戏 代码

下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开

<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  


    
  <script type="importmap">
  {
    "imports": {
      "three": "//repo.bfw.wiki/bfwrepo/js/module/three/build/152/three.module.js",
      "three/addons/": "//repo.bfw.wiki/bfwrepo/js/module/three/examples/152/jsm/"
    }
  }
</script>
  
  
  
<style>
@import url("https://fonts.googleapis.com/css2?family=Henny+Penny&family=Tinos:wght@400;700&display=swap");
:root {
  --font-body: "Tinos", serif;
  --font-heading: "Henny Penny", cursive;
  --font-weight-body: 400;
  --font-weight-bold: 700;
  --font-weight-heading: 400;
  --color-black: black;
  --color-black-alpha: rgba(0, 0, 0, 0.7);
  --color-white: white;
  --color-grey: #767474;
  --color-grey-dark: #3e3e3e;
  --color-crystal: #d54adf;
  --color-crystal-light: #d68ddc;
}

html,
body,
.app {
  overflow: hidden;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: var(--font-body);
  font-weight: var(--font-weight-body);
}

body {
  font-size: clamp(20px, 4vmin, 26px);
  line-height: 110%;
}

.app {
  background-color: var(--color-black);
  color: #f9f9f9;
}
.app h1,
.app h2,
.app h3,
.app h4,
.app h5,
.app h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-weight-heading);
  margin: 0;
}
.app h1 {
  font-size: clamp(30px, 14vmin, 130px);
}
.app h2 {
  font-size: clamp(30px, 11vmin, 100px);
}
.app h3 {
  font-size: clamp(24px, 6.5vmin, 60px);
}
.app h4 {
  font-size: clamp(20px, 4vmin, 40px);
}
.app a,
.app a:visited {
  color: var(--color-crystal-light);
  pointer-events: all;
}
.app a:hover,
.app a:visited:hover {
  color: var(--color-crystal);
}

.top-bar {
  position: absolute;
  top: 1em;
  left: 1em;
  right: 1em;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 80%;
  pointer-events: none;
}
.top-bar .left,
.top-bar .right {
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.top-bar .left {
  gap: 1em;
}
.top-bar .left > * {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  display: none;
}
.top-bar button {
  --size: 40px;
  --border-color: var(--color-grey);
  display: none;
  width: var(--size);
  height: var(--size);
  background-color: var(--color-black-alpha);
  border: solid 2px var(--border-color);
  border-radius: 40px;
  cursor: pointer;
  position: relative;
  align-items: center;
  justify-content: center;
  pointer-events: all;
}
.top-bar button svg {
  transition: transform 0.2s ease-in-out;
}
.top-bar button.show-unless {
  display: flex;
}
.top-bar button[data-off] svg {
  transform: scale(0.8);
}
.top-bar button[data-off]::after {
  content: "";
  width: 100%;
  height: 2px;
  position: absolute;
  top: 50%;
  left: 50%;
  background-color: var(--border-color);
  transform: translate(-50%, -50%) rotate(-45deg);
}
.top-bar button:hover, .top-bar button:active {
  --border-color: var(--color-crystal);
}

.count {
  font-variant-numeric: tabular-nums;
}

.health-bar {
  width: 260px;
  height: 20px;
  border: 2px solid var(--color-grey);
  background-color: var(--color-black-alpha);
  overflow: hidden;
  position: relative;
}
.health-bar::after {
  content: "";
  position: absolute;
  inset: 5px;
  background-color: var(--color-crystal);
  transform-origin: left center;
  transform: scaleX(calc(1 * var(--health)));
}

.canvas,
.overlay,
.screens {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  max-height: 100vw;
  max-width: 2000px;
  transform: translate(-50%, -50%);
}

.screens {
  pointer-events: none;
  max-width: 1280px;
  margin: 0 auto;
}
.screens > * {
  --pad: 5vmin;
  position: absolute;
  inset: var(--pad);
  display: grid;
  align-items: stretch;
  justify-items: stretch;
  justify-content: center;
  display: none;
}
.screens > *::after {
  grid-area: space;
}
.screens .spells {
  inset: unset;
  bottom: 3vmin;
  right: 3vmin;
  z-index: 10;
  max-width: 46%;
  display: none;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 1fr;
  gap: 1.5rem;
  justify-content: center;
  align-items: center;
  padding: 0.5rem 1.5rem;
}
.screens .spells .spell-path {
  width: 50px;
  position: relative;
}
.screens .spells .spell-path .check {
  opacity: 0;
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translate(-50%, 0%);
  transition-property: opacity, transform;
  transition-duration: 0.4s;
  transition-timing-function: cubic-bezier(0.52, -0.47, 0.37, 1);
}
.screens .spells .spell-path svg {
  width: 100%;
  fill: none;
}
.screens .spells .info {
  display: none;
  flex-direction: column;
}
.screens .spells .info h4 {
  margin-bottom: 1rem;
}
.screens .spells .info p {
  font-size: 20px;
}
.screens .spells .charge-path {
  stroke-width: 6;
  stroke-dasharray: var(--length) var(--length);
  stroke-dashoffset: calc(((1 - var(--charge))) * var(--length));
}
.screens .spells .guide-path {
  stroke: rgba(255, 255, 255, 0.2);
}
.screens .spells .spell-details {
  display: flex;
  flex-direction: row;
  gap: 3rem;
  align-items: center;
  z-index: 2;
}
.screens .spells .background {
  position: absolute;
  inset: 0;
  border: solid 2px var(--color-grey);
  background-color: var(--color-black-alpha);
}
.screens .spells.corner {
  cursor: pointer;
  pointer-events: all;
  display: grid;
}
.screens .spells.corner .spell-path.ready .check {
  opacity: 1;
  transform: translate(-50%, -200%);
}
.screens .spells.corner:hover .background {
  border-color: var(--color-crystal);
}
.screens .spells.full {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr 1fr;
  bottom: 50%;
  gap: 2rem;
  transform: translateY(50%);
  padding: 2rem 3rem;
}
.screens .spells.full .spell-path {
  width: 160px;
}
.screens .spells.full .spell-path .check {
  transition: none;
}
.screens .spells.full .spell-path svg {
  --charge: 1 !important;
}
.screens .spells.full .info {
  display: flex;
}
.screens .content {
  text-align: center;
  grid-area: content;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}
.screens .content > *:not(:last-child) {
  margin-bottom: clamp(20px, 5vmin, 50px);
}
.screens .button-row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: row;
  gap: 0.7em;
}
.screens button {
  --border-color: var(--color-grey);
  color: var(--color-white);
  pointer-events: all;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: var(--font-weight-body);
}
.screens button:not(.simple, .no-style) {
  background-color: var(--color-black-alpha);
  border: 2px solid var(--border-color);
  font-size: 30px;
  padding: 0.2em 1.4em;
}
.screens button.simple {
  background-color: transparent;
  border: none;
  text-decoration: underline;
  -webkit-text-decoration-color: var(--border-color);
          text-decoration-color: var(--border-color);
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
  font-size: 20px;
}
.screens button:hover, .screens button:active {
  --border-color: var(--color-crystal);
}
.screens p {
  max-width: 600px;
  margin: 0;
}

.loading-bar {
  width: 260px;
  height: 2px;
  background-color: var(--color-grey-dark);
  overflow: hidden;
  position: relative;
}
.loading-bar::after {
  content: "";
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  background-color: var(--color-crystal);
  transform-origin: left center;
  transform: scaleX(calc(1 * var(--loaded)));
}

[data-state=IDLE] #sounds-button,
[data-state=INIT] #sounds-button {
  display: none;
}

[data-state=INIT] #sounds-button {
  display: none;
}

[data-state=LOADING] [data-screen=LOADING] {
  display: grid;
  grid-template-columns: 0px 1fr;
  grid-template-areas: "space content";
}
[data-state=LOADING] #sounds-button {
  display: none;
}

[data-state=LOAD_ERROR] #sounds-button {
  display: none;
}

[data-state=TITLE_SCREEN] [data-screen=TITLE_SCREEN] {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-areas: "space content";
}
[data-state=TITLE_SCREEN] [data-screen=TITLE_SCREEN] h1 {
  line-height: 1.2em;
}

[data-state=CREDITS] [data-screen=CREDITS] {
  display: grid;
  grid-template-columns: 2fr 1fr;
  grid-template-areas: "content space";
}
[data-state=CREDITS] [data-screen=CREDITS] h3,
[data-state=CREDITS] [data-screen=CREDITS] .content {
  width: auto;
  text-align: left;
}
[data-state=CREDITS] [data-screen=CREDITS] li {
  margin-bottom: 1rem;
}

[data-state=INSTRUCTIONS_CRYSTAL] [data-screen=INSTRUCTIONS_CRYSTAL] {
  display: grid;
  grid-template-rows: 1fr 1.2fr;
  grid-template-areas: "space" "content";
}

[data-state=INSTRUCTIONS_DEMON] [data-screen=INSTRUCTIONS_DEMON] {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  grid-template-areas: "content space";
}
[data-state=INSTRUCTIONS_DEMON] [data-screen=INSTRUCTIONS_DEMON] .content {
  justify-content: flex-end;
}

[data-state=INSTRUCTIONS_CAST] [data-screen=INSTRUCTIONS_CAST] {
  display: grid;
  grid-template-columns: 0px 1fr;
  grid-template-areas: "space content";
}

#spell-guide {
  width: 70%;
  max-width: 400px;
  opacity: 0;
  transition: opacity 1s ease-in-out;
}
#spell-guide.show {
  opacity: 0.5;
}

[data-state=INSTRUCTIONS_SPELLS] [data-screen=INSTRUCTIONS_SPELLS] {
  display: grid;
  grid-template-columns: 1fr 1.5fr;
  grid-template-areas: "content space";
}

[data-state=GAME_RUNNING] #health-bar,
[data-state=SPECIAL_SPELL] #health-bar {
  display: flex;
}
[data-state=GAME_RUNNING] #demon-state,
[data-state=SPECIAL_SPELL] #demon-state {
  display: flex;
}
[data-state=GAME_RUNNING] #pause-button,
[data-state=SPECIAL_SPELL] #pause-button {
  display: flex;
}

[data-state=ENDLESS_MODE] #endless-mode,
[data-state=ENDLESS_SPECIAL_SPELL] #endless-mode {
  display: flex;
}
[data-state=ENDLESS_MODE] #close-button,
[data-state=ENDLESS_SPECIAL_SPELL] #close-button {
  display: flex;
}
[data-state=ENDLESS_MODE] #pause-button,
[data-state=ENDLESS_SPECIAL_SPELL] #pause-button {
  display: flex;
}

[data-state=ENDLESS_SPELL_OVERLAY] #endless-mode {
  display: flex;
}

[data-state=PAUSED] [data-screen=PAUSED],
[data-state=ENDLESS_PAUSE] [data-screen=PAUSED] {
  display: grid;
  grid-template-rows: 2fr 1fr;
  grid-template-areas: "space" "content";
}
[data-state=PAUSED] [data-screen=PAUSED] .content,
[data-state=ENDLESS_PAUSE] [data-screen=PAUSED] .content {
  justify-content: flex-end;
}
[data-state=PAUSED] #paused,
[data-state=ENDLESS_PAUSE] #paused {
  display: flex;
}
[data-state=PAUSED] #pause-button,
[data-state=ENDLESS_PAUSE] #pause-button {
  display: flex;
}

[data-state=ENDLESS_PAUSE] #close-button,
[data-state=CREDITS] #close-button {
  display: flex;
}

[data-state=SPELL_OVERLAY] [data-screen=SPELL_OVERLAY],
[data-state=ENDLESS_SPELL_OVERLAY] [data-screen=SPELL_OVERLAY] {
  display: grid;
  grid-template-columns: 1fr 2fr;
  grid-template-areas: "content space";
}

[data-state=SPELL_OVERLAY] #health-bar {
  display: flex;
}
[data-state=SPELL_OVERLAY] #demon-state {
  display: flex;
}

[data-state=GAME_OVER] [data-screen=GAME_OVER] {
  display: grid;
  grid-template-columns: 0px 1fr;
  grid-template-areas: "space content";
}
[data-state=GAME_OVER] [data-screen=GAME_OVER] .content {
  justify-content: flex-end;
}

[data-state=WINNER] [data-screen=WINNER] {
  display: grid;
  grid-template-columns: 0px 1fr;
  grid-template-areas: "space content";
}
[data-state=WINNER] [data-screen=WINNER] .content {
  justify-content: flex-end;
}

.debug-panels {
  position: absolute;
  bottom: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  color: white;
  padding: 10px;
  gap: 10px;
  z-index: 100;
  pointer-events: none;
}

.panel {
  border: 1px solid white;
  padding: 10px;
  max-width: 250px;
  width: 250px;
}
.panel p {
  margin: 0;
  padding: 0;
}
.panel button {
  border: 0;
  background-color: #f9f9f9;
  color: #444;
  font-size: 1em;
  padding: 6px 10px;
  cursor: pointer;
  pointer-events: all;
}
.panel > div {
  position: relative !important;
}

#spell-path {
  stroke: red;
  stroke-width: 2;
  fill: none;
}

#spell-points circle {
  fill: white;
}

#spells {
  width: 0;
  height: 0;
  overflow: hidden;
  position: absolute;
  top: 0;
  left: 0;
}

.controls {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 12px;
}

.state {
  padding-bottom: 0.5em;
}

.spell-stat {
  padding: 0 1rem;
  font-size: 14px;
  border-left: 5px solid transparent;
}
.spell-stat:not(:last-child) {
  padding-bottom: 1rem;
}
.spell-stat .spell-preview {
  stroke: white;
  stroke-width: 2;
  fill: none;
  width: 60px;
}
.spell-stat .score {
  font-size: 1.4em;
  width: 120px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline;
}
.spell-stat div {
  display: flex;
  align-items: center;
  gap: 2rem;
  flex-direction: row;
}
.spell-stat.cast {
  border-left: 5px solid red;
}

.debug-overlays {
  pointer-events: none;
}

.clear-interface .debug-panels,
.clear-interface .debug-overlays,
.clear-interface .audio-controls,
.clear-interface .top-bar,
.clear-interface .screens {
  display: none;
}

.debug-layout .top-bar {
  outline: solid 2px purple;
}
.debug-layout .screens {
  outline: solid 2px green;
}
.debug-layout .screens > *::after {
  display: grid;
  align-items: center;
  justify-content: center;
  content: "SPACE";
  background-color: #ff000055;
  outline: solid 2px red;
}
.debug-layout .content {
  background-color: #0000ff55;
  outline: solid 2px blue;
}

.sr-only {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>


  
</head>

<body >
  <svg id="spells" aria-hidden="true">


      <refs>
        <path
          id="spell-shape-arcane"
          data-spell="arcane"
          class="spell"
          d="M1 5L1 24.5L1 37L4 50.5L9.5 61.5L16.5 69.5L25.5 77L35.5 81.5L46 85L57 86L67.5 85L76.5 81.5L86.5 77L96 69.5L102.5 61.5L108 52.5L111 43.5L112 34L112.5 26L112.5 17.5L112.5 7.5L112.5 2L57.5 1L58.5 43.5"
        />
        <path
          id="spell-shape-fire"
          data-spell="fire"
          class="spell"
          d="M1.38133 71L38.7997 2L72.643 71L110.061 2L143.905 71"
        />
        <path
          id="spell-shape-vortex"
          data-spell="vortex"
          class="spell"
          d="M48.8852 110L47.4198 2L1 65.6158L85 65.6158L85 110"
        />

        <path id="check" d="M9.44172 20L0 10.5198L2.36043 8.14969L9.44172 15.2599L24.6396 0L27 2.37006L9.44172 20Z" fill="white"/>
        
      </refs>
    </svg>

    <!-- 
    
      GAME SCREENS
    
    -->

    <div class="app">
      
      <!-- THREE JS DOES IT'S RENDERERING IN HERE -->
      <div class="canvas"></div>


      
      
      <!-- TOP STATUS BAR, FOR THINGS LIKE LIFE INDICATOR, SOUND CONTROLS AND OTHER QUICK OPTIONS -->
      <div class="top-bar">
        <div class="left">
          <div class="health" id="health-bar" data-show-on="GAME_RUNNING,PAUSED,SPELL_OVERLAY">
            <svg class="icon" xmlns="http://www.w3.org/2000/svg" width="22" height="20" viewBox="0 0 22 20" fill="none">
              <g clip-path="url(#clip0_203_249)">
                <path d="M17.7218 0H4.2774C4.12462 0 3.97948 0.078125 3.89546 0.210938L0.0760111 5.96094C-0.0347528 6.13281 -0.0232945 6.35938 0.102747 6.51563L10.6444 19.8281C10.8277 20.0586 11.1715 20.0586 11.3548 19.8281L21.8965 6.51563C22.0225 6.35547 22.034 6.13281 21.9232 5.96094L18.1076 0.210938C18.0198 0.078125 17.8784 0 17.7218 0ZM16.9847 1.875L19.4024 5.625H16.7899L14.8152 1.875H16.9847ZM9.26559 1.875H12.7298L14.7045 5.625H7.29476L9.26559 1.875ZM5.01455 1.875H7.184L5.20934 5.625H2.59684L5.01455 1.875ZM3.37219 7.5H5.33539L7.94407 13.75L3.37219 7.5ZM7.3024 7.5H14.6968L10.9996 17.0039L7.3024 7.5ZM14.0552 13.75L16.66 7.5H18.6232L14.0552 13.75Z" fill="white"/>
              </g>
              <defs>
                <clipPath id="clip0_203_249">
                  <rect width="22" height="20" fill="white"/>
                </clipPath>
              </defs>
            </svg>
            <div class="info health-bar">
              <span class="sr-only"></span>
            </div>
          </div>
          <div class="demons" id="demon-state" data-show-on="GAME_RUNNING,SPELL_OVERLAY">
            <svg class="icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22" fill="none">
              <path d="M9.77778 11.7857C9.77778 12.2519 9.63441 12.7076 9.36581 13.0953C9.09722 13.4829 8.71545 13.785 8.26878 13.9634C7.82212 14.1418 7.33062 14.1885 6.85645 14.0976C6.38227 14.0066 5.94671 13.7821 5.60485 13.4525C5.26299 13.1228 5.03018 12.7028 4.93586 12.2456C4.84154 11.7883 4.88995 11.3144 5.07496 10.8837C5.25998 10.453 5.57329 10.0848 5.97527 9.82582C6.37726 9.56682 6.84987 9.42857 7.33333 9.42857C7.98164 9.42857 8.60339 9.67691 9.06182 10.119C9.52024 10.561 9.77778 11.1606 9.77778 11.7857ZM14.6667 9.42857C14.1832 9.42857 13.7106 9.56682 13.3086 9.82582C12.9066 10.0848 12.5933 10.453 12.4083 10.8837C12.2233 11.3144 12.1749 11.7883 12.2692 12.2456C12.3635 12.7028 12.5963 13.1228 12.9382 13.4525C13.28 13.7821 13.7156 14.0066 14.1898 14.0976C14.664 14.1885 15.1555 14.1418 15.6021 13.9634C16.0488 13.785 16.4305 13.4829 16.6991 13.0953C16.9677 12.7076 17.1111 12.2519 17.1111 11.7857C17.1111 11.1606 16.8536 10.561 16.3952 10.119C15.9367 9.67691 15.315 9.42857 14.6667 9.42857ZM22 10.2143C22 13.146 20.6708 15.8891 18.3333 17.8279V20.0357C18.3333 20.5567 18.1187 21.0563 17.7367 21.4247C17.3547 21.793 16.8366 22 16.2963 22H5.7037C5.16345 22 4.64532 21.793 4.2633 21.4247C3.88128 21.0563 3.66667 20.5567 3.66667 20.0357V17.8279C1.32407 15.8891 0 13.146 0 10.2143C0 4.5817 4.93472 0 11 0C17.0653 0 22 4.5817 22 10.2143ZM19.5556 10.2143C19.5556 5.88205 15.7178 2.35714 11 2.35714C6.28222 2.35714 2.44444 5.88205 2.44444 10.2143C2.44444 12.6019 3.60657 14.8304 5.63343 16.333C5.78206 16.4431 5.90245 16.5847 5.98528 16.7468C6.06811 16.909 6.11117 17.0873 6.11111 17.268V19.6429H7.74074V17.6786C7.74074 17.366 7.86951 17.0662 8.09872 16.8452C8.32793 16.6242 8.63881 16.5 8.96296 16.5C9.28712 16.5 9.59799 16.6242 9.8272 16.8452C10.0564 17.0662 10.1852 17.366 10.1852 17.6786V19.6429H11.8148V17.6786C11.8148 17.366 11.9436 17.0662 12.1728 16.8452C12.402 16.6242 12.7129 16.5 13.037 16.5C13.3612 16.5 13.6721 16.6242 13.9013 16.8452C14.1305 17.0662 14.2593 17.366 14.2593 17.6786V19.6429H15.8889V17.268C15.889 17.0875 15.9321 16.9093 16.0149 16.7474C16.0978 16.5854 16.2181 16.444 16.3666 16.334C18.3934 14.8304 19.5556 12.6019 19.5556 10.2143Z" fill="white"/>
            </svg>
            <span class="info">
              <span class="sr-only">Demons killed:</span> <span class="count" data-demon-count>0</span> / <span class="count" data-demon-total>50</span> 
            </span>
          </div>
          <div class="endless" id="endless-mode"  data-show-on="ENDLESS_MODE">
            <svg class="icon" xmlns="http://www.w3.org/2000/svg" width="42" height="20" viewBox="0 0 42 20" fill="none">
              <path d="M42 10C42.0002 11.9777 41.4043 13.9111 40.2876 15.5557C39.171 17.2002 37.5837 18.4819 35.7265 19.2388C33.8693 19.9957 31.8257 20.1937 29.8542 19.8078C27.8826 19.4219 26.0717 18.4694 24.6504 17.0708L24.5674 16.9825L14.4283 5.71885C13.5711 4.89093 12.4844 4.33055 11.3047 4.10802C10.125 3.8855 8.90474 4.01075 7.79711 4.46806C6.68947 4.92536 5.74379 5.69435 5.07874 6.67851C4.41369 7.66268 4.05891 8.81818 4.05891 10C4.05891 11.1818 4.41369 12.3373 5.07874 13.3215C5.74379 14.3057 6.68947 15.0746 7.79711 15.5319C8.90474 15.9892 10.125 16.1145 11.3047 15.892C12.4844 15.6695 13.5711 15.1091 14.4283 14.2812L14.95 13.7012C15.1269 13.5043 15.3416 13.3435 15.5817 13.2282C15.8217 13.1128 16.0826 13.0451 16.3492 13.029C16.6159 13.0128 16.8832 13.0485 17.1359 13.1339C17.3885 13.2194 17.6216 13.353 17.8218 13.5271C18.022 13.7012 18.1854 13.9123 18.3026 14.1486C18.4199 14.3848 18.4887 14.6414 18.5051 14.9038C18.5215 15.1661 18.4853 15.4291 18.3984 15.6777C18.3115 15.9263 18.1758 16.1556 17.9988 16.3526L17.4314 16.9825L17.3484 17.0708C15.927 18.469 14.1162 19.4211 12.1448 19.8068C10.1735 20.1925 8.13019 19.9944 6.27328 19.2375C4.41636 18.4807 2.82925 17.1991 1.71262 15.5549C0.595995 13.9106 0 11.9775 0 10C0 8.0225 0.595995 6.0894 1.71262 4.44514C2.82925 2.80088 4.41636 1.51931 6.27328 0.762477C8.13019 0.00564237 10.1735 -0.192466 12.1448 0.193202C14.1162 0.578871 15.927 1.531 17.3484 2.92918L17.4314 3.01751L27.5705 14.2812C28.4277 15.1091 29.5143 15.6695 30.6941 15.892C31.8738 16.1145 33.094 15.9892 34.2017 15.5319C35.3093 15.0746 36.255 14.3057 36.92 13.3215C37.5851 12.3373 37.9399 11.1818 37.9399 10C37.9399 8.81818 37.5851 7.66268 36.92 6.67851C36.255 5.69435 35.3093 4.92536 34.2017 4.46806C33.094 4.01075 31.8738 3.8855 30.6941 4.10802C29.5143 4.33055 28.4277 4.89093 27.5705 5.71885L27.0488 6.29878C26.8719 6.49574 26.6572 6.65648 26.4171 6.77182C26.177 6.88717 25.9162 6.95486 25.6495 6.97103C25.3829 6.9872 25.1156 6.95154 24.8629 6.86607C24.6102 6.7806 24.3772 6.64701 24.177 6.47292C23.9768 6.29883 23.8134 6.08765 23.6962 5.85144C23.5789 5.61523 23.5101 5.35862 23.4937 5.09624C23.4772 4.83387 23.5135 4.57088 23.6004 4.3223C23.6872 4.07371 23.823 3.84439 24 3.64743L24.5674 3.01751L24.6504 2.92918C26.0717 1.53059 27.8826 0.578099 29.8542 0.192194C31.8257 -0.193711 33.8693 0.00430047 35.7265 0.761184C37.5837 1.51807 39.171 2.79982 40.2876 4.44434C41.4043 6.08885 42.0002 8.02225 42 10Z" fill="white"/>
            </svg>
            <span class="info">Endless Mode</span>
          </div>
          <div class="paused" id="paused" data-show-on="ENDLESS_PAUSE,PAUSED">
            <svg class="icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
              <path d="M14.4444 3.44444L12 1M12 1L9.55556 3.44444M12 1V23M12 23L14.4444 20.5556M12 23L9.55556 20.5556M20.5556 14.4444L23 12M23 12L20.5556 9.55556M23 12H1M1 12L3.44444 14.4444M1 12L3.44444 9.55556" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            <span class="info">Paused, drag to move around the scene</span>
          </div>
        </div>
        <div class="right">

          <button id="close-button" data-send="end" data-show-on="ENDLESS_MODE,ENDLESS_PAUSE">
            <span class="sr-only">Back to the menu.</span>
            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
              <path d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" fill="white"/>
            </svg>
          </button>
          <button id="pause-button" data-send="pause" data-show-on="GAME_RUNNING,PAUSED,ENDLESS_MODE,ENDLESS_PAUSE">
            <span class="sr-only">Pause the game.</span>
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none">
              <path d="M8 14V0H12V14H8ZM0 14V0H4V14H0Z" fill="white"/>
            </svg>  
          </button>
          <button id="sounds-button" class="show-unless" >
            <span class="sr-only" data-copy="Turn sounds $$state."></span>
            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" viewBox="0 0 18 17" fill="none">
              <path d="M17.4776 0.522705V11.6023C17.4776 12.425 17.1508 13.2141 16.569 13.7959C15.9872 14.3777 15.1981 14.7045 14.3754 14.7045C13.5526 14.7045 12.7635 14.3777 12.1817 13.7959C11.5999 13.2141 11.2731 12.425 11.2731 11.6023C11.2731 10.7795 11.5999 9.9904 12.1817 9.40861C12.7635 8.82682 13.5526 8.49998 14.3754 8.49998C14.854 8.49998 15.306 8.60634 15.7049 8.80134V3.59839L6.84126 5.48634V13.375C6.84126 14.1978 6.51442 14.9868 5.93263 15.5686C5.35084 16.1504 4.56177 16.4773 3.73899 16.4773C2.91622 16.4773 2.12714 16.1504 1.54535 15.5686C0.963564 14.9868 0.636719 14.1978 0.636719 13.375C0.636719 12.5522 0.963564 11.7631 1.54535 11.1813C2.12714 10.5996 2.91622 10.2727 3.73899 10.2727C4.21763 10.2727 4.66967 10.3791 5.06854 10.5741V3.1818L17.4776 0.522705Z" fill="white"/>
            </svg>
          </button>
        </div>
      </div>

      <!-- MAIN CONTENT THAT SITS OVER THE GAME -->
      <div class="screens">

        <!-- SPELLS -->

        <div class="spells" data-send="spells">

            <div class="background" data-flip-spell></div>
         
            <div class="spell-details">
              <div class="spell-path" id="spell-svg-viz-arcane" >
                <svg class="check" xmlns="http://www.w3.org/2000/svg" width="27" height="20" viewBox="0 0 27 20" fill="none">
                  <use href="#check" />
                </svg> 
                <svg data-flip-spell xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 98" fill="none">
                  <path class="guide-path" d="M5 5V76C5 87.0457 13.9543 96 25 96H107C118.046 96 127 87.0457 127 76V25C127 13.9543 118.046 5 107 5H83.0416C71.9959 5 63.0416 13.9543 63.0416 25V53.6304" stroke-width="4"/>
                  <path id="spell-path-viz-arcane" class="charge-path" d="M5 5V76C5 87.0457 13.9543 96 25 96H107C118.046 96 127 87.0457 127 76V25C127 13.9543 118.046 5 107 5H83.0416C71.9959 5 63.0416 13.9543 63.0416 25V53.6304" stroke="#9BCFFF"/>
                  <circle cx="5" cy="5" r="5" fill="#9BCFFF"/>
                  <path d="M54 44L63 56.5L71.5 44" stroke="#9BCFFF" stroke-width="4"/>
                </svg>
              </div>
              <div class="info">
                <h4 data-flip-spell>Arcane</h4>
                <p data-flip-spell>The reliable Arcane spell shoots a powerful bolt of magic, killing one demon. It has a fast recharge.</p>
              </div>
            </div>
            
            <div class="spell-details">
              <div class="spell-path" id="spell-svg-viz-fire">
                <svg class="check" xmlns="http://www.w3.org/2000/svg" width="27" height="20" viewBox="0 0 27 20" fill="none">
                  <use href="#check" />
                </svg> 
                <svg  data-flip-spell xmlns="http://www.w3.org/2000/svg" viewBox="0 0 148 120" fill="none">
                  <path class="guide-path" d="M5 93.4113L17.9176 55.9579C23.6054 39.4668 47.2737 40.5189 51.4757 57.4497V57.4497C55.7764 74.778 80.2103 75.318 85.2723 58.1966L86.0675 55.507C91.1314 38.379 115.23 37.9355 120.92 54.8656L138 105.677" stroke-width="4"/>
                  <path id="spell-path-viz-fire" class="charge-path" d="M5 93.4113L17.9176 55.9579C23.6054 39.4668 47.2737 40.5189 51.4757 57.4497V57.4497C55.7764 74.778 80.2103 75.318 85.2723 58.1966L86.0675 55.507C91.1314 38.379 115.23 37.9355 120.92 54.8656L138 105.677" stroke="#F2C092" />
                  <circle cx="5" cy="93" r="5" fill="#F2C092"/>
                  <path d="M126.359 99.4829L139.186 108.011L142.738 93.3183" stroke="#F2C092" stroke-width="4"/>
                </svg>
              </div>
              <div class="info">
                <h4 data-flip-spell>Fire</h4>
                <p data-flip-spell>The Fire spell releases two fireballs, kills two unsuspected demons!</p>
              </div>
            </div>
            
            <div class="spell-details">
              <div class="spell-path" id="spell-svg-viz-vortex">
                <svg class="check" xmlns="http://www.w3.org/2000/svg" width="27" height="20" viewBox="0 0 27 20" fill="none">
                  <use href="#check" />
                </svg> 
                <svg data-flip-spell xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 170" fill="none">
                  <path class="guide-path" d="M75 166V61.7475C75 42.3543 50.1681 34.3112 38.798 50.0217L22.9608 71.9046C13.3899 85.129 22.8384 103.63 39.1628 103.63H78H105C116.046 103.63 125 112.585 125 123.63V166" stroke-width="4"/>
                  <path id="spell-path-viz-vortex" class="charge-path" d="M75 166V61.7475C75 42.3543 50.1681 34.3112 38.798 50.0217L22.9608 71.9046C13.3899 85.129 22.8384 103.63 39.1628 103.63H78H105C116.046 103.63 125 112.585 125 123.63V166" stroke="#C5F298"/>
                  <circle cx="75" cy="165" r="5" fill="#C5F298"/>
                  <path d="M116 154L125 166.5L133.5 154" stroke="#C5F298" stroke-width="4"/>
                </svg>
              </div>
              <div class="info">
                <h4 data-flip-spell>Vortex</h4>
                <p data-flip-spell>Opens a vortex that sucks in all the demons in the room. This one takes a while to charge so choose when to use it wisely!</p>
              </div>
            </div>
             
          
          <!--<div class="spells-full-description">
            <div class="arcane">
              <div class="path"><svg><use href="#spell-pretty-arcane" /></svg></div>
              <div class="info">
                <h4>Arcane</h4>
                <p class="description">The reliable Arcane spell shots a powerful bolt of magic, killing one demon. It has a fast recharge.</p>
              </div>
            </div>
            <div class="fire">
              <div class="path"><svg><use href="#spell-pretty-fire" /></svg></div>
              <div class="info">
                <h4>Fire</h4>
                <p class="description">The Fire spell releases two fireballs, kills two unsuspected demon!</p>
              </div>
            </div>
            <div class="vortex">
              <div class="path"><svg><use href="#spell-pretty-vortex" /></svg></div>
              <div class="info">
                <h4>Vortex</h4>
                <p class="description">Opens a vortex that sucks in all the demons in the room. This one takes a while to charge so choose when to use it wisely!</p>
              </div>
            </div>
            <button data-send="close">Close</button>
          </div>-->
          
        </div>

        <div data-screen="LOADING" class="loading">
          <div class="content">
            <span>Loading...</span>
            <div class="loading-bar"></div>
          </div>
        </div>

        <div data-screen="LOAD_ERROR" class="load-error">
          <div class="content">
            <span >Load Error</span>
          </div >
        </div>

        <div data-screen="TITLE_SCREEN" class="title">
          <div class="content">
            <h1 data-fade>Spell<br/>Caster</h1>
            <button data-send="next" data-fade>Start</button>
            <ul class="button-row">
              <li><button data-fade class="simple" data-send="skip">Skip instructions</button></li>
              <li><button data-fade class="simple" data-send="endless">Endless mode</button></li>
              <li><button data-fade class="simple" data-send="credits">Credits</button></li>
            </ul>
          </div>
        </div>
        
        <div data-screen="CREDITS" >
          <div class="content">
            <h3 data-fade>Credits</h3>
            
            <ul>
              <li data-fade>Game code: <a href="" target="_blank">Steve Gardner</a></li>
              <li data-fade>Room model: <a href="" target="_blank">Modular Ruins Pack</a> by <a href="" target="_blank">Quaternius</a> </li>
              <li data-fade><a href="" target="_blank" >Skeletal Hand</a>: by <a href="" target="_blank">Jeremy Swan</a> </li>
              <li data-fade>Demon: An edited version of <a href="" target="_blank">Skeleton Boy</a> by <a href="" target="_blank">Polygonal Mind</a></li>
              <li data-fade>Sound from <a href="" target="_blank">Zapsplat.com</a></li>
            </ul>

            <button data-fade data-send="close">Back</button>
          </div>
        </div>
        
        <div data-screen="INSTRUCTIONS_CRYSTAL" class="instructions-crystal">
          <div class="content">
            <h3 data-fade>Protect the crystal</h3>
            <p data-fade>Welcome, Guardian. Your mission is clear: safeguard this  crystal. Demons seek to destroy it, for if they succeed, the consequences will be catastrophic.</p>
            <button data-fade data-send="next">Next</button>
          </div>
        </div>

        <div data-screen="INSTRUCTIONS_DEMON" class="instructions-demon">
          <div class="content">
            <h3 data-fade>Face the onslaught</h3>
            <p data-fade>A horde of <span data-demon-total>50</span> demons approaches, relentless in their quest to seize the crystal's power. Stand resolute, for you alone are its defender. Ready your spells and prepare to face the coming onslaught.</p>
            <button data-send="next" data-fade>Next</button>
          </div>
        </div>

        <div data-screen="INSTRUCTIONS_CAST" class="instructions-cast">
          <div class="content">
            <svg id="spell-guide" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 505 385" fill="none">
              <g filter="url(#filter0_d_200_49)">
                <path d="M20.3333 47C20.3333 61.7276 32.2724 73.6667 47 73.6667C61.7276 73.6667 73.6667 61.7276 73.6667 47C73.6667 32.2724 61.7276 20.3333 47 20.3333C32.2724 20.3333 20.3333 32.2724 20.3333 47ZM249.464 217.536C251.417 219.488 254.583 219.488 256.536 217.536L288.355 185.716C290.308 183.763 290.308 180.597 288.355 178.645C286.403 176.692 283.237 176.692 281.284 178.645L253 206.929L224.716 178.645C222.763 176.692 219.597 176.692 217.645 178.645C215.692 180.597 215.692 183.763 217.645 185.716L249.464 217.536ZM42 47V339.5H52V47H42ZM67 364.5H460V354.5H67V364.5ZM485 339.5V67H475V339.5H485ZM460 42H273V52H460V42ZM248 67V214H258V67H248ZM273 42C259.193 42 248 53.1929 248 67H258C258 58.7157 264.716 52 273 52V42ZM485 67C485 53.1929 473.807 42 460 42V52C468.284 52 475 58.7157 475 67H485ZM460 364.5C473.807 364.5 485 353.307 485 339.5H475C475 347.784 468.284 354.5 460 354.5V364.5ZM42 339.5C42 353.307 53.1929 364.5 67 364.5V354.5C58.7157 354.5 52 347.784 52 339.5H42Z" fill="white"/>
              </g>
              <defs>
                <filter id="filter0_d_200_49" x="0.333008" y="0.333313" width="504.667" height="384.167" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
                  <feFlood flood-opacity="0" result="BackgroundImageFix"/>
                  <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
                  <feOffset/>
                  <feGaussianBlur stdDeviation="10"/>
                  <feComposite in2="hardAlpha" operator="out"/>
                  <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
                  <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_200_49"/>
                  <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_200_49" result="shape"/>
                </filter>
              </defs>
            </svg>
            <p data-fade>Drawing upon ancient magic, you can protect the crystal by casting spells in the air. Try drawing this shape to destroy the demon. </p>
          </div>
        </div>

        <div data-screen="INSTRUCTIONS_SPELLS" class="instructions-spells">
          <div class="content">
            <p data-fade>You possess three potent spells: Arcane, Fire, and Vortex. Each wields unique power, but beware, they take time to recharge.</p>
            <p data-fade>Now, stand tall and protect the crystal. The fate of our world rests in your hands.</p>
            <button data-fade data-send="next">Start</button>
          </div>
        </div>

        <div data-screen="PAUSED" class="paused">
          <div class="content">
            <button data-fade data-send="resume">Resume</button>
            <button data-fade class="simple" data-send="end">Back to menu</button>
          </div>
        </div>

        <div data-screen="SPELL_OVERLAY" class="spell-overlay">
          <div class="content">
            <button data-fade data-send="close">Close</button>
          </div>
        </div>

        <div data-screen="GAME_OVER" class="game-over">
          <div class="content">
            <h2 data-fade data-split>Game Over</h2>
            <button data-fade data-send="restart">Try again</button>
            <ul class="button-row">
              <li><button data-fade class="simple" data-send="instructions">Instructions</button></li>
              <li><button data-fade class="simple" data-send="endless">Endless mode</button></li>
              <li><button data-fade class="simple" data-send="credits">Credits</button></li>
            </ul>
          </div>
        </div>

        <div data-screen="WINNER" class="winner">
          <div class="content">
            
            <h2 data-fade>You did it!</h2>
            <button data-fade data-send="restart">Play again</button>
            <ul class="button-row">
              <li><button data-fade class="simple" data-send="instructions">Instructions</button></li>
              <li><button data-fade class="simple" data-send="endless">Endless mode</button></li>
              <li><button data-fade class="simple" data-send="credits">Credits</button></li>
            </ul>
          </div>
        </div>
      </div>
    </div>

    <!-- 
    
    DEBUG SCREENS AND OVERLAYS. 
    THESE ONLY SHOW IF THEY ARE ENABLED IN JS 
    
    -->

    <div class="debug-overlays">
      <svg class="overlay" id="spell-helper" style="display: none">
        <path id="spell-path" />
        <g id="spell-points"></g>
      </svg>
    </div>

    <div class="debug-panels">
      <div id="fps" class="panel"  style="display: none"></div>
      
      <div id="health-states" class="panel" style="display: none">
        <div class="health-bar"></div>
      </div>

      <div id="app-state" class="panel" style="display: none">
        <div class="state"></div>
        <div class="controls"></div>
      </div>


      <div id="endless-mode" class="panel"  style="display: none">
        <p>Endless Mode</p>
      </div>

      <div class="panel" id="spell-stats" style="display: none">
        <div class="spell-stat" data-spell-shape="spell-shape-arcane">
          <h2>Arcane</h2>
          <div>
            <svg class="spell-preview" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113 86" fill="none">
              <use href="#spell-shape-arcane" />
            </svg>
            <div class="score">0</div>
          </div>
        </div>
        <div class="spell-stat" data-spell-shape="spell-shape-fire">
          <h2>Fire</h2>
          <div>
            <svg class="spell-preview" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 145 73" fill="none">
              <use href="#spell-shape-fire" />
            </svg>
            <div class="score">0</div>
          </div>
        </div>
        <div class="spell-stat" data-spell-shape="spell-shape-vortex">
          <h2>Vortex</h2>
          <div>
            <svg class="spell-preview" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 111" fill="none">
              <use href="#spell-shape-vortex" />
            </svg>
            <div class="score">0</div>
          </div>
        </div>
        
          
        
      
    </div>
    <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/gsap.3.9.1.js"></script>
  <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/MotionPathPlugin.js"></script>
  <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/Flip.3.10.5.js"></script>
      <script  type="module">

gsap.registerPlugin(MotionPathPlugin, Flip)

import { createNoise3D } from "//repo.bfw.wiki/bfwrepo/js/module/simplex-noise/simplex-noise.js"
const noise3D = createNoise3D()

import {
  AnimationMixer,
  Clock,
  PointLight,
  AmbientLight,
  ColorManagement,
  DirectionalLight,
  Group,
  LinearSRGBColorSpace,
  Mesh,
  PCFSoftShadowMap,
  PerspectiveCamera,
  ReinhardToneMapping,
  Scene,
  ShaderMaterial,
  WebGLRenderer,
  Color,
  Raycaster,
  ArrowHelper,
  Box3,
  Box3Helper,
  ConeGeometry,
  DoubleSide,
  MeshBasicMaterial,
  MeshMatcapMaterial,
  Plane,
  Vector2,
  AdditiveBlending,
  BufferAttribute,
  CustomBlending,
  OneFactor,
  Points,
  ZeroFactor,
  AxesHelper,
  BufferGeometry,
  TubeGeometry,
  CatmullRomCurve3,
  Vector3,
  PlaneGeometry,
  Audio,
  AudioListener,
  SphereGeometry,
  LoadingManager,
  TextureLoader,
  AudioLoader,
} from "three"

// import { lerp } from "https://cdn.skypack.dev/three@0.152.0/src/math/MathUtils"
import { OrbitControls } from "three/addons/controls/OrbitControls.js"
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"
import { RenderPass } from "three/addons/postprocessing/RenderPass.js"
import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js"
import { GUI } from "three/addons/libs/lil-gui.module.min.js"
import Stats from "three/addons/libs/stats.module.js"
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js"

import { interpret, createMachine } from "//repo.bfw.wiki/bfwrepo/js/module/xstate.js"

// DEBUG

window.DEBUG = {
  /* 
    Show FPS meter.
  */
  fps: false,
  /*
    Show current app state
    and available state actions
    as defined in the app-machine
  */
  appState: false,
  /* 
    Show information about the 
    the available spells, the current
    spell being drawn and confidence
    score for each
  */
  casting: false,
  /* 
    Add yellow arrows that show the 
    particle sim flow field, this is 
    a vector grid that applies directional
    and speed influence on each particle
  */
  simFlow: false,
  /* 
    The flow field also has noise
    applied to it. This shows those
    values with red arrows.
  */
  simNoise: false,
  /* 
    Some particles are invisible
    and just there to apply force
    to the flow field. Setting this
    to true renders them as red 
    particles
  */
  forceParticles: false,
  locations: false,
  entrances: false,
  lights: false,
  trail: false,
  disableSounds: false,
  allowLookAtMoveWhenPaused: false,
  layoutDebug: false,
}

//  CONSTS

export const AXIS = ["x", "y", "z"]

export const PARTICLE_STYLES = {
  invisible: 0,
  smoke: 1,
  plus: 2,
  soft: 3,
  point: 4,
  circle: 5,
  flame: 6,
}

export const SPELLS = {
  arcane: "arcane",
  fire: "fire",
  vortex: "vortex",
}

const DEFAULT_EMITTER_SETTINGS = {
  startingPosition: { x: 0.5, y: 0.5, z: 0.5 },
  startingDirection: { x: 0, y: 0, z: 0 },
  emitRate: 0.001,
  particleOrder: [],
  model: null,
  animationDelay: 0,
  lightColor: { r: 1, g: 1, b: 1 },
  group: "magic",
}

const DEFAULT_ENEMY_SETTINGS = {
  position: { x: 0, y: 0, z: 0 },
  model: null,
  animationDelay: 0,
}

const DEFAULT_PARTICLE_SETTINGS = {
  speed: 0.2,
  speedDecay: 0.6,
  speedSpread: 0,
  force: 0.2,
  forceDecay: 0.1,
  forceSpread: 0,
  life: 1,
  lifeDecay: 0.6,
  directionSpread: { x: 0.001, y: 0.001, z: 0.001 },
  positionSpread: { x: 0.01, y: 0.01, z: 0.01 },
  color: { r: 1, g: 1, b: 1 },
  scale: 1,
  scaleSpread: 0,
  style: PARTICLE_STYLES.soft,
  acceleration: 0.1,
}

const DOM = {
  body: document.body,
  app: document.querySelector(".app"),
  state: document.querySelector(".state"),
  controls: document.querySelector(".controls"),
  canvas: document.querySelector(".canvas"),
  svg: document.querySelector("#spell-helper"),
  demonCount: document.querySelector("[data-demon-count]"),
  spellGuide: document.querySelector("#spell-guide"),
}

const ENEMY_SETTINGS = {
  lastSent: 0,
  sendFrequency: 5,
  sendFrequencyReduceBy: 0.2,
  minSendFrequency: 2,
  totalSend: 42,
  sendCount: 0,
  killCount: 0,
}

const T0_LOAD = {
  models: [
    { id: "room", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/room.glb", scale: 0.15, position: [0.03, -0.26, -0.55] },
    { id: "demon", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/demon.glb", scale: 0.1, position: [0, 0, 0] },
    { id: "crystal", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/crystal.glb", scale: 0.05, position: [0, 0, 0] },
  ],
  sounds: [
    { id: "music", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/music.mp3" },
    { id: "kill-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/kill-1.mp3" },
    { id: "kill-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/kill-2.mp3" },
    { id: "kill-3", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/kill-3.mp3" },
    { id: "enter-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/enter-1.mp3" },
    { id: "enter-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/enter-2.mp3" },
    { id: "error-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/error-1.mp3" },
    { id: "cast-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/cast-1.mp3" },
    { id: "cast-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/cast-2.mp3" },
    { id: "ping-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/ping-1.mp3" },
    { id: "ping-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/ping-2.mp3" },
    { id: "laugh-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/laugh-1.mp3" },
    { id: "laugh-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/laugh-2.mp3" },
    { id: "laugh-3", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/laugh-3.mp3" },
    { id: "spell-travel-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-travel-1.mp3" },
    { id: "spell-travel-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-travel-2.mp3" },
    { id: "spell-travel-3", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-travel-3.mp3" },
    { id: "spell-failed-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-failed-1.mp3" },
    { id: "spell-failed-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-failed-2.mp3" },
    { id: "trapdoor-close-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/trapdoor-close-1.mp3" },
    { id: "trapdoor-close-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/trapdoor-close-2.mp3" },
    { id: "torch-1", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/torch-1.mp3" },
    { id: "torch-2", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/torch-2.mp3" },
    { id: "torch-3", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/torch-3.mp3" },
    { id: "crystal-explode", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/crystal-explode.mp3" },
    { id: "crystal-reform", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/crystal-reform.mp3" },
    { id: "glitch", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/glitch.mp3" },
    { id: "portal", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/portal.mp3" },
    { id: "crumble", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/crumble.mp3" },
    { id: "reform", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/reform.mp3" },
  ],
  textures: [
    { id: "magic-particles", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/magic-particles.png" },
    { id: "smoke-particles", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/smoke-particles.png" },
    { id: "spell-arcane", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/spell-arcane.png" },
    { id: "crystal-matcap", file: "//repo.bfw.wiki/bfwrepo/threemodel/gameholly/crystal-matcap.png" },
  ],
}

const DEFAULT_PARTICLE_MATERIAL_SETTINGS = {
  depthWrite: false,
  vertexColors: true,
  vertexShader: `
	
uniform float uSize;
uniform float uTime;
uniform bool uGrow;

attribute float scale;
attribute float life;
attribute float type;
attribute vec3 random;

varying vec3 vColor;
varying float vLife;
varying float vType;
varying vec3 vRandom;


void main()
{
    /**
     * Position
     */
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    float spiralRadius = 0.1;
    // projectedPosition = projectedPosition + vec4(cos(uTime * random.x) * spiralRadius, sin(uTime * random.y) * spiralRadius, 0.0, 0.0);
    gl_Position = projectedPosition;

    

    vColor = color;
    vRandom = random;
    vLife = life;
    vType = type;

    /**
     * Size
     */
    if(uGrow) {
      gl_PointSize = uSize * scale * (2.5 - life);
    }
    else {
      gl_PointSize = uSize * scale * life;
    }
    
    gl_PointSize *= (1.0 / - viewPosition.z);
}
	`,
}

const PROPERTIES = {
  vec3: ["position", "direction", "random", "color"],
  float: ["type", "speed", "speedDecay", "force", "forceDecay", "acceleration", "life", "lifeDecay", "size"],
}

// STATE MACHINES

const AppMachine = createMachine(
  {
    id: "App",
    initial: "IDLE",
    states: {
      IDLE: {
        on: {
          load: {
            target: "LOADING",
            internal: false,
          },
        },
      },
      LOADING: {
        on: {
          error: {
            target: "LOAD_ERROR",
            internal: false,
          },
          complete: {
            target: "INIT",
            internal: false,
          },
        },
      },
      LOAD_ERROR: {
        on: {
          reload: {
            target: "LOADING",
            internal: false,
          },
        },
      },
      INIT: {
        on: {
          begin: {
            target: "TITLE_SCREEN",
            internal: false,
          },
        },
      },
      TITLE_SCREEN: {
        on: {
          next: {
            target: "INSTRUCTIONS_CRYSTAL",
            internal: false,
          },
          skip: {
            target: "SETUP_GAME",
            internal: false,
          },
          credits: {
            target: "CREDITS",
            internal: false,
          },
          endless: {
            target: "SETUP_ENDLESS",
            internal: false,
          },
          debug: {
            target: "SCENE_DEBUG",
          },
        },
      },
      INSTRUCTIONS_CRYSTAL: {
        on: {
          next: {
            target: "INSTRUCTIONS_DEMON",
            internal: false,
          },
        },
      },
      SETUP_GAME: {
        on: {
          run: {
            target: "GAME_RUNNING",
            internal: false,
          },
        },
      },
      CREDITS: {
        on: {
          close: {
            target: "TITLE_SCREEN",
            internal: false,
          },
          end: {
            target: "TITLE_SCREEN",
          },
        },
      },
      SETUP_ENDLESS: {
        on: {
          run: {
            target: "ENDLESS_MODE",
            internal: false,
          },
        },
      },
      SCENE_DEBUG: {
        on: {
          close: {
            target: "TITLE_SCREEN",
          },
        },
      },
      INSTRUCTIONS_DEMON: {
        on: {
          next: {
            target: "INSTRUCTIONS_CAST",
            internal: false,
          },
        },
      },
      GAME_RUNNING: {
        on: {
          pause: {
            target: "PAUSED",
            internal: false,
          },
          "game-over": {
            target: "GAME_OVER_ANIMATION",
            internal: false,
          },
          spells: {
            target: "SPELL_OVERLAY",
            internal: false,
          },
          win: {
            target: "WIN_ANIMATION",
          },
          special: {
            target: "SPECIAL_SPELL",
          },
        },
      },
      ENDLESS_MODE: {
        on: {
          end: {
            target: "CLEAR_ENDLESS",
            internal: false,
          },
          pause: {
            target: "ENDLESS_PAUSE",
            internal: false,
          },
          spells: {
            target: "ENDLESS_SPELL_OVERLAY",
          },
          special: {
            target: "ENDLESS_SPECIAL_SPELL",
          },
        },
      },
      INSTRUCTIONS_CAST: {
        on: {
          next: {
            target: "INSTRUCTIONS_SPELLS",
            internal: false,
          },
        },
      },
      PAUSED: {
        on: {
          resume: {
            target: "GAME_RUNNING",
            internal: false,
          },
          end: {
            target: "CLEAR_GAME",
          },
        },
      },
      GAME_OVER_ANIMATION: {
        on: {
          end: {
            target: "GAME_OVER",
            internal: false,
          },
        },
      },
      SPELL_OVERLAY: {
        on: {
          close: {
            target: "GAME_RUNNING",
            internal: false,
          },
        },
      },
      WIN_ANIMATION: {
        on: {
          end: {
            target: "WINNER",
          },
        },
      },
      SPECIAL_SPELL: {
        on: {
          complete: {
            target: "GAME_RUNNING",
          },
          win: {
            target: "WIN_ANIMATION",
          },
        },
      },
      CLEAR_ENDLESS: {
        on: {
          end: {
            target: "TITLE_SCREEN",
            internal: false,
          },
        },
      },
      ENDLESS_PAUSE: {
        on: {
          end: {
            target: "CLEAR_ENDLESS",
            internal: false,
          },
          resume: {
            target: "ENDLESS_MODE",
            internal: false,
          },
        },
      },
      ENDLESS_SPELL_OVERLAY: {
        on: {
          close: {
            target: "ENDLESS_MODE",
          },
        },
      },
      ENDLESS_SPECIAL_SPELL: {
        on: {
          complete: {
            target: "ENDLESS_MODE",
          },
        },
      },
      INSTRUCTIONS_SPELLS: {
        on: {
          next: {
            target: "SETUP_GAME",
            internal: false,
          },
        },
      },
      CLEAR_GAME: {
        on: {
          end: {
            target: "TITLE_SCREEN",
            internal: false,
          },
        },
      },
      GAME_OVER: {
        on: {
          restart: {
            target: "SETUP_GAME",
            internal: false,
          },
          instructions: {
            target: "RESETTING_FOR_INSTRUCTIONS",
            internal: false,
          },
          credits: {
            target: "RESETTING_FOR_CREDITS",
            internal: false,
          },
          endless: {
            target: "SETUP_ENDLESS",
            internal: false,
          },
        },
      },
      WINNER: {
        on: {
          restart: {
            target: "SETUP_GAME",
          },
          instructions: {
            target: "INSTRUCTIONS_CRYSTAL",
          },
          credits: {
            target: "CREDITS",
          },
          endless: {
            target: "SETUP_ENDLESS",
          },
        },
      },
      RESETTING_FOR_INSTRUCTIONS: {
        on: {
          run: {
            target: "INSTRUCTIONS_CRYSTAL",
          },
        },
      },
      RESETTING_FOR_CREDITS: {
        on: {
          run: {
            target: "CREDITS",
          },
        },
      },
    },
    predictableActionArguments: true,
    preserveActionOrder: true,
  },
  {
    actions: {},
    services: {},
    guards: {},
    delays: {},
  }
)

const CasterMachine = createMachine(
  {
    id: "Caster",
    initial: "IDLE",
    states: {
      IDLE: {
        on: {
          ready: {
            target: "INACTIVE",
          },
        },
      },
      INACTIVE: {
        on: {
          activate: {
            target: "ACTIVE",
          },
        },
      },
      ACTIVE: {
        on: {
          start_cast: {
            target: "CASTING",
          },
          deactivate: {
            target: "INACTIVE",
          },
        },
      },
      CASTING: {
        on: {
          finished: {
            target: "PROCESSING",
          },
          deactivate: {
            target: "INACTIVE",
          },
        },
      },
      PROCESSING: {
        on: {
          success: {
            target: "SUCCESS",
          },
          fail: {
            target: "FAIL",
          },
          deactivate: {
            target: "INACTIVE",
          },
        },
      },
      SUCCESS: {
        on: {
          complete: {
            target: "ACTIVE",
          },
          deactivate: {
            target: "INACTIVE",
          },
        },
      },
      FAIL: {
        on: {
          complete: {
            target: "ACTIVE",
          },
          deactivate: {
            target: "INACTIVE",
          },
        },
      },
    },
    predictableActionArguments: true,
    preserveActionOrder: true,
  },
  {
    actions: {},
    services: {},
    guards: {},
    delays: {},
  }
)

const CrystalMachine = createMachine(
  {
    id: "Crystal",
    initial: "IDLE",
    states: {
      IDLE: {
        on: {
          start: {
            target: "INIT",
          },
        },
      },
      INIT: {
        on: {
          ready: {
            target: "WHOLE",
          },
        },
      },
      WHOLE: {
        on: {
          overload: {
            target: "OVERLOADING",
          },
        },
      },
      OVERLOADING: {
        on: {
          break: {
            target: "BREAKING",
          },
        },
      },
      BREAKING: {
        on: {
          broke: {
            target: "BROKEN",
          },
        },
      },
      BROKEN: {
        on: {
          fix: {
            target: "FIXING",
          },
        },
      },
      FIXING: {
        on: {
          fixed: {
            target: "WHOLE",
          },
        },
      },
    },
    predictableActionArguments: true,
    preserveActionOrder: true,
  },
  {
    actions: {},
    services: {},
    guards: {},
    delays: {},
  }
)

const EnemyMachine = createMachine(
  {
    id: "Enemy",
    initial: "IDLE",
    states: {
      IDLE: {
        on: {
          spawn: {
            target: "ANIMATING_IN",
            internal: false,
          },
        },
      },
      ANIMATING_IN: {
        on: {
          complete: {
            target: "ALIVE",
            internal: false,
          },
          accend: {
            target: "ACCEND",
            internal: false,
          },
        },
      },
      ALIVE: {
        on: {
          incoming: {
            target: "TAGGED",
            internal: false,
          },
          accend: {
            target: "ACCEND",
            internal: false,
          },
          vortex: {
            target: "VORTEX_ANIMATION",
          },
        },
      },
      ACCEND: {
        on: {
          leave: {
            target: "GONE",
            internal: false,
          },
        },
      },
      TAGGED: {
        on: {
          kill: {
            target: "ANIMATING_OUT",
            internal: false,
          },
          accend: {
            target: "ANIMATING_OUT",
          },
        },
      },
      VORTEX_ANIMATION: {
        on: {
          complete: {
            target: "DEAD",
          },
        },
      },
      GONE: {},
      ANIMATING_OUT: {
        on: {
          complete: {
            target: "DEAD",
            internal: false,
          },
        },
      },
      DEAD: {},
    },
    predictableActionArguments: true,
    preserveActionOrder: true,
  },
  {
    actions: {},
    services: {},
    guards: {},
    delays: {},
  }
)

// UTILS

const degToRad = (value) => {
  return (value * Math.PI) / 180
}

const simplelerp = (start, end, amount) => {
  return start + amount * (end - start)
}

const randomFromArray = (arr) => {
  if (!arr || !arr.length) return null
  return arr[Math.floor(Math.random() * arr.length)]
}

// VECTOR UTILS

const lerpVectors = (start, end, amount) => {
  // return {
  //   x: lerp(start.x, end.x, amount),
  //   y: lerp(start.y, end.y, amount),
  //   z: lerp(start.z, end.z, amount),
  // }

  return {
    x: start.x + (end.x - start.x) * amount,
    y: start.y + (end.y - start.y) * amount,
    z: start.z + (end.z - start.z) * amount,
  }
  // return this;
}

const multiplyScalar = (vector, amount) => {
  return {
    x: vector.x * amount,
    y: vector.y * amount,
    z: vector.z * amount,
  }
}

const divideScalar = (vector, amount) => {
  return multiplyScalar(vector, 1 / amount)
}

const add = (a, b) => {
  return {
    x: a.x + b.x,
    y: a.y + b.y,
    z: a.z + b.z,
  }
}

const normalize = (vector) => {
  const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)

  return {
    x: vector.x / length,
    y: vector.y / length,
    z: vector.z / length,
  }
}

const clamp = (vector, min, max) => {
  vector.x = Math.max(min.x, Math.min(max.x, vector.x))
  vector.y = Math.max(min.y, Math.min(max.y, vector.y))
  vector.z = Math.max(min.z, Math.min(max.z, vector.z))

  return vector
}

const length = (vector) => {
  return Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
}

const clampLength = (vector, min, max) => {
  const l = length(vector)

  const divided = divideScalar(vector, l || 1)
  return multiplyScalar(divided, Math.max(min, Math.min(max, l)))
}

const vector = {
  lerpVectors,
  multiplyScalar,
  divideScalar,
  add,
  normalize,
  clamp,
  length,
  clampLength,
}

const math = {
  degToRad,
}

// EMITTERS

class ControlledEmitter {
  constructor(sim) {
    this.sim = sim
    this.particles = {}
    this.remainingTime = 0
    this.active = false

    this.gsapDefaults = {
      onUpdate: this.update,
      onUpdateProperties: [],
    }
  }

  emit(particle = {}) {
    const newParticle = {
      size: 1,
      color: { r: 1, g: 1, b: 1 },
      position: { z: 0.5, y: 0.35, x: 0.5 },
      life: 0.9,
      style: PARTICLE_STYLES.point,
      ...particle,
      lifeDecay: 0,
      speed: 0,
      speedDecay: 0,
      force: 0,
      forceDecay: 0,
      acceleration: 0,
    }

    const particleIndex = this.sim.createParticle("magic", newParticle)

    this.particles[particleIndex] = {
      index: particleIndex,
      ...newParticle.color,
      ...newParticle.position,
      size: newParticle.size,
      life: newParticle.life,
      lastPosition: null,
      animation: null,
    }

    return this.particles[particleIndex]
  }

  update(particle) {
    const { index, size, life } = particle

    const color = { r: particle.r, g: particle.g, b: particle.b }
    const position = { x: particle.x, y: particle.y, z: particle.z }

    const updates = ["size", "life", "color", "position"]
    const values = { size, life, color, position }

    for (let i = 0; i < updates.length; i++) {
      const update = updates[i]
      this.sim.setParticleProperty("magic", index, update, values[update])
    }

    // this.sim()
  }

  destory(particle) {
    // this.sim
    this.sim.setParticleProperty("magic", particle.index, "life", 0)
    delete this.particles[particle.index]
  }

  release(particle) {
    //this.sim.setParticleProperty("magic", particle.index, "force", 0)
    delete this.particles[index]
    // this.sim
  }
}

class Emitter {
  constructor(sim, emitterSettings, particleTypes, light) {
    this.sim = sim
    this.light = light
    this.active = true
    this.animations = []
    this.settings = { ...DEFAULT_EMITTER_SETTINGS, ...emitterSettings }
    this.delay = this.settings.animationDelay

    if (this.light) {
      gsap.killTweensOf(this.light)
      this.light.color.setRGB(this.settings.lightColor.r, this.settings.lightColor.g, this.settings.lightColor.b)
      this.light.intensity = 3
      // this.animations.push(gsap.to(this.light, { intensity: 5, duration: this.delay }))
    }

    this.particles = { ...particleTypes }

    // particleSettings.forEach((settings) => {
    //   this.particles.push({ ...DEFAULT_PARTICLE_SETTINGS, ...settings })
    // })

    this.position = { ...this.settings.startingPosition }
    // this.previousPosition = { ...this.settings.startingPosition }
    this.direction = { ...this.settings.startingDirection }
    this.remainingTime = 0
    this.destroyed = false
    this.modelScale = 1

    this.count = 0

    this.moveFunction()

    if (this.settings.model) {
      this.model = this.settings.model
      // this.model.group.rotation.y = Math.PI * 0.5
      if (this.model.animations && this.model.animations.length) {
        this.mixer = new AnimationMixer(this.model.scene)
        this.mixer.timeScale = 1.3
        this.mixer.clipAction(this.model.animations[0]).play()
      }
    }
  }

  moveFunction = (delta, elapsedTime) => {
    if (this.model) {
      // this.model.group.scale.set(this.modelScale, this.modelScale, this.modelScale)
    }

    if (this.light)
      this.light.position.set(
        this.position.x * this.sim.size.x,
        this.position.y * this.sim.size.y,
        this.position.z * this.sim.size.z
      )
  }

  pause() {
    this.animations.map((animation) => animation.pause())
  }

  resume() {
    this.animations.map((animation) => animation.resume())
  }

  destory() {
    if (this.model) {
      this.model.group.parent.remove(this.model.group)
      // this.model.group.traverse((obj) => {
      //   if (obj.geometry) obj.geometry.dispose()
      //   if (obj.material) obj.material.dispose()
      // })
      this.model = null
    }

    if (this.light) {
      // this.light.intensity = 0
      this.animations.push(gsap.fromTo(this.light, { intensity: 15 }, { intensity: 0, ease: "power1.in", duration: 1 }))
    }

    this.destroyed = true
  }

  emit(particle, group, casted = false) {
    if (!group) group = this.settings.group

    const positionAlongLine = this.previousPosition
      ? vector.lerpVectors(this.previousPosition, this.position, Math.random())
      : this.position

    const position = {
      x: positionAlongLine.x + (Math.random() * 2 - 1) * particle.positionSpread.x,
      y: positionAlongLine.y + (Math.random() * 2 - 1) * particle.positionSpread.y,
      z: positionAlongLine.z + (Math.random() * 2 - 1) * particle.positionSpread.z,
    }

    let direction = {}

    // console.log("direction", particle.direction)
    if (!particle.direction) {
      direction = {
        x: Math.random() * 2 - 1,
        y: Math.random() * 2 - 1,
        z: Math.random() * 2 - 1,
      }
    } else {
      direction = {
        x: this.direction.x * particle.direction.x + (Math.random() * 2 - 1) * particle.directionSpread.x,
        y: this.direction.y * particle.direction.y + (Math.random() * 2 - 1) * particle.directionSpread.y,
        z: this.direction.z * particle.direction.z + (Math.random() * 2 - 1) * particle.directionSpread.z,
      }
    }

    // console.log("direction", direction)

    const speed = particle.speed + Math.random() * particle.speedSpread
    const force = particle.force + Math.random() * particle.forceSpread
    const scale = particle.scale * (particle.scaleSpread > 0 ? Math.random() * particle.scaleSpread : 1)

    // console.log(particle)

    this.sim.createParticle(group, {
      ...particle.settings,
      position,
      direction,
      speed,
      force,
      scale,
      casted,
    })
  }

  tick(delta, elapsedTime) {
    if (this.active && this.settings.emitRate > 0) {
      this.remainingTime += delta
      if (this.mixer) this.mixer.update(delta * this.mixer.timeScale)
      if (this.moveFunction) this.moveFunction(delta, elapsedTime)

      const emitCount = Math.floor(this.remainingTime / this.settings.emitRate)
      // console.logLimited(emitCount)
      this.remainingTime -= emitCount * this.settings.emitRate

      for (let i = 0; i < emitCount; i++) {
        this.emit(this.particles[this.settings.particleOrder[this.count % this.settings.particleOrder.length]])

        this.count++
      }
    }

    this.previousPosition = { ...this.position }
  }
}

class ArcaneSpellEmitter extends Emitter {
  constructor(sim, light, startPosition, enemy) {
    const color = { r: 0.2, g: 0, b: 1 }

    const settings = {
      // model: ASSETS.getModel("parrot"),
      emitRate: 0.001,
      animationDelay: 1,

      startingPosition: startPosition,
      lightColor: color,
      particleOrder: [
        "smoke",
        "smoke",
        "smoke",
        "smoke",
        "smoke",
        "circle",
        // "smoke",
        // "smoke",
        "circle",
        // "smoke",
        // "smoke",
        // "smoke",
        // "smoke",
        // "sparkle",
      ],
    }

    const particles = {
      smoke: new SpellTrailParticle({
        color,
      }),
      sparkle: new SpellTrailParticle({
        style: PARTICLE_STYLES.point,
        scale: 0.1,
      }),
      circle: new SpellTrailParticle({
        color,
        style: PARTICLE_STYLES.disc,
        // scale: 4,
      }),
      explodeSmoke: new ExplodeParticle({ color }),
      explodeSpark: new ExplodeParticle({
        speed: 0.4,
        color: { r: 1, g: 1, b: 1 },
        // force: 2,
        forceDecay: 2,
        style: PARTICLE_STYLES.point,
      }),
      explodeShape: new ExplodeParticle({
        color,
        style: PARTICLE_STYLES.disc,
        scale: 0.5,
      }),
    }

    super(sim, settings, particles, light)

    this.active = false
    this.enemy = enemy
    // console.log("startPostion", startPosition)

    this.particleOrder = ["smoke"]

    // this.lastPosition = { ...this.settings.startingPosition, z: this.settings.startingPosition.z + 0.001 }
    this.lookAt = { x: 0, y: 0, z: 0 }
    this.lookAtTarget = null
    // this.scale = 1
    this.scale = 0.1

    SOUNDS.play("spell-travel")
    this.animations.push(
      gsap.to(
        this.position,

        {
          duration: 0.9,
          delay: this.delay,
          motionPath: {
            curviness: 1.5,
            // resolution: 6,
            path: [
              this.position,
              { x: 0.5, y: Math.random(), z: 0.8 },
              { x: 0.1 + Math.random() * 0.8, y: 0.2 + Math.random() * 0.4, z: 0.4 },
              this.enemy
                ? { x: this.enemy.location.x, y: 0.4, z: this.enemy.location.z }
                : { x: 0.1 + Math.random() * 0.8, y: 1, z: 0.2 },
            ],
          },
          ease: "linear",
          onStart: () => {
            if (this.enemy) this.enemy.incoming()
          },
          onComplete: () => this.onComplete(),
          onUpdate: () => this.onUpdate(),
        }
      )
    )

    // gsap.to(this, { duration: 0.3, scale: 1, ease: "linear" })

    // gsap.to(this.settings.directionSpread, { duration: 1, x: 0.001, y: 0.001, z: 0.001, ease: "power4.in" })
  }

  onComplete = () => {
    if (this.enemy) {
      this.enemy.kill()
      SOUNDS.play("kill")
      const explode = 200
      for (let i = 0; i < explode; i++) {
        const random = Math.random()
        if (random > 0.55) this.emit(this.particles["explodeSmoke"])
        else if (random > 0.1) this.emit(this.particles["explodeSpark"])
        else this.emit(this.particles["explodeShape"])
      }
    }
    this.destory()
  }

  onUpdate = () => {
    if (this.lastPosition) {
      this.direction = {
        x: this.position.x - this.lastPosition.x,
        y: this.position.y - this.lastPosition.y,
        z: this.position.z - this.lastPosition.z,
      }

      if (this.model) {
        this.model.group.position.set(
          this.position.x * this.sim.size.x,
          this.position.y * this.sim.size.y,
          this.position.z * this.sim.size.z
        )

        this.lookAtTarget = {
          x: this.sim.startCoords.x + (this.position.x + this.direction.x) * this.sim.size.x,
          y: this.sim.startCoords.y + (this.position.y + this.direction.y) * this.sim.size.y,
          z: this.sim.startCoords.z + (this.position.z + this.direction.z) * this.sim.size.z,
        }

        const lerpAmount = 0.08

        this.lookAt.x = lerp(this.lookAt.x, this.lookAtTarget.x, lerpAmount)
        this.lookAt.y = lerp(this.lookAt.y, this.lookAtTarget.y, lerpAmount)
        this.lookAt.z = lerp(this.lookAt.z, this.lookAtTarget.z, lerpAmount)

        this.model.group.lookAt(this.lookAt.x, this.lookAt.y, this.lookAt.z)
      }
    }
    this.active = true
    this.lastPosition = { ...this.position }
  }
}

class CastEmitter extends Emitter {
  constructor(sim) {
    const settings = {
      emitRate: 0,
      particleOrder: ["sparkle", "sparkle", "sparkle", "sparkle", "smoke"],
    }

    const particles = {
      smoke: new SmokeParticle(),
      sparkle: new SparkleParticle(),
    }

    super(sim, settings, particles)
  }

  move(position) {
    this.position = position

    const emitCount = 10
    for (let i = 0; i < emitCount; i++) {
      this.emit(this.particles[randomFromArray(this.settings.particleOrder)], "magic", true)
    }

    this.previousPosition = { ...this.position }
  }

  reset() {
    this.previousPosition = null
  }
}

class CrystalEnergyEmitter extends ControlledEmitter {
  constructor(sim) {
    super(sim)

    this.emitRate = 0.2
    this._active = false
  }

  tick(delta, elapsedTime) {
    if (this.active && this.emitRate > 0) {
      this.remainingTime += delta
      const emitCount = Math.floor(this.remainingTime / this.emitRate)
      this.remainingTime -= emitCount * this.emitRate

      for (let i = 0; i < emitCount; i++) {
        const particle = this.emit({
          life: 1,
          size: 0.4,
          color: { r: 0.8, g: Math.random(), b: 1 },
          position: {
            x: 0.5 + (Math.random() * 0.05 - 0.025),
            y: 0.5 + (Math.random() * 0.05 - 0.025),
            z: 0.5 + (Math.random() * 0.05 - 0.025),
          },
        })

        particle.animation = gsap.to(particle, {
          y: 0.35,
          x: 0.5,
          z: 0.5,
          duration: 2,
          life: 0.5,
          ease: "power4.in",
          onUpdate: () => this.update(particle),
          onComplete: () => this.destory(particle),
        })
      }
    }
  }

  set active(value) {
    this.remainingTime = 0
    this._active = value
  }

  get active() {
    return this._active
  }
}

class DustEmitter extends Emitter {
  constructor(sim, assets) {
    const settings = {
      emitRate: 0.05,
      particleOrder: ["dust"],
    }

    const particles = {
      dust: new DustParticle(),
    }

    super(sim, settings, particles)

    const startCount = 5
    for (let i = 0; i < startCount; i++) {
      this.emit(this.particles["dust"], "smoke")
    }
  }
}

class EnemyEnergyEmitter extends ControlledEmitter {
  constructor(sim, location) {
    super(sim)

    this.location = location
    this.emitRate = 0.05
    // this.active = true
  }

  start() {
    this.active = true
  }

  stop() {
    this.active = false
  }

  tick(delta, elapsedTime) {
    if (this.active && this.emitRate > 0) {
      this.remainingTime += delta
      const emitCount = Math.floor(this.remainingTime / this.emitRate)
      // console.logLimited(emitCount)
      this.remainingTime -= emitCount * this.emitRate

      for (let i = 0; i < emitCount; i++) {
        const particle = this.emit({
          life: 1,
          size: 0.3 + Math.random() * 0.1,
          style: Math.random() > 0.5 ? PARTICLE_STYLES.plus : PARTICLE_STYLES.point,
          color: { r: 0.8, g: Math.random(), b: 1 },
        })

        particle.aniamtion = gsap.to(particle, {
          motionPath: [
            { x: 0.5, y: 0.35, z: 0.5 },
            {
              x: simplelerp(0.5, this.location.x, 0.5) + Math.random() * 0.1,
              y: 0.4 + Math.random() * 0.1,
              z: simplelerp(0.5, this.location.z, 0.5) + Math.random() * 0.1,
            },
            { x: this.location.x, y: 0.3, z: this.location.z },
          ],
          duration: 1 + Math.random() * 0.5,
          life: 0.1,
          ease: "none",
          onUpdate: () => this.update(particle),
          onComplete: () => this.destory(particle),
        })
      }
    }
  }
}

class FireSpellEmitter extends Emitter {
  constructor(sim, light, startPosition, enemy) {
    const settings = {
      // model: ASSETS.getModel("skull"),
      emitRate: 0.01,
      animationDelay: 1,
      startingDirection: { x: 0, y: 1, z: 0 },
      startingPosition: startPosition,
      particleOrder: ["flame"],
      lightColor: { r: 0.9, g: 0.8, b: 0.1 },
    }

    const color = { r: 1, g: 0.8, b: 0 }

    const particles = {
      flame: new FlameParticle({
        scale: 2,
      }),
      ember: {
        speed: 0.5,
        color: { r: 1, g: 0.3, b: 0 },
        speedSpread: 0.3,
        forceSpread: 0,
        direction: { x: 1, y: 1, z: 1 },
        lifeDecay: 1.5,
        force: 0,
        type: 1,
        directionSpread: { x: 0.1, y: 0.1, z: 0.1 },
        positionSpread: { x: 0, y: 0, z: 0 },
        acceleration: 0.02,
      },

      explodeSmoke: new ExplodeParticle({ color, speed: 0.1, forceDecay: 1.1 }),
      explodeSpark: new ExplodeParticle({
        speed: 0.4,
        color: { r: 1, g: 1, b: 1 },
        // force: 2,
        forceDecay: 2,
        // style: PARTICLE_STYLES.point,
      }),
      explodeShape: new ExplodeParticle({
        color,
        style: PARTICLE_STYLES.circle,
        scale: 0.5,
      }),
    }

    super(sim, settings, particles, light)

    this.active = false
    // console.log("startPostion", startPosition)

    this.particleOrder = ["flame"]

    this.lastPosition = { ...this.settings.startingPosition, z: this.settings.startingPosition.z + 0.001 }
    this.lookAt = null
    this.lookAtTarget = null
    // this.scale = 1
    this.scale = 0.1

    if (this.model) {
      this.model.group.rotateX(math.degToRad(-160))
      this.model.group.rotateZ(math.degToRad(-40))
      this.model.group.scale.set(0, 0, 0)
    }

    this.onUpdate(true)
    this.enemy = enemy

    const introDuration = 0.5

    SOUNDS.play("spell-travel")

    if (this.model) {
      this.animations.push(
        gsap.to(this.model.group.scale, {
          motionPath: [
            // { x: 0, y: 0, z: 0 },
            { x: 2, y: 2, z: 2 },
            { x: 1, y: 1, z: 1 },
          ],
          ease: "power1.inOut",
          duration: this.delay + introDuration * 1.2,
        })
      )
      this.animations.push(
        gsap.to(this.model.group.rotation, {
          motionPath: [
            { y: math.degToRad(0), x: math.degToRad(-160), z: math.degToRad(-40) },
            { y: math.degToRad(0), x: math.degToRad(-90), z: math.degToRad(192) },
          ],
          ease: "power1.inOut",
          duration: this.delay + introDuration,
        })
      )
    }

    this.animations.push(
      gsap.to(this.position, {
        duration: 1,
        delay: this.delay + introDuration * 0.25,
        motionPath: {
          curviness: 1.5,
          // resolution: 6,
          path: [
            this.position,
            { x: 0.5, y: Math.random(), z: 0.8 },
            { x: 0.1 + Math.random() * 0.8, y: 0.2 + Math.random() * 0.4, z: 0.4 },
            this.enemy
              ? { x: this.enemy.location.x, y: 0.4, z: this.enemy.location.z }
              : { x: 0.1 + Math.random() * 0.8, y: 1, z: 0.2 },
          ],
        },
        ease: "power1.in",
        onStart: () => {
          if (this.enemy) this.enemy.incoming()
          this.settings.emitRate = 0.005
        },
        onComplete: () => this.onComplete(),
        onUpdate: () => this.onUpdate(),
      })
    )

    // gsap.to(this, { duration: 0.3, scale: 1, ease: "linear" })

    // gsap.to(this.settings.directionSpread, { duration: 1, x: 0.001, y: 0.001, z: 0.001, ease: "power4.in" })
  }

  onComplete = () => {
    if (this.enemy) {
      SOUNDS.play("kill")
      this.enemy.kill()
      const explode = 500
      for (let i = 0; i < explode; i++) {
        const random = Math.random()
        if (random > 0.55) this.emit(this.particles["explodeSmoke"])
        else if (random > 0.1) this.emit(this.particles["explodeSpark"])
        else this.emit(this.particles["explodeShape"])
      }
    }
    this.destory()
  }

  onUpdate = (skipDirection = false) => {
    // if (this.lastPosition) {
    if (!skipDirection)
      this.direction = {
        x: this.position.x - this.lastPosition.x,
        y: this.position.y - this.lastPosition.y,
        z: this.position.z - this.lastPosition.z,
      }

    if (this.model) {
      this.model.group.position.set(
        this.position.x * this.sim.size.x,
        this.position.y * this.sim.size.y,
        this.position.z * this.sim.size.z
      )

      // this.lookAtTarget = {
      //   x: this.sim.startCoords.x + (this.position.x + this.direction.x) * this.sim.size.x,
      //   y: this.sim.startCoords.y + (this.position.y + this.direction.y) * this.sim.size.y,
      //   z: this.sim.startCoords.z + (this.position.z + this.direction.z) * this.sim.size.z,
      // }

      // const lerpAmount = 0.08

      // this.lookAt.x = lerp(this.lookAt.x, this.lookAtTarget.x, lerpAmount)
      // this.lookAt.y = lerp(this.lookAt.y, this.lookAtTarget.y, lerpAmount)
      // this.lookAt.z = lerp(this.lookAt.z, this.lookAtTarget.z, lerpAmount)

      // this.model.group.lookAt(this.lookAt.x, this.lookAt.y, this.lookAt.z)
    }
    // }
    this.active = true
    this.lastPosition = { ...this.position }
  }
}

class GhostEmitter extends Emitter {
  constructor(sim) {
    const settings = {
      emitRate: 0,
      particleOrder: ["trailSmoke"],
      startingDirection: { x: 0, y: -1, z: 0 },
      group: "smoke",
      // direction: { x: -1, y: -1, z: -1 },
    }

    const particles = {
      trailSmoke: new SmokeParticle({
        positionSpread: { x: 0.03, y: 0.03, z: 0.03 },
        directionSpread: { x: 0.3, y: 0, z: 0.3 },
        direction: { x: 1, y: 1, z: 1 },
        force: 0.2,
        speed: 0.3,
        speedDecay: 0.2,
        lifeDecay: 0.8,
        acceleration: 0.1,
        scale: 0.4,
      }),
      smoke: new SmokeParticle({
        color: { r: 0, g: 0, b: 0 },
        positionSpread: { x: 0.05, y: 0, z: 0.05 },
        directionSpread: { x: 0.3, y: 0, z: 0.3 },
        direction: { x: 1, y: 1, z: 1 },
        force: 0,
        speed: 0.3,
        speedDecay: 0.2,
        lifeDecay: 0.4,
        acceleration: 0.1,
        scale: 1,
      }),
      force: new ForceParticle({
        directionSpread: { x: 0.4, y: 0, z: 0.4 },
      }),
      smokeUp: new SmokeParticle({
        color: { r: 0, g: 0, b: 0 },
        positionSpread: { x: 0.1, y: 0.3, z: 0.1 },
        directionSpread: { x: 0.3, y: 0, z: 0.3 },
        direction: { x: -1, y: -1, z: -1 },
        force: 0.2,
        speed: 0.6,
        speedDecay: 0.2,
        lifeDecay: 0.6,
        acceleration: 0,
        scale: 1,
      }),
      sparkle: new SparkleParticle({
        speed: 0.6,
        life: 1.0,
        lifeDecay: 0.7,
        positionSpread: { x: 0.1, y: 0.1, z: 0.1 },
        directionSpread: { x: 1, y: 1, z: 1 },
        // style: PARTICLE_STYLES.skull,
        // scaleSpread: 0,
      }),
    }

    super(sim, settings, particles)
  }

  puffOfSmoke(sparkles = false) {
    const smokePuff = 50
    for (let i = 0; i < smokePuff; i++) {
      this.emit(this.particles["smokeUp"], "smoke")
    }
    if (sparkles) {
      const sparks = 100
      for (let i = 0; i < sparks; i++) {
        this.emit(this.particles["sparkle"], "magic")
      }
    }
  }

  animatingIn() {
    this.settings.emitRate = 0.0015
  }

  idle() {
    this.settings.particleOrder = ["force", "smoke", "smoke", "smoke", "smoke", "smoke", "smoke"]
    this.settings.emitRate = 0.03
  }
}

class TorchEmitter extends Emitter {
  constructor(position, sim) {
    const settings = {
      emitRate: 0.03,
      particleOrder: ["force", "flame", "redFlame", "smoke", "flame", "redFlame", "smoke", "flame", "flame"],
      startingPosition: position,
      startingDirection: { x: 0, y: 1, z: 0 },
      // direction: { x: -1, y: -1, z: -1 },
    }

    const particles = {
      flame: new FlameParticle({
        positionSpread: { x: 0, y: 0, z: 0 },
        directionSpread: { x: 0.4, y: 0, z: 0.4 },
        direction: { x: 1, y: 1, z: 1 },
        force: 0.1,
        speed: 0.25,
        speedDecay: 0.99,
        lifeDecay: 1.7,
        acceleration: 0.2,
        scale: 2.5,
        scaleSpread: 0.3,
      }),
      redFlame: new FlameParticle({
        color: { r: 1, g: 0.3, b: 0 },
        positionSpread: { x: 0, y: 0, z: 0 },
        directionSpread: { x: 0.4, y: 0, z: 0.4 },
        direction: { x: 1, y: 1, z: 1 },
        force: 0.1,
        speed: 0.3,
        speedDecay: 0.99,
        lifeDecay: 1,
        acceleration: 0.2,
        scale: 2.5,
        scaleSpread: 0.3,
      }),
      smoke: new SmokeParticle({
        positionSpread: { x: 0, y: 0, z: 0 },
        directionSpread: { x: 0.4, y: 0, z: 0.4 },
        direction: { x: 1, y: 1, z: 1 },
        force: 0.1,
        speed: 0.3,
        speedDecay: 0.6,
        lifeDecay: 0.7,
        acceleration: 0.2,
        color: { r: 0.1, g: 0.1, b: 0.1 },
        scale: 4,
        scaleSpread: 0.3,
      }),

      force: new ForceParticle(),
    }

    super(sim, settings, particles)
  }

  flamePuff() {
    gsap.fromTo(this.particles.flame, { scale: 5 }, { scale: 2.5, duration: 1 })
  }

  set green(value) {
    if (value) {
      this.particles.flame.color = { r: 0, g: 1, b: 0 }
      this.particles.redFlame.color = { r: 0.5, g: 1, b: 0.2 }
    } else {
      this.particles.flame.color = { r: 1, g: 1.0, b: 0.3 }
      this.particles.redFlame.color = { r: 1, g: 0.3, b: 0 }
    }
  }
}

class VortexSpellEmitter extends Emitter {
  constructor(sim, light, startPosition) {
    const color = { r: 0, g: 1, b: 0 }

    const settings = {
      // model: ASSETS.getModel("parrot"),
      emitRate: 0.0001,
      animationDelay: 1,

      startingPosition: startPosition,
      lightColor: color,
      particleOrder: ["smoke", "smoke", "smoke", "smoke", "smoke", "circle", "circle"],
    }

    const particles = {
      smoke: new SpellTrailParticle({
        color,
      }),
      sparkle: new SpellTrailParticle({
        style: PARTICLE_STYLES.point,
        scale: 0.1,
      }),
      circle: new SpellTrailParticle({
        color,
        style: PARTICLE_STYLES.disc,
        // scale: 4,
      }),
      explodeSmoke: new ExplodeParticle({
        color,
        // force: 0,
        direction: { x: 0, y: 1, z: 0 },
        directionSpread: { x: 0.05, y: 0.05, z: 0.05 },
      }),
      explodeSpark: new ExplodeParticle({
        speed: 0.1,
        direction: { x: 0, y: 1, z: 0 },
        directionSpread: { x: 0.05, y: 0.05, z: 0.05 },
        color: { r: 1, g: 1, b: 1 },
        // force: 2,

        speedDecay: 0.99,
        lifeDecay: 0.9,
        style: PARTICLE_STYLES.point,
        acceleration: 0.01,
      }),
      explodeShape: new ExplodeParticle({
        color,

        direction: { x: 0, y: 1, z: 0 },
        directionSpread: { x: 0.05, y: 0.05, z: 0.05 },
        style: PARTICLE_STYLES.disc,

        speedDecay: 0.99,
        scale: 0.9,
        speed: 0.1,
        lifeDecay: 0.8,
        acceleration: 0.01,
      }),
    }

    super(sim, settings, particles, light)

    this.active = false

    this.particleOrder = ["smoke"]

    this.lookAt = { x: 0, y: 0, z: 0 }
    this.lookAtTarget = null
    this.scale = 0.1

    SOUNDS.play("spell-travel")

    this.animations.push(
      gsap.to(
        this.position,

        {
          duration: 0.6,
          delay: this.delay,

          motionPath: {
            curviness: 0.5,
            // resolution: 6,
            path: [
              { x: 0.5, y: 1, z: 0.5 },
              { x: 0.5, y: 0.1, z: 0.5 },
            ],
          },
          ease: "linear",
          onComplete: () => this.onComplete(),
          onUpdate: () => this.onUpdate(),
        }
      )
    )
  }

  onComplete = () => {
    const explode = 1000
    for (let i = 0; i < explode; i++) {
      const random = Math.random()
      if (random > 0.55) this.emit(this.particles["explodeSmoke"])
      else if (random > 0.1) this.emit(this.particles["explodeSpark"])
      else this.emit(this.particles["explodeShape"])
    }

    this.destory()
  }

  onUpdate = () => {
    // if (this.lastPosition) {
    //   this.direction = {
    //     x: this.position.x - this.lastPosition.x,
    //     y: this.position.y - this.lastPosition.y,
    //     z: this.position.z - this.lastPosition.z,
    //   }
    // }
    this.active = true
    this.lastPosition = { ...this.position }
  }
}

class WinEmitter extends Emitter {
  constructor(sim, assets) {
    const settings = {
      emitRate: 0.001,
      particleOrder: ["dustRed", "dustGreen", "dustBlue"],
    }

    const particles = {
      dustRed: new DustParticle({ color: { r: 1, g: 1, b: 0 }, style: PARTICLE_STYLES.point, scale: 0.5 }),
      dustGreen: new DustParticle({ color: { r: 0, g: 1, b: 1 }, style: PARTICLE_STYLES.point, scale: 0.5 }),
      dustBlue: new DustParticle({ color: { r: 1, g: 0, b: 1 }, style: PARTICLE_STYLES.point, scale: 0.5 }),
    }

    super(sim, settings, particles)

    // const startCount = 5
    // for (let i = 0; i < startCount; i++) {
    //   this.emit(this.particles["dustGreen"], "magic")
    // }
  }
}

// DEMON

class Enemy {
  constructor(sim, demon, spell) {
    this.machine = interpret(EnemyMachine)
    this.sim = sim
    this.timeOffset = Math.random() * (Math.PI * 2)
    this.state = this.machine.initialState.value

    this.uniforms = demon.uniforms
    this.onDeadCallback = null

    this.animations = []

    const availableSpellTypes = Object.keys(SPELLS).map((key) => SPELLS[key])
    this.spellType = spell ? spell : availableSpellTypes[Math.floor(Math.random() * availableSpellTypes.length)]

    // const geometry = new BoxGeometry(0.1, 0.15, 0.05)
    // const material = new MeshStandardMaterial({ color: this.spellType === SPELLS.arcane ? 0xbb11ff : 0xbbff11 })

    // this.model = new Mesh(geometry, material)

    this.demon = demon
    this.model = demon.demon

    this.elements = {
      leftHand: null,
      rightHand: null,
      sphere: null,
      cloak: null,
      skullParts: [],
    }

    this.model.scene.traverse((item) => {
      if (this.elements[item.name] === null) this.elements[item.name] = item
      else if (item.name.includes("skull")) this.elements.skullParts.push(item)

      if (item.name === "cloak") {
        item.material.onBeforeCompile = (shader) => {
          console.log("COMPILING SHADER")
        }
      }
    })

    this.modelOffset = { x: 0, y: -0.6, z: 0 }

    this.group = this.model.group
    // this.group.add(this.model)

    this.position = { x: 0, y: 0, z: 0 }

    this.emitter = new GhostEmitter(sim)
    this.emitter.emitRate = 0

    this.machine.onTransition((s) => this.onStateChange(s))
    this.machine.start()
  }

  moveFunction(delta, elapsedTime) {
    if (this.state === "ALIVE" || this.state === "TAGGED") {
      this.position.y = 0.2 + 0.15 * ((Math.sin(elapsedTime + this.timeOffset) + 1) * 0.5)
    }
  }

  pause() {
    this.animations.map((animation) => animation.pause())
  }

  resume() {
    this.animations.map((animation) => animation.resume())
  }

  spawn(location) {
    this.location = location
    this.location.add(this.group)
    this.group.rotation.y = this.location.rotation
    this.model.scene.visible = false

    this.machine.send("spawn")
  }

  incoming() {
    this.machine.send("incoming")
  }

  kill() {
    this.machine.send("kill")
  }

  accend() {
    this.machine.send("accend")
  }

  getSuckedIntoTheAbyss() {
    this.machine.send("vortex")
  }

  onStateChange = (state) => {
    this.state = state.value
    if (state.changed || this.state === "IDLE") {
      switch (this.state) {
        case "IDLE":
          this.model.scene.rotation.set(0, 0, 0)
          break
        case "ANIMATING_IN":
          if (this.location) {
            SOUNDS.play("enter")
            const entrancePath = this.location.getRandomEntrance()
            this.animations.push(
              gsap.fromTo(
                this.position,
                { ...entrancePath.points[0] },
                {
                  motionPath: { path: entrancePath.points, curviness: 2 },
                  ease: "none",
                  duration: 1.1,
                  onStart: () => {
                    setTimeout(() => {
                      this.emitter.animatingIn()
                    }, 100)
                  },
                }
              )
            )

            this.animations.push(gsap.from(this.elements.leftHand.position, { z: -0.1, duration: 2 }))
            this.animations.push(gsap.from(this.elements.leftHand.scale, { x: 0, y: 0, z: 0, duration: 2 }))

            this.animations.push(gsap.from(this.elements.rightHand.position, { z: -0.1, duration: 2 }))
            this.animations.push(gsap.from(this.elements.rightHand.scale, { x: 0, y: 0, z: 0, duration: 2 }))

            this.elements.skullParts.forEach((part) => {
              this.animations.push(
                gsap.from(part.rotation, {
                  y: (Math.random() - 0.5) * 0.1,
                  x: 1.5,

                  ease: "power2.inOut",
                  delay: 0.8,
                  duration: 1,
                })
              )
              this.animations.push(gsap.from(part.scale, { x: 0.2, y: 4, z: 0.2, delay: 0.8, duration: 1 }))
              this.animations.push(gsap.from(part.position, { y: 0.1, delay: 0.8, duration: 1 }))
            })

            this.animations.push(gsap.from(this.elements.cloak.scale, { y: 0.2, duration: 1.7 }))

            this.animations.push(gsap.delayedCall(1, this.machine.send, ["complete"]))
            const trail = entrancePath.trail
            const material = trail.material

            entrancePath.entrance.enter()

            this.animations.push(
              gsap.fromTo(
                material.uniforms.progress,
                { value: 0 },
                {
                  duration: 1.1,
                  delay: 0.2,
                  value: 1,
                  ease: "none",
                  onStart: () => {
                    trail.visible = true
                  },
                  onComplete: () => {
                    trail.visible = false
                  },
                }
              )
            )
          } else {
            this.machine.send("complete")
          }
          break
        case "ALIVE":
          SOUNDS.play("laugh")
          this.emitter.puffOfSmoke()
          this.emitter.idle()
          this.model.scene.visible = true
          this.location.energyEmitter.start()
          this.animations.push(
            gsap.fromTo(
              this.model.scene.scale,
              { x: 0.1, y: 0.001, z: 0.1 },
              { x: 0.9, y: 0.9, z: 0.9, ease: "power4.out", duration: 0.2 }
            )
          )
          this.animations.push(gsap.fromTo(this.modelOffset, { y: 0 }, { y: -0.05, ease: "back", duration: 0.5 }))
          break
        case "TAGGED":
          break
        case "ANIMATING_OUT":
          this.emitter.puffOfSmoke()
          this.emitter.destory()
          this.location.energyEmitter.stop()
          this.animations.push(
            gsap.to(this.elements.cloak.scale, {
              x: 12,
              y: 9,
              z: 9,
              ease: "power3.out",
              duration: 1.5,
            })
          )

          this.animations.push(
            gsap.to(this.uniforms.out, {
              value: 1,
              ease: "back.in",
              delay: 0.2,
              duration: 1,
              onComplete: () => {
                this.emitter.puffOfSmoke(true)
                this.machine.send("complete")
              },
            })
          )

          this.elements.skullParts.forEach((part) => {
            const duration = 1 + Math.random() * 0.3
            this.animations.push(
              gsap.to(part.position, {
                delay: 0.15,
                y: (Math.random() - 0.5) * 0.1,
                x: (Math.random() - 0.5) * 0.3,
                z: (Math.random() - 0.5) * 0.3,
                ease: "back.in",
                duration,
              })
            )
            this.animations.push(
              gsap.to(part.rotation, {
                delay: 0,
                y: (Math.random() - 0.5) * 0.8,
                x: (Math.random() - 0.5) * 0.8,
                z: (Math.random() - 0.5) * 0.8,
                ease: "power2.inOut",
                duration,
              })
            )
            this.animations.push(
              gsap.to(part.scale, {
                delay: 0.2,
                y: 0,
                x: 0,
                z: 0,
                ease: "back.in",
                duration: duration * 0.6,
              })
            )

            this.animations.push(
              gsap.to(this.elements.leftHand.scale, {
                x: 0,
                y: 0,
                z: 0,
                ease: "power3.out",
                duration: 0.6,
              })
            )

            this.animations.push(
              gsap.to(this.elements.rightHand.scale, {
                x: 0,
                y: 0,
                z: 0,
                ease: "power3.out",
                duration: 0.6,
              })
            )

            // this.elements.sphere.visible = false
            // this.animations.push(gsap.from(part.scale, { x: 0.2, y: 4, z: 0.2, delay: 0.8, duration: 1 }))
            // this.animations.push(gsap.from(part.position, { y: 0.1, delay: 0.8, duration: 1 }))
          })
          break
        case "VORTEX_ANIMATION":
          this.emitter.destory()
          this.location.energyEmitter.stop()

          const mainDelay = Math.random() * 0.5
          const moveDelay = mainDelay + 1.5

          this.animations.push(
            gsap.to(this.modelOffset, {
              y: -1,
              z: 0.2,
              ease: "power4.in",
              duration: 1,
              delay: moveDelay,
              onComplete: () => this.machine.send("complete"),
            })
          )

          this.animations.push(
            gsap.to(this.model.scene.rotation, {
              y: Math.random() * 2,

              ease: "power4.in",
              duration: 1.5,
              delay: mainDelay + 1,
            })
          )

          this.animations.push(
            gsap.to(this.model.scene.scale, {
              y: 1.2,

              ease: "power4.in",
              duration: 1.5,
              delay: mainDelay + 1,
            })
          )

          this.animations.push(
            gsap.to(this.uniforms.stretch, {
              value: 1,
              ease: "power4.in",
              delay: mainDelay,
              duration: 2,
            })
          )

          break
        case "DEAD":
          this.destory()
          break
        case "GONE":
          this.emitter.destory()
          this.destory()
          break
        case "ACCEND":
          this.location.energyEmitter.stop()
          this.animations.push(
            gsap.to(this.position, {
              y: 1.1,
              ease: "Power4.in",
              duration: 0.6,
              delay: Math.random(),
              onStart: () => this.emitter.puffOfSmoke(),
              onComplete: () => {
                this.destory()
                this.machine.send("leave")
              },
            })
          )
      }
    }
  }

  resetDemon() {
    console.log("----reseting demon")
    console.log(this.uniforms)
    this.uniforms.in.value = 0
    this.uniforms.out.value = 0
    this.uniforms.stretch.value = 0
    this.model.scene.traverse((item) => {
      if (item.isMesh) {
        const types = ["position", "rotation", "scale"]
        types.forEach((type) => {
          item[type].set(item.home[type].x, item.home[type].y, item.home[type].z)
        })
      }
    })
  }

  destory() {
    if (this.model) {
      this.group.removeFromParent()
      this.resetDemon()
      this.demon.returnToPool()
      // this.model.scene.parent.remove(this.model.scene)
      // this.model = null
    }

    this.animations.forEach((animation) => {
      animation.kill()
      animation = null
    })

    if (this.location) this.location.release()

    if (this.onDeadCallback) {
      this.onDeadCallback()
      this.onDeadCallback = null
    }
  }

  tick(delta, elapsedTime) {
    this.uniforms.time.value = elapsedTime
    this.moveFunction(delta, elapsedTime)

    this.group.position.set(
      this.position.x * this.sim.size.x,
      this.position.y * this.sim.size.y,
      this.position.z * this.sim.size.z
    )

    this.model.scene.position.set(
      this.modelOffset.x * this.sim.size.x,
      this.modelOffset.y * this.sim.size.y,
      this.modelOffset.z * this.sim.size.z
    )

    if (this.location)
      this.emitter.position = {
        x: this.position.x + this.location.position.x,
        y: this.position.y + this.location.position.y,
        z: this.position.z + this.location.position.z,
      }
  }

  get dead() {
    return this.state === "DEAD" || this.state === "GONE"
  }

  get active() {
    return this.state === "ALIVE"
  }
}

/* 
  The demon needs a little moment to get loaded
	into memory. So rather than wait for the first
	in game enemy to appear and get hit with a 
	stutter, we use this preloader to do some 
	heavy lifting during the loading screen
*/

class EnemyPreloader {
  constructor(stage) {
    this.totalDemons = 6
    this.demons = []

    for (let i = 0; i < this.totalDemons; i++) {
      this.demons.push({
        isAvailable: true,
        returnToPool: function () {
          this.isAvailable = true
        },
        uniforms: {
          in: { value: 0 },
          out: { value: 0 },
          stretch: { value: 0 },
          time: { value: 1 },
        },
        demon: ASSETS.getModel("demon", true),
      })
    }

    this.demons.forEach((enemy, i) => {
      enemy.demon.group.position.y = -0.1
      enemy.demon.group.position.x = 0.05 + 0.02 * (i + 1)
      stage.add(enemy.demon.group)
      enemy.demon.scene.traverse((item) => {
        if (item.name === "cloak") {
          // item.castShadow = true

          // item.material.transparent = true
          // item.material.forceSinglePass = true
          // item.renderOrder = 0
          // item.material.writeDepth = false

          item.material.onBeforeCompile = (shader) => {
            // const uniform = { value: 1 }

            shader.uniforms.uIn = enemy.uniforms.in
            shader.uniforms.uOut = enemy.uniforms.out
            shader.uniforms.uStretch = enemy.uniforms.stretch
            shader.uniforms.uTime = enemy.uniforms.time

            shader.vertexShader = shader.vertexShader.replace(
              "#define STANDARD",
              `#define STANDARD
							
							${includes.noise}
							uniform float uOut;
							uniform float uTime;
							uniform float uStretch;
							varying vec2 vUv;
							varying float vNoise;
							`
            )

            shader.vertexShader = shader.vertexShader.replace(
              "#include <begin_vertex>",
              `
									#include <begin_vertex>
					
									vUv = uv;
									float xNoise = snoise(vec2((position.x * 200.0) + (position.z * 100.0), uTime * (0.6 + (0.3 * uOut))));
									float yNoise = snoise(vec2((position.y * 200.0) + (position.z * 100.0), uTime * (0.6 + (0.3 * uOut))));
									float amount = (0.0015 + 0.02 * uOut) ;

                  float moveAmount = smoothstep(0.02 + (1.0 * uOut), 0.0, position.y);

									transformed.x += moveAmount * amount * xNoise;
									transformed.y += moveAmount * amount * yNoise;

									transformed.x = transformed.x * (1.0 - uOut);
									transformed.y = transformed.y * (1.0 - uOut)+ (0.0 * uOut);
									transformed.z = transformed.z * (1.0 - uOut);

                  transformed.y -= (moveAmount * uStretch) * 0.01;
                  transformed.x += (moveAmount * uStretch) * 0.003;
									
									vNoise = snoise(vec2(position.x * 500.0, position.y * 500.0 ));
							`
            )

            shader.fragmentShader = shader.fragmentShader.replace(
              "#include <common>",
              `
            		uniform float uIn;
            		uniform float uOut;
            		uniform float uTime;
            		varying vec2 vUv;
            		varying float vNoise;

								${includes.noise}

            		#include <common>
            `
            )
            shader.fragmentShader = shader.fragmentShader.replace(
              "#include <output_fragment>",
              `#include <output_fragment>

              // float noise = snoise(vUv);

              // vec3 blackout = mix(vec3(vUv, 1.0), gl_FragColor.rgb, uOut);
							float noise = snoise(vUv * 80.0);

							float glowNoise = snoise((vUv * 4.0) + (uTime * 0.75));
							float glow = smoothstep(0.3, 0.5, glowNoise);
							glow *= smoothstep(0.7, 0.5, glowNoise);
							// glowNoise = smoothstep(0.7, 0.5, glowNoise);

							float grad =  smoothstep(0.925 + (uOut * 0.2), 1.0, vUv.y) * noise;
							

              // gl_FragColor = vec4(vec3(grad, 0.0, 0.0), 1.0 - grad);
              // gl_FragColor.a = 1.0 - grad;
              gl_FragColor.rgb *= 1.0 - grad;
              gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(1.0), glow * 0.2 * pow(uOut, 0.25) )  ;
							
            `
            )

            enemy.demon.group.removeFromParent()
          }
        }
      })
    })
  }

  resetAll() {
    this.demons.forEach((d) => (d.isAvailable = true))
  }

  borrowDemon() {
    const availableDemons = this.demons.filter((d) => d.isAvailable)

    const demon = availableDemons[0]
    demon.isAvailable = false
    return demon
  }
}

// LIGHTS

class CrystalLight {
  constructor(position, offset) {
    const color = new Color("#861388")
    this.position = position
    this.offset = offset
    this.group = new Group()
    this.pointLight = new PointLight(color, 5, 0.8)

    this.group.add(this.pointLight)
    this.group.position.set(position.x, position.y, position.z)

    if (window.DEBUG.lights) {
      const helper = new Mesh(new SphereGeometry(0.02), new MeshBasicMaterial(0xffffff))
      this.group.add(helper)
    }
  }

  get light() {
    return this.group
  }

  tick(delta, elapsedTime) {
    // this.group.position.set(
    //   this.position.x, // * this.offset.x,
    //   this.position.y, // * this.offset.y,
    //   this.position.z // * this.offset.z
    // )
    // const n = (Math.cos(elapsedTime * 1.8) + 1) * 0.5
    // this.pointLight.intensity = 8 + 6 * n
  }
}

class TorchLight {
  constructor(position, offset, noise) {
    const color = new Color("#FA9638")
    this.position = position
    this.offset = offset
    this.group = new Group()
    this.pointLight = new PointLight(color, 0, 0.6)
    this.group.add(this.pointLight)
    this.group.position.set(
      this.position.x * this.offset.x,
      this.position.y * this.offset.y,
      this.position.z * this.offset.z
    )
    this.noise = noise
    this._active = false
    this.baseIntesity = 1

    if (window.DEBUG.lights) {
      const helper = new Mesh(new SphereGeometry(0.02), new MeshBasicMaterial({ color: 0xff0000 }))
      this.group.add(helper)
    }
  }

  get light() {
    return this.group
  }

  get object() {
    return this.group
  }

  set active(value) {
    if (value !== this._active) {
      this._active = value
      if (this._active) {
        gsap.fromTo(this, { baseIntesity: 3 }, { baseIntesity: 1, duration: 0.3 })
      }
    }
  }

  set color(newColor) {
    this.pointLight.color = new Color(newColor)
  }

  tick(delta, elapsedTime) {
    const n = this.noise(this.position.x * 2, this.position.y * 2, elapsedTime * 3) + 1 * 0.5
    this.pointLight.intensity = this._active ? this.baseIntesity + 0.5 * n : 0
  }
}

// PARTICLES

class ParticleType {
  constructor(settings) {
    this.settings = { ...DEFAULT_PARTICLE_SETTINGS, ...settings }
  }

  get speed() {
    return this.settings.speed
  }

  get speedDecay() {
    return this.settings.speedDecay
  }

  get speedSpread() {
    return this.settings.speedSpread
  }

  get force() {
    return this.settings.force
  }

  get forceDecay() {
    return this.settings.forceDecay
  }

  get forceSpread() {
    return this.settings.forceSpread
  }

  get life() {
    return this.settings.life
  }

  get lifeDecay() {
    return this.settings.lifeDecay
  }

  get directionSpread() {
    return this.settings.directionSpread
  }

  get direction() {
    return this.settings.direction
  }

  get position() {
    return this.settings.position
  }

  get positionSpread() {
    return this.settings.positionSpread
  }

  get color() {
    return this.settings.color
  }

  set color(value) {
    this.settings.color = value
  }

  get scale() {
    return this.settings.scale
  }

  set scale(value) {
    this.settings.scale = value
  }

  get scaleSpread() {
    return this.settings.scaleSpread
  }

  get style() {
    return this.settings.style
  }

  get acceleration() {
    return this.settings.acceleration
  }
}

class DustParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0,
      speedDecay: 0.4,
      color: { r: 0.5, g: 0.5, b: 0.5 },
      speedSpread: 0,
      forceSpread: 0,
      force: 0,
      style: PARTICLE_STYLES.circle,
      life: 1,
      lifeDecay: 0.3,
      scale: 0.06,
      acceleration: 1,
      positionSpread: { x: 0.5, y: 0.5, z: 0.5 },
      ..._overides,
    })
  }
}

class ExplodeParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0.4,
      speedSpread: 0,
      speedDecay: 0.8,
      forceSpread: 0,
      force: 2,
      forceDecay: 0.9,
      type: PARTICLE_STYLES.smoke,
      ..._overides,
    })
  }
}

class FlameParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0.5,
      speedDecay: 0.4,
      color: { r: 1, g: 1.0, b: 0.3 },
      speedSpread: 0.1,
      forceSpread: 0.2,
      force: 0.8,
      forceDecay: 0.8,
      scale: 1,
      scaleSpread: 1,
      lifeDecay: 1.5,
      direction: { x: 1, y: 1, z: 1 },
      directionSpread: { x: 0.1, y: 0.1, z: 0.1 },
      positionSpread: { x: 0, y: 0, z: 0 },
      acceleration: 0.02,
      style: PARTICLE_STYLES.flame,
      ..._overides,
    })
  }
}

class ForceParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0.4,
      speedDecay: 0.4,
      color: { r: 1, g: 0, b: 0 },
      force: 1,
      forceDecay: 0,
      direction: { x: 1, y: 1, z: 1 },
      directionSpread: { x: 0.3, y: 0, z: 0.3 },
      acceleration: 0,
      scale: 0.3,
      style: window.DEBUG.forceParticles ? PARTICLE_STYLES.circle : PARTICLE_STYLES.invisible,
      ..._overides,
    })
  }
}

class SmokeParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0,
      speedDecay: 0,
      speedSpread: 0,
      forceSpread: 0,
      force: 0,
      life: 1,
      lifeDecay: 0,
      scaleSpread: 1,
      acceleration: 0,
      positionSpread: { x: 0.02, y: 0, z: 0.001 },
      color: { r: 0.75, g: 0.75, b: 0.75 },
      style: PARTICLE_STYLES.smoke,
      scale: 1,
      ..._overides,
    })
  }
}

class SparkleParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}
    super({
      speed: 0.1,
      speedSpread: 0,
      forceSpread: 0,
      force: 0,
      life: 0.5,
      lifeDecay: 0,
      scaleSpread: 1,
      acceleration: 0,
      color: { r: 1, g: 1, b: 1 },
      style: PARTICLE_STYLES.point,
      scale: 1.2,
      speedDecay: 0.2,
      positionSpread: { x: 0.01, y: 0.001, z: 0.01 },
      ..._overides,
    })
  }
}

class SpellTrailParticle extends ParticleType {
  constructor(overrides) {
    const _overides = overrides ? overrides : {}

    super({
      speed: 0.5,
      speedDecay: 0.4,
      color: { r: 1, g: 1, b: 1 },
      speedSpread: 0.1,
      forceSpread: 0.2,
      force: 0.8,
      forceDecay: 0.8,
      scale: .........完整代码请登录后点击上方下载按钮下载查看

网友评论0