diff --git a/Changelog.md b/Changelog.md index 6c51ff4..3f8a18a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,18 +1,20 @@ # Changelog -## 1.0.3 (current dev) +## 1.0.3 (4/15/2022) ### UI Enhancements: -- Improved input box styling +- Improved styling ### Simulation Enhancements: -- +- Added ability to save/load organisms +- Added ability to save/load worlds ### Bug Fixes: - charste changed to charset +- Fixed species tracking -Thanks to contributors: +Thanks to contributors: @TerraMaster85 ## 1.0.2 (12/21/2021) diff --git a/dist/index.html b/dist/index.html index d36ce90..e4465e6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,7 +6,6 @@ - @@ -21,7 +20,7 @@
- +
@@ -78,6 +77,9 @@ + + +
@@ -187,6 +189,15 @@
+
+ + + .json +
+ + + +
@@ -238,7 +249,7 @@ - +
@@ -268,7 +279,7 @@
- + diff --git a/package-lock.json b/package-lock.json index d276bd2..7994f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -198,9 +198,9 @@ "dev": true }, "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -2048,9 +2048,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mississippi": { diff --git a/src/Controllers/ControlPanel.js b/src/Controllers/ControlPanel.js index f57b94b..46cda00 100644 --- a/src/Controllers/ControlPanel.js +++ b/src/Controllers/ControlPanel.js @@ -1,7 +1,6 @@ const Hyperparams = require("../Hyperparameters"); const Modes = require("./ControlModes"); const StatsPanel = require("../Stats/StatsPanel"); -const RandomOrganismGenerator = require("../Organism/RandomOrganismGenerator") const WorldConfig = require("../WorldConfig"); class ControlPanel { @@ -48,6 +47,8 @@ class ControlPanel { defineHotkeys() { $('body').keydown( (e) => { + let focused = document.activeElement; + if (focused.tagName === "INPUT" && focused.type === "text") return; switch (e.key.toLowerCase()) { // hot bar controls case 'a': @@ -206,6 +207,43 @@ class ControlPanel { this.env_controller.add_new_species = true; this.env_controller.dropOrganism(org, center[0], center[1]) }); + $('#save-env').click( () => { + let was_running = this.engine.running; + this.setPaused(true); + let env = this.engine.env.serialize(); + let data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(env)); + let downloadEl = document.getElementById('download-el'); + downloadEl.setAttribute("href", data); + downloadEl.setAttribute("download", $('#save-env-name').val()+".json"); + downloadEl.click(); + if (was_running) + this.setPaused(false); + }); + $('#load-env').click(() => { + $('#upload-env').click(); + }); + $('#upload-env').change((e)=>{ + let files = e.target.files; + if (!files.length) {return;}; + let reader = new FileReader(); + reader.onload = (e) => { + try { + let was_running = this.engine.running; + this.setPaused(true); + let env = JSON.parse(e.target.result); + this.engine.env.loadRaw(env); + if (was_running) + this.setPaused(false); + this.updateHyperparamUIValues(); + this.env_controller.resetView(); + } catch(except) { + console.error(except) + alert('Failed to load world'); + } + $('#upload-env')[0].value = ''; + }; + reader.readAsText(files[0]); + }); } defineHyperparameterControls() { @@ -279,9 +317,9 @@ class ControlPanel { downloadEl.click(); }); $('#load-controls').click(() => { - $('#upload-el').click(); + $('#upload-hyperparams').click(); }); - $('#upload-el').change((e)=>{ + $('#upload-hyperparams').change((e)=>{ let files = e.target.files; if (!files.length) {return;}; let reader = new FileReader(); @@ -290,7 +328,7 @@ class ControlPanel { Hyperparams.loadJsonObj(result); this.updateHyperparamUIValues(); // have to clear the value so change() will be triggered if the same file is uploaded again - $('#upload-el')[0].value = ''; + $('#upload-hyperparams')[0].value = ''; }; reader.readAsText(files[0]); }); @@ -428,7 +466,6 @@ class ControlPanel { this.env_controller.org_to_clone = this.engine.organism_editor.getCopyOfOrg(); this.env_controller.add_new_species = this.editor_controller.new_species; this.editor_controller.new_species = false; - // console.log(this.env_controller.add_new_species) } } diff --git a/src/Controllers/EditorController.js b/src/Controllers/EditorController.js index d5c2cb7..38cec42 100644 --- a/src/Controllers/EditorController.js +++ b/src/Controllers/EditorController.js @@ -13,6 +13,7 @@ class EditorController extends CanvasController{ this.new_species = false; this.defineCellTypeSelection(); this.defineEditorDetails(); + this.defineSaveLoad(); } mouseMove() { @@ -109,6 +110,43 @@ class EditorController extends CanvasController{ }.bind(this)); } + defineSaveLoad() { + $('#save-org').click(()=>{ + let org = this.env.organism.serialize(); + let data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(org)); + let downloadEl = document.getElementById('download-el'); + downloadEl.setAttribute("href", data); + downloadEl.setAttribute("download", "organism.json"); + downloadEl.click(); + }); + $('#load-org').click(() => { + $('#upload-org').click(); + }); + $('#upload-org').change((e)=>{ + let files = e.target.files; + if (!files.length) {return;}; + let reader = new FileReader(); + reader.onload = (e) => { + try { + let org=JSON.parse(e.target.result); + this.env.clear(); + this.env.organism.loadRaw(org); + this.refreshDetailsPanel(); + this.env.organism.updateGrid(); + this.env.renderFull(); + if (this.mode === Modes.Clone) + $('#drop-org').click(); + // have to clear the value so change() will be triggered if the same file is uploaded again + $('#upload-org')[0].value = ''; + } catch(except) { + console.error(except) + alert('Failed to load organism'); + } + }; + reader.readAsText(files[0]); + }); + } + clearDetailsPanel() { $('#organism-details').css('display', 'none'); $('#edit-organism-details').css('display', 'none'); diff --git a/src/Environments/WorldEnvironment.js b/src/Environments/WorldEnvironment.js index 6f47b27..5e89fef 100644 --- a/src/Environments/WorldEnvironment.js +++ b/src/Environments/WorldEnvironment.js @@ -7,6 +7,8 @@ const EnvironmentController = require('../Controllers/EnvironmentController'); const Hyperparams = require('../Hyperparameters.js'); const FossilRecord = require('../Stats/FossilRecord'); const WorldConfig = require('../WorldConfig'); +const SerializeHelper = require('../Utils/SerializeHelper'); +const Species = require('../Stats/Species'); class WorldEnvironment extends Environment{ constructor(cell_size) { @@ -164,6 +166,57 @@ class WorldEnvironment extends Environment{ this.num_rows = Math.ceil(this.renderer.height / cell_size); this.grid_map.resize(this.num_cols, this.num_rows, cell_size); } + + serialize() { + let env = SerializeHelper.copyNonObjects(this); + env.grid = this.grid_map.serialize(); + env.organisms = []; + for (let org of this.organisms){ + env.organisms.push(org.serialize()); + } + env.fossil_record = FossilRecord.serialize(); + env.controls = Hyperparams; + return env; + } + + loadRaw(env) { // species name->stats map, evolution controls, + this.organisms = []; + FossilRecord.clear_record(); + this.resizeGridColRow(this.grid_map.cell_size, env.grid.cols, env.grid.rows) + this.grid_map.loadRaw(env.grid); + + // create species map + let species = {}; + for (let name in env.fossil_record.species) { + let s = new Species(null, null, 0); + SerializeHelper.overwriteNonObjects(env.fossil_record.species[name], s) + species[name] = s; // the species needs an anatomy obj still + } + + for (let orgRaw of env.organisms) { + let org = new Organism(orgRaw.col, orgRaw.row, this); + org.loadRaw(orgRaw); + this.addOrganism(org); + let s = species[orgRaw.species_name]; + if (!s){ // ideally, every organisms species should exists, but there is a bug somewhere + s = new Species(org.anatomy, null, env.total_ticks); + species[orgRaw.species_name] = s; + } + if (!s.anatomy) { + //if the species doesn't have anatomy we need to initialize it + s.anatomy = org.anatomy; + s.calcAnatomyDetails(); + } + org.species = s; + } + for (let name in species) + FossilRecord.addSpeciesObj(species[name]); + FossilRecord.loadRaw(env.fossil_record); + SerializeHelper.overwriteNonObjects(env, this); + if ($('#override-controls').is(':checked')) + Hyperparams.loadJsonObj(env.controls) + this.renderer.renderFullGrid(this.grid_map.grid); + } } module.exports = WorldEnvironment; diff --git a/src/Grid/GridMap.js b/src/Grid/GridMap.js index 84db4ca..6c69773 100644 --- a/src/Grid/GridMap.js +++ b/src/Grid/GridMap.js @@ -78,6 +78,34 @@ class GridMap { r = 0; return [c, r]; } + + serialize() { + // Rather than store every single cell, we will store non organism cells (food+walls) + // and assume everything else is empty. Organism cells will be set when the organism + // list is loaded. This reduces filesize and complexity. + let grid = {cols:this.cols, rows:this.rows}; + grid.food = []; + grid.walls = []; + for (let col of this.grid) { + for (let cell of col) { + if (cell.state===CellStates.wall || cell.state===CellStates.food){ + let c = {c: cell.col, r: cell.row}; // no need to store state + if (cell.state===CellStates.food) + grid.food.push(c) + else + grid.walls.push(c) + } + } + } + return grid; + } + + loadRaw(grid) { + for (let f of grid.food) + this.setCellType(f.c, f.r, CellStates.food); + for (let w of grid.walls) + this.setCellType(w.c, w.r, CellStates.wall); + } } module.exports = GridMap; diff --git a/src/Organism/Anatomy.js b/src/Organism/Anatomy.js index b1c3cb6..ec3173d 100644 --- a/src/Organism/Anatomy.js +++ b/src/Organism/Anatomy.js @@ -1,14 +1,19 @@ const CellStates = require("./Cell/CellStates"); const BodyCellFactory = require("./Cell/BodyCells/BodyCellFactory"); +const SerializeHelper = require("../Utils/SerializeHelper"); class Anatomy { constructor(owner) { this.owner = owner; + this.birth_distance = 4; + this.clear(); + } + + clear() { this.cells = []; this.is_producer = false; this.is_mover = false; this.has_eyes = false; - this.birth_distance = 4; } canAddCellAt(c, r) { @@ -61,7 +66,7 @@ class Anatomy { break; } } - this.checkTypeChange(cell.state); + this.checkTypeChange(); return true; } @@ -93,9 +98,7 @@ class Anatomy { } getNeighborsOfCell(col, row) { - var neighbors = []; - for (var x = -1; x <= 1; x++) { for (var y = -1; y <= 1; y++) { @@ -107,6 +110,37 @@ class Anatomy { return neighbors; } + + isEqual(anatomy) { // currently unused helper func. inefficient, avoid usage in prod. + if (this.cells.length !== anatomy.cells.length) return false; + for (let i in this.cells) { + let my_cell = this.cells[i]; + let their_cell = anatomy.cells[i]; + if (my_cell.loc_col !== their_cell.loc_col || + my_cell.loc_row !== their_cell.loc_row || + my_cell.state !== their_cell.state) + return false; + } + return true; + } + + serialize() { + let anatomy = SerializeHelper.copyNonObjects(this); + anatomy.cells = []; + for (let cell of this.cells) { + let newcell = SerializeHelper.copyNonObjects(cell); + newcell.state = {name: cell.state.name}; + anatomy.cells.push(newcell) + } + return anatomy; + } + + loadRaw(anatomy) { + this.clear(); + for (let cell of anatomy.cells){ + this.addInheritCell(cell); + } + } } module.exports = Anatomy; \ No newline at end of file diff --git a/src/Organism/Cell/BodyCells/KillerCell.js b/src/Organism/Cell/BodyCells/KillerCell.js index ca82218..c7dcd92 100644 --- a/src/Organism/Cell/BodyCells/KillerCell.js +++ b/src/Organism/Cell/BodyCells/KillerCell.js @@ -18,7 +18,6 @@ class KillerCell extends BodyCell{ } killNeighbor(n_cell) { - // console.log(n_cell) if(n_cell == null || n_cell.owner == null || n_cell.owner == this.org || !n_cell.owner.living || n_cell.state == CellStates.armor) return; var is_hit = n_cell.state == CellStates.killer; // has to be calculated before death diff --git a/src/Organism/Organism.js b/src/Organism/Organism.js index 2d00ee6..80f8404 100644 --- a/src/Organism/Organism.js +++ b/src/Organism/Organism.js @@ -5,6 +5,7 @@ const Directions = require("./Directions"); const Anatomy = require("./Anatomy"); const Brain = require("./Perception/Brain"); const FossilRecord = require("../Stats/FossilRecord"); +const SerializeHelper = require("../Utils/SerializeHelper"); class Organism { constructor(col, row, env, parent=null) { @@ -33,15 +34,12 @@ class Organism { this.move_range = parent.move_range; this.mutability = parent.mutability; this.species = parent.species; - // this.birth_distance = parent.birth_distance; for (var c of parent.anatomy.cells){ //deep copy parent cells this.anatomy.addInheritCell(c); } - if(parent.anatomy.is_mover) { - for (var i in parent.brain.decisions) { - this.brain.decisions[i] = parent.brain.decisions[i]; - } + if(parent.anatomy.is_mover && parent.anatomy.has_eyes) { + this.brain.copy(parent.brain); } } @@ -104,7 +102,6 @@ class Organism { var new_c = this.c + (direction_c*basemovement) + (direction_c*offset); var new_r = this.r + (direction_r*basemovement) + (direction_r*offset); - // console.log(org.isClear(new_c, new_r, org.rotation, true)) if (org.isClear(new_c, new_r, org.rotation, true) && org.isStraightPath(new_c, new_r, this.c, this.r, this)){ org.c = new_c; org.r = new_r; @@ -118,11 +115,12 @@ class Organism { } } Math.max(this.food_collected -= this.foodNeeded(), 0); - } mutate() { - let mutated = false; + let added = false; + let changed = false; + let removed = false; if (this.calcRandomChance(Hyperparams.addProb)) { let branch = this.anatomy.getRandomCell(); let state = CellStates.getRandomLivingType();//branch.state; @@ -130,7 +128,7 @@ class Organism { let c = branch.loc_col+growth_direction[0]; let r = branch.loc_row+growth_direction[1]; if (this.anatomy.canAddCellAt(c, r)){ - mutated = true; + added = true; this.anatomy.addRandomizedCell(state, c, r); } } @@ -138,15 +136,15 @@ class Organism { let cell = this.anatomy.getRandomCell(); let state = CellStates.getRandomLivingType(); this.anatomy.replaceCell(state, cell.loc_col, cell.loc_row); - mutated = true; + changed = true; } if (this.calcRandomChance(Hyperparams.removeProb)){ if(this.anatomy.cells.length > 1) { let cell = this.anatomy.getRandomCell(); - mutated = this.anatomy.removeCell(cell.loc_col, cell.loc_row); + removed = this.anatomy.removeCell(cell.loc_col, cell.loc_row); } } - return mutated; + return added || changed || removed; } calcRandomChance(prob) { @@ -319,6 +317,22 @@ class Organism { return this.env.grid_map.cellAt(real_c, real_r); } + serialize() { + let org = SerializeHelper.copyNonObjects(this); + org.anatomy = this.anatomy.serialize(); + if (this.anatomy.is_mover && this.anatomy.has_eyes) + org.brain = this.brain.serialize(); + org.species_name = this.species.name; + return org; + } + + loadRaw(org) { + SerializeHelper.overwriteNonObjects(org, this); + this.anatomy.loadRaw(org.anatomy) + if (org.brain) + this.brain.copy(org.brain) + } + } module.exports = Organism; diff --git a/src/Organism/Perception/Brain.js b/src/Organism/Perception/Brain.js index 20c6258..4c60b03 100644 --- a/src/Organism/Perception/Brain.js +++ b/src/Organism/Perception/Brain.js @@ -20,16 +20,18 @@ class Brain { this.observations = []; // corresponds to CellTypes - this.decisions = []; - this.decisions[CellStates.empty.name] = Decision.neutral; + this.decisions = {}; + for (let cell of CellStates.all) { + this.decisions[cell.name] = Decision.neutral; + } this.decisions[CellStates.food.name] = Decision.chase; - this.decisions[CellStates.wall.name] = Decision.neutral; - this.decisions[CellStates.mouth.name] = Decision.neutral; - this.decisions[CellStates.producer.name] = Decision.neutral; - this.decisions[CellStates.mover.name] = Decision.neutral; this.decisions[CellStates.killer.name] = Decision.retreat; - this.decisions[CellStates.armor.name] = Decision.neutral; - this.decisions[CellStates.eye.name] = Decision.neutral; + } + + copy(brain) { + for (let dec in brain.decisions) { + this.decisions[dec] = brain.decisions[dec]; + } } randomizeDecisions(randomize_all=false) { @@ -58,9 +60,7 @@ class Brain { continue; } if (obs.distance < closest) { - // console.log(obs.cell.state) decision = this.decisions[obs.cell.state.name]; - // console.log(decision) move_direction = obs.direction; closest = obs.distance; } @@ -81,6 +81,10 @@ class Brain { this.decisions[CellStates.getRandomName()] = Decision.getRandom(); this.decisions[CellStates.empty.name] = Decision.neutral; // if the empty cell has a decision it gets weird } + + serialize() { + return {decisions: this.decisions}; + } } Brain.Decision = Decision; diff --git a/src/Stats/FossilRecord.js b/src/Stats/FossilRecord.js index 3189f7a..17ec347 100644 --- a/src/Stats/FossilRecord.js +++ b/src/Stats/FossilRecord.js @@ -1,4 +1,5 @@ const CellStates = require("../Organism/Cell/CellStates"); +const SerializeHelper = require("../Utils/SerializeHelper"); const Species = require("./Species"); const FossilRecord = { @@ -18,7 +19,6 @@ const FossilRecord = { }, addSpecies: function(org, ancestor) { - // console.log("Adding Species") var new_species = new Species(org.anatomy, ancestor, this.env.total_ticks); this.extant_species.push(new_species); org.species = new_species; @@ -26,33 +26,30 @@ const FossilRecord = { }, addSpeciesObj: function(species) { - // console.log("Adding Species") this.extant_species.push(species); return species; }, fossilize: function(species) { - // console.log("Extinction") species.end_tick = this.env.total_ticks; for (i in this.extant_species) { var s = this.extant_species[i]; if (s == species) { this.extant_species.splice(i, 1); + species.ancestor = undefined; // garbage collect dead species + // if (species.ancestor) + // species.ancestor.ancestor = undefined; if (species.cumulative_pop < this.min_pop) { return false; } // disabled for now, causes memory problems on long runs // this.extinct_species.push(s); - - // console.log("Extant species:", this.extant_species.length) - // console.log("Extinct species:", this.extinct_species.length) return true; } } }, resurrect: function(species) { - // console.log("Resurrecting species") if (species.extinct) { for (i in this.extinct_species) { var s = this.extinct_species[i]; @@ -67,12 +64,13 @@ const FossilRecord = { setData() { // all parallel arrays - this.tick_record = [0]; - this.pop_counts = [0]; - this.species_counts = [0]; - this.av_mut_rates = [0]; - this.av_cells = [0]; - this.av_cell_counts = [this.calcCellCountAverages()]; + this.tick_record = []; + this.pop_counts = []; + this.species_counts = []; + this.av_mut_rates = []; + this.av_cells = []; + this.av_cell_counts = []; + this.updateData(); }, updateData() { @@ -121,12 +119,39 @@ const FossilRecord = { this.av_cell_counts.push(cell_counts); }, - clear_record: function() { + clear_record() { this.extant_species = []; this.extinct_species = []; this.setData(); }, + serialize() { + this.updateData(); + let record = SerializeHelper.copyNonObjects(this); + record.records = { + tick_record:this.tick_record, + pop_counts:this.pop_counts, + species_counts:this.species_counts, + av_mut_rates:this.av_mut_rates, + av_cells:this.av_cells, + av_cell_counts:this.av_cell_counts, + }; + let species = {}; + for (let s of this.extant_species) { + species[s.name] = SerializeHelper.copyNonObjects(s); + delete species[s.name].name; // the name will be used as the key, so remove it from the value + } + record.species = species; + return record; + }, + + loadRaw(record) { + SerializeHelper.overwriteNonObjects(record, this); + for (let key in record.records) { + this[key] = record.records[key]; + } + } + } FossilRecord.init(); diff --git a/src/Stats/Species.js b/src/Stats/Species.js index f251844..6f5b5f9 100644 --- a/src/Stats/Species.js +++ b/src/Stats/Species.js @@ -1,19 +1,26 @@ const CellStates = require("../Organism/Cell/CellStates"); +let FossilRecord = undefined; // workaround to a circular dependency problem +const getFossilRecord = () => { + if (!FossilRecord) + FossilRecord = require("./FossilRecord"); + return FossilRecord; +} class Species { constructor(anatomy, ancestor, start_tick) { this.anatomy = anatomy; - // this.ancestor = ancestor; // garbage collect ancestors to avoid memory problems + this.ancestor = ancestor; // eventually need to garbage collect ancestors to avoid memory problems this.population = 1; this.cumulative_pop = 1; this.start_tick = start_tick; this.end_tick = -1; - this.name = '_' + Math.random().toString(36).substr(2, 9); + this.name = Math.random().toString(36).substr(2, 10); this.extinct = false; this.calcAnatomyDetails(); } calcAnatomyDetails() { + if (!this.anatomy) return; var cell_counts = {}; for (let c of CellStates.living) { cell_counts[c.name] = 0; @@ -33,8 +40,7 @@ class Species { this.population--; if (this.population <= 0) { this.extinct = true; - const FossilRecord = require("./FossilRecord"); - FossilRecord.fossilize(this); + getFossilRecord().fossilize(this); } } diff --git a/src/Utils/SerializeHelper.js b/src/Utils/SerializeHelper.js new file mode 100644 index 0000000..62930a9 --- /dev/null +++ b/src/Utils/SerializeHelper.js @@ -0,0 +1,20 @@ +const SerializeHelper = { + copyNonObjects(obj) { + let newobj = {}; + for (let key in obj) { + if (typeof obj[key] !== 'object') + newobj[key] = obj[key]; + } + return newobj; + }, + overwriteNonObjects(copyFrom, copyTo) { + for (let key in copyFrom) { + if (typeof copyFrom[key] !== 'object' && typeof copyTo[key] !== 'object') { + // only overwrite if neither are objects + copyTo[key] = copyFrom[key]; + } + } + } +} + +module.exports = SerializeHelper; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 62523fd..c0243c9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const webpack = require("webpack"); module.exports = { entry: './src/index.js', @@ -6,7 +7,9 @@ module.exports = { filename: 'bundle.js', path: path.resolve(__dirname, 'dist/js/'), }, - externals: { - jquery: 'jQuery' - } + plugins: [ + new webpack.ProvidePlugin({ + $: "jquery", + }) + ] }; \ No newline at end of file